ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

让AI Agent严格遵守代码规范:规则优先级与反馈回路实战

让AI Agent严格遵守代码规范:规则优先级与反馈回路实战

先说个真实场景。某个周二下午,我打开一位同事提交的代码评审请求,里面是某后台管理模块的批量更新接口,整个实现非常干净:类型注解齐全、函数拆分清晰、异常处理覆盖到位。但我往下翻了两屏,心就沉了——所有服务类的命名用的是缩写风格,和我们仓库里约定俗成的全称命名完全相反;日志里没有任何traceId链路信息;还引入了旧模块的废弃工具函数,理由只是"它看起来能用"。代码本身不差,但它不像"我们这个团队写出来的代码"。

这位同事不是新人,他背后其实是一个已经在团队里跑了一个季度的AI编码代理,内部代号就叫Pi Agent。这件事让我意识到一个扎心的事实:让AI Agent写出能正常运行的代码,已经不难了;难的是让它写出"符合公司规范的代码"。这就引出了这篇文章的主题——如何让AI Agent老老实实遵守公司代码规范。

我花了大半年时间在不同场景踩坑、调整、验证,把一套"规范落地方法论"打磨了出来。这篇文章会聊几个核心问题:Agent为什么不遵守规范?怎么把规范从wiki里搬进Agent的执行逻辑?怎么用工具链反馈回路让Agent"被规范"?以及这条路的边界在哪里。如果你也正在从事相关探索,或者准备引入类似的编码代理工具,这篇文章应该能帮你少走几个月的弯路。

1. 为什么AI Agent写代码总是"像新人":先认识问题的本质

1.1 大模型生成代码的内在偏好:通用不等于合规

在给Agent立规矩之前,得先弄明白它为什么"野"。大模型本身的训练集是公开代码仓库,这决定了它生成代码时的统计学偏好:缩进用标准格式、命名用社区通用风格、依赖选最流行的写法。

举个我实际遇到的例子:我们仓库里有一条规定,Python代码里禁止使用私有模块,所有通用逻辑必须进固定的公共模块。Agent拿到一个需求后,第一反应是新建一个工具函数放到它自己创建的文件夹里,因为它要"保持内聚"。从工程角度这没错,但它完全无视了团队对模块归属的约束。原因很简单,训练数据里根本没有你们公司的目录规划,它只能按最普遍的写法来。

本质上,这像一个在多家公司工作过的老程序员第一天入职你这家公司,写出来的代码带着上一家公司的习惯,甚至是所有公司习惯的"平均数"。这个类比很关键:你不可能靠一句"请遵守公司代码规范"就让一个老手放弃肌肉记忆,Agent也一样。

1.2 prompt为什么不靠谱:规范失效的三种典型形态

很多人第一反应是:把规范写在系统提示词里不就行了?我一开始也是这么干的,把wiki里三十多条规范塞进system prompt,效果很一般,而且存在三种典型的失效形态。

第一种叫"指令稀释"。模型对上下文的理解有个注意力衰减的问题,当三十条规范排在一起,它在生成具体代码时,真正记住的往往只有前面几条和最后几条,中段那些"中等重要"的规范会被忽略。你写了"禁止使用旧模块",它却在第17行引入了旧模块的函数。

第二种叫"规范互斥"。团队的真实规范里天然存在矛盾,比如日志规范要求"打印完整的参数上下文"以方便排查,但安全规范要求"敏感字段一律脱敏"。这种冲突连人类开发者有时都会拿不准,Agent没有"提问"的能力,它只会按概率选一个,选错的时候你完全不知道它为什么这么选。

第三种叫"隐性规范缺失"。很多规范根本不在wiki上,而是存在于资深开发者的脑子里。比如"新增枚举值必须同步升级数据库字典表""领域服务不允许直接依赖缓存客户端",这种隐性规范Agent无从知晓。

所以结论很明确:这不是提示词工程能单独解决的问题,而是一个"规则表达 + 工具链约束 + 反馈回路"的系统工程。

