ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

棕地Agent工程实战:遗留系统AI Agent落地的七条反直觉法则

棕地Agent工程实战:遗留系统AI Agent落地的七条反直觉法则 1. 棕地 Agent 工程到底在解决什么问题1.1 从“白地”到“棕地”一个被忽视的战场过去两年大部分关于 AI Agent 的讨论都集中在一个理想化的场景里你有一个全新的项目干净的代码库清晰的架构然后你从零开始搭建一个 Agent 系统。这种场景我称之为“白地 Agent 工程”——绿地项目没有历史包袱想怎么设计就怎么设计。但现实是绝大多数开发者面对的并不是白地。你手里有一个跑了五年的 Spring Boot 单体应用或者一个 Django 项目里面塞满了各种历史遗留的 service 层、工具类、定时任务测试覆盖率不到 30%文档停留在三年前。这时候老板跟你说“给咱们系统加个 AI Agent 吧让用户能用自然语言查数据、触发流程。”这就是“棕地 Agent 工程”要解决的问题。棕地Brownfield这个词来自城市规划指的是那些已经被开发过、可能存在污染或基础设施老化的地块。对应到软件领域就是那些已经存在、有技术债务、但仍在承载核心业务的代码库。你不可能推倒重来只能在现有基础上做增量改造。我过去一年在三个不同规模的遗留系统上落地过 AI Agent踩过的坑比想象中多得多。这篇文章不讲那些“从零搭建 Agent”的教程而是聚焦一个更现实的问题当你面对一个几十万行代码、依赖关系错综复杂的遗留系统时怎么让 AI Agent 真正跑起来而不是变成一个演示完就没人用的玩具。1.2 棕地 Agent 工程的三个核心约束在动手之前你必须先认清棕地场景的三个硬约束这决定了你后续所有技术选型和架构决策。约束一不能大改现有代码结构。遗留系统的代码可能很烂但它能跑而且在承载真实业务。你没有权限也没有时间去做大规模重构。Agent 必须作为一个“外挂层”存在通过接口调用现有能力而不是侵入式地修改核心逻辑。约束二现有接口的语义不清晰。白地项目里你设计接口时会考虑语义化和可组合性。但遗留系统里的接口往往是“历史堆积”的产物——一个processData()方法可能做了七八件事参数是一个巨大的 DTO返回值是一个 Map。Agent 要调用这些接口首先得理解它们到底在干什么。约束三没有完整的测试覆盖。这是最要命的一点。你想改任何东西都担心会不会把某个隐藏的业务逻辑搞崩。特征测试Characterization Test在这里不是可选项而是必选项。你得先给现有系统“拍照”记录它当前的行为才能在后续改造中有底气。理解了这三个约束你就能明白为什么棕地 Agent 工程需要一套完全不同的方法论。下面我拆成七个反直觉的法则来讲每一条都是我在实际项目中验证过的。2. 法则一先写特征测试再碰 Agent 代码2.1 为什么特征测试是棕地 Agent 的地基很多人拿到一个遗留系统第一反应是“我先搭个 Agent 框架跑通一个 demo 再说”。这个思路在白地项目里没问题但在棕地场景里是致命的。原因很简单Agent 要调用现有系统的接口而你对这些接口的实际行为可能并不完全了解。文档写的是 A代码实现是 B线上跑出来的结果是 C。如果你不先把现有行为固化下来后面 Agent 出了 bug你根本分不清是 Agent 的逻辑问题还是底层接口本来就有坑。特征测试的核心思路是不关心代码“应该”怎么跑只记录它“实际”怎么跑。你给一个输入记录输出把这个输入输出对作为测试用例。这些测试不验证业务正确性只验证行为一致性。后续你改任何东西只要这些测试还过就说明你没有破坏现有行为。2.2 特征测试的实操步骤具体怎么做我以一个有十年历史的 Java 后端系统为例。第一步找出 Agent 将要调用的核心接口。不要贪多先圈定 Agent 需要用到的那 10 到 20 个方法。这些方法通常集中在几个 service 类里比如订单查询、用户信息获取、库存检查等。第二步为每个方法构造典型输入。这里有个技巧从生产日志里捞真实请求。把过去一周的接口调用日志导出来按方法名分组每个方法取 50 到 100 个真实输入样本。这比你自己拍脑袋构造的输入靠谱得多。第三步记录输出并生成测试。写一个简单的脚本遍历这些输入调用方法把输入和输出序列化成 JSON然后自动生成 JUnit 测试。下面是一个简化的示例// 自动生成的特征测试骨架 Test public void characterize_queryOrder() { // 输入来自生产日志样本 OrderQueryRequest request new OrderQueryRequest(); request.setOrderId(ORD-2023-001); request.setUserId(12345L); // 调用实际方法 OrderResult result orderService.queryOrder(request); // 断言实际输出首次运行时从实际结果复制 assertEquals(PAID, result.getStatus()); assertEquals(3, result.getItems().size()); assertEquals(new BigDecimal(299.00), result.getTotalAmount()); }第四步跑通所有特征测试确保全绿。这时候你就有了一个“行为基线”。后续任何改动只要这些测试还过就说明底层行为没变。注意特征测试不是单元测试不要试图去 mock 依赖。让它跑真实逻辑哪怕慢一点。你追求的是行为快照的准确性不是测试执行速度。2.3 特征测试的常见坑第一个坑是数据依赖。遗留系统的接口往往依赖数据库里的特定数据状态。你今天跑测试是绿的明天数据库被人改了一条记录测试就红了。解决办法是给测试准备独立的数据集或者在测试前用脚本重置数据。第二个坑是时间依赖。很多接口的行为跟当前时间有关比如“查询最近 30 天订单”。这种测试你没法直接断言输出需要把时间参数化或者用固定时钟注入。第三个坑是外部服务依赖。如果接口调用了第三方支付、短信等服务特征测试会变得不稳定。我的做法是先用挡板Stub把外部依赖隔离掉只测试本地逻辑的行为。3. 法则二Agent 不是替代者是翻译层3.1 重新理解 Agent 在遗留系统中的角色很多团队在引入 Agent 时潜意识里把它当成一个“更聪明的接口”。用户说一句话Agent 理解意图然后调用对应接口返回结果。这个理解不算错但太浅了。在棕地场景里Agent 的真正价值是翻译层。它翻译的不是语言而是语义鸿沟。遗留系统的接口是给程序员用的参数是技术化的返回值是结构化的。但用户的需求是业务化的、模糊的、带有上下文依赖的。Agent 要做的是把“帮我看看上个月那个大单子到哪了”翻译成queryOrder(orderId..., userId...)这样的技术调用。这个翻译过程涉及三个层次的理解意图识别、参数映射、结果解释。意图识别是基础参数映射是难点结果解释是加分项。3.2 参数映射棕地 Agent 最脏最累的活参数映射为什么难因为遗留系统的接口参数往往不是为自然语言设计的。我见过一个查询接口需要传一个sceneCode参数取值是01、02、03分别代表不同的业务场景。用户说“查一下我的订单”Agent 怎么知道该传哪个 sceneCode解决办法是建一个语义映射表。把接口参数的业务含义显式地写出来作为 Agent 的知识库。这个表不需要很复杂一个 YAML 文件就够了queryOrder: description: 查询订单详情 parameters: sceneCode: type: enum values: 01: 普通商城订单 02: 团购订单 03: 预售订单 default: 01 orderId: type: string description: 订单编号通常以 ORD- 开头 userId: type: long description: 用户ID从当前登录态获取有了这个映射表Agent 在解析用户意图时就有了依据。用户说“查一下我的团购订单”Agent 就能推断出 sceneCode 应该是 02。3.3 结果解释让 Agent 说人话遗留系统的返回值往往是给程序看的不是给人看的。一个订单查询可能返回几十个字段包含各种状态码、时间戳、内部标识。用户只想知道“我的订单到哪了”。Agent 的结果解释层要做的是从结构化数据中提取关键信息用自然语言组织成用户能理解的回答。这里有个原则宁可少说不要乱说。如果某个字段的含义不确定宁可不提也不要瞎猜。我通常会在映射表里加一个responseTemplate字段定义每种意图的返回格式queryOrder: responseTemplate: | 您的订单 {{orderId}} 当前状态是{{statusDesc}}。 下单时间{{createTime}} 订单金额{{totalAmount}} 元 包含 {{itemCount}} 件商品。这样 Agent 只需要做字段填充不需要自己组织语言既保证了准确性又降低了幻觉风险。4. 法则三迁移盲区比技术债务更危险4.1 什么是迁移盲区迁移盲区Migration Blind Spot是我自己造的一个词指的是那些在遗留系统中“看起来能用但实际上已经腐烂”的部分。它们不在你的技术债务清单上因为没人觉得它们是问题直到 Agent 调用它们时炸了。举个例子。你有一个用户信息查询接口返回用户的基本信息。这个接口跑了五年一直没问题。但当你让 Agent 去调用它时发现返回的userLevel字段有时候是数字有时候是字符串有时候是 null。为什么因为五年前有个实习生写了一段代码在某些分支下直接返回了原始数据库值没有做类型转换。这个 bug 一直存在但因为前端做了兼容处理没人发现。现在 Agent 直接消费这个接口就踩坑了。4.2 如何发现迁移盲区发现迁移盲区的方法论是用 Agent 的调用方式去压测现有接口。具体来说做三件事。第一边界值测试。把每个参数推到极端值——空字符串、超长字符串、负数、零、最大整数——看接口怎么反应。很多遗留接口在边界条件下会返回意料之外的结果。第二并发测试。Agent 的调用模式跟人类用户不同它可能在短时间内发起大量请求。遗留系统里那些依赖单例状态、静态变量、线程不安全集合的代码在并发场景下会暴露问题。第三时序测试。Agent 可能会在非工作时间调用接口或者以人类不会采用的顺序调用接口。比如先查订单再查用户而正常流程是先查用户再查订单。这种时序变化可能触发隐藏的状态依赖问题。4.3 迁移盲区的处理策略发现迁移盲区后你有三个选择修复、绕过、或者隔离。修复是最彻底的但成本最高。如果这个盲区影响面大而且修复方案清晰那就修。但更多时候我建议绕过。在 Agent 层加一个适配器把脏数据洗干净再返回给 Agent。这样既不影响现有系统又能让 Agent 正常工作。隔离是最保守的策略。如果某个接口的盲区太多修不动也绕不过那就干脆不让 Agent 调用它。在映射表里把这个接口标记为deprecated让 Agent 走别的路径。我的经验是迁移盲区的处理优先级应该是“绕过 隔离 修复”。因为你的首要目标是让 Agent 跑起来而不是借这个机会重构遗留系统。重构的事等 Agent 稳定运行三个月后再考虑。5. 法则四Agent 的并发能力取决于最慢的那个接口5.1 为什么 Agent 并发是个伪命题“AI Agent 怎么扛并发”是最近被问得最多的问题之一。很多人的思路是Agent 框架本身要支持高并发要用异步、要用协程、要用消息队列。这个思路在白地项目里成立但在棕地场景里Agent 的并发能力根本不取决于 Agent 框架而取决于它调用的最慢的那个遗留接口。我做过一个测试Agent 框架本身用异步 IO单机能扛 5000 QPS。但它调用的订单查询接口底层是一个没有索引的数据库查询平均响应时间 800ms并发超过 50 就开始排队。结果整个 Agent 系统的实际吞吐量被卡在 50 QPS 左右。5.2 棕地 Agent 的并发优化策略既然瓶颈在遗留接口优化就要从接口层入手。但你不能直接去改数据库索引或者重写查询逻辑那超出了 Agent 项目的范围。你能做的是在 Agent 层做请求合并和结果缓存。请求合并的思路是如果多个 Agent 请求在短时间内查询同一个数据把它们合并成一个底层调用。比如用户 A 和用户 B 同时查同一个订单Agent 层只发一次查询请求然后把结果分发给两个请求。结果缓存的思路更直接对于读多写少的数据在 Agent 层加一层缓存。缓存的有效期不需要很长5 到 10 秒就够了。这能挡住大部分重复请求。下面是一个简单的请求合并实现思路class RequestCoalescer: def __init__(self): self.pending {} async def query(self, key, fetch_func): if key in self.pending: return await self.pending[key] future asyncio.ensure_future(fetch_func()) self.pending[key] future try: result await future return result finally: del self.pending[key]这段代码的核心逻辑是如果同一个 key 的请求已经在处理中就复用那个 future而不是发起新的调用。5.3 并发场景下的降级策略即使做了合并和缓存遗留接口在高峰期仍然可能扛不住。这时候你需要一个降级策略当底层接口响应时间超过阈值时Agent 自动切换到“简化模式”。简化模式的意思是不查完整数据只返回最核心的信息。比如订单查询正常模式返回订单详情、商品列表、物流信息简化模式只返回订单状态。这样底层接口的负载能降低一个数量级。降级策略的触发条件需要根据实际压测结果来定。我的经验值是当 P99 响应时间超过 2 秒或者错误率超过 5%就触发降级。6. 法则五Agent 的提示词要写进代码仓库6.1 提示词不是配置是代码很多团队把 Agent 的提示词当成配置文件放在数据库里或者配置中心运行时动态加载。这个做法在白地项目里可以但在棕地场景里会带来严重问题。原因很简单棕地 Agent 的提示词跟遗留系统的接口语义强耦合。接口改了提示词必须跟着改。如果提示词在配置中心代码在 Git 仓库两者的版本就对不上了。你改了一个接口的参数名忘了同步更新配置中心的提示词Agent 就开始胡言乱语。我的做法是提示词必须跟代码在同一个仓库同一个分支同一个提交里。提示词文件用 Markdown 或者 YAML 格式放在代码目录下跟调用它的代码放在一起。6.2 提示词的版本管理策略提示词进仓库后版本管理就变得很重要。我通常采用三层结构第一层是基础提示词定义 Agent 的角色、能力边界、输出格式。这部分相对稳定变更频率低。第二层是接口提示词每个遗留接口对应一段提示词描述这个接口的功能、参数、返回值。这部分跟接口代码强绑定接口改了就改它。第三层是场景提示词针对特定业务场景的补充说明。比如“查询订单时如果用户没有指定订单号优先查询最近一笔订单”。这部分最灵活可以频繁调整。三层提示词在运行时拼接成完整的 prompt。这样既保证了稳定性又保留了灵活性。6.3 提示词的测试与回归提示词进仓库的另一个好处是可以做回归测试。你可以像写单元测试一样为提示词写测试用例给定一个用户输入断言 Agent 的输出包含某些关键信息。def test_query_order_prompt(): user_input 帮我查一下上个月那个大单子 context {userId: 12345} result agent.process(user_input, context) assert 订单 in result assert ORD- in result # 应该包含订单号 assert 元 in result # 应该包含金额这些测试跑在 CI 里每次改提示词都会触发。如果某个改动导致测试失败你立刻就知道有问题。7. 法则六不要追求全自动人机协同才是终点7.1 全自动 Agent 的幻觉陷阱很多团队对 Agent 的期望是“全自动”——用户说一句话Agent 从头到尾搞定不需要人工介入。这个目标在演示环境里很容易实现但在生产环境里尤其是棕地场景里几乎不可能。原因在于遗留系统的不确定性。接口可能返回脏数据业务规则可能有例外情况用户输入可能模糊不清。Agent 在这些情况下如果强行“自动处理”结果往往是灾难性的。我见过一个案例Agent 自动帮用户提交了一个退款申请因为用户说“这个订单我不想要了”。但用户的实际意思是“我想修改订单地址”只是表达得比较随意。结果退款流程走了一半用户收到退款通知才发现问题。7.2 人机协同的三种模式在棕地 Agent 工程里我推荐三种人机协同模式根据场景风险等级选择。模式一确认式协同。Agent 完成意图理解和参数映射后把结果展示给用户确认用户点“确认”后才执行。这种模式适合高风险操作比如退款、删除、修改关键数据。模式二建议式协同。Agent 给出建议但由人工决定是否采纳。比如 Agent 说“我建议查询 sceneCode02 的团购订单是否正确”用户可以选择“是”或者手动指定其他值。这种模式适合中等风险操作。模式三静默式协同。Agent 自动执行但记录完整日志人工可以事后审计。这种模式适合低风险操作比如查询类请求。7.3 协同模式的选择标准怎么判断一个操作该用哪种模式我通常看三个维度可逆性、影响范围、用户预期。可逆性操作能不能撤销查询可以退款不行。影响范围操作影响一个用户还是所有用户影响越大越需要确认。用户预期用户是否期望这个操作自动完成如果用户说“帮我查一下”他期望立刻看到结果如果用户说“帮我处理一下”他可能期望有人工介入。把这三个维度画成一个矩阵就能快速判断该用哪种协同模式。8. 法则七Agent 的日志比 Agent 本身更重要8.1 为什么棕地 Agent 需要超详细日志在白地项目里Agent 出问题了你可以看代码、看测试、看监控。但在棕地场景里Agent 出问题往往是因为底层遗留系统的某个隐藏行为。如果你没有详细的日志根本无从排查。我要求棕地 Agent 的日志必须包含以下信息用户原始输入、Agent 解析后的意图、映射到的接口和参数、接口的实际调用时间和返回值、Agent 生成的最终回复。这五段信息缺一不可。8.2 日志的结构化与可查询日志不能是纯文本必须是结构化的 JSON。每个字段都有明确的含义方便后续查询和分析。{ traceId: abc-123, timestamp: 2024-01-15T10:30:00Z, userInput: 帮我查一下上个月那个大单子, parsedIntent: queryOrder, mappedParams: { sceneCode: 01, userId: 12345, timeRange: last_month }, apiCall: { endpoint: /api/order/query, durationMs: 850, responseCode: 200 }, agentResponse: 您的订单 ORD-2023-1234 当前状态是已发货... }有了这样的日志排查问题就简单了。用户说“Agent 回答错了”你拿 traceId 一查立刻能看到是意图解析错了还是参数映射错了还是接口返回了脏数据。8.3 日志驱动的持续优化日志不仅是排查工具还是优化依据。我每周会做一次日志分析看三件事第一意图解析失败率。哪些用户输入 Agent 无法正确理解把这些输入收集起来补充到提示词的示例里。第二参数映射错误率。哪些参数经常映射错检查映射表是不是写得不清楚或者接口本身有歧义。第三接口异常率。哪些接口经常返回错误或超时这些接口就是迁移盲区的候选需要重点处理。这个日志驱动的优化循环是棕地 Agent 工程能持续迭代的关键。没有日志你就是在盲人摸象。9. 棕地 Agent 工程的工具选型与团队配置9.1 工具选型的核心原则棕地 Agent 工程的工具选型跟白地项目完全不同。白地项目可以追求最新最酷的框架棕地项目必须追求稳定、可观测、易集成。Agent 框架方面我倾向于选择那些对遗留系统友好的方案。比如 LangChain 和 LangGraph 提供了丰富的工具集成能力可以很方便地包装现有 HTTP 接口。Spring AI 对于 Java 技术栈的团队来说是个不错的选择它能直接复用 Spring 生态的依赖注入和配置管理。如果你追求极致的性能和并发Rust 生态的 Agent 框架也值得考虑但前提是你的团队有 Rust 经验。否则学习成本会拖慢整个项目。9.2 团队配置建议棕地 Agent 工程不需要很大的团队但需要角色搭配合理。我的建议配置是一个架构师负责整体方案设计和迁移盲区的判断。这个人必须对遗留系统有深入了解知道哪些地方能碰、哪些地方不能碰。一个后端工程师负责 Agent 层的开发和接口适配。这个人要熟悉 Agent 框架同时也要能读懂遗留代码。一个测试工程师负责特征测试和回归测试。这个人要有耐心愿意写大量的行为快照测试。如果条件允许再加一个业务分析师负责梳理接口语义和编写映射表。这个角色往往被忽视但在棕地场景里非常重要。9.3 项目推进的节奏棕地 Agent 工程不能搞“大爆炸”式上线。我的建议是分三个阶段推进。第一阶段是单点验证选一个最简单的查询场景把 Agent 跑通。这个阶段的目标不是功能完整而是验证技术路线可行。第二阶段是场景扩展把 Agent 的能力扩展到 5 到 10 个核心场景。这个阶段会暴露大量的迁移盲区和接口问题是工作量最大的阶段。第三阶段是稳定运行重点做日志分析、性能优化、降级策略。这个阶段的目标是让 Agent 在生产环境里稳定跑三个月以上。每个阶段之间留出至少两周的缓冲期用来处理意料之外的问题。棕地项目的问题总是比预想的多。10. 一些踩坑之后的个人体会棕地 Agent 工程最反直觉的一点是技术不是最大的障碍对遗留系统的理解才是。我见过太多团队Agent 框架玩得很溜但一碰到遗留系统的脏数据、隐藏逻辑、 undocumented 行为就束手无策。我的建议是在写第一行 Agent 代码之前先花两周时间做三件事读遗留系统的核心代码跑特征测试跟业务方聊接口的实际使用场景。这三件事做完你对系统的理解会超过过去半年的总和。另一个体会是不要试图用 Agent 去解决遗留系统的所有问题。Agent 是一个增量能力不是重构工具。它的价值在于让用户用更自然的方式使用现有系统而不是让现有系统变得更好。把这两个目标混在一起项目一定会失控。最后分享一个实用技巧在 Agent 的映射表里给每个接口加一个confidence字段表示你对这个接口语义理解的置信度。置信度低的接口Agent 在调用时自动触发人工确认。这个简单的机制能挡住大部分因为接口理解错误导致的问题。棕地 Agent 工程没有标准答案每个遗留系统都是独特的。但上面这七条法则是我在三个项目里反复验证过的。它们不一定全对但至少能让你少走一些弯路。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表