# Spring Boot 3.x 升级 + Dify 替代 Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 将 cfc-backend 从 Spring Boot 2.7.18 + Java 8 升级到 Spring Boot 3.4.x + Java 21 LTS,用 Spring AI + AxonHub 替换 Dify 全部功能。 **Architecture:** 分层替代 Dify:Spring AI 承担 AI 应用层(对话/RAG/工具),AxonHub 承担模型路由层(分发/降级/计费),专用工具承担文档预处理。5 阶段串行,每阶段有独立编译/测试验证 + git tag 回退点。 **Tech Stack:** Java 21, Spring Boot 3.4.x, Spring AI 1.x, AxonHub (axonhub.iwintrue.com), MyBatis-Plus 3.5.9, springdoc-openapi 2.7, exp4j 0.4.8, jjwt 0.12.6, mysql-connector-java 8.0.33, sqlite-vec, docling (PDF预处理) --- ## Global Constraints 1. **Java 21 LTS** — JDK 21 必须安装到所有开发/CI/生产环境,maven-compiler-plugin source/target=21 2. **Spring Boot BOM** — 以 spring-boot-starter-parent 3.4.x 为根 BOM,避免传递依赖冲突 3. **javax → jakarta** — 所有 javax.* import 必须改为 jakarta.*,包括但不限于 persistence/annotation/validation/servlet/sql 4. **Nashorn 已废弃** — WisdomMathService 中的 ScriptEngine eval 必须替换为 exp4j 5. **springdoc v2** — artifactId 从 `springdoc-openapi-ui` 改为 `springdoc-openapi-starter-webmvc-ui` 6. **jjwt 0.12** — `Jwts.parserBuilder()` 改为 `Jwts.parser()`, Keys API 变化 7. **统一 @PostMapping** — 遵循项目现有规范,禁止 @GetMapping/@PutMapping/@DeleteMapping 8. **统一 Result 响应** — 所有接口返回 `Result` (code/message/data) 9. **不再依赖 Dify** — 全部 8 个 Service + 3 个 Controller 必须改为 Spring AI ChatClient 10. **AxonHub 统一网关** — 所有模型调用经 `axonhub.iwintrue.com`,Spring AI 配置 `base-url` 指向 AxonHub 11. **多模型编排** — 支持工作流各节点使用不同 `ChatModel` Bean(DeepSeek/Qwen/本地模型等),通过 `@Qualifier` 注入 12. **PDF 预处理** — 复杂文档预处理使用 Python 专有工具(docling / unstructured.io),不在 Spring AI ETL 中处理 --- ## 文件变更清单 ### 创建 | 文件 | 用途 | |------|------| | `src/main/java/com/etotem/cfc/service/ai/LLMService.java` | Spring AI ChatClient 封装 | | `src/main/java/com/etotem/cfc/service/ai/SystemPromptLoader.java` | System Prompt 外部化加载 | | `src/main/java/com/etotem/cfc/service/ai/RAGService.java` | 本地知识库查询 | | `src/main/java/com/etotem/cfc/config/SpringAiConfig.java` | Spring AI ChatClient Bean 配置 | | `resources/prompts/family-advisor.yaml` | 家庭顾问 System Prompt | | `resources/prompts/growth-plan.yaml` | 成长规划 System Prompt | | `resources/prompts/diet-recommend.yaml` | 饮食推荐 System Prompt | | `resources/prompts/math-tutor.yaml` | 数学辅导 System Prompt | | `scripts/doc-preprocess/` | PDF 预处理 Python 脚本(docling) | ### 修改 | 文件 | 变更类型 | |------|---------| | `pom.xml` | 全部依赖版本更新 + 新增 | | `src/main/resources/application.yml` | Spring AI 配置、springdoc 配置调整 | | 所有 `@Entity` 类 (~80) | javax.persistence → jakarta.persistence | | 所有 DTO 类 (~200) | javax.validation → jakarta.validation | | 所有 Service 类 (~60) | javax.annotation → jakarta.annotation | | 所有 Controller 类 (~48) | javax.servlet → jakarta.servlet (如果使用) | | 所有 Config 类 | 同上 | | `JwtUtil.java` | jjwt 0.12 API 适配 | | `WisdomMathService.java` | Nashorn → exp4j | | `AIService.java` | 重构为 LLMService 封装 | | `DifySyncService.java` | 替换为本地知识库同步 | | `AiConversationSummaryService.java` | 替换为 Spring AI MemoryAdvisor | | `AiUserFactService.java` | 替换为 @Tool + ChatClient | | 其余 Dify 相关 4 个 Service | 逐一替换 | | `AiChatController.java` + 2 个相关 Controller | Spring AI 实现 | | 删除 `springdoc-openapi-ui` 依赖 | v1→v2 artifact 替换 | --- ## 任务分解 ### Phase 0 — 前置准备(3 天) #### Task 0.1: 环境准备 + BOM 对齐 **Files:** - Modify: `pom.xml` **Interfaces:** - Produces: `mvn clean compile` 通过(尚未改 javax,暂时只对齐依赖) - [ ] **Step 1:更新 parent 和 properties** 在 `pom.xml` 中修改: ```xml org.springframework.boot spring-boot-starter-parent 3.4.3 21 3.5.9 0.12.6 ``` - [ ] **Step 2:替换 springdoc v1 → v2** 找到并替换: ```xml org.springdoc springdoc-openapi-ui 1.8.0 org.springdoc springdoc-openapi-starter-webmvc-ui 2.7.0 ``` - [ ] **Step 3:移除 jjwt 旧版本,确认 BOM 覆盖** ```xml io.jsonwebtoken jjwt-api ${jjwt.version} io.jsonwebtoken jjwt-impl ${jjwt.version} runtime io.jsonwebtoken jjwt-jackson ${jjwt.version} runtime ``` - [ ] **Step 4:添加 exp4j 依赖** ```xml net.objecthunter exp4j 0.4.8 ``` - [ ] **Step 5:编译验证** 运行: ```bash mvn clean compile ``` Expected: 编译失败(因为 javax 包名不存在,这是预期的。失败信息用来确认 Phase 1 的替换范围) - [ ] **Step 6:录制 javax 报错清单** ```bash mvn clean compile 2>&1 | Select-String "javax\." | Sort-Object -Unique > docs/javax-errors.txt ``` 查看输出行数:`Get-Content docs/javax-errors.txt | Measure-Object` - [ ] **Step 7:打基线 tag** ```bash git tag v2.7.18-baseline git push origin v2.7.18-baseline ``` --- #### Task 0.2: 确认 Nashorn 影响范围 **Files:** - Read: `src/main/java/.../WisdomMathService.java` - [ ] **Step 1:提取 Nashorn 表达式求值用例** 在 WisdomMathService 中搜索所有 `engine.eval(` 和 `ScriptEngine` 出现的位置,提取所有使用的表达式样例(记录输入和输出)。 ```java // 按行记录类似这样的调用 engine.eval("12+38*2"); // → 88 engine.eval("Math.round(a)"); // → 四舍五入 // ... 记录全部 ``` - [ ] **Step 2:验证 exp4j 是否覆盖全部语法** 对照提取的表达式,确认 exp4j 支持: - 四则运算 + - * / ✅ - 括号 ✅ - 幂运算 ^ ✅ - Math 函数: abs, sin, cos, log, round, ceil, floor ✅ 对于不支持的函数(如果有),记录到 Phase 2 的自定义函数实现方案。 --- ### Phase 1 — javax → jakarta 迁移(5 天) #### Task 1.1: 实体类包名替换 **Files:** - Modify: `src/main/java/com/etotem/cfc/entity/` 下全部 Entity 类(~80 个) **Interfaces:** - Produces: 全部 Entity 类编译通过(包名迁移后) - [ ] **Step 1:全局替换 javax.persistence → jakarta.persistence** ```bash # PowerShell 在 entity 目录下执行 Get-ChildItem -Path "src/main/java/com/etotem/cfc/entity" -Filter "*.java" -Recurse | ForEach-Object { (Get-Content $_.FullName) -replace "javax\.persistence\.", "jakarta.persistence." | Set-Content $_.FullName } ``` - [ ] **Step 2:编译验证** ```bash mvn clean compile 2>&1 | Select-String "ERROR" ``` Expected: javax 错误行数减少(仅 entity 包部分消除) --- #### Task 1.2: DTO 校验注解替换 **Files:** - Modify: `src/main/java/com/etotem/cfc/dto/` 下全部 DTO 类(~200 个) - [ ] **Step 1:全局替换 javax.validation → jakarta.validation** ```bash Get-ChildItem -Path "src/main/java/com/etotem/cfc/dto" -Filter "*.java" -Recurse | ForEach-Object { (Get-Content $_.FullName) -replace "javax\.validation\.", "jakarta.validation." | Set-Content $_.FullName } ``` - [ ] **Step 2:编译验证** ```bash mvn clean compile 2>&1 | Select-String "ERROR" | Select-String "javax" ``` Expected: 仅剩非 persistence/validation 的 javax 错误 --- #### Task 1.3: 全局 javax.* 收尾替换 **Files:** - Modify: 所有剩余包含 `javax.*` 的 Java 文件 - [ ] **Step 1:扫描所有剩余 javax.* import** ```bash Get-ChildItem -Path "src/main/java" -Filter "*.java" -Recurse | Select-String "^import javax\." | ForEach-Object { $_.Filename + ": " + $_.Line.Trim() } | Sort-Object -Unique ``` - [ ] **Step 2:按以下规则逐条替换** | 原 import | 目标 import | |-----------|------------| | `javax.annotation.Resource` | `jakarta.annotation.Resource` | | `javax.annotation.PostConstruct` | `jakarta.annotation.PostConstruct` | | `javax.annotation.PreDestroy` | `jakarta.annotation.PreDestroy` | | `javax.servlet.http.HttpServletRequest` | `jakarta.servlet.http.HttpServletRequest` | | `javax.servlet.http.HttpServletResponse` | `jakarta.servlet.http.HttpServletResponse` | | `javax.servlet.Filter` | `jakarta.servlet.Filter` | | `javax.servlet.FilterChain` | `jakarta.servlet.FilterChain` | | `javax.servlet.ServletException` | `jakarta.servlet.ServletException` | | `javax.sql.DataSource` | `jakarta.sql.DataSource` | | `javax.xml.bind.*` | 确认是否使用,如使用需加 jakarta.xml.bind 依赖 | - [ ] **Step 3:逐文件编译验证** 每替换完一批,运行: ```bash mvn clean compile ``` 修复残留错误,直到零错。 - [ ] **Step 4:打阶段 tag** ```bash git add -A git commit -m "phase1: javax → jakarta migration complete" git tag v3.0-javax-migrated ``` --- ### Phase 2 — 行为适配 + Nashorn 替换(4 天) #### Task 2.1: WisdomMathService Nashorn → exp4j **Files:** - Modify: `src/main/java/.../service/WisdomMathService.java` - [ ] **Step 1:编写 exp4j 替换实现** ```java // 替换前 import javax.script.ScriptEngineManager; import javax.script.ScriptEngine; ScriptEngineManager mgr = new ScriptEngineManager(); ScriptEngine engine = mgr.getEngineByName("nashorn"); Object result = engine.eval("12+38*2"); // 替换后 import net.objecthunter.exp4j.ExpressionBuilder; double result = new ExpressionBuilder("12+38*2").build().evaluate(); ``` 如果需要支持 Math.round/ceil/floor 等函数,注册自定义函数: ```java import net.objecthunter.exp4j.function.Function; Function roundFunc = new Function("round", 1) { @Override public double apply(double... args) { return Math.round(args[0]); } }; Expression exp = new ExpressionBuilder(expression) .function(roundFunc) .build(); ``` - [ ] **Step 2:编译验证** ```bash mvn clean compile ``` Expected: 零错误 --- #### Task 2.2: jjwt 0.12 API 适配 **Files:** - Modify: 搜索所有使用 jjwt 的文件 - [ ] **Step 1:找到所有 jjwt 调用** ```bash Get-ChildItem -Path "src/main/java" -Filter "*.java" -Recurse | Select-String "Jwts\.|Keys\.|SignatureAlgorithm" | ForEach-Object { $_.Filename + "(" + $_.LineNumber + "): " + $_.Line.Trim() } ``` - [ ] **Step 2:按规则适配** ```java // jjwt 0.11 // jjwt 0.12 Jwts.builder() // 基本不变(返回 Builder 类型更新) .signWith(key, SignatureAlgorithm.HS256) // → .signWith(key) 自动推导算法 Jwts.parserBuilder() // → Jwts.parser() .setSigningKey(key) // → .verifyWith(key) 或 .decryptWith(key) .build() .parseClaimsJws(token) // 基本不变 Keys.secretKeyFor(SignatureAlgorithm.HS256) // → Jwts.SIG.HS256.keyBuilder().build() ``` - [ ] **Step 3:编译验证** ```bash mvn clean compile ``` --- #### Task 2.3: Spring Boot 3 行为变更适配 **Files:** - Modify: `src/main/resources/application.yml` - [ ] **Step 1:检查路径匹配策略** 添加/确认配置(如果现有代码依赖 Ant 风格路径匹配): ```yaml spring: mvc: pathmatch: matching-strategy: ant-path-matcher ``` - [ ] **Step 2:检查 springdoc 配置** ```yaml # v1 (如有) springdoc: swagger-ui: path: /swagger-ui.html # v2 相同,但验证可用 ``` - [ ] **Step 3:编译运行验证** ```bash mvn clean compile ``` 验证应用启动: ```bash mvn spring-boot:run ``` Expected: 应用启动成功,Swagger UI 可访问 /swagger-ui.html(或 v2 默认路径) --- #### Task 2.4: springdoc v2 Swagger UI 验证 - [ ] **Step 1:启动应用** ```bash mvn spring-boot:run ``` - [ ] **Step 2:访问 Swagger UI 验证 API 文档正确渲染** ```bash curl http://localhost:9082/v3/api-docs | Select-Object -First 20 ``` Expected: 返回 OpenAPI 3.0 JSON,paths 字段包含 /api/ 路由 - [ ] **Step 3:打阶段 tag** ```bash git commit -am "phase2: Spring Boot 3 behavior adaptation complete" git tag v3.0-build-ready ``` --- ### Phase 3 — Spring AI 集成 + Dify 替换(5 天) #### Task 3.1: 引入 Spring AI 依赖 + 配置 **Files:** - Modify: `pom.xml` - Modify: `src/main/resources/application.yml` - [ ] **Step 1:添加 Spring AI BOM + starter** ```xml org.springframework.ai spring-ai-bom 1.0.0 pom import org.springframework.ai spring-ai-openai-spring-boot-starter org.springframework.ai spring-ai-pdf-document-reader ``` - [ ] **Step 2:配置 AxonHub 模型网关(application.yml)** 所有模型调用走 AxonHub 统一网关,不直连任何模型 Provider: ```yaml spring: ai: openai: # 默认模型 → AxonHub → DeepSeek base-url: https://axonhub.iwintrue.com/v1 api-key: ${AXONHUB_API_KEY} chat: options: model: deepseek-chat temperature: 0.7 dashscope: # 第二模型 → AxonHub → Qwen (可选) api-key: ${AXONHUB_API_KEY} chat: options: model: qwen-max ollama: # 本地模型 (不经过 AxonHub) base-url: http://localhost:11434 chat: options: model: qwen2.5:7b vectorstore: sqlite: path: ${cfc.data.dir:/data/cfc}/knowledge.db ``` - [ ] **Step 3:编译验证** ```bash mvn clean compile ``` --- #### Task 3.2: 创建 LLMService 核心封装 **Files:** - Create: `src/main/java/com/etotem/cfc/service/ai/LLMService.java` - Create: `src/main/java/com/etotem/cfc/config/SpringAiConfig.java` - [ ] **Step 1:创建 SpringAiConfig(含多模型支持)** ```java package com.etotem.cfc.config; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.model.ChatModel; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class SpringAiConfig { /** 默认聊天客户端(经 AxonHub 路由) */ @Bean ChatClient chatClient(ChatClient.Builder builder) { return builder.build(); } /** * 如需在工作流中使用不同模型,注入命名后的 ChatModel Bean: * @Resource @Qualifier("openAiChatModel") private ChatModel deepSeek; * @Resource @Qualifier("dashScopeChatModel") private ChatModel qwen; * @Resource @Qualifier("ollamaChatModel") private ChatModel localModel; * * Spring AI 自动注册以下 Bean(按 application.yml 配置): * openAiChatModel → AxonHub → DeepSeek * dashScopeChatModel → AxonHub → Qwen (如果配置了 dashscope) * ollamaChatModel → localhost:11434 (如果配置了 ollama) */ } ``` - [ ] **Step 2:创建 LLMService(含多模型编排)** ```java package com.etotem.cfc.service.ai; import jakarta.annotation.Resource; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.memory.InMemoryChatMemory; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; import reactor.core.publisher.Flux; @Service public class LLMService { @Resource private ChatClient chatClient; @Resource @Qualifier("openAiChatModel") private ChatModel deepSeekModel; // → AxonHub → DeepSeek @Resource @Qualifier("dashScopeChatModel") private ChatModel qwenModel; // → AxonHub → Qwen @Resource @Qualifier("ollamaChatModel") private ChatModel localModel; // → 本地 Ollama private final ChatMemory chatMemory = new InMemoryChatMemory(); // 默认模型聊天 public String chat(String query, String systemPrompt, String conversationId) { return chatClient.prompt() .system(systemPrompt) .user(query) .advisors(new MessageChatMemoryAdvisor(chatMemory, conversationId, 10)) .call() .content(); } // 指定模型的聊天(工作流各节点可用不同模型) public String chatWithModel(String query, String systemPrompt, String conversationId, ChatModel model) { return ChatClient.builder(model).build() .prompt() .system(systemPrompt) .user(query) .advisors(new MessageChatMemoryAdvisor(chatMemory, conversationId, 10)) .call() .content(); } // 流式聊天 public Flux stream(String query, String systemPrompt, String conversationId) { return chatClient.prompt() .system(systemPrompt) .user(query) .advisors(new MessageChatMemoryAdvisor(chatMemory, conversationId, 10)) .stream() .content(); } // 多模型工作流示例 public WorkflowResult executeMultiModelWorkflow(String input) { // Step 1: DeepSeek 做意图分类(强推理) String intent = ChatClient.builder(deepSeekModel).build() .prompt().user("分类用户意图: " + input).call().content(); // Step 2: Qwen 做长文本分析(长上下文) String analysis = ChatClient.builder(qwenModel).build() .prompt().system("分析专家").user(intent).call().content(); // Step 3: 本地模型格式化输出(零成本) String output = ChatClient.builder(localModel).build() .prompt().user("格式化: " + analysis).call().content(); return new WorkflowResult(intent, analysis, output); } } ``` - [ ] **Step 3:编译验证** ```bash mvn clean compile ``` --- #### Task 3.3: System Prompt 外部化 **Files:** - Create: `resources/prompts/family-advisor.yaml` - Create: `resources/prompts/growth-plan.yaml` - Create: `resources/prompts/diet-recommend.yaml` - Create: `resources/prompts/math-tutor.yaml` - Create: `src/main/java/.../service/ai/SystemPromptLoader.java` - [ ] **Step 1:创建 SystemPromptLoader** ```java package com.etotem.cfc.service.ai; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.stereotype.Component; import org.yaml.snakeyaml.Yaml; import jakarta.annotation.Resource; import java.util.Map; @Component public class SystemPromptLoader { @Resource private ResourceLoader resourceLoader; public String load(String promptName) { Resource resource = resourceLoader.getResource("classpath:prompts/" + promptName + ".yaml"); // 解析 YAML,返回 prompt 字段 Map data = new Yaml().load(resource.getInputStream()); return data.get("prompt"); } } ``` - [ ] **Step 2:创建可替换的原 Dify Prompt 映射表** 从数据库 chat_assistant 表的 description/system_prompt 字段提取: ```yaml # resources/prompts/family-advisor.yaml prompt: | 你是家庭成长顾问,帮助家长理解孩子的成长需求。 请基于五维能量体系(身/心/智/行/富)提供建议。 ... ``` --- #### Task 3.4: AIService 重构为 LLMService **Files:** - Modify: `src/main/java/.../service/AIService.java` - Modify: `src/main/java/.../controller/AiChatController.java` - [ ] **Step 1:重构 AIService** ```java // 原有 AIService 用 OkHttp 调 Dify API,改为注入 LLMService @Service public class AIService { @Resource private LLMService llmService; @Resource private SystemPromptLoader promptLoader; public String sendMessage(String assistantType, String query, String conversationId) { String systemPrompt = promptLoader.load(assistantType); return llmService.chat(query, systemPrompt, conversationId); } // 流式版本 public Flux sendMessageStream(String assistantType, String query, String conversationId) { String systemPrompt = promptLoader.load(assistantType); return llmService.stream(query, systemPrompt, conversationId); } } ``` - [ ] **Step 2:编译验证 + 冒烟测试** ```bash mvn clean compile mvn spring-boot:run # 调用 /api/ai/chat/send 验证响应 ``` --- #### Task 3.5: DifySyncService → RAGService **Files:** - Create: `src/main/java/.../service/ai/RAGService.java` - Modify: `src/main/java/.../service/DifySyncService.java` - [ ] **Step 1:创建 RAGService** ```java package com.etotem.cfc.service.ai; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.ai.document.Document; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; import java.util.List; @Service public class RAGService { // sqlite-vec 或其他 VectorStore 实现 // 先使用搜索+vector 方案,第一期可简化为 keyword search @Resource private VectorStore vectorStore; public List search(String query, int topK) { return vectorStore.similaritySearch(query, topK); } public void addDocument(Document doc) { vectorStore.add(List.of(doc)); } } ``` - [ ] **Step 2:DifySyncService 标记为待迁移** 保留原有接口签名,内部改为从本地知识库读取: ```java @Service public class DifySyncService { @Resource private RAGService ragService; public List searchKnowledge(String query) { return ragService.search(query, 5).stream() .map(Document::getContent) .collect(Collectors.toList()); } } ``` --- #### Task 3.6: 剩余 6 个 Dify 相关 Service 替换 **Files:** - Modify: 逐个修改以下 Service(每个独立的文件修改,验证后继续下一个) - `AiConversationSummaryService.java` - `AiUserFactService.java` - 其余 4 个 - [ ] **Step 1-6:逐一替换** 每个 Service 的工作模式: 1. 读取当前代码,理解其调用的 Dify API(chat-messages / workflows / datasets) 2. 映射到 LLMService 或 RAGService 的对应方法 3. 替换实现,保持接口签名不变(最小化 Controller 层变动) 4. 编译验证 --- #### Task 3.7: 打阶段 tag - [ ] **Step 1:提交 + tag** ```bash git commit -am "phase3: Spring AI integration + Dify migration complete" git tag v3.0-spring-ai ``` --- ### Phase 4 — 回归验证 + 上线(3 天) #### Task 4.1: 全量功能回归 - [ ] **Step 1:Spring Boot 启动验证** ```bash mvn spring-boot:run ``` Expected: 应用启动成功,无 Error 日志 - [ ] **Step 2:所有 Controller API 冒烟测试** 使用 Postman 用例集或 curl 脚本验证以下关键路径: ``` POST /api/ai/chat/send → AI 聊天正常 POST /api/ai/chat/stream → 流式返回正常 POST /api/auth/login → JWT 正常签发 POST /api/user/info → 用户信息正常 ... 以及全部 48 个 Controller ``` - [ ] **Step 3:数学表达式精度对比验证** ``` 输入: "12+38*2" → 期望: 88.0 输入: "(1+2)^3" → 期望: 27.0 输入: "round(3.7)" → 期望: 4.0 输入: "abs(-5)" → 期望: 5.0 ... 提取全部原表达式用例 ``` --- #### Task 4.2: 性能基准对比 - [ ] **Step 1:记录原 Dify 链路延迟(基線)** 如果 Dify 仍可用,取 10 次请求的 P50/P95/P99 延迟。 如果 Dify 已不可用,使用 Phase 3 完成后的测试数据。 - [ ] **Step 2:记录 Spring AI 链路延迟** 对相同查询,取 10 次请求的 P50/P95/P99 延迟。 - [ ] **Step 3:确认不比原链路差** 延迟、错误率均不劣化则可进入上线。 --- #### Task 4.3: 灰度上线 - [ ] **Step 1:灰度 10% 流量(1 天)** Nginx/网关配置 10% 流量指向新版本,监控错误率。 - [ ] **Step 2:灰度 50% 流量(1 天)** 确认 10% 稳定后扩大到 50%,继续监控。 - [ ] **Step 3:全量上线 + Dify 实例保留下线** 100% 流量切到新版本,Dify 实例暂停但不销毁(数据保留 30 天)。 - [ ] **Step 4:打最终 tag** ```bash git tag v3.0-production ``` --- ## 执行顺序图 ``` Phase 0 ── Task 0.1 (BOM对齐) └── Task 0.2 (Nashorn范围) Phase 1 ── Task 1.1 (Entity) ├── Task 1.2 (DTO) └── Task 1.3 (全局收尾) Phase 2 ── Task 2.1 (exp4j) ├── Task 2.2 (jjwt) ├── Task 2.3 (SB3行为) └── Task 2.4 (springdoc验证) Phase 3 ── Task 3.1 (Spring AI依赖) ├── Task 3.2 (LLMService) ├── Task 3.3 (System Prompt) ├── Task 3.4 (AIService重构) ├── Task 3.5 (RAGService) └── Task 3.6 (剩余Service) Phase 4 ── Task 4.1 (功能回归) ├── Task 4.2 (性能对比) └── Task 4.3 (灰度上线) ``` Phase 1 可在 Task 1.1 + 1.2 中 2 人并行。 Phase 3 的 Task 3.2-3.6 建议 1 人连续完成(上下文依赖强)。 其余阶段按顺序执行。