用 Java 25 + Spring AI 构建新加坡小学数学 AI 辅导 App — Phase 1 实战记录
目录
项目背景#
我正在开发一个面向新加坡 2026 PSLE 考纲的小学数学 AI 辅导 App。核心理念:
- 使用新加坡教育部推崇的 CPA (Concrete-Pictorial-Abstract) 教学法
- 多 Agent 协作:Planner → CPA Designer + Persona 并行处理
- 为家长和孩子分别生成不同语气的解题指导
技术栈选型比较激进:Java 25 + Spring Boot 4.0 + Spring AI 2.0 + Ollama 本地模型。这篇文章记录 Phase 1 的搭建过程和踩过的坑。
架构概览#
Planner 先跑,分析题目后输出结构化 JSON,然后 CPA Designer 和 Persona 两个 Agent 并发执行。并发使用的是 Java 25 的 Structured Concurrency(StructuredTaskScope)。
错误处理流程#
兼容性踩坑记录#
这次用的技术栈都比较新,版本冲突是第一个大坑。最终跑通的版本组合:
| 组件 | 原计划 | 实际版本 | 原因 |
|---|---|---|---|
| Java | 25 | 25 (Temurin-25+36) | 不变 |
| Gradle | 8.x | 9.2 | Gradle 8.13 在 Java 25 上直接崩溃 |
| Spring Boot | 3.5 | 4.0.3 | 插件 3.5.0 内部拉取 spring-boot:4.0.1 导致冲突 |
| Spring Framework | 6.x | 7.0.5 | Spring Boot 4.0 依赖 |
| Hibernate | 6.x | 7.2.4 | Spring Boot 4.0 依赖 |
| Spring AI | 2.0.0 | 2.0.0-M2 | 正式版尚未发布 |
| Lombok | 1.18.36 | 1.18.42 | 1.18.36 不兼容 Java 25 编译器内部 API |
Gradle 9 + Spring Boot 4.0#
Gradle 8.13 在 Java 25 运行时上直接报错(错误信息只有一个 “25”),需要升级到 Gradle 9:
gradle wrapper --gradle-version 9.2
Gradle 9 的一个重大变化是 io.spring.dependency-management 插件不再兼容。需要改用 Gradle 原生的 platform() BOM 方式管理依赖:
// ❌ 旧方式 - Gradle 9 不兼容
plugins {
id("io.spring.dependency-management") version "1.1.7"
}
// ✅ 新方式 - 原生 platform() BOM
dependencies {
implementation(platform(
org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES
))
implementation(platform(
"org.springframework.ai:spring-ai-bom:2.0.0-M2"
))
}
Spring Boot 4.0 模块化自动配置#
Spring Boot 4.0 的一个大变化是自动配置从单体 spring-boot-autoconfigure 拆分到各个独立模块。比如:
- JPA 自动配置 → 需要
spring-boot-starter-data-jpa中的独立模块 - Flyway 自动配置 → 需要独立的
spring-boot-starter-flyway - MockMvc 测试 →
AutoConfigureMockMvc移到了org.springframework.boot.webmvc.test.autoconfigure包
如果只引了 flyway-core,Flyway 根本不会自动执行迁移。需要改为:
// ❌ Spring Boot 3.x 时代可用
implementation("org.flywaydb:flyway-core")
// ✅ Spring Boot 4.0 需要
implementation("org.springframework.boot:spring-boot-starter-flyway")
implementation("org.flywaydb:flyway-database-postgresql")
Lombok + Java 25#
Lombok 1.18.36 在 Java 25 上编译直接报 NoSuchFieldException: com.sun.tools.javac.code.TypeTag :: UNKNOWN,因为 Java 25 编译器内部 API 发生了变化。升级到 1.18.42 解决。
核心实现#
多 Agent 编排 — StructuredTaskScope#
使用 Java 25 的 Structured Concurrency 来并行执行 CPA Designer 和 Persona Agent:
public SolveResult solve(SolveRequest request) {
// Step 1: Planner Agent(串行)
String plannerResult = runPlannerAgent(request);
// Step 2: CPA Designer + Persona(并行)
try (var scope = StructuredTaskScope.open()) {
var cpaFuture = scope.fork(() ->
runCpaDesignerAgent(plannerResult));
var personaFuture = scope.fork(() ->
runPersonaAgent(plannerResult, request.grade()));
scope.join();
return new SolveResult(
extractJsonField(personaFuture.get(), "parentGuide"),
extractJsonField(personaFuture.get(), "childScript"),
cpaFuture.get(),
extractKnowledgeTags(plannerResult)
);
}
}
StructuredTaskScope 的优势:
scope.join()等待所有 fork 完成- 如果任一子任务抛异常,可以配置策略自动取消其他子任务
- try-with-resources 确保作用域关闭时所有子任务已终止
System Prompts — 新加坡 CPA 教学法#
Planner Agent 的 System Prompt 核心要求:
You are an expert Singapore primary school math teacher,
specializing in the 2026 MOE syllabus.
Given a math question and the student's grade level (P1-P6):
1. Identify the topic and relevant knowledge points
2. Break down the solution into clear, numbered steps
3. Use the CPA (Concrete-Pictorial-Abstract) teaching approach
Respond in JSON format:
{
"knowledgeTags": ["algebra.substitution"],
"steps": [{"stepNumber": 1, "description": "..."}],
"answer": "...",
"difficulty": "easy|medium|hard"
}
Persona Agent 则为同一道题生成两种不同语气的输出:
- parentGuide — 给家长看的教学指导,专业、简练
- childScript — 给孩子看的趣味解释,用类比和 emoji,让数学像冒险一样
SSE 流式输出#
解题支持 SSE (Server-Sent Events) 流式输出,前端可以逐字显示:
@PostMapping(value = "/stream",
produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> solveStream(@RequestBody SolveRequest request) {
return chatClient.prompt()
.system(SOLVE_SYSTEM_PROMPT)
.user("Grade: P%d\nQuestion: %s"
.formatted(request.grade(), request.question()))
.stream()
.content();
}
这里用了 Spring AI 的 ChatClient 流式接口,底层对接 Ollama。curl -N 可以直接在终端看到逐字输出效果。
身份认证#
使用 Spring Security + BCrypt + JWT 的标准方案:
# 注册
curl -X POST localhost:8080/api/v1/auth/register \
-H 'Content-Type: application/json' \
-d '{"email":"student@test.com","password":"123456"}'
# → 201 {"message":"Registration successful","userId":"..."}
# 重复注册
# → 409 {"error":"Email already registered"}
# 登录
curl -X POST localhost:8080/api/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"student@test.com","password":"123456"}'
# → 200 {"token":"..."}
数据库 — pgvector + Flyway#
数据库使用 PostgreSQL 17 + pgvector 扩展,Flyway 管理迁移。初始 Schema 包含:
| 表 | 用途 |
|---|---|
users |
用户账号 |
student_profiles |
学生档案(年级、学习偏好) |
solve_records |
解题记录 |
sg_math_questions |
PSLE 题库 + 768维向量(为 Phase 2 RAG 准备) |
knowledge_progress |
知识点掌握度追踪 |
vector_store |
Spring AI VectorStore 内置表 |
向量维度使用 768(匹配 nomic-embed-text 模型),索引类型选择 HNSW(IVFFlat 无法在空表上构建)。
ALTER TABLE sg_math_questions
ADD COLUMN embedding vector(768);
CREATE INDEX idx_questions_embedding
ON sg_math_questions USING hnsw (embedding vector_cosine_ops);
本地开发环境#
系统部署架构#
# 启动基础设施
cd infra && docker compose up -d # PostgreSQL 17 + Redis 7
# Ollama 运行在 macOS 原生环境
ollama pull qwen3.5 # 对话模型 6.6GB
ollama pull nomic-embed-text # Embedding 模型 274MB
# 启动后端
cd backend
./gradlew bootRun --args='--spring.profiles.active=dev'
注意:Docker 版 Ollama 需要 NVIDIA GPU,macOS 上建议直接用原生 Ollama。我在 docker-compose.yml 中将 Ollama 服务设为 profiles: [gpu],默认不启动。
测试#
写了两类测试来验证 Phase 1 交付:
AuthControllerTest — 认证接口的 MockMvc 测试:
@SpringBootTest
@AutoConfigureMockMvc
class AuthControllerTest {
@Test
void register_NewUser_Returns201() { ... }
@Test
void register_DuplicateEmail_Returns409() { ... }
@Test
void login_ValidCredentials_ReturnsToken() { ... }
@Test
void login_InvalidCredentials_Returns401() { ... }
}
MathSolverOrchestratorIntegrationTest — Agent 链的端到端测试(需要 Ollama 运行中):
@SpringBootTest
@EnabledIfEnvironmentVariable(
named = "OLLAMA_AVAILABLE", matches = "true")
class MathSolverOrchestratorIntegrationTest {
@Test
void solve_SimpleAddition_ReturnsStructuredResult() {
SolveResult result = orchestrator.solve(
new SolveRequest("5 + 3 = ?", 1));
assertNotNull(result.parentGuide());
assertNotNull(result.childScript());
assertNotNull(result.barModelJson());
assertFalse(result.knowledgeTags().isEmpty());
}
}
所有测试 ./gradlew test 全绿通过。
Phase 1 总结#
| 任务 | 状态 |
|---|---|
| 后端 bootRun 启动 | ✅ |
| Flyway 迁移建表 | ✅ |
| 注册/登录 API | ✅ |
| MathSolverOrchestrator 多 Agent 链 | ✅ |
| Planner Agent System Prompt | ✅ |
| Persona Agent System Prompt | ✅ |
| SSE 流式输出 | ✅ |
核心收获:
- Java 25 + Spring Boot 4.0 + Gradle 9 这套组合目前可用,但需要留意各种兼容性问题。Gradle 9 和 Spring Boot 4.0 都有破坏性变更。
- Structured Concurrency 用于 Agent 并发非常自然。
StructuredTaskScope.open()+fork()+join()的 API 比CompletableFuture更简洁、更安全。 - Spring AI ChatClient 的流式接口开箱即用,对接 Ollama 后 SSE 流式输出零额外代码。
- 本地用
qwen3.5做数学推理效果基本可用,但响应较慢(一次完整 Agent 链约 3-4 分钟),生产环境需要用云端模型。
Phase 2 计划:搭建 RAG 知识库(向量检索 PSLE 题库),让 Agent 的回答有真题/模拟题作为参考上下文。