这个问题在线上不会自己消失,按现象、配置、日志三步走基本能定位。 带 tools 的请求返回后直接执行 message.content.strip(),抛 AttributeError: ‘NoneType’ object has no attribute ‘strip’,打印原始响应发现 content 是 null,finish_reason 是 tool_calls。
拖得越久,排查成本越高,因为现场状态会被后续操作覆盖。
先分清是哪一类原因
- 模型决定调用工具时正文本来就是空的,属于正常行为
- 代码只判断了 content,没有读 tool_calls 数组
- 流式拼装时增量里没有 content 字段,被当成空串覆盖
- 不同兼容厂商对空正文的表示不一致,有的是空字符串
按顺序处理
1. 先按 finish_reason 分支处理,不直接取正文
if resp.choices[0].finish_reason == 'tool_calls': handle(resp.choices[0].message.tool_calls)
2. 从 tool_calls 里取出函数名和参数 JSON
python -c "import json;print(json.loads(tc.function.arguments))"
3. 把执行结果以 role=tool 追加回消息列表
messages.append({'role':'tool','tool_call_id':tc.id,'content':result})
4. 再请求一次拿到自然语言答案
resp = client.chat.completions.create(model='gpt-4o-mini', messages=messages, tools=tools)
怎么确认已经恢复
第二轮请求返回 200,finish_reason 变成 stop,message.content 是自然语言答案而不是 null,工具结果也出现在回答里。
容易忽略的地方
并行工具调用会一次返回多个 tool_calls,每个 id 都必须回填对应的 tool 消息,少一个就会报缺少工具响应,而且错误位置在下一轮而不是当轮。 命令执行完不要只看有没有报错,还要看输出里的计数和状态码是否符合预期。