最近好几个读者私信问我同一个困惑:OpenClaw 的 Skill 和 OpenAI 的 Function Calling,看起来都是让模型去调工具,到底为什么搞成两套机制?这个问题我太有共鸣了,因为我第一次接触 OpenClaw 的 Skill 系统时,第一反应也是“这不就是 Function Calling 套了个壳吗”。后来真的在项目里把两种方案各跑了一遍,才明白这俩表面都叫“工具调用”,实际上一个是在给 AI 装技能,另一个是在给 AI 开接口,压根不是一个层面的东西。
这篇文章我就把两者的设计初衷、定义方式、执行流程、适用场景全部摊开讲一遍。不会只甩概念,我会给技能卡的写法和函数调用的代码示例,也会把我在实际折腾中踩过的坑一块儿放进来。无论你是刚开始做个人助理自动化,还是已经在用 Function Calling 做业务系统,读完应该都能搞清楚:自己手头的需求到底该选哪一套。
1. 定位对比:一个管“能力积累”,一个管“接口接入”
1.1 OpenClaw Skill 到底解决什么问题
OpenClaw 这类开源个人 AI 助手框架,核心目标是让大模型像管家一样,直接操作电脑上的各种真实环境:终端、浏览器、文件系统、邮箱、日历、聊天应用等等。它要做的事不是“回答一个问题”,而是“替你完成一整套操作”。比如自动整理报销单、定时抓取网页数据、把邮件附件里的账单汇总成表格——这些都不是一次 API 调用能解决的,而是一连串的动作编排。
Skill 就是为这种场景发明的。在我理解里,一个 Skill 更像“任务配方”:它用自然语言告诉 Agent 在什么情况下使用这个能力、任务可以拆成哪几步、每一步需要调什么动作脚本。配方是给模型读的,不是给编译器读的。同一台电脑上可以装几十个技能,Agent 每次接到用户指令时,会根据上下文自己挑技能、组合技能、按步骤执行。社区的技能市场里还会有人共享做好的技能包,装了就能用,不需要你自己写代码。
这个机制的真正价值在于能力积累。你的 Agent 今天学会了一个“整理报销单”技能,明天你再去别的设备上部署同一套 OpenClaw,这个技能可以直接复用,不用重新教一遍。这跟传统软件里“装插件”的思路很像,但插件面向的是固定功能,Skill 面向的是“让模型学会做一件模糊的事情”,定义权在模型手里,不在程序员手里。
1.2 OpenAI Function Calling 到底解决什么问题
OpenAI 的 Function Calling(官方现在更常叫 Tool Use / Tool Calling)是模型 API 层面的一项能力。它的核心问题只有一个:怎么让大模型在生成文本的过程中,稳定地请求外部程序执行一段函数。你在 API 请求里通过 tools 参数传一组函数声明,模型根据用户的话判断需要调用哪个函数,然后返回一个结构化的调用请求,包含函数名和参数。执行完你的代码再把结果作为一条 tool 消息传回给模型,模型基于结果继续生成回复。
这套机制出现的历史背景,是当时大家发现大模型如果纯粹靠“对话”输出,无法可靠地对接业务系统。你想让它查个数据库、发个邮件,它只能“假装”做了,或者在文本里输出一段含糊的描述。Function Calling 的意义在于:它把“模型想调用什么函数、参数是什么”这件事变成了严格的 JSON 输出,你的代码可以直接解析、执行、校验。它保证的是接口层面的确定性,而不是任务编排层面的智能。
1.3 一层抽象的差距决定了工具定位差异
把两者放在一起看,最核心的区别在于抽象层级。Function Calling 站在“单次人机交互”的粒度上,解决的是模型和外部代码之间的一次握手。Skill 站在“完整任务周期”的粒度上,解决的是 Agent 如何在长时间、多步骤、多环境里组织自己的能力。
所以你会看到,Function Calling 完全不知道“做完这一步之后下一步干嘛”,它只是响应式地告诉外部程序“现在请调用这个函数”。而 Skill 可以把“打开邮件 → 找到附件 → 提取金额 → 生成汇总 → 发送提醒”写成一个技能卡,让 Agent 自己按顺序做。这不是“一个工具的两种叫法”,而是“一层抽象的差距”。想清楚这层差距,后面所有细节对比才有意义。
2. 定义语言:人看的技能卡 vs 机器读的接口契约
2.1 Skill 用自然语言写“能力说明书”
OpenClaw 的 Skill 格式,不同版本细节有差异,但大体上是一份 Markdown 格式的技能卡。里面一般会写明技能名称、用途描述、触发时机、执行步骤,以及关联的动作脚本。我常用的写法如下:
--- name: daily_report_generator description: 自动汇总当天工作记录并生成日报 triggers: - 用户要求生成日报 - 时间到达每日18:00 steps: 1. 扫描工作日志目录中的当日记录 2. 提取关键任务和耗时项 3. 按日报模板填充并输出 actions: - command: generate_report params: date: today注意这里有一个很关键的点:steps 里的内容不是严格的编程指令,而是“描述给 Agent 听的意图”。真正执行层面,generate_report 这个动作会对应一个脚本,脚本负责读写文件、调用模板等实际操作。也就是说,技能卡本身是一份“人可读、模型可理解”的能力说明,底层的动作脚本才是真正干活的工具。
这种设计的直接好处是维护成本低。你想给 Agent 加一个新能力,不需要像写 API 那样定义一堆参数结构,只需要写清楚“什么情况能用、大概怎么做”。我实际用下来的感受是,一份一百来字的技能卡,效果通常能顶得上几十行工具定义代码,而且改起来也方便——想调整步骤,改文本就行,不用重新编译部署。
2.2 Function Calling 用 JSON Schema 写“接口契约”
Function Calling 的定义方式则完全是另一套思路。它要求你为每一个函数提供一个 JSON Schema,用结构化字段精确描述函数名、用途、参数类型、是否必填、取值范围。一个典型的天气查询函数定义长这样:
{ "type": "function", "function": { "name": "get_weather", "description": "查询指定城市的实时天气", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名" }, "unit": { "type": "string", "enum": ["celsius", "fahrenheit"], "description": "温度单位" } }, "required": ["city"] } } }调用时,你在请求里把 tools 数组传进去,模型在需要时就会返回一个 tool_calls 结构。代码里通常这样写:
response = client.chat.completions.create( model="gpt-4o", messages=messages, tools=[weather_tool], ) tool_call = response.choices[0].message.tool_calls[0] # 拿到函数名和参数,再自己写逻辑去执行这里有个容易误会的地方:模型永远不会替你执行任何函数,它只是输出“我希望调用这个函数,参数是这些”。真正执行函数、拿到结果后再把结果喂回模型的工作,全在你自己的代码里。所以我说它是“接口契约”——它规范的是模型和程序之间的通信格式,不负责业务逻辑的编排。
2.3 两种定义的侧重点差异
自然语言技能卡和结构化接口契约,各自的侧重点完全不同。技能卡追求的是“表达能力的边界和步骤”,它能描述模糊的目标,比如“整理一下今天的账单”,也能描述复杂的条件分支,比如“如果附件里没有明细则提示用户补发”。但代价是它不保证参数的合法性,模型可能脑补出不存在的参数,也可能跳过某个你觉得很重要的步骤。
JSON Schema 追求的是“参数的确定性和可校验性”。模型输出的参数可以直接拿来执行,类型不对可以被 JSON Schema 校验拦住,参数缺失可以通过 required 约束提前规避。代价是它表达不了“怎么做”的编排逻辑,只能表达“调用什么、传什么”。用生活化的比喻来说:技能卡是一本菜谱,写着“准备食材、热锅、翻炒、装盘”;Function Calling 是一个点菜接口,输入“菜名、口味、忌口”就能下单,但怎么炒菜完全不归接口管。
3. 执行控制权在谁手里:这是最本质的分水岭
3.1 Skill 模式下,控制权在 Agent 自己手里
两种机制最大的分水岭是执行控制权。Skill 模式下,决策主体是 Agent。用户发来一句“帮我整理报销单”,Agent 的大脑(也就是大模型)会先在自己的技能列表里搜索,找到报销单技能,读取技能卡里的步骤,然后按照步骤一步步调用动作脚本。执行完一步,结果会回到对话上下文里,Agent 看结果再决定下一步怎么做——结果不对就重试,缺附件就提示用户,甚至临时改计划。
这种自驱执行的好处是它能应对模糊任务和意外情况。比如步骤 1 扫描目录时发现文件格式不对,技能卡里没写这条,Agent 也能根据常识判断要不要换个方式扫描,或者问用户。它不会因为“程序里没写这个分支”就卡死,因为模型本身具备推理能力。坏处则是不可预测:你没法保证 Agent 每次都严格执行同样的步骤,它可能会擅自调整流程,甚至调用错误的技能。所以 OpenClaw 这类框架一般会做权限管理,把高风险操作限制在可审计范围内。
3.2 Function Calling 模式下,控制权在应用代码手里
Function Calling 模式完全是另一套控制流。整个流程本质是一个 agent loop,由你的代码驱动:
- 你把用户消息和 tools 数组发给模型。
- 模型判断需要调用某个函数时,返回 tool_calls。
- 你的代码解析 tool_calls,自己执行对应函数,拿到结果。
- 把执行结果作为 role 为 tool 的消息返回给模型。
- 模型看到结果,要么继续请求下一个函数,要么输出最终回答。
每一步都不由模型主动推进,而由外部程序循环驱动。模型从来不会说“我先把邮件附件扫描了,然后再提取金额”,它只会单次输出一个“请调用 scan_mail”的请求。你要做多步任务,就必须自己在代码里写好“如果 scan_mail 返回成功,就把结果的 attachments 字段传给 extract_total 函数”这种编排逻辑。你就是那个编排者,模型只是一个个孤立的“决策点”。
这种模式的特征是可预测、可审计。每一步调用都能打日志,哪个函数被调用了、传了什么参数、花了多少时间,全都清清楚楚。适合对稳定性和安全性要求高的业务场景。代价是:一旦任务变复杂,你的编排代码会呈指数级膨胀。我见过有人为了做一个“查询订单状态并自动发短信”的小功能,写了整整一百多行循环和分支逻辑,而那点事在 Skill 模式下也就是一张几十字的技能卡。
3.3 六个维度快速对比
| 对比维度 | OpenClaw Skill | OpenAI Function Calling |
|---|---|---|
| 抽象层级 | Agent 应用层,面向完整任务 | 模型 API 层,面向单次工具调用 |
| 定义语言 | 自然语言技能卡加动作脚本 | JSON Schema 结构化声明 |
| 决策主体 | 模型自主选择技能、编排步骤 | 外部应用代码驱动调用循环 |
| 状态维护 | 上下文和记忆里自带任务状态 | 需要应用自己维护状态机 |
| 错误恢复 | 模型可根据结果自愈重试 | 需代码显式处理,分支语焉不详 |
| 可预测性 | 较低,但灵活 | 较高,适合审计 |
这张表基本就是全文的核心。看懂了它,你就能理解为什么一个叫“技能”,一个叫“函数调用”——技能是 Agent 自己的能力,函数调用是外部程序递给模型的接口。
4. 同一个需求,两套落地方案怎么走
4.1 Skill 方案:一张技能卡搞定复合任务
拿一个很常见的个人助理需求举例:“抓取邮件附件里的账单,汇总金额,并生成提醒。”这种任务在真实工作中特别典型,在 OpenClaw 里实现时,核心就是一张技能卡加两个动作脚本。技能卡长这样:
--- name: bill_summarizer description: 读取邮件附件中的账单文件,汇总金额并生成提醒 triggers: - 用户要求汇总账单 - 检测到新邮件含账单附件 steps: 1. 从收件箱筛选包含“账单”或“invoice”字样的邮件 2. 下载附件中的表格或 PDF 文件 3. 提取金额、日期、商户条目 4. 汇总总金额,生成摘要文本 5. 通过通知动作推送给用户 actions: - command: scan_mail params: { keyword: "账单" } - command: summarize_bills params: { source_dir: "./downloads" }动作脚本我一般用 Python 写,核心逻辑就是一个扫描目录、解析表格、求和的过程:
# summarize_bills.py def run(params): total = 0 for f in list_files(params["source_dir"]): amount = parse_bill_amount(f) total += amount return f"本月账单合计: {total} 元,共 {len(files)} 笔"重点是:Agent 在运行时看到的是整张技能卡,它会自己决定先调 scan_mail 下载文件,再调 summarize_bills 汇总。整个过程对用户来说只有一句话交互,对开发者来说只需要维护技能卡和底层脚本,不需要写任何编排代码。任务步骤再多几层,比如“汇总后按类别分类”“超过阈值才提醒”,也只需要在技能卡的 steps 里加两行描述。
4.2 Function Calling 方案:三个函数加一个循环
同样这个需求,如果用 Function Calling 实现,你就得先把任务拆成三个独立函数。我写的 tools 定义大概是这样:
tools = [ {"type": "function", "function": {"name": "scan_mail", "parameters": {...}}}, {"type": "function", "function": {"name": "summarize_bills", "parameters": {...}}}, {"type": "function", "function": {"name": "send_reminder", "parameters": {...}}}, ]然后自己写一个循环来驱动流程:
def run_agent(messages): for _ in range(10): response = client.chat.completions.create( model="gpt-4o", messages=messages, tools=tools, ) msg = response.choices[0].message if not msg.tool_calls: return msg.content for tc in msg.tool_calls: result = execute_function(tc) messages.append({"role": "tool", "tool_call_id": tc.id, "content": result})注意,这里即使你已经把三个函数的定义传给了模型,模型也只是在每一步单独决定“现在要调用哪一个”。它不会主动把“扫描完了再总结”这个流程串起来。你必须依赖第二步的返回结果,再触发第三步。
第一次跑这种循环的人,十有八九会栽在“模型没有按照你设想的顺序调用函数”上。它可能扫描完邮件之后直接输出“发现三封账单邮件,需要我现在汇总吗?”——因为模型觉得跟用户确认一下更合理,而你还没来得及实现“确认后继续”的分支。这就是编排能力缺失的直接表现:模型没法在函数调用里表达“我先做 A,如果成功再做 B”。它只能发出一个孤立的调用请求,剩下的都得代码来兜。
4.3 案例对比小结
| 维度 | Skill 方案 | Function Calling 方案 |
|---|---|---|
| 任务拆分粒度 | 整张技能卡描述全套流程 | 拆成多个独立函数,代码负责串联 |
| 定义成本 | 一张 Markdown 加两个脚本 | 三个 JSON Schema 加循环编排代码 |
| 流程控制 | 模型基于技能卡自主编排 | 外部应用强制编排 |
| 异常处理 | 模型根据结果灵活调整 | 需要提前写好分支逻辑 |
| 适用任务 | 多步、跨应用、长期任务 | 单步接口调用、事务性操作 |
这个对比不是我硬凑的,而是我实际用两种方案实现同类需求后最直观的体感。需求越复合,Skill 的优势越明显;需求越单一,Function Calling 的轻量性就越突出。
5. 工程上怎么选型:我的三个判断标准
5.1 标准一:任务规模是一步还是一个流程
如果任务本质上是“一次操作”,例如根据用户话语查询天气、翻译一句话、搜索一下知识库,直接上 Function Calling 就足够了。你只需要定义一个函数,模型填好参数,你的代码执行并返回,完事。
如果任务涉及多个步骤、多个系统、甚至需要跨时间周期执行,比如“每周五自动汇总本周工作并生成周报”,那就必须用 Skill 模式。因为这种任务的关键不是“模型能不能调用一个函数”,而是“模型能不能记住整个任务的上下文,并且自主推进流程”。Skill 天然把步骤、条件、动作组织在一起,能力边界清晰,也方便长期维护。
5.2 标准二:谁来维护这套能力定义
这个问题经常被问的人忽略,但其实特别关键。Function Calling 的函数定义最终是给应用程序看的,要求类型准确、参数可控,通常由后端开发维护。Skill 的技能卡是给模型看的,描述可以模糊,步骤可以随经验调整,维护者可以是懂业务但不一定懂编程的人。
如果你的团队里有能写脚本的人,但缺少专门的后端接口开发,或者你们不想为了一个 AI 助理能力长期维护一套 API 服务,那 Skill 的 Markdown 加脚本模式会轻松很多。反过来,如果你已经有成熟的业务服务,只想给现有系统加一个 AI 入口,那你肯定会选择 Function Calling——你不需要让模型去编排,只需要让模型帮你把用户意图翻译成标准参数。
5.3 标准三:你有多能容忍模型的“自由发挥”
Skill 模式的代价是模型有自由度,它可能跳过步骤、改流程、自作主张。为了提高可控性,OpenClaw 需要有完善的权限体系、操作审计、沙箱机制。如果你做的是财务、医疗这类高风险场景,任何一步都不能出差错,那 Skill 的自由发挥就是灾难。Function Calling 可以做到严格约束:只允许模型调用白名单函数,参数必须通过 JSON Schema 校验,执行结果由代码判断后决定是否继续。模型本质上没有决定权,只有建议权,你的代码才是最终的守门员。
我的经验是:在不落地的对话场景里,Skill 的自由度是优点;在要落地的业务系统里,自由度高就是隐患。自己搭个人助理,优先考虑 Skill;给公司业务做 AI 集成,优先考虑 Function Calling。
5.4 组合用法:Skill 编排加 Function Calling 收口
需要特别说明的是,这两者并不是只能二选一,很多人其实已经在混用。一个很自然的组合方式:Skill 负责决策和编排,动作脚本内部再调用 Function Calling 风格的接口,把参数收口到现有业务系统。这样既获得了 Skill 的任务编排能力,又获得了函数调用的参数稳定性。
我在 OpenClaw 的动作脚本里经常写类似这样的逻辑:
def run(params): user_query = params["query"] # 动作脚本内部调用模型接口,按 tools 方式请求业务函数 result = call_model_with_tools(user_query, tools=[existing_api_tool]) return resultSkill 告诉模型“你可以做这件事,大概分几步”,动作脚本再通过 Function Calling 跟外部系统精确对接。“上层编排靠技能,底层参数靠函数”,这是我目前用过最顺手的组合,推荐你试试。
6. 实操踩坑记录与排查速查
6.1 我踩过的坑清单
坑一:技能描述写得太抽象,模型死活不触发。我一开始把技能卡 description 写成“账单处理助手”,结果 Agent 在用户说“看看这个月花了多少”时完全没想起这个技能。后来改成“用户询问消费金额、账单统计、发票汇总时使用,尤其适用于邮件附件里有电子账单的场景”,触发率立刻上去了。写技能描述时要站在模型角度想:用户什么话会让我想到这个技能。
坑二:Function Calling 返回的参数不符合预期。最常见的情况是 required 字段没设全,模型就敢不传参数。比如城市字段没设 required,模型排到“查询当前城市天气”时直接把 city 省略了,我的代码拿到空字符串直接报错。解决办法是把必要参数全部标记为 required,能在 Schema 里用 enum 约束的就别用自由文本,宁可多写几个字段,也不要让模型自由发挥。
坑三:定义太多函数导致上下文膨胀。OpenAI 的 tools 数量理论上限不算低,但工具一多,模型选错函数的概率也在涨。我实测过,十几个工具时就开始偶发漏选,二十几个工具时模型经常把相近功能搞混。Skill 模式没有这种困扰,因为技能卡可以按需加载。用 Function Calling 时建议分组动态传 tools,别一把全塞进去。
坑四:tool 消息回流格式不对,模型开始胡言乱语。执行完函数后,必须用正确的 tool_call_id 把结果回传。我一开始图省事,直接把执行结果塞在普通 user 消息里,结果模型完全无法关联到之前的函数调用请求,就开始瞎编结果。正确做法是结构严格写清楚 role、tool_call_id、content,对话历史一条都不能错位。
坑五:Agent 陷入无限循环调用。Skill 模式下,模型如果连续几次执行失败,它会不停换方式重试,直到触及对话长度限制。Function Calling 模式也会出现模型反复请求同一个函数的情况。最好在循环里设置最大迭代次数,我一般设 5 到 8 次,超过就中断并把中间结果整理成摘要告诉模型“尝试终止”。
坑六:技能卡里的 steps 和实际动作脚本行为不一致。写技能卡时把步骤写得挺美,但脚本实际上只做了前两步,后三步根本没实现。Agent 执行完脚本后以为整个任务完成了,直接向用户汇报成功,其实结果根本不对。所以每次改技能卡,必须同步检查动作脚本是否真正实现了所有步骤,不能让描述和现实脱节。
6.2 排查速查表
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| Skill 不触发 | description 与场景关键词不匹配 | 重写技能描述,写明用户话语模式 |
| Skill 触发后乱做 | steps 太模糊,模型无法对齐流程 | 拆细步骤,关键步骤加条件说明 |
| 函数返回空参数 | missing required | 补全 schema 的 required 字段 |
| 模型选择错误工具 | 工具定义过多或描述相似 | 精简 tools,按场景分组加载 |
| 无限调用循环 | 编排循环缺终止条件 | 代码里设最大迭代次数 |
| 结果回传失真 | tool_call_id 错位或格式错误 | 严格按 tool 消息格式回执 |
| 执行结果与技能描述不符 | 动作脚本未完整实现 | 同步修订脚本与技能卡 |
这套速查表是我自己排查问题时反复用到的,基本上覆盖了这两套机制里最常见的坑。如果你遇到的是其他怪异现象,建议先确认调用日志里模型实际看到了什么——很多时候真相就在上下文里,只不过被自己忽略了。
最后再分享一个个人经验:如果你要做的是那种“会长期跑、会不断学习新技能”的个人助理,Skill 这套思路带来的长期收益非常明显。它真正把能力定义这件事从程序员手里交还给了模型,让你能在几天内积累出一整套自己的自动化技能库。而如果你只是想让模型偶尔调一下现有系统的接口,别过度设计,Function Calling 直接上就行。我在实际折腾中最深的体会是:别一上来就纠结“哪个更高级”,先想清楚谁该做决策——模型还是你的代码。这个答案定了,技术选型基本就定了。