Appearance
Brownfield 实战一:让 Agent 先理解,再修改
系列第 09 篇
实战对象:独立 Spring Boot 订单服务
核心问题:怎样用基线、代码地图和任务契约阻止 Agent 在旧系统里盲改
最后核验:2026-07-20
对应视频:第 01 集(第一章)《AI 编程实战:让 Codex 从需求走到可验证交付》
Greenfield 演示常从空目录开始,Agent 可以自由选择结构,生成结果也容易显得完整。真实工作更多发生在 Brownfield:代码已经运行,测试表达了一部分历史,命名不完全一致,团队还有未写出的约束。此时最危险的不是 Agent 写不出代码,而是它太快写出一套与现有系统不一致、却局部合理的代码。
本篇从一份真实可运行的 Spring Boot 服务出发。基线只支持创建订单,共 8 个测试;任务是在不引入数据库、不改变创建接口的条件下增加取消订单。整个过程保留五个 Git 标签:baseline、failing-tests、initial-implementation、review-correction、final。文章、视频和代码都引用这些节点,而不是事后编造过程。
阅读路线:先看全局,再进入机制
视频版先展示整章地图,再逐层展开。博客沿用同一判断顺序,但保留更多机制、案例、失败模式和生产边界:
- 可信基线:先证明仓库当前能够构建和测试,而不是把旧失败归因于新改动。
- 代码库地图:从入口、调用链、状态和测试建立任务相关子图。
- 只读探索:让 Agent 引用真实文件与符号,主动修正错误假设。
- 实施计划:把发现转成文件级步骤、风险和验证顺序,再开放编辑。
这四步不是目录装饰,而是一条决策链:可信基线 → 代码库地图 → 只读探索 → 实施计划。阅读后面的案例时,可以随时回到这条链判断当前问题发生在哪一层。
真实问题:为什么一句“增加取消订单接口”远远不够
Agent 若直接搜索 Controller,可能在 Web 层修改状态;若看到 Repository 只有 save,可能顺手引入 JPA;若只实现成功路径,会遗漏重复取消、已支付和不存在;若没有基线,它也不知道原测试是被自己破坏,还是环境一开始就失败。
Brownfield 的第一目标不是生成代码,而是降低未知数:当前系统能否构建?入口、业务和状态分别在哪里?已有测试怎样组织?哪些行为必须保留?新功能的状态模型是什么?只有这些问题有证据,实施计划才不是模型根据常见项目模板作出的猜测。
机制:先建立仓库地图,而不是先读所有文件

订单服务使用清晰但很小的六部分结构:Web 接收 HTTP 并生成响应;Application 编排用例;Domain 保存订单和状态;Port 定义存储契约;Infrastructure 提供内存实现;Tests 覆盖单元和 MockMvc。探索目标是确认这种职责分布是否真实存在,而不是仅根据目录名推断。
典型只读命令如下:
bash
pwd
git status --short
rg --files -g '!target/**'
sed -n '1,220p' pom.xml
rg "class Order|record Order|enum OrderStatus|interface OrderRepository"
rg "@PostMapping|OrderService|MockMvc" src先看工作目录和 Git 状态,是为了不把别人的未提交改动当基线;排除 target,避免生成物污染搜索;读取构建文件,确认 Java、Spring 和测试工具;再沿领域实体、路由和测试追踪。一次性把 src 全部读进上下文并不会自动形成调用链。
1. 从已有请求链推断新功能的落点

基线的 POST /api/orders 由 OrderController 接受 CreateOrderRequest,转换为 Command;OrderService 检查金额上限、创建 Order 并调用 OrderRepository.save;Controller 返回 201 和 Location。这个链路证明项目已经选择“Web 适配协议、Service 编排、Domain 表达数据、Port 隔离存储”。
取消接口应沿用相同方向:Controller 解析 UUID,Service 查询并编排,Domain 决定状态是否可转换,Repository 保存新状态,Exception Handler 映射 HTTP。若 Agent 绕过 Service 直接操作 InMemoryOrderRepository,即使接口工作,也破坏了既有边界。
“沿用现有模式”不等于盲目复制。创建订单没有查询,而取消必须读取当前状态,因此 Port 的能力需要扩展。探索要区分可复用结构与新需求带来的真实差异。
2. 基线是一份可复现的事实声明

