导学阶段-NPC 把 Agent 挂到仓库里
本文整理自 CNB 上的开源课程 《Vibe Agent:从零搭建 Coding Agent》。课程讲义、示例代码与练习仓库均在 CNB 上公开,文中示例出自该仓库对应章节。
前几节做完的 Agent 跑在自己的终端里。要让它干活,需要打开命令行、输入任务、等待返回。这套流程对个人够用,对团队不行:同事不会为了请人看一个 bug 而先学会启动一个脚本。
NPC 处理的是交付方式。它把同一个 Agent 搬到仓库里,用事件触发,用评论交付。游戏里的 NPC 不主动行动,玩家走上去对话它才给任务;仓库里的 NPC 同理,没人 @ 它的时候它不存在,有人 @ 就接活,干完把结果发回同一条评论。
两种形态的差别
| 维度 | 本地 Agent | 仓库 NPC |
|---|---|---|
| 谁在跑 | 本机 | 云端容器 |
| 谁触发 | 仅本人 | 仓库里任何有权限的人 |
| 何时跑 | 手动启动 | 事件触发 |
| 结果去哪 | 终端 | 自动回写评论 |
关键变化在最后一行。本地跑的时候,操作者是唯一的读者,输出打在屏幕上就完事;挂到仓库之后没有屏幕可看,评论就是结果本身。这个差别决定了入口脚本的写法。
三样配置
把 Agent 变成仓库 NPC,需要准备三样东西。
角色设定定义这个 NPC 是谁:名字、头像、口吻、擅长什么、遵循什么约束。这段文本在每次任务时拼进 prompt,相当于一段 system prompt 前缀。
事件绑定定义谁触发、跑什么。平台需要知道评论里出现哪个名字对应执行哪条流水线,因此要建立角色名到镜像的映射,并约定没有点名时的默认角色。
入口脚本是容器的启动点。它从平台注入的环境变量里读出评论内容、仓库地址、issue 编号,组装成任务描述,驱动 Agent 执行,最后把结果发回原评论。
三样东西里入口脚本最容易写错。它运行在非交互容器里,没有终端,没人看得见它的输出。任何只打印不发送的实现,效果都等于什么都没做。
一次触发经过的环节是这样:评论 @ 角色,平台匹配事件,拉起镜像,入口脚本读环境变量并组装 prompt,Agent 干活,回写评论。中间的 Agent 部分与前几节完全一致,改变的是输入来源和输出目的地。
另一种触发源:日志
评论触发适合”人发现问题之后求助”的场景。还有一类问题不需要人发现,系统自己就会报错,这时触发源是日志。
HyperTicket 的 log_ai_detector 是一个可以直接对照的实现。它把触发、定位、回写三件事拆成独立模块,串成一条常驻链路:
1 | 日志轮询(inode + offset,支持滚动) |
触发源不同,后面几步的设计就跟着变。评论内容是自然语言,模型可以从中读出意图;日志是一行结构化文本,必须先解析出错误级别、源文件、函数名和行号,模型才有定位的起点。这也是这类系统把日志解析单独抽成一个模块的原因。
从日志行到源码位置
日志能说明”哪里炸了”,说不清”为什么炸”。从前者走到后者,靠的是代码索引。
log_ai_detector 把 codegraph 的调用封装成可配置命令模板,索引、检索、增量更新三类命令都写在环境变量里,上游命令行变化时不必改代码。检索用的查询词就是日志里解析出来的函数名、源文件名和错误消息片段,一次失败可以换关键词再试,最多取三段结果。
索引不可用时有一条降级路径:按日志里的源文件名在项目里定位文件,取报错行上下各二十行作为上下文。这条路径精度低,但比没有强,也让整个系统不依赖单一外部工具。
索引还需要跟着代码变。仓库里有一个 git hook:检测到 feat 类型的提交,就增量更新 codegraph 索引,其他类型的提交跳过,更新失败不阻断提交。没有这一步,索引很快会和代码脱节,定位质量随提交次数下降。
拼一个够用的上下文
定位日志问题需要的材料不止一段源码。这个实现把四类信息拼在一起送进模型:
- 触发事件的原始字段:时间、级别、线程、文件、行号、函数、消息;
- 该行前后的日志上下文,放大器场景下按字节截断;
- 另一侧的联调日志:事件来自前端就附上后端日志尾部,反之亦然;
- 代码索引结果,以及最近一次 feat 提交的 diff。
第三项值得单独说。前后端分离的系统里,一个前端报错往往只是后端失败的结果,只看一侧日志容易把责任判错方向。把同一时间窗口内另一侧的日志尾部一起给出,模型才有可能判断问题出在协议、数据还是某一侧的代码。
第四项解决的是”最近改了什么”的问题。大量线上错误由最近的提交引入,把 feat diff 作为线索附上,定位命中率会明显提高。
交付形态也不一样
评论触发的结果是回复,日志触发的结果是报告。
这个实现让模型按固定章节输出:摘要、严重程度、检查方向、检查思路、问题定位、修复建议、需要补充的信息。修复建议要求给出具体文件、函数、行号,以及修改前后的 diff,涉及数据修复时给出带 WHERE 条件的 SQL 并提醒先查影响行数。
报告写入报告目录,同时通过 webhook 或邮件推送,另有一个 Web 界面可以查看历史报告、切换模型、对单个日志文件触发一次分析。这就是这条链路里”回写”的位置:评论型 NPC 写回评论,日志型 NPC 写回报告与通知。
为什么不做成自动改代码
这套系统有定位能力,也有生成修复方案的能力,但没有让模型直接改代码。
原因有两层。一层是证据:日志只记录了失败瞬间的状态,很多错误需要复现步骤、数据库现状或配置才能确认,仅凭一段上下文给出的修复方案可能是错的。另一层是影响面:自动修改意味着自动提交,错误修改会进入主干并影响其他人。
所以它的提示词里明确要求只给建议,不输出任何要求系统自动执行的指令或结构化修复块,所有修复由开发者人工评估后执行。这个边界不是能力不足,而是对”谁来承担后果”的回答。
评论触发的 NPC 有一条类似的边界。它可以读代码、跑命令、给结论,但写操作要限定范围,危险操作要拦截,改动要能从评论记录里还原。
打包成镜像
课程把 Docker 打包留成课后实验,这里补一条能直接照做的最小路径。
镜像是把内核和入口脚本装在一起:前者是前面几章写好的 LLMClient、ToolRegistry、Agent 与工具集,后者负责读事件、调内核、写回结果。依赖控制在标准库加一个 SDK,镜像体积和启动时间都可控。
1 | FROM python:3.12-slim |
入口脚本的骨架只有三步:读环境变量、驱动 Agent、写回结果。
1 | import os |
变量名取决于平台注入什么,具体以平台文档为准;这里用一组自定义名字示意,入口脚本负责把它们翻译成任务描述。写回评论通常走平台的 API,或者在流水线配置里把标准输出接到评论接口,两条路选一条即可。
镜像做好之后,事件绑定就落在仓库的流水线配置里。课程仓库自己用的写法是把角色名映到一条流水线,stage 只声明镜像,由镜像的入口接管执行:
1 | 小光: |
有几个容器环境特有的约束值得提前处理。第一,没有终端,所有中间输出都必须落进日志或报告;第二,进程退出即销毁,回写评论这一步必须在退出前完成,超时时间要显式设置;第三,失败也要回写,否则用户只看到一条永远没有回复的评论;第四,镜像里不要放长期有效的凭据,用平台注入的服务身份。
落到自己项目上
把 Agent 做成”挂在工作流里自动干活”的形态,要回答的问题其实只有三个。
哪个事件值得触发。评论区里有人 @,日志里出现 ERROR,提交里出现 feat,都是明确的事件源。事件选得越准,无效调用越少。
去哪里找定位信息。代码索引、最近提交、日志上下文、联调日志,这几类材料决定了模型能看到什么。没有索引就只能在文本里猜,定位质量会差一个量级。
结果交付给谁。评论、报告、通知、Web 界面,选一个真正会被人看到的位置。跑完没人知道,等于没跑。
能力来自工具集,行为来自 Skills,可达性来自 NPC。三者叠加,一个 Agent 才具备被他人使用的条件。缺任何一环,它都还停留在”作者自己的工具”这个阶段。


