资讯动态

openclaw 中关于 OpenAI 模型适配层详细分析:工具调用处理

发布时间:2026/9/28 11:26:03 来源:尧图企业网站定制
1. 为什么 OpenAI 工具调用在 openclaw 里需要一层适配如果你正在用 openclaw 接 OpenAI 兼容模型大概率遇到过这种场景模型明明返回了工具调用但下游 Agent 拿到的参数是半截 JSON或者多个工具调用挤在同一个 delta 里被覆盖最后执行时报Unexpected end of JSON input。这不是模型的问题而是流式响应下tool_calls分片拼接没处理干净。openclaw 的模型适配层源码里对应src/agents/openai-transport-stream.ts就是干这件事的把 OpenAI 两种 API 形态——新版 Responses API 和经典 Chat Completions API——返回的工具调用统一转成内部的toolcall_start / toolcall_delta / toolcall_end事件流。上层 Agent 只认这套事件不关心底层是/v1/responses还是/v1/chat/completions。这篇文章面向三类人一是正在给 openclaw 接第三方 OpenAI 兼容通道的开发者二是想搞清楚 function calling 流式拼接原理的后端同学三是被delta.tool_calls增量解析坑过、想找一份可跟做配置的人。我会先讲适配层的转换逻辑再给一份可复制的配置骨架最后用一次完整的工具调用链路验证并说明怎么通过 TaoToken 统一 Key 和 API 通道接入省去多套凭证来回切。2. TaoToken 前置统一 Key 与 API 通道openclaw 适配层本身只负责协议转换它不关心你的 Key 从哪来。但实际开发里如果你同时接 OpenAI 官方、Azure 和几个兼容接口凭证管理会变成负担。我的做法是走 TaoToken 的统一通道一个 Key 覆盖多种模型入口适配层里只需要改baseURL和apiKey两个字段。TaoToken 官网入口https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentAPI 基地址不带 UTM直接用于配置https://taotoken.net/api需要提前准备的东西一个可用的 API Key在控制台创建https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewriteKey 管理页后续轮换、限额都在这里https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite接入文档适配层参数对照看这份https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite注意适配层配置里baseURL要写成https://taotoken.net/api不要带末尾斜杠否则部分 SDK 会拼出双斜杠路径导致 404。如果你只是想先验证模型能不能正常返回工具调用不想动 openclaw 代码可以直接在模型对话页试https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite3. 适配层配置骨架两种 API 模式怎么选openclaw 适配层暴露了两个工厂函数对应两种 API 模式。选哪个取决于你的通道兼容性工厂函数API 端点特点适用场景createOpenAIResponsesTransportStreamFn()/v1/responses事件流更细function_call_arguments.delta独立事件通道支持 Responses APIcreateOpenAICompletionsTransportStreamFn()/v1/chat/completions兼容性最广delta.tool_calls数组第三方兼容通道、老模型两者最终都走processResponsesStream或processOpenAICompletionsStream对外暴露的事件格式一致。下面是适配层的配置骨架我把它写成一份可直接放进 openclaw 配置目录的 JSON{ transport: openai-completions, baseURL: https://taotoken.net/api, apiKey: ${TAOTOKEN_API_KEY}, model: gpt-4o-mini, stream: true, toolChoice: auto, tools: [ { type: function, function: { name: exec, description: 执行本地命令并返回输出, parameters: { type: object, properties: { command: { type: string, description: 要执行的命令 } }, required: [command] } } } ] }关键字段说明transport决定走哪个 process 函数toolChoice设为auto让模型自己决定是否调用tools数组会被convertTools()转成 OpenAI 的 FunctionTool 格式。如果你走 Responses API把transport改成openai-responses其余字段结构不变适配层内部会调用convertResponsesTools()。环境变量建议单独放不要硬编码进配置文件export TAOTOKEN_API_KEYsk-你的key提示适配层读取apiKey时支持${VAR}占位符这样配置文件可以进版本库Key 留在本地环境。4. 流式响应下 tool_calls 分片拼接与参数增量解析这是适配层最容易出 bug 的地方。OpenAI 的流式工具调用不是一次性给你完整 JSON而是把arguments拆成多个 delta 分片推送。以 Completions API 为例一个exec调用的参数{command:dir}可能被拆成三段chunk 1: delta.tool_calls[0] { index: 0, id: call_abc, function: { name: exec, arguments: {\comm } } chunk 2: delta.tool_calls[0] { index: 0, function: { arguments: and\:\d } } chunk 3: delta.tool_calls[0] { index: 0, function: { arguments: ir\} } }适配层的处理逻辑是维护一个currentBlock每来一个 delta 就做三件事拼接partialArgs、用parseStreamingJson()尝试增量解析、推送toolcall_delta事件。核心代码结构如下if (choice.delta.tool_calls choice.delta.tool_calls.length 0) { for (const toolCall of choice.delta.tool_calls) { // 新工具调用开始id 出现即视为新块 if (!currentBlock || currentBlock.type ! toolCall || toolCall.id) { finishCurrentBlock(); currentBlock { type: toolCall, id: toolCall.id || , name: toolCall.function?.name || , arguments: {}, partialArgs: , }; output.content.push(currentBlock); stream.push({ type: toolcall_start, contentIndex: blockIndex(), partial: output }); } // 参数增量累积 if (toolCall.function?.arguments) { currentBlock.partialArgs toolCall.function.arguments; currentBlock.arguments parseStreamingJson(currentBlock.partialArgs); stream.push({ type: toolcall_delta, contentIndex: blockIndex(), delta: toolCall.function.arguments, partial: output, }); } } }parseStreamingJson()是容错解析的关键当partialArgs还是{comm这种半截 JSON 时它不会抛异常而是返回一个部分对象或空对象等后续分片补齐后再解析出完整结构。这样上层即使在中途读取arguments也不会崩。Responses API 的处理更细参数增量走独立的response.function_call_arguments.delta事件适配层在response.output_item.added时创建currentBlock在response.output_item.done时用parseStreamingJson(currentBlock.partialJson)做最终解析并推送toolcall_end。两种 API 的差异被这层完全吸收。5. 验证请求一次完整工具调用链路配置好之后用一段最小请求验证适配层是否正常工作。下面这个 Node 脚本直接打 TaoToken 的 Completions 端点模拟 openclaw 适配层的请求构建方式const res await fetch(https://taotoken.net/api/v1/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${process.env.TAOTOKEN_API_KEY}, }, body: JSON.stringify({ model: gpt-4o-mini, stream: true, tool_choice: auto, tools: [{ type: function, function: { name: exec, description: 执行本地命令, parameters: { type: object, properties: { command: { type: string } }, required: [command], }, }, }], messages: [{ role: user, content: 帮我列出当前目录文件 }], }), }); const reader res.body.getReader(); const decoder new TextDecoder(); let buffer ; while (true) { const { done, value } await reader.read(); if (done) break; buffer decoder.decode(value, { stream: true }); const lines buffer.split(\n); buffer lines.pop(); for (const line of lines) { if (!line.startsWith(data: )) continue; const payload line.slice(6); if (payload [DONE]) continue; const chunk JSON.parse(payload); const tc chunk.choices?.[0]?.delta?.tool_calls; if (tc) console.log(tool_call delta:, JSON.stringify(tc)); } }预期你会看到类似这样的分片输出arguments被拆成多段tool_call delta: [{index:0,id:call_x1,function:{name:exec,arguments:}}] tool_call delta: [{index:0,function:{arguments:{\comm}}] tool_call delta: [{index:0,function:{arguments:and\:\ls\}}}]适配层把这些分片拼完后会推送一个toolcall_end事件携带完整结构{ type: toolcall_end, toolCall: { type: toolCall, id: call_x1, name: exec, arguments: { command: ls } } }拿到这个事件后pi-embedded-subscribe.handlers.tools.ts会触发实际执行执行结果通过emitToolResultOutput()转成function_call_outputResponses API或tool消息Completions API作为下一轮请求的输入回填给模型。整条链路就闭合了。6. 本篇常见错排查报错一Unexpected end of JSON input原因通常是parseStreamingJson()没做容错直接对半截 JSON 调了JSON.parse。检查你的适配层是否在toolcall_delta阶段就尝试解析完整对象。正确做法是增量阶段只累积字符串toolcall_end时才做最终解析。报错二多个工具调用互相覆盖当模型一轮返回两个tool_calls时delta.tool_calls数组里会有index: 0和index: 1。如果适配层只维护一个currentBlock第二个会覆盖第一个。修复方式是按index建 map或者像 openclaw 那样在检测到新id时先finishCurrentBlock()。报错三toolcall_start事件重复推送Completions API 的第一个 delta 里id和name同时出现后续 delta 只有arguments。如果你的判断条件是toolCall.id存在就新建块那第一个 delta 之后每个带id的都会误判。openclaw 的判断是!currentBlock || currentBlock.type ! toolCall || toolCall.id注意toolCall.id只在真正新调用时才有值。报错四请求 404 或路径拼接错误baseURL末尾带了斜杠SDK 又拼了/v1/chat/completions结果变成//v1/...。统一写成https://taotoken.net/api不要带尾斜杠。报错五工具结果回填后模型不继续检查toolResult的格式是否匹配当前 API 模式。Responses API 要转成function_call_outputCompletions API 要转成role: tool的消息且必须带tool_call_id。格式不对模型会当成普通文本忽略。7. 接入与后续按场景选通道排障和接入阶段重点是把 Key 和文档对齐建议直接看 API Keys 管理页和接入文档参数对照着改适配层配置API Keyshttps://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite如果你只是想快速验证某个模型返回的工具调用结构对不对不用写代码在模型对话页发一句带工具意图的 prompt 就能看到原始响应https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite长期跑编码类 Agent、需要稳定长连接和额度管理的走 Coding Plan 更合适省得每次手动换 Keyhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite最后说个我踩过的坑适配层的parseStreamingJson()一定要用 try-catch 包住并且对空字符串返回{}而不是null。因为toolcall_delta阶段上层可能随时读取arguments做 UI 渲染返回null会让渲染层直接报错。这个细节在源码里体现为stringifyJsonLike(item.arguments, {})的默认值处理抄配置的时候别漏了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