2. 建立"规则优先级":把公司规范拆成Agent能听懂的执行单元

2.1 规范文档清洗:从几百页wiki到触发式规则

我们团队的做法是从清洗规范文档开始的。那本wiki有上千页,真正需要Agent外显遵守的没那么多。我们做了一次减法,把规范分成了三类:机器可判定、Agent规则可表达、人工评审兜底。

机器可判定的是指格式、命名、行宽、禁止使用某个模块这类可以交给静态检查工具自动拦截的,比如Python的格式约束直接交给Ruff处理。Agent规则可表达的是需要写入Agent上下文的约束,比如"新增外部依赖必须显式申请""领域层禁止引用基础设施层"。人工评审兜底的是架构评审、方案设计这类需要人判断的,不强求Agent保证。

清洗出来的结果是一个规则清单,格式统一为"如果A,必须B;禁止C;例外D"。举例来说,原始的规范条目可能是"为了链路排查方便,所有对外接口需要打印请求日志",清洗后变成"凡是在controller目录下新增的接口方法,必须使用trace_logger记录requestId、method、path三个字段;灰度环境除外"。

做完这步我才意识到,规范文档清洗本身就是一个巨大的价值:团队里很多模糊的口头约定在这个过程中被硬性化了。

2.2 规则颗粒度与冲突仲裁:例举三个真实冲突

规则写得太粗,Agent不知道怎么执行;写得太细,Agent会被规则绑死,动不动就停下来问"到底要不要做"。我们最后定了一个颗粒度标准:尽可能贴近"一次改动就能验证"的执行单元。

颗粒度确定之后,下一步是处理冲突。我举三个真实处理过的例子,你可以感受一下思路。

第一个冲突是上面提到的日志完整性和敏感字段脱敏。解决方式是给规则加优先级字段:脱敏规则标记为P0(不遵守绝对不允许合入),日志字段规则标记为P1(默认必须遵守,但允许在审查时人工豁免)。Agent在选择时,P0永远优先于P1。

第二个冲突是"公共工具库复用"和"防腐层隔离"。团队既有规范要求尽量复用公共工具库,又有规范要求某些目录禁止依赖基础设施。这两个会同时出现在一个需求里。我们在规则文件里加了路径作用域:公共工具库规则对全局生效,防腐层规则只对特定目录生效,并且写明"当路径匹配防腐层目录时,跳过复用规则并说明原因"。

第三个冲突是关于新增依赖的。现有规范要求"优先使用团队内部封装库",但某需求涉及的加密算法,内部库没有实现,Agent陷入两难。解决方式是在规则中写明白"内部库无法覆盖的场景允许引入新依赖,但必须输出DEPEND-VERIFY.md说明选型理由",把冲突转化为显式的决策记录。

最终规则文件长这样:

rules: - id: RULE-001 level: P0 scope: "**/*.py" condition: "public function" action: "require docstring" exception: "magic methods, overrides" - id: RULE-002 level: P1 scope: "src/controllers/**" condition: "new interface method" action: "must log trace_id, method, path" exception: "health check endpoints" - id: RULE-003 level: P0 scope: "src/**" action: "forbid import from deprecated_tools"

提示:优先级字段是关键。没有优先级的规则库只是一堆"好建议",Agent无法做出取舍;有了优先级之后,规则之间的冲突才变得可计算、可裁决。

3. 用反馈回路"训练"Agent:Pi Agent接入lint、类型检查与单测的完整配置

3.1 Pi Agent的感知边界:它其实看不到你仓库的全貌

先说清楚Pi Agent这类工具的工作机制。它在终端里跑,表面上能读写文件、执行命令,但它看代码的方式和你不一样:它靠的是"上下文窗口",生成代码时看到的是你提供给它的那几个文件的切片,不是整个仓库的全局结构。

这意味着它会犯两类你意想不到的错误。第一类,跨文件视野缺失:修改A模块的公共接口签名时,它不会主动去检查所有调用方,因为它根本没读过那些文件。第二类,对CI的认知缺失:它不知道CI里跑着哪些检查,所以可能开心地提交一份能通过本地编译但会被CI拦下的代码。

