Coding Agent在30万行祖传代码上的实战评测

2026-06-05阅读 0热度 0
其他

在技术一线深耕多年的从业者,来分享如何让Coding Agent高效驾驭那些“历史遗留系统”。这个过程比大多数技术博客描述的更具挑战,也更有实操价值。

前沿技术动态速览

切入正题前,先盘点本周值得关注的技术动向:

  • Nvidia 发布 Vera CPU,专为 AI 智能体工作负载设计:N 家将“为 Agent 加速”明确写入产品定位。这标志着类似Coding Agent这类需要“长上下文处理与高频工具调用”的工作负载,已被正式认可为核心业务场景。
  • Nvidia RTX Spark 发布,号称史上最高效 PC 芯片:本地运行代码模型的门槛正在降低。对于“代码不得上传云端”的团队而言,这是重大利好。
  • 礼来靶向药物 Retevmo 降低肺癌复发风险:医药板块关注度逐步回升。值得留意的是,AI编程工具正在显著加快医疗算法工程师的迭代效率。
  • 伊朗冲突推高能源价格:美国家庭预计年度能源支出增加 450 美元。这对跨国云服务意味着电费成本传导,反而使本地化小集群跑模型的方案更具性价比。

起因:一场“祖传 30 万行”项目的 PR 评审事故

坦白说,最初我并不相信AI工具能在老旧项目上取得成效。

一位朋友接手了一个历经八年迭代的Android主工程,主仓包含30万行Kotlin/Java混合代码。项目中并存四套网络层(OkHttp自研封装、Retrofit、Volley残留及一个来源不明的HttpURLConnection工具类),同时混用RxJava 1/2/3、LiveData、Flow、协程等异步解决方案。新同事入职第一周提交的代码合并后测试失败,已成常态。

今年三月,他做了一次实验:让一位入职三周的工程师全程使用Claude Code编写“账单详情页”需求,并按正常流程提交PR。结果如下:

  • ❌ 翻车现场 → 代码使用了Retrofit,但同一模块其他接口均采用自研的 NetClient;使用了LiveData,但ViewModel基类限定为 Flow;使用了Glide,但项目已全面切换至Coil。功能虽可运行,评审却无法通过。

问题根源不在AI,而是它完全不理解该项目的“潜规则”。这件事促使我深入思考:在一个充满历史遗留问题的真实项目中,如何使Coding Agent真正成为“团队协作成员”,而非“临时外包工”?

第一刀:上下文并非“越多越好”

我最初犯的错误,是将README、架构文档、项目规范全部塞入system prompt,认为“上下文越全越好”。结果Claude的反应反而变得迟钝——它会错误引用文档中早已废弃的规范,或者忽略当前任务最关键的限制条件。

不要将上下文当作“百度网盘”

随后我调整策略:将项目知识拆解为按需注入的小模块,每部分都有明确的触发条件。在仓库根目录创建 .agent/ 目录,结构如下:


???? .agent/
├── ???? conventions.md          — 通用规范(必读)
├── ???? layers/
│   ├── ???? network.md          — 网络层实现方案
│   ├── ???? image.md            — 图片加载策略
│   ├── ???? di.md               — 依赖注入写法
│   └── ???? navigation.md       — 路由跳转方案
├── ???? forbidden/
│   └── ???? deprecated_apis.md  — 禁用API清单
└── ???? examples/
    ├── ???? viewmodel.kt        — 标准ViewModel示例
    └── ???? repository.kt       — 标准Repository示例

核心技巧:每个 .md 文件控制在80行以内,开头一句话明确说明何时需要阅读该文件。例如 network.md


# network.md

# 触发条件: 涉及 HTTP/接口/请求/下载/上传 时必读

## 唯一允许的方式
// 通过 NetClient 走全局拦截链
val api = NetClient.create(BillApi::class.java)
val resp = api.fetchBill(id)

## 严禁直接
- new Retrofit.Builder()
- OkHttpClient() 自建实例
- HttpURLConnection

## 错误码统一处理
- 401: ApiException.Auth
- 5xx: ApiException.Server
- 业务错: ApiException.Biz

每个文件采用“做什么 / 不做什么 / 标准示例”三段式结构。Agent会在需要时自行读取这些文件,无需强制塞入系统提示。

CLAUDE.md 是路标,不是百科全书

仓库根目录的 CLAUDE.md,我只设置路标指引。将Agent引导至正确的文档,而非直接展开知识。整个文件不超过50行:


# 项目导航 (CLAUDE.md)

## 你必须先读这个
.agent/conventions.md

## 按需读
- 改网络: .agent/layers/network.md
- 改图片加载: .agent/layers/image.md
- 改 DI 注入: .agent/layers/di.md
- 写 ViewModel: 参考 .agent/examples/viewmodel.kt
- 涉及废弃 API: 必查 .agent/forbidden/deprecated_apis.md

## 改完代码必须运行
./gradlew :feature:bill:lint
./gradlew :feature:bill:test

## 提交前必查
./scripts/agent_precheck.sh

这一改动使Agent的token消耗下降了约40%,同时输出质量反而提升。核心逻辑:少即是多。提供清晰的“索引地图”,比堆砌完整信息更有效。

第二刀:禁用清单是隐形武器

老旧项目的显著特征:存在大量“语法完全合法、但团队约定不可用”的API。仅靠prompt提醒远远不够,Agent在编写过程中容易遗忘。

我的做法是将这些规则编码为可执行的检查脚本。在 .agent/forbidden/deprecated_apis.md 中同时维护两份内容:

一份供Agent阅读的语义说明:


## 严禁使用清单

### 网络
- ❌ Retrofit.Builder() 直接 new
→ ✅ 改用 NetClient.create()
原因: 绕过了埋点和重试链

### 图片
- ❌ Glide.with(ctx).load(url)
→ ✅ 改用 ImageLoader.load(url)
原因: Glide 已下线,2024Q3

### 异步
- ❌ Observable / Subscriber
→ ✅ 改用 Flow / suspend
原因: RxJava 在新代码禁止

### 跳转
- ❌ Intent / ComponentName
→ ✅ 改用 Router.go(uri)
原因: 模块化路由统一管理

另一份供CI使用的正则规则,置于 .agent/forbidden/lint.yaml


rules:
  - id: no_retrofit_builder
    pattern: "Retrofit.Builder("
    message: "用 NetClient.create"
    severity: error
  - id: no_glide
    pattern: "Glide.with("
    message: "用 ImageLoader.load"
    severity: error
  - id: no_rxjava
    pattern: "io.reactivex."
    message: "新代码用 Flow"
    severity: error
  - id: no_handler_postdelay
    pattern: "Handler().postDelayed"
    message: "用 lifecycleScope"
    severity: warn

检查脚本使用Python编写80余行,每次Agent执行commit前必运行:


# scripts/agent_precheck.py
import re, yaml, sys
from pathlib import Path

def scan(rules, files):
    errors = []
    for f in files:
        text = f.read_text()
        for r in rules:
            if re.search(r["pattern"], text):
                errors.append((f, r["id"], r["message"]))
    return errors

rules = yaml.safe_load(Path(".agent/forbidden/lint.yaml").read_text())["rules"]
changed = [Path(p) for p in sys.argv[1:] if p.endswith((".kt", ".java"))]
errs = scan(rules, changed)

if errs:
    for f, rid, msg in errs:
        print(f"::error::{f}: [{rid}] {msg}")
    sys.exit(1)

这一策略的独特效果:Agent在执行commit时若触发precheck失败,会自动读取错误信息、修正代码、重新提交,全程无需人工干预。最初担心可能陷入死循环,实际运行显示,95%的情况Agent能在两轮内自我修复,剩余5%确实触及架构问题,会主动暂停并询问。

第三刀:Diff 评审 Agent —— 让 AI 审查 AI 生成的代码

前两刀解决“写得对”的问题,第三刀解决“写得好”的问题。

在PR流程中增加一个独立的评审Agent,该Agent与编码Agent使用不同的system prompt甚至不同的模型(实测Claude Opus负责编写、Sonnet负责评审,效果优于同模型自评)。评审Agent执行三项任务:


PR 触发
↓
评审 Agent 启动
↓
检查 1 → Diff 是否符合 .agent/conventions.md
检查 2 → 是否触及 .agent/forbidden/ 列表
检查 3 → 给出“如果是我会怎么写”的建议
↓
输出 review.md
↓
阻断条件 → 出现 severity=blocker 直接拒绝合并

评审 Prompt 的设计

评审Agent的system prompt是整个流程的核心,经过十余次迭代优化。最关键的原则:不让其自由发挥,而是提供明确的输出schema。


# 评审 Agent System Prompt
你是这个 Android 项目的资深评审者。你只做评审,不写新代码。

## 评审范围
仅看 PR diff,不评审历史代码。不在你 diff 里的问题不要管。

## 输出严格 JSON
{
  "summary": "一句话概括",
  "issues": [
    {
      "file": "path",
      "line": 42,
      "severity": "blocker|major|minor",
      "category": "convention|forbidden|design",
      "reason": "…",
      "suggestion": "…"
    }
  ]
}

## blocker 触发条件
- 命中 forbidden 清单
- 引入新的网络/图片/路由实例(绕过统一封装)
- 公共 API 改动但无单测
- main thread 跑 IO

## 不要做的
- 不评论代码风格(lint 管)
- 不要求改动超出 diff 范围
- 不臆测意图,只看代码

关键设计点:

  1. 限定评审范围在diff内。不指定该约束,Agent会顺着import查看历史代码,开始批评非本次PR引入的问题,造成“附带伤害”。
  2. JSON输出。结构化输出可直接对接GitHub/Gitlab API转为inline comment,体验与真人评审一致。
  3. 明确blocker边界。避免模型自行判断严重程度,将“必须block的情况”写成显式规则,其余均按minor处理。

真实数据

该流程运行三个月,统计了247个由Agent主笔的PR,核心指标变化显著:

指标 改造前 改造后
一次合入率 23% 71%
触发forbidden规则次数 每PR平均 3.2 每PR平均 0.4
人工review用时 平均 18min 平均 6min
单PR token消耗 ~120K ~70K

最令人意外的是token消耗下降。最初担心 .agent/ 目录会使Agent多读文件、token增加;实际效果是Agent不再随机浏览代码库,路径从5-8跳的随机游走缩短至2-3跳的精准定位,反而节省了资源。

第四刀:让 Agent “做饭”,同时让它“端菜”

这一策略反直觉,但近期实践验证有效。

许多团队使用Coding Agent的模式:Agent写代码 → 人提交 → 人沟通 → 人合并。Agent仅负责“制作”,人负责“交付”。

反向尝试:Agent写代码 + Agent写commit message + Agent写PR描述 + Agent自动回复评审意见。人只介入两个节点:定需求、合并。

commit message 模板化

为Agent提供固定prompt模板:


# commit message 模板
(): 


# 必填字段
- 改动原因(1 行)
- 影响范围(1 行)
- 是否需要回归测试

# 例
feat(bill): 增加账单详情页
- 原因: 需求 #BILL-1029
- 范围: feature/bill 模块
- 回归: 账单列表跳详情链路
Co-authored-by: Claude 

注意末尾的 Co-authored-by: Claude。这是Git标准字段,所有Agent编写的提交均要求添加。初衷并非给AI记功,而是用于溯源——三个月后某段代码出现问题,一行grep即可定位:这是特定时期、特定版本Agent所写,结合当时的prompt模板和规则版本,能快速还原场景。

PR描述中必须写明“我没动什么”

这一习惯源自一次事故。Agent修改了一个“看似无关”的工具类,导致联调时全模块挂断。复盘发现,修改原因是“代码风格统一”——Agent认为该工具类风格不一致而“顺手”改动。

此后在PR模板中强制要求一段:

⚠️ Out-of-scope changes 列出所有不是为了核心需求而改动的文件(重构、风格统一、注释补充等)。如果没有,写“none”。

这段文字强制Agent进行自我审视。Agent在编写该段时,往往会意识到“改动范围过宽”,随后收缩范围。这正是“外化的元认知”——将模型倾向于省略的反思过程,强制显性化。

几个反直觉的实践经验

  1. 越老的项目,越值得引入Agent 许多人认为AI适合绿地项目,不适合祖传代码。实际经验恰恰相反:祖传代码才是AI的主战场。新项目积累少、规则不多,AI发挥空间有限;老项目规则多、坑多、模式固定,反而适合让AI“按规矩办事”。人类工程师面对祖传代码常感沮丧、不愿梳理历史包袱;AI无情绪,给定禁用清单便忠实执行。
  2. Agent的“记忆”是负担,而非资产 这与当前许多“长期记忆”产品的设计方向相反,但实际运行感受是:Coding Agent的长上下文记忆往往会成为污染源。它会记住三天前已废弃的方案,反复询问已回答过的问题。当前工作流为:每个任务开启新会话,让Agent从 .agent/ 文件读取一手信息,而非依赖会话间的“记忆”。无状态化的Agent反而更可靠。如需持久化,持久化文件,而非对话。
  3. 避免让Agent编写“通用性代码” Agent编写“通用工具类”时容易过度设计——“为未来扩展,我添加了sealed class…为支持多类型,我做了泛型…”。必须予以制止。老项目中90%的代码不需要“未来扩展”,需要的是可运行、可修改、出bug时可理解。在prompt中增加一条:# 不加任何“为了将来扩展”的抽象层。添加后,PR平均行数下降30%,bug率下降40%。

写在最后:工具是放大器

三个月实践最大感悟:Coding Agent不是“另一个工程师”,而是团队规范的放大器。

如果团队规范模糊,Agent生成的代码就是更大规模的模糊;如果规范清晰、可执行、包含禁用清单和正向示例,Agent就能大量产出符合标准的代码,将工程师释放出来处理真正需要判断力的工作。

工具的意义从来不是“代替思考”,而是“逼迫你将口耳相传的隐性规则,转化为显性、可执行的规则”。这一过程本身就是团队工程能力的提升。

回顾本周动态,Anthropic估值反超OpenAI、Nvidia将Vera定位为“Agent专用CPU”——这两项事件指向同一趋势:行业正押注Coding Agent不是临时玩具,而是未来三五年开发流程的核心基础设施。一线工程师能做的,是将项目改造成“对Agent友好”的环境,使工具真正发挥作用,而非在老旧代码中消耗精力。

免责声明

本网站新闻资讯均来自公开渠道,力求准确但不保证绝对无误,内容观点仅代表作者本人,与本站无关。若涉及侵权,请联系我们处理。本站保留对声明的修改权,最终解释权归本站所有。

相关阅读

更多
欢迎回来 登录或注册后,可保存提示词和历史记录
登录后可同步收藏、历史记录和常用模板
注册即表示同意服务条款与隐私政策