Agent 任务没做完就停了?我扒了 Claude Code 源码,找到了 4 层原因

Agent 任务没做完就停了?我扒了 Claude Code 源码,找到了 4 层原因

从一个让人抓狂的 Bug 出发,深入 Claude Code 源码,拆解 Agent Loop 提前停止背后的四层防护体系,附可直接落地的分级决策树改造方案。


凌晨两点,Agent 跟我说「已完成」

那天我让 Agent 帮我重构一个模块------把分散在 15 个文件里的代码逐个迁移。

Agent 开局很猛:Glob 定位文件、Read 逐个读取、Edit 开始改第一个。

然后它停下来,发来一条消息:

"我已经修改了第一个文件,其他文件也需要我修改吗?"

我盯着屏幕,血压上升。废话,我说的是「所有文件」。

更诡异的是另一次:Agent 调了一个命令,返回空输出,然后冷静地告诉我:

"好的,已完成。"

什么都没做,语气笃定得像刚写完整个代码库。

这不是玄学,这是工程问题。我花了一个周末扒了 Claude Code 的源码,发现 Agent Loop「提前停止」这件事背后,藏着一套精密的四层防护体系------每一层都有坑,每一层也都有解法。

今天把这套体系拆给你看,顺便给你一个可以直接抄的改造方案。


第一层:API 协议层 ------ 空结果触发的「幽灵停止」

这是最隐蔽的 Bug,也是 Claude Code 源码里藏得最深的一行修复。

先说现象:工具明明执行了,但 Agent 突然停止,既没报错,也没解释。

根因在协议层 。Claude API 用 \n\nHuman: 作为对话轮次的停止标记。当工具返回空内容时,连续的空白 tool_result 块在某些情况下会被解析器误判为对话结束信号。

模型不是「不想干活」,它是被协议层面的静默信号给卡住了。

Claude Code 的修复简单到令人意外:

ini 复制代码
// 任何返回空内容的 tool_result 都会被替换为占位文本
if (!toolResult.content || toolResult.content === '') {
  toolResult.content = `(${toolName} completed with no output)`
}

永远不让模型看到空的 tool_result。 任何空结果都注入一个包含工具名的占位文本,确保没有空内容能穿透到模型视野。

你的 Agent 要做的:工具返回空字符串时,主动注入占位文本,而不是直接透传。


第二层:模型层 ------ 不调用工具 ≠ 任务完成

协议层的坑填了,还有更难缠的:模型确实会主动决定不调用工具

大多数 Agent Loop 的实现长这样:

arduino 复制代码
if (response.hasToolCalls) {
  await executeTools(response.toolCalls)
} else {
  break  // ❌ 一刀切,停了
}

Claude Code 的做法截然不同------它把「模型不调用工具」当成一个可恢复的信号,进入一棵精心设计的决策树:

css 复制代码
模型不调用工具
  ├─ 上下文过长(413)?
  │    ├─ collapse drain retry(压缩历史后重试)
  │    └─ reactiveCompact(实时削减上下文)→ 用尽才终止
  ├─ 输出被截断?
  │    └─ 追加 recovery 消息("Continue from where you left off")→ 有次数限制
  └─ 弹尽粮绝?
       ├─ 有 blocking errors → 追加到 messages 让模型看到
       ├─ token budget 还有余量 → 发 nudge 消息推一把
       └─ 所有手段用尽 → 才允许终止

这棵树的设计哲学只有一句话:你不想干活,我推你干;你干不了,我给你创造条件干;只有山穷水尽了,我才放你走。


第三层:提示词层 ------ 心理暗示 + 威慑

工程手段能修 Bug,但治不了模型的「主观意愿」。Claude Code 在系统提示词里植入了两条精妙的护栏。

护栏一:「别轻易放弃」

如果一个方法失败了,先诊断原因再切换策略------读错误、检查假设、精准修复。不要盲目重试同一种做法,但也不要因为一次失败就放弃整个可行方案。

这条指令给模型构建了一个「诊断 → 修复」的认知框架,而不是「失败 → 停止」的本能反应。

护栏二:「停了也没用」

Token 预算是硬性下限,不是建议。如果你提前停止,系统会自动继续推进。

这甚至不像是给模型的指令,更像是一种心理威慑。当模型知道「停下也会被 nudge 回来」,它的策略就从「投机取巧」变成了「老实干完」。

你的 Agent 要做的:在系统提示词里明确告诉模型「不调用工具不等于完成」,并说明系统会继续推进。


第四层:工程层 ------ 硬限制与容错

最后一层是基础设施的保护,也是最容易被忽视的一层。