我们要做的,不是期待Agent自己"长眼睛",而是把仓库外部工具链当作它的眼睛,让它在提交产物之前,先被真实存在的东西检验一遍。

3.2 配置静态检查作为前置闸门

我们把Pi Agent的工作流配置成了"生成代码、运行检查、捕获报错、自动修正、验证无新增告警"的闭环。这个闭环的核心是在Agent的执行配置里声明"前置闸门":

{ "preflight": { "linters": [ { "name": "ruff", "cmd": "ruff check --output-format=json" }, { "name": "mypy", "cmd": "mypy src --show-error-codes" }, { "name": "pytest", "cmd": "pytest -q --tb=short" } ], "max_iterations": 3, "blocking_level": "warning" } }

注意几个设计细节。第一,lint和类型检查一定要配JSON输出格式,不要用人类可读的文本格式,因为Agent解析结构化数据的能力远强于解析纯文本。第二,测试命令要加上快速参数,让每条用例尽量在秒级返回,否则整个反馈链路会拖到难以忍受。第三,blocking_level设为warning意味着warning级问题就会被打回修正,这在一开始可能会显得严格,但正是这种严格让Agent逐渐形成"生成时就应该规避问题"的倾向,而不是指望事后修补。

执行完配置后,Agent每次改动完代码都必须运行上述命令。如果有一条报错,它必须读取报错、定位文件、修复、再跑,直到全部通过,或者达到最大迭代次数后停下来向人求助。

3.3 把报错信息转化成Agent可读的修正信号

配置过程中最大的坑在于:静态检查的原始输出,并不适合直接喂给Agent。举个例子,Ruff检查一个改动涉及20个文件时,输出会有几十条,如果一股脑全塞给Agent,它会按文件顺序逐条修,修完第3个文件时又产生了新的问题,最后陷入循环,最坏情况是它在原地打转四五轮都修不完。

所以我们对报错信息做了处理,分成两步。

第一步,按severity排序,并把同一文件的问题聚合到一起,让Agent可以按文件为单位处理,而不是按错误条目零散处理。这更符合它"生成代码"时的单位习惯。

第二步,限制自动修正的迭代次数。我们在配置里设了max_iterations=3,超过3轮就放弃自动修复,把"修改建议+当前失败信息"打包发给开发者。实测中这个限制很有效,它避免了Agent在无解问题上浪费大量时间。从实际质量看,接入反馈回路后,单个PR的lint告警数从平均17条降到了2条左右,效果立竿见影。

注意:每一步修正完成后,要把整个代码文件重新放进上下文让Agent重新生成,而不是只把报错追加在上下文末尾。我之前踩过这个坑,效果很差。Agent需要看到完整的、更新后的代码才能做出正确的下一步判断。

4. 上下文管理:让规范在长任务中不"失忆"

4.1 注意力衰减:为什么十条规范后面会失效

即使有了规则文件,还有一个绕不开的问题:长任务会导致"规范失忆"。一个Agent任务动辄涉及十几个文件。如果你给它传递了十条规范,它在改第3个文件时可能还记得,改到第15个文件时就只记得"把功能写完"了。

我把这个现象叫"注意力衰减",它和上下文窗口大小无关——即使窗口足够大,较早写入的规则也会被后续大量的代码内容稀释。我做过一个很直观的验证:给它同一个任务,规则放在提示词最前面和放在紧跟代码示例的位置,最终遵守率差了20个百分点。

所以"把规则写进配置文件"只是一半工作,另一半是决定这些规则在什么时机、以什么形式出现在Agent的上下文里。

4.2 分层注入:仓库级、目录级、文件级的规范分层

我们最终采用的方案是"规范分层注入",不同粒度的规则在任务的不同阶段加载。

第一层是仓库级规则,也就是"宪法"。内容包括:禁止提交密钥、所有改动必须运行测试、严禁在公共代码中引入测试专用依赖等。这一层文件放在仓库根目录,命名为REPO.md,Agent在每次任务开始时强制读取。

