提炼编码智能体的学习经验
Repo: https://github.com/voku/agent-loop
Demo: https://voku.github.io/agent_loop_demo/
你的编码智能体缺的不是记忆,而是一个有规可循的执行循环。
编码智能体总是重复犯错。最直观的解决方法是给它塞更多记忆:
- MEMORY.md
- project-rules.md
- agent-notes.md
- lessons-learned.md
- MEMORY_FINAL.md
没多久,智能体手里就堆满了过去的决策、临时补丁、复制的对话记录、已被放弃的想法,还有一堆谁都不记得当初为啥批准的规则。上下文确实多了,但未必更好。在某些节点,记忆就变成了垃圾填埋场。
问题不在于智能体忘得太多,而在于大多数工作流程根本没分清:什么是临时上下文,什么是证据,什么是待确认的学习内容,什么是已经批准的项目指导。
对话记录不是记忆。
随手笔记不是规则。
一个发现不代表指导方针。
一次成功的补丁也不自动成为项目规范。
所以我没让智能体扛一个越堆越大的上下文大杂烩,而是围绕一个有规可循的工作流构建了 voku/agent-loop:
- 任务
- 已批准的计划
- 选择性回查
- 实现
- 验证
- 记录证据
- 审核学习内容
从已批准的范围开始
编码智能体不应该一上来就读取工单,然后自行脑补工单里没写的东西。它应该从一个明确的作业简报开始:目标是什么、允许做什么、不做什么、影响哪些文件、需要哪些验证、以及人类批准。举个例子:
vendor/bin/agent-loop workflow plan PROJECT-123 \
--by lars \
--learning-root infra/doc/agent-learning \
--file src/Order/OrderService.php \
--file tests/Order/OrderServiceTest.php \
--goal "拒绝无效的订单状态转换" \
--scope "订单状态验证及其测试" \
--non-goal "不要重新设计订单聚合" \
--validate "composer phpstan" \
--validate "composer test"
然后由人来批准这个特定版本:
vendor/bin/agent-loop workflow approve PROJECT-123 --by lars
精简智能体的编码经验
当计划变更时,旧版本作废,新版本需要重新审批。这听着有些官僚,直到某天智能体未经任何人要求就进行了一次技术上很厉害的重构。审批应针对具体计划,而不是永久绑定到一个任务ID上——它的含义可能会悄悄改变。
少加载上下文,而非更多
智能体通常需要两类上下文:项目指导;代码结构。agent-recall-compiler 负责选取任务相关的指导。agent-map 提供相关代码的紧凑信息。这两者分开,是因为它们回答的问题不同:
召回(Recall):
- 哪些规则适用?
- 哪些之前的决策重要?
- 什么必须验证?
地图(Map):
- 这个类定义在哪里?
- 哪些方法和依赖项相关?
- 最小的有用代码邻域是什么?
目标不是将整个仓库压缩到提示词里,而是避免加载仓库的大部分内容。把完整的AST或几个月的事务记录一股脑倒进上下文窗口,那不是理解,不过是数据批量搬运,外加异常自信的自动补全。
保持工作记忆是临时的
在执行过程中,智能体可能需要跟踪:假设;声称的文件;检查点;未决问题;中间决策。这些应归到某个任务会话里,并且可以被检查、关闭和移除。临时观察不应该悄悄变成永久项目知识。某次任务有用的临时方案,在代码变更后可能变得完全误导。
因此,一个实用的系统至少应有三层:
- 会话状态 → 临时的
- 发现 → 待审查的证据
- 指导 → 经批准的可持久化知识
把这三层合并到一个记忆文件里,就丢失了让信息可信的来源依据。
验证是循环的一部分
智能体说“做完了”并不是证据。工作概要中已经包含了预期的验证命令。循环可以检查任务状态、召回输出、会话、文件和仓库状态是否一致。
例如:
vendor/bin/agent-loop verify PROJECT-123
结果应基于仓库特定的检查,例如:
提炼编码智能体的学习经验
composer phpstancomposer testcomposer cs
具体用哪条命令并不重要,关键规则是:验证必须在实现之前声明,而不是事后写成什么样子就补什么样子。
测试通过了,也并不代表智能体没有跑偏。这就是为什么工作流验证和代码验证是相关但独立的两件事。
被检索到的规则不一定有用
"检索"可以为任务选出一条规则,但选了不等于这条规则有用。这条规则可能是:
- 有帮助(HELPFUL)
- 不相关(IRRELEVANT)
- 有害(HARMFUL)
- 没用到(NOT_USED)
- 未知(UNKNOWN)
"没用到"这一点尤其值得注意。一条规则可能被选中用于审计或调查,但实际过程中根本没人去看它。如果不区分这一点,选择的统计数据就会悄悄变成"使用"的统计数据,指标描述的其实是一个从未发生过的工作流程。
区别很简单:
- 选择 = 系统提供了
- 使用 = 智能体去查阅了
- 效果 = 到底有没有帮助
分开记录这三类信息,我们才能真正找到改进检索的依据,而不是光统计某个文件在生成的提示里出现了多少次。
学习还是得靠人
任务完成后,智能体可以记录"发现"(finding)。一个发现是一条证据,不是一条新规则。它可以变成一条"建议"(proposal),附带以下动作之一:
- ADD(添加)
- REPLACE(替换)
- DELETE(删除)
- REJECT(拒绝)
- NO_DURABLE_LEARNING(不做永久性学习)
在永久性规则变更之前,需要人工审查这条建议。
大多数任务可能都应该以 NO_DURABLE_LEARNING 结尾。修复一个 bug 不一定能提炼出一个可复用的项目规则。有时候正确的教训仅仅就是:这个 bug 修好了。这个结论也有自己的终态:ACKNOWLEDGED(已确认)。它既不是批准为规则变更,也不是驳斥为错误,只是记录:这个任务审查过了,不需要做永久性改动。
对一个状态名称来说,这听起来可能过于精细。其实不是。如果生命周期状态用错了动词,审计日志就会慢慢变得说不清到底发生了什么。
我们用这套方法审计了自己
当我们用这套方法来审计 agent-learning 自身时,说服力就更强了。审计发现了三个真实的缺陷:
提炼编码代理的教训
NO_DURABLE_LEARNING缺少语义正确的终止转换。- 有几个命令接受完整的提案路径,但传入自然的裸提案 ID 时会失败。
- 旧的结果记录会被悄悄忽略,不纳入指导统计。
这些修复随版本 0.8.2、0.8.3 和 0.8.4 发布。没有一个需要新的代理架构。它们需要:一个显式的生命周期转换;一个一致的路径解析器;一个可见的警告(而不是静默排除)。
这正是真实内部测试通常能发现的问题——不是什么革命性的自主平台,而是一个缺失的状态、一次不一致的辅助调用、一次静默的跳过。软件终究是软件,哪怕有人在 README 里加上了“agentic”这个词。
agent-loop 到底做什么
voku/agent-loop 是一个 PHP 8.3 CLI 工具,它协调了几个专注的包:
- agent-kanban — 工作与任务状态
- agent-session — 临时工作记忆
- agent-recall-compiler — 选择性项目引导
- agent-map — 紧凑代码智能
- agent-learning — 受控的持久学习
- agent-loop — 编排
它不会取代维护者、代码审查、静态分析、测试或领域知识。它只是给这些已有的工程实践增加了一个围绕编码代理的显式工作流程。
一个典型任务大致如下:
vendor/bin/agent-loop board card show PROJECT-123
vendor/bin/agent-loop workflow plan PROJECT-123 ...
vendor/bin/agent-loop workflow approve PROJECT-123 --by lars # recall, map, implementation and review
vendor/bin/agent-loop verify PROJECT-123 # close the task and evaluate findings
重要的不是 CLI 的语法,而是每一次转换都是可见的:
- 请求了什么?
- 批准了什么?
- 选择了哪些上下文?
- 改变了什么?
- 如何验证的?
- 留下了什么证据?
- 有什么值得成为持久指导?
真正的教训
编码代理不需要记住所有东西。它们需要的是:
- 明确的边界
- 有版本记录审批
- 选择性上下文
- 临时工作记忆
- 声明的验证
- 记录的结果
- 人工审核的学习
- 有意的遗忘
蒸馏编码代理学习心得
更多内存反而会掩盖糟糕的上下文管理。受控的循环(governed loop)能让它暴露出来。一旦工作流能够拒绝过期的审批、无关的指导、无依据的断言以及偶然“学到”的东西,代理就不需要表现得像记住了整个仓库。它只需要足够的、经过验证的上下文来完成一个受控任务。
仓库:https://github.com/voku/agent-loop
演示:https://voku.github.io/agent_loop_demo/
你的编码代理不需要更多内存,它需要的是一个受控的循环。
编码代理会重复犯错。显然的应对方式是给它们更多内存: