当仓库里超过九成代码由 AI 生成,决定系统走向的不再是"谁写得更快",而是"谁能约束 AI 的输出"。没有统一规范,AI 只会成倍放大混乱——命名不一致、架构漂移、重复抽象层层叠加,技术债不是线性增长,而是指数膨胀。
我们用 31 万行代码的重构实践验证了一个思路:把 Agent 评测的方法论搬到 AI Coding 管理上,通过技术债盘点、Rule 建设、重构 SOP 和 Pre-PR 机制,让重构从"高成本专项工程"变成"随迭代持续推进的日常动作"。
技术债不是模糊感觉,而是可量化的清单
AI Coding 的技术债有一个特征:表面看起来运行正常,内部结构却在持续腐烂。因为 AI 倾向于在局部上下文中补全代码,不会主动回溯全局一致性。一个函数名 getUserById 和另一个 fetch_user_by_id 并存,AI 不会觉得有问题,但人类维护者会。
第一步不是动手重构,而是把债盘点清楚。我们按三个维度做梳理:
| 维度 | 具体项 | 量化方式 |
|---|---|---|
| 命名一致性 | 同一概念的不同命名变体 | grep 统计变体数量 |
| 架构合规 | 跨层调用、循环依赖 | 依赖图分析工具输出 |
| 重复抽象 | 功能重叠的函数/类 | 相似度检测或人工标注 |
盘点结果不是一份文档,而是一份可追踪的清单,每条债有编号、影响范围和优先级。这和 Agent 评测中的"评测项清单"逻辑一致——先定义测什么,再定义怎么测。
Rule 不是写给人看的规范,而是喂给 AI 的约束
传统代码规范写在 Confluence 里,开发者偶尔翻翻,AI 根本不看。AI Coding 的 Rule 必须以机器可读、上下文可注入的形式存在。
我们建设的 Rule 分三层:
- 全局 Rule:项目级架构约束、命名约定、禁止模式,写入
.cursorrules或类似配置文件,AI 每次生成代码时自动加载。 - 模块 Rule:特定目录下的局部约定,比如
src/api/下必须用 RESTful 命名、统一错误码格式,放在对应目录的.mdc文件中。 - 任务 Rule:单次重构任务的精确约束,比如"本次只改命名,不改逻辑",在 prompt 中显式声明。
下面是一个实际可用的全局 Rule 文件示例:
# .cursorrules — 项目级 AI Coding 约束
# 此文件会被 AI 编码工具在每次生成时自动注入上下文
project:
name: "order-service"
language: "python"
framework: "fastapi"
architecture:
layers:
- name: "api"
path: "src/api/"
rule: "只做参数校验和路由,不包含业务逻辑"
- name: "service"
path: "src/service/"
rule: "业务逻辑层,禁止直接操作数据库"
- name: "repo"
path: "src/repo/"
rule: "数据访问层,禁止引用 service 层"
dependency_direction: "api → service → repo,禁止反向依赖"
naming:
function: "snake_case,动词开头,如 get_order_by_id"
class: "PascalCase,名词,如 OrderService"
variable: "snake_case,禁止驼峰混用"
constant: "UPPER_SNAKE_CASE"
# 同一概念的统一命名映射,AI 必须遵守
concept_map:
user_id: ["user_id", "userId", "uid"] # 禁止使用后两个变体
order_id: ["order_id", "orderId", "oid"] # 禁止使用后两个变体
prohibited_patterns:
- "禁止在 api 层写 SQL 查询"
- "禁止用 print 替代 logger"
- "禁止硬编码配置值,必须从 settings 读取"
- "禁止 catch 所有异常后静默忽略"
- "禁止在循环内做数据库查询(N+1 问题)"
error_handling:
framework: "fastapi.HTTPException"
format:
status_code: "HTTP 状态码"
detail: "面向调用方的错误描述"
internal_code: "内部错误码,格式 ERR_{MODULE}_{NUMBER}"
关键设计点:concept_map 和 prohibited_patterns 是最有效的两个字段。前者解决 AI 命名漂移问题,后者划定硬性红线。Rule 不是越长越好,而是越精确越好——AI 对"禁止"比"建议"响应更稳定。
重构 SOP:把大手术拆成可评测的小步骤
31 万行代码不可能一次性重构。我们的做法是把重构拆成可评测的原子步骤,每个步骤有明确的输入、输出和验证条件——这和 Agent 评测中"每个评测项独立可判"的思路完全一致。
一个典型的重构 SOP 流程:
SOP: 统一命名重构
├── Step 1: 扫描变体
│ 输入: concept_map + 项目源码
│ 输出: 变体清单 (文件路径 + 行号 + 当前命名)
│ 验证: 变体数量 > 0 才继续
│
├── Step 2: 执行替换
│ 输入: 变体清单 + 目标命名
│ 输出: 替换后的文件 diff
│ 验证: diff 中只有命名变更,无逻辑变更
│
├── Step 3: 回归测试
│ 输入: 替换后的代码
│ 输出: 测试报告
│ 验证: 全部通过,无新增失败
│
├── Step 4: Rule 更新
│ 输入: 本次统一的概念
│ 输出: 更新后的 concept_map
│ 验证: concept_map 包含新概念条目
每个 Step 的验证条件就是评测判据。AI 执行 Step 2 时,prompt 里要写清楚约束:
# refactor_prompt.py — 生成重构 prompt 的辅助脚本
# 用法: python refactor_prompt.py --concept user_id --target user_id
import argparse
import json
def build_prompt(concept: str, target_name: str, variants_file: str) -> str:
"""根据 concept_map 和扫描结果,生成精确的重构 prompt"""
with open(variants_file) as f:
variants = json.load(f)
# 只取当前概念的变体
items = [v for v in variants if v["concept"] == concept]
prompt = f"""你正在执行一个命名统一重构任务,严格约束如下:
1. 只修改命名,不修改任何逻辑、注释或格式
2. 将以下变体统一为 `{target_name}`:
{json.dumps(items, indent=2, ensure_ascii=False)}
3. 修改后运行: pytest tests/ -x --tb=short
4. 如果测试失败,回滚所有变更并报告失败原因
5. 输出变更文件列表和每个文件的 diff 摘要
禁止行为:
- 禁止重构之外的任何改动
- 禁止删除或新增代码行
- 禁止修改 import 顺序
"""
return prompt
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--concept", required=True)
parser.add_argument("--target", required=True)
parser.add_argument("--variants-file", default="variants.json")
args = parser.parse_args()
prompt = build_prompt(args.concept, args.target, args.variants_file)
print(prompt)
运行方式:
# Step 1: 扫描命名变体(可以用 grep + AI 辅助分析)
grep -rn "userId\|uid" src/ --include="*.py" | python scan_variants.py > variants.json
# Step 2: 生成重构 prompt
python refactor_prompt.py --concept user_id --target user_id --variants-file variants.json
# Step 3: 将 prompt 交给 AI 执行,人工审核 diff
git diff --stat # 只看变更范围
git diff # 逐行确认只有命名变更
# Step 4: 运行回归测试
pytest tests/ -x --tb=short
SOP 的核心价值:每个步骤的输出可评测,失败可定位,回滚可执行。AI 不是自由发挥,而是在受限轨道上运行。
Pre-PR:AI 代码进入主分支前的最后一道闸门
AI 生成的代码最容易出问题的不是逻辑错误(测试能拦住),而是结构性违规——跨层调用、硬编码、命名不一致。这些在功能测试中不会暴露,但在长期维护中会持续腐烂。
Pre-PR 机制就是在代码提交到主分支之前,用自动化 + 人工双重评测拦截结构性问题:
# .github/workflows/pre-pr-check.yml
# AI 生成代码的 Pre-PR 结构性检查
name: Pre-PR Structural Check
on:
pull_request:
types: [opened, synchronize]
jobs:
architecture-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# 检查跨层依赖:api 层禁止直接 import repo 层
- name: Cross-layer dependency check
run: |
violations=0
# api 层不应直接引用 repo 层
for f in src/api/*.py; do
if grep -n "from src.repo" "$f"; then
violations=$((violations + 1))
echo "::error file=$f::API layer directly imports repo layer"
fi
done
if [ $violations -gt 0 ]; then
echo "Found $violations cross-layer violations"
exit 1
fi
# 检查命名变体:禁止 concept_map 中标记的变体出现
- name: Naming convention check
run: |
violations=0
# 禁止 userId 和 uid 变体
for variant in userId uid; do
matches=$(grep -rn "$variant" src/ --include="*.py" | wc -l)
if [ $matches -gt 0 ]; then
violations=$((violations + matches))
echo "::warning::Found $matches occurrences of prohibited variant '$variant'"
fi
done
# 禁止 oid 和 orderId 变体
for variant in orderId oid; do
matches=$(grep -rn "$variant" src/ --include="*.py" | wc -l)
if [ $matches -gt 0 ]; then
violations=$((violations + matches))
echo "::warning::Found $matches occurrences of prohibited variant '$variant'"
fi
done
if [ $violations -gt 0 ]; then
echo "Found $violations naming violations"
exit 1
fi
# 检查硬编码配置值
- name: Hardcoded config check
run: |
# 搜索常见的硬编码模式
hardcoded=$(grep -rn "localhost:5432\|localhost:6379\|hardcoded_password" src/ --include="*.py" | wc -l)
if [ $hardcoded -gt 0 ]; then
echo "::error::Found $hardcoded hardcoded config values"
exit 1
fi
# 检查 N+1 查询模式(循环内数据库调用)
- name: N+1 query pattern check
run: |
# 简化检测:for/while 循环体内出现 session.query 或 db.query
violations=0
for f in src/api/*.py src/service/*.py; do
if python -c "
import ast, sys
tree = ast.parse(open('$f').read())
for node in ast.walk(tree):
if isinstance(node, (ast.For, ast.While)):
for child in ast.walk(node):
if isinstance(child, ast.Call):
func = child.func
if isinstance(func, ast.Attribute) and func.attr in ('query', 'execute', 'fetchone', 'fetchall'):
print(f'$f:line {child.lineno}')
sys.exit(1)
"; then
violations=$((violations + 1))
fi
done
if [ $violations -gt 0 ]; then
exit 1
fi
# 运行功能测试
- name: Run tests
run: pytest tests/ -x --tb=short
Pre-PR 的设计原则:功能测试拦逻辑错误,结构性检查拦架构腐烂。两者缺一不可。AI Coding 场景下,结构性检查的权重应该高于传统项目,因为 AI 的"局部合理、全局混乱"倾向比人类更强。
从专项到日常:重构的可持续节奏
31 万行重构最关键的认知转变是:重构不能是里程碑式的专项工程,必须是迭代中的日常动作。
实现这个转变需要三个条件:
- 债可追踪——每条技术债有编号和状态,不是模糊的"代码质量差"。
- 步可原子——每次重构只做一件事,有明确的评测判据,失败可回滚。
- 门可自动——Pre-PR 自动拦截结构性违规,不需要人工逐行审查。
我们最终的工作节奏:
每个迭代周期(1-2周):
├── 开发阶段:AI 生成代码 + Rule 约束 + 任务级 prompt
├── 提交阶段:Pre-PR 自动检查 → 人工审核结构性变更
├── 合入阶段:通过后合入主分支
└── 盘点阶段:从债清单中选 2-3 条,作为下个迭代的重构任务
每个迭代消化 2-3 条债,31 万行代码的债清单大概 60-80 条,20-30 个迭代就能清完。不需要停下来做"重构月",也不需要全员投入。AI 写新代码,人类管旧债,Rule 拦新增债——三条线并行推进。
采用建议与风险边界
先盘点,再写 Rule,最后建 SOP——顺序不能反。没有债清单,Rule 写不出精确的 concept_map 和 prohibited_patterns;没有 Rule,SOP 的约束条件无从定义。
Rule 的维护成本容易被低估。项目演进时,concept_map 和架构分层会变化,Rule 必须同步更新。我们每两周 review 一次 Rule 文件,删除过时约束,新增当前痛点。过时的 Rule 比 没有 Rule 更危险——AI 会遵守已经不合理的约束。
Pre-PR 检查项要从实际痛点出发,不要提前铺满。一开始只加最痛的两三条(比如命名变体和跨层依赖),跑稳了再逐步增加。检查项太多,开发者会绕过或忽略所有警告。
AI Coding 的管理本质是评测问题——定义什么是好的输出,定义如何判断输出是否达标,定义不达标时如何回退。Agent 评测的思路——清单化、可判性、可追溯——恰好是管理 AI Coding 最需要的骨架。31 万行代码验证了这一点:约束 AI 的能力,才是决定系统走向的能力。