在 e081137(baseline)运行:
bash
mvn test观察结果是 8 个测试通过。这个证据只说明当前本地环境、当前提交的测试通过,不等于系统所有行为正确。它仍然有三个价值:工具链可用;后续失败可归因于改动;原有创建、校验和 Spring 对象图行为成为回归边界。
基线标签还保存了教程的起点。任何读者可以执行 git switch --detach baseline 重现只支持创建的服务,再回到 final。没有标签的“修改前截图”无法确认是否来自同一源码和依赖。
基线检查还应包含 git status --short。若起点有用户改动,Agent 不能擅自清理;需要识别任务文件与已有变化是否重叠。本文的演示仓库在每个标签都保持可解释状态,最终工作树为空。
3. Git 节点保存推理转折,而不是只保存最终结果

五个节点分别回答不同问题:
baselinee081137:原服务是否稳定;failing-tests4328c1f:新需求是否被可执行测试表达;initial-implementation9e4a379:第一次实现能否满足行为;review-correction0d3a019:测试通过后是否仍需结构修正;final48582bb:文档、验证和限制是否完整。
提交不是为了制造漂亮历史,而是缩短调试半径。发现初次实现职责错误时,可以比较两个节点,而不必从最终 Diff 猜测演进过程;失败测试可以单独运行,证明它不是和实现同时出现的“装饰测试”。
只读探索需要产出决策,不是仓库摘要

有效探索应回答与任务直接相关的六个问题:
- HTTP 入口和响应对象在哪里?
- 业务用例由哪个对象编排?
- 当前状态模型在哪里,是否已有状态行为?
- Repository 能否按 UUID 查询?
- 项目如何把异常映射成 HTTP?
- 单元和端到端测试使用什么替身与启动方式?
答案来自源码和运行结果:OrderStatus 只有 CREATED;Order 是不可变 record;Port 只有 save;已有异常通过 OrderExceptionHandler 转为 JSON;Service 测试使用轻量 Repository 替身和固定 Clock;Web 测试通过 @SpringBootTest 与 MockMvc。由此可以推导取消功能需要新增查询契约、状态分支、异常和多层测试。
仓库摘要说“这是一个分层 Spring Boot 项目”几乎没有实施价值。探索产物应该能直接进入计划,例如“在 Port 增加 Optional<Order> findById(UUID),并同步修改所有实现和测试替身”。
任务契约把自然语言转为可执行行为

本次契约明确:增加 PAID、CANCELLED;增加 UUID 查询、应用用例和 POST /api/orders/{id}/cancel;首次取消返回 200/CANCELLED,重复取消幂等,已支付返回 409/ORDER_CANNOT_BE_CANCELLED,不存在返回 404/ORDER_NOT_FOUND;保留创建接口;不引入数据库、支付接口、认证和远程部署。
非目标尤其重要。没有“不修改数据库结构”,Agent 可能为了查询引入 JPA;没有“无支付接口”,它可能扩建完整支付流程;没有“本地交付”,教程可能把远程 PR 当作完成标准。非目标不是阻止合理设计,而是冻结本任务不需要承担的决策。
契约还应写验证方法。mvn test 是自动化门禁,真实 HTTP Smoke 检查创建、首次取消、重复取消和不存在。已支付路径由 Service 与 Domain 测试覆盖,因为公开 API 没有把订单变成 PAID 的入口;这项证据边界要在交付时如实说明。
在写测试前先固定状态模型

取消不是简单字段赋值,而是状态转换:CREATED 可以到 CANCELLED;CANCELLED 再次取消返回同一成功结果;PAID 拒绝;未知 ID 甚至没有状态,属于应用查询失败。状态模型决定测试矩阵和 HTTP 映射。
幂等需要精确定义。本文要求重复取消的可观察结果仍为 200/CANCELLED,且不额外保存。这比“接口要幂等”更可测试。若生产系统允许重复保存但结果一致,幂等语义也可能成立;本任务把无额外保存纳入验收,是为了展示状态对象返回同一实例和 Service 短路的设计。
409 表达资源当前状态与请求冲突,404 表达目标不存在。HTTP 选择属于适配层,核心业务仍是“PAID 不可取消”和“找不到订单”。测试不应让 Domain 依赖 HTTP。
实施计划必须把文件与责任一一对应

计划不是“1. 修改代码,2. 运行测试”。它应说明:
text
Domain 增加状态和取消不变量
Port 增加按 UUID 查询的契约
Infrastructure 实现内存查询
Application 处理不存在并编排保存
Web 暴露路由并映射 404/409
Tests 先固定状态、保存和 HTTP 行为
Docs 记录节点、命令、证据和限制同时确定顺序:先测试并确认真实失败;再补最小实现;运行聚焦与全量测试;Review 领域职责;通过真实 HTTP;最后记录证据。计划中的“Review 规则归属”预留了对初次实现的质疑,不把一次绿色测试当作设计终点。
风险清单提前暴露实现容易忽略的分支