第二层是目录级规则。每个核心目录放一个目录级规范文件,只描述与这个目录相关的约束。比如src/domain下写明"领域对象不得依赖ORM模型",src/infra下写明"基础设施模块的接口变更必须提供迁移说明"。Agent在进入该目录时才读取,这样它能保持规范与当前工作内容的强相关性。

第三层是文件级规则,针对特定文件类型或特定文件。比如"init.py内不允许有业务逻辑""配置文件禁止硬编码环境信息"。

这套分层很有用,因为它让规范在任何一个时刻都占据较小的上下文比例。之前一次把所有规范都加载进来等于让Agent背一整套大部头,分层之后它每一刻只需要记住当前目录的那几条。"宪法"永远在,"部门规章"按需出现。

如果你用的是支持指令文件的Agent,可以直接用这种分层文件的组织方式:

REPO.md src/domain/RULES.md src/infra/RULES.md src/controllers/RULES.md

Agent侧只需要约定:任务开始读取REPO.md,切换目录时自动加载该目录的RULES.md。

4.3 给Agent"装记忆":规则加载器与决策日志

分层注入解决了"当下"的取舍,但还有一个"历史"的问题:Agent在长任务里做了许多决策,下一次对话时它可能完全不记得这次为什么这么写。

我们的做法是引入规则加载器和决策日志两个机制,它们组合起来相当于给Agent装了一个简单的记忆层。

规则加载器是一个本地命令,Agent每次开始任务时通过它拿到当前工作目录应该遵守的规则文件列表,保证路径和规则的映射始终与仓库实际结构一致,避免规则文件更新了但Agent还在用旧版本。决策日志是一个Markdown文件,每当Agent做出偏离规则的决策时,要求它在指定的DECISIONS.md里记录场景、决策、原因、待解决事项。

# DECISIONS.md ## 2024-11-21 auth模块拆分 - 场景:重构 auth 模块拆分 - 遵守:domain层新增 policy 文件,未引用基础设施 - 偏离:临时保留旧接口兼容层,原因:未完成全部调用方迁移,需后续两个PR处理

这个文件对开发者有个额外价值:评审时可以直接看到Agent的决策轨迹,而不是只看到结果,大大降低了人工评审的心智负担。

5. 实测数据与效果对比:从60%合规到90%合规的那段路

5.1 测试方法:怎么量化"老老实实"

讲完方法论,说说怎么验证这套方案是否真的有效。我们当时定了两个量化指标。

第一个指标是"每千行代码不合规问题数"。这个指标的困扰是问题类型太多,格式化问题、命名问题、架构问题全混在一起。我们把问题按来源分成了机器检查项和人工评审项:机器检查项是lint、类型检查、单测、密钥扫描,人工评审项是每次评审时标注的架构和命名问题。

第二个指标是"单PR平均评审时长"。在引入规则体系之前,评审一个中大型PR大约需要40分钟,主要时间花在讲解那些"团队规范"上。如果规则体系真的有效,这个数字应该明显下降。

为了控制变量,我们选了四类典型任务做测试:新功能开发、缺陷修复、代码重构、接口联调,每个任务分别在"无规则约束"和"有规则约束"两种状态下跑一遍,由两个开发者背对背评审打分。

5.2 三个典型失败案例及处理策略

测试过程中当然不是一帆风顺,有几个失败案例特别值得拿出来说。

第一个案例:import排序问题反复出现。规则明确规定了标准库、第三方库、本地模块三段的空行分隔,但Agent经常把本地模块和第三方库挤在一起。排查后发现问题不出在规则本身,而是Agent读取到了规则文件的缓存旧版。我们于是要求版本变更时同步更新规则加载器的刷新逻辑,从那以后这个问题再没出现过。

第二个案例:为了通过lint检查,Agent大面积添加空注释。lint要求所有公共函数必须有docstring,Agent在几个内部函数和私有方法上也强行加了无意义的docstring,等于在刷存在感。规则里把条件改成"public function requires docstring,private和magic方法禁止添加",并加了一条"docstring必须描述入参和返回逻辑"的质量约束才解决。

