复盘

Article / 复盘

两个项目教会我们的事

两个真实项目在权限、重复实现、文档漂移、评测复跑和范围取舍上踩过的坑与做法。

NovaCodeCitedRAG 复盘工程实践AI 应用

NovaCode 在终端里指挥 Agent 改代码,方向是把自治能力往外铺;CitedRAG 在浏览器里用证据回答问题,方向是把可核验性往深做。两个项目的模块文档记了实现细节,我更想记的是那些换个项目也成立的经验,以及它们在真实代码里付出的代价。

机制与策略的分工

策略是希望系统「尽量这样做」的安排:提示词里的叮嘱、一个阈值、一份白名单;机制是保证某件事「不可能那样发生」的结构:唯一裁决点、状态机、失败关闭。模型输出带有随机性,能用代码强制的部分,我在两个项目里都尽量放进了代码;只靠提示词约束的部分,会当作随时可能失效的策略来对待。

NovaCode 的权限链是一个机制:Plan 例外、危险命令检测、路径沙箱、三级规则文件、权限模式、人工确认,按固定顺序在同一个入口逐层判定;规则文件里 deny 始终压过 allow,规则写在哪一层都不影响优先级。用户不需要理解判定顺序,只需要知道「拒绝的规则一定赢」;在确认框里选择「总是允许」,系统会生成一条本地规则,下一次评估立即生效——确认动作本身也沉淀为机制。反例同样来自它:安全命令白名单只看命令前缀就提前放行,xargs rm -rf / 这类命令因此绕过检测;沙箱信任判定只看「开关打开且自动放行」,没有检查隔离后端是否真的可用。判定链上任何一个想当然的捷径,都会让机制退化成策略(完整判定链见权限与沙箱)。命名也要和实现一致:没实现的能力,不留一个看起来可用的名字。

CitedRAG 把同一条原则放在回答侧:引用通过解析角标、拒绝越界编号、逐句做蕴含校验来保证,任何一步无法完成就按拒答处理(失败关闭)。流式输出也先过校验再推送,看到的和落库的一致。上下文管理里有一个不那么显眼的例子:大工具结果在进入历史之前就被溢写和预算裁剪,这是机制;接近窗口上限时调用模型做摘要压缩,这是策略——它有失败率、有成本,所以配了熔断和重试。这两类机制的建立有先后:溢写和裁剪先做,摘要压缩作为补充再调。数据一致性上也有同样的对照:重复提交用幂等键与请求指纹拦截,工作区删除后用代际栅栏拒绝迟到的写入,崩溃中断的运行在启动时被标记为可恢复。这些行为都由代码结构保证,靠小心谨慎替代不了。

重复实现与共享管线

NovaCode 最大的一笔技术债是「同一语义多路径重复实现」:工具的前置检查在流式、一次性执行、非交互三条路径各有一份;取消语义在 TUI 与 Remote 是两套;三种模型协议的错误映射和用量口径各自维护。代价很少体现在行数上,更多体现在「改一处漏多处」:环境上下文在某个分支里才赋值、取消后子任务继续跑、Skill 派生的子 Agent 绕过权限链,这些分叉缺陷都能追溯到重复实现。安全路径上的重复影响更大:主入口收紧了,旁边的入口还开着。

CitedRAG 有一个正面对照:流式与非流式共用同一个角标强制函数,直答、研究者与评测共用同一条检索管线,评测的临时配置覆盖机制同时服务矩阵对比和应用内的批次评估。同样的语义只有一处实现,修一次两边受益,行为也不会漂移。两个项目里判断的标准都一样:同一个语义出现第二个调用方时,就抽共享管线;如果这条路径跨过权限、校验或计费边界,优先级再往前提。这也是两个项目都把共享做在函数级的原因:权限判定、引用校验、检索入口都有唯一的函数名,重复实现下,修 bug 的人要先找齐所有副本,评审者也很难证明没有漏改。抽象在这里的作用是让正确性只需要被证明一次,少写几行代码只是顺带的结果。

文档与代码的漂移

两个项目的文档都经历过漂移。NovaCode 在后期做过一次系统性对齐:README 里的工具数量从 28 改成 24、MCP 传输从三种改成两种、Hook 动作类别和斜杠命令数量逐一回填成代码事实。这次集中修订本身说明了一件事:漂移会持续发生,一次性的修订解决不了,后来改成从注册表与枚举自动生成能力清单,让「文档里有、代码里没接」的落差在启动时就暴露。

CitedRAG 的三处小问题指向同一类风险:README 写「30 题子集」而文件实际是 27 行;类型检查工具写进了 README 却没进入 CI 门禁;诊断接口读取的字段名与重排结果不一致,导致分数恒为 0、预览恒为空——主链路不受影响,但文档承诺的能力在界面上是静默失效的。

