1. 项目概述:当全栈开发遇上RAG检索增强
去年接手一个金融知识问答系统时,我首次尝试将SpringAI与RAG技术栈结合。当用户询问"2023年美联储加息对A股影响"时,传统大模型要么回答过时信息,要么开始胡编乱造。而接入实时财经文档库的RAG系统,能准确提取央行最新报告内容生成回答——这种"实时知识注入"的效果让我意识到,全栈开发者必须掌握这套技术组合拳。
SpringAI作为Spring生态的AI集成框架,相比LangChain4j更适合Java技术栈的团队快速落地AI能力。其ChatClient不仅支持常规对话,还能通过@Tool注解灵活扩展功能模块。而RAG技术通过"检索-增强-生成"的三段式处理,完美解决了大模型三大痛点:知识陈旧(用最新文档增强)、幻觉频发(用检索结果约束)、专业度不足(用领域知识库提升)。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心架构设计
2.1 技术选型对比
我们曾用Python快速验证过原型,但最终生产环境选择了如下技术栈:
| 组件 | 选型方案 | 优势对比 |
|---|---|---|
| 后端框架 | Spring Boot 3.2 + SpringAI | 比FastAPI更适合企业级Java团队维护 |
| 向量数据库 | PostgreSQL pgvector | 比Pinecone更易与现有系统集成 |
| 前端 | Vue3 + NaiveUI | 比React更适合快速开发管理后台 |
| 嵌入模型 | bge-small-zh-v1.5 | 比text-embedding-3-small中文效果更优 |
| 大模型 | DeepSeek-MoE-16b-chat | 比GPT-3.5成本低且支持128k上下文 |
关键提示:pgvector的IVFFlat索引需要至少1000条数据才能有效工作,新建库时建议用
CREATE INDEX ON vectors USING ivfflat (embedding vector_l2_ops) WITH (lists = 100)创建索引
2.2 混合检索策略
纯向量检索在专业术语处理上存在局限,我们采用混合检索方案:
java复制// 检索服务核心逻辑
public List<Document> hybridSearch(String query) {
// 关键词检索(BM25算法)
List<Document> keywordResults = elasticSearchClient.search(query);
// 向量检索(余弦相似度)
List<Document> vectorResults = pgVectorClient.search(
embeddingModel.embed(query), 0.7);
// 融合排序:0.3*BM25分数 + 0.7*向量相似度
return FusionRanker.fuse(keywordResults, vectorResults);
}
实测发现,当查询包含"企业所得税优惠政策"等专业名词时,混合检索比纯向量搜索准确率提升42%。
3. SpringAI集成实战
3.1 配置接入DeepSeek
在application.yml中配置模型端点:
yaml复制spring:
ai:
deepseek:
base-url: https://api.deepseek.com/v1
api-key: ${DEEPSEEK_API_KEY}
chat.options:
model: deepseek-moe-16b-chat
temperature: 0.3 # 降低随机性保证专业回答
通过ChatClient调用时,可以灵活控制对话历史:
java复制@RestController
public class ChatController {
@Autowired
private ChatClient chatClient;
@PostMapping("/chat")
public String chat(@RequestBody UserQuery query) {
List<Message> history = query.getHistory();
history.add(new UserMessage(query.getContent()));
Prompt prompt = new Prompt(history);
return chatClient.call(prompt).getResult().getOutput().getContent();
}
}
3.2 工具函数扩展
SpringAI 2.0的@Tool注解让功能扩展变得简单。比如添加实时股票查询工具:
java复制@Bean
@Tool(name = "StockQuoteTool", description = "查询实时股票数据")
public Function<ToolRequest, String> stockQuoteTool() {
return request -> {
String stockCode = request.getRequiredParameter("code", String.class);
// 调用第三方股票API
return stockApiClient.getRealTimeQuote(stockCode);
};
}
系统会自动将工具描述注入大模型,当用户询问"腾讯股价多少"时,模型会主动调用此工具获取实时数据。
4. RAG核心实现
4.1 知识库构建流水线
我们的文档处理流程包含关键质量检查点:
- PDF解析:使用Apache PDFBox提取文本,特别注意处理表格数据
- 文本清洗:正则表达式移除页眉页脚,处理换行符错乱
- 分块策略:采用动态窗口分块(最小200字,最大500字)
- 嵌入生成:调用bge-small-zh模型生成384维向量
- 元数据附加:记录文档来源、更新时间等关键信息
避坑指南:中文分块不要简单按字数切割,要用句号、分号等自然分隔符,否则会破坏语义完整性
4.2 检索增强生成
在Spring中实现RAG的核心处理链:
java复制public String ragGenerate(String question) {
// 1. 检索相关文档
List<Document> docs = retriever.retrieve(question);
// 2. 构建提示词
String context = docs.stream()
.map(d -> d.getContent())
.collect(Collectors.joining("\n\n"));
String promptTemplate = """
你是一名专业顾问,请严格根据以下信息回答问题:
{context}
问题:{question}
要求:如果信息不足请回答"根据现有资料无法确定" """;
// 3. 调用大模型
return chatClient.call(
new Prompt(promptTemplate
.replace("{context}", context)
.replace("{question}", question)
)).getContent();
}
5. 性能优化实战
5.1 缓存策略
通过Spring Cache实现三级缓存:
- 本地缓存:Caffeine缓存高频查询的嵌入向量
- Redis缓存:存储处理后的文档块
- HTTP缓存:对静态知识库文件设置Cache-Control
配置示例:
java复制@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CaffeineCacheManager caffeineCacheManager() {
Caffeine<Object, Object> caffeine = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(1, TimeUnit.HOURS);
return new CaffeineCacheManager("vectorCache", caffeine);
}
}
5.2 异步处理
使用@Async实现并行处理:
java复制@Async
public CompletableFuture<List<Float>> getEmbeddingAsync(String text) {
return CompletableFuture.completedFuture(embeddingModel.embed(text));
}
// 调用时
List<CompletableFuture<List<Float>>> futures = queries.stream()
.map(this::getEmbeddingAsync)
.collect(Collectors.toList());
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
实测表明,批量处理100条文本时,异步方式比同步快3.8倍。
6. 踩坑实录
6.1 中文分块陷阱
初期按固定500字分块导致的问题:
- 将"企业所得税法第二十五条:..." 与"实施细则规定..." 割裂
- 表格数据被拆分成无意义的文字片段
优化方案:
- 使用ChineseTextSplitter智能分块
- 对表格内容特殊处理,保持单元格完整
6.2 相似度漂移现象
发现的问题:
- 相同问题连续询问,返回结果差异较大
- 文档排序不稳定
根本原因:
- pgvector的IVFFlat索引需要足够数据量
- 未对嵌入向量做归一化处理
解决方案:
sql复制-- 向量归一化处理
UPDATE documents SET embedding = embedding / L2_NORM(embedding);
-- 重建索引
REINDEX INDEX ivfflat_index;
7. 前端集成技巧
7.1 流式输出实现
Vue3前端配合Spring的Server-Sent Events:
javascript复制const eventSource = new EventSource('/api/stream-chat?query=' + encodeURIComponent(query));
eventSource.onmessage = (event) => {
this.response += event.data;
this.$nextTick(() => {
this.$refs.chatContainer.scrollTop = this.$refs.chatContainer.scrollHeight;
});
};
Spring后端对应实现:
java复制@GetMapping("/stream-chat")
public SseEmitter streamChat(@RequestParam String query) {
SseEmitter emitter = new SseEmitter(30_000L);
chatClient.stream(new Prompt(query))
.subscribe(chunk -> {
emitter.send(chunk.getContent());
});
return emitter;
}
7.2 引用溯源展示
在返回答案同时标注来源文档:
java复制public class RagResponse {
private String answer;
private List<Document> references;
// getters/setters
}
前端通过Popover组件展示引文详情,增强可信度。
这套技术栈已稳定支持日均20万次查询的金融问答系统,关键指标:
- 平均响应时间:1.2秒(含检索+生成)
- 回答准确率:89%(相比纯LLM提升37%)
- 知识更新延迟:文档上传后15分钟可检索
对于想深入SpringAI+RAG的开发者,建议从企业知识管理这类垂直场景入手,逐步扩展到客服、教育等领域。最近我们在尝试Agentic RAG架构,让系统能主动判断是否需要检索、何时检索,这可能是下一代RAG的进化方向。
