☰
OpenClaw 工具调研决策核心逻辑:从 Agent Loop 到 Tool Use 的 TypeScript 实现
2026/10/2 11:41:02 网站建设 项目流程

1. OpenClaw 工具调研决策核心逻辑:从 Agent Loop 到 Tool Use 的 TypeScript 实现

OpenClaw 是一个用 TypeScript 实现的 Agent 运行时,它把「LLM 自动调用工具」这件事拆成了两个可独立调试的模块:Agent Loop(循环调度器)和 Tool Use(工具协议层)。如果你正在评估多工具调用策略,或者想搞清楚为什么 Agent 能自己决定「先读文件、再改代码、最后跑测试」,那 OpenClaw 的源码结构值得逐层拆开看。它适合三类人:需要给现有系统加 Agent 能力的后端开发者、想自建工具调用框架的架构师、以及正在对比不同 Agent 实现方案的技术选型者。

我试过把 OpenClaw 的循环逻辑单独抽出来跑,发现它的核心其实非常朴素——一个 while 循环,每次把对话历史和工具定义一起发给 LLM,LLM 返回「要不要调工具」的指令,Agent 负责执行并把结果塞回历史,直到 LLM 说「我说完了」。但真正让它在生产环境可用的,是循环外面的那些约束:最大迭代次数、工具权限控制、沙箱隔离、错误重试。这篇文章会从 Agent Loop 的 TypeScript 实现讲起,给出可复制的配置片段和 Tool Use 决策表,最后用一次完整的工具选择验证流程收尾。

在开始之前,你需要准备一个能访问 LLM API 的 Key。我用的是 TaoToken 的兼容接口,它的 Base URL 是https://taotoken.net/api,支持 Anthropic 和 OpenAI 两种消息格式,这样在 OpenClaw 里切换 Provider 时不用改太多代码。下面所有示例都基于这个接口跑通。

2. Agent Loop 的 TypeScript 实现与工具注册机制

OpenClaw 的 Agent Loop 藏在src/agents/pi-embedded-runner/run/attempt.ts里,但它的逻辑和教学项目 learn-claude-code 的 Python 版本几乎一一对应。理解了这个循环,你就理解了所有 Agent 的骨架。

先看最核心的循环体。OpenClaw 把循环封装在 Pi Agent SDK 内部,上层只需要配置 session 和 streamFn:

// openclaw/src/agents/pi-embedded-runner/run/attempt.ts const { session } = await createAgentSession({ tools: builtInTools, // 内置工具:exec、read、write、edit customTools: allCustomTools, // 自定义工具:通过插件注册 model: params.model, maxIterations: 32, // 默认最大迭代次数 }); activeSession.agent.streamFn = streamSimple; // 绑定 LLM 调用函数

这里的streamFn就是循环里「调用 LLM」那一步的具体实现。OpenClaw 默认用 Anthropic 的消息格式,streamSimple会把 messages 和 tools 一起发给模型。SDK 内部的循环逻辑用伪代码表示是这样的:

async function agentLoop(messages: Message[], tools: ToolDefinition[]) { let iteration = 0; while (iteration < maxIterations) { iteration++; const response = await streamFn(messages, tools); messages.push({ role: "assistant", content: response.content }); if (response.stop_reason !== "tool_use") { return response.content; // 没有工具调用,循环结束 } for (const block of response.content) { if (block.type === "tool_use") { const output = await executeTool(block.name, block.input); messages.push({ role: "user", content: [{ type: "tool_result", tool_use_id: block.id, content: output, }], }); } } } throw new Error("Max iterations reached"); }

关键点在于stop_reason的判断。LLM 返回的 response 里,如果stop_reason是"tool_use",说明模型要求调用工具;如果是"end_turn",说明模型认为任务完成,直接返回文本。Agent 本身不「理解」工具,它只是把工具的 JSON Schema 描述传给 LLM,由 LLM 决定调不调、调哪个、传什么参数。

工具注册在 OpenClaw 里是通过pi-tools.ts完成的:

// openclaw/src/tools/pi-tools.ts export function createOpenClawCodingTools(options: ToolOptions) { return [ createExecTool({ ... }), // bash 执行 createProcessTool({ ... }), // 进程管理 createApplyPatchTool({ ... }), // 补丁应用 createOpenClawReadTool({ ... }), // 文件读取 createSandboxedWriteTool({ ... }), // 沙箱写入 createSandboxedEditTool({ ... }), // 沙箱编辑 ]; }

每个工具都实现了统一的接口:name、description、parameters(JSON Schema)、execute。SDK 在调用 LLM 前,会把这些工具转成 Anthropic 的 tool 格式:

{ name: "read_file", description: "Read the contents of a file", input_schema: { type: "object", properties: { path: { type: "string", description: "File path to read" }, limit: { type: "number", description: "Max lines to read" } }, required: ["path"] } }

这个 Schema 就是 LLM 做决策的全部依据。描述写得越清楚,LLM 选错工具的概率越低。我在实测中发现,把description从「读取文件」改成「读取指定路径的文本文件内容,支持限制行数,用于查看代码或配置」,工具选择准确率有明显提升。

3. 可复制的 Agent Loop 配置与 Tool Use 决策表

要让 OpenClaw 跑起来,你需要一份完整的配置。下面这个agent-config.json可以直接复制,路径放在项目根目录的config/下:

{ "agent": { "name": "openclaw-research", "model": "claude-sonnet-4-20250514", "maxIterations": 32, "temperature": 0.2, "systemPrompt": "你是一个工具调研助手。根据用户需求选择合适的工具,优先使用只读工具收集信息,确认后再使用写入工具。" }, "provider": { "baseUrl": "https://taotoken.net/api", "apiKey": "${TAOTOKEN_API_KEY}", "format": "anthropic" }, "tools": { "enabled": ["read_file", "write_file", "edit_file", "exec", "search"], "permissions": { "exec": { "allow": ["ls", "cat", "grep", "find"], "deny": ["rm", "curl"] }, "write_file": { "sandbox": "./workspace" } } }, "loop": { "maxIterations": 32, "timeoutMs": 120000, "retryOnError": 2 } }

对应的 TypeScript 加载代码:

import { createAgentSession } from "openclaw"; import config from "./config/agent-config.json"; const session = await createAgentSession({ model: config.agent.model, maxIterations: config.loop.maxIterations, tools: createOpenClawCodingTools({ sandboxRoot: config.tools.permissions.write_file.sandbox, execAllowlist: config.tools.permissions.exec.allow, }), }); session.agent.streamFn = createStreamFn({ baseUrl: config.provider.baseUrl, apiKey: process.env.TAOTOKEN_API_KEY, format: config.provider.format, });

Tool Use 的决策逻辑可以用一张表来对照。这张表是我在调试多个 Agent 项目后总结的,列出了 LLM 在不同 stop_reason 和 content 组合下的行为:

stop_reasoncontent 类型Agent 动作下一步
end_turn纯文本返回文本,循环结束输出最终回复
tool_usetext + tool_use执行 tool_use 块结果追加到 messages,继续循环
tool_use多个 tool_use并行执行所有工具所有结果一起追加,继续循环
max_tokens截断文本记录警告,返回已有内容可选:重新请求补全
error空或错误信息触发重试逻辑重试次数内重新调用 LLM

这张表的关键在于第三行:当 LLM 一次返回多个 tool_use 时,OpenClaw 会并行执行它们。比如用户说「帮我看看 src 目录下有哪些文件,顺便读一下 package.json」,LLM 可能同时返回exec(ls src)和read_file(package.json)两个工具调用。Agent 并行执行后,把两个结果一起塞回 messages,LLM 下一轮就能同时看到目录列表和依赖信息。

但并行执行有个坑:如果两个工具都写同一个文件,会产生竞态。OpenClaw 的解法是在工具层加锁,write_file和edit_file共享一个文件锁,同一路径的写入串行化。这个细节在配置里体现为tools.permissions.write_file.sandbox的路径隔离。

4. 验证请求与成功结果:一次完整的工具选择流程

配置写好后,怎么验证 Agent Loop 真的在工作?我设计了一个最小验证流程:让 Agent 完成「读取一个文件、修改它、再读回来确认」的任务,观察每一轮 LLM 的 stop_reason 和工具调用。

先准备测试文件:

mkdir -p workspace && echo "hello world" > workspace/test.txt

然后写验证脚本:

import { createAgentSession } from "openclaw"; const session = await createAgentSession({ model: "claude-sonnet-4-20250514", maxIterations: 10, tools: createOpenClawCodingTools({ sandboxRoot: "./workspace" }), }); session.agent.streamFn = createStreamFn({ baseUrl: "https://taotoken.net/api", apiKey: process.env.TAOTOKEN_API_KEY, format: "anthropic", }); // 监听每一轮循环 session.agent.on("iteration", (data) => { console.log(`[第 ${data.iteration} 轮] stop_reason=${data.stopReason}`); console.log(` 工具调用: ${data.toolCalls.map(t => t.name).join(", ") || "无"}`); }); const result = await session.run( "读取 workspace/test.txt,把内容改成 'hello openclaw',然后读回来确认修改成功。" ); console.log("最终结果:", result);

运行后,控制台会输出类似这样的日志:

[第 1 轮] stop_reason=tool_use 工具调用: read_file [第 2 轮] stop_reason=tool_use 工具调用: write_file [第 3 轮] stop_reason=tool_use 工具调用: read_file [第 4 轮] stop_reason=end_turn 工具调用: 无 最终结果: 文件已修改为 'hello openclaw',读取确认成功。

这个流程验证了四件事:第一,LLM 能根据用户意图选择正确的工具序列(读→写→读);第二,Agent Loop 在每次 tool_use 后正确追加结果并继续循环;第三,stop_reason=end_turn时循环正常终止;第四,最终文本回复包含了任务完成的确认。

如果你想更直观地看工具调用链路,可以用 TaoToken 的模型对话功能手动发一轮请求,把 tools 定义贴进去,观察模型返回的 tool_use 块结构。这比读日志更直接,能帮你理解 LLM 到底「看到」了什么。

验证通过后,你可以把maxIterations调小到 3,再跑一次同样的任务。这时 Agent 会在第三轮被强制终止,返回一个「达到最大迭代次数」的错误。这个实验能帮你理解循环边界的重要性——生产环境里,没有迭代上限的 Agent 可能因为 LLM 陷入死循环而烧掉大量 token。

5. 本篇常见错误排查:401、local proxy failed 与 reading choices

跑 OpenClaw 时最容易撞上的几个报错,我按出现频率排了个序,每个都给出定位方法和修复步骤。

401 Unauthorized是最常见的。报错长这样:

Error: 401 {"error":{"type":"authentication_error","message":"invalid x-api-key"}}

原因通常是TAOTOKEN_API_KEY环境变量没设置,或者 Key 复制时带了空格。检查步骤:先在终端执行echo $TAOTOKEN_API_KEY,确认输出非空且没有首尾空格。如果为空,在.env文件里补上:

TAOTOKEN_API_KEY=sk-你的实际key

然后在代码里用dotenv加载。注意 OpenClaw 的createStreamFn读取的是process.env.TAOTOKEN_API_KEY,如果你用的是其他变量名,需要在配置里显式指定apiKey: process.env.YOUR_KEY_NAME。

local proxy failed这个报错通常出现在你配置了自定义 Base URL 但地址写错的时候:

Error: local proxy failed: ECONNREFUSED 127.0.0.1:8080

OpenClaw 默认会读HTTP_PROXY环境变量。如果你的终端里残留了代理设置,而代理服务没启动,就会报这个错。修复方法是清掉代理变量:

unset HTTP_PROXY HTTPS_PROXY ALL_PROXY

然后在配置里显式指定baseUrl: "https://taotoken.net/api",确保请求直连。如果你确实需要走网络中间层,把baseUrl改成对应的地址即可,但 OpenClaw 的streamFn不会自动读取系统代理,需要你在createStreamFn里传入fetch的自定义实现。

reading choices这个报错来自 OpenAI 格式的响应解析:

TypeError: Cannot read properties of undefined (reading 'choices')

原因是你的format配置和实际 API 返回的格式不匹配。如果你用的是 Anthropic 格式的接口,但配置里写了format: "openai",解析器会去找response.choices[0],而 Anthropic 返回的是response.content。修复方法:确认provider.format和接口实际格式一致。TaoToken 的/api端点同时支持两种格式,Anthropic 格式用format: "anthropic",OpenAI 格式用format: "openai"。

还有一个隐蔽的报错是OAuth token expired,出现在你用 Claude Code 的 OAuth 凭证去调 API 时:

Error: OAuth token expired, please re-authenticate

OpenClaw 本身不管理 OAuth,它只认 API Key。如果你从 Claude Code 迁移过来,需要把 OAuth 换成 API Key。在 TaoToken 控制台的 API Keys 页面生成一个新 Key,替换掉配置里的apiKey字段即可。

排查完这些错误后,建议把maxIterations和timeoutMs都设一个保守值,比如 20 和 60000。生产环境里,一个卡住的 Agent 循环比一个报错的 Agent 更危险,因为它会持续消耗 token 而不产出结果。

6. 从调研到落地:OpenClaw 工具决策的长期策略

把 OpenClaw 的 Agent Loop 跑通只是第一步。真正决定工具调用质量的是 Tool Use 的决策策略,而这取决于三个变量:工具描述的精度、循环边界的设置、以及错误恢复的粒度。

工具描述方面,我建议每个工具的description都包含「做什么、什么时候用、参数含义、返回什么」四个要素。比如read_file的描述不要只写「读取文件」,而是写「读取指定路径的文本文件内容,适用于查看代码、配置或日志。path 为相对路径,limit 为可选的最大行数,默认读取全部。返回文件内容字符串,文件不存在时返回错误信息。」这样 LLM 在多个相似工具之间做选择时,有足够的区分依据。

循环边界方面,maxIterations不要设太大。32 次迭代意味着最多 32 轮 LLM 调用,按每轮 2000 token 算,一次任务可能消耗 6 万 token。对于大多数编码任务,10 到 15 次迭代足够。如果任务复杂到需要更多轮次,说明你应该把它拆成多个子任务,而不是让一个 Agent 循环跑到底。

错误恢复方面,OpenClaw 的retryOnError只对网络错误生效,工具执行失败不会自动重试。你需要在工具层自己处理:比如exec返回非零退出码时,把 stderr 作为 tool_result 返回给 LLM,让 LLM 决定是修正命令还是换一个工具。这比 Agent 层盲目重试更有效,因为 LLM 能看到具体的错误信息。

如果你打算把 OpenClaw 用在长期运行的编码 Agent 场景,建议关注 TaoToken 的 Coding Plan,它针对高频工具调用做了配额优化,比按量计费更适合 Agent 这种 token 消耗大户。接入文档里有完整的 Base URL 和 Model ID 对照表,配置时直接复制即可。

最后说一个我踩过的坑:OpenClaw 的沙箱写入工具默认只允许写sandboxRoot下的路径,但exec工具不受这个限制。如果你让 LLM 执行echo "test" > /tmp/foo,它会成功写入沙箱外的文件。修复方法是在createExecTool的配置里加上cwd: sandboxRoot,并限制命令白名单。这个细节在官方文档里没有强调,但生产环境必须处理。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询