码上拾光

把编码智能体接进编排系统:四个花时间最多的坑

· #智能体#AI应用#排查

上一篇讲了研发全链路智能体的架构设计。这次把它实现出来,并且接了一个独立进程的编码智能体进去——它会在真实 git 仓库里读文件、改代码、跑编译。

接口对接本身很快。真正花时间的是四个看起来在工作、其实没有的问题,都不报错,日志也正常。记下来。

一、补丁类写了,但没接到装配上

被集成的那个编码服务用 Google ADK 做智能体,底下是 Spring AI 调模型。它的节点链上有工具调用检测、权限守卫、工具事件推送,一整套。

实测:智能体执行了 5 次工具,节点链判定「无工具调用」。

往下查,事件流里 functionCalls()functionResponses() 在 294 个事件上全是空的。仓库里有个补丁类,专门设 internalToolExecutionEnabled(false),注释写得明明白白「关闭后 ADK 事件流中的 FunctionResponse 正常返回」。但这个类在生产代码里一次都没被引用,只出现在一个测试文件里——装配的地方用的是框架自带的实现。

于是 Spring AI 在内部把工具循环跑完了,ADK 只看到最终文本。连锁后果:负责工具检测的那个节点成了死代码,权限守卫一次都没被调用过。

我把补丁接上做了对照实验:节点链、工具事件、权限守卫一起复活了——但第二轮回传工具结果时被网关拒绝,No tool output found for function call,任务反而跑不完。原因是上游走 Responses 协议,关掉内部执行后由 ADK 构造的工具结果消息,这个协议表达不了。

教训:协议层会反过来约束你能选哪种工具执行模式。也说明「读注释」不等于「读装配」——注释说的是意图,装配才是事实。

二、matches()find()

另一个模块用关键词识别用户的纠正意图,写法是:

if (REGRET_PATTERN.matcher(content).matches()) { ... }

REGRET_PATTERN(不对|不是这样|错了|搞错了|…) 这种关键词列表。但 matches() 要求整条消息完全匹配——用户发「不对,你搞错了,以后一律用 X」不会命中,只有恰好发「不对」两个字才触发。

这个模块底下挂着长期记忆的写入。所以长期记忆从来没写进去过,召回自然也是空的。整个功能在代码里完整存在,从未运行过。

配套还有两个:写入时用户 ID 硬编码成 "default"(带 TODO 注释),召回时按真实用户 ID 查;另一处直接传了字符串字面量 "userId_placeholder"。三个缺陷叠在一起,把「没生效」掩盖得很彻底。

三、幂等和人工返工是一对天然矛盾

链路里有人工确认节点:模型改完代码,人看 diff,可以通过也可以驳回带意见重做。

能力单元的写操作都带幂等键,重复调用不重复改代码——这是重试和断点续跑的基础。但驳回后要求重做,键不变就会命中缓存。

我一开始把「驳回后换一个新幂等键」写进了提示词,靠模型自觉。实测模型不改键,驳回重跑直接命中缓存,零改动。界面上一切正常:任务重新跑了、状态流转了、又回到确认页了,只是代码一个字没变。

后来把返工轮次改成由编排服务维护、在驳回时递增,提示词只负责原样拼接。驳回才真正生效。

凡是影响正确性的控制变量,都要由系统维护,不能交给模型。 提示词能表达意图,但不能保证执行。

四、「一切皆可追溯」需要一条断言守着

这套系统的核心主张是:任意一条测试用例都能回溯到源需求文档的那一行。

某次运行结束,界面提示「入库 261 条」。追溯库里查,只有 60 条。

第 61 到 261 条用例:业务库里存在、被人工确认入库了、在测试平台里能看到,但没有追溯制品、没有关系边,在追溯图上查不到

根因是两处各自正确的设计撞在一起。生成用例的能力单元为了不把整份用例回灌进模型上下文,返回时截断到前 60 条;而记录追溯的代码恰好从这个被截断的数组里建制品;确认入库用的却是未截断的完整列表。

阈值恰好是 60。小规模测试永远发现不了。

修法是给能力单元加一个不带正文的紧凑全量映射,专供追溯用,保留截断不牺牲上下文预算。更重要的是加了一条断言:「业务库已入库用例数 == 追溯制品数」,进冒烟脚本。后来连跑三轮,分别是 216/216、204/204、274/274。

这类系统级主张——「全都可追溯」「全都有审计」「全都幂等」——必须有一条自动化断言守着,否则它会在某个规模阈值之后悄悄失效,而且失效的时候一切看起来都正常。

共同点

四个问题都不报错。日志正常、状态流转正常、界面显示正常。

它们都属于同一类:降级路径太安静。异常被 catch 住回退默认值、截断被当成正常返回、幂等命中和真正执行长得一样。

所以现在我倾向于:每个降级路径都应该有独立的计数或告警,而不是一行 warn 日志;每个系统级承诺都应该有一条断言,而不只是写在文档里。


← 回到文章列表