Skip to content

第 5 章 循环设计

保持循环简单,复杂度下沉到工具和提示词。这话说起来简单,但我花了一个项目的时间才真正信服它。

开篇:两条路一轻一重,把自己绕进去了

我做的那个有记忆功能的 Agent,跑在一个群聊场景里。群聊这件事有个特点——消息很多,但真正需要 Agent 响应的很少。大部分消息它只需要"看到",记下来就行;只有被直接点名的时候,才需要认真回复。

为了处理这个区别,我做了一个看起来很合理的设计:两条路

一条叫"轻路径",处理那些不需要回复的消息。Agent 收到消息,存个记忆就完事,不打扰群聊。另一条叫"重路径",处理被直接点名的消息。这条路径要走完整流程——先判断意图,再读写记忆,最后生成回复。

设计的时候我觉得这很清晰:轻的走轻路径,重的走重路径,各司其职。

用起来才发现完全是另一回事。

第一个问题:两条路径纠缠在一起。轻路径在某些情况下会回调重路径,重路径又要去查轻路径存的记忆。代码里出现了一个调用链,我自己都快看不懂了。

第二个问题:上下文是断的。用户没被点名时,轻路径只是默默存了记忆;用户被点名时,重路径要回复,但它看不到轻路径当时的上下文,只能去翻记忆库。这一翻,信息又对不上。Agent 表现得像失忆——明明半小时前群里聊的事,它回复的时候好像完全不知道。

第三个问题:没有中间态。我的设计只有两个选择——要么完全不响应,要么走完整的重路径流程。但现实里很多情况是介于两者之间的:Agent 注意到了某件事,但判断暂时不需要行动。这种"我看到了,先记着,不打扰"的状态,我的两条路设计根本表达不了。

折腾了两天,我最终只能无奈地告诉 Claude Code:我在测试和使用的过程中对现有的循环非常不满意。

不满意之后:又去调研了一圈

不满意之后怎么办?我没有急着改代码。老规矩——让 CC 帮我调研。

这一次我让 CC 去研究 agent loop 的设计——主流的 Agent 项目都是怎么组织执行循环的。CC 搜了一圈,研究了六种主流的循环模式,还逐一拆解了几个知名项目的源码。调研结果我让 CC 整理成了一份文档存下来,方便后面反复对照。

看完那份调研报告,我得到一个让自己意外的结论:所有成熟项目的核心循环,都出奇地简单。

简单到什么程度?就是一句"模型决定调什么工具,拿到结果,再让模型决定下一步"——一个循环,反反复复。没有复杂的状态机,没有"先做 A 再做 B 然后判断 C"的多阶段编排。模型自己在循环里决策:要不要调工具、调哪个、什么时候停。

这个发现让我重新审视自己的两条路设计。我的问题到底出在哪?

出在我把流程编排塞进了循环

"轻路径还是重路径"——这本该是模型在循环里自己判断的事。模型收到一条消息,它完全有能力判断"这条消息我需不需要回复"。但我没让它判断,我在代码里用 if-else 替它决定了:满足某个条件走轻路径,满足另一个条件走重路径。

这一替它决定,就出了问题。因为真实的群聊消息,不是非轻即重的——有大量中间状态,有大量"看情况"的判断。我用 if-else 切成两条路,等于把一个连续的决策空间,硬切成两个离散的格子。挤不进这两个格子的消息,就表现得各种别扭。

重构:统一成一条路

想通这件事之后,我做了一个决定——把两条路合成一条

新的设计很简单:所有消息走同一个循环。循环里,模型自己决定一切。它决定要不要调工具、调哪个工具、什么时候停止。我不再在代码里替它做"轻还是重"的判断。

但"统一"不等于"放任不管"。我做了两件事来保证边界:

第一件事,用消息格式区分场景。 被 @ 的消息,格式是"用户某某对你说:xxx";没被 @ 的消息,格式是"群聊消息 — 某某:xxx"。然后在系统提示里加一条规则:"当消息以'群聊消息'开头时,你只是在观察,不要回复。"

就这么一个格式上的区别,模型就自然知道什么时候该说话、什么时候该闭嘴。不用我在代码里写 if-else。

第二件事,保证"触发了就一定有回复"。 如果 Agent 被 @ 了,走完整个循环却没产出任何文字(比如光顾着调工具忘了说话),我会强制再调它一次,让它用自然语言总结一下刚才做了什么。这是一个"回复保证"——用户点了 Agent 的名,它不能装聋作哑。

这个设计我到现在还在用。它比之前那两条路清晰太多了。上下文不会再断——因为只有一条路,所有消息共享同一个上下文。不再有中间态缺失——模型自己判断要不要回复,连续决策,不被离散格子切割。

一个更深的拆分:触发 ≠ 响应

统一循环之后,我顺手解决了一个之前一直困扰我的概念问题——触发和响应是两件不同的事。

在我的旧设计里,这两件事是绑在一起的:被触发(被 @)就必须走重路径全流程响应,没被触发就必须走轻路径完全不响应。这是一个二分法,没有中间地带。

但真实的助手行为不是这样的。想象一下一个助理在一个会议室里:有人直接问助理问题(触发),助理会回答(响应);有人在闲聊提到一个助理关心的信息(触发但弱),助理可能会记下来但不插嘴(触发但不响应);有人在说完全无关的事(没触发),助理直接忽略(不触发不响应)。

触发和响应是两个独立的决策,不应该绑在一起。

我把这个拆分做成了一个三层决策模型:

  1. 需要关注吗? 这条消息跟 Agent 有关吗?(触发判断)
  2. 需要回复吗? Agent 要不要说点什么?(响应判断)
  3. 需要做复杂处理吗? Agent 要不要调工具、查记忆、走完整流程?(处理深度判断)

这三层是独立的。可以"关注但不回复"(群里有人 @ Agent,但 Agent 判断不需要插话),也可以"回复但不做复杂处理"(简单问答回一句就行)。

这个拆分的落地,就是前面说的消息格式区分。模型通过消息格式知道自己是"被问到"还是"在偷听",然后自己做三层决策。代码层面几乎没增加复杂度,但 Agent 的行为变得自然多了——不再是非黑即白的"要么装聋要么全响应"。

循环简单,但护栏不能省

讲到这里,我必须补一个重要的提醒,免得你产生"循环越简单越好,那就啥都不用管了"的误解。

循环要简单,但循环外围的护栏要显式。

什么叫护栏?就是那些保证"循环出问题时不会彻底崩掉"的机制。我在重构时加了几个:

  • 循环次数上限:最大迭代次数从早期的 5 次提到了 15 次。不是无限循环,到了上限就强制收尾。
  • 回复保证:前面说的那个——循环走完如果没产出文字,强制再来一次纯文本总结。
  • 卡住检测:如果模型连续好几次调用同一个失败的工具,或者反复在同一个地方打转,系统会注入一条提醒,让它换个方式。这一招是从另一个项目里学来的,后来成了我所有 Agent 的标配。

这些护栏的共同特点是:它们不干预模型的决策自由,只在模型"卡住"或者"跑偏"的时候兜底。循环本身依然简单——就是"模型决策 → 调工具 → 拿结果 → 再决策"。复杂度没有堆到循环里,而是下沉到了这些外围的护栏配置上。

这才是"复杂度下沉"的真正含义。不是不要复杂度,是把复杂度从"循环的控制流"挪到"工具的设计"和"护栏的配置"上。循环本身保持清澈。

但是——别拿着锤子找钉子

讲到这里,我得给自己泼一盆冷水,也给这一章加一点平衡。

我前面说的"循环要简单、别用复杂状态机",这条经验是针对模型做自主决策这个场景的。当 Agent 需要在开放环境里自己判断"下一步干什么"时,简单循环加好工具,确实比复杂状态机强。

