报错是这么冒出来的。一次请求返回两个 tool_calls,代码只回了第一个结果,第二次请求立刻 400,提示每个 tool_call_id 都必须有对应的 tool 消息,任务当场中断。

中间走过弯路,把「tool 消息里缺了 tool_call_id 字段」当成主因,改完现象没变。

这类报错背后常见的原因有几处,逐个排除就行。模型一次触发多个工具,需要逐个回填结果。少数情况下是回填顺序与 tool_calls 顺序不一致。

不容易想到的是,函数执行抛异常后直接跳过,少了对应条目。

遍历全部 tool_calls 逐个执行并收集结果

results = [(tc.id, run(tc.function.name, json.loads(tc.function.arguments))) for tc in msg.tool_calls]

再往下按 id 一一回填 role 为 tool 的消息

messages += [{'role':'tool','tool_call_id':i,'content':str(r)} for i, r in results]

再发起一次请求拿到最终答案

resp = client.chat.completions.create(model='gpt-4o-mini', messages=messages, tools=tools)

末了对执行失败的工具回填错误说明而不是空串

messages.append({'role':'tool','tool_call_id':i,'content':'error: timeout'})

怎么算真的好了,看的是同一批现象。第二轮返回 200,finish_reason 是 stop,回答里同时体现了两个工具的结果,日志中每个调用都有对应回填。

容易忽略的是,回填顺序不影响正确性,但 id 必须严格对应,写反了两个结果就会串到别的工具上,而且接口不会报错,只会给出错误答案。拖得越久,排查成本越高,因为现场状态会被后续操作覆盖。

作者 9823189