从零构建智能作业批改系统:Spring Boot + Vue3 + RAG 实战

一个基于 AI 的全栈作业批改平台开发实录


项目概述

这是一个面向教育场景的智能作业批改系统,支持多格式文档上传、AI 智能批改、流式对话交互,以及基于 RAG(检索增强生成)的知识库问答功能。

技术栈:

  • 后端: Spring Boot 4.x + MyBatis-Plus + PostgreSQL + JWT
  • 前端: Vue 3 + Element Plus + Pinia
  • AI 层: OpenClaw 网关 + 自研 RAG 引擎
  • 文档解析: Apache Tika

一、系统架构设计

1.1 整体架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
┌─────────────────────────────────────────────────────────────────┐
│ 前端层 (Vue 3) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 登录页面 │ │ 聊天页面 │ │ 文件上传 │ │ 历史记录 │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│ API 网关层 │
│ Spring Boot + Spring Security + JWT │
└─────────────────────────────────────────────────────────────────┘

┌───────────────────┼───────────────────┐
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 用户认证模块 │ │ 聊天/批改模块 │ │ RAG 知识库 │
│ - 注册/登录 │ │ - 流式对话 │ │ - 文档解析 │
│ - JWT 鉴权 │ │ - 作业批改 │ │ - 智能分块 │
│ - 权限控制 │ │ - 历史记录 │ │ - 向量检索 │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
└───────────────────┼───────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ 数据持久层 │
│ PostgreSQL (业务数据) + pgvector (向量存储) │
└─────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│ AI 服务层 (OpenClaw) │
│ 大模型调用 + 智能体编排 + 多模型路由 │
└─────────────────────────────────────────────────────────────────┘

1.2 核心模块划分

模块 职责 关键技术
auth 用户认证与授权 JWT、Spring Security
chat 对话与批改核心 SSE 流式、WebFlux
rag 知识库与检索 向量嵌入、语义搜索
document 文档管理 Apache Tika、文件存储
storage 文件上传下载 NIO、UUID 命名

二、核心技术实现

2.1 RAG 检索增强生成

RAG 是本系统的核心亮点,实现了文档的智能解析、分块、嵌入和检索。

2.1.1 智能文档分块 (SmartChunkService)

不同于简单的固定长度切割,系统实现了多策略智能分块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 智能切割入口 - 根据文档类型选择最优策略
*/
public List<DocumentChunk> chunk(String content, ChunkConfig config) {
// 1. 文档类型检测
DocType docType = detectDocType(content);

// 2. 根据类型选择切割策略
List<String> rawChunks;
switch (docType) {
case MARKDOWN:
rawChunks = splitByMarkdownHeaders(content, config); // 按标题层级
break;
case CODE:
rawChunks = splitByCodeStructure(content, config); // 按函数/类
break;
case CONVERSATION:
rawChunks = splitByConversationTurns(content, config); // 按对话轮次
break;
default:
rawChunks = splitBySemanticBoundaries(content, config); // 语义边界
}

// 3. 滑动窗口 + 上下文引用
return applySlidingWindow(rawChunks, config);
}

分块策略对比:

策略 适用场景 优势
Markdown 标题分割 文档、笔记 保持章节完整性
代码结构分割 源代码文件 按函数/类边界切割
对话轮次分割 聊天记录 保持对话上下文
语义边界分割 通用文本 基于句子相似度

2.1.2 语义边界检测算法

核心思想:语义相似度低的句子边界 = 主题转换点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private List<String> splitBySemanticBoundaries(String content, ChunkConfig config) {
// 1. 按句子分割
String[] sentences = content.split("(?<=[。!?.!?])\\s+");

// 2. 批量获取句子嵌入向量
List<float[]> embeddings = embeddingService.embedBatch(Arrays.asList(sentences));

// 3. 检测语义边界
for (int i = 0; i < sentences.length; i++) {
if (i > 0) {
double similarity = cosineSimilarity(embeddings.get(i), embeddings.get(i-1));
// 相似度低于阈值 = 主题转换 = 切割点
if (similarity < SEMANTIC_THRESHOLD) {
// 在此处切割
}
}
}
}

2.1.3 轻量级嵌入服务

考虑到国内网络环境,放弃了 HuggingFace 模型下载,实现了基于词频的稀疏向量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public class EmbeddingService {

public float[] embed(String text) {
// 提取关键词 + 2-gram
Map<String, Integer> wordFreq = extractKeywords(text);

// 哈希映射到固定维度 (384维)
float[] vector = new float[384];
for (Map.Entry<String, Integer> entry : wordFreq.entrySet()) {
int idx = Math.abs(entry.getKey().hashCode()) % 384;
vector[idx] += entry.getValue();
}

// L2 归一化
return normalize(vector);
}
}

优势:

  • 无需下载大模型,启动即用
  • 计算速度快,CPU 即可运行
  • 支持中文分词和 2-gram 特征

2.1.4 向量存储与检索

使用 PostgreSQL + pgvector 扩展:

1
2
3
4
5
6
7
8
9
// 相似度搜索 - 使用 pgvector 的 <=> 操作符(余弦距离)
public List<DocumentChunk> similaritySearch(float[] queryEmbedding, int topK) {
String sql = """
SELECT * FROM document_chunk
ORDER BY embedding_vec <=> ?::vector
LIMIT ?
""";
return jdbcTemplate.query(sql, rowMapper, vectorStr, topK);
}

降级策略: 当 pgvector 不可用时,自动回退到全表扫描 + 余弦相似度计算。

2.2 流式对话实现

2.2.1 SSE 流式响应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public ResponseEntity<StreamingResponseBody> streamMessage(
@RequestParam String message,
@RequestParam(required = false) String sessionId) {

StreamingResponseBody responseBody = outputStream -> {
openClawService.streamChat(message, sessionId, status)
.doOnNext(chunk -> {
// SSE 格式: data: {...}\n\n
String sseLine = "data: " + chunk + "\n\n";
outputStream.write(sseLine.getBytes());
outputStream.flush();
})
.doOnComplete(() -> {
outputStream.write("data: [DONE]\n\n".getBytes());
})
.blockLast(Duration.ofMinutes(5));
};

return ResponseEntity.ok()
.header("X-Accel-Buffering", "no") // 禁用 Nginx 缓冲
.body(responseBody);
}

2.2.2 前端流式接收

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 使用 ReadableStream API 处理 SSE
const reader = response.body.getReader();
const decoder = new TextDecoder();

while (true) {
const { done, value } = await reader.read();
if (done) break;

buffer += decoder.decode(value, { stream: true });

// 按行分割处理 SSE 数据
const lines = buffer.split('\n');
buffer = lines.pop();

for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
// 追加到消息内容
this.messages[msgIndex].content += data;
}
}
}

2.3 多格式文档解析

使用 Apache Tika 实现统一的文档解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
public class FileStorageServiceImpl implements FileStorageService {

private String parseWithTika(Path filePath) {
AutoDetectParser parser = new AutoDetectParser();
BodyContentHandler handler = new BodyContentHandler(5 * 1024 * 1024); // 5MB 限制
Metadata metadata = new Metadata();
ParseContext context = new ParseContext();

// PDF 配置:关闭图片提取,按位置排序文本
PDFParserConfig pdfConfig = new PDFParserConfig();
pdfConfig.setExtractInlineImages(false);
pdfConfig.setSortByPosition(true);
context.set(PDFParserConfig.class, pdfConfig);

parser.parse(inputStream, handler, metadata, context);
return handler.toString();
}
}

支持格式: PDF、Word、Excel、PPT、TXT、Markdown 等。

2.4 JWT 认证与权限控制

2.4.1 JWT 工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
public class JwtUtil {

public String generateToken(Long userId, Integer status) {
return Jwts.builder()
.setSubject(String.valueOf(userId))
.claim("status", status)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}

public Long getUserIdFromToken(String token) {
Claims claims = parseToken(token);
return Long.valueOf(claims.getSubject());
}
}

2.4.2 Spring Security 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**", "/api/chat/health").permitAll()
.anyRequest().authenticated()
);
return http.build();
}
}

三、关键技术点总结

3.1 知识点梳理

技术领域 核心知识点 应用场景
Spring Boot 自动配置、Starter 依赖、Actuator 快速搭建后端服务
Spring Security 过滤器链、JWT 认证、CORS 配置 统一认证授权
MyBatis-Plus 代码生成、分页插件、逻辑删除 数据库 CRUD
WebFlux Reactive 编程、Mono/Flux、背压 流式数据处理
RAG 文档分块、向量嵌入、相似度检索 知识库问答
Vue 3 Composition API、响应式系统、Teleport 前端组件化
SSE EventSource、ReadableStream 实时消息推送

3.2 设计模式应用

