Skip to content
Agent 的常见问题、调试与限制
Agent 的运行依赖模型推理与外部工具的协作,这种不确定的组合会带来四类典型问题:工具未被调用、循环不终止、上下文超限和模型幻觉。本章从可观测性入手,介绍通过结构化日志、token 消耗和调用链定位问题的方法,随后讨论超时、重试、降级、限流与成本控制等稳定性策略。
可观测性:消息、token 与调用链
当 Agent 行为出现偏差时,第一步不是调整 prompt,而是弄清它实际做了什么。可观测性的基础由三部分构成:每次模型调用和工具执行的消息记录、输入输出的 token 数量、以及完整的调用链路。
消息记录应结构化存储,每条至少包含时间戳、角色(system / user / assistant / tool)、内容摘要或完整内容、以及触发原因。以下示例在每次迭代后追加一条 StepLog:
typescript
interface StepLog {
step: number;
role: "user" | "assistant" | "tool";
content: string;
toolCalls?: { name: string; arguments: string }[];
tokenUsage?: { input: number; output: number };
timestamp: number;
}
const trace: StepLog[] = [];
// 在 Agent 循环中,模型返回后写入
trace.push({
step: iteration,
role: "assistant",
content: response.content ?? "",
toolCalls: response.tool_calls,
tokenUsage: {
input: response.usage?.prompt_tokens ?? 0,
output: response.usage?.completion_tokens ?? 0
},
timestamp: Date.now(),
});这类记录能回答三个问题:Agent 做了什么(调用链)、结果是什么(返回链)、以及为什么这么做(决策链)。当 Agent 陷入循环时,回溯最近几步日志通常就能发现工具反复返回相似内容,导致模型基于相同信息做重复判断。
token 用量需要单独追踪。每次模型响应的 usage 对象包含 prompt_tokens 和 completion_tokens,持续累加可以获得整个会话的成本和窗口占用。若总量异常增长,可以反查是哪一步携带了大量上下文——例如搜索结果页的全文被原样送入了下一轮请求。
调用链可借助 trace ID 和 span ID 实现。为每次用户会话生成一个 trace ID,每次模型调用或工具执行分配一个 span,通过父子关系构建执行树。LLM 可观测平台(如 Langfuse、CloudWatch GenAI Observability)已内置此能力;即便只使用文件日志,手动维护父子关系也能在事后还原执行路径。
工具未被调用:排查模型意图与 Schema
Agent 最常见的故障之一是模型完全忽略已注册的工具,直接用自身知识回答。可能的原因集中在几个方面。
未挂载工具列表。请求中缺少 tools 参数会导致模型只返回纯文本,在请求发出处打印 tools.length 即可确认。
工具描述不合适。模型依据 function.description 和参数描述判断是否调用。描述过于模糊或未与用户问题关联时,模型会倾向直接回答。例如仅写 "Search the web",未说明输入是查询语句还是 URL,模型可能放弃调用。
JSON Schema 不合法。如果手动拼接 JSON 定义,或 SDK 版本升级后对 schema 的校验更严格,可能触发 400 错误,提示“does not match JSON Schema draft 2020-12”。排查时先用最简单的合法 schema 替换当前定义,确认能否恢复调用,然后逐步增加字段定位违规属性。
System Prompt 优先级的干扰。提示词中过度强调“请用你自己的知识回答”会抑制工具调用。若需要工具参与,应在系统消息中明确告知模型“当需要实时信息时,必须调用 search 工具”。部分模型还支持 intent 模式,通过在系统消息中声明 Response in INTENT_MODE 并注入工具列表的 JSON,使模型先输出意图再生成内容,有助于更精确地控制调用行为。
排查步骤如下:
- 确认请求的
tools数组非空。 - 确认模型中至少有一条用户消息。
- 检查返回对象是否包含
tool_calls;若无,则模型未打算调用工具。 - 将工具数量减至一个,用最简描述先跑通一次调用。
- 检查完整的
error字段,而非仅关注 HTTP 状态码。
循环不终止:终止条件与输出模式
Agent 循环配有最大步数安全网,但仍可能因震荡而迟迟无法触发,持续消耗 token。震荡的典型表现是:模型调用搜索工具,返回“未找到结果”,模型换一种查询再次搜索,依然无结果,如此循环。根因在于工具返回的信息不足以让模型做出终止决策。若工具仅返回空字符串,模型会怀疑查询方式有误并不断重试。
统一的工具返回格式可以消除这种不确定性。无论成功、失败还是空结果,都返回相同的 JSON 结构:
json
{
"status": "success" | "error" | "empty",
"data": "...",
"error": {
"code": "TIMEOUT" | "NOT_FOUND" | "INVALID_ARGUMENT",
"message": "..."
}
}模型看到 "status": "empty" 时能区分“确实没有”和“出错了”,从而停止盲目重试。在循环中,若连续两次调用同一工具且参数高度相似,并且均返回 empty 或相同错误码,可以主动中断循环并告知用户。
停止词也可辅助控制输出。若模型应在回答结束时停止却继续生成内容,可在推理参数中设置 stop 序列,当模型生成该序列时强制结束。不过这对工具调用循环影响有限。
指令约束则从 prompt 端限制。在系统消息中加入类似“如果搜索两次均无结果,请如实告知用户,不要再继续搜索”的强制规则,并将其放在不可压缩层级,确保即便上下文被截断,该约束仍然有效。
上下文超限:长度监控与动态裁剪
前置章节实现了基于 token 计数的滑动窗口和摘要压缩,此处聚焦于超限发生时的诊断路径。
现象通常是 Agent 突然“忘记”先前对话内容,回答不连贯,或直接收到 context_length_exceeded 错误。需要检查两个指标:
- 每次请求的
prompt_tokens变化曲线。若呈台阶式攀升,说明上下文持续膨胀,裁剪逻辑可能未生效。 - 消息数组中各类角色消息的数量和长度。重点观察工具返回是否携带了大量文本(如网页抓取的整页 HTML),未截断的高体积消息会迅速占满窗口。
常见原因之一是摘要压缩策略错误丢弃了系统指令。若系统消息与用户消息一同被送入摘要,压缩后的历史可能完全丢失行为约束。日志中可见摘要之后,Assistant 的行为偏离预期,例如本该调用工具却直接猜测答案。
另一种原因是动态裁剪的触发条件不当。例如设定 “当总 token 超过 N 时裁剪”,但 N 与模型上下文窗口之间缓冲不足,导致请求在裁剪执行前已因超限失败。通常将 N 设为模型最大窗口的 80% 左右,可留下足够余量。
遇到超限错误时,最直接的恢复手段是在下一次请求前丢弃最旧的几对 user/assistant 对话(保留系统消息和最近几轮),或调用 LLM 压缩历史后重新发送。关键在于这些操作必须被日志记录,否则无法追溯“失忆”发生的准确位置。
幻觉:事实错误与工具误用
幻觉的表现形式不仅是凭空捏造,还包括工具误用——模型尝试调用不存在或未注册的工具,或用错误参数调用正确的工具。
工具误用的典型症状是万能工具被滥用。如果 Agent 被允许执行 Shell 命令,模型可能图省事直接用 ls 而不是调用你封装的文件列表工具。防范方法是在 Bash 工具描述中明确限制,或干脆不提供万能型工具,只暴露原子化、单一职责的工具。当发生误用时,工具返回的错误消息应直接说明替代方案,例如 “Use LS tool instead of Bash for listing directories”。
事实错误——模型自信地给出错误信息——排查时需要结合调用链。先确认模型是否调用了搜索或查询工具;如果调用了,错误可能源于工具返回数据不准确,或模型提取信息时发生了扭曲;如果没调用,说明模型在凭记忆回答,此时应回到工具未调用的排查步骤,找出模型为何未选择调用工具。
降低事实错误的一种方式是在系统提示词中要求模型引用事实时附上来源工具及返回的原始片段,但这会增加 token 消耗,需要在准确性与成本之间权衡。
超时、重试与降级
请求与工具的超时控制
Agent 的每次外部调用(模型 API、搜索 API、数据库查询)都需要超时。一个工具的无限等待会阻塞整个循环。Node.js 中可利用 AbortController 为 fetch 或 SDK 请求设置超时:
typescript
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10_000); // 10 秒
try {
const response = await fetch(url, { signal: controller.signal });
// 处理响应...
} catch (err) {
if (err.name === "AbortError") {
return {
status: "error",
error: { code: "TIMEOUT", message: "工具调用超过 10 秒" }
};
}
throw err;
} finally {
clearTimeout(timeoutId);
}针对模型 API 的超时不应设置过短,某些模型的 TTFB(首 token 时间)就可能达到数秒。建议模型请求超时设为 30–60 秒,工具则依据各自特性独立配置。
指数退避与重试策略
遇到限流(429)或偶发 5xx 错误时,立即重试大概率再次失败,并可能加剧服务端压力。指数退避是标准处理方式:
typescript
async function withRetry<T>(
fn: () => Promise<T>,
maxRetries = 3,
baseDelayMs = 1000
): Promise<T> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (err) {
if (attempt === maxRetries) throw err;
if (isRetryableError(err)) {
const delay = baseDelayMs * Math.pow(2, attempt) + Math.random() * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
throw err;
}
}
throw new Error("unreachable");
}
function isRetryableError(err: any): boolean {
if (err.status === 429) return true;
if (err.status >= 500 && err.status < 600) return true;
if (err.code === "ECONNRESET" || err.code === "ETIMEDOUT") return true;
return false;
}重试应当仅用于幂等操作。模型推理请求本身是幂等的(相同输入可重试),可以安全重试;涉及写操作的工具需评估后果,通常直接向模型返回错误,由模型决定后续动作更为稳妥。
工具不可用时的降级与兜底
当某个工具持续失败(超时、依赖服务不可用),Agent 不应原地崩溃。可在执行循环中加入降级逻辑:统计同一工具近几次调用的失败率,若超过阈值则将其标记为不可用,并从后续请求的 tools 列表中移除,同时通知模型。例如:“search 工具当前不可用,请基于已有知识回答,或引导用户更换请求。”
在所有工具均不可用的情况下,仍可给出兜底回答:“我暂时无法获取实时数据,但根据我的知识……”这比让用户看到死循环或异常堆栈好得多。兜底文案通过最外层异常捕获生成,但它不解决根因,仅保证用户体验下限。
Rate Limit 与成本控制
客户端限流可以避免触发服务端限流,同时控制请求速率和花费。以下示例维护一个并发请求计数器,限制每秒发出的请求数:
typescript
class RateLimiter {
private queue: (() => void)[] = [];
private lastTime = 0;
private intervalMs: number;
constructor(maxPerSecond: number) {
this.intervalMs = 1000 / maxPerSecond;
}
async acquire(): Promise<void> {
const now = Date.now();
const wait = Math.max(0, this.intervalMs - (now - this.lastTime));
this.lastTime = now + wait;
await new Promise(resolve => setTimeout(resolve, wait));
}
}成本控制的另一路径是缓存。若工具结果对相同参数是幂等且在一段时间内有效,可在工具层增加短期内存缓存,避免重复调用昂贵的搜索 API 或 LLM。
令牌预算(token budget)在执行循环中加入硬性限制:当累积输入 token 或输出 token 超过预设值时,循环必须进入收尾模式,不再调用工具,直接生成最终回答,避免一次过于开放的对话消耗大量费用。
限制与权衡
即使落实上述所有防御措施,Agent 仍然是一个概率系统。提升可控性是可能的,但无法达到确定性程序的稳定程度。对于流程固定、规则明确的任务,简单的静态工作流远比 Agent 可靠。Agent 的价值体现在需要动态规划、根据中间结果调整策略的场景,代价是更高的延迟、成本和不稳定性。
可诊断性是可靠性的基础。若一次交互出错后,能在几分钟内通过日志锁定是哪个工具超时、哪段 prompt 被裁剪、哪次重试触发了限流,这次错误就是可控的。缺少结构化日志的 Agent 会让问题无从下手。开发初期不必追求面面俱到,优先确保能稳定多步推理、能记录证据、能给出可执行的结果,再逐步加入本章所述的各类防御策略。
参考链接
- [1] https://aws.amazon.com/cn/blogs/china/agentic-ai-infrastructure-practice-series-7
- [7] https://github.com/datawhalechina/hello-agents/blob/main/Extra-Chapter/Extra09-Agent%E5%BA%94%E7%94%A8%E5%BC%80%E5%8F%91%E5%AE%9E%E8%B7%B5%E8%B8%A9%E5%9D%91%E4%B8%8E%E7%BB%8F%E9%AA%8C%E5%88%86%E4%BA%AB.md
- [14] https://github.com/RooCodeInc/Roo-Code/issues/10320
- [16] https://github.com/adongwanai/AgentGuide/blob/main/resources/agent/official-guides.md
- [19] https://help.aliyun.com/zh/model-studio/intent-detect-capability
- [21] https://cloud.tencent.com/developer/article/2651857