场景 Claude Code 的处理
maxTurns / budget 超限 硬终止,合理退出
单个工具权限被拒绝 记录错误,继续循环,不拉垮整个 Loop
上下文压缩触发 压缩成功后直接进入下一轮,跳过模型调用
停止原因 语义化分类:max_turns / prompt_too_long / completed / stop_hook_prevented

最关键的设计:单个工具失败不终止整个 Loop。这听起来理所当然,但大多数自己实现的 Agent 都会在这里翻车------一个工具抛异常,整个循环就崩了。


四层防护全景图


落地改造:把「一刀切」换成分级决策树

改造前(常见写法)

csharp 复制代码
// ❌ 模型不调工具就直接停
while (true) {
  const response = await model.generate(messages)
  if (!response.hasToolCalls) break
  await executeTools(response.toolCalls)
}

改造后(分级决策树)

kotlin 复制代码
// ✅ 只有山穷水尽才停
let recoveryAttempts = 0
const MAX_RECOVERY = 3
​
while (true) {
  const response = await model.generate(messages)
​
  // 正常路径:有工具调用,执行后继续
  if (response.hasToolCalls) {
    await executeTools(response.toolCalls)
    recoveryAttempts = 0  // 干活了就重置计数
    continue
  }
​
  // 恢复路径一:上下文过长
  if (contextTooLong(messages)) {
    messages = await compressContext(messages)
    continue
  }
​
  // 恢复路径二:输出被截断
  if (response.truncated && recoveryAttempts < MAX_RECOVERY) {
    messages.push({ role: 'user', content: 'Continue from where you left off.' })
    recoveryAttempts++
    continue
  }
​
  // 恢复路径三:有未处理的错误
  if (hasUnresolvedErrors(messages)) {
    messages.push(...formatBlockingErrors())
    continue
  }
​
  // 恢复路径四:budget 还有余量,nudge 一下
  if (tokenBudgetRemaining > 0) {
    messages.push({ role: 'user', content: 'Is there more work to do? If yes, continue.' })
    continue
  }
​
  // 所有手段用尽,才真正终止
  break
}

两条核心原则:

  1. 模型不调工具 ≠ 任务完成 ------ 先救,别急着停
  2. 每种停止原因都有对应的恢复手段 ------ 压缩 / 追加 / nudge,用尽了再放弃

对照检查:你的 Agent Loop 做到了几条?

检查项 做了吗?
空 tool_result 注入了占位文本?
模型返回纯文本时,有多级恢复而非直接 break?
系统提示词里有「别轻易放弃」的明确指令?
上下文过长能自动压缩 + 重试?
单个工具失败不会终止整个 Loop?
能区分「模型主动停」和「资源耗尽」?

最关键的一条 :把那个简单粗暴的 if (!hasToolCalls) break 替换成分级决策树。模型不调用工具,不一定是做完了------很可能只是卡住了。


写在最后

扒完这段源码,我最大的感受是:Claude Code 对「停止」这件事的态度,比大多数 Agent 框架严肃得多。

它不把「模型不调工具」当成终止信号,而是当成一个需要诊断的异常。每一种停止原因都有对应的恢复手段,每一次真正的终止都有语义化的分类。

这种设计哲学值得借鉴:Agent 的鲁棒性,不是靠模型更聪明,而是靠工程层把每一个可能的失败路径都想清楚。

你在实现 Agent Loop 时,遇到过哪些奇怪的「莫名停止」?欢迎留言分享,说不定我们踩的是同一个坑。

如果这篇文章对你有帮助,点个赞收藏一下~后续还会继续扒 Claude Code 源码,下一篇准备聊聊上下文压缩的实现细节。

相关推荐
老成说AI2 小时前
DEEPSEEK V4 实测:它不够炸裂,但正在啃最硬的骨头
人工智能·ai·deepseek
Wanderer X2 小时前
【LLM】GSPO DAPO
人工智能
IpdataCloud2 小时前
IP查询工具的准确率怎么评估?一份可上生产的选型与验收指南
网络·人工智能·算法
喜欢流萤吖~2 小时前
消息队列:微服务的异步通信枢纽
微服务·架构
Jutick2 小时前
Python 行情数据清洗实战:Z-Score、MAD 与分位数过滤的异常值检测
后端·架构
大龄码农-涵哥2 小时前
Java调用AI大模型API入门:从零开始接入ChatGPT/通义千问
java·人工智能·chatgpt
沫儿笙2 小时前
焊接机器人弧焊节气设备
人工智能·机器人
人工智能AI技术2 小时前
网络协议基础:三次握手、四次挥手通俗讲解
人工智能
疯狂成瘾者2 小时前
大模型与后端如何协作?
人工智能