ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Codex 工具调用报 400?从 Responses API 的 call_id 排查到 TaoToken 配置骨架

Codex 工具调用报 400?从 Responses API 的 call_id 排查到 TaoToken 配置骨架 1. Codex 工具调用 400 的典型现场你在终端里敲下 Codex 的命令模型正常返回了 function_callPython 侧也老老实实把工具结果拼回去结果第二次请求直接甩回来一个 400No tool call found for function call output with call_id ...这条报错信息量其实不小。它说的是当前处理function_call_output的服务端在本次请求关联的上下文里找不到你传上来的那个call_id对应的工具调用。换句话说不是模型不会用工具而是你回传结果时那个“配对钥匙”对不上锁。Codex 本身支持自定义 provider 和 Base URL当前 provider 协议走的是 Responses。这意味着你完全可以把 Codex 指向一个兼容 Responses API 的入口比如 TaoToken 提供的统一通道。但兼容层是否完整实现了工具语义和状态续接需要你自己实测。400 出现在 Codex 终端不代表错误一定由 Codex 或原生服务生成很可能就卡在兼容网关的字段转换上。这篇面向本地调试 AI 工具的开发者目标很具体把 400 定位到具体字段。我会先讲清四种 ID 的区别再给出可复制的config.toml/settings.json骨架然后用两个独立的 Python 协议验证模板跑一次最小请求最后按故障矩阵逐项排查。适合已经能跑通普通对话、但一上工具调用就翻车的人。2. 先分清四种 ID再谈 call_id 配对很多人一看到call_id就懵因为它和item.id、response.id长得像用途却完全不同。我试过把item.id当成call_id回传结果就是那条 400。先把这张表刻进脑子标识来源用途item.id模型返回的 output Item标识一条输出 Itemitem.call_id模型返回的function_call将工具结果与具体工具调用配对response.id一次 Responses 响应供previous_response_id续接响应链Conversation IDConversation 对象对应 API 中的持久会话关键结论只有一句回传function_call_output时必须使用对应function_call的item.call_id。它不是函数名不是数组下标不是item.id更不能由客户端自己重新生成。官方示例同样是把response.output加回输入并用item.call_id回传结果。注意call_id是服务端生成的配对凭证。你自己造一个 UUID 塞进去服务端一定找不到对应的工具调用400 就是这么来的。2.1 为什么 Codex 场景更容易踩这个坑Codex 走 Responses 协议工具调用链是“模型出 function_call → 你执行 → 回传 function_call_output → 模型继续”。中间只要有一环把 ID 换了、丢了、或者按数组下标去配对链路就断。尤其是并发工具调用时完成顺序和返回顺序不一致如果你用下标配对必然错位。3. TaoToken 前置统一 Key 与 API 通道在动手改配置之前先把入口固定下来。TaoToken 提供统一的 Key 和 API 通道Codex 这类支持自定义 provider 的工具可以直接把 Base URL 指过来。你需要准备的东西一个可用的 API Key在控制台的 API Keys 页面创建https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewriteBase URL 统一用https://taotoken.net/api注意这个地址不带任何查询参数目标模型 ID选一个明确支持 Responses function calling 的模型不要凭感觉填如果你还没决定用哪个模型可以先去模型对话页面试一下工具调用是否正常https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite接入文档在这里字段细节以文档为准https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite提示不同兼容端点支持的字段并不相同。示例里我用环境变量传模型 ID不提供“万能模型 ID”。如果目标端点不支持指定函数的tool_choice、strict或某种状态路径先记录为兼容性差异再按文档建立单独基线。4. 可复制配置骨架config.toml 与 settings.jsonCodex 的配置分两层一层是 provider 和模型一层是本地工具行为。下面这份config.toml骨架可以直接改重点是base_url和wire_api两个字段。# ~/.codex/config.toml model your-responses-model-id model_provider taotoken [model_providers.taotoken] name TaoToken base_url https://taotoken.net/api wire_api responses env_key TAOTOKEN_API_KEY [profiles.default] model_provider taotoken model your-responses-model-id对应的settings.json骨架用于本地工具与超时控制{ provider: taotoken, base_url: https://taotoken.net/api, wire_api: responses, api_key_env: TAOTOKEN_API_KEY, request_timeout_ms: 60000, max_tool_rounds: 4, log_level: info }环境变量单独设置不要把 Key 写进配置文件export TAOTOKEN_API_KEYsk-你的key export OPENAI_BASE_URLhttps://taotoken.net/api export TEST_MODEL_IDyour-responses-model-idwire_api responses这一行很关键。Codex 当前 provider 协议是 Responses如果你写成别的协议工具调用的字段结构就对不上400 会以各种奇怪的形式出现。5. Python 侧最小验证两条独立路径配置只是骨架真正定位问题要靠可复现的协议实验。下面两个模板相互独立一个显式重放完整 Items一个用previous_response_id。每次只跑一个隔离变量。5.1 公共辅助代码import hashlib import json import os from importlib.metadata import version from openai import OpenAI MODEL os.environ[TEST_MODEL_ID] MAX_TOOL_ROUNDS 4 client OpenAI( api_keyos.environ[TAOTOKEN_API_KEY], base_urlos.environ[OPENAI_BASE_URL], ) tools [ { type: function, name: get_status, description: Return the current status of a task., parameters: { type: object, properties: {task_id: {type: string}}, required: [task_id], additionalProperties: False, }, strict: True, } ] forced_tool {type: function, name: get_status} def short_hash(value): if not value: return None return hashlib.sha256(value.encode(utf-8)).hexdigest()[:12] def log_response(label, response): print({ label: label, sdk_version: version(openai), response_id_hash: short_hash(response.id), item_types: [item.type for item in response.output], call_id_hashes: [ short_hash(item.call_id) for item in response.output if item.type function_call ], }) def create_response(label, **request): try: return client.responses.create(**request) except Exception as exc: print({label: label, error: api_request_failed, type: type(exc).__name__}) raise def run_tool(name, arguments): if name ! get_status: raise ValueError(funknown tool: {name}) return {task_id: arguments[task_id], status: running} def build_tool_outputs(response): outputs [] for item in response.output: if item.type ! function_call: continue try: arguments json.loads(item.arguments) except json.JSONDecodeError as exc: print({ error: invalid_tool_arguments_json, response_id_hash: short_hash(response.id), tool_name: item.name, call_id_hash: short_hash(item.call_id), }) raise RuntimeError(工具参数不是合法 JSON) from exc try: result run_tool(item.name, arguments) except Exception as exc: result { ok: False, error_type: type(exc).__name__, message: tool execution failed, } outputs.append({ type: function_call_output, call_id: item.call_id, output: json.dumps(result, ensure_asciiFalse), }) return outputs日志只保留 SDK 版本、Item 类型序列和 ID 短哈希不打印原始 ID、工具参数或结果。真实项目里凭证走环境变量或密钥管理别写进代码。5.2 路径一显式重放完整 Itemsdef verify_with_explicit_replay(): history [{role: user, content: 查询任务 demo-001 的状态}] for round_index in range(MAX_TOOL_ROUNDS): response create_response( fexplicit_round_{round_index 1}, modelMODEL, instructions根据工具结果回答需要时可以继续调用工具。, toolstools, tool_choiceforced_tool if round_index 0 else auto, inputhistory, ) log_response(fexplicit_round_{round_index 1}, response) tool_outputs build_tool_outputs(response) if not tool_outputs: if round_index 0: raise RuntimeError(首轮未产生 function_call协议验证无效) if not response.output_text: raise RuntimeError(工具链结束但没有最终文本) return response.output_text history.extend(response.output) history.extend(tool_outputs) raise RuntimeError(超过最大工具轮数主动终止) print(verify_with_explicit_replay())这里不能只挑出function_call。推理模型首轮还可能包含下一轮需要的 reasoning Items必须保留完整 output。5.3 路径二previous_response_id 续接def verify_with_previous_response_id(): previous_id None pending_input [{role: user, content: 查询任务 demo-001 的状态}] for round_index in range(MAX_TOOL_ROUNDS): request { model: MODEL, instructions: 根据工具结果回答需要时可以继续调用工具。, tools: tools, tool_choice: forced_tool if round_index 0 else auto, input: pending_input, } if previous_id is not None: request[previous_response_id] previous_id response create_response(fprevious_id_round_{round_index 1}, **request) log_response(fprevious_id_round_{round_index 1}, response) tool_outputs build_tool_outputs(response) if not tool_outputs: if round_index 0: raise RuntimeError(首轮未产生 function_call协议验证无效) if not response.output_text: raise RuntimeError(工具链结束但没有最终文本) return response.output_text previous_id response.id pending_input tool_outputs raise RuntimeError(超过最大工具轮数主动终止) print(verify_with_previous_response_id())两条路径的状态来源不同显式重放由应用携带完整 Itemsprevious_response_id引用服务端保存的响应链。生产实现除非有明确协议依据和端到端测试不要在引用previous_response_id的同时重复提交同一份完整历史以免引入重复上下文。6. 验证请求与成功结果长什么样跑通之后日志应该呈现这样的结构首轮item_types里出现function_callcall_id_hashes有一个短哈希第二轮请求带上function_call_outputitem_types里出现messageoutput_text有内容。{ label: explicit_round_1, sdk_version: 1.x.x, response_id_hash: a1b2c3d4e5f6, item_types: [function_call], call_id_hashes: [9f8e7d6c5b4a] }第二轮{ label: explicit_round_2, sdk_version: 1.x.x, response_id_hash: f6e5d4c3b2a1, item_types: [message], call_id_hashes: [] }如果第二轮直接抛 400且错误里带call_id对照日志里的短哈希看回传的call_id是否和首轮function_call的哈希一致。不一致就是配对错了一致还报错就往状态续接和网关转换方向查。7. 仍然报 400按故障矩阵逐项排查位置典型问题验证动作ID 配对把item.id、旧链 ID 或自建 ID 当成call_id按短哈希建立 function call 与 output 一一对应参数解析item.arguments不是合法 JSON单独捕获JSONDecodeError只记结构元数据状态续接漏传必要 Items或previous_response_id指错链分别运行两个独立模板不共享可变输入协议转换call_id与另一协议的工具 ID 映射错误或丢 reasoning Item对照网关入口、出口的脱敏 Item 序列并发工具完成顺序与返回顺序不同按数组下标配对每个任务携带自己的call_id以 ID 为键汇总自动重试新响应链收到旧链工具结果记录重试序号和 response 关系隔离后逐项恢复工具执行超时或业务失败被误当成协议失败回传受控错误结果或按策略明确终止多上游网关或上游状态无法跨节点恢复前述项目通过后再做固定与受控切换实验固定上游成功、切换上游失败只能增强假设不能直接定论。还要定位究竟是网关映射、上游 Response ID 作用域还是转换链丢项。无法控制路由时把结论保留为待验证别在生产流量上强制切换。7.1 最终验收清单SDK、模型、Base URL、完整 endpoint 和状态路径已记录协议测试使用目标端点支持的确定性tool_choice每个function_call_output.call_id均来自对应function_call参数 JSON 失败、工具失败和 API 失败能分开识别显式重放与previous_response_id在独立输入中分别验证若使用store: false或 ZDR已核对 encrypted reasoning 支持网关转换前后的 Item 类型和 ID 关系能对应并发、重试和连续工具调用均在最大轮数保护下回归模型最终消费工具结果并生成有效回答而不只是第二次请求返回 200日志、截图和公开文章不含真实凭证、完整 ID、业务参数或工具输出8. 长期编码与 Agent 场景的接入建议如果你只是偶尔调试工具调用上面两个 Python 模板够用了。但如果你打算把 Codex 长期挂在编码或 Agent 工作流里反复手动拼call_id不现实建议走 Coding Plan 把通道和额度固定下来https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewriteClaude Code 这类 Anthropic 协议的工具接入位置单独看这里https://taotoken.net/claude-code?utm_sourcetaotoken_aicg_blog_endutm_contentclaude_codeutm_campaignrewrite看到No tool call found ... call_id先把脚本变成可重复的协议实验固定版本和端点强制首轮工具调用分别验证两种状态路径再对照网关转换、并发与重试。只有完成这些步骤后才有条件讨论路由架构否则改负载均衡只是把一个可验证的配对问题换成新的猜测。
返回列表