资讯动态

我以为 Coding Agent 死循环是模型太笨,直到发现是自己的 Runtime 在添乱

发布时间:2026/10/22 7:36:08 来源:尧图企业网站定制
我以为 Coding Agent 死循环是模型太笨直到发现是自己的 Runtime 在添乱做 Coding Agent 时你有没有遇到过这样的情况模型反复读取同一个文件、不断执行同一条命令明明已经失败了却还是不肯停下来我在开发 mini-claude 的早期就遇到了这个问题。当时第一反应是模型是不是太笨了既然它不知道什么时候该停那我就给它加一个死循环检测器。于是从重复检测、提示词提醒到工具拦截、失败分类、硬熔断治理机制越做越复杂。结果后来发生了两件很有意思的事当我逐渐完善上下文管理后观察真实执行记录发现许多原本令人头疼的重复行为已经明显减少。但死循环检测和拦截仍然经常触发。继续排查才发现其中不少其实是正常的代码探索和调试被我们自己的治理规则误判了。更夸张的是一次过于激进的治理实验让 36 次测试运行全部提前终止。这篇文章就聊聊mini-claude 的死循环治理为什么越做越复杂以及这段经历让我对 Agent Runtime 有了哪些新的理解。一、先介绍一下 mini-claudemini-claude 是我用 Python 开发的一个轻量级 Coding Agent 项目主要参考 Claude Code 的设计思路探索一个能够自主操作本地代码仓库的 Agent 应该如何实现。它支持读取、搜索、修改文件也可以执行 Shell 命令、运行测试并根据工具返回的结果决定下一步操作。例如用户提出一个任务修复项目中的登录接口并运行测试确认问题已经解决。Agent 可能会执行用户提出任务 ↓ 读取登录模块代码 ↓ 分析接口和调用关系 ↓ 修改代码 ↓ 执行 pytest ↓ 根据结果继续修改 ↓ 测试通过结束任务从实现角度看它的核心其实就是一个循环whileiterationmax_iterations:responsellm.generate(messages,tools)ifnotresponse.tool_calls:returnresponse.contentforcallinresponse.tool_calls:resultexecute_tool(call)messages.append(result)以上是简化后的伪代码。完整实现还涉及工具调用批次、权限检查、Trace 记录和任务状态管理。这种架构的特点是下一步做什么并不是程序提前规定好的而是模型根据当前上下文和工具结果动态决定的。这让 Agent 具备了很强的灵活性但也引出了一个问题。如果模型始终认为“再试一次可能就成功”它什么时候才会停下来二、开发初期我一直以为死循环是模型不够聪明最开始我经常观察到 Agent 不断重复一些操作。比如执行pipinstallsome-package网络请求失败后模型可能换一种写法python-mpipinstallsome-package仍然失败又换成pipinstallsome-package --no-cache-dir但根本原因可能是当前环境无法访问依赖源。无论怎么修改 pip 参数都没有解决问题。还有一种情况是重复编辑代码读取文件 ↓ 尝试修改 ↓ 修改失败 ↓ 重新读取 ↓ 再次采用相似方式修改 ↓ 再次失败 ↓ ……当时看到这些行为我最直接的想法就是模型自己不会判断有没有必要继续那就在外面加一层规则替它判断。1. 第一版重复调用检测最简单的方案是记录工具名称和参数。例如importjsondeffingerprint(tool_name,args):return(tool_name,json.dumps(args,sort_keysTrue))假设模型连续调用read_file(src/user.py) read_file(src/user.py) read_file(src/user.py)就能够通过指纹识别重复。不过发现重复之后又有一个问题怎么处理早期我尝试过几种方式。第一种是软提示。检测到重复命令就向模型反馈[系统提示] 你已经多次执行相同命令 而且执行结果没有明显变化。 请重新分析失败原因 考虑使用其他方法解决问题。优点是不影响模型继续探索。缺点是模型可能忽略提示继续重复操作。于是又有了第二种方式直接拦截。2. 从提示模型到物理阻断工具调用后来项目加入了LoopGuard可以理解为工具调用守卫。它会在真正执行工具前检查是否存在重复行为。如果触发拦截不再执行原工具而是生成一条模拟工具结果[重复操作提醒 — 本次调用未执行] 检测到你正在重复使用相同的工具和参数。 工具名称read_file 调用参数{path: src/user.py} 本次调用已经被拦截。 请结合之前的执行结果 判断是否需要改变策略。这个处理方式和单纯输出日志不一样。它会把拦截信息放进对话历史让模型在下一轮能够看到。Assistant调用 read_file ↓ Runtime发现重复阻断执行 ↓ Tool返回模拟的阻断结果 ↓ Assistant收到提醒重新思考我们甚至曾经尝试过要求模型输出reflection标签先分析自己为什么失败再提出不同的方案。但这逐渐暴露出一个问题要求模型反思并不等于模型一定会改变策略。一个模型完全可能写出一段看起来很认真的反思然后继续做同样的事情。因此后续治理逐渐从“让模型意识到自己在循环”转向“由程序判断哪些行为可以继续执行”。不过现在回头看当时我其实忽略了一个更值得优先排查的问题模型为什么会不断重复它真的拿到了足够的信息吗三、真正让我改变想法的是上下文管理这是整个项目中让我印象比较深的一次认知变化。早期开发时我一直把注意力集中在死循环治理模块上。模型重复调用加强检测。换参数继续重试增加语义识别。提醒以后仍然重复提高拦截强度。但后来随着上下文管理模块逐渐完善我发现情况发生了变化。观察一些真实执行记录Trace时明显无效的重复行为比以前少了。可是循环检测、拦截相关的记录仍然不少。这就有点奇怪了。模型看起来没有一直原地打转为什么治理系统还是认为它在死循环继续排查后我逐渐意识到这里面可能同时存在两类问题第一类是上下文管理不好导致模型反复获取已经获得过的信息。第二类是循环检测规则太激进把正常操作当成了死循环。这两个问题表面上都可能表现为“重复调用很多”但解决方式完全不同。1. 早期的上下文压缩太激进在早期实现中我曾经把上下文压缩阈值设置得很低只有几千 Token 的量级。可能刚交互几轮或者工具输出稍微长一点就触发压缩。当时想得也很简单既然上下文越长Token 成本越高那就早点压缩反正摘要能保存主要信息。但后来发现Coding Agent 对上下文的要求比普通聊天复杂得多。假设用户要求修改一个接口。前几轮Agent 已经知道用户要求 不要修改接口返回结构。 已确认 问题来自 Token 过期时间计算。 已经尝试 修改配置项 A但测试仍然失败。 下一步 检查配置项 B 和对应单元测试。如果压缩时只生成用户正在修复登录相关问题 已经尝试修改配置需要继续检查。看起来保留了主要内容但关键事实已经丢了很多。模型下一轮可能不知道哪个方案已经尝试过之前的失败原因是什么哪些文件已经检查过用户明确禁止修改什么接下来原本准备验证哪个假设。于是它只能重新探索。第一次读取 user.py ↓ 得到关键线索 ↓ 触发激进压缩 ↓ 关键线索丢失 ↓ 再次读取 user.py ↓ 重新分析 ↓ 再次触发压缩 ↓ ……这里模型表面上一直重复读取文件。但根因未必是模型“笨”而是它每次准备继续时之前建立的工作上下文已经被破坏了。这就像一个程序员每分析完几个文件别人就把他的笔记删掉只留下“正在修复登录问题”。他当然也会不断回头检查。对 Agent 来说上下文就是它下一次决策时能够利用的工作记忆。如果这份记忆不完整再好的循环检测器也只能处理已经发生的重复行为而不能解决重复发生的原因。2. 后来才发现压缩阈值本身也需要结合模型能力设计项目后续的文档还记录过一个问题当时默认模型的上下文窗口已经达到约 100 万 Token但此前仍然沿用约 70K/100K 的隐式压缩阈值。换句话说模型本来还能容纳更多有效信息我们却过早地开始压缩。而且旧压缩策略还存在其他问题摘要前对单条消息做字符截断只取部分历史内容生成摘要对工具调用和结果的对应关系保护不足Token 估算没有完整覆盖实际发送给模型的内容。后来逐渐改成显式配置compression:context_window_tokens:1000000microcompact_token_threshold:250000full_compression_token_threshold:500000其中context_window_tokens模型可使用的上下文窗口配置。microcompact_token_threshold触发轻量压缩的阈值。full_compression_token_threshold触发更完整摘要压缩的阈值。这里的 250K/500K 是当前使用的一组基础配置不代表已经通过实验找到最优值。真正发生变化的是设计思路不再把压缩理解成“越早越省钱”而是开始考虑信息保留、工具输出、历史一致性和实际 Token 成本之间的平衡。3. 光调整阈值还不够工具输出也是上下文的上游除了压缩时机工具返回什么也很关键。例如 Agent 执行一条测试命令输出了两千多行日志。真正的报错可能在第 1100 行。如果工具只返回前后几十行模型根本看不到关键错误。它可能只知道“测试失败了”却不知道失败在哪里。于是再次运行测试或者不断尝试其他命令寻找线索。从循环检测器看来这可能是一连串重复调用。但问题实际上发生在更早的地方工具没有把足够有效的信息传递给模型。因此mini-claude 后来尝试把长工具输出保存为文件再把简短预览和日志路径返回给模型。类似测试执行失败 完整日志.agent/logs/cmd_xxx.log 总行数2251 前 10 行…… 后 20 行…… 如需定位具体错误 可通过 search_code 或 read_file 进一步检查完整日志。这样既不需要每轮把几千行日志全部塞进上下文又保留了按需获取细节的能力。项目中有一个专门用于验证这种方案的测试任务关键错误线索位于长日志中间不能单纯依靠首尾预览得到答案。在相同任务和模型条件下改造前后各运行 5 次结果如下指标改造前改造后任务通过情况5/55/5平均 Token253,99879,288平均执行轮数14.813.8平均耗时22.59 秒21.59 秒Token 消耗下降约68.8%同时保持了任务通过结果。这组数据衡量的是长日志处理不是整个 Agent 的死循环率。但它说明一个很实际的问题改进工具输出的信息组织方式有时比单纯减少模型调用更有效。后来我们还增加了压缩前保存关键工作笔记的提醒以及有限大小的近期文件观察记忆尽量避免 Agent 在压缩后失去全部工作线索。这些方案分别解决不同问题工具输出文件化避免大量日志挤占上下文。消息压缩降低历史对话的长期成本。工作笔记保留任务目标、已确认结论和关键约束。近期文件记忆让 Agent 更容易知道自己最近读过什么减少不必要的重复读取。它们并不能完全消除重复探索但把部分问题从“循环发生后拦截”前移到了“让模型持续获得足够的信息”。Anthropic 在《Effective context engineering for AI agents》中也讨论过类似取舍上下文压缩需要保留未来执行所依赖的关键事实过度压缩可能造成信息损失。这和我在 mini-claude 中遇到的问题非常相似。4. 一个很重要的启示先检查上游再加强下游治理回过头看之前的排查方向有些本末倒置。上下文丢失 ↓ Agent 忘记之前做过什么 ↓ 重复读取 / 重复尝试 ↓ LoopGuard 检测到重复 ↓ 拦截工具 ↓ Agent 被迫寻找其他办法如果一直盯着最后的 LoopGuard我们可能会不断提高检测能力却始终没有修复真正的问题。更复杂的拦截规则甚至可能进一步干扰模型的正常恢复。因此我后来更愿意先问这是 Agent 的决策出了问题还是 Agent 作出决策时拿到的信息就已经有问题在 Agent 系统中上游问题可能包括上下文压缩、工具结果截断、错误信息丢失、状态注入不及时等。先把这些地方检查清楚再去判断模型本身是否存在无意义重试往往更有价值。四、第二个转折死循环告警很多不代表真的有很多死循环上下文管理逐渐改善后我开始重新审视死循环治理模块。因为出现了一个很有意思的现象真实 Trace 中明显的无效循环减少了但治理模块仍然频繁报告重复或触发拦截。这时就不能简单地把检测次数当成问题数量了。如果一个检测器过于敏感即使正常执行也会被它拦截拦截率自然居高不下。1. 一个真实的误判不同文件的编译被当成重复在早期命令归一化方案中我们希望将不同的命令形式统一成稳定的操作意图。例如pipinstallrequests python-mpipinstallrequests两者都在安装同一个依赖可以归一成相似的 Intent操作意图。但归一化做得太粗糙也会产生问题。项目曾遇到python-ccompile(a.py)python-ccompile(b.py)它们明明在检查不同文件。旧规则却将两者归一成类似EXECUTE::-c结果第三次相关操作被误认为重复并拦截。模型为了绕过这个限制又尝试了其他办法额外消耗了大约 8,452 Token。这就非常讽刺了。我们设计循环检测器原本是为了减少无效调用和 Token 消耗。结果检测器自己判断错了反而让模型多走了弯路。类似问题还发生在java-versionmvn-version因为旧指纹忽略了可执行程序的区别两条正常命令也可能被归一到相同目标。后来通过保留更多有效参数、调整检测阈值并放宽合法的不同范围检查才逐步修复这些误拦截。2. 重复动作和真正停滞应该分开判断假设 Agent 正在 Debug第 1 次 pytest5 个测试失败 ↓ 修改代码 ↓ 第 2 次 pytest3 个测试失败 ↓ 修改代码 ↓ 第 3 次 pytest1 个测试失败 ↓ 修改代码 ↓ 第 4 次 pytest全部通过如果只看操作序列pytest 重复了四次。但如果看执行结果失败数量一直在下降。这不是死循环而是正常进展。反过来修改代码 → 状态 A 修改代码 → 状态 B 修改代码 → 状态 A 修改代码 → 状态 B虽然每轮都有文件变化但系统可能只是在两个状态之间来回振荡。所以需要区分Activity活动Agent 确实在做事。Progress进展任务真的在接近目标。Stagnation停滞重复尝试后没有观察到有效改善。而且不能把“暂时没有进展”直接当成“已经停滞”。比如连续读取几个源码文件可能只是正常的分析阶段。3. 一次过于激进的实验36 次任务全部提前终止后来我们尝试设计进展感知治理机制。所谓 Progress-aware Governance简单理解就是综合最近的操作、结果和状态变化判断 Agent 是否真的在向目标前进。实验中的第二版候选方案尝试维护一个“连续无明显进展次数”。没有明显进展 ↓ 计数 1 ↓ 达到阈值提醒 ↓ 继续无进展重新规划 ↓ 仍然无进展终止听起来很合理。结果在 12 个任务、每个执行 3 次的测试中36 次运行全部提前终止。原因是正常探索被错误算进了停滞次数。Agent 可能需要读取代码 → 搜索函数 → 查看配置 → 检查依赖 → 开始修改但前面的读取和搜索未必立即产生可见的业务进展。如果每一步都增加停滞计数Agent 还没动手就可能被终止。这个实验让我真正意识到没有证据证明 Agent 正在进步不等于有证据证明 Agent 正在原地打转。后来我们把正常的信息收集称为 Neutral Activity中性活动尽量避免对这些操作进行激进惩罚。还尝试了针对具体对象的局部停滞检测以及识别A→B→A→B这类周期重复的状态振荡检测。但实践也证明治理逻辑越复杂不一定越好。如果一个判断机制介入正常恢复过程太深即使局部规则看起来非常合理也可能让整个 Agent 更容易失败。所以最后的设计方向逐渐转向尽量允许模型探索只在证据足够明确的地方介入。五、处理循环不能只有“拦截”还要明确让 Agent 怎么继续到这里有必要把循环治理的实际处理动作讲清楚。因为发现死循环只完成了一半工作。另一半是发现以后到底怎么办在 mini-claude 的演进过程中逐渐形成了几种不同程度的处理方式。处理方式作用是否继续执行ALLOW放行证据不足允许正常操作是SOFT_BLOCK软阻断阻止当前工具执行返回原因可以继续REPLAN重新规划要求模型改变策略是HARD_STOP硬停止终止当前任务否这里的 SOFT_BLOCK 指的是对一次调用的阻断它不等于终止整个任务具体场景中Runtime 也可能使用其他非执行反馈来达到类似效果。1. 先提示而不是立即结束比如最近多次依赖安装失败第 1 次网络超时 第 2 次网络超时 第 3 次网络超时如果最近失败类型一致而且策略没有明显变化可以要求模型重新规划。当前主线中类似的重新规划反馈会提示[Progress Governance] 最近的策略没有产生新的有效观察结果 或状态进展。 请重新规划改变解决路径 不要继续重复同一失败策略。模型接下来可以检查网络、寻找合法本地依赖或者明确报告阻断。这一步只是要求改变策略不替模型预先决定唯一的解决办法。2. 对明显无效的操作可以直接阻断比如连续两次编辑文件都没有产生任何文件变化。第三次又准备执行相似的写入操作。此时 Runtime 可以在执行前介入返回本次调用未执行的反馈要求模型重新检查修改目标。这种做法能够避免已知无意义操作真正发生。但 Agent 的整体任务仍然可以继续。3. 再次重复且证据充分时才硬停止例如同一失败类型在近期不断复现策略变化非常有限而且已经给过重新规划机会。此时继续尝试很可能只是消耗资源。Runtime 可以选择 HARD_STOP。项目中保留了一个专门用于熔断的异常classRuntimeEscalationException(Exception):passCircuitBreaker可以在收到硬停止决策后抛出该异常classCircuitBreaker:defstop(self,decision):ifdecision.actionRuntimeDecision.HARD_STOP:raiseRuntimeEscalationException(fRuntime HARD_STOP:{decision.reason})异常会传递到 Agent 主循环的控制处理逻辑而不是作为普通工具失败被模型继续重试。当前主线还支持直接根据 RuntimePolicy 的硬停止决策结束任务关闭工具调用批次记录执行 Trace并设置终止状态。因此重新规划是给模型一次机会硬停止则是程序收回控制权。至于最大执行轮数仍然保留作为最终兜底而不是取代上述判断。六、机制越来越多又出现了一个架构问题随着项目不断演进我们已经加入了重复检测、失败分类、工作区状态检测和终止控制。这些功能各有用途却带来了另一个问题。不同模块可能保存不同的历史并对同一条执行轨迹得出互相矛盾的结论。比如修改代码成功 → 测试失败 修改代码成功 → 测试失败 修改代码成功 → 测试失败 修改代码成功 → 测试失败 修改代码成功 → 测试失败一个模块知道每次代码都有变化。但另一个模块可能只累计测试失败5 次然后认为任务应该终止。问题并不一定是失败阈值设置得太小而是不同模块看的信息不一样。因此后来我们将每次工具尝试统一记录成AttemptEvent由AttemptHistory保存按顺序排列的执行历史。每条事件包含本次调用的工具与操作目标 执行是否成功 观察到了什么结果 失败属于哪种类型 工作区有没有变化 失败是否得到恢复 Runtime 作出了什么决策当前主线最多保留最近 32 条工具尝试事件。循环检测和失败复现检测不再各自维护完整的独立历史而是读取同一条事件流。Tool Attempt ↓ AttemptEvent ↓ AttemptHistory ↓ ┌──────────┴──────────┐ ↓ ↓ 重复行为检测 失败复现检测 │ │ └──────────┬──────────┘ ↓ RuntimePolicy 统一决策 ↓ 放行 / 重规划 / 终止这里的RuntimePolicy就是统一运行时决策层。其他检测模块负责提供证据而不是各自决定整个任务是否应该停止。这解决的是一个很基础、但容易被忽略的问题系统里应该只有一份可追溯的执行事实不能让多个模块各自记账、各自裁判。七、还有一种问题工具说成功业务却没有成功在统一执行事件后我们还遇到了更底层的问题。如果工具本身返回的结果就不准确那后续治理再聪明也没有意义。1. Shell 退出码为 0服务可能仍然失败例如 Windows CMD 中的复合命令curl http://localhost:8080/health echo OK前面的请求即使失败后面的echo成功也可能让最终退出码表现为 0。如果 Runtime 只读取Exit Code: 0就可能错误判断命令成功。但实际服务仍可能返回 HTTP 503。因此后来我们让工具保留结构化执行结果ToolResult(execution_successTrue,exit_code0,stdout...,stderr...,segment_exit_codes(7,0))再由观察结果归一化模块判断业务目标是否真的健康。例如命令执行完成是 目标服务健康否 观察结果HTTP 503这说明命令执行成功和任务目标成功是两件不同的事情。反过来输出文本中出现HTTP 503也不能直接说明服务失败。比如grepHTTP 503README.md这只是在检索文档。所以观察判断必须结合工具类型、执行上下文和结构化结果而不是简单匹配错误关键词。2. 模型宣布完成也不能直接相信另一个典型问题是服务返回 HTTP 503 ↓ Agent 尝试修复 ↓ 没有获得健康验证 ↓ Agent服务已经恢复如果只看模型的最终回答就可能把失败任务当成成功。因此我们又在完成边界加入了Completion Gate也就是任务完成校验。当模型准备不再调用工具、直接输出最终答案时Runtime 会检查之前是否存在没有解决的失败。例如curl localhost:8080/health ↓ HTTP 503 ↓ 修复服务 ↓ health_check(port8080) ↓ healthytrue ↓ 允许完成这里两个工具虽然不同但验证的是同一个服务。所以项目还引入了subject_key用来识别操作针对哪个对象。例如service://localhost:8080 test://pytest/tests/user这样可以将不同工具对同一对象的验证关联起来。但不能因为某个无关操作成功就认为旧问题已经解决。例如pytest tests/user → failed pytest tests/order → passed订单模块测试通过不能证明用户模块的测试问题已经解决。对于仍然没有恢复证据的失败Runtime 第一次可以要求模型重新验证再次无依据地宣布完成则阻止这个成功状态。我觉得这里体现了一个很实用的思路程序不一定要知道问题该怎么修但至少应该知道模型有没有足够证据宣布修好了。八、怎么证明治理真的有效而不是把任务更快杀掉做完这些机制后还需要验证它们是否真的有用。最开始我也很关注平均轮数、Token 和执行耗时。但后来发现这些指标很容易误导人。比如一个 Agent 每次遇到失败就立即结束平均轮数1 Token 消耗很低 执行耗时很短看起来性能很好。实际上它根本没有尝试恢复任务。因此后来我们设计了一套专门衡量死循环治理的评测。1. 同时测试应该停止和应该恢复的任务任务被分为两类应该停止的任务确实存在无法恢复的阻断继续尝试只会浪费资源。应该恢复的任务存在合法解决路径Runtime 应该允许模型继续探索。因此一次治理决策可能产生四种结果实际情况Runtime 行为评价应该停止正确停止正确识别应该停止没有停止漏检应该继续错误停止误杀应该继续没有误停正确放行其中我最关心的是误杀。因为如果一个本来可以完成的任务被治理系统提前终止实际上是 Runtime 主动制造了失败。2. 一组实际评测结果在统一执行事件流的重构阶段我们使用 12 个测试任务每个运行 3 次一共 36 次。改造前后的结果如下指标改造前改造后治理决策准确率80.56%86.11%应停任务识别率61.11%72.22%错误终止率0%0%可恢复任务成功率77.78%83.33%最终业务验证通过率61.11%66.67%简单解释治理决策准确率Runtime 对应该继续还是停止的判断有多少次是正确的。应停任务识别率真正应该停止的任务中有多少被正确停止。错误终止率原本应该继续的任务中有多少被提前误杀。可恢复任务成功率原本存在恢复路径的任务最终真正完成了多少。最终业务验证通过率通过独立测试程序确认最终结果正确的比例。这组结果最值得关注的是应停任务识别率提高了约 11.11 个百分点同时没有增加错误终止。也就是说改造后能够识别更多真正应该停止的任务而不是简单地通过更激进的终止策略提高分数。这些数据对应统一事件流重构阶段并不代表所有后续实验的整体表现。3. 评测系统自己也可能出问题还有一个很有意思的插曲。当时明明执行了 36 次测试报告最初却只统计了 33 次。后来发现3 次模型服务超时被标记为基础设施异常直接从统计分母中排除了。结果就是失败样本被悄悄过滤指标自然可能变得更好看。我们还遇到过独立验证脚本出错、测试用例存在答案泄露、只检查标记文件却没有真正检查业务结果等问题。因此后来开始区分治理行为是否正确 ↓ 任务最终是否完成 ↓ 模型服务和执行环境是否正常不能把三个问题混为一谈。尤其是在 Agent 这种具有随机性的系统中如果评测本身存在漏洞就很容易不断针对某几个 Case 调规则最后只得到一个擅长通过特定测试的 Agent。九、这段经历给了我哪些可复用的经验经过这些迭代我越来越觉得死循环治理其实只是 Agent 可靠性问题的一个缩影。其中有几条经验即使换一个 Agent 产品我觉得仍然适用。1. 看到异常行为先考虑上游有没有问题比如模型重复读取文件。可能是模型决策能力不足也可能是上下文压缩后丢失了文件信息工具结果被截断上一次工具没有返回有效观察文件确实变化了需要重新读取检测器把不同范围的读取当成了同一次调用。如果不区分这些情况只会不断增加错误的拦截规则。所以我现在更倾向于先还原完整执行链路再决定究竟应该修模型行为、上下文、工具还是治理模块。2. 活动不等于进展进展也不能只看文件变化Agent 修改了文件说明发生了操作。但问题是否解决还需要结合测试结果、服务健康状态或其他业务验证。同样没有立即修改文件也不代表 Agent 没有进展。前几轮读取和搜索可能是后续正确修改所需的准备。3. 治理模块不应该拥有无限的终止权重复检测、失败分类和状态监测可以提供证据。但最终是否终止任务最好交由统一的 RuntimePolicy 决定。而且终止应该是有明确原因、可以追溯的决策。4. 优先减少误杀再提高识别能力一个只会不断终止任务的 Runtime很容易获得漂亮的 Token 和耗时数据。但它无法完成实际任务。因此治理效果至少要同时观察“抓住了多少死循环”和“误杀了多少正常任务”。5. 不要只优化一个模块的局部指标这一点是我整个项目中最大的感受之一。当初一直觉得死循环治理模块效果不好就继续加强它。后来才发现改善上下文管理后有些重复行为本来就可以减少。而检测器即使保持较高触发率也可能只是因为规则过于敏感。这种问题在其他系统中也很常见。例如后端接口超时不一定是接口代码本身慢也可能是数据库连接池耗尽、下游服务阻塞或者队列堆积。局部指标异常不代表根因一定存在于对应模块。上面这些经验更多是从 mini-claude 中提炼出来的通用原则。那么如果不再是 Coding Agent而是浏览器、运维或者数据分析Agent这些思路应该怎么落地十、如果换一个 Agent这套经验还能复用吗前面聊的都是 Coding Agent但我觉得这次实践中沉淀下来的一些思路并不局限于代码开发。因为无论是 Coding Agent、浏览器 Agent还是运维 Agent它们通常都遵循一个类似的过程观察环境 → 决定行动 → 执行工具 → 获取反馈 → 继续决策只要存在这个循环就可能出现重复操作、无效重试、上下文丢失以及错误地判断任务已经完成等问题。当然不同 Agent 的业务目标不一样所以我更倾向于复用治理思路而不是直接复制 mini-claude 的检测规则和阈值。1. 浏览器 Agent重复点击不一定是死循环假设我们开发一个自动填写表单的浏览器 Agent。用户让它填写信息并提交结果提交按钮一直没有反应。Agent 可能不断执行点击提交 ↓ 页面没有明显变化 ↓ 再次点击提交 ↓ 页面仍然没有变化 ↓ 继续点击……这看起来很像死循环。但问题可能有很多种页面正在加载、表单校验没有通过、请求已经成功但页面还没更新甚至是 Agent 没看到页面上的错误提示。如果直接规定“连续点击三次就终止”很容易误杀正常任务。结合 mini-claude 的经验我会这样处理首先不能只记录click(button)还应该记录按钮状态、页面 URL、表单校验结果以及提交后的响应。其次在决定重试之前先检查是否出现了新的页面状态。例如从SUBMITTING变成SUCCESS即使中间多次检查也不应该认为是无效循环。最后对于提交订单、支付等可能产生副作用的操作还要先确认上一次操作是否已经成功不能为了打破循环就直接重复提交。这里复用的其实是两个原则相同操作不代表没有进展重新执行之前要先确认外部世界是否已经发生变化。2. 运维 Agent不要把重启服务当成万能恢复策略再来看自动排查服务故障的 Agent。假设 Spring Boot 服务启动失败模型不断尝试启动服务 ↓ 启动失败 ↓ 修改启动参数 ↓ 再次启动 ↓ 继续失败单纯检测重复命令可能不够因为每次启动参数都不同。但如果几次失败的根因都是端口占用就可以将它们归为相同的失败类型。这时可以参考 mini-claude 的失败分类与策略识别机制操作目标Spring Boot 服务 失败类型端口占用 当前策略反复启动服务 失败次数多次 ↓ Runtime 要求重新规划 ↓ Agent 转向端口和进程排查另外服务启动命令退出码为 0也不意味着服务已经正常运行。应该进一步检查端口监听、健康检查接口必要时验证业务接口。这对应我们之前讨论过的工具执行成功不等于业务目标已经完成。这套思路也适用于数据库运维、容器故障排查和自动部署等场景。3. 数据分析 Agent真正的循环可能源于信息不足假设用户让数据分析 Agent 查询某个指标。模型连续生成 SQL执行 SQL ↓ 字段不存在 ↓ 修改 SQL ↓ 仍然报错 ↓ 继续修改 SQL……第一反应可能也是加强 SQL 重试检测。但如果进一步排查发现模型一直没有拿到最新的数据表结构那真正的问题就不是重试次数而是缺少必要的信息。此时与其不断提醒模型“不要重复执行错误 SQL”不如先给它提供可靠的表结构、字段说明及最近的错误信息。这和我在 mini-claude 中遇到的上下文问题非常相似。当 Agent 不断重复某个动作时不妨先检查它是不是缺少完成任务所需的信息4. 真正可以抽象成一套通用的治理流程如果以后重新开发一个 Agent我不会一上来就设计复杂的死循环检测器而是优先建立下面这条链路Agent 执行工具 ↓ 获取真实、完整的执行结果 ↓ 记录操作目标与状态变化 ↓ 判断有没有出现有效新信息 ↓ ┌──────────┼──────────┐ ↓ ↓ ↓ 正常探索 疑似停滞 明确阻断 ↓ ↓ ↓ 继续执行 提醒/重规划 有条件终止 ↓ Agent 准备宣布完成 ↓ 验证目标是否达成具体落地时我会优先做四件事保证信息完整先处理工具输出、上下文压缩和关键状态保留避免模型因为信息丢失而重复探索。建立统一执行记录记录每次操作的目标、结果和状态变化让后续判断有事实依据。分级处理异常行为不要发现重复就终止先区分正常探索、需要重新规划的停滞以及有充分证据的阻断。独立验证最终结果模型说任务完成了还需要实际业务状态作为支撑。至于到底重复多少次才提醒、什么样的状态变化算进展则应该根据 Agent 的业务场景单独设计。比如 Coding Agent 更关注代码修改和测试结果浏览器 Agent 更关注页面状态和操作副作用运维 Agent 更关注服务健康和外部依赖。它们不需要拥有完全相同的规则但可以共享相似的 Runtime 设计思路。我觉得这也是做 mini-claude 最值得沉淀的东西之一具体的循环检测规则可能会随着产品变化而失效但“先保证观察正确再判断是否进展最后决定如何干预”这套方法是可以迁移的。十一、目前还没有解决的问题虽然 mini-claude 已经做了不少尝试但我觉得距离真正可靠的 Coding Agent Runtime还有一些值得继续研究的方向。1. 什么才算真正的 Progress目前我们可以观察文件变化、测试结果、重复失败和状态振荡。但这些都不等同于对业务目标的完整理解。比如 Agent 修改了某个关键函数测试仍然失败但实际上已经解决了其中一个隐藏问题。如何更准确地识别这种中间进展同时避免额外调用大量 LLM 进行判断仍然是一个很有意思的问题。2. 怎样区分合理重试和无意义循环例如等待服务初始化INITIALIZING ↓ MIGRATING ↓ VERIFYING ↓ READY多次健康检查虽然调用相同但状态一直在变化这是合理的。反过来如果INITIALIZING ↓ INITIALIZING ↓ INITIALIZING也不能直接说明服务永远不会恢复。实际系统中可能还需要考虑时间预算、预期启动时间、外部依赖以及恢复路径。3. 如何衡量上下文丢失造成的重复探索这是我接下来比较感兴趣的方向。传统的重复调用指标通常只关注工具序列。但我更想知道某次重复读取前是否发生了上下文压缩之前读取到的关键内容是否仍然存在于模型实际收到的上下文中重复调用是在补充新信息还是重新获取丢失的信息如果改变上下文策略真实无效重试会减少多少如果能够建立这类关联就有机会从产生原因上治理重复行为而不是只在工具执行阶段拦截。4. 反循环机制能不能更少依赖固定阈值目前系统仍然使用有限历史窗口和一些工程阈值。例如最近多少次失败、重复多少次后需要重新规划。但不同任务的合理执行长度差异很大。一个简单的文件修改任务连续失败三次可能已经值得介入。一个复杂的环境配置任务连续尝试三次却未必代表异常。如何在控制成本的前提下让治理策略适应任务本身的特点仍然有不少探索空间。十二、最后聊几句虽然这次实践发生在 Coding Agent 中但我更希望这些经验能帮助自己以后设计其他 Agent而不是每换一个场景就重新踩一遍相同的坑。回顾这段经历我发现自己的思路发生了一个比较明显的变化。最开始我想解决的是模型为什么这么笨总在重复做同一件事后来逐渐变成模型在每一轮究竟看到了什么它为什么会选择这个操作上一次失败的信息有没有正确传递我们的 Runtime 有没有把正常探索误判成死循环这种变化让我意识到Coding Agent 的可靠性不仅取决于模型本身。上下文管理、工具执行、状态观察、失败恢复和 Runtime 控制都会影响模型最终表现出来的行为。如果上游给模型的信息就不完整下游的治理模块再严格也未必能让 Agent 更聪明。有时候最有效的死循环优化可能不是再增加一条检测规则而是让模型能够记住之前已经做过什么。而有时候真正需要优化的恰恰是那个一直声称“检测到死循环”的检测器。留给大家讨论的几个问题如果你也开发过 Agent我比较好奇你们会把相同工具连续调用多少次视为异常还是完全依赖执行结果判断如果 Agent 压缩上下文后重新读取之前的文件你觉得这是正常恢复还是应该被计入无效重试对于死循环治理你更倾向于保守放行还是及时终止以控制 Token 成本欢迎在评论区分享实际遇到的情况尤其是那些看起来像死循环最后却发现根因在其他模块的案例。关于 mini-claude本文提到的机制基本都来自我正在开发的开源项目GitHubhuwuhu6/mini-claude项目主要围绕 Coding Agent Runtime 展开除了本文的死循环治理还在探索工具调用、上下文压缩、失败恢复、运行时状态管理、Checkpoint、Trace 和自动化评测等工程问题。目前仍然在持续迭代。如果你对这些方向感兴趣可以去 GitHub 看看源码和项目演进文档。有想法或者发现问题也欢迎通过 Issue 交流。如果这篇文章对你有帮助也欢迎给项目点一个 Star。参考资料mini-claude GitHub 项目mini-claude上下文管理演进记录mini-claude工具去重与循环治理演进mini-claudeAnti-Loop Benchmark 审计AnthropicEffective context engineering for AI agentsAnthropicManaging context on the Claude Developer PlatformReAct: Synergizing Reasoning and Acting in Language Models

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