把 AI 编码助手变成“确定性 Spring 升级专家”:一套可复制的升级流程

适用场景

你维护多个 Spring Boot / Java 服务,想用 AI 编码助手协助框架升级,但又不希望升级结果靠模型随机试错。本文给出一套更稳的流程:

  1. 先识别哪些升级问题可以确定性转换。
  2. 把确定性问题交给 OpenRewrite / codemod / CLI recipe。
  3. 把非确定性问题交给受约束的 Agent Skill 或人工评审。
  4. 用 CI/CD 固化编译、测试、弃用告警和依赖安全检查。

核心原则很简单:AI Agent 负责调度和补边界,确定性工具负责批量改代码。

原文事实:为什么不能只靠 AI Agent 升级 Spring

原文用 Spring Petclinic 从 Spring Boot 3.5.x 升级到 Spring Boot 4 的实验说明问题:

原文给出的判断是:Claude、Cursor、Copilot 这类编码助手默认并不真正理解代码库里每个表达式的类型和正确性。它们可以运行命令、生成补丁、解释错误,但不等同于 IDE 或编译器级别的语义分析。

OpenRewrite 的价值在这里:它作为 Maven / Gradle 插件运行,可以解析 Java 代码类型信息,用 recipe 做确定性重构。原文也提到 Tanzu Platform / Tanzu Spring Essentials 基于 OpenRewrite 提供升级计划和应用能力。

本文提炼:升级流程应该分成三条通道

通道 1:确定性 recipe

适合:

处理方式:

通道 2:受约束的 Agent Skill

适合:

处理方式:

通道 3:人工设计评审

适合:

处理方式:

实践扩展:用一个离线脚本模拟升级分流

这个示例不是原文代码,而是把原文方法转成一个可运行的最小练习。目标是让团队先学会把升级问题分流,而不是直接让 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 步:冻结升级目标

先明确:

验收标准:升级目标能写成一句话,例如:

将 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 步:把问题分流

按下面规则分流:

验收标准:没有“让 Agent 自己看着办”的高风险项。

第 4 步:让 Agent 只处理受约束任务

一个合格的升级 Agent Skill 至少写清楚:

示例 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 不能只看编译。

团队落地清单

本教程验证记录

结论

不要把 AI Agent 当成框架升级的主执行器。更可靠的做法是:

  1. 用确定性工具处理能规则化的迁移。
  2. 用受约束 Agent Skill 处理上下文相关的小范围修复。
  3. 用人工评审处理高风险语义决策。
  4. 用 CI/CD 把升级变成可重复、可审计、可回滚的工程流程。