新人 免费注册,送 10 次调用,最高 $1,无需绑卡。

LLM 智能体排障:一条追踪中的 3 种故障,其中一种状态码为 200

通过标签查找某个用户的一次智能体运行记录,并阅读该次运行的追踪,逐项定位三种失败情况:请求返回 404 后发生重试;回复虽然返回状态码 200,内容却被截断;第二轮对话则出现 400 错误。区分重试、成功状态下的回复截断与第二轮请求失败,明确各类故障出现的位置。

我们的推荐

实时价格
模型 结论 价格
GPT-6 Luna 当保存的历史记录丢失了一次工具调用时,在 0.5 s 内以 400 拒绝了对话的第二轮,且未收费。 演示中的主智能体 低至 $0.1/百万
DeepSeek V4.1 Flash 将 max_tokens 设为 40 时,连续三次调用均返回状态码 200,且输出恰好 40 个 token,没有可用回复。提高上限并关闭推理后,一次调用用 49 个 token 给出了回答。 演示中的子智能体 低至 $0.3/百万
目录
  1. 如何找到某个用户的一次运行?
  2. 失败调用及其重试在追踪中是什么样?
  3. 为什么每次调用都成功,智能体却没有返回内容?
  4. 为什么第一轮正常,第二轮却失败?
  5. 每个智能体应该记录哪些信息,才能这样排障?
  6. 常见问题
  7. 如何在生产环境中排查 LLM 智能体故障?
  8. 为什么 LLM 返回状态码 200,却没有回复内容?
  9. “No tool call found for function call output”是什么原因?
  10. LLM 调用失败后应该重试吗?
  11. 延伸阅读

“昨天下午,助手给的回答根本没用。”大多数智能体故障就是这样报上来的:只有一个用户、大致的时间,没有请求 ID。要找到那次运行,需要给请求打标签;要弄清哪里出了问题,需要查看追踪。故障通常只出在多次调用中的某一次,而且往往看起来不像报错。本文从用户投诉入手,一步步定位出问题的调用,再用一个小型客服智能体复现三种故障,展示它们在 Synthorai 控制台中的表现。

TL;DR

  • 每个请求都带上 _user 标签,就能在 Traces 列表中通过一次搜索找到某个用户的智能体运行记录。
  • 重试沿用同一个追踪 ID 时,失败调用和重试会并排显示。示例中的 404 耗时 95 ms,费用为 $0。
  • 推理模型设置 max_tokens=40 后,连续三次调用都返回状态码 200,输出 40 个 token,却没有回复。
  • 保存的历史记录丢失了工具调用,导致第二轮对话返回 400,未产生费用。

本文基于追踪 LLM 智能体,该文介绍了这里使用的请求头。按照其中的步骤配置示例环境(Python 3.10 或更高版本,并激活 virtualenv),然后在 github.com/synthorai-io/use-cases 的同一目录下执行:

cd use-cases/agent-tracing
python run.py --fail                    # a failed call with a retry
python incidents.py cut-off bad-history

本文的数据来自 2026 年 10 月 11 日各执行一次的结果。

追踪中的表现含义示例
一个带状态码的斜线填充标记,后面跟着一次类似的调用调用失败后进行了重试run.py --fail
同一个 span 重复出现,状态码全是 200,Output 列等于设置的上限回复尚未形成可用内容就被截断cut-off
会话第二轮出现 400,0 个 token,$0代码构造的请求无效bad-history

如何找到某个用户的一次运行?

通过请求携带的标签查找。如果每个请求的 X-Synthorai-Metadata 都包含 _user 键,只需一个筛选条件就能找到该用户的请求:

labels = {"feature": "support-agent", "release": "v2", "_user": "u_1002"}
  1. 打开 Traces,在搜索框中输入 metadata._user = u_1002,按 Enter。调整时间范围,覆盖用户投诉的时段。
  2. 查看列表。此时只显示该用户的会话及其追踪,每条记录都包含调用数、错误数、耗时和费用。Calls、Errors、Duration 和 Cost 列均支持排序。优先查看有错误,或调用次数明显多于相邻记录的追踪。
  3. 打开这条追踪。它会展示整次运行;“Open session”可查看前后的对话,“See the calls in Logs”可列出其中的各个请求。

Traces 列表的搜索框中输入了 metadata._user = u_1002。图中显示前七行,包括会话及其内部的追踪,每行都有调用数、错误数、耗时和费用

