1. 一次工具调用卡住时,先看这三个节点
OpenClaw 的工具调用链路,说到底是三个节点在接力:AgentEvent、tool_call、tool_result。你如果正在接入 OpenClaw,或者线上突然出现“模型说要调工具但没动静”“工具执行了但模型像没看见结果”这类问题,八成是这三个节点里有一个断了。OpenClaw 是一个事件驱动的 Agent 运行时,它和 OpenAI 那种流式delta.tool_calls的玩法不一样——模型一次性吐出结构化的tool_callsJSON,OpenClaw 的适配层解析后创建tool_call类型的 AgentEvent,工具执行完再封装成tool_result事件回灌给模型。整条链路是“事件总线”串起来的,不是增量流。
这篇文章面向正在接入或排查 OpenClaw 工具调用问题的开发者。我会把从模型生成工具调用、到 OpenClaw 解析、到执行、到结果回传的完整路径拆开,给出可复制的配置骨架和逐步验证动作。你跟着做,能定位到链路到底断在哪一环。适合谁:已经跑通基础对话、准备接工具或正在被工具调用问题卡住的同学。下面所有配置和验证都基于本地源码路径C:\work\openclaw的目录结构来写,你换成自己的路径即可。
2. 前置准备:TaoToken 接入与工具注册骨架
在拆链路之前,先把“模型从哪来”和“工具从哪注册”这两件事定下来。OpenClaw 本身不绑定模型供应商,它通过适配层对接。我这边习惯用 TaoToken 做统一接入,原因是它的 API 兼容 OpenAI 与 Anthropic 两种协议,OpenClaw 的openai-transport-stream.ts和anthropic-transport-stream.ts都能直接对上,省得为不同模型改适配代码。
先拿 Key。打开控制台创建 API Key,地址是 https://taotoken.net/console ,创建完复制保存。如果你还没决定用哪个模型,可以先去模型对话页面试一下工具调用能力,地址 https://taotoken.net/chat ,选一个支持 function calling 的模型发一句“帮我列出当前目录文件”,看它会不会生成结构化的 tool_calls。这一步能提前排除“模型本身不支持工具调用”这个最容易被忽略的原因。
拿到 Key 后,在 OpenClaw 的模型配置里填上。OpenClaw 的模型适配层读取的是环境变量或配置文件,我一般写成环境变量,避免硬编码:
# Windows PowerShell $env:OPENCLAW_MODEL_BASE_URL="https://taotoken.net/api" $env:OPENCLAW_MODEL_API_KEY="sk-你的Key" $env:OPENCLAW_MODEL_NAME="claude-sonnet-4-5" # macOS / Linux export OPENCLAW_MODEL_BASE_URL="https://taotoken.net/api" export OPENCLAW_MODEL_API_KEY="sk-你的Key" export OPENCLAW_MODEL_NAME="claude-sonnet-4-5"注意 base URL 用https://taotoken.net/api,不要带多余路径,适配层会自己拼/v1/messages或/v1/chat/completions。填错这里最常见的表现是 404,而不是 401,别被误导。
工具注册这一侧,OpenClaw 的注册中心是src/agents/openclaw-tools.ts里的createOpenClawTools()。所有工具在这里被创建并注册,name是调用的唯一标识。你新增一个工具,本质就是往这个注册表里加一项,包含name、description、parameters三要素。系统提示注入则由src/agents/skills/workspace.ts的buildWorkspaceSkillsPrompt()完成,它把可用技能和底层工具格式化成 XML 文本塞进 system prompt。也就是说,模型能“看见”哪些工具,取决于这一步注入了什么。
3. 可复制配置:把 tool_call 到 tool_result 串起来
这一节是核心,我把链路拆成四段,每段给你可复制的配置或代码骨架。
3.1 阶段一:模型生成 tool_calls
模型收到 system prompt(含工具描述)和用户请求后,推理并生成工具调用。关键点:OpenClaw 里模型的tool_calls是模型输出的文本内容的一部分,不是框架事件。它被包在模型的content字段里,一次性生成,不是流式增量。这跟 OpenAI 的delta.tool_calls有本质区别,排查时别拿 OpenAI 的经验套。
你可以在src/agents/skills/skill-contract.ts的formatSkillsForPrompt()里确认 XML 格式是否正确。如果工具描述没注入进去,模型根本不知道有exec这个工具,自然不会生成 tool_calls。验证方法:打印发给模型的完整 system prompt,搜<available_skills>和工具名,搜不到就是注入断了。
3.2 阶段二:OpenClaw 解析并创建 AgentEvent
模型响应回来后,适配层(openai-transport-stream.ts或anthropic-transport-stream.ts)接收完整响应,再由openai-ws-message-conversion.ts这类转换器从响应文本里提取tool_calls对象。解析成功后,系统创建一个tool_call类型的 AgentEvent,结构如下:
{ "type": "tool_call", "toolName": "exec", "toolCallId": "call_abc123", "arguments": { "command": "dir" }, "runId": "run_xyz789" }这个事件被投递到 OpenClaw 内部事件总线。toolCallId是后续把结果对回去的关键,runId用来串同一次运行的所有事件。排查时如果事件总线里没有tool_call,说明解析环节失败,重点看转换器有没有正确识别模型返回的 JSON 结构。
3.3 阶段三:执行工具
事件分发由src/agents/pi-embedded-subscribe.handlers.tools.ts的handleToolExecutionStart()负责。它监听tool_call事件,根据toolName在createOpenClawTools()生成的注册表里查找工具实现,然后调用其执行函数。以exec为例,最终落到bash-tools.exec.ts:
// 模拟执行逻辑 const result = await exec({ command: "dir" });工具返回结果有三种形态,这个区分很重要,因为不同形态后续处理路径不同:
| 返回状态 | 结构 | 后续动作 |
|---|---|---|
| success | {"status":"success","output":"..."} | 直接封装为 tool_result |
| error | {"status":"error","error":"..."} | 封装为 tool_result,模型据此纠错 |
| approval-pending | {"status":"approval-pending","approvalId":"req_456"} | 挂起,等待审批后再继续 |
参数标准化由src/agents/pi-embedded-runner/run/attempt.tool-call-normalization.ts处理,核心调度在attempt.ts。如果你遇到“工具收到了参数但格式不对”,先看这个标准化文件。
3.4 阶段四:结果回传模型
handleToolExecutionEnd()接收工具最终结果,封装成tool_result类型的 AgentEvent:
{ "type": "tool_result", "toolCallId": "call_abc123", "result": { "status": "success", "output": "文件1.txt\n文件2.txt" }, "runId": "run_xyz789" }然后它把tool_result连同原始tool_call一起,作为新的对话历史重新发给模型。模型基于这个新信息生成最终回复。注意toolCallId必须和tool_call事件里的一致,否则模型对不上号,会表现为“工具执行了但模型装没看见”。
4. 验证请求:逐步确认链路没断
配置完别急着上复杂任务,用最小请求逐段验证。我一般分四步走。
第一步,验证模型能生成 tool_calls。直接发一个明确需要工具的请求,比如“列出当前目录下的文件”。在适配层加一行日志,打印模型原始响应,确认content里含tool_calls字段。没有就回到 3.1 检查 system prompt 注入。
第二步,验证 AgentEvent 创建。在事件总线上订阅tool_call类型,打印事件体。用 curl 直接打模型接口做对照,确认解析器没漏字段:
curl https://taotoken.net/api/v1/messages \ -H "x-api-key: sk-你的Key" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-sonnet-4-5", "max_tokens": 1024, "messages": [{"role":"user","content":"列出当前目录文件"}], "tools": [{"name":"exec","description":"执行命令","input_schema":{"type":"object","properties":{"command":{"type":"string"}}}}] }'返回里如果stop_reason是tool_use,说明模型侧没问题,问题在 OpenClaw 解析。
第三步,验证工具执行。在handleToolExecutionStart()里打断点或加日志,确认toolName能查到实现、执行函数被调用、返回值符合预期。这一步最常见的坑是工具名大小写不一致,注册表里是exec,事件里传成Exec,查不到就静默失败。
第四步,验证结果回传。订阅tool_result事件,确认toolCallId与tool_call匹配,然后看模型最终回复是否用上了工具输出。如果模型回复里没有工具结果,检查回传的历史里tool_result有没有正确挂到对应tool_call下。
5. 本篇常见错排查
报错一:模型不生成 tool_calls。先确认模型支持 function calling,去 https://taotoken.net/chat 用同样 prompt 试一次。再检查buildWorkspaceSkillsPrompt()是否把工具描述注入了 system prompt。最后看formatSkillsForPrompt()的 XML 有没有格式错误导致模型解析不了。
报错二:有 tool_call 事件但工具没执行。九成是toolName在注册表里查不到。打印注册表所有name,和事件里的toolName逐字对比,注意大小写和连字符。另一个可能是handleToolExecutionStart()没订阅到事件,检查事件总线订阅是否在工具注册之后才建立。
报错三:工具执行了但模型没反应。检查tool_result的toolCallId是否和tool_call一致。不一致的常见原因是转换器在解析时重新生成了 ID,而不是复用模型给的。还要确认回传历史时tool_result和tool_call的配对顺序正确,顺序错了模型会忽略。
报错四:approval-pending 卡住不继续。这是审批流没走完,不是链路断了。检查approvalId对应的审批请求有没有被处理,处理完要主动触发继续,否则事件总线一直在等。
报错五:参数格式不对。看attempt.tool-call-normalization.ts的标准化逻辑,模型给的参数类型可能和工具 schema 不匹配,比如数字给成了字符串。标准化层负责兜底转换,如果它没覆盖你的场景,就得手动补规则。
6. 把链路跑通之后
链路跑通后,你会发现 OpenClaw 这套事件驱动设计的好处:每个节点都是可观测的,断在哪一环一目了然。我建议你在接入阶段就把tool_call和tool_result两个事件都打上日志,带上runId和toolCallId,线上出问题直接按 runId 捞全链路。长期做编码类 Agent 或需要多轮工具调用的场景,可以考虑用 Coding Plan 来管理额度和并发,地址是 https://taotoken.net/coding-plan ,接入方式不变,还是那套 base URL 和 Key。工具注册和适配层的细节文档在 https://taotoken.net/doc ,遇到协议对不上的时候翻一下比猜快。