这类问题的修复成本都很低,伤害的是信任:使用者对系统能力的预期被悄悄扭曲。更隐蔽的是数字类漂移——题数、工具数、命令数写在最容易过期的散文里,却最常被引用;这类数字能自动统计的都不手写,不能自动统计的定期用一条命令核对。比起写更多文档,更有效的做法是让事实只有一处来源——注册表、枚举与配置结构,都比散文更可靠。两个项目里比较有用的做法有三条:文档里承诺的能力要能被验证,能自动生成的清单不手写;暂时没实现的能力让它显式失败,默认成功会把问题藏起来;README 里的命令和数字,尽量能被一条命令复算。已知问题清单保留了修复批次,能区分「有意取舍」与「尚未修好」。

评测要能复跑

不复跑的评测,过一段时间就不敢再引用。CitedRAG 的检索评测一次深扫、按不同 top_k 重判分,判分规则写进报告 JSON 的 scoring 字段,报告带时间戳;--matrix 在真实管线上对比十种检索开关组合;应用内评测页会记录 provider 与检索管线快照,让任何一次结果都能归因到当时的配置。它对口径也做了说明——「41/45」与「44/50」分母不同、不能直接比较,这一点在文档里被明确写出。

NovaCode 的测试策略是另一极:六百多个用例默认全 mock,无网络、无密钥、十几秒跑完;唯一的真实模型调用在缺少密钥时跳过;测试盲区集中在跨进程路径。拿它和 CitedRAG 对照,可以看到分层:便宜且确定性的检查(静态检查、构建、单元与集成测试)放进 CI,昂贵且带随机性的行为评测留在本地按需复跑,两边都用可复制的命令和留痕的报告连接起来。

对 LLM 应用来说,评测需要两层:一层验证确定性的逻辑(协议解析、权限矩阵、状态机、恢复流程),一层评估行为质量(检索命中、引用正确、拒答是否恰当、延迟与成本)。后者的难点是判分标准要自己建——关键词覆盖、结构化校验、语义拒答,没有现成答案。可行的路径是把判分规则当代码管理并写进报告,每次运行留下配置快照,复跑就是一条命令的事。评测的结果也用于决策:一个开关该不该默认开启,用同一批题、两组配置跑出净收益,比凭直觉判断可靠。评测集本身也要当产品维护:题目要覆盖可答、拒答、多文档比较与工具故障等不同场景,期望来源与页码要能被程序判分,中文与英文提问都要有样本,否则通过率只能说明单一场景的情况。两个项目的评测报告都保留 schema 版本与生成时间,几个月后回头看,仍能回答「这个数字是什么配置下跑出来的」。

做透一条链路还是铺开功能

单人项目的时间有限,必须在做深和铺开之间做选择。CitedRAG 选了纵深:高级检索开关(改写、多查询、CRAG)实测收益不稳或为负就默认关闭;多用户、分布式、通用 Agent 能力明确不做;省下的时间投给幂等运行、工作区代际、断线恢复、检查点与凭证隔离这些「看不见的部分」——它们不产生演示效果,却决定系统在真实环境里能不能被信任。

NovaCode 选了横向:六类扩展点全部打通(工具、Skill、Hook、MCP、模型协议、前端),还做了多 Agent 协作与工作树隔离。收益是事件流契约与协议归一化被反复复用;代价是跨进程协调用文件做了最小实现——邮箱写入非原子、任务板没有锁、投递只有至多一次语义——这些在单机单人场景够用,却经不起真实并发。横向铺开本身没有问题,问题在于没有为每个新能力检查它是否共享同一套机制:只要引入第二条权限、上下文或状态路径,就会产生分叉成本。

复盘得出的结论是给取舍排一个顺序:决定产品可信度的那条主链路先做透;往外铺能力之前,先确认它能复用现有的信任与状态机制;不能复用的部分,把失败模式和边界写进已知问题清单,让债务可见、可排序。更常见的错误顺序是先铺功能再补一致性:功能可以演示,一致性问题往往要到真实压力下才暴露。把顺序倒过来,先让一条链路在幂等、恢复、隔离上闭环,再加能力,后面的扩展会省力得多。

反复出现的三个难题

AI 应用工程还处在方法论没定型的阶段,我在两个项目里反复遇到三个难题。第一是测试的概率性:输出带随机性,判分标准得自己建,所以确定性的逻辑和行为质量要分开测。第二是上下文的经济性:token 是持续成本,prompt cache 的前缀稳定性、工具结果的体积、压缩时机都属于架构问题,不是调参能解决的。第三是安全的边界感:Agent 能执行命令、能读写文件,信任要按入口显式定义——交互式终端、脚本、浏览器、子 Agent 各自生效什么权限与隔离;靠默认值会留下说不清的缺口。

回头看,我最想保留的三种做法是:接口契约定在实现之前,事件流、流式协议、配置结构都属于这一类;同一语义收敛到一条路径,尤其是安全与校验;把「默认拒绝、显式放行」作为兜底,能失败关闭的校验不开失败开放。另外,失败也需要产品语言:拒答、超时、部分完成都说明发生了什么、能不能重试、下一步做什么,让系统的边界对用户透明,而不是只抛出一个栈回溯。模型会换代,协议会更名,但这两个项目里,把随机性约束在边缘、把确定性机制放在核心的做法一直没变。