
把AI Agent接入遗留代码库这件事表面看是“往老系统里加了个新工具”实际做起来跟重新开垦一块荒地差不多。我见过太多团队拿着绿地Demo的成绩单去迎接棕地项目结果两三周后灰溜溜回退到人工改代码。所谓棕地Agent工程就是要在老代码库、老业务约束、老测试缺失的环境下让Agent产生稳定、可控、可审计的价值。这篇文章想聊的不是Agent入门教程而是我在多个遗留系统改造项目中总结出的七条反直觉法则。1. 先搞清“棕地Agent工程”到底是什么别拿绿地Demo的思路硬套1.1 绿地Demo和棕地现实之间的落差我在不少团队里见过同一个剧本新项目、干净仓库、Agent几分钟生成代码、API接口一通、演示惊艳。接下来老板说“把咱们那套跑了十年的结算系统也接一下”然后一切都变得不对劲了。绿地工程里那些熟悉操作——把完整代码库喂给模型、让Agent自由重构、甚至“你看着办吧”——在遗留代码里几乎全都不成立。遗留代码库用“棕地”来比喻非常贴切你面对的是一块已经被开发过的土地地下埋着无数条管线和电缆随便挖一铲子都可能挖爆别人赖以生存的东西。所谓棕地Agent工程并不是“让AI Agent去读老代码”这么简单而是要用一套面向存量的工程化方法把Agent的能力限制在可控范围内同时最大化它对隐性知识的挖掘效率。它和绿地Agent开发的主要区别不在模型而在基础设施。1.2 遗留代码库真正拦住Agent的是哪些事为了后面七条法则好理解先盘点遗留代码库最常见的五个障碍。这些障碍决定了Agent不是“更聪明”就能解决的而是“更讲究方法”才能解决隐式契约方法签名没变行为却悄悄变了。上游依赖某个“副作用”但文档里一句都没提Agent光从代码表面看不出来。测试稀疏老模块普遍测试覆盖极低有的模块甚至完全没有测试。Agent改一个函数你根本找不到验证手段。耦合深且暗业务逻辑散落在服务类、工具类、配置文件、甚至SQL存储过程里。调用链复杂到你无法通过读一个文件就判断影响范围。构建和依赖体系陈旧编译慢、依赖带病、本地环境复杂Agent每跑一次验证都要消耗大量时间和计算资源。上下文碎片化代码量和文件数很大但有效信息被长年累积的重复代码、废弃分支稀释模型很难判断哪些内容重要。这几个障碍合在一起指向一个结论在遗留代码库上做Agent方案核心不是“让模型更聪明”而是“让模型在更小、更准确的范围内做决策”。2. 七条反直觉法则逐条拆解每一条都是踩坑踩出来的下面这七条是我在棕地Agent工程里反复验证过的原则。每一条基本都对冲直觉哪条看起来越“省事”往往就越危险。2.1 法则一先让Agent去搜索而不是把代码库塞进上下文这是最反直觉的一条几乎每个第一次做棕地Agent工程的人都会踩中。我第一次试验时用的模型上下文窗口很大心想“既然窗口够大那就把项目全量索引塞进去”。结果很惨一方面token成本烧得飞快另一方面模型开始混淆细节把不同年代的代码风格混在一起强行“合理化”甚至虚构出不存在的调用关系。后来我换了思路不再投喂全量而是给Agent配了一套只读检索工具让它用rg搜索符号引用、用tree-sitter查AST节点、用git log看文件演变历史。效果差非常多。遗留代码库的信息密度其实不高大量重复代码、废弃分支、历史实现会把模型注意力稀释掉。全量投喂本质是在让模型同时处理“信号”和“噪声”检索式交互则可以让它只拿“信号”。实际操作上我把代码仓库当成外部工具而不是上下文的一部分。Agent每次行动前先触发搜索拿到匹配行再决定下一步。工具清单通常长这样search_symbol(name)通过rg或ctags查找符号定义和所有引用位置get_callers(file, line)用AST或语言服务协议获取调用者列表git_commits(file)查看文件最近的变更节奏和修改主题。这个结构更接近“Agent工具化检索”而不是“RAG一次性灌库”。RAG适合知识库问答但遗留代码场景里文件级精确检索更可信。另一个反直觉点是很多团队在向量Embedding上投入大量时间却忽略了几乎免费的rg。实测下来老代码里的命名五花八门普通全文检索的准确度往往比向量召回更理想。2.2 法则二先建测试围栏再动业务代码第二点和安全有关。遗留代码不能盲目重构但也不能不做改造。折中的办法是Agent做任何业务逻辑修改之前先建“测试围栏”。“测试围栏”这个概念是从特性测试characterization tests延伸来的。逻辑很简单遗留代码的行为是多年积累的隐性契约有些行为看着像bug但上层系统确实依赖它。如果Agent按“看起来更正确”的写法直接重构你得到的可能是“逻辑正确但流程崩塌”的代码。所以先让Agent为即将改动的函数写一组历史行为测试用当前运行结果作为期望值。测试不需要符合业务脑补的“应该怎样”而是忠实记录现状。用一个非常简单的例子说明def order_status(order): if order.cancelled and order.paid: return refund_required if order.paid: return paid return pending模型很可能会把cancelled and paid返回refund_required视为异常想“优化”成先判断paid。如果一开始就有围栏测试锁住现状后续修改要么明确保持兼容要么在PR里显式提出行为变更。这样就等于把Agent的自由发挥变成了受监控的偏差。流程上我把围栏测试作为Agent任务的验收门槛Agent先准备测试文件并运行通过Agent再做业务修改修改后测试必须保持通过除非任务定义中显式批准行为变化。这条法则反直觉的地方在于业务方通常催着“快重构”你反而让Agent先花时间写不产生新功能的测试。但这部分投入会在后续每一次Agent修改里以低返工率的形式连本带利还回来。2.3 法则三依赖图优先于“AI业务理解”我常听到一句话“先让Agent学习业务再让它写代码。”这话听着没毛病实战里却是伪需求。老代码里的“业务”不是自然语言能讲清楚的它藏在一层一层的调用关系里。与其让Agent读文档、做总结不如直接给它建依赖图。我们通常用ctags加tree-sitter离线生成全仓符号表再写脚本把“函数A调用函数B”“文件M引用了配置项N”落成结构化数据。数据可以存成JSON或SQLite。任务开始前Agent直接查询依赖图而不是“读”整个工程。举个简化输出PaymentService.checkout - PaymentValidator.validate - ThirdPartyGateway.charge - NotificationService.push光看这棵树Agent就知道自己改ThirdPartyGateway.charge时上游会牵扯到PaymentService.checkout。它不需要用语言模型去“猜测这个函数是否被外部调用”而是运行“获取调用者”工具拿到确定答案。反直觉之处在这里你以为让Agent掌握全貌才能干活实际上80%的任务只需要知道“我要改的这棵子树上有哪些叶子节点”。依赖图把“全局理解”替换成了“按需子图”这才是降低幻觉的关键。即使不完美也比让模型凭印象推断可靠得多。2.4 法则四读代码的Agent和写代码的Agent必须分开这是我早期Agent架构里回报最高的一次调整。最初我也倾向于一个Agent同时具备读、写、执行测试三合一的能力结果它的注意力很快被打乱读完一条调用链后一激动就在不该改的文件里开了个新分支。后来参考多Agent协作的常见架构把职责拆开维度读AgentReader写AgentWriter主要工具rg、tree-sitter、git log、ctagsfile write/patch、pytest、git diff上下文策略可大保持仓库地图和全局索引小而准只接收当前任务相关的片段权限只读不允许修改任何文件受限写只允许修改白名单内文件产出物影响清单、风险文件列表、行为陷阱可编译的代码、测试、提交说明两者之间的接口协议是关键。我一般用JSON或Markdown传任务例如{ task_id: PAY-1024, target_file: payment/gateway.py, allowed_edits: [refund_logic], forbidden: [auth, settlement], tests: [tests/test_gateway_refund.py], done_definition: [ pytest tests/test_gateway_refund.py -q, ruff check payment/gateway.py ] }读Agent可以上下文大一些因为它需要信息广度写Agent上下文要小且准因为它需要修改精度。两个Agent共享同一个索引库和依赖图不需要重复阅读。反直觉点在于合并Agent看着省事实际是把“理解”和“修改”两个完全不同性质的问题捆在一起。遗留代码里这两个问题的时间和错误模式都不一样强行揉合只会相互污染。分开后即使写Agent犯错影响半径也小。2.5 法则五静态分析工具链比提示词工程更值得投入我见过有人花两周时间打磨一套史诗级Prompt想让Agent理解系统之间的隐性依赖结果还是一次简单的静态分析工具调用就解决了。在棕地工程里ripgrep、tree-sitter、ctags、clangd、jq这些老派工具的确定性远高于模型对Prompt的临场发挥。Agent真正需要的不是更复杂的自然语言指令而是能直接调用结构查询的工具。工具链设计上我遵循一条原则所有Agent能拿到的结构性事实都优先从工具获取不让模型靠猜。例如# 找到函数的所有定义和引用 rg -n def calculate_fee --type py # 查看文件最近被谁改过 git log --oneline -- payment/gateway.py | head -20 # 用 tree-sitter 解析出某个函数的AST tree-sitter parse app/payments/schema.py反直觉的是投入顺序没有稳定的静态事实层提示词设计得再好也是沙上建塔把工具接口做干净Prompt哪怕写得简单粗暴也能奏效。可以说棕地Agent工程在遗留代码上的核心竞争力是信息架构。Embedding和Prompt只是配角工具链路才是主角。2.6 法则六给Agent限定可写范围范围外只读第六点关于可控性。Agent一旦开始改代码范围控制比代码质量更重要。遗留代码库中任意文件改动都可能破坏未知调用链所以要从机制上约束Agent的写权限。实现上我把权限抽象成三层文件访问规则可读可写任务负责的模块Agent可以在这里自由修改只读会受影响但不在任务范围内的模块Agent可以查阅但不能修改完全禁止构建脚本、部署文件、反向迁移脚本以及正在重大重构中的目录。工具层上我用一个简单的白名单脚本包裹file_edit工具每次写文件前先检查路径是否在可写白名单里不在就直接拒绝。这个设计比在Agent提示词里反复强调“不要修改xxx”可靠得多因为Prompt只是建议工具拒绝对是执行。反直觉的部分是我实践后才认的绿地工程强调Agent自主性越高越好棕地工程反其道行之很多“成功”恰恰来自一堆限制。让Agent在可写范围内自由发挥在范围外保持只读顾问身份既不损害产出又大幅降低灾难概率。2.7 法则七用可执行断言衡量完成而不是听Agent的自我汇报最后一条关于验收。“Agent告诉我它完成了”这句话在棕地项目中可信度极低。模型可能因为上下文遗忘而漏掉步骤也可能由于过度自信而隐瞒测试失败。所以每个Agent任务一开始就要定义“完成”的可执行断言。我们叫它完成定义通常长这样done_definition: - command: python -m compileall payment/ - command: pytest tests/test_payment_core.py -q expect: passed - command: git diff --name-only expect: only allowed_edits files - command: ruff check payment/ expect: 0 errors执行机制是Agent每完成一轮修改必须运行这些命令并把输出原封不动贴回对话。如果断言失败Agent进入有限重试最多三轮。三轮后仍不过直接标记任务失败并转人工。这个设计的反直觉点在于你把Agent的自我判断降级成“命令执行器”看似限制了模型的智慧实际是取长补短。模型不擅长精确计数和持续记忆但擅长模式识别和方案生成命令和测试是精确裁判。让模型做擅长的事把裁判权交给可执行断言项目整体质量立刻稳定一个台阶。3. 实战复盘一个20年支付模块的Agent化改造理论说多了容易飘我拿一个实际案例来讲。去年我们接手了一个跑了快20年的支付结算模块PHP和Python混写核心逻辑还嵌在一个老框架里。目标是整理支付对账逻辑并迁移掉一个已经过期的外部服务。3.1 第一步建立仓库地图和风险清单开工第一周我们没有让任何Agent写代码精力全放在搭“仓库地图”上用ctags -R --languagesPHP,Python生成符号表用tree-sitter快速解析文件AST并建立索引写脚本扫描include、import、RPC调用输出模块依赖列表人工定位并标注三个高风险入口支付回调、对账定时任务、退款冲正。最后产出一张结构化的风险清单而不是一段“代码总结”。表里包含文件名、作用、耦合模块、测试覆盖情况。有了这张清单我们才知道支付模块里真正涉及核心链路的文件只有12个却有37个文件“看起来像相关”。如果这一步不做Agent会在37个文件里白白消耗大量精力。3.2 第二步读Agent产出的影响清单第二周开始让读Agent对核心链路的每个函数做影响分析。它的产出不是散文而是结构化清单。简化示例如下{ node: PaymentGateway::refund, callers: [refund_order, auto_reverse, admin_manual_refund], external_calls: [BankAPI.refund, NotificationService.push], behavior_traps: [ 当订单金额为0时BankAPI.refund会被跳过但状态会更新为已退款 ], test_coverage: 无直接测试仅有间接集成测试 }读Agent的价值在于它能在短时间内遍历大量调用路径找出人工翻三天代码才发现的行为特例。尤其“金额为0时跳过退款但更新状态”这类隐蔽逻辑靠人工搜索非常耗神。但同样要注意读Agent的输出必须经过人工抽查尤其是“behavior_traps”这种结论极容易被模型脑补。我们的做法是请维护老系统的同事逐条确认认可后才进入下一阶段。3.3 第三步写Agent执行的小步修改第三周才开始真刀真枪地改代码。每次只拆一个小任务改动范围锁定在1到3个文件。写Agent先补围栏测试然后修改最后跑指定测试。有一轮让我印象特别深Agent把“退款后状态置为closed”改成了“置为refunded”测试结果看起来更合理但读Agent影响清单里明确写着外部老系统会读取这个状态字段。测序围栏覆盖了这个断言失败后Agent重新查依赖图主动撤回修改补了一层兼容转换。这轮真实返工大约40分钟听起来不短但人工在不知道隐式契约的情况下改出这种错可能上线后才会发现。时间成本完全不是一个量级。3.4 最后的结果和一些具体数据整体改造持续了一个半月写Agent负责实际编码人工负责审核和决策。体感数据是Agent产出的第一批代码约六成能直接合入其余四成里又有六成是测试失败后自己修好的真正需要人工动手的不到整体工作的两成。相比纯人工最明显的收益不是“打字速度快”而是这种问题的解决时间降了一个量级“这个字段在哪里被读”“这个调用链会走到哪去”“这个分支为什么存在”。老系统迁移最贵的从来不是写代码而是信息梳理。4. 文档里不会写的反直觉经验除了上面七条法则还有一些更软的体会通常不会出现在技术文档里但对项目成败影响很大。4.1 Agent最怕的不是“不懂业务”而是没有边界连续做了几个项目后我得出的经验是Agent在遗留代码库失败很少因为模型能力不足更多是因为任务边界模糊。边界包括范围、验收、权限、完成定义。宁可让Agent在一个小范围内干得保守也比让它在全库里干得漂亮要安全得多。这种边界感有两个来源一个是任务描述本身必须在启动前说清楚“改什么、不改什么、完成标准是什么”另一个是工具层的硬约束比如文件白名单、只读权限、禁止执行命令。后者尤其重要因为模型对长文本任务的执行力会随着对话轮数衰减硬约束能兜住这种衰减。4.2 模型升级是副产品索引和测试围栏才是核心资产换模型、换Agent框架这件事在棕地工程里带来的边际收益其实有限。真正能沉淀的资产是那些与模型无关的东西依赖图库、围栏测试、文件权限白名单、完成定义。这些抽象数据层意味着将来出现更好的模型整套Agent流程可以低成本迁移不需要推倒重来。这也是棕地工程和绿地工程在资产管理上的本质差异。绿地Demo里模型能力本身就是主角换模型等于换产品棕地里主角是信息架构和测试体系模型只是执行引擎。4.3 数据迁移类任务建议人工全程把关并不是所有存量任务都适合放Agent出来跑。我特别警惕的是数据迁移类任务比如把旧库数据搬到新库、重写历史交易的对账逻辑。这类任务有两个特点一是无法通过单元测试完全覆盖二是出错后的修复成本极高。这类场景下我通常让Agent只做“辅助分析”和“生成迁移脚本草稿”真正的执行、评审、灰度上线必须由人完成。不是信不过Agent而是数据迁移这种场景根本等不起试错。4.4 给Agent一点“故意低效”的空间最后一条软经验是不要追求Agent一步到位。我们在初期总希望Agent能一次性生成完美的完整方案结果往往是返工最高。后来刻意让Agent先产出“可能有点笨但完整”的方案再通过测试和审查去迭代。这种“故意低效”的好处是Agent不会跳过细节直接冲进一个过于自信的坑里。它更像一个高配实习生你可以让它先干一遍但你得给它检查清单和时间线而不是让它自由发挥。5. 最后想分享的一句话经验如果这篇文章只留一句话我想说棕地Agent工程的第一责任不是发挥Agent上限而是压低它的破坏下限。操作顺序必须是先建地图再圈围栏最后才谈智能。地图、索引、测试围栏和权限边界都稳住以后Agent换谁都能干活这些资产没建好换什么模型都等于高档碰运气。我现在的项目里团队已经形成一种潜意识评价Agent改造第一看返工率第二看失控次数第三才看生成了多少行代码。这个顺序正是从绿地走向棕地最重要的心态切换。最后再补一个实用细节如果你的Agent任务连续三轮测试不过不要急着换Prompt先回去查依赖图和围栏测试是不是建错了。多数时候问题不在模型而在你没有给它一个准确、有限、可验证的战场。这个坑我踩过也帮你提前踩了。