DocuMind AI 技术学习文档
DocuMind AI 技术学习文档
项目:DocuMind AI 智能文档问答系统
技术栈:Spring Boot 3.3.5 + Spring AI Alibaba + MyBatis-Plus + Spring Security + JWT + Redis + MySQL
作者:DocuMind Team
目录
- 项目概述
- 技术栈总览
- Spring Boot 3.x 核心知识
- Spring Security + JWT 认证体系
- MyBatis-Plus 数据访问层
- Spring AI Alibaba 与大模型集成
- RAG 检索增强生成
- 向量存储与文档处理
- 流式响应与 SSE
- AOP 切面编程
- 异步任务与事务管理
- 全局异常处理
- 数据库设计
- 业务模块详解
- API 接口设计规范
- 配置管理与多环境
- Lombok 使用指南
- Redis 缓存集成
- 文件上传与处理
- 项目架构与设计模式
1. 项目概述
1.1 系统简介
DocuMind AI 是一个智能文档问答系统,核心理念是"我的专属 AI"。每个用户可以创建多个智能体,为每个智能体上传专属文档,AI 基于这些文档回答问题。
1.2 核心功能
- 智能体管理:用户创建、配置、管理多个 AI 智能体
- 文档知识库:上传 PDF/Word/Excel/TXT/MD 文档,自动向量化
- RAG 问答:基于文档内容进行检索增强生成,回答更准确
- 多提示词模式:根据用户问题自动切换最合适的系统提示词
- 流式对话:SSE 实时流式输出 AI 回复
- 对话历史:完整保存每次对话,支持多会话管理
- API 监控:AOP 切面自动记录所有接口调用日志
1.3 技术架构图
前端请求
↓
Spring Security 过滤链(JWT 验证)
↓
Controller 层(REST API)
↓
Service 层(业务逻辑)
↓
├── MyBatis-Plus(MySQL 数据持久化)
├── Spring AI(大模型调用)
├── VectorStore(向量检索)
└── Redis(缓存)
1.4 项目包结构
cn.edu.cdu.documind
├── aspect/ # AOP 切面(API 监控)
├── common/ # 公共类(统一响应 Result)
├── config/ # 配置类
├── controller/ # 控制器层
├── dto/ # 数据传输对象
│ ├── request/ # 请求 DTO
│ └── response/ # 响应 DTO
├── entity/ # 实体类(对应数据库表)
├── exception/ # 全局异常处理
├── mapper/ # MyBatis-Plus Mapper
├── reader/ # 文档读取器
├── security/ # 安全相关(JWT 过滤器)
├── service/ # 业务逻辑层
└── util/ # 工具类
2. 技术栈总览
2.1 核心依赖清单
| 技术 | 版本 | 用途 |
|---|---|---|
| Spring Boot | 3.3.5 | 基础框架 |
| Spring AI Alibaba | 1.0.0.2 | 大模型集成 |
| MyBatis-Plus | 3.5.11 | ORM 框架 |
| Spring Security | 随 Boot | 认证授权 |
| JJWT | 0.12.5 | JWT 令牌 |
| MySQL | 8.x | 主数据库 |
| Redis | 7.x | 缓存 |
| PDFBox | 3.0.1 | PDF 解析 |
| Apache POI | 5.2.5 | Excel/Word 解析 |
| Springdoc OpenAPI | 2.3.0 | Swagger 文档 |
| Lombok | 1.18.30 | 代码简化 |
| Java | 17 | 运行环境 |
2.2 pom.xml 关键配置解析
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.5</version>
</parent>
知识点:spring-boot-starter-parent 是 Spring Boot 的父 POM,它做了三件事:
- 统一管理常用依赖版本(无需写
<version>) - 配置 Maven 插件默认值
- 设置资源过滤规则
2.3 Spring Boot 3.x 与 2.x 的主要区别
| 特性 | Spring Boot 2.x | Spring Boot 3.x |
|---|---|---|
| Java 最低版本 | Java 8 | Java 17 |
| Jakarta EE | javax.* | jakarta.* |
| Spring Security | WebSecurityConfigurerAdapter | SecurityFilterChain Bean |
| AOT 编译 | 不支持 | 支持 GraalVM |
重要:本项目使用
jakarta.servlet.*而非javax.servlet.*,这是 Spring Boot 3.x 的重大变化。
3. Spring Boot 3.x 核心知识
3.1 自动配置原理
Spring Boot 的核心是自动配置(Auto-Configuration)。当你引入一个 Starter 依赖,Spring Boot 会自动配置相关 Bean。
原理链路:
@SpringBootApplication
└── @EnableAutoConfiguration
└── AutoConfigurationImportSelector
└── 读取 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
└── 按条件注册 Bean(@ConditionalOnClass、@ConditionalOnMissingBean 等)
项目入口:
// DocumindApplication.java
@SpringBootApplication
public class DocumindApplication {
public static void main(String[] args) {
SpringApplication.run(DocumindApplication.class, args);
}
}
3.2 常用注解详解
@Configuration
标记一个类为配置类,等价于 XML 中的 <beans>。
@Configuration
public class VectorStoreConfig {
@Bean
public VectorStore vectorStore(EmbeddingModel embeddingModel) {
return SimpleVectorStore.builder(embeddingModel).build();
}
}
知识点:@Bean 方法的参数会自动从 Spring 容器中注入,这叫方法参数注入。
@Value 注入配置
@Value("${documind.vector.store.path:./data/vector-store.json}")
private String vectorStorePath;
语法:${配置键:默认值},冒号后面是默认值,当配置文件中没有该键时使用。
@Component 系列注解
| 注解 | 语义 | 使用场景 |
|---|---|---|
@Component |
通用组件 | 工具类、辅助类 |
@Service |
业务服务 | Service 层 |
@Repository |
数据访问 | DAO 层(MyBatis-Plus 用 @Mapper) |
@Controller |
MVC 控制器 | 返回视图 |
@RestController |
REST 控制器 | 返回 JSON,= @Controller + @ResponseBody |
3.3 依赖注入方式对比
项目中大量使用构造器注入(推荐方式):
@Service
@RequiredArgsConstructor // Lombok 生成构造器
public class ChatService {
private final AgentMapper agentMapper; // final 字段
private final ChatHistoryService historyService;
// Lombok 自动生成包含所有 final 字段的构造器
}
三种注入方式对比:
| 方式 | 写法 | 优缺点 |
|---|---|---|
| 字段注入 | @Autowired private XxxService xxx |
简洁但不推荐,无法做 null 检查 |
| Setter 注入 | @Autowired public void setXxx(...) |
可选依赖时使用 |
| 构造器注入 | @RequiredArgsConstructor + final |
推荐,依赖不可变,便于测试 |
4. Spring Security + JWT 认证体系
4.1 整体认证流程
客户端请求
↓
JwtAuthenticationFilter(OncePerRequestFilter)
↓ 提取并验证 JWT
SecurityContextHolder.setAuthentication(authToken)
↓
SecurityFilterChain 授权检查
↓
Controller 处理请求
4.2 SecurityConfig 详解
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // 开启方法级别安全(@PreAuthorize 等)
@RequiredArgsConstructor
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable) // 禁用 CSRF(JWT 无状态不需要)
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll() // 白名单
.anyRequest().authenticated() // 其余需认证
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态
)
.addFilterBefore(jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class); // 插入 JWT 过滤器
return http.build();
}
}
知识点:
SessionCreationPolicy.STATELESS:不创建 Session,每次请求都通过 JWT 验证addFilterBefore:将 JWT 过滤器插入到默认用户名密码过滤器之前
4.3 JWT 工具类详解
JWT(JSON Web Token)由三部分组成:Header.Payload.Signature
// 生成 Token
public String generateToken(Long userId, String username) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId); // 自定义载荷:存入 userId
return Jwts.builder()
.claims(claims)
.subject(username) // 标准载荷:sub(主题)
.issuedAt(new Date()) // 标准载荷:iat(签发时间)
.expiration(expiryDate) // 标准载荷:exp(过期时间)
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
JWT 三部分:
- Header:算法类型(HS256)和令牌类型(JWT)
- Payload:载荷,存放用户信息(userId、username、过期时间等)
- Signature:签名,防止篡改
4.4 JWT 过滤器实现
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain) {
String authHeader = request.getHeader("Authorization");
// Bearer token 格式:Authorization: Bearer eyJhbGci...
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
String jwt = authHeader.substring(7); // 去掉 "Bearer " 前缀
String username = jwtUtil.extractUsername(jwt);
Long userId = jwtUtil.extractUserId(jwt);
if (username != null && SecurityContextHolder.getContext()
.getAuthentication() == null) {
if (jwtUtil.validateToken(jwt, username)) {
// 构建认证对象,放入 SecurityContext
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
}
优化点:本项目直接从 JWT 中提取 userId,无需查询数据库,减少了每次请求的数据库压力。
4.5 CORS 跨域配置
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(List.of("*")); // 允许所有源
configuration.setAllowedMethods(Arrays.asList(
"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"));
configuration.setAllowCredentials(true); // 允许携带 Cookie
configuration.setMaxAge(3600L); // 预检请求缓存 1 小时
// ...
}
知识点:setAllowedOriginPatterns("*") 与 setAllowedOrigins("*") 的区别:前者支持 allowCredentials(true),后者不支持。
5. MyBatis-Plus 数据访问层
5.1 MyBatis-Plus 简介
MyBatis-Plus(MP)是 MyBatis 的增强工具,在 MyBatis 基础上只做增强不做改变,提供了大量开箱即用的 CRUD 方法。
5.2 实体类注解
@TableName("agent") // 对应数据库表名
@Data // Lombok:生成 getter/setter/toString/equals/hashCode
@NoArgsConstructor // Lombok:无参构造器
@AllArgsConstructor // Lombok:全参构造器
public class Agent {
@TableId(value = "id", type = IdType.AUTO) // 主键,自增
private Long id;
@TableField("user_id") // 对应数据库列名(驼峰自动映射可省略)
private Long userId;
@TableField("system_prompt")
private String systemPrompt;
}
IdType 枚举:
| 值 | 说明 |
|---|---|
AUTO |
数据库自增 |
ASSIGN_ID |
雪花算法生成 Long 型 ID |
ASSIGN_UUID |
UUID |
INPUT |
手动输入 |
5.3 Mapper 接口
@Mapper
public interface AgentMapper extends BaseMapper<Agent> {
// BaseMapper 已提供:insert、deleteById、updateById、selectById
// selectList、selectPage、selectCount 等基础方法
// 自定义方法(需要在 XML 或用注解实现)
void incrementDocumentCount(Long agentId);
void decrementDocumentCount(Long agentId);
}
BaseMapper 常用方法:
// 插入
int insert(T entity);
// 根据 ID 删除
int deleteById(Serializable id);
// 根据条件删除
int delete(Wrapper<T> queryWrapper);
// 根据 ID 更新
int updateById(T entity);
// 根据 ID 查询
T selectById(Serializable id);
// 条件查询单条
T selectOne(Wrapper<T> queryWrapper);
// 条件查询列表
List<T> selectList(Wrapper<T> queryWrapper);
// 分页查询
IPage<T> selectPage(IPage<T> page, Wrapper<T> queryWrapper);
// 统计数量
Long selectCount(Wrapper<T> queryWrapper);
5.4 LambdaQueryWrapper 条件构造器
// 等值查询
new LambdaQueryWrapper<Agent>()
.eq(Agent::getUserId, userId) // WHERE user_id = ?
// 模糊查询
.like(Agent::getName, keyword) // WHERE name LIKE '%keyword%'
// 多条件 OR
.and(w -> w.like(Agent::getName, keyword)
.or()
.like(Agent::getRoleName, keyword))
// 排序
.orderByDesc(Agent::getCreatedAt) // ORDER BY created_at DESC
// 不等于
.ne(User::getId, user.getId()) // WHERE id != ?
为什么用 Lambda 方式:避免硬编码字段名字符串,重构时编译器能检查错误。
5.5 分页插件配置
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
PaginationInnerInterceptor paginationInterceptor =
new PaginationInnerInterceptor(DbType.MYSQL);
paginationInterceptor.setMaxLimit(500L); // 单页最大 500 条
interceptor.addInnerInterceptor(paginationInterceptor);
return interceptor;
}
}
使用分页:
// 创建分页对象:第 page 页,每页 pageSize 条
Page<Agent> pageParam = new Page<>(page, pageSize);
IPage<Agent> result = agentMapper.selectPage(pageParam, wrapper);
result.getRecords(); // 当前页数据
result.getTotal(); // 总记录数
result.getPages(); // 总页数
result.getCurrent(); // 当前页码
6. Spring AI Alibaba 与大模型集成
6.1 Spring AI 核心概念
Spring AI 是 Spring 官方的 AI 集成框架,提供统一的 API 对接各大 AI 服务商。
核心抽象:
| 接口 | 说明 |
|---|---|
ChatModel |
对话模型接口 |
EmbeddingModel |
向量嵌入模型接口 |
ChatClient |
高级对话客户端(推荐使用) |
VectorStore |
向量存储接口 |
6.2 DashScope 配置
# application-dev.yml
spring:
ai:
dashscope:
api-key: ${DASHSCOPE_API_KEY} # 从环境变量读取
chat:
options:
model: qwen-plus
temperature: 0.7
embedding:
options:
model: text-embedding-v2 # 向量化模型
通义千问模型对比:
| 模型 | 特点 | 适用场景 |
|---|---|---|
| qwen-turbo | 速度最快,成本最低 | 简单问答 |
| qwen-plus | 均衡,项目默认 | 通用场景 |
| qwen-max | 能力最强,成本最高 | 复杂推理 |
6.3 ChatClient 使用方式
// 注入 ChatClient.Builder(Spring AI 自动配置)
private final ChatClient.Builder chatClientBuilder;
// 同步调用
ChatClient chatClient = chatClientBuilder.build();
ChatResponse response = chatClient.prompt(prompt).call().chatResponse();
String text = response.getResult().getOutput().getText();
// 流式调用(返回 Flux<String>)
Flux<String> stream = chatClient.prompt(prompt).stream().content();
6.4 动态参数配置
项目中每个智能体可以独立配置 AI 参数:
DashScopeChatOptions chatOptions = DashScopeChatOptions.builder()
.withModel(agent.getModelName()) // 动态模型
.withTemperature(agent.getTemperature().doubleValue()) // 动态温度
.withTopP(agent.getTopP().doubleValue()) // 动态 TopP
.build();
Prompt prompt = new Prompt(messages, chatOptions);
参数说明:
- temperature(温度):0~1,越高回复越随机有创意,越低越确定保守
- top_p:核采样,控制词汇多样性,通常与 temperature 二选一调整
- max_tokens:最大输出 token 数,控制回复长度
6.5 消息类型
List<Message> messages = new ArrayList<>();
// 系统消息:定义 AI 角色和行为规则
messages.add(new SystemMessage("你是一个专业的医生..."));
// 用户消息:用户输入
messages.add(new UserMessage("我最近头疼怎么办?"));
// 助手消息:AI 历史回复(用于多轮对话上下文)
messages.add(new AssistantMessage("建议您多休息..."));
多轮对话原理:大模型本身无记忆,每次请求都需要把历史对话一起发送,模型才能"记住"上下文。
6.6 Token 消耗统计
// 同步调用获取 token 信息
ChatResponse response = chatClient.prompt(prompt).call().chatResponse();
Integer totalTokens = response.getMetadata().getUsage().getTotalTokens();
// 流式调用无法直接获取 token,项目中用估算方式
int estimatedTokens = (int) (completeReply.length() * 0.7);
// 估算规则:中文约 1.5 字符/token,英文约 4 字符/token
7. RAG 检索增强生成
7.1 什么是 RAG
RAG(Retrieval-Augmented Generation,检索增强生成)是一种将知识库检索与大模型生成结合的技术。
解决的问题:
- 大模型训练数据有截止日期,不了解最新信息
- 大模型不了解用户的私有文档
- 大模型可能"幻觉"(编造不存在的信息)
RAG 流程:
用户问题
↓
向量化问题(Embedding)
↓
在向量库中检索相似文档片段(Top-K)
↓
将文档片段 + 用户问题组合成 Prompt
↓
发送给大模型生成回答
↓
返回基于文档的准确回答
7.2 项目中的 RAG 实现
// ChatService.chatStream() 中的 RAG 逻辑
if (agent.getRagEnabled() == 1) {
List<Document> relevantDocs = vectorService.searchRelevantDocuments(
userMessage,
agentId,
agent.getRagTopK(), // 检索 Top-K 个片段
agent.getRagSimilarityThreshold().doubleValue() // 相似度阈值
);
if (!relevantDocs.isEmpty()) {
StringBuilder contextBuilder = new StringBuilder("\n\n【参考文档】\n");
for (int i = 0; i < relevantDocs.size(); i++) {
Document doc = relevantDocs.get(i);
contextBuilder.append(String.format("%d. 来源:%s\n内容:%s\n\n",
i + 1,
doc.getMetadata().get("file_name"),
doc.getText()
));
}
// 将检索到的文档内容追加到用户消息后面
finalMessage = userMessage + contextBuilder.toString();
}
}
7.3 RAG 参数调优
| 参数 | 项目默认值 | 说明 |
|---|---|---|
rag_top_k |
10 | 检索最相关的 10 个文档片段 |
rag_similarity_threshold |
0.50 | 相似度低于 0.5 的片段不返回 |
调优建议:
top_k越大,召回越全面,但 Prompt 越长,成本越高similarity_threshold越高,精度越高,但可能漏掉相关内容- 通常先调低阈值(0.3~0.5)确保召回,再根据效果调整
7.4 数据隔离设计
项目中多个智能体共用一个向量存储,通过 agent_id 实现数据隔离:
// 向量化时写入 agent_id
metadata.put("agent_id", document.getAgentId().toString());
// 检索时手动过滤
List<Document> filteredResults = allResults.stream()
.filter(doc -> {
Object agentIdObj = doc.getMetadata().get("agent_id");
return agentIdObj != null &&
agentIdObj.toString().equals(agentId.toString());
})
.limit(topK)
.toList();
注意:SimpleVectorStore 不支持原生 metadata 过滤,所以先多查(topK * 2),再手动过滤。生产环境建议升级到 Qdrant 等支持过滤的向量数据库。
8. 向量存储与文档处理
8.1 向量化原理
向量(Embedding):将文本转换为高维数值向量,语义相近的文本在向量空间中距离更近。
"我头疼" → [0.12, -0.34, 0.89, ...] (1536 维向量)
"头部疼痛" → [0.11, -0.33, 0.91, ...] (距离很近,语义相似)
"今天天气好" → [-0.45, 0.67, -0.23, ...] (距离很远,语义不同)
8.2 文档向量化流程
@Async // 异步执行,不阻塞上传接口
public void vectorizeDocument(Long documentId) {
// 1. 读取文件
DocumentReader reader;
if ("xls".equals(fileType) || "xlsx".equals(fileType)) {
reader = new ExcelReader(new FileSystemResource(file));
} else {
reader = new TextReader(new FileSystemResource(file));
}
List<Document> documents = reader.get();
// 2. 文档分块(Chunking)
TokenTextSplitter splitter = new TokenTextSplitter(
500, // chunkSize:每块最大 500 token
50, // chunkOverlap:相邻块重叠 50 token(保持上下文连贯)
5, // minChunkSizeChars:最小块字符数
10000, // maxChunkSizeChars:最大块字符数
true // keepSeparator:保留分隔符
);
List<Document> chunks = splitter.apply(documents);
// 3. 添加 metadata
for (int i = 0; i < chunks.size(); i++) {
chunks.get(i).getMetadata().put("agent_id", agentId.toString());
chunks.get(i).getMetadata().put("file_name", fileName);
chunks.get(i).getMetadata().put("chunk_index", i);
}
// 4. 存入向量库(自动调用 EmbeddingModel 生成向量)
vectorStore.add(chunks);
// 5. 持久化到文件
simpleVectorStore.save(vectorFile);
}
分块(Chunking)的重要性:
- 文档太长无法直接向量化(有 token 限制)
- 分块后每块语义更集中,检索更精准
- 重叠(overlap)确保跨块的信息不丢失
8.3 SimpleVectorStore 配置
@Bean
public VectorStore vectorStore(EmbeddingModel embeddingModel) {
SimpleVectorStore vectorStore = SimpleVectorStore.builder(embeddingModel).build();
File vectorFile = new File(vectorStorePath);
if (vectorFile.exists()) {
vectorStore.load(vectorFile); // 启动时从文件加载已有向量
}
return vectorStore;
}
SimpleVectorStore 特点:
- 数据存储在内存中,重启后从 JSON 文件恢复
- 相似度搜索使用余弦相似度
- 适合开发和小规模应用(< 10 万条向量)
- 生产环境建议使用 Qdrant、Milvus、Pinecone 等
8.4 向量状态机
文档向量化有 4 个状态:
0(待处理)→ 1(处理中)→ 2(已完成)
↘ 3(失败)
// 状态常量
document.setVectorStatus(0); // 待处理(上传后初始状态)
document.setVectorStatus(1); // 处理中(开始向量化)
document.setVectorStatus(2); // 已完成(向量化成功)
document.setVectorStatus(3); // 失败(记录错误信息)
8.5 并发安全处理
多个文档同时向量化时,保存向量文件需要加锁:
private final Object saveLock = new Object(); // 对象锁
synchronized (saveLock) { // 同一时刻只有一个线程保存文件
simpleVectorStore.save(vectorFile);
}
9. 流式响应与 SSE
9.1 什么是 SSE
SSE(Server-Sent Events,服务器推送事件)是一种服务器向客户端单向推送数据的技术。
与 WebSocket 对比:
| 特性 | SSE | WebSocket |
|---|---|---|
| 方向 | 单向(服务器→客户端) | 双向 |
| 协议 | HTTP | WS |
| 断线重连 | 自动 | 需手动 |
| 适用场景 | AI 流式输出、通知 | 实时聊天、游戏 |
9.2 项目中的流式实现
// Controller:声明返回 SSE 格式
@PostMapping(value = "/stream",
produces = MediaType.TEXT_EVENT_STREAM_VALUE) // 关键:声明 SSE
public Flux<String> sendMessageStream(
@RequestBody ChatRequest request,
Authentication authentication) {
return chatService.chatStream(...);
}
// Service:返回 Reactor Flux
public Flux<String> chatStream(...) {
return chatClient.prompt(prompt)
.stream()
.content() // 每个 token 作为一个事件
.doOnNext(chunk -> {
fullReply.append(chunk); // 收集完整回复
})
.doOnComplete(() -> {
// 流结束后保存到数据库
historyService.saveMessage(..., fullReply.toString(), ...);
})
.doOnError(error -> {
// 错误处理
});
}
9.3 Project Reactor 基础
Spring WebFlux 使用 Project Reactor 实现响应式编程:
| 类型 | 说明 | 类比 |
|---|---|---|
Mono<T> |
0 或 1 个元素的异步序列 | Optional |
Flux<T> |
0 到 N 个元素的异步序列 | Stream |
常用操作符:
Flux<String> flux = Flux.just("a", "b", "c");
flux.map(s -> s.toUpperCase()) // 转换每个元素
.filter(s -> !s.equals("B")) // 过滤
.doOnNext(s -> log.info(s)) // 副作用(不改变元素)
.doOnComplete(() -> log.info("完成"))
.doOnError(e -> log.error(e.getMessage()));
9.4 AtomicReference 线程安全
流式处理中,doOnNext 和 doOnComplete 可能在不同线程执行,需要线程安全的变量:
// 错误写法(非线程安全)
StringBuilder fullReply = new StringBuilder();
// 正确写法(线程安全引用)
AtomicReference<Long> selectedTemplateId = new AtomicReference<>(null);
// 在 doOnComplete 中安全读取
Long templateId = selectedTemplateId.get();
10. AOP 切面编程
10.1 AOP 核心概念
AOP(Aspect-Oriented Programming,面向切面编程)用于将横切关注点(日志、监控、事务等)从业务逻辑中分离。
核心术语:
| 术语 | 说明 | 项目示例 |
|---|---|---|
| Aspect(切面) | 横切关注点的模块化 | ApiMonitorAspect |
| JoinPoint(连接点) | 程序执行的某个点 | Controller 方法执行 |
| Pointcut(切点) | 匹配连接点的表达式 | execution(* cn.edu.cdu.documind.controller..*(..)) |
| Advice(通知) | 切面在连接点执行的动作 | @Around |
10.2 ApiMonitorAspect 详解
@Slf4j
@Aspect // 声明为切面
@Component // 注册为 Spring Bean
@RequiredArgsConstructor
public class ApiMonitorAspect {
// 切点表达式:拦截 controller 包下所有类的所有方法
@Around("execution(* cn.edu.cdu.documind.controller..*(..))")
public Object monitorApi(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
try {
Object result = joinPoint.proceed(); // 执行目标方法
return result;
} catch (Exception e) {
throw e;
} finally {
long responseTime = System.currentTimeMillis() - startTime;
// 异步保存日志(不影响主流程性能)
saveApiLog(endpoint, method, responseStatus, responseTime, ...);
}
}
}
10.3 切点表达式语法
execution(修饰符 返回类型 包名.类名.方法名(参数))
execution(* cn.edu.cdu.documind.controller..*(..))
↑ ↑ ↑ ↑
| | | └── 任意参数
| | └── 任意方法名
| └── 包名(.. 表示包含子包)
└── 任意返回类型
10.4 异步日志的线程安全问题
问题:@Async 方法在新线程中执行,此时 HttpServletRequest 和 SecurityContext 已失效。
解决方案:在主线程中提前提取所有数据,再传给异步方法:
// ✅ 正确:在主线程(finally 块)中提取数据
String requestParams = getRequestParams(request); // 主线程提取
String ipAddress = getClientIp(request); // 主线程提取
Long userId = getCurrentUserId(); // 主线程提取
// 传递基本类型,不传 request 对象
saveApiLog(endpoint, method, responseStatus, responseTime,
requestParams, ipAddress, userId); // 异步方法
// ❌ 错误:在异步方法中访问 request(已失效)
@Async
public void saveApiLog(HttpServletRequest request) {
request.getParameter("xxx"); // 报错!request 已关闭
}
10.5 编程式事务(TransactionTemplate)
异步方法中不能使用 @Transactional(因为事务绑定到线程),需要用编程式事务:
@Autowired
private TransactionTemplate transactionTemplate;
@Async
public void saveApiLog(...) {
transactionTemplate.execute(status -> {
try {
apiCallLogMapper.insert(log);
return null;
} catch (Exception e) {
// 日志保存失败不影响主流程,不回滚
return null;
}
});
}
11. 异步任务与事务管理
11.1 @Async 异步执行
@Configuration
@EnableAsync // 开启异步支持
public class AsyncConfig {
@Bean
public TransactionTemplate transactionTemplate(
PlatformTransactionManager transactionManager) {
return new TransactionTemplate(transactionManager);
}
}
@Async // 标记为异步方法,在新线程中执行
@Transactional
public void vectorizeDocument(Long documentId) {
// 这个方法在独立线程中执行,不阻塞调用方
}
注意事项:
@Async方法必须是public的- 不能在同一个类中调用自己的
@Async方法(会绕过代理) - 默认使用 Spring 的
SimpleAsyncTaskExecutor,生产环境建议配置线程池
11.2 事务传播机制
@Transactional(rollbackFor = Exception.class)
public DocumentResponse uploadDocument(...) {
// 保存文档记录到数据库
documentMapper.insert(document);
// 注册事务提交后的回调
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCommit() {
// 事务提交后才启动向量化
// 确保数据库中已有文档记录
vectorService.vectorizeDocument(document.getId());
}
}
);
}
为什么要在事务提交后启动向量化:
- 向量化是异步的,可能在事务提交前就开始执行
- 如果向量化先执行,查询
documentId时数据库中还没有这条记录 - 使用
afterCommit回调确保数据已持久化
11.3 @Transactional 常用属性
@Transactional(
rollbackFor = Exception.class, // 遇到任何异常都回滚(默认只回滚 RuntimeException)
readOnly = true, // 只读事务,优化查询性能
propagation = Propagation.REQUIRED, // 默认:有事务就加入,没有就新建
timeout = 30 // 超时时间(秒)
)
传播行为:
| 传播行为 | 说明 |
|---|---|
REQUIRED |
默认,有事务加入,没有新建 |
REQUIRES_NEW |
总是新建事务,挂起当前事务 |
SUPPORTS |
有事务加入,没有就不用事务 |
NOT_SUPPORTED |
不使用事务,挂起当前事务 |
12. 全局异常处理
12.1 @RestControllerAdvice
@Slf4j
@RestControllerAdvice // = @ControllerAdvice + @ResponseBody
public class GlobalExceptionHandler {
// 处理参数校验异常
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST) // 返回 400 状态码
public Result<Void> handleValidationException(MethodArgumentNotValidException e) {
String errorMessage = e.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining("; "));
return Result.error(400, errorMessage);
}
// 处理业务异常
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<Void> handleIllegalArgumentException(IllegalArgumentException e) {
return Result.error(400, e.getMessage());
}
// 处理权限异常
@ExceptionHandler(SecurityException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public Result<Void> handleSecurityException(SecurityException e) {
return Result.error(403, e.getMessage());
}
// 兜底处理
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Result<Void> handleException(Exception e) {
log.error("系统异常: ", e);
return Result.error(500, "系统错误,请稍后重试");
}
}
12.2 异常处理优先级
Spring 会选择最精确匹配的 @ExceptionHandler:
MethodArgumentNotValidException(最精确)
↓ 不匹配
IllegalArgumentException
↓ 不匹配
RuntimeException
↓ 不匹配
Exception(最宽泛,兜底)
12.3 参数校验注解
// 在 DTO 上使用校验注解
public class LoginRequest {
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 20, message = "密码长度 6-20 位")
private String password;
@Email(message = "邮箱格式不正确")
private String email;
}
// 在 Controller 中触发校验
@PostMapping("/login")
public Result<LoginResponse> login(@Valid @RequestBody LoginRequest request) {
// @Valid 触发校验,失败时抛出 MethodArgumentNotValidException
}
常用校验注解:
| 注解 | 说明 |
|---|---|
@NotNull |
不为 null |
@NotBlank |
不为空字符串(去空格后) |
@NotEmpty |
不为空集合/字符串 |
@Size(min,max) |
长度范围 |
@Min / @Max |
数值范围 |
@Email |
邮箱格式 |
@Pattern(regexp) |
正则表达式 |
13. 数据库设计
13.1 数据库表关系
user(用户)
└── agent(智能体,多对一 user)
├── document(文档,多对一 agent)
├── chat_session(会话,多对一 agent)
│ └── chat_history(对话历史,多对一 session)
└── prompt_template(提示词模板,多对一 agent)
13.2 核心表设计详解
user 表
CREATE TABLE `user` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`username` VARCHAR(50) NOT NULL,
`password` VARCHAR(255) NOT NULL, -- BCrypt 加密,固定 60 字符
`email` VARCHAR(100),
`nickname` VARCHAR(50),
`role` VARCHAR(20) DEFAULT 'user', -- admin/user
`status` TINYINT DEFAULT 1, -- 0-禁用 1-启用
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY `uk_username` (`username`)
);
设计要点:
password用 BCrypt 加密,长度固定 60 字符,VARCHAR(255) 留余量ON UPDATE CURRENT_TIMESTAMP自动更新修改时间username唯一索引防止重复注册
agent 表(核心表)
-- AI 模型配置
`model_name` VARCHAR(50) DEFAULT 'qwen-plus',
`temperature` DECIMAL(3,2) DEFAULT 0.70, -- 精度:3位数字,2位小数
`top_p` DECIMAL(3,2) DEFAULT 0.90,
-- RAG 配置
`rag_enabled` TINYINT DEFAULT 1,
`rag_top_k` INT DEFAULT 10,
`rag_similarity_threshold` DECIMAL(3,2) DEFAULT 0.50,
-- 统计字段(冗余设计,避免频繁 COUNT 查询)
`document_count` INT DEFAULT 0,
`chat_count` INT DEFAULT 0,
`total_tokens` BIGINT DEFAULT 0,
冗余统计字段的权衡:
- 优点:查询智能体详情时无需 JOIN 或子查询,性能好
- 缺点:需要在业务代码中维护一致性(增删时同步更新)
chat_history 表
CREATE TABLE `chat_history` (
`session_id` VARCHAR(100) NOT NULL, -- 会话 UUID
`role` VARCHAR(20) NOT NULL, -- user/assistant
`content` TEXT NOT NULL, -- 消息内容
`sources` JSON, -- 来源文档(JSON 数组)
`tokens` INT, -- token 消耗
KEY `idx_session_id` (`session_id`) -- 按会话查询的索引
);
JSON 类型字段:MySQL 5.7+ 支持 JSON 类型,可以存储结构化数据,支持 JSON 函数查询。
13.3 索引设计原则
-- 高频查询字段建索引
KEY `idx_user_id` (`user_id`) -- 按用户查智能体
KEY `idx_session_id` (`session_id`) -- 按会话查历史
KEY `idx_agent_user` (`agent_id`, `user_id`) -- 联合索引
-- 唯一约束
UNIQUE KEY `uk_username` (`username`)
UNIQUE KEY `uk_session_id` (`session_id`)
索引原则:
- WHERE 条件字段、JOIN 字段、ORDER BY 字段考虑建索引
- 联合索引遵循最左前缀原则
- 不要过度建索引(影响写入性能)
14. 业务模块详解
14.1 用户认证模块
注册流程:
POST /api/auth/register
↓
AuthService.register()
↓
检查用户名/邮箱唯一性(LambdaQueryWrapper)
↓
BCrypt 加密密码(passwordEncoder.encode)
↓
User.builder() 构建实体
↓
userMapper.insert(user)
↓
返回 UserResponse(不含密码)
登录流程:
POST /api/auth/login
↓
AuthService.login()
↓
查询用户(selectOne by username)
↓
检查状态(status == 0 则禁用)
↓
验证密码(passwordEncoder.matches)
↓
生成 JWT(jwtUtil.generateToken(userId, username))
↓
返回 LoginResponse { accessToken, tokenType, expiresIn, user }
14.2 智能体管理模块
创建智能体:
// AgentService.createAgent()
Agent agent = new Agent();
agent.setUserId(userId); // 绑定用户
agent.setSystemPrompt(request.getSystemPrompt()); // 系统提示词
agent.setModelName(request.getModelName()); // AI 模型
agent.setRagEnabled(request.getRagEnabled()); // RAG 开关
// ... 其他配置
agentMapper.insert(agent);
分页查询:
// 支持:关键词搜索 + 状态筛选 + 多字段排序 + 分页
LambdaQueryWrapper<Agent> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Agent::getUserId, userId);
if (keyword != null) {
wrapper.and(w -> w.like(Agent::getName, keyword)
.or()
.like(Agent::getRoleName, keyword));
}
if (status != null) {
wrapper.eq(Agent::getStatus, status);
}
// Java 17 switch 表达式
switch (sortBy) {
case "name" -> wrapper.orderBy(true, isAsc, Agent::getName);
case "chatCount" -> wrapper.orderBy(true, isAsc, Agent::getChatCount);
default -> wrapper.orderBy(true, isAsc, Agent::getCreatedAt);
}
14.3 文档管理模块
上传文档完整流程:
1. 验证文件(非空、大小、类型)
2. 验证智能体权限(agent.userId == 当前用户)
3. 生成唯一文件名(UUID + 原始文件名)
4. 创建目录(按 agentId 分目录存储)
5. 保存文件到磁盘(file.transferTo)
6. 插入 document 记录(vectorStatus=0)
7. 更新 agent.documentCount + 1
8. 注册事务提交后回调,启动异步向量化
文件目录结构:
./data/uploads/
├── 5/ ← agentId=5 的文档
│ ├── uuid1_健康档案.txt
│ └── uuid2_体检报告.txt
└── 6/ ← agentId=6 的文档
└── uuid3_食谱.md
14.4 多提示词模板模块
PromptSelectorService 选择逻辑:
// 根据用户消息内容,从模板列表中选择最匹配的模板
public PromptTemplate selectBestTemplate(Long agentId, String userMessage) {
// 1. 查询该智能体所有启用的模板(按优先级降序)
List<PromptTemplate> templates = promptTemplateMapper.selectList(
new LambdaQueryWrapper<PromptTemplate>()
.eq(PromptTemplate::getAgentId, agentId)
.eq(PromptTemplate::getIsActive, 1)
.orderByDesc(PromptTemplate::getPriority)
);
// 2. 遍历模板,检查触发条件
for (PromptTemplate template : templates) {
if (matchesTriggerCondition(template, userMessage)) {
return template; // 返回第一个匹配的(优先级最高)
}
}
return null; // 无匹配,使用默认系统提示词
}
触发条件 JSON 格式:
{
"keywords": ["急性", "剧烈", "疼痛"],
"has_code_block": true,
"message_length": {"min": 100, "max": 1000}
}
15. API 接口设计规范
15.1 RESTful 设计原则
项目遵循 RESTful 风格:
| HTTP 方法 | 语义 | 示例 |
|---|---|---|
| GET | 查询 | GET /api/agents 获取列表 |
| POST | 创建 | POST /api/agents 创建智能体 |
| PUT | 全量更新 | PUT /api/agents/{id} 更新智能体 |
| PATCH | 部分更新 | PATCH /api/agents/{id}/status 切换状态 |
| DELETE | 删除 | DELETE /api/agents/{id} 删除智能体 |
15.2 统一响应格式
// Result<T> 泛型响应类
{
"code": 200, // 业务状态码
"message": "success",
"data": { ... } // 泛型数据
}
// 错误响应
{
"code": 400,
"message": "用户名不能为空",
"data": null
}
// 使用方式
return Result.success(agentResponse); // 200 + 数据
return Result.success("创建成功", agentResponse); // 200 + 自定义消息 + 数据
return Result.error(400, "参数错误"); // 400 + 错误消息
return Result.error(403, "无权访问"); // 403 + 错误消息
15.3 Swagger 文档注解
@Tag(name = "智能体管理", description = "智能体的增删改查接口")
@RestController
public class AgentController {
@Operation(summary = "创建智能体", description = "创建一个新的智能体")
@PostMapping
public Result<AgentResponse> createAgent(...) { }
@Operation(summary = "分页查询智能体")
@GetMapping
public Result<PageResponse<AgentResponse>> getAgentPage(
@Parameter(description = "页码(从1开始)")
@RequestParam(defaultValue = "1") Integer page,
...
) { }
}
访问地址:http://localhost:8080/swagger-ui.html
15.4 分页响应封装
// PageResponse<T> 通用分页响应
@Data
@Builder
public class PageResponse<T> {
private List<T> items; // 当前页数据
private Long total; // 总记录数
private Integer page; // 当前页码
private Integer pageSize; // 每页数量
private Integer totalPages; // 总页数
}
15.5 SecurityUtil 获取当前用户
// 在 Controller 中获取当前登录用户 ID
@GetMapping("/list")
public Result<List<AgentResponse>> getAgentList(Authentication authentication) {
Long userId = SecurityUtil.getUserId(authentication);
// ...
}
// SecurityUtil 实现
public class SecurityUtil {
public static Long getUserId(Authentication authentication) {
if (authentication == null) return null;
CustomUserDetails userDetails =
(CustomUserDetails) authentication.getPrincipal();
return userDetails.getUserId();
}
}
16. 配置管理与多环境
16.1 多环境配置
# application.yml(主配置,通用配置)
spring:
profiles:
active: dev # 激活 dev 环境
# application-dev.yml(开发环境)
spring:
datasource:
url: jdbc:mysql://localhost:3306/documind_ai
username: health
password: health
# application-prod.yml(生产环境,如果有)
spring:
datasource:
url: jdbc:mysql://prod-server:3306/documind_ai
激活方式:
# 方式1:配置文件
spring.profiles.active=dev
# 方式2:启动参数
java -jar app.jar --spring.profiles.active=prod
# 方式3:环境变量
SPRING_PROFILES_ACTIVE=prod
16.2 敏感配置用环境变量
# 不要把 API Key 写死在配置文件中
spring:
ai:
dashscope:
api-key: ${DASHSCOPE_API_KEY} # 从环境变量读取
设置环境变量:
# Linux/Mac
export DASHSCOPE_API_KEY=sk-xxxxx
# Windows
set DASHSCOPE_API_KEY=sk-xxxxx
# IDEA 运行配置:Edit Configurations → Environment variables
16.3 HikariCP 连接池配置
spring:
datasource:
hikari:
maximum-pool-size: 10 # 最大连接数
minimum-idle: 5 # 最小空闲连接
connection-timeout: 30000 # 获取连接超时(毫秒)
HikariCP 是 Spring Boot 默认的连接池,以高性能著称。
16.4 MyBatis-Plus 全局配置
mybatis-plus:
configuration:
map-underscore-to-camel-case: true # 下划线转驼峰(user_id → userId)
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印 SQL
global-config:
db-config:
id-type: auto # 全局主键策略:自增
下划线转驼峰:数据库字段 user_id 自动映射到 Java 字段 userId,无需手动配置 @TableField(但项目中仍然写了,更清晰)。
16.5 文件上传配置
spring:
servlet:
multipart:
max-file-size: 50MB # 单文件最大 50MB
max-request-size: 50MB # 请求体最大 50MB
enabled: true
17. Lombok 使用指南
17.1 项目中使用的 Lombok 注解
| 注解 | 作用 | 使用场景 |
|---|---|---|
@Data |
getter + setter + toString + equals + hashCode | 实体类、DTO |
@Builder |
建造者模式 | 创建复杂对象 |
@NoArgsConstructor |
无参构造器 | 实体类(MyBatis 需要) |
@AllArgsConstructor |
全参构造器 | 实体类 |
@RequiredArgsConstructor |
final 字段构造器 | Service(依赖注入) |
@Slf4j |
注入 log 日志对象 |
需要打日志的类 |
17.2 @Builder 使用示例
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Long id;
private String username;
private String email;
}
// 使用 Builder 创建对象(链式调用,清晰易读)
User user = User.builder()
.username("admin")
.email("admin@example.com")
.role("admin")
.status(1)
.createdAt(LocalDateTime.now())
.build();
注意:@Builder 与 @NoArgsConstructor + @AllArgsConstructor 同时使用时,需要三个注解都写,否则 MyBatis 反射创建对象会失败。
17.3 @RequiredArgsConstructor 原理
// 写法
@RequiredArgsConstructor
public class ChatService {
private final AgentMapper agentMapper;
private final ChatHistoryService historyService;
}
// Lombok 生成的代码
public class ChatService {
private final AgentMapper agentMapper;
private final ChatHistoryService historyService;
// 自动生成包含所有 final 字段的构造器
public ChatService(AgentMapper agentMapper,
ChatHistoryService historyService) {
this.agentMapper = agentMapper;
this.historyService = historyService;
}
}
Spring 看到只有一个构造器时,自动用它进行依赖注入(无需 @Autowired)。
17.4 @Slf4j 日志使用
@Slf4j
@Service
public class AgentService {
public AgentResponse createAgent(Long userId, AgentCreateRequest request) {
log.info("创建智能体 - 用户ID: {}, 名称: {}", userId, request.getName());
// {} 是占位符,避免字符串拼接的性能损耗
log.debug("调试信息: {}", someObject); // DEBUG 级别
log.warn("警告: {}", warningMsg); // WARN 级别
log.error("错误: ", exception); // ERROR 级别(异常用逗号)
}
}
日志级别(从低到高):TRACE < DEBUG < INFO < WARN < ERROR
配置文件中设置级别:
logging:
level:
root: INFO # 全局 INFO
cn.edu.cdu.documind: DEBUG # 项目包 DEBUG
18. Redis 缓存集成
18.1 Redis 配置
spring:
data:
redis:
host: localhost
port: 6379
password: # 无密码留空
database: 1 # 使用 DB1(与其他应用隔离)
timeout: 3000ms
lettuce: # Lettuce 是 Spring Boot 默认的 Redis 客户端
pool:
max-active: 8 # 最大连接数
max-idle: 8 # 最大空闲连接
min-idle: 0
max-wait: -1ms # 等待连接超时(-1 表示无限等待)
18.2 Redis 在项目中的作用
项目引入了 Redis 依赖,主要用于:
- 会话缓存:缓存用户会话信息,减少数据库查询
- Token 黑名单:存储已注销的 JWT Token(防止 Token 被滥用)
- 统计数据缓存:缓存智能体统计数据
18.3 RedisTemplate 基本使用
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 字符串操作
redisTemplate.opsForValue().set("key", "value");
redisTemplate.opsForValue().set("key", "value", 1, TimeUnit.HOURS); // 1小时过期
String value = (String) redisTemplate.opsForValue().get("key");
// Hash 操作(适合存储对象)
redisTemplate.opsForHash().put("user:1", "username", "admin");
redisTemplate.opsForHash().get("user:1", "username");
// 删除
redisTemplate.delete("key");
// 判断是否存在
Boolean exists = redisTemplate.hasKey("key");
// 设置过期时间
redisTemplate.expire("key", 30, TimeUnit.MINUTES);
18.4 StringRedisTemplate
// 专门处理字符串类型,更常用
@Autowired
private StringRedisTemplate stringRedisTemplate;
stringRedisTemplate.opsForValue().set("token:blacklist:" + token, "1",
24, TimeUnit.HOURS); // JWT 黑名单,24小时后自动清除
19. 文件上传与处理
19.1 MultipartFile 文件上传
// Controller 接收文件
@PostMapping("/upload")
public Result<DocumentResponse> uploadDocument(
@RequestParam("agentId") Long agentId,
@RequestParam("file") MultipartFile file, // 接收上传文件
Authentication authentication) {
// ...
}
// Service 处理文件
public DocumentResponse uploadDocument(Long agentId, Long userId,
MultipartFile file) throws IOException {
// 获取文件信息
String originalFilename = file.getOriginalFilename(); // 原始文件名
long fileSize = file.getSize(); // 文件大小(字节)
String contentType = file.getContentType(); // MIME 类型
// 生成唯一文件名(防止重名覆盖)
String uniqueFileName = UUID.randomUUID() + "_" + originalFilename;
// 保存文件
Path filePath = agentUploadPath.resolve(uniqueFileName);
file.transferTo(filePath.toFile()); // 写入磁盘
}
19.2 文件路径处理
// 处理相对路径 vs 绝对路径
File baseDir = new File(uploadBasePath); // "./data/uploads"
if (!baseDir.isAbsolute()) {
// 转换为绝对路径(相对于 JVM 工作目录)
baseDir = new File(System.getProperty("user.dir"), uploadBasePath);
}
// 按 agentId 分目录
Path agentUploadPath = Paths.get(baseDir.getAbsolutePath(), agentId.toString());
// 创建目录(包括父目录)
agentUploadPath.toFile().mkdirs();
19.3 文档解析
PDF 解析(PDFBox):
// Spring AI 的 TextReader 内部使用 PDFBox
DocumentReader reader = new TextReader(new FileSystemResource(file));
List<Document> documents = reader.get();
Excel 解析(Apache POI):
// 自定义 ExcelReader
public class ExcelReader implements DocumentReader {
@Override
public List<Document> get() {
try (Workbook workbook = WorkbookFactory.create(resource.getInputStream())) {
StringBuilder content = new StringBuilder();
for (Sheet sheet : workbook) {
for (Row row : sheet) {
for (Cell cell : row) {
content.append(cell.toString()).append("\t");
}
content.append("\n");
}
}
return List.of(new Document(content.toString()));
}
}
}
19.4 支持的文件类型
private boolean isValidFileType(String fileType) {
List<String> validTypes = List.of("pdf", "doc", "docx", "txt", "md", "xls", "xlsx");
return validTypes.contains(fileType.toLowerCase());
}
| 文件类型 | 解析方式 |
|---|---|
| txt, md | TextReader(直接读取) |
| TextReader(PDFBox 提取文本) | |
| doc, docx | TextReader(POI 提取文本) |
| xls, xlsx | ExcelReader(自定义,POI 解析) |
20. 项目架构与设计模式
20.1 三层架构
Controller(表现层)
↓ 调用
Service(业务逻辑层)
↓ 调用
Mapper(数据访问层)
↓ 操作
Database(数据库)
各层职责:
- Controller:接收请求、参数校验、调用 Service、返回响应
- Service:业务逻辑、事务管理、调用多个 Mapper
- Mapper:数据库 CRUD,不含业务逻辑
20.2 DTO 模式
项目严格区分 Entity 和 DTO:
Entity(数据库实体)← Mapper → 数据库
↓ 转换
DTO(数据传输对象)← Controller → 前端
// Entity:对应数据库表,包含所有字段(含密码等敏感字段)
public class User {
private Long id;
private String username;
private String password; // 敏感字段
// ...
}
// Response DTO:返回给前端,不含敏感字段
public class UserResponse {
private Long id;
private String username;
// 没有 password 字段!
}
// 转换
private UserResponse convertToUserResponse(User user) {
return UserResponse.builder()
.id(user.getId())
.username(user.getUsername())
// 不映射 password
.build();
}
20.3 Builder 模式
项目大量使用 Builder 模式创建对象:
// 链式调用,清晰表达每个字段的含义
LoginResponse response = LoginResponse.builder()
.accessToken(token)
.tokenType("Bearer")
.expiresIn(jwtExpiration)
.user(convertToUserResponse(user))
.build();
20.4 策略模式(多提示词选择)
PromptSelectorService 实现了策略模式:根据用户消息动态选择不同的提示词策略。
// 不同场景使用不同提示词(策略)
if (matchKeywords(message, ["急性", "剧烈"])) → 紧急风险评估提示词
if (matchKeywords(message, ["报告", "体检"])) → 体检报告分析提示词
else → 日常健康咨询提示词
20.5 观察者模式(事务同步)
TransactionSynchronization 是观察者模式的应用:
// 注册观察者:事务提交后触发
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCommit() {
// 事务提交事件发生后,执行向量化
vectorService.vectorizeDocument(document.getId());
}
}
);
20.6 Java 17 新特性应用
switch 表达式(Java 14+):
switch (sortBy) {
case "name" -> wrapper.orderBy(true, isAsc, Agent::getName);
case "updatedAt" -> wrapper.orderBy(true, isAsc, Agent::getUpdatedAt);
default -> wrapper.orderByDesc(Agent::getCreatedAt);
}
var 局部变量类型推断(Java 10+):
var agent = agentMapper.selectById(agentId); // 类型由右侧推断
var testResults = vectorStore.similaritySearch(...);
instanceof 模式匹配(Java 16+):
// 旧写法
if (vectorStore instanceof SimpleVectorStore) {
SimpleVectorStore simpleVectorStore = (SimpleVectorStore) vectorStore;
simpleVectorStore.save(vectorFile);
}
// 新写法(项目中使用)
if (vectorStore instanceof SimpleVectorStore simpleVectorStore) {
simpleVectorStore.save(vectorFile); // 直接使用,无需强转
}
21. 对话历史与会话管理
21.1 会话(Session)设计
用户
└── 智能体 A
├── 会话1(session_id: uuid-001)
│ ├── 消息1: user: "你好"
│ ├── 消息2: assistant: "你好!"
│ └── 消息3: user: "帮我分析体检报告"
└── 会话2(session_id: uuid-002)
└── 消息1: user: "最近头疼"
会话 ID 生成:使用 UUID,保证全局唯一:
String sessionId = UUID.randomUUID().toString();
21.2 多轮对话上下文管理
// 获取最近 N 条历史(避免 Prompt 过长)
List<ChatHistory> histories = historyService.getHistoryBySession(sessionId, 10);
// 构建消息列表
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage(agent.getSystemPrompt())); // 系统提示词
// 添加历史对话(保持上下文)
for (ChatHistory history : histories) {
if ("user".equals(history.getRole())) {
messages.add(new UserMessage(history.getContent()));
} else if ("assistant".equals(history.getRole())) {
messages.add(new AssistantMessage(history.getContent()));
}
}
messages.add(new UserMessage(userMessage)); // 当前问题
历史条数限制:
- 同步接口:最近 10 条
- 流式接口:最近 5 条(流式更注重实时性,减少延迟)
21.3 重新生成回答
public Flux<String> regenerateResponse(Long agentId, String sessionId, Long userId) {
List<ChatHistory> histories = historyService.getHistoryBySession(sessionId, 100);
// 从后往前找最后一条用户消息和 AI 回复
String lastUserMessage = null;
Long lastAssistantMessageId = null;
for (int i = histories.size() - 1; i >= 0; i--) {
ChatHistory history = histories.get(i);
if ("assistant".equals(history.getRole()) && lastAssistantMessageId == null) {
lastAssistantMessageId = history.getId(); // 记录要删除的 AI 回复
} else if ("user".equals(history.getRole())) {
lastUserMessage = history.getContent(); // 找到最后一条用户消息
break;
}
}
// 删除最后一条 AI 回复
if (lastAssistantMessageId != null) {
historyService.deleteMessage(lastAssistantMessageId);
}
// 用同样的用户消息重新生成
return chatStream(agentId, sessionId, lastUserMessage, userId);
}
22. 安全设计最佳实践
22.1 密码安全
// 注册时加密
user.setPassword(passwordEncoder.encode(request.getPassword()));
// BCrypt 每次生成不同的 hash(含随机 salt),相同密码 hash 不同
// 登录时验证
if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
throw new RuntimeException("用户名或密码错误");
// 注意:不要说"密码错误",要说"用户名或密码错误",防止用户名枚举攻击
}
BCrypt 特点:
- 自动加盐(salt),防彩虹表攻击
- 计算慢(可调节),防暴力破解
- 相同密码每次 hash 不同,但
matches能正确验证
22.2 数据权限控制
每个接口都验证数据归属,防止越权访问:
// 验证智能体属于当前用户
Agent agent = agentMapper.selectById(agentId);
if (!agent.getUserId().equals(userId)) {
throw new SecurityException("无权操作该智能体");
}
// 验证文档属于当前用户(通过智能体间接验证)
Document document = documentMapper.selectById(id);
Agent agent = agentMapper.selectById(document.getAgentId());
if (!agent.getUserId().equals(userId)) {
throw new SecurityException("无权访问该文档");
}
22.3 JWT 安全配置
jwt:
# 密钥至少 256 位(32 字节)才能用于 HS256
secret: documind-ai-secret-key-for-jwt-token-generation-must-be-at-least-256-bits-long
expiration: 86400000 # 24小时,不要设置太长
生产环境建议:
- 密钥从环境变量读取,不写在配置文件中
- 考虑实现 Token 刷新机制(Refresh Token)
- 实现 Token 黑名单(注销时加入 Redis 黑名单)
22.4 SQL 注入防护
MyBatis-Plus 的 LambdaQueryWrapper 使用预编译语句,自动防止 SQL 注入:
// 安全:使用参数化查询
wrapper.eq(Agent::getUserId, userId)
// 生成:WHERE user_id = ?(参数化)
// 危险:字符串拼接(不要这样做)
"WHERE user_id = " + userId // 可能被注入
23. 统计与监控模块
23.1 API 调用日志
ApiCallLog 实体记录每次 API 调用:
@TableName("api_call_log")
public class ApiCallLog {
private Long id;
private Long userId; // 调用用户
private String endpoint; // 接口路径
private String httpMethod; // HTTP 方法
private String requestParams; // 请求参数(JSON)
private Integer responseStatus; // 响应状态码
private Integer responseTime; // 响应时间(毫秒)
private String errorMessage; // 错误信息
private String ipAddress; // 客户端 IP
private String userAgent; // 浏览器信息
private LocalDateTime createdAt;
}
23.2 IP 地址获取
private String getClientIp(HttpServletRequest request) {
// 优先从代理头获取真实 IP
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP"); // Nginx 代理
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr(); // 直连 IP
}
// 多级代理时取第一个(真实客户端 IP)
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip;
}
23.3 统计数据查询
// StatisticsService 提供统计概览
public StatisticsOverviewResponse getOverview(Long userId) {
// 智能体总数
Long agentCount = agentMapper.selectCount(
new LambdaQueryWrapper<Agent>().eq(Agent::getUserId, userId));
// 文档总数(通过 agent_id 关联)
Long documentCount = documentMapper.selectCount(...);
// 总对话次数(汇总所有智能体)
Long totalChats = agentMapper.sumChatCount(userId);
// 总 token 消耗
Long totalTokens = agentMapper.sumTotalTokens(userId);
return StatisticsOverviewResponse.builder()
.agentCount(agentCount)
.documentCount(documentCount)
.totalChats(totalChats)
.totalTokens(totalTokens)
.build();
}
24. 常见问题与解决方案
24.1 向量化后检索不到数据
问题:上传文档后,RAG 检索不到相关内容。
排查步骤:
// 1. 检查向量化状态(vectorStatus 是否为 2)
Document doc = documentMapper.selectById(documentId);
System.out.println("向量化状态: " + doc.getVectorStatus());
// 2. 检查向量文件是否存在
File vectorFile = new File("./data/vector-store.json");
System.out.println("向量文件大小: " + vectorFile.length());
// 3. 验证向量加载(VectorStoreConfig 中的诊断代码)
var testResults = vectorStore.similaritySearch(
SearchRequest.builder().query("测试").topK(20)
.similarityThreshold(0.0).build()
);
System.out.println("内存中向量数: " + testResults.size());
// 4. 检查 agent_id 是否正确写入 metadata
testResults.forEach(doc ->
System.out.println("agent_id: " + doc.getMetadata().get("agent_id")));
常见原因:
- 向量文件路径不一致(相对路径解析问题)
- 应用重启后向量文件未加载
agent_id类型不匹配(Long vs String)
24.2 流式响应中断
问题:SSE 流式输出中途断开。
解决方案:
// 前端设置重连
const eventSource = new EventSource('/api/chat/stream');
eventSource.onerror = () => {
// 自动重连
setTimeout(() => reconnect(), 3000);
};
// 后端设置超时
server:
tomcat:
connection-timeout: 300000 # 5分钟
24.3 异步事务问题
问题:@Async 方法中 @Transactional 不生效。
原因:@Transactional 基于 AOP 代理,而 @Async 在新线程中执行,事务上下文不传递。
解决方案:使用 TransactionTemplate 编程式事务(项目中已采用此方案)。
24.4 CORS 跨域问题
问题:前端请求报 CORS 错误。
检查点:
// 确保 SecurityConfig 中 CORS 配置正确
configuration.setAllowedOriginPatterns(List.of("*"));
// 注意:不能用 setAllowedOrigins("*") + setAllowCredentials(true),会报错
24.5 MyBatis-Plus 分页不生效
问题:分页查询返回所有数据。
原因:未配置分页插件。
解决方案:确保 MybatisPlusConfig 中添加了 PaginationInnerInterceptor,且 DbType 与数据库匹配。
25. 项目启动与部署
25.1 本地开发环境准备
前置条件:
✅ JDK 17+
✅ Maven 3.8+
✅ MySQL 8.0+
✅ Redis 6.0+
✅ 阿里云百炼平台 API Key(DASHSCOPE_API_KEY)
数据库初始化:
mysql -u root -p < database/init-v2.sql
设置环境变量:
# Windows(IDEA 中配置)
DASHSCOPE_API_KEY=sk-your-api-key
# Linux/Mac
export DASHSCOPE_API_KEY=sk-your-api-key
修改数据库配置:
# application-dev.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/documind_ai
username: your_username
password: your_password
data:
redis:
host: localhost
port: 6379
25.2 启动命令
# 方式1:Maven 直接运行
mvn spring-boot:run
# 方式2:打包后运行
mvn clean package -DskipTests
java -jar target/documind-0.0.1-SNAPSHOT.jar
# 方式3:指定环境
java -jar target/documind-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod
25.3 验证启动成功
# 访问 Swagger UI
http://localhost:8080/swagger-ui.html
# 测试登录接口
curl -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"test","password":"test123"}'
25.4 数据目录结构
项目根目录/
├── data/
│ ├── uploads/ ← 上传的文档文件
│ │ ├── {agentId}/
│ │ │ └── {uuid}_{filename}
│ └── vector-store.json ← 向量数据持久化文件
26. 知识点速查表
26.1 Spring Boot 注解速查
| 注解 | 位置 | 作用 |
|---|---|---|
@SpringBootApplication |
主类 | 启动入口,包含 @Configuration + @EnableAutoConfiguration + @ComponentScan |
@RestController |
类 | REST 控制器,方法返回 JSON |
@RequestMapping |
类/方法 | 映射请求路径 |
@GetMapping |
方法 | GET 请求 |
@PostMapping |
方法 | POST 请求 |
@PutMapping |
方法 | PUT 请求 |
@DeleteMapping |
方法 | DELETE 请求 |
@PatchMapping |
方法 | PATCH 请求 |
@PathVariable |
参数 | 路径变量 /agents/{id} |
@RequestParam |
参数 | 查询参数 ?page=1 |
@RequestBody |
参数 | 请求体(JSON) |
@Valid |
参数 | 触发参数校验 |
@Service |
类 | 业务层 Bean |
@Configuration |
类 | 配置类 |
@Bean |
方法 | 注册 Bean |
@Value |
字段 | 注入配置值 |
@Transactional |
方法/类 | 事务管理 |
@Async |
方法 | 异步执行 |
26.2 MyBatis-Plus 注解速查
| 注解 | 说明 |
|---|---|
@TableName("表名") |
指定数据库表名 |
@TableId(type=IdType.AUTO) |
主键,自增 |
@TableField("列名") |
指定数据库列名 |
@TableLogic |
逻辑删除字段 |
26.3 HTTP 状态码速查
| 状态码 | 含义 | 项目使用场景 |
|---|---|---|
| 200 | 成功 | 正常响应 |
| 400 | 请求错误 | 参数校验失败、业务参数错误 |
| 401 | 未认证 | 未登录或 Token 无效 |
| 403 | 无权限 | 越权访问 |
| 404 | 未找到 | 资源不存在 |
| 500 | 服务器错误 | 系统异常 |
26.4 项目接口速查
| 接口 | 方法 | 路径 | 说明 |
|---|---|---|---|
| 注册 | POST | /api/auth/register |
用户注册 |
| 登录 | POST | /api/auth/login |
用户登录 |
| 创建智能体 | POST | /api/agents |
创建智能体 |
| 智能体列表 | GET | /api/agents/list |
获取列表 |
| 智能体详情 | GET | /api/agents/{id} |
获取详情 |
| 更新智能体 | PUT | /api/agents/{id} |
更新 |
| 删除智能体 | DELETE | /api/agents/{id} |
删除 |
| 上传文档 | POST | /api/documents/upload |
上传文档 |
| 文档列表 | GET | /api/documents |
文档列表 |
| 同步对话 | POST | /api/chat/send |
同步问答 |
| 流式对话 | POST | /api/chat/stream |
SSE 流式问答 |
| 对话历史 | GET | /api/chat/history/{sessionId} |
获取历史 |
| 创建会话 | POST | /api/sessions |
新建会话 |
27. 扩展学习建议
27.1 当前技术的进阶方向
向量数据库升级:
- 当前:
SimpleVectorStore(内存 + JSON 文件) - 进阶:
Qdrant(支持过滤、持久化、分布式) - 学习:Qdrant Spring AI 集成文档
大模型能力扩展:
- 当前:文本对话
- 进阶:多模态(图片理解)、Function Calling(工具调用)
- 学习:Spring AI 官方文档 Function Calling 章节
认证体系增强:
- 当前:JWT 无状态认证
- 进阶:OAuth2 第三方登录、Refresh Token 机制
- 学习:Spring Security OAuth2 文档
27.2 性能优化方向
1. 数据库连接池调优(HikariCP 参数)
2. Redis 缓存热点数据(智能体配置、用户信息)
3. 向量检索优化(升级 Qdrant,使用原生过滤)
4. 异步线程池配置(自定义 ThreadPoolTaskExecutor)
5. 文档分块策略优化(语义分块 vs Token 分块)
27.3 推荐学习资源
| 技术 | 推荐资源 |
|---|---|
| Spring Boot | spring.io/guides |
| Spring AI | docs.spring.io/spring-ai |
| Spring AI Alibaba | java.aliyun.com/spring-ai-alibaba |
| MyBatis-Plus | baomidou.com |
| Spring Security | spring.io/projects/spring-security |
| Project Reactor | projectreactor.io |
| RAG 原理 | 《Building LLM Apps》 |
27.4 代码质量提升
单元测试:
@SpringBootTest
class AgentServiceTest {
@Autowired
private AgentService agentService;
@Test
void testCreateAgent() {
AgentCreateRequest request = new AgentCreateRequest();
request.setName("测试智能体");
request.setSystemPrompt("你是测试助手");
// ...
AgentResponse response = agentService.createAgent(1L, request);
assertNotNull(response.getId());
}
}
代码规范:
- 方法长度不超过 50 行
- 类长度不超过 500 行
- 每个方法只做一件事(单一职责)
- 异常信息要有意义,便于排查
28. 总结
28.1 项目技术亮点
-
Spring AI Alibaba 集成:使用最新的 Spring AI 框架对接通义千问,支持同步和流式两种调用方式
-
RAG 架构:文档向量化 + 相似度检索 + 大模型生成,实现基于私有文档的精准问答
-
多提示词策略:根据用户消息内容动态选择最合适的系统提示词,提升 AI 回复质量
-
无状态认证:JWT + Spring Security,无需 Session,天然支持水平扩展
-
AOP 监控:切面自动记录所有 API 调用,无侵入式监控
-
异步向量化:文档上传后异步处理,不阻塞用户操作
28.2 学习路径建议
初级:Spring Boot 基础 → MyBatis-Plus CRUD → REST API 设计
↓
中级:Spring Security + JWT → 事务管理 → AOP 切面
↓
高级:Spring AI 集成 → RAG 架构 → 流式响应 → 向量数据库
↓
进阶:性能优化 → 分布式部署 → 监控告警
28.3 核心设计原则
- 数据隔离:通过
agent_id确保每个智能体的数据相互独立 - 权限控制:每个接口都验证数据归属,防止越权
- 异步解耦:耗时操作(向量化、日志)异步处理,不影响主流程
- 统一响应:
Result<T>统一封装,前端处理一致 - 配置外化:敏感配置通过环境变量注入,不写死在代码中
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)