返回博客

故障排查指南

工具调用后返回 400:把 reasoning_content 发回去

为什么工具调用循环的第二次请求在思考模型上返回 400,一个追加完整消息的 Python 修复方法,以及何时可以跳过它。

作者:CheapTokens Team发布于 4 分钟阅读

封面:一个工具调用循环,其中 reasoning_content 从 assistant 消息传递到下一次请求
本页内容
  1. 第一次请求和第二次请求之间发生了什么
  2. 修复方法:按返回原样追加消息
  3. 常见元凶:手工重建的消息
  4. 何时不需要这样做
  5. 如果仍然返回 400

你的 agent 循环在第一次请求时正常,第二次却返回 400。在思考模型上,一个已有文档记载的原因是缺少字段:你回传的 assistant 消息丢失了它的 reasoning_content。如果原因正是这个,那么把 API 返回的整条消息原样追加回去即可修复。

DeepSeek 的思考模式指南(文档,查阅于 2026 年 11 月 8 日)用一句话给出了规则:当请求带有 tools 参数时,reasoning_content 必须在后续所有请求中完整回传,包括没有工具调用的轮次。否则 API 返回 400。同一页面还说明思考默认开启,effort 为 high。因此,你为非思考模型写的循环可能一行代码都没改就坏了。

第一次请求和第二次请求之间发生了什么#

在思考模式下,思维链通过 reasoning_content 返回,该字段与 content 处于同一层级。设想这个循环:

  1. 你发送问题和 tools 列表。
  2. 模型用 tool_calls 回答(旁边还有 reasoning_content)。
  3. 你运行工具并把所有内容发回去:历史记录、assistant 消息,以及一条带有结果的 tool 消息。

如果第 3 步请求中的 assistant 消息没有 reasoning_content,400 就会出现在该请求上。

工具循环的三个步骤:请求、带 reasoning_content 的 assistant 消息、第二次请求
字段必须传递的位置:从 assistant 消息进入下一次请求

DeepSeek 页面只针对这个工具调用场景记录了 400,且没有引用错误文本,所以当你检查自己是否有这个问题时,要看 HTTP 状态码,而不是某条具体消息。

修复方法:按返回原样追加消息#

最稳妥的做法是完全不重建 assistant 消息。从响应中取出它并整体追加。下面是一个完整循环,使用指向 CheapTokens 的 OpenAI Python SDK。天气工具是桩实现,因此你可以直接运行。

python
import json, os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.cheaptokens.dev/v1",
    api_key=os.environ["CHEAPTOKENS_API_KEY"],
)
MODEL = "deepseek-ai/DeepSeek-V4-Flash-0731"

tools = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "Current weather for a city",
        "parameters": {
            "type": "object",
            "properties": {"city": {"type": "string"}},
            "required": ["city"],
        },
    },
}]

def get_weather(city):
    return {"city": city, "temp_c": 7}  # stub

messages = [{"role": "user", "content": "Do I need a jacket in Oslo today?"}]

for _ in range(5):  # hard step limit
    resp = client.chat.completions.create(model=MODEL, messages=messages, tools=tools)
    msg = resp.choices[0].message
    messages.append(msg.model_dump(exclude_none=True))  # the whole message
    if not msg.tool_calls:
        print(msg.content)
        break
    for call in msg.tool_calls:
        args = json.loads(call.function.arguments)
        messages.append({
            "role": "tool",
            "tool_call_id": call.id,
            "content": json.dumps(get_weather(**args)),
        })

关键的一行是 messages.append(msg.model_dump(exclude_none=True))。SDK 会保留它不认识的字段,因此 API 放在消息上的任何内容,包括 reasoning_content,都会随它一起回传。最终答案也会被追加,这正是规则要求的:在第一次工具调用之后,后续每次请求都携带先前 assistant 轮次的推理内容。

工具调用机制本身(schema、tool_call_id、解析参数)见 Function calling in Python with an OpenAI-compatible API。base URL、key 和模型 id 在 CheapTokens 文档页上;这里使用的模型在 DeepSeek 页面上有描述:380K 上下文,每次回答最多 32,768 个输出 token,这为长思考加回答留出了空间。

常见元凶:手工重建的消息#

丢失该字段的一种常见方式是代码不追加响应对象,而是构建一个新的 dict。它看起来无害:role、content、tool calls。框架和包装器在用自己的格式存储历史时也会这样做。它们从未听说过的那个字段,正是会丢失的那个。

左:从 role、content、tool_calls 重建的 assistant 消息。右:按返回原样的消息,带有 reasoning_content
两条 assistant 消息的差别就是一个字段

比较

手工重建 vs 整体追加

手工重建(会坏)

python
messages.append({
    "role": "assistant",
    "content": msg.content,
    "tool_calls": [c.model_dump() for c in msg.tool_calls],
})

整体追加(可用)

python
messages.append(msg.model_dump(exclude_none=True))

如果你使用框架且无法改动那一行,检查它如何存储 assistant 轮次。经过只保留 content 和 tool_calls 的序列化器的历史记录有同样的 bug,只是低了一层。

何时不需要这样做#

并非每次思考模型调用都需要额外小心。对于没有工具的多轮聊天,DeepSeek 的指南说 reasoning_content 不必回传;如果你仍然发送它,它会被忽略,先前的推理不会加入上下文。因此普通聊天可以继续丢弃它,不会出问题。

选择你的情况

你属于哪种情况?

请求带有 tools,模型在思考

每一轮都追加完整的 assistant 消息,工具轮次和最终答案都一样。

普通聊天,没有 tools 参数

无需修复。你可以从历史中丢弃 reasoning_content,也可以发送它;它会被忽略。

框架替你构建历史

找到它序列化 assistant 消息的位置,确保 reasoning_content 能保留下来。如果无法保留,就用自己的循环调用 API 来处理工具部分。

需要记住的一个副作用:你重新发送的历史在每次请求中都是输入,因此长循环中的长推理会累积。思考 token 如何计数见 Reasoning effort and thinking tokens,请求增长超过模型限制时该怎么办见 Request too long: 400 vs 413。

如果仍然返回 400#

按顺序检查以下各项:

  • 在失败调用之前立即打印 messages。如果响应中有 reasoning_content,那么每条来自模型的 assistant 消息是否都有它?
  • 你是否在任何地方重建消息:辅助函数、历史类、json.dumps 并用固定键列表转回?
  • 每个 tool_call_id 对应的 tool 消息是否都存在,且 id 相同?
  • 真的是长度问题吗?超过上下文限制的请求是另一种 400,上文文章中有描述。

快速检查

模型请求工具后,你追加什么?

显示答案

答案: 按返回原样的完整 assistant 消息,包含 reasoning_content

tool_calls 正是你的工具消息所回应的内容,而请求中有工具时,推理必须随它们一起传递。

这就是全部修复:一次追加,不重建。把示例中的步骤上限加到你要发布的任何循环中,这样一次坏轮次只会花费五次请求,而不是五百次。