模式 应用位置 说明
策略模式 SmartChunkService 不同文档类型使用不同分块策略
工厂模式 EmbeddingService 根据配置创建不同的嵌入实现
降级模式 VectorStoreService pgvector 不可用时回退到全表扫描
拦截器模式 JWT 过滤器 统一处理认证逻辑

3.3 性能优化点

  1. 连接池优化: 使用 HikariCP,配置合理的最大连接数
  2. 向量检索优化: pgvector 索引 + HNSW 算法
  3. 流式响应: 禁用 Nginx 缓冲,减少内存占用
  4. 批量嵌入: 一次性处理多个句子,减少 IO 次数

四、项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
demo/
├── src/main/java/com/firedemo/demo/
│ ├── Bean/ # 配置类
│ │ ├── SecurityConfig.java # 安全配置
│ │ ├── OpenClawConfig.java # AI 网关配置
│ │ └── OpenClawProperties.java
│ ├── Controller/ # 控制器层
│ │ ├── AuthController.java # 认证接口
│ │ ├── ChatController.java # 聊天/批改接口
│ │ ├── DocumentController.java
│ │ └── FileUploadController.java
│ ├── Service/ # 服务层
│ │ ├── ChatHistoryService.java
│ │ ├── DocumentService.java
│ │ ├── FileStorageService.java
│ │ ├── OpenClawService.java
│ │ └── ServiceImpl/ # 实现类
│ ├── Entity/ # 实体类
│ │ ├── User.java
│ │ ├── Document.java
│ │ ├── DocumentChunk.java
│ │ ├── ChatHistory.java
│ │ └── HomeworkEvaluation.java
│ ├── mapper/ # MyBatis Mapper
│ ├── DTO/ # 数据传输对象
│ ├── VO/ # 视图对象
│ ├── utils/ # 工具类
│ │ ├── JwtUtil.java
│ │ └── JwtAuthenticationFilter.java
│ └── rag/ # RAG 模块 ⭐
│ ├── SmartChunkService.java # 智能分块
│ ├── EmbeddingService.java # 嵌入服务
│ ├── VectorStoreService.java # 向量存储
│ └── RAGController.java
├── src/main/resources/
│ └── application.yml
└── pom.xml

vue-project/
├── src/
│ ├── api/
│ │ └── request.ts # Axios 封装
│ ├── views/
│ │ ├── LoginView.vue # 登录页
│ │ ├── PageTwo.vue # 聊天页 ⭐
│ │ └── ...
│ ├── stores/
│ │ └── auth.ts # Pinia 状态管理
│ ├── router/
│ │ └── index.ts
│ └── main.ts
└── package.json

五、开发过程中的踩坑记录

5.1 Lombok 与 Spring Boot 4.x 兼容问题

现象: 编译时找不到生成的 getter/setter 方法

解决: 确保 maven-compiler-plugin 正确配置 annotationProcessorPaths

1
2
3
4
5
6
7
8
9
10
11
12
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>

5.2 MyBatis-Plus Spring Boot 4 适配

现象: 启动报错,找不到 Mapper

解决: 使用专为 Spring Boot 4 适配的依赖

1
2
3
4
5
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot4-starter</artifactId>
<version>3.5.15</version>
</dependency>

5.3 SSE 连接被 Nginx 缓冲

现象: 流式响应一次性返回,没有实时效果

解决: 添加响应头禁用缓冲

1
2
3
4
return ResponseEntity.ok()
.header("X-Accel-Buffering", "no")
.header("Cache-Control", "no-cache")
.body(responseBody);

5.4 HuggingFace 模型下载失败

现象: DJL 框架无法下载 embedding 模型

解决: 改用自研的基于词频的轻量级嵌入方案


六、未来规划

  • 多模态支持:图片、手写体识别
  • 批改模板:支持自定义批改标准
  • 批量批改:一次上传多份作业
  • 数据分析:评分统计、错题分析
  • 多租户:支持班级/学校隔离

结语

这个项目从 0 到 1 的过程中,最大的收获不是技术本身,而是工程化思维的培养:

  1. 先跑通,再优化 - 不要一开始就追求完美架构
  2. 降级思维 - 每个外部依赖都要有 fallback 方案
  3. 可观测性 - 日志、监控、健康检查缺一不可
  4. 用户视角 - 流式响应比等待完整回复体验好 10 倍

希望这篇文章对你有所帮助。如有问题,欢迎交流!


项目地址: (待补充)
作者: 龙空灵
开发周期: 2 周