但这不代表状态机就一无是处。状态机不是Agent的敌人,它有它适用的场景。

后来我做另一个项目,里面有一块功能是"定时数据流水线"——按固定的时间表跑一系列数据处理任务:刷新数据、做聚合计算、扫描异常、生成报告。这些任务的流程是固定的、确定性的,每一步做什么、什么顺序,都是事先定好的,不需要模型在里面做任何自主决策。

这种场景,我用的是什么?一个声明式的薄状态机。我把这些任务拆成了几条流水线,每条流水线就是一组有序的步骤,每个步骤包一个具体的处理函数。执行的时候逐步落库,单步失败不阻断后续,整体状态只有成功、部分成功、失败三种。

它是个状态机吗?是。它清晰吗?非常清晰。因为每一步都是确定性的,不存在"看情况"的判断,所以用状态机组织比用循环自然得多。

这个经历提醒了我一个道理:我们是在做产品,不是在做纯 Agent。Agent 只是产品的一部分。一个产品里有需要模型自主决策的部分(用简单循环),也有流程固定确定的部分(用状态机)。道理本身不复杂,但当你整天沉浸在 Agent 开发里的时候,方向容易跑偏——不知不觉就拿着"循环"这把锤子到处找钉子了。

什么时候用循环,什么时候用状态机?我的判断标准很简单:

  • 如果"下一步干什么"需要模型来判断 → 用简单循环
  • 如果"下一步干什么"是事先确定的 → 用状态机(或者干脆就是普通的流水线代码)

很多 Agent 项目会混合这两种场景。拿我那个项目来说,用户对话的部分是循环(模型自主决策怎么回复),定时数据流水线的部分是状态机(固定步骤按顺序跑)。两者并存,各管各的,谁也不欺负谁。

这一章的工具:循环设计检查清单

🔧 循环设计检查清单

设计 Agent 的执行循环时,逐条过一遍:

循环本身

  • [ ] 我的核心循环能用一句话描述吗?("模型决策 → 调工具 → 拿结果 → 再决策"——如果你描述不出来,说明太复杂了)
  • [ ] 我有没有在循环里塞多阶段编排?(如果有,想想这些阶段是不是模型自己能判断的)
  • [ ] 我有没有用 if-else 替模型做了它本该自己做的决定?

触发与响应

  • [ ] 触发和响应是不是绑死了?(被触发就必须全流程响应?)
  • [ ] 有没有"关注但不回复"的中间态?
  • [ ] 能不能通过消息格式或提示词,让模型自己区分场景?

护栏

  • [ ] 循环有没有次数上限?到了上限怎么办?(不能直接退出让用户啥也收不到)
  • [ ] 有没有"回复保证"?(触发了就一定要有文字回复)
  • [ ] 有没有"卡住检测"?(反复调同一个失败工具、原地打转)

该用循环还是状态机?

  • [ ] 这块功能的"下一步"是模型决定的,还是事先确定的?
  • [ ] 如果是事先确定的,我有没有为了"统一"硬套循环?(反过来,如果是模型决定的,我有没有为了"可控"硬套状态机?)

小结

循环本身保持简单——模型决策、调工具、拿结果、再决策。复杂度别堆在循环里,下沉到工具和护栏。剩下的就是看场景选工具:模型自主决策的部分用循环,确定性流程该用状态机就用状态机,别拿着锤子找钉子。

到这一章为止,"怎么把一个 Agent 做出来"的基本功讲完了:颗粒度怎么控(第 3 章),工具怎么设计(第 4 章),循环怎么搭(第 5 章)。但做出来只是第一步——你怎么知道它"能用"?这就是下一部分要讲的:验证。

下一章

第 6 章 · eval-driven 开发 —— Agent 时代的测试驱动开发。先写评测用例,再写功能。