如果要搜索单个请求而不是整次运行,Logs 的筛选框也支持同样的标签,还可以使用 status(error、success、4xx、5xx 或具体状态码)、latency、cost、model、session 和 trace。

没有标签时,就从现有信息查起:把自己日志中的追踪 ID 填入 Traces 搜索框;把响应头中的请求 ID 作为 request_id 填入 Logs 筛选条件。因此,应将 x-request-id 响应头与你自己的用户 ID、对话 ID 一起写入日志。

_user 应使用内部用户 ID,不要使用邮箱地址或姓名。标签会随请求一起存储。

失败调用及其重试在追踪中是什么样?

失败调用会显示为带状态码的斜线填充标记,旁边是替代它的重试调用。在追踪指南中,三个并行检查里有一个请求了不存在的模型 ID。追踪中的 policy-check 显示为标有 404 的 95 ms 标记,旁边的 policy-check.retry 则是正常条形。追踪摘要显示“6 model calls, 1 failed”,会话页面列出“1 × 404 Not found”。失败调用的费用为 $0。

能看清这段过程,靠的是代码中的两个选择。首先,重试沿用同一个追踪 ID,失败和重试便会出现在同一张图中。其次,重试使用独立的 span 名称,方便区分两次尝试。如果重试新建了追踪,看起来就像两次互不相关的运行,其中一次莫名失败了。

追踪中出现失败,不一定就有问题。这次运行已经恢复,客户也拿到了回答。追踪能告诉你故障带来的代价:这里是 95 ms,没有额外费用。有了这些信息,才能判断重试是否足够,还是需要修复根因。

为什么每次调用都成功,智能体却没有返回内容?

因为状态码 200 只代表请求已完成,并不保证有回复。这个示例让使用 DeepSeek V4.1 Flash 的子智能体判断一条消息涉及哪些政策主题,但仍沿用了先前更简单的 prompt 所设置的 max_tokens=40。与示例中的其他子智能体调用不同,这个有问题的检查保留了模型默认的推理设置(代码片段中的 think)。模型先推理再回答,推理用完了 40 个 token,最终返回空回复,finish_reason 为 length。代码看到空回复便继续尝试,一共调用了三次:

for attempt in range(1, 4):
    resp = await check(run, f"policy-check-{attempt}", prompt, think=variant == "before", max_tokens=limit)
    choice = resp.choices[0]
    if choice.finish_reason != "length" and (choice.message.content or "").strip():
        break                                    # a usable reply; otherwise try again

追踪中有三次调用,名称分别为 policy-check-1、policy-check-2 和 policy-check-3,均显示为耗时 1.0 至 1.4 s 的正常条形,没有错误标记,Output 列均为 40。policy-check-3 的详情面板显示状态码 200、40 个输出 token,下方显示 Reasoning 40、100%

瀑布图中没有任何失败标记。线索在于重复出现的 span 和 Output 列:40、40、40,刚好都等于上限。选中一次调用后,右侧面板会进一步确认:状态码 200、40 个输出 token,下方一行是“Reasoning 40 · 100%”。

输出 token 数恰好等于设置的上限,是回复被截断的典型表现。修复版本将上限提高到 2,000,并关闭了这个检查不需要的推理。随后一次调用就用 49 个输出 token 给出了回答,追踪中的三根条形变成了一根。只提高上限也能停止重试,但那次调用的推理消耗了 1,876 个输出 token,耗时 22 s。

在自己的智能体中:

  • 检查每次调用的 finish_reason。length 表示回复被截断。即使状态码为 200,也要在自己的日志中将它记为失败。
  • 为推理和回复合计预留 max_tokens。推理模型的上限同时覆盖两者。如果只按可见回答设置上限,可能根本没有空间输出回答。
  • 不要用相同的上限重试被截断的回复。第二次尝试仍会碰到同一个限制。提高上限、降低推理强度,或同时调整两者。

为什么第一轮正常,第二轮却失败?

通常是两轮之间保存的数据出了问题。模型请求会携带完整对话,因此历史记录的存储或重建逻辑有 bug 时,往往到第二轮才会暴露。这个示例的第一轮直接使用内存中的消息:GPT-6 Luna 发起工具调用、拿到结果并给出回答,一切正常。接着代码保存这一轮,却只把助手消息保存为文本,丢掉了工具调用,同时又把工具结果保存在它旁边:

# What the app stores for the next turn.
saved = msg.model_dump(exclude_none=True)
if variant == "before":
    saved.pop("tool_calls", None)                # the bug: the assistant message is saved as text only
    saved["content"] = saved.get("content") or ""
history = [saved, *results, {"role": "assistant", "content": answer.choices[0].message.content or ""}]

客户再次发消息时,第二轮请求由保存的历史记录构造。请求中有工具结果,但历史记录中已不存在对应的调用,因此请求被拒绝并返回以下错误(“function call output”是提供商对工具结果的称呼):

400 No tool call found for function call output with call_id call_Nqc1zUFvwLLuF5oGMEE9WheQ.

会话页面显示 3 次调用、1 个错误,错误列为 1 × 400 Bad request。会话包含两条追踪:第一条有 2 次调用、无错误、耗时 4.6 s、268 个输入 token;第二条有 1 次调用、1 个错误、耗时 0.5 s、0 个输入 token,费用为 $0

会话页面清楚地展示了这个模式:两条追踪,第一条有 2 次调用且无错误,第二条有 1 次调用和 1 个错误,错误卡片显示“1 × 400 Bad request”。失败追踪的输入 token 数为 0,耗时 0.5 s,费用为 $0。这三个值合起来说明模型根本没有运行,请求因无效而被拒绝。由此可以将它与模型或提供商故障区分开来,后者通常会出现 token 消耗、更长的耗时,或 5xx 状态码。

400 表示请求有问题,重试无济于事,错误文本已经指出了修复方向。完整保存助手消息后,第二轮就能成功。

其他几种历史记录 bug 也会呈现同样的模式:第一轮正常,后续某一轮被拒绝。例如,保存工具结果时丢了调用 ID,消息顺序存错,或裁剪历史记录时从工具交互中间切开,保留了结果却删掉了调用。如果为了控制费用而裁剪历史记录,应在轮次之间裁剪,绝不要从一轮内部切开。

每个智能体应该记录哪些信息,才能这样排障?

记录五项信息:

  • 每次运行的追踪 ID,以及每段对话的会话 ID,通过 X-Trace-Id 和 X-Session-Id 发送,同时写入自己的日志。
  • 每次调用的 span 名称,明确标出步骤和尝试次数,例如 plan-2 或 policy-check.retry。
  • 用户、功能和发布版本标签,通过 X-Synthorai-Metadata 发送。
  • 每次调用的 x-request-id 响应头,与自己的 ID 一起记录。
  • 每次调用的 finish_reason 和输出 token 数,将 length 视为失败。

还要为每次调用设置客户端超时和 max_tokens,并为两者留足余量。如果都不设置,只要模型还在生成,调用就可能一直运行,代码也会一直等待。但如果只按可见回复设置上限,就会重现第二个案例中的 bug;如果超时时间短于长文本生成所需的时间,则会中断本来正常的调用。推理模型的一次调用可能耗时数分钟。示例的等待时间为 300 s,默认将每次调用限制为 2,000 个输出 token,除非调用单独设置了上限。

常见问题

如何在生产环境中排查 LLM 智能体故障?

先找到那次运行:按用户或对话标签筛选请求日志,再打开匹配请求的追踪。追踪会按顺序展示这次运行中的所有模型调用,并标记失败调用,无需先看代码就能知道哪个步骤出了问题。

为什么 LLM 返回状态码 200,却没有回复内容?

通常是模型还没写出回答,就已经耗尽了输出额度。对于推理模型,max_tokens 同时包含推理和回复。检查 finish_reason:如果它是 length,且输出 token 数等于上限,就说明回复被截断了。示例中连续三次调用都返回 200,恰好输出 40 个 token,却没有可用回复。

“No tool call found for function call output”是什么原因?

对话历史中有工具结果,却缺少对应的工具调用。保存发起调用的助手消息时丢掉了 tool_calls,或裁剪历史时从调用与结果之间切开,都会导致这个问题。应完整保存助手消息,并且只在轮次之间裁剪。

LLM 调用失败后应该重试吗?

取决于状态码。429(限流)或 5xx(服务器错误)可能在重试后成功。400 是请求本身有问题,重试仍会以同样的方式失败;被 max_tokens 截断的回复,如果不调整上限,再试也会被截断。重试时保留追踪 ID,让两次尝试出现在同一条追踪中。

延伸阅读

← 使用场景