把 AI 编码助手变成“确定性 Spring 升级专家”:一套可复制的升级流程
- 原文:<https://thenewstack.io/deterministic-ai-spring-upgrades/>
- 原文标题:Transform your AI coding agent into a deterministic Java Spring expert
- 来源:The New Stack
- 发布时间:2026-06-11
- 作者:原文未提供作者姓名
- 本文性质:基于原文事实整理,并加入一个离线可运行的实践扩展;实践扩展不是原文直接提供的代码。
适用场景
你维护多个 Spring Boot / Java 服务,想用 AI 编码助手协助框架升级,但又不希望升级结果靠模型随机试错。本文给出一套更稳的流程:
- 先识别哪些升级问题可以确定性转换。
- 把确定性问题交给 OpenRewrite / codemod / CLI recipe。
- 把非确定性问题交给受约束的 Agent Skill 或人工评审。
- 用 CI/CD 固化编译、测试、弃用告警和依赖安全检查。
核心原则很简单:AI Agent 负责调度和补边界,确定性工具负责批量改代码。
原文事实:为什么不能只靠 AI Agent 升级 Spring
原文用 Spring Petclinic 从 Spring Boot 3.5.x 升级到 Spring Boot 4 的实验说明问题:
- AI 助手规划阶段消耗了 478,380 tokens。
- 代码修改阶段消耗了 908,900 tokens。
- 总计约 1,387,280 tokens。
- 最终仍然失败:助手做了未请求的 Starter / import 修改,引入编译错误和弃用警告。
原文给出的判断是:Claude、Cursor、Copilot 这类编码助手默认并不真正理解代码库里每个表达式的类型和正确性。它们可以运行命令、生成补丁、解释错误,但不等同于 IDE 或编译器级别的语义分析。
OpenRewrite 的价值在这里:它作为 Maven / Gradle 插件运行,可以解析 Java 代码类型信息,用 recipe 做确定性重构。原文也提到 Tanzu Platform / Tanzu Spring Essentials 基于 OpenRewrite 提供升级计划和应用能力。
本文提炼:升级流程应该分成三条通道
通道 1:确定性 recipe
适合:
- 明确的 API 替换。
- 配置 key 改名。
- 依赖版本范围调整。
- 已知框架迁移规则,例如某些测试框架迁移。
处理方式:
- 写 OpenRewrite recipe、codemod 或内部迁移脚本。
- 在 CI 中批量执行。
- 用编译、测试、依赖审计验证结果。
通道 2:受约束的 Agent Skill
适合:
- 有固定步骤,但不同仓库细节略有不同的问题。
- 需要 Agent 读取上下文、生成局部补丁的问题。
- 编译能发现一部分错误,但仍需要语义判断的问题。
处理方式:
- 写集中维护的 Agent Skill。
- 明确允许工具、禁止操作、验证命令和停止条件。
- 让 Agent 只处理 recipe 无法覆盖的小范围问题。
通道 3:人工设计评审
适合:
- 内部框架不再兼容。
- 安全认证、事务边界、公共 API 语义变化。
- 编译通过但运行时行为可能变化。
处理方式:
- 先做设计决策,不让 Agent 自行猜。
- 决策稳定后,再把可重复部分沉淀为 recipe 或 Skill。
实践扩展:用一个离线脚本模拟升级分流
这个示例不是原文代码,而是把原文方法转成一个可运行的最小练习。目标是让团队先学会把升级问题分流,而不是直接让 Agent 改业务代码。
目录结构
deterministic-ai-spring-upgrades-demo/
├── deterministic_upgrade_router.py
└── upgrade_findings.json文件:upgrade_findings.json
[
{
"id": "SPRING-001",
"description": "Replace deprecated configuration property names after Spring Boot upgrade",
"signal": "compiler warning and migration guide mapping",
"deterministic": true,
"risk": "low"
},
{
"id": "SPRING-002",
"description": "Migrate Spock-based integration tests to JUnit where Spring Boot 4 support is removed",
"signal": "known incompatible testing framework",
"deterministic": true,
"risk": "medium"
},
{
"id": "SPRING-003",
"description": "Choose replacement for an unsupported internal authentication starter",
"signal": "internal framework has no Spring Boot 4 compatible release",
"deterministic": false,
"risk": "high"
},
{
"id": "SPRING-004",
"description": "Adjust one service method after changed exception semantics",
"signal": "compile passes but integration behavior changed",
"deterministic": false,
"risk": "medium"
}
]文件:deterministic_upgrade_router.py
#!/usr/bin/env python3
"""Classify Spring upgrade findings into deterministic recipes or agent skills."""
from __future__ import annotations
import json
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Literal
ResolutionType = Literal["deterministic_recipe", "agent_skill", "manual_review"]
@dataclass(frozen=True)
class Finding:
"""One migration finding discovered during a framework upgrade."""
id: str
description: str
signal: str
deterministic: bool
risk: str
@dataclass(frozen=True)
class RoutedFinding:
"""Finding plus the selected execution lane."""
id: str
lane: ResolutionType
action: str
risk: str
def load_findings(path: Path) -> list[Finding]:
"""Load findings from a JSON file."""
data = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(data, list):
raise ValueError("findings JSON must be a list")
findings: list[Finding] = []
for item in data:
findings.append(
Finding(
id=str(item["id"]),
description=str(item["description"]),
signal=str(item["signal"]),
deterministic=bool(item["deterministic"]),
risk=str(item["risk"]),
)
)
return findings
def route_finding(finding: Finding) -> RoutedFinding:
"""Choose a lane for one upgrade finding."""
if finding.risk == "high" and not finding.deterministic:
return RoutedFinding(
id=finding.id,
lane="manual_review",
action="require human design decision before agent edits code",
risk=finding.risk,
)
if finding.deterministic:
return RoutedFinding(
id=finding.id,
lane="deterministic_recipe",
action="implement OpenRewrite/codemod recipe and run in CI",
risk=finding.risk,
)
return RoutedFinding(
id=finding.id,
lane="agent_skill",
action="write a scoped agent skill with compile/test checks",
risk=finding.risk,
)
def build_plan(findings: list[Finding]) -> dict[str, object]:
"""Build an auditable upgrade plan."""
routed = [route_finding(finding) for finding in findings]
return {
"summary": {
"total": len(routed),
"deterministic_recipe": sum(1 for item in routed if item.lane == "deterministic_recipe"),
"agent_skill": sum(1 for item in routed if item.lane == "agent_skill"),
"manual_review": sum(1 for item in routed if item.lane == "manual_review"),
},
"items": [item.__dict__ for item in routed],
"ci_gate": ["compile", "unit_tests", "deprecation_warnings", "dependency_audit"],
}
def main(argv: list[str]) -> int:
"""CLI entrypoint."""
if len(argv) != 2:
print("usage: deterministic_upgrade_router.py findings.json", file=sys.stderr)
return 2
plan = build_plan(load_findings(Path(argv[1])))
print(json.dumps(plan, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv))运行验证
python3 deterministic_upgrade_router.py upgrade_findings.json期望输出节选:
{
"summary": {
"total": 4,
"deterministic_recipe": 2,
"agent_skill": 1,
"manual_review": 1
},
"ci_gate": [
"compile",
"unit_tests",
"deprecation_warnings",
"dependency_audit"
]
}这个练习的意义不是替代 OpenRewrite,而是训练升级前的决策习惯:先判断问题是否确定性,再决定交给 recipe、Agent Skill 还是人工评审。
接入真实 Spring 升级时的操作顺序
第 1 步:冻结升级目标
先明确:
- 当前 Spring Boot 版本。
- 目标 Spring Boot 版本。
- 是否跨大版本。
- 是否涉及内部共享库。
- 是否是安全漏洞驱动升级。
- CI 当前是否能稳定通过。
验收标准:升级目标能写成一句话,例如:
将 service-a 从 Spring Boot 2.7.x 升级到受支持的 3.x 或 4.x 路径,先处理编译和依赖兼容,再进入运行时回归。第 2 步:跑确定性发现工具
原文提到的 Tanzu Platform 路径是:
cf repo unpack-skills
cf repo upgrade-plan
cf repo apply-upgrade-plan边界说明:这些命令来自原文,依赖 Tanzu 工具链;本文未在本地验证 Tanzu CLI。没有 Tanzu 时,可以用 OpenRewrite、Maven/Gradle 插件、内部脚本或静态检查工具替代同一层职责。
验收标准:输出一份升级计划,至少包含:
- 自动可改项。
- 需人工确认项。
- 需先升级的内部库或第三方库。
- 编译和测试预期影响。
第 3 步:把问题分流
按下面规则分流:
- 如果迁移规则明确、可重复、可静态验证:进入 deterministic_recipe。
- 如果需要仓库上下文但风险可控:进入 agent_skill。
- 如果涉及架构、安全、认证、事务、公共 API 语义:进入 manual_review。
验收标准:没有“让 Agent 自己看着办”的高风险项。
第 4 步:让 Agent 只处理受约束任务
一个合格的升级 Agent Skill 至少写清楚:
- 允许读取哪些文件。
- 允许执行哪些命令。
- 禁止做哪些改动,例如升级无关依赖、重命名 Starter、修改认证逻辑。
- 每轮修改后的验证命令。
- 失败几轮后停止并交回人工。
示例 Skill 边界:
目标:只修复 Spring Boot 升级后的编译错误和弃用警告。
禁止:修改业务语义、替换认证框架、升级未列入计划的依赖。
验证:每次修改后运行 ./mvnw test;若同一错误连续失败 2 次,停止并报告。第 5 步:把验证放进 CI/CD
最小 CI gate:
compile
unit_tests
integration_tests(如存在)
dependency_audit
deprecation_warnings
upgrade_plan_artifact_nonempty如果是安全漏洞驱动升级,还应增加:
vulnerability_scan_no_known_blocker
rollback_plan_exists
release_owner_approved验收标准:升级 PR 不是“Agent 说完成”,而是 CI 证明可合并。
常见失败模式
失败 1:Agent 自行扩 scope
表现:原本只升级 Spring Boot,Agent 顺手改 Starter、import、依赖树或测试框架。
处理:把“禁止修改升级计划外依赖”写入 Skill,并用依赖 diff 检查兜底。
失败 2:把非确定性问题伪装成 recipe
表现:内部认证框架不兼容,却写了一个机械替换脚本。
处理:高风险且不可确定的问题必须先人工设计评审。
失败 3:CI 只检查编译,不检查运行时行为
表现:编译通过,但框架行为变化导致运行时 bug。
处理:关键路径保留集成测试或契约测试;框架升级 PR 不能只看编译。
团队落地清单
- 先选 1 个低风险服务试点,不要从核心交易链路开始。
- 收集升级发现项,按 deterministic_recipe / agent_skill / manual_review 三类标注。
- 对 deterministic_recipe 类问题优先沉淀为 OpenRewrite recipe 或内部脚本。
- 对 agent_skill 类问题写清楚禁止项、验证命令和停止条件。
- 对 manual_review 类问题先做设计决策,再考虑自动化。
- CI/CD 必须保存升级计划、变更 diff、测试结果和依赖审计结果。
- 跨仓库升级时,先处理共享库和底层依赖,再处理业务服务。
本教程验证记录
- static_publish_ok:已通过发布脚本生成 Markdown/HTML,并完成远端 Markdown/HTML 非空校验。
- mock_code_run_ok:已执行离线分流脚本,输出 4 个发现项,其中 2 个进入 deterministic_recipe、1 个进入 agent_skill、1 个进入 manual_review。
- real_backend_contract_ok:未验证;Tanzu CLI / OpenRewrite 真实接入依赖具体 Java 项目和工具链。
- real_backend_smoke_ok:未执行;本文没有调用真实 Tanzu / OpenRewrite 环境。
结论
不要把 AI Agent 当成框架升级的主执行器。更可靠的做法是:
- 用确定性工具处理能规则化的迁移。
- 用受约束 Agent Skill 处理上下文相关的小范围修复。
- 用人工评审处理高风险语义决策。
- 用 CI/CD 把升级变成可重复、可审计、可回滚的工程流程。