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预处理)
springdoc-openapi-ui 改为 springdoc-openapi-starter-webmvc-uiJwts.parserBuilder() 改为 Jwts.parser(), Keys API 变化Result<T> (code/message/data)axonhub.iwintrue.com,Spring AI 配置 base-url 指向 AxonHubChatModel Bean(DeepSeek/Qwen/本地模型等),通过 @Qualifier 注入| 文件 | 用途 |
|---|---|
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 替换 |
Files:
pom.xmlInterfaces:
Produces: mvn clean compile 通过(尚未改 javax,暂时只对齐依赖)
[ ] Step 1:更新 parent 和 properties
在 pom.xml 中修改:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.3</version> <!-- 2026年最新 3.4.x -->
<relativePath/>
</parent>
<properties>
<java.version>21</java.version>
<mybatis-plus.version>3.5.9</mybatis-plus.version>
<jjwt.version>0.12.6</jjwt.version>
</properties>
找到并替换:
<!-- 删除 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.8.0</version>
</dependency>
<!-- 添加 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.7.0</version>
</dependency>
[ ] Step 3:移除 jjwt 旧版本,确认 BOM 覆盖
<!-- jjwt 版本已通过 properties 控制 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
[ ] Step 4:添加 exp4j 依赖
<dependency>
<groupId>net.objecthunter</groupId>
<artifactId>exp4j</artifactId>
<version>0.4.8</version>
</dependency>
[ ] Step 5:编译验证
运行:
mvn clean compile
Expected: 编译失败(因为 javax 包名不存在,这是预期的。失败信息用来确认 Phase 1 的替换范围)
[ ] Step 6:录制 javax 报错清单
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
git tag v2.7.18-baseline
git push origin v2.7.18-baseline
Files:
Read: src/main/java/.../WisdomMathService.java
[ ] Step 1:提取 Nashorn 表达式求值用例
在 WisdomMathService 中搜索所有 engine.eval( 和 ScriptEngine 出现的位置,提取所有使用的表达式样例(记录输入和输出)。
// 按行记录类似这样的调用
engine.eval("12+38*2"); // → 88
engine.eval("Math.round(a)"); // → 四舍五入
// ... 记录全部
对照提取的表达式,确认 exp4j 支持:
对于不支持的函数(如果有),记录到 Phase 2 的自定义函数实现方案。
Files:
src/main/java/com/etotem/cfc/entity/ 下全部 Entity 类(~80 个)Interfaces:
Produces: 全部 Entity 类编译通过(包名迁移后)
[ ] Step 1:全局替换 javax.persistence → jakarta.persistence
# 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:编译验证
mvn clean compile 2>&1 | Select-String "ERROR"
Expected: javax 错误行数减少(仅 entity 包部分消除)
Files:
Modify: src/main/java/com/etotem/cfc/dto/ 下全部 DTO 类(~200 个)
[ ] Step 1:全局替换 javax.validation → jakarta.validation
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:编译验证
mvn clean compile 2>&1 | Select-String "ERROR" | Select-String "javax"
Expected: 仅剩非 persistence/validation 的 javax 错误
Files:
Modify: 所有剩余包含 javax.* 的 Java 文件
[ ] Step 1:扫描所有剩余 javax.* import
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 依赖 |
每替换完一批,运行:
mvn clean compile
修复残留错误,直到零错。
[ ] Step 4:打阶段 tag
git add -A
git commit -m "phase1: javax → jakarta migration complete"
git tag v3.0-javax-migrated
Files:
Modify: src/main/java/.../service/WisdomMathService.java
[ ] Step 1:编写 exp4j 替换实现
// 替换前
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 等函数,注册自定义函数:
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:编译验证
mvn clean compile
Expected: 零错误
Files:
Modify: 搜索所有使用 jjwt 的文件
[ ] Step 1:找到所有 jjwt 调用
Get-ChildItem -Path "src/main/java" -Filter "*.java" -Recurse |
Select-String "Jwts\.|Keys\.|SignatureAlgorithm" |
ForEach-Object { $_.Filename + "(" + $_.LineNumber + "): " + $_.Line.Trim() }
[ ] Step 2:按规则适配
// 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:编译验证
mvn clean compile
Files:
Modify: src/main/resources/application.yml
[ ] Step 1:检查路径匹配策略
添加/确认配置(如果现有代码依赖 Ant 风格路径匹配):
spring:
mvc:
pathmatch:
matching-strategy: ant-path-matcher
[ ] Step 2:检查 springdoc 配置
# v1 (如有)
springdoc:
swagger-ui:
path: /swagger-ui.html
# v2 相同,但验证可用
[ ] Step 3:编译运行验证
mvn clean compile
验证应用启动:
mvn spring-boot:run
Expected: 应用启动成功,Swagger UI 可访问 /swagger-ui.html(或 v2 默认路径)
[ ] Step 1:启动应用
mvn spring-boot:run
[ ] Step 2:访问 Swagger UI 验证 API 文档正确渲染
curl http://localhost:9082/v3/api-docs | Select-Object -First 20
Expected: 返回 OpenAPI 3.0 JSON,paths 字段包含 /api/ 路由
[ ] Step 3:打阶段 tag
git commit -am "phase2: Spring Boot 3 behavior adaptation complete"
git tag v3.0-build-ready
Files:
pom.xmlModify: src/main/resources/application.yml
[ ] Step 1:添加 Spring AI BOM + starter
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.0</version> <!-- 2026年最新 1.x -->
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pdf-document-reader</artifactId>
</dependency>
</dependencies>
[ ] Step 2:配置 AxonHub 模型网关(application.yml)
所有模型调用走 AxonHub 统一网关,不直连任何模型 Provider:
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:编译验证
mvn clean compile
Files:
src/main/java/com/etotem/cfc/service/ai/LLMService.javaCreate: src/main/java/com/etotem/cfc/config/SpringAiConfig.java
[ ] Step 1:创建 SpringAiConfig(含多模型支持)
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(含多模型编排)
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<String> 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:编译验证
mvn clean compile
Files:
resources/prompts/family-advisor.yamlresources/prompts/growth-plan.yamlresources/prompts/diet-recommend.yamlresources/prompts/math-tutor.yamlCreate: src/main/java/.../service/ai/SystemPromptLoader.java
[ ] Step 1:创建 SystemPromptLoader
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<String, String> data = new Yaml().load(resource.getInputStream());
return data.get("prompt");
}
}
[ ] Step 2:创建可替换的原 Dify Prompt 映射表
从数据库 chat_assistant 表的 description/system_prompt 字段提取:
# resources/prompts/family-advisor.yaml
prompt: |
你是家庭成长顾问,帮助家长理解孩子的成长需求。
请基于五维能量体系(身/心/智/行/富)提供建议。
...
Files:
src/main/java/.../service/AIService.javaModify: src/main/java/.../controller/AiChatController.java
[ ] Step 1:重构 AIService
// 原有 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<String> sendMessageStream(String assistantType, String query, String conversationId) {
String systemPrompt = promptLoader.load(assistantType);
return llmService.stream(query, systemPrompt, conversationId);
}
}
[ ] Step 2:编译验证 + 冒烟测试
mvn clean compile
mvn spring-boot:run
# 调用 /api/ai/chat/send 验证响应
Files:
src/main/java/.../service/ai/RAGService.javaModify: src/main/java/.../service/DifySyncService.java
[ ] Step 1:创建 RAGService
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<Document> search(String query, int topK) {
return vectorStore.similaritySearch(query, topK);
}
public void addDocument(Document doc) {
vectorStore.add(List.of(doc));
}
}
[ ] Step 2:DifySyncService 标记为待迁移
保留原有接口签名,内部改为从本地知识库读取:
@Service
public class DifySyncService {
@Resource
private RAGService ragService;
public List<String> searchKnowledge(String query) {
return ragService.search(query, 5).stream()
.map(Document::getContent)
.collect(Collectors.toList());
}
}
Files:
Modify: 逐个修改以下 Service(每个独立的文件修改,验证后继续下一个)
AiConversationSummaryService.javaAiUserFactService.java[ ] Step 1-6:逐一替换
每个 Service 的工作模式:
[ ] Step 1:提交 + tag
git commit -am "phase3: Spring AI integration + Dify migration complete"
git tag v3.0-spring-ai
[ ] Step 1:Spring Boot 启动验证
mvn spring-boot:run
Expected: 应用启动成功,无 Error 日志
使用 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
... 提取全部原表达式用例
如果 Dify 仍可用,取 10 次请求的 P50/P95/P99 延迟。 如果 Dify 已不可用,使用 Phase 3 完成后的测试数据。
对相同查询,取 10 次请求的 P50/P95/P99 延迟。
延迟、错误率均不劣化则可进入上线。
Nginx/网关配置 10% 流量指向新版本,监控错误率。
确认 10% 稳定后扩大到 50%,继续监控。
100% 流量切到新版本,Dify 实例暂停但不销毁(数据保留 30 天)。
[ ] Step 4:打最终 tag
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 人连续完成(上下文依赖强)。 其余阶段按顺序执行。