小功能仍有风险:重复取消是否写两次;创建接口是否仍生成 CREATED;404 与 409 是否被通用 500 吞掉;状态改变后是否保存;所有 Repository 替身是否实现新接口;内存 Map 能否代表生产并发语义。
最后一项不会在本任务中解决。ConcurrentHashMap 让单次读写线程安全,却没有提供“读取 CREATED 后只允许一个调用转换”的原子比较与保存。教程明确不引入数据库,因此最终只能声称单进程示例行为通过,不能声称生产并发安全。识别并记录边界,比超范围实现一套假事务更负责。
从证据到计划的可追溯关系

每个计划判断都应有来源:从创建调用链得出沿用分层;从 Port 得出需要查询;从 Order 不可变 record 得出返回新对象;从异常处理器得出集中映射;从契约得出状态矩阵;从基线测试得出回归范围。
仍有设计选择时要明确取舍。例如状态规则可以先放 Service,也可以放 Domain。计划记录偏好“领域对象拥有状态不变量”,但允许初次实现后用测试和 Review 校正。可追溯不要求预先知道所有答案,而是区分事实、推断与选择。
什么时候证据足够,可以开始编辑

满足五个条件即可开始:基线在当前环境通过;主要调用链和职责明确;范围、非目标与错误语义明确;每个验收行为能写成测试;改动文件、风险和验证顺序已经可解释。无需等到理解整个仓库,也不应为了“多了解”无限探索。
探索的停止条件是最小充分。若新发现会改变计划,就继续读取;若只是补充无关背景,就保持当前工作集。对大型仓库,可以先让 Agent 输出“已确认事实、未确认假设、下一步需要读取的文件”,人工检查后再授权编辑。
失败模式
失败一:从 Controller 直接开始写
入口最容易找到,却不一定拥有业务规则。先追踪已有用例和状态所有者,再决定落点。
失败二:把目录名当成架构事实
domain 目录可能只是 DTO,service 也可能承担所有规则。读取调用者和测试,确认实际职责。
失败三:没有干净基线
后续失败无法归因,也可能覆盖用户已有改动。记录 Git 状态、提交和测试结果后再工作。
失败四:任务只描述成功路径
不存在、已支付和重复请求会由 Agent 自行猜测。把状态矩阵和错误码写进验收。
失败五:计划按搜索结果罗列文件
文件清单没有职责和顺序,就不能审查方向。每个文件必须对应一个行为或架构责任。
失败六:探索无限扩大
理解 Brownfield 不等于读取所有模块。以任务实体、调用链、消费者和测试为边界,达到最小充分后开始实验。
生产边界
只读探索也可能接触 Secret、客户数据和生产日志。文件和工具权限应按任务最小化;外部 Issue 和文档视为不可信数据。不要为了建立“完整上下文”读取 .env 或下载生产数据。
本篇只建立本地证据,没有创建远程仓库、PR 或部署。内存 Repository 无持久化和事务;真实生产设计还需认证、授权、并发控制、审计、监控与回滚。后续文章的验证结论保持在本地服务范围。
实践清单
- [ ] 确认工作目录、Git 状态和构建入口。
- [ ] 在改动前运行并记录基线测试。
- [ ] 从任务实体追踪入口、用例、领域、存储和测试。
- [ ] 区分现有事实、推断、未确认假设和设计选择。
- [ ] 写明目标、范围、非目标、错误语义和验证命令。
- [ ] 用状态模型覆盖成功、幂等、冲突和不存在。
- [ ] 将计划文件对应到明确架构职责。
- [ ] 在实现前列出兼容、持久化、替身和并发风险。
- [ ] 用 Git 节点保存基线和重要实验阶段。
- [ ] 达到最小充分理解后停止探索,进入可验证修改。
读完之后,你应该能完成什么
- 能建立可复现的 Brownfield 基线。
- 能让 Agent 在只读阶段暴露方向错误。
- 能画出请求到领域与存储的调用链。
- 能产出与真实仓库一致的最小实施计划。
如果只能复述概念,却不能完成上述动作,说明还没有把内容转化成工程能力;可以回到对应机制图、失败模式和实践清单重新核对。
小结
Brownfield 中,Agent 的第一项工程工作不是写代码,而是建立可信的现状模型。仓库地图给出职责,调用链给出依赖,基线给出可复现事实,任务契约冻结行为与边界,状态模型和测试计划定义完成。只有这些证据连接起来,编辑才从“根据常见模式猜测”变成“对现有系统进行受控实验”。
实战资料
- 演示仓库:
demo/order-service - 基线:
e081137/baseline - 任务契约:
demo/order-service/docs/task-contract.md - 实施计划:
demo/order-service/docs/implementation-plan.md - 验证命令:
mvn test
