文章展示 Favur 如何根据记录的任务状态自动生成后续指令,减少人工反复催促测试和推进工作的消息。一次公开运行中,系统生成了 607 条用户角色消息,并用重复操作计数触发提醒,推动陷入循环的 Agent 继续执行。
一个 coding Agent 完成了一部分工作,写下总结,然后停了下来。这并不意味着哪里出了问题:chat model 结束了自己的回合,下一步就轮到坐在用户席位上的人了。于是,你输入“看起来不错,现在跑测试”,一小时后,又输入了一遍。
这些消息大多不涉及判断。它们只是在交代任务进行到了哪里、接下来做什么,以及当前这部分工作做到什么程度才算完成。而负责运行任务的程序,本来就掌握这三项信息。在 Favur 的一次公开运行中,Favur Evals 背后的 harness(运行 Agent 团队的程序)记录了 607 条以用户身份发送的消息。这些消息全部由 harness 根据运行状态记录生成。
其中一条消息,一字不差地写着:
别再重读文件,也别再重复执行已经执行过的命令。你需要的信息已经齐了。
忙到一天快结束时,你也曾对 Agent 敲下过这样一句话。harness 发出这条消息,是因为一个计数器达到了十五。此次运行中,21 个 Agent 收到了这条消息,其中 17 个只收到过这一次警告。随后,这 21 个 Agent 都结束了卡住的步骤,继续推进工作。

打开你与 Agent 的一次长对话,只读自己发出的消息,就会发现它们可以分成两堆。
小的一堆,是必须由人来做的决策,比如调整范围,或者在两个设计方案之间做选择。大的一堆,是进度记账:“继续。”“这部分完成了,接着做 parser。”“先跑 linter,再说完成。”“你还有两个测试没通过。”
大堆里的每句话,都在重复程序能够从自身记录中读出的信息。任务列表知道哪些工作已经完成,test runner 知道哪些测试失败,workflow 知道接下来是哪一步,以及这一步必须产出什么。
这堆消息还带着一种语气。对话进行到后面,进度提醒会越来越简短,但“你已经检查过了”仍然是在记账。它报告的是对话中的一个事实:Agent 已经在同一个地方停留了很久。程序也能统计这一点。
这些进度信息最显而易见的归宿是 system prompt,但放在那里会出现两个问题。
携带状态的 system prompt,需要在每次请求时重新构建。当 Agent 从第三步进入第四步时,关于第三步的文字会被替换。于是,model 能看到自己当前在哪里,却没有记录说明自己是如何走到这里的。
每个回合都变化的 prompt,还会让 prefix caching 失效。prefix cache 从头开始匹配文本,遇到第一个差异就停止,所以把状态放在 prompt 靠前的位置,会让后面几乎所有内容都无法复用缓存。
user message 没有这两个问题。它会追加到对话中,并一直保留,因此,一连串这样的消息就像一本日志,记录着任务如何向前推进。这样,system prompt 就可以只保留整个任务中固定不变的内容,比如 Agent 的角色、工作标准和 workflow 概要,在一次对话中很少需要修改。
自动生成的回复,如果发错了时机,就会变成噪声。我们的 harness 会在四种时刻发送这种消息,我们称它为 progress report。
普通的 tool 调用之后,harness 会保持安静。读取文件或执行 shell 命令,只会返回结果,不会附带其他消息,因此 model 可以直接根据结果采取下一步行动。
下面是此次运行中的一份 progress report,顶层 Agent 进入 workflow 的第四步时收到了它。我们缩短了内容,把内部 tool 名和字段名换成了普通说法;结构和指令则与实际发送时一致。
# Progress Report
## Current Step: 4 - Context Init
Entry criteria: Architecture approved
Exit criteria: Six context documents created
Create the six documents. Read all six back to confirm non-empty.
### How to Advance This Step
When you have completed all exit criteria above, call the
step-completion tool with your evidence.
If you cannot proceed, call it with success set to false instead.
Do not call it speculatively before exit criteria are met.
### Step Requirements
A list of the documents created (required).
Omitting it or passing an empty list will fail validation.
Agent 的下一条消息只有一句:“六份 context 文档均已创建,并通过模板校验。进入下一步。”
退出条件会在步骤开始时送达,因此,Agent 还没开始写任何东西,就已经知道之后会检查什么。报告明确指出检查需要哪些证据,并说明空答案无法通过。它还始终提供第二条出路:报告“我无法完成这一步”,也算一次有效行动。
其他部分只有在有内容可说时才会出现。只有存在错误时,才会显示错误部分;未解决的问题和持续有效的指令也是如此。
有些部分只属于特定类型的 Agent。我们的几个 Agent 会维护故障计数器,每个计数器记录一种具体失败,比如一次出问题的委派。计数达到上限后,Agent 会被告知向上升级处理,而不是继续重试。报告会打印这些计数器。测试 Agent 的报告会重申它的工作规则:不能修复代码,连续三次出现相同失败就必须升级处理。model 深入执行一个长任务时,可能会忘记很久以前读过的规则;而打印在最新消息里的规则,则是它刚刚读过的。
当 Agent 开始原地打转时,第二条出路尤其重要。一个不断重读相同文件的 Agent,并没有停下来等待,因此,前三种发送时机都不会触发。如果有人在旁边看着,就会出手干预。
harness 则会计数。在同一个步骤内循环十五次却仍未完成后,Agent 会收到一条由三部分组成的通知。第一部分说明循环次数,并点明所在步骤。第二部分是本文开头引用的那两句话,后面列出这一步仍需提交的证据。第三部分再次提供第二条出路。对于确实无法完成的 Agent,通知要求它报告失败,并附上目前已经发现的信息,最后以“不要继续探索”收尾。
这里没有任何东西会阅读对话记录,判断 Agent 看起来是否迷失了方向。触发条件只是回合数,阈值也是普通配置。计数器无法区分一个迷失的 Agent,和一个再过两个回合就能完成的 Agent,所以措辞必须对两者都适用。它会指出出口,并要求 Agent 提交已经掌握的证据。
再循环五次后,Agent 会收到一条更直白的通知。开头说明它已经循环二十次,却仍停留在当前步骤,下一句则是:“之前的警告被忽略了。”通知要求它在紧接着的下一个回合调用 step-completion tool,并在结尾说明后果:继续探索“将导致这个 Agent 被终止”。
此次运行中,21 个 Agent 各收到了一次首次通知。其中 17 个没有收到更直白的警告。4 个收到了,其中 2 个又收到了第二次。

