从一次“模型像没收到工具结果”的排查说起
在 TaoToken 通道下用 gpt-4o 跑工具调用,最容易踩的坑不是模型不会调工具,而是回传结果时tool_call_id对不上。现象很典型:第一轮tool_calls正常返回,你本地也执行了函数,把role: "tool"的消息追加进messages再发第二轮,结果模型像完全没看到工具结果一样,要么重复调用同一个工具,要么直接编一个答案。本文就按排障视角走一遍:先用 OpenAI SDK 复现tool_calls后tool_call_id对不上的场景,再去 TaoToken 官网 创建 Key,把 SDK 的 Base URL 填成https://taotoken.net/api,让请求走 TaoToken 通道后检查返回的tool_calls.id,最后把 gpt-4o 的工具调用闭环配通。需要先明确一点:TaoToken 只负责给你 Key 和 Base URL,它不替代tool_call_id的关联逻辑——关联这件事,永远是你代码里的责任。
一、原问题与场景:tool_call_id 对不上到底错在哪
工具调用的基本工作流是“请求-执行-回传-续答”。模型第一轮返回的 assistant 消息里带着tool_calls数组,每个元素有一个id,比如call_abc123。外部系统执行完函数后,必须构造一条role: "tool"的消息,把tool_call_id设成同一个call_abc123,再追加回messages。模型靠这个 id 判断“这个结果对应我刚才的哪次调用”。
对不上的常见来源有三类:
第一类是手写 id。有人图省事,回传时自己编一个"tool_call_id": "1"或者干脆不写,模型自然无法归属。id 必须从assistant_msg.tool_calls[i].id原样取。
第二类是并行调用时顺序错位。模型一次返回三个tool_calls,你用asyncio.gather并发执行,结果回来顺序和tool_calls顺序不一致,却用zip硬配,导致 A 的结果配了 B 的 id。正确做法是执行时把tc.id一起带进任务,回传时按 id 配对,而不是按位置配对。
第三类是assistant 消息没有原样追加。有人只把content追加回去,丢掉了tool_calls字段,或者自己重新拼了一条 assistant 消息。模型看不到自己发过的调用请求,tool_call_id就失去了参照物。
这三类问题的共同表现都是:回传后模型像没收到工具结果。排障的第一步,就是先把请求打到能稳定返回tool_calls的通道上,观察真实的 id 长什么样。
二、TaoToken 前置:拿 Key、填 Base URL
在复现之前,先把通道准备好。打开 TaoToken 官网,注册后在控制台创建 API Key。如果你需要直接进创建页,走这个 deep link:创建 API Key。拿到形如YOUR_API_KEY的密钥后,记住两个地址:
- Base URL:
https://taotoken.net/api(注意不要加 UTM 参数,SDK 里填这个纯净地址) - API Key:
YOUR_API_KEY
TaoToken 在这里的角色是通道:它把你的请求转发到 gpt-4o,并把模型返回的tool_calls(含id)原样交回给你。它不会帮你生成 id,也不会帮你做关联。所以配通通道只是第一步,真正的排障重点在下一节的代码里。
如果你更习惯用 CLI 管理,也可以装 TaoToken 的命令行工具:
npm i -g @taotoken/taotoken taotoken cc -k YOUR_API_KEY -u https://taotoken.net/api -m gpt-4o不过本文的排障以 SDK 为主,CLI 只作为备选。
三、可复制配置:用 OpenAI SDK 复现并修正 tool_call_id
下面这段代码可以直接跑。它做了三件事:走 TaoToken 通道请求 gpt-4o、打印真实的tool_calls.id、用正确的 id 回传工具结果。
import json from openai import OpenAI client = OpenAI( api_key="YOUR_API_KEY", base_url="https://taotoken.net/api", ) def get_weather(city: str) -> str: data = {"北京": 32, "上海": 28, "广州": 30} return json.dumps({"city": city, "temperature": data.get(city, 25)}) tools = [ { "type": "function", "function": { "name": "get_weather", "description": "查询指定城市的天气温度", "parameters": { "type": "object", "properties": { "city": {"type": "string", "description": "城市名称"} }, "required": ["city"], }, }, } ] messages = [{"role": "user", "content": "北京和上海今天多少度?"}] # 第一轮:模型返回 tool_calls response = client.chat.completions.create( model="gpt-4o", messages=messages, tools=tools, ) assistant_msg = response.choices[0].message messages.append(assistant_msg) # 关键:打印真实 id,确认通道返回的 tool_calls.id if assistant_msg.tool_calls: for tc in assistant_msg.tool_calls: print("tool_call_id =", tc.id, "| name =", tc.function.name) args = json.loads(tc.function.arguments) result = get_weather(args["city"]) # 回传时必须用 tc.id,不能自己编 messages.append({ "role": "tool", "tool_call_id": tc.id, "content": result, }) # 第二轮:模型基于工具结果续答 final_response = client.chat.completions.create( model="gpt-4o", messages=messages, tools=tools, ) print(final_response.choices[0].message.content)这段代码里有两个必须守住的动作:messages.append(assistant_msg)要原样追加,tool_call_id要取tc.id。只要这两步对了,gpt-4o 就能正确判断结果归属。
并行调用时,把 id 一起带进任务,回传时按 id 配对:
import asyncio import json from openai import AsyncOpenAI client = AsyncOpenAI( api_key="YOUR_API_KEY", base_url="https://taotoken.net/api", ) async def call_tool(tool_call): name = tool_call.function.name args = json.loads(tool_call.function.arguments) if name == "get_weather": data = {"北京": 32, "上海": 28} return tool_call.id, json.dumps({"city": args["city"], "temperature": data.get(args["city"], 25)}) return tool_call.id, json.dumps({"error": "unknown tool"}) async def main(): messages = [{"role": "user", "content": "查一下北京天气和上海天气"}] tools = [ {"type": "function", "function": {"name": "get_weather", "description": "查天气", "parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}}}, ] resp = await client.chat.completions.create(model="gpt-4o", messages=messages, tools=tools) assistant_msg = resp.choices[0].message messages.append(assistant_msg) if assistant_msg.tool_calls: # 并发执行,每个任务返回 (id, result) results = await asyncio.gather(*[call_tool(tc) for tc in assistant_msg.tool_calls]) for tool_call_id, result in results: messages.append({"role": "tool", "tool_call_id": tool_call_id, "content": result}) final = await client.chat.completions.create(model="gpt-4o", messages=messages, tools=tools) print(final.choices[0].message.content) asyncio.run(main())注意这里call_tool返回的是(tool_call_id, result)元组,而不是只返回结果。这样即使并发完成顺序打乱,回传时也能按 id 精确配对,不会出现 A 的结果配 B 的 id。
四、验证请求与成功结果
跑完上面的代码,你应该看到两类输出。
第一类是tool_call_id的打印,形如:
tool_call_id = call_xxxxxxxx | name = get_weather tool_call_id = call_yyyyyyyy | name = get_weather这说明 TaoToken 通道正常返回了带 id 的tool_calls。如果这里打印为空,说明模型这一轮没触发工具调用,可以换一个更明确的用户提问,或者检查tools定义是否被正确传入。
第二类是第二轮模型的最终回答,应该是一句综合了两个城市温度的自然语言,比如“北京今天 32 度,上海 28 度”。如果模型重复调用工具、或者回答里没有用到工具结果,基本可以判定是tool_call_id关联出了问题,回到上一节检查messages.append和 id 取值。
想进一步确认通道行为,可以到 模型对话 页面手动发一条带工具的请求,观察返回结构里的tool_calls.id字段。这个页面适合做单点验证,不用写代码就能看到原始返回。
五、本篇常见错排查
错误一:tool_call_id用了自增值。表现是模型像没收到结果。修正:永远从assistant_msg.tool_calls[i].id取。
错误二:并行调用按位置配对。表现是偶发的结果错位,单次调用正常、并发时出错。修正:让每个任务返回自己的 id,回传时按 id 配对,不要用zip硬配。
错误三:assistant 消息被重建。表现是模型丢失调用上下文。修正:messages.append(assistant_msg)原样追加,不要只取content。
错误四:role: "tool"消息缺少tool_call_id字段。有些 SDK 版本会直接报参数错误,有些则静默丢弃。修正:每条 tool 消息都必须带tool_call_id。
错误五:Base URL 填错。如果填了带 UTM 的地址,或者漏了/api,请求可能打到错误端点,返回结构异常。修正:SDK 里统一用https://taotoken.net/api。
错误六:工具执行失败后直接抛异常中断。表现是对话卡死。修正:把错误信息作为content回传,让模型自行判断下一步,而不是直接中断循环。
排查顺序建议:先确认tool_calls.id有值,再确认回传 id 与之一致,最后确认 assistant 消息完整。三步都过,闭环基本就通了。
六、语义一致的下一步
工具调用闭环配通之后,如果你要长期跑编码类 Agent,反复手动管理 Key 和模型切换会比较累,可以了解 Coding Plan,把常用模型和额度统一管理。接入过程中如果遇到鉴权、Base URL 或 settings 相关问题,直接查 接入文档,里面覆盖了 SDK、CLI 和常见客户端的配置方式。需要新建或轮换密钥时,回到 API Keys 页面操作即可。记住本文的核心:TaoToken 负责通道,tool_call_id的关联永远在你自己的代码里。