从 Vibe Coding 到 Harness Engineering: JDK 升级中可控演进的 AI 工程实践
原文
过去一年,AI Coding 从代码补全工具逐渐发展为能理解项目、规划任务并执行开发的工程助手。但在真实软件工程中,它更多仍用于新功能开发。实践中我们发现,真正消耗工程时间的并不是"写新代码",而是框架迁移、版本升级、安全修复等维护性工程 。这类工作的核心难点 ,也不在生成代码,而在于如何把系统安全、稳定地改正确。
本文结合一次 JDK21 升级实践,分享我们在 AI 辅助维护性工程中的工程化思路,如何通过 Harness Engineering,让 AI 在复杂约束下稳定完成系统升级。
01 AI 擅长写代码,工程却不止是写代码
如果只看新功能开发,AI Coding 已经足够优秀。
无论是生成一个模块、补全一个函数,还是快速完成原型验证,如今的大模型都已经能够胜任。Vibe Coding 之所以曾流行,也正是因为这种"边想边写、快速迭代"的开发方式,在探索性开发中拥有极高效率。
但真正的软件工程,很少只有新代码。
工程团队的大部分时间,其实并不花在开发新功能,而是花在框架迁移、安全补丁、版本升级、依赖治理、代码重构等维护性工程。而这类工作的特点,恰恰不是"写出来",而是"改得对"。
JDK 升级就是其中最典型的一类。从 旧版本升级到 JDK 21,看起来只是改一个版本号,实际上往往牵一发而动全身:Spring 框架兼容性、第三方依赖、编译插件、JVM 参数、启动脚本、测试用例,任何一个环节出问题,都可能导致服务无法启动。
一个典型的场景是:团队每季度都会讨论一次 JDK 升级,它几乎每个季度都会被提上技术规划,但真正启动的次数却很少。不是不知道该做,而是投入与风险之间的账始终很难算清。两三个开发者投入数周时间,承担升级风险,换来的只是版本号更新。
维护性工程真正困难的地方,也正在这里。**写代码关注的是"能不能实现",维护工程关注的是"能不能安全实现"。**一个修改不仅要满足需求,还要遵守项目规范,通过编译、能够启动、通过测试,并且不破坏已有系统。这些约束,往往都藏在项目历史和开发经验里,而不是代码本身。
开始先尝试直接用 Vibe Coding 的方式完成这类工作。AI 确实能够完成部分修改,但在面对复杂依赖关系和长链路验证时,很容易逐渐偏离目标,出现如下问题:
| 问题类型 | 具体案例 |
|---|---|
| 编译级成功 ≠ 运行时成功 | AI 在编译通过后,就通知升级成功任务完成,启动时暴露大量运行时问题,导致升级工作反复。 |
| 表面修复掩盖深层问题 | 在升级 JDK 时,出现定时任务组件初始化失败导致项目无法启动问题,AI 发现后,居然直接注释掉了该组件的注册注解,移除该任务组件在容器的注册方式来确保启动成功,实际这导致定时任务功能彻底失效。 |
| 重复错误 | JDK 升级涉及大量的命名空间迁移和依赖变更,这些都是通用知识,本应扫描项目后一轮对话全部修改,实际 AI 在 Agent 循环中不断重复"升级依赖->发现新的依赖问题->再升级依赖"的过程,极大降低了效率。 |
| 范围失控 | 不断 case by case 的解决问题,ai 被各种问题处理逐渐占据上下文,导致修改愈发偏离目标约束。 Plain Text 1. 首先遇到 Lombok 编译报错 NoSuchFieldError: UNKNOWN------它尝试将 Lombok 降级到更早版本,而不是意识到需要升级到 1.18.32 以上并配置 .mvn/jvm.config 中的模块开放参数。 2. 接着遇到 javax.servlet.Filter 的 ClassNotFoundException,它只替换了部分 javax 包名为 jakarta,遗漏了 validation 和 persistence 命名空间,又误改了 javax.crypto 等 JDK 自带的包。 3. 随后 CMS GC 参数报 Unrecognized VM option 'UseConcMarkSweepGC',它直接删除了 GC 配置而非替换为 ZGC。 4. 最终,AI 在依赖冲突中开始"猜测式修复"------随意锁定版本号,编译通过后启动阶段却报 NoSuchMethodError,修改范围越来越大,不得不中止任务。 |
| 违背默认约定 | 项目原本限定日志组件使用 log4j2,然而 AI 在升级后因为直接替换的 springboot 依赖,变成了 Logback,底层日志组件都被错误改掉了,原本基于 Log4j2 的一系列日志配置全部失效。 |
JDK 升级是一个 系统工程 ,涉及:
- 依赖版本联动(传递依赖、内部 SDK)
- 配置多层级(父 POM、子模块、启动脚本、配置文件)
- 运行期行为变化(GC 策略、模块系统、日志框架)
AI 缺少一套能够约束工程执行过程的方法,最终导致修改范围失控、依赖关系混乱、配置相互冲突,只能终止升级。于是,我们开始思考:如何让 AI 在复杂工程中,也能像成熟工程团队一样稳定工作?
02 Harness Engineering
用工程约束 AI
答案不在模型能力上,在系统设计上。我们实践使用 Harness Engineering 的方式,它的解决思路是:不过分依赖模型能力,而是通过系统设计约束 AI 的行为 。如果把开发过程比作装修,Vibe Coding 更像告诉施工人员"你看着办";Harness Engineering 则先给出施工图,把边界、规范和流程提前定义清楚,让执行过程尽可能减少猜测。
这套体系主要包括三部分:首先构建执行约束,让 AI 在明确边界内工作;随后沉淀工程经验,将一次实践转化为长期可复用能力;最后建立反馈闭环,让系统在实践中持续进化。三者串起来,就是一条"约束执行 → 经验复用 → 反馈进化"的链路。
构建执行约束:让 AI 在正确的边界内工作
项目上下文构建
回顾 Vibe Coding 的失败,我们发现,第一个急需解决的问题,并不是 AI 不知道如何升级 JDK,而是 AI 在不了解项目约束的情况下,就已经开始修改代码。
它不知道哪些文件由 Thrift 自动生成不能修改,不知道本地编译需加 -Plocal 参数,不知道 JVM 启动参数需要从 start.sh 脚本中提取。这些原本散落在资深开发者经验中的隐性知识,对 AI 来说就是看不见的暗礁。我们的第一步,就是把这些知识变成 AI 可消费的结构化规范------这就是 Harness Engineering 的起点:上下文工程。
核心是动态组装 Agent 每步所需的精确信息,避免过多或过少。我们构建了知识规范层,它们在其他场景也是通用的:
| 组成 | 类型 | 说明 |
|---|---|---|
| AGENTS.md | memory | 对应项目级的主体记忆文件,每次会话全部加载,包括: • 项目介绍:描述,技术栈,项目结构,项目级的编码规范,核心约束,常见陷阱信息 • 项目命令:包括如何编译构建,如何启动,如何调试。 |
| code_standard.md | rule | 开发规范约束,每次会话全部加载,建议主要沉淀项目团队独有的约定知识,例如: • 不要修改 thrift 模块下的 .java 文件,它们由 .thrift 文件自动生成。 • 项目使用 Slf4j+log4j2,引入新依赖时必须排除 log4j:log4j 等依赖 避免日志组件冲突。 |
| security_base.md | rule | 安全规范约束,每次会话全部加载,例如: • 对所有来自前端接口的用户输入进行验证。 • 不要将异常直接暴露给用户。 |
| java-business-developer | skill | 团队业务开发专家,提供必要知识,以渐进式披露的索引方式组织,包括 • 业务项目独有名词解释。 • 上下游链路相关项目介绍和信息索引。 |
工程工具编排
有了上下文,AI 知道了项目边界。但紧接着第二个问题就来了:JDK 升级涉及的工程动作非常多样------扫描依赖树、解析冲突链路、执行集成测试、读取测试结果------如果每一项都让 AI 临场发挥,结果仍然不可控。于是我们构建了工具编排层 :把工程动作封装成输入输出明确、失败路径清晰的可调用工具,让 AI 只需要视情况决定"调用哪个工具",保证对固定问题表现稳定。
| 组成 | 类型 | 说明 |
|---|---|---|
| windows_powershell.md | rule | 用于解决 AI 在 windows 的 powershell 场景下,习惯性使用 unix 风格的 shell,导致频繁出现语法错误问题: • 环境变量要加 env: 前缀:$env:VARNAME 格式。 • 命令分隔符用 ;,不支持 &&、` |
| java-code-reviewer | skill | 在开发完成/提交 PR 触发,review 代码,二次确认 ai 生成的代码符合代码风格和安全规范等各类约束。 |
| pr-creator | skill | 创建标题和描述都完全符合项目规范的 PR。 |
| gitlab | skill | 提供 GitLab 全量资源,包括各种 Issue、CI/CD Pipeline,Merge Request 的操作。 |
| JUnit5-integration-testing | skill | 集成测试技能,包括: 1. 测试的开发标准,确保测试有效性。 2. 测试的执行标准:必须指定超时避免无限挂起,串行执行避免进程冲突。 3. 测试的验收标准:包括从哪里获取测试结果,如何分析测试报告。 |
| JUnit4-integration-testing | skill | 集成测试技能,适用 JUnit4 的项目 |
| dependency-analysis-process | skill | 提供如何分析依赖树,解决依赖冲突的标准化流程和能力。 |
| jdk-upgrade | skill | 核心 skill,用于指导 jdk 升级过程,下文有详细介绍。 |
| maven | mcp | 查询各种依赖有哪些版本和最新版本。 |
| context7 | mcp | 提供面向 AI 模型的文档检索与上下文注入能力,可实时获取官方文档的最新内容和指定版本的代码示例。 |

沉淀工程经验:把一次升级变成长期能力
每次升级,AI 都像从零开始,升级经验散落在对话、文档和开发者脑中,没有被结构化为 AI 能用的格式。因此,我们把执行过程中积累的工程经验沉淀成可复用的 Skill:jdk-upgrade 。它是输入输出明确、可以长期复用的能力模块 。 它回答以下问题:什么时候使用、任务的专家知识、不能做什么,按什么步骤做,什么时候结束。
明确能力边界
首先,明确定义了升级过程的核心约束:禁止修改业务逻辑代码、禁止优化或重构业务方法、禁止添加新功能、禁止删除原有功能、仅进行必要的兼容性修改。这些约束明确了 Skill 的能力边界,避免了 AI 在升级过程中"顺手优化"或"重构代码",导致修改范围失控。SKILL 示例:
markdown
## 【核心约束规范】
> **所有升级操作必须严格遵守以下约束,违反任一条均属超出升级范围。**
1. **禁止修改业务逻辑代码**。
2. **禁止优化或重构业务方法**。
3. **禁止添加新的业务功能**。
4. **禁止删除或废弃原有功能**。
5. **禁止修改接口的输入输出格式**。
6. **仅进行必要的兼容性修改**,确保代码在 JDK 21 + Spring Boot 3.5.3 环境下正常编译和运行。
渐进式组织知识
其次,我们不会把所有的升级知识塞进一个文件里。SKILL.md 作为入口和导航,详细的参考资料渐进式披露方式查阅,SKILL 示例:
jdk-upgrade/
├── SKILL.md # 主技能文档(概览 + 约束 + 执行步骤)
└── references/
├── maven.md # Maven 插件升级详细配置
├── dependency.md # Pom 依赖处理详细列表
├── jvm.md # JVM 参数调整详细方案
└── code.md # 代码兼容修改详细映射
标准化执行流程
最后,JDK 升级操作脆弱且易错,一致性至关重要 。因此 Skill 不能是概括性描述,而必须是指令式的具体动作 。我们将升级过程拆分为 5 个清晰步骤,关键步骤末尾包含反馈闭环。SKILL 示例:
markdown
### Step 1 --- Maven 插件升级
**目标**:使构建工具链支持 JDK 21 编译和 JUnit 5 测试。
操作要点示例:
- `maven-compiler-plugin` 升级至 `3.12.1+`,设置 `source/target=21`
- `spring-boot-maven-plugin` 升级至 `3.5.3`
- `maven-surefire-plugin` 升级至 `3.2.5+`
### Step 2 --- POM 依赖处理
**目标**:清除 javax 时代依赖,迁移至 Jakarta EE 体系。
操作要点示例:
- **移除**:`javax.annotation-api`、`javax.validation:validation-api`、`junit:junit`
- **替换**:`javax.*` → `jakarta.*`
- **升级**:`spring-boot-starter` → `3.5.3`、`lombok` → `1.18.32+`
### Step 3 --- JVM 参数调整
**目标**:移除已废弃参数,适配模块系统和新 GC 体系。
操作要点示例:
- **移除**:所有 CMS 相关参数、旧版 GC 日志参数、PermGen 参数
- **新增**:`--add-opens` 模块权限参数组、G1GC 或 ZGC 参数
### Step 4 --- 代码兼容修改
**目标**:消除所有 `javax.*` 引用,迁移测试框架至 JUnit 5。
操作要点示例:
- **包名替换**:`javax.annotation` → `jakarta.annotation` 等
- **JUnit 4 → JUnit 5 迁移**:`@Before/@After` → `@BeforeEach/@AfterEach`
-(反馈闭环) 若编译失败,排查Step1到Step4可能存在的错误。
### Step 5 --- 验证
- mvn clean compile -pl <module>
- mvn test -pl <module>
(反馈闭环) 若测试结论为不通过,基于测试报告的分析结果进行排查处理。
Skill 解决了"应该怎样做"的问题,但真正决定执行质量的,还在于每一步是否能够被验证。
建立反馈闭环:让系统持续提升

验证反馈循环是 Harness Engineering 中 ROI 最高的机制。它的核心是:每步操作后进行检查,防止错误传播。实践表明,验证循环可显著提升任务完成率,无需改模型或提示词。
在 JDK 升级场景中,我们采用了测试前置驱动的验证策略。
升级前:撰写集成测试
我们要求 AI 针对项目的主要功能组件撰写集成测试,具体覆盖:
-
核心业务逻辑的链路验证。
-
关键依赖的兼容性验证。
-
关键组件的功能可用性验证。
测试撰写遵循严格的规范:
-
每个测试方法必须配置 @Timeout 注解,防止测试挂起。
-
每个测试必须真实完整加载 Spring 上下文,避免 mock 导致的假性通过。
-
每个测试必须能结构化的输出通过/不通过,和不通过时的错误原因,便于 AI 分析排查。
升级后:重新执行测试
升级完毕后,再次执行相同的测试集。如果测试通过,说明升级没有破坏已有功能;如果测试失败,AI 需要根据失败信息定位问题并修复。这种"测试前置 + 升级后验证"的循环,确保了升级的可验证性。AI 不再能够"谎报结果"------编译通过不代表升级成功,必须通过测试验证。
验证循环的另一个关键作用是防止错误传播。在升级过程中,AI 可能会做出错误的修改(如注释掉某个组件的注册)。如果没有验证循环,这些错误会一直累积,直到部署时才暴露。通过测试前置,我们能够在升级过程中及时发现并纠正这些错误。更重要的是,这套验证机制并不限于 JDK 升级,而是适用于绝大多数软件工程任务。一旦建立,便能够持续复用。
经验回流机制
验证循环保证了一次升级能够顺利完成,但 Harness Engineering 并不止于完成当前任务。更重要的是,如何把这次升级过程中积累的经验,转化为下一次升级可以直接复用的能力。
jdk-upgrade 解决了"如何执行升级"的问题,但还有一个问题:如何让每次升级的经验都能反哺下一次升级?因此设计了 jdk-upgrade-lessons Skill,这是一个经验沉淀 Hook。当 JDK 升级任务完成后,它会主动触发,总结升级过程中遇到的问题,生成结构化的经验文档,并基于这些经验生成新版本的升级 Skill。
- 触发时机:当用户完成 JDK 升级任务后,Skill 会主动询问:JDK 升级已完成,是否需要回顾本次升级过程中遇到的问题,沉淀经验供后续参考?
- 执行流程:Skill 自动按照 5 个步骤引导经验沉淀:
-
收集升级上下文:总结升级目标版本、项目类型、升级范围、升级耗时
-
引导经验回顾:按 5 个维度逐一整理------阻断性问题、隐蔽陷阱、配置遗漏、依赖冲突、代码迁移
-
生成经验文档:将收集到的经验整理为结构化文档,保存到项目目录
-
生成新版本技能:基于本次升级经验,生成优化后的新版本技能(如
jdk-upgrade-21) -
确认与归档:向用户展示生成的经验文档,确认后归档
SKILL 示例:
markdown
# JDK升级经验总结
## 升级概况
- **项目**:[项目名]
- **升级路径**:JDK X → JDK Y
- **升级日期**:YYYY-MM-DD
- **耗时**:X小时
## 阻断性问题
[记录升级过程中遇到的无法继续的问题及解决方案]
## 隐蔽陷阱
[记录看似通过但实际隐藏问题的情况]
## 配置遗漏
[记录配置项遗漏导致返工的情况]
## 依赖冲突
[记录传递依赖冲突及排查方法]
## 代码迁移经验
[记录代码迁移过程中的意外情况]
## 升级前检查清单
[根据本次经验,补充到升级前摸底排查清单]
效果验证:从工程实践到业务收益
上述方法并非理论设计,而是在真实项目中持续验证。下面以一次典型的 JDK 升级为例。目标:从一个旧的 JDK 版本升级到 JDK 21+Spring Boot 3.5.3。
单个项目整体涉及 30+ 个文件修改、50+ 个依赖升级/新增/移除和大量的配置、启动参数修改。AI 承担了绝大部分工作:从项目分析、任务规划,到代码修改、编译调试和自动修复大部分启动错误。人工只需最终代码 Review 核验变更质量。
| 方案 | 单项目升级总耗时 | 一轮对话完成升级比例 | 升级并行度 |
|---|---|---|---|
| 人工升级 | 1 个工作日以上 | - | 1 人 1 项目 |
| Vibe Coding | ~ 1 个工作日 | 0% | 1 人 2-3 项目 |
| Harness Engineering | 0.5 ~ 1.5 小时 | 70% | 1 人看管 5+ 项目自动升级 |
一轮对话完成升级比例"指在无人工介入的情况下,AI 单次对话即完成整个升级任务的比例。70% 的项目可一轮完成,剩余 30% 超过循环次数需要在关键节点人工介入。
升级完成后,多个服务的 CPU 使用率平均降幅约 22.5%;同等负载下堆内存平均下降约 23%。GC 改善尤为显著------每分钟 GC 停顿时间降至 1ms 以下,业务几乎无感。接口延迟方面,P99 平均降幅 18.1%,尾部延迟最大降幅达到 47%(548ms→288ms)。在 60% CPU 负载下,主要 Web 服务峰值 QPS 提升超过 30%(以上数据基于内部多个项目升级前后的生产环境监控对比)。
对于 JDK 升级这类长期积压的技术债,Harness Engineering 改变的不只是执行效率,更是成本结构。过去需要投入大量人力、反复评估 ROI,如今 AI 已经能够承担绝大部分工程工作,团队只需在关键节点进行决策和审查。这意味着,许多过去"知道应该做、却一直没有时间做"的工程优化,可以进入常态化执行。AI 的价值也从一次性的编码助手,真正演进为可持续的工程生产力。
03 从写代码,到定义约束
这次实践最大的收获,不是升级快了多少,而是我们对 AI 在工程中的位置有了新的认识。
-
规范正在成为比代码更重要的资产。 项目约束、升级经验、排查方案、执行流程------这些原本存在于开发者经验中的知识,一旦结构化沉淀下来,就能够持续被 AI 复用。代码反而变成了产物。
-
文档的首要读者正在从人转变为 AI 。 这些工程知识不是为了约束人,而是为了约束 AI 。当我们定义项目规范时,目标不是写一份"人类可读的文档",而是构建一份"AI 可执行的约束"。
-
开发者的角色正在从代码编写者转变为约束设计者 。 当 AI 承担执行层工作后,人的核心价值变成了定义"什么能做、什么不能做、做到什么程度"。
这些变化共同指向一点:工程的重心正在从"写代码"转向"定义约束"。JDK 升级只是第一块试验田,框架迁移、安全修复、依赖治理,都能用同一套方法。真正提升工程效率的,不是让 AI 更聪明,而是让工程体系能够持续约束、复用和进化 AI 的能力。