第三个案例:跨目录重构时引用旧路径。Agent在移动某个模块后,其他文件的import仍指向旧路径,因为它没有重新检查所有引用文件。这个问题暴露出上下文管理的薄弱点,最后我们用"目录级规则的读取触发机制"修补了它:进入新目录时强制触发一次引用检查命令,确认所有import目标有效。

5.3 收益与成本:值得投入吗

接入完整方案后,我们的测试数据大致如下:

指标接入规则前接入后完整方案
每千行代码不合规问题数14.22.1
单PR平均评审时长(分钟)3821
自动迭代轮次与人工介入比例4.6:12.3:1
评审中"规范教育"类评论占比42%11%

数字之外,我更看重一个感受上的变化:评审不再那么累了。以前评审AI的代码,大量时间花在教它"我们的习惯"上,现在那些基础规范被前置了,我可以把精力集中在真正需要判断的地方,比如方案设计是否合理、边界条件是否覆盖。

成本方面,初始化规则库大概花了3到5个人日,包括清洗规范、编写冲突仲裁规则、配置静态检查命令。之后每次规范变更大约15到30分钟。相比长期的人力纠偏成本,这笔投入是相当划算的。完整的合规对比表格确实帮我向团队证明了投入的必要性。

6. 需要长期维护的事:规范演进与Agent的"再培训"

6.1 规则的版本化:规范库用Git管理

规则库不是写完就结束的,它需要像代码一样被维护、评审、测试。我们把所有规则文件放进了独立的Git仓库,每次变更都走MR流程,要有明确的评审人说明变更影响。这个做法的好处是规则变更变得可追溯,出了问题可以回滚。

还有一个容易被忽略的点:每次规则变更时,要附带一个"反例"或者"正例"示例,让Agent能直观看到变化。比如我们规定"控制器方法禁止直接使用缓存客户端",规则变更的同时加上了违反示例和正确示例各一段。这种做法比纯文本规则更有效,因为模型的生成更受示例影响。

我也建议规则不要频繁变更。曾经有一段时间我们每周都在调规则,Agent在各种约束之间反复摇摆,反而出现了一些奇怪的行为。后来约定规范化整一个月评审一次,紧急安全规则除外。

6.2 我们沉淀的定期评审机制与"已知不能做"清单

最后说说长期维护的机制。每个月我们都会做一次"规范有效性评审",从Agent提交中抽取那些它处理不了或搞砸了的报错,逐条分类。

分类有三个方向:规则缺失,说明规范没写到位,补充规则;规则冲突,说明优先级或路径作用域设计有矛盾,调整仲裁逻辑;模型能力不足,也就是当前模型确实做不到,比如需要复杂的跨模块架构判断,我们就把这类需求标记为"已知不能做",暂不投放给Agent,等模型升级后再做回归验证。

第一版评审时,规则缺失约占60%,模型能力不足约占30%,冲突约占10%。经过三轮迭代后,比例大致变成规则缺失25%,能力不足60%,冲突15%。这意味着大部分问题已经转化为"Agent根本做不到",而不是"它不知道怎么做"。这两者的区别很重要:前者是能力边界,清晰可见;后者是管理缺口,需要人去补。

我个人在这个过程中的体会是:规则库的建设与其说是在约束Agent,不如说是在逼着团队把自己长期积累的隐性知识显性化。很多规范以前靠口头传递,新人来了靠"多看看老代码"来领悟,现在全部变成了一条条机器可读的规则。这个过程的产出,受益的不只是Agent,整个团队的代码评审效率、新人上手速度,都在跟着变好。想让Agent老实,你首先要让规范本身变得老实。

如果你也在做类似的事情,我的建议是:不要一开始就追求完美覆盖,先把最容易出问题的20条规则立起来,接上反馈回路,跑两个迭代再逐步扩张。规则是长出来的,不是设计出来的。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进