导学阶段-整合与形态
本文整理自 CNB 上的开源课程 《Vibe Agent:从零搭建 Coding Agent》。课程讲义、示例代码与练习仓库均在 CNB 上公开,文中示例出自该仓库对应章节。
前面几章已经把零件焊完了:一个能对话的 LLMClient,一个能注册工具的 ToolRegistry,一个带循环的 Agent,一套上下文与记忆组件,外加五个编程工具。这一节不引入新功能,只处理两个工程问题:这些零件按什么顺序拼装,拼好之后能做成什么形态。
手上的零件
| 来源 | 组件 | 主要方法 | 职责 |
|---|---|---|---|
| 第一章 | LLMClient | chat、count_tokens | 调模型、数 token |
| 第二章 | ToolRegistry | register、to_schemas、invoke | 注册工具、生成 schema、按名分发 |
| 第三章 | Agent | run(user_message) | 持有循环,调模型、执行工具、回灌结果 |
| 第四章 | ContextManager | should_compress、compress | 历史过长时压缩 messages |
| 第四章 | Memory | add、search | 存长期事实、按语义召回 |
| 第五章 | 编程工具 | read、write、edit、list_dir、bash | 读写文件、执行命令 |
自定义工具用 register(func, description),第二个参数是写给模型看的说明;第 5 章的五个编程工具由 build_registry(workdir=...) 一次装配,注册时顺带把工作区注入进去。
组件之间有两种关系,混在一起看容易糊涂。一类是结构上的持有关系:Agent 构造时注入 LLMClient 与 ToolRegistry,这两个缺一不可;ContextManager 与 Memory 是可选的,不注入也能跑。另一类是运行时的协作次序:一次 run 开始时先召回记忆、取工具清单,然后进入循环,每轮判断是否需要压缩,再调模型,有工具调用就执行并回灌,直到模型不再要求调用为止。
所有组件都围绕一个数据结构转:messages。记忆拼进它,压缩裁剪它,模型读它产出结果,工具结果又写回它。理解这一点,后面的组装步骤就只是摆放位置的问题。
先让它跑起来
组装不必一步到位。工程上常用的节奏是:先能跑,再扛得住,然后才谈好看。第一阶段只需要三个组件。
1 | from llm import LLMClient |
这套代码已经是可用的 Coding Agent。它会自己读文件、改文件,再返回一句总结。循环、工具分发、消息回灌都封装在 Agent 内部,调用方拿到的契约只有一条:传进一句话,返回一个字符串。
再让它能连续对话
真实使用很少一次结束,通常是一来一回。把 run 放进循环就是 REPL:
1 | def run_repl(): |
这里有个坑值得单独说:agent 实例必须在循环外创建。多轮对话的历史保存在实例内部,每轮新建一个实例,第二句话时它就不知道第一句聊了什么。很多”Agent 失忆”的报障,根源是把构建逻辑写进了循环。
循环能用了,还差三件小事才算合格。第一是异常处理,用 try/except 捕获 EOFError 与 KeyboardInterrupt,否则 Ctrl+C 会把整个进程带走。第二是退出与重置,/quit 结束进程,/reset 丢掉历史重建实例。第三是过程可见,run 只返回最终字符串,中间调了什么工具用户完全看不到,需要在 Agent 的循环里打印工具名、参数和截断后的结果。
第三件事看似是体验问题,实际影响信任。用户看不到 read 了哪个文件、bash 跑了什么命令,就无法判断结论是否可靠。
外挂式增强
对话变长、需要跨会话记住事实时,再把第四章的两个组件接进来。它们不修改 Agent 内核,只在调用前后加一层。
1 | def run_with_memory(agent, user_input): |
ContextManager 的接入点在内核循环的开头。每轮先问一次是否超长,超长就压缩,再交给模型。两个组件的共同点是都可以缺席:没有记忆,Agent 只是每次重新认识用户;没有压缩,长对话迟早撞窗口。缺席不会立刻报错,所以更容易被忽略。
剩下的是显示层。把 print 换成 rich 的面板与语法高亮,内核一行都不用改。界面美化与逻辑增强在这里彻底分开,这也是判断一个 Agent 代码结构是否健康的小测试:如果换个终端样式需要动循环,说明耦合写错了地方。
一个内核,三种壳
内核组装好之后,它面向哪些场景使用,取决于外面套哪层壳。
REPL 是多轮交互形态,代表是编辑器侧边栏与终端里的编程助手。Solo 是一次性任务形态,给一个任务、跑完退出,适合 CI 脚本和定时任务,比如每天自动审 PR、流水线失败时生成分析报告。WebUI 是浏览器形态,适合查看 diff、贴截图、做演示。
Solo 的实现只是不进循环:
1 | def run_solo(query, output_file=None): |
有一个细节容易被忽略:Solo 模式下,工具调用、调试日志这类过程信息应该走 stderr,最终结果走 stdout。这样用户用 > 重定向或接管道时,拿到的文件里只有答案,不会混入中间过程。这条约定看起来不起眼,却是 Agent 能被脚本组合的前提。
三种形态共用同一个 build_agent 与同一个 run,main 函数里分流即可。形态是壳,内核只有一套。
组装顺序本身就是设计
回头看这五个步骤,顺序不是随意的。三件套先跑通,说明最小闭环成立;REPL 暴露上下文归属问题;打磨阶段暴露可观测性问题;接入记忆与压缩时,才需要决定哪些能力是可选的;最外一层只换显示。
每一次都是在上一版能跑的前提下加东西,任何一步出问题都能立刻定位到刚加的那层。这和”先把所有组件都接上再调试”是两种完全不同的排错体验。
下一节讨论两个部署层面的问题:业务同事写的规范怎么在不改代码的前提下装进 Agent,以及怎么把这个 Agent 挂进代码仓库,让它在有人提 issue 时自动接手。


