简介:本文从代码实现角度对比Agent技能(Skill)与函数调用(Function Calling)的异同。读者将了解两者在职责边界、上下文组织、宿主实现和选型场景上的核心区别,掌握如何在不增加业务函数的前提下为现有工具循环引入技能机制,并明确何时该用函数调用、何时该用技能包。
在构建智能体(Agent)系统时,开发者经常会遇到两个容易混淆的概念:函数调用(Function Calling) 和技能(Skill)。前者是模型与外部工具交互的经典机制,后者是近年来在智能体架构中逐渐流行的一种能力封装方式。两者在代码层面究竟如何实现?它们各自的职责边界在哪里?什么时候应该用哪一个?
本文将从代码实现的角度,对这两个概念进行系统对比。这里讨论的“技能”特指带有 SKILL.md 描述文件的Agent Skills开放格式,而不是某些框架中把任意函数简单命名为skill的旧用法。
在早期的智能体系统中,函数调用几乎是模型使用外部能力的唯一方式。开发者定义一组工具(tools),模型根据用户请求决定调用哪个工具、传什么参数,宿主程序负责执行并返回结果。这个循环简单直接,但它有一个隐含假设:模型已经知道在什么场景下该用哪个工具,以及如何组织多步操作。
当任务变得复杂——比如“检查CSV数据质量并输出摘要”——模型不仅需要知道有“读取文件”和“运行脚本”这两个工具,还需要知道:
这些“操作方法”和“流程约束”如果全部塞进系统提示词,会变得臃肿且难以复用。技能机制正是为了解决这个问题而出现的。
函数调用是模型输出结构化指令、宿主执行外部函数并返回结果的机制。它解决的是“调用哪个工具、传什么参数”的表达问题。在代码层面,它表现为模型返回一个包含工具名和参数JSON的对象,宿主通过分发器执行对应函数。
技能是一组可复用的操作知识包,通常包含一个 SKILL.md 文件(提供名称、描述和Markdown格式的操作方法)以及可选的脚本、参考材料和模板。它解决的是“什么时候用、按什么步骤做、要参考哪些材料”的组织问题。
用一个类比来理解:函数调用像是给你一把锤子和一把螺丝刀,告诉你它们能干什么;技能则像是一本“组装书架指南”,告诉你先做什么、后做什么、遇到什么问题该用哪个工具。
尽管两者定位不同,但它们在设计目标上有若干共同点:
函数调用的核心是一个循环:模型返回工具调用请求 → 宿主执行 → 结果回传 → 模型继续推理。在这个循环中,模型负责决策,宿主负责执行。
技能的核心是一个上下文注入与按需加载机制:宿主在启动时扫描技能目录,解析元数据,将技能目录注入系统提示词;当模型判断任务匹配某个技能时,主动读取对应的 SKILL.md,获取详细操作指南,然后再决定调用哪些已有工具。
关键差异在于:函数调用不自动增加新函数。放一个 SKILL.md 文件在目录里,并不会让模型多出一个“csv-audit”工具。它只是让模型知道“有这么一种做事方法可用”。
| 维度 | 函数调用 | 技能 |
|---|---|---|
| 新增能力 | 需要定义工具schema、实现函数、注册分发 | 只需放置 SKILL.md 和可选脚本 |
| 模型侧变化 | 工具列表变化,模型直接看到新工具 | 系统提示词中增加技能目录,模型按需读取 |
| 宿主侧变化 | 需要实现分发器、参数校验、执行环境 | 需要实现技能发现、元数据解析、文件读取 |
| 对现有循环的影响 | 通常需要修改工具注册逻辑 | 可以在不增加业务函数的前提下引入 |
从改造量来看,技能机制对现有工具循环的侵入性更小。你可以在已有的 read_file 和 run_process 两个工具之上,仅通过增加技能发现和目录渲染逻辑,就实现技能支持。
函数调用的上下文组织相对直接:工具schema以结构化形式放入请求,模型在每次推理时都能看到完整工具列表。
技能的上下文组织是两层的:
read_file 读取 SKILL.md,获取完整的操作方法和约束。这一层按需加载,避免了一次性注入所有技能详情。这种设计的好处是:技能数量增加时,目录层仍然保持精简,只有实际用到的技能才会展开详情。
函数调用的表达能力受限于JSON Schema:它擅长表达“参数是什么类型、有哪些枚举值、是否必填”,但不擅长表达“先做什么、再做什么、遇到异常怎么处理、参考哪些材料”。
技能的表达能力来自Markdown的自由格式:它可以写步骤、写约束、写注意事项、写示例。例如:
---name: csv-auditdescription: 检查 CSV 的缺失值和重复键,输出数据质量摘要。---先确认主键列和空值定义,不自行猜测。使用 scripts/audit.py 检查,不修改输入文件。报告行数、缺失值数、重复键数。执行失败时说明原因,不编造统计数字。
这段文字无法用工具schema表达,但它是模型正确执行任务的关键。
函数调用的安全边界相对清晰:每个工具可以独立设置权限、参数校验规则和沙箱策略。宿主的分发器可以针对不同工具实施不同的检查。
技能的安全边界更复杂:技能本身可能包含脚本,脚本的执行仍然要通过 run_process 等工具。因此,宿主需要对技能目录的发现、SKILL.md 的读取、以及技能中引用的脚本路径进行额外的校验,防止路径穿越、恶意脚本注入等风险。
从性能角度看,函数调用的开销主要在模型推理和工具执行本身。技能机制增加了一次额外的 read_file 调用(读取 SKILL.md),这会增加一轮交互延迟,但换来了更精准的操作指导,可能减少后续的错误重试。
从扩展性角度看,函数调用在工具数量增长时面临“工具列表膨胀”问题——每个工具的描述都占用上下文,工具太多会导致模型选择困难。技能机制通过“目录+按需加载”缓解了这个问题:技能数量增长时,目录层仍然可控。
| 对比维度 | 函数调用(Function Calling) | 技能(Skill) |
|---|---|---|
| 核心职责 | 表达“调用哪个工具、传什么参数” | 组织“什么时候用、按什么步骤做、参考哪些材料” |
| 代码载体 | 工具schema + 宿主分发器 | SKILL.md + 可选脚本/资源 |
| 是否新增可调用函数 | 是,直接增加工具 | 否,默认不注册新工具 |
| 上下文注入方式 | 工具schema全量注入 | 目录层全量 + 详情层按需读取 |
| 表达能力 | 结构化参数,适合精确调用 | 自由文本,适合流程与约束 |
| 接入改造量 | 需要实现工具与分发 | 需要实现发现与加载 |
| 安全关注点 | 参数校验、权限、沙箱 | 路径校验、脚本来源、读取权限 |
| 扩展性瓶颈 | 工具列表膨胀 | 技能目录膨胀(较轻) |
| 典型配合方式 | 作为执行层被技能引导 | 作为知识层引导函数调用 |
优先使用函数调用的场景:
优先使用技能的场景:
两者配合使用的典型模式:
技能作为“操作手册”,函数调用作为“执行手段”。模型先读取 SKILL.md 了解流程,然后按照流程调用已有的 read_file、run_process 等工具完成任务。这种分工让知识层和执行层各自独立演进。
如果你的系统目前只有少量工具,且任务流程简单,直接使用函数调用即可,不必过早引入技能机制。
如果你发现系统提示词中塞入了大量“操作步骤”和“注意事项”,或者同一套操作流程需要在多个任务中复用,那么可以考虑引入技能机制。
如果你需要支持非开发者编写操作指南(例如业务专家撰写数据检查流程),技能的Markdown格式比工具schema更友好。
如果你对安全隔离要求极高,需要对每个可执行单元进行独立审计,函数调用的分发器模式可能更容易实施细粒度控制。
从纯函数调用迁移到“函数调用+技能”模式时,需要注意:
SKILL.md 的解析要健壮:元数据格式要校验,正文内容要防止注入攻击。run_process 等工具执行,复用权限检查和沙箱策略。函数调用和技能不是替代关系,而是分层协作关系。函数调用解决“怎么执行”的问题,技能解决“怎么组织执行”的问题。在代码层面,函数调用体现为工具schema和分发循环,技能体现为 SKILL.md 的发现、加载和上下文注入。
理解这个差异的关键在于:放一个技能文件不会自动长出一个函数。技能改变的是模型的知识和决策依据,而不是宿主的可执行能力。宿主真正要补的,是“发现”和“加载”这两个环节——扫描技能目录、解析元数据、渲染目录、按需读取详情。
在实际系统中,两者经常配合使用:技能提供操作指南,函数调用提供执行手段。选型的核心判断依据是:你的瓶颈在于“模型不知道该怎么做”,还是在于“模型没有可用的执行工具”。前者用技能,后者用函数调用,两者都需要时则组合使用。