我们回看了保存的对话记录,逐一追踪这 21 个 Agent 从收到通知到结束当前步骤的过程。
21 个全部结束了当前步骤,workflow 得以继续。4 个在紧接着的下一个回合就完成了。14 个在三个回合内完成,最慢的用了十三个回合。

其中一个 coding Agent 一直在最终审查中打转。收到通知后的第一个回合,它就提交了完成申请,并附上要求的证据。下一条消息写道:“workflow 已进入 done 状态,所有关卡均已通过。结束任务。”
收到更直白警告的那 4 个 Agent 也都完成了,距离首次通知分别用了六到十二个回合。保存的记录显示,没有任何 Agent 因停滞而被终止。
这些通知还集中出现在某类步骤中。21 条首次通知里,有 14 条发生在同一种步骤:审查 Agent 在写出结论前收集证据。这个步骤没有自然的终点,因为总有另一个文件可以打开,而认真负责的审查者会一直打开下去。其余 7 条发生在最终审查、规划步骤,以及一个实现步骤中。

我们的解读是,计数器捕捉到的大多是过度细致。这些 Agent 已经掌握了步骤所要求的内容,只是没有人告诉它们:已经足够了。不过,这些计数无法证明是通知促成了完成,因为其中一些 Agent 可能本来就只差一个回合。
停滞通知还有一些更小的同类消息,它们的构造方式相同。
此次运行中,有两次,Agent 连续三次调用同一个 tool,参数完全相同或为空。每次 harness 都以用户身份回复。消息写着“你之前的调用被拒绝,或没有产生效果”,要求 Agent 在再次尝试前构造有效参数,并指出正确参数列在哪里:就在它最新报告的当前步骤部分。
还有一次,一个 coding Agent 尝试开启一个工作单元,但请求在 harness 内部失败了。回复说明了这一点,也承认问题可能出在 harness 自身。接着,它列出有效请求需要哪些内容,并说明 Agent 在下一回合不应调用哪个 tool。
每条消息都会说明发生了什么、给出下一步行动,并排除错误做法。model 刚刚失败后,往往会尝试一个相近的操作,而这个相近操作经常比直接重试更糟。
日志会不断增长,一次长时间运行会积累数百份这样的报告。旧报告描述的,是早已完成的步骤。
因此,每份报告在写入时都会打上标签。标签会告诉 context compressor 如何处理它。context compressor 是 harness 中负责在 context window 填满时压缩旧对话的部分。轻度压缩会保留这些报告不动;当 context window 接近满载、需要重度压缩时,则允许将它们折叠压缩。我们在此前一篇讨论 Agent 可以忘记什么的文章中介绍过这个 compressor。
人仍然可以引导任务,因为人的备注和其他消息走的是同一条通道。它会出现在下一份报告的顶部,下方仍是常规的概要内容。这样,Agent 在同一条消息里既能收到指令,也能确认自己当前的位置。
这些数字对应的那次运行,根据一份简短的工作说明,用 TypeScript 构建了一个小型桌面应用。公开运行页面记录了 174 分钟的工作时间和 1,594 次 model 请求。
此次运行保留了 110 个 Agent 对话,其中有 607 条以用户身份发送的消息,396 条是 progress report。另有 67 条用于开启 Agent 工作的新阶段,36 条带回受委派 Agent 的结果,30 条交付 Agent 请求的截图。停滞通知及其升级警告共计 27 条。剩余消息大多是各个 Agent 开始工作时收到的目标。没有一条是人手动输入的。
我们还没有在关闭这些报告的情况下运行同一个任务,所以,请把 607 理解为 harness 开口的次数。在相信这个思路之前,先拿一个任务做你自己的对照实验。
Favur 是闭源的,只能通过邀请使用,但 Favur Evals 看板上的运行页面是公开的。你可以在我们的网站上查看 Favur 如何运行它的 Agent 团队。
Agent 等着听到的大多数话,都是进度记账,而负责记账的程序就能把这些话说出来。
如需进一步采取行动,你可以考虑屏蔽此人和/或举报滥用行为。