Spring AI RAG 实战
摘要:本文介绍了基于SpringAI构建检索增强生成(RAG)系统的完整流程。系统通过LLM提取问题关键词进行元数据过滤,结合向量检索提升精度,整合文档摘要构建增强上下文,并支持工具调用和记忆功能。代码展示了关键词提取、向量检索、上下文构建和流式响应的关键实现步骤,相比传统RAG显著提高了回答准确性和相关性。
为什么需要“智能 RAG”?
传统的 RAG 系统通常直接使用用户原始问题进行向量检索。这种方式看似简单,实则存在两大痛点:
- 噪声干扰严重:用户提问中常含无关词、语气词、口语化表达,导致检索结果不精准。
- 上下文缺失:仅返回原始文本片段,大模型难以理解文档背景,容易“断章取义”。
为解决这些问题,本文将带你构建一个高精度、可扩展的智能 RAG 系统,融合以下四大核心能力:
- 关键词提取 + 元数据过滤 → 提升检索精度
- 增强型上下文构造(摘要 + 内容)→ 提升回答准确性
- 工具调用(Function Calling)→ 支持实时数据查询
- 上下文记忆(Chat Memory)→ 实现多轮对话
整个系统基于 Spring AI 实现,代码简洁、可维护性强,适合生产环境部署。
核心流程设计
我们对传统 RAG 流程进行了升级,整体执行链路如下:
用户提问
↓
[LLM] 提取关键词 → 减少噪声
↓
构造元数据过滤条件(keywords in [...])
↓
向量检索 + 元数据过滤 → 精准召回
↓
拼接【摘要 + 内容】作为增强上下文
↓
注入 Prompt 模板,调用 ChatClient
↓
支持工具调用 & 上下文记忆
↓
流式返回最终回答
优势:通过“关键词提取 + 元数据过滤”,我们实现了语义+结构双重过滤,显著提升检索相关性。
关键词提取:让 LLM 帮你“提炼问题本质”
我们使用一个轻量级 Prompt,让 LLM 从用户问题中提取 3-5 个核心关键词:
private static final String KEYWORD_EXTRACTION_PROMPT = """
请从以下用户问题中提取出最相关的 3-5 个关键词。
只返回关键词列表,用英文逗号分隔,不要解释。
问题:%s
""";
例如:
- 输入:
“Spring Bean 的生命周期有哪些阶段?” - 输出:
Spring, Bean, 生命周期
这些关键词将用于后续的元数据过滤,确保只检索包含这些关键词的文档。
注意:此步骤本身也是一次 LLM 调用,适用于对精度要求高的场景。若性能敏感,可考虑使用 NLP 库(如 HanLP)做本地关键词提取。
元数据过滤:精准召回,减少“无效检索”
我们假设文档在 ETL 阶段已提取关键词并存入元数据(如 keywords 字段):
Filter.Expression expression = new FilterExpressionBuilder()
.in("keywords", keywords)
.build();
结合向量相似度检索:
List<Document> retrievedDocs = vectorStore.similaritySearch(
SearchRequest.builder()
.query(prompt)
.filterExpression(expression) // 元数据过滤
.similarityThreshold(0.7) // 相似度阈值
.topK(6) // 最多返回 6 篇
.build()
);
效果:避免召回大量“语义相似但主题无关”的文档,提升 RAG 准确率。
增强型上下文构造:不只是“文本片段”
传统 RAG 仅拼接 Document.getText(),信息量有限。我们引入文档摘要元数据,构造更丰富的上下文:
private String buildEnrichedContext(List<Document> docs) {
if (docs.isEmpty()) {
return "无相关参考资料。";
}
return docs.stream()
.map(doc -> {
String summary = doc.getMetadata().getOrDefault("summary", "无摘要").toString();
String content = doc.getText();
return "--- 摘要 ---\n" + summary + "\n--- 内容 ---\n" + content;
})
.collect(Collectors.joining("\n\n"));
}
这样,LLM 在生成回答时,不仅能“看到”原文,还能“理解”文档背景,回答更自信、更准确。
自定义 Prompt 模板:引导模型行为
我们设计了一个结构化 Prompt,明确告诉 LLM 如何决策:
private static final String ENHANCED_RAG_TEMPLATE = """
请严格根据以下参考资料回答问题。参考资料在三横线之间。
1. 如果问题涉及实时数据(如天气、时间),优先调用相应工具。
2. 否则,请参考以下资料作答:
---------------------
{question_answer_context}
---------------------
问题:{query}
要求:
- 必须基于参考资料回答
- 如果参考资料中没有相关信息,请回答:“抱歉,我无法根据参考资料回答这个问题。”
- 不要编造或推测答案
- 回答时不要提及“上下文”或“参考资料”
""";
关键设计:
- 明确优先级:工具 > 参考资料 > 拒绝回答
- 防止幻觉(Hallucination)
- 输出格式干净,适合前端展示
工具调用(Function Calling):支持实时数据
我们整合了天气查询工具,支持动态调用:
FunctionToolCallback<WeatherService.WeatherRequest, WeatherService.WeatherResponse> weatherTool =
FunctionToolCallback.builder("currentWeather", new WeatherService())
.description("Get the weather in location")
.inputType(WeatherService.WeatherRequest.class)
.build();
在 ChatClient 中启用:
.toolCallbacks(weatherTool)
.toolNames(WeatherTools.CURRENT_WEATHER_TOOL)
当用户问“今天北京天气如何?”时,系统会自动调用
currentWeather工具,而非依赖静态知识库。
上下文记忆(Chat Memory):支持多轮对话
通过 advisors 注入会话 ID,实现对话记忆:
.advisors(memoryAdvisor ->
memoryAdvisor.param(ChatMemory.CONVERSATION_ID, chatId))
chatId来自请求头,可用于区分不同用户或会话- 支持多轮问答,如:
- Q: “Spring Bean 是什么?”
- Q: “它有哪些作用域?”
实现真正的“对话式 AI”,而非单轮问答。
完整代码实现
@RestController
@RequestMapping("/rag")
public class RAGController {
@Autowired
private ChatClient chatClient;
@Autowired
private VectorStore vectorStore;
// 提取关键词 Prompt
private static final String KEYWORD_EXTRACTION_PROMPT = """
请从以下用户问题中提取出最相关的 3-5 个关键词。
只返回关键词列表,用英文逗号分隔,不要解释。
问题:%s
""";
// 增强型 RAG Prompt 模板
private static final String ENHANCED_RAG_TEMPLATE = """
请严格根据以下参考资料回答问题。参考资料在三横线之间。
1. 如果问题涉及实时数据(如天气、时间),优先调用相应工具。
2. 否则,请参考以下资料作答:
---------------------
{question_answer_context}
---------------------
问题:{query}
要求:
- 必须基于参考资料回答
- 如果参考资料中没有相关信息,请回答:“抱歉,我无法根据参考资料回答这个问题。”
- 不要编造或推测答案
- 回答时不要提及“上下文”或“参考资料”
""";
@GetMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> rag(
HttpServletResponse response,
@Validated @RequestParam("prompt") String prompt,
@RequestHeader(value = "chatId", required = false, defaultValue = "rag") String chatId) {
response.setCharacterEncoding("UTF-8");
return Flux.defer(() -> {
try {
// Step 1: 提取关键词
List<String> keywords = extractKeywordsFromPrompt(prompt);
// Step 2: 构造元数据过滤表达式
Filter.Expression expression = new FilterExpressionBuilder()
.in("keywords", keywords)
.build();
// Step 3: 向量检索 + 元数据过滤
List<Document> retrievedDocs = vectorStore.similaritySearch(
SearchRequest.builder()
.query(prompt)
.filterExpression(expression)
.similarityThreshold(0.7)
.topK(6)
.build()
);
// Step 4: 构造增强上下文(摘要 + 内容)
String context = buildEnrichedContext(retrievedDocs);
// Step 5: 渲染最终 Prompt
String finalPrompt = new PromptTemplate(ENHANCED_RAG_TEMPLATE).render(
Map.of("question_answer_context", context, "query", prompt)
);
// Step 6: 注册工具回调
FunctionToolCallback<WeatherService.WeatherRequest, WeatherService.WeatherResponse> weatherTool =
FunctionToolCallback.builder("currentWeather", new WeatherService())
.description("Get the weather in location")
.inputType(WeatherService.WeatherRequest.class)
.build();
// Step 7: 调用 ChatClient 流式响应
return chatClient.prompt()
.user(finalPrompt)
.toolCallbacks(weatherTool)
.toolNames(WeatherTools.CURRENT_WEATHER_TOOL)
.advisors(advisor -> advisor.param(ChatMemory.CONVERSATION_ID, chatId))
.stream()
.content();
} catch (Exception e) {
return Flux.just("抱歉,处理请求时发生错误:" + e.getMessage());
}
});
}
/**
* 使用 LLM 提取关键词
*/
private List<String> extractKeywordsFromPrompt(String prompt) {
String keywordPrompt = String.format(KEYWORD_EXTRACTION_PROMPT, prompt);
String result = chatClient.prompt(keywordPrompt).call().content();
return Arrays.stream(result.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.limit(5)
.collect(Collectors.toList());
}
/**
* 构造增强上下文:包含摘要和内容
*/
private String buildEnrichedContext(List<Document> docs) {
if (docs.isEmpty()) {
return "无相关参考资料。";
}
return docs.stream()
.map(doc -> {
String summary = doc.getMetadata().getOrDefault("summary", "无摘要").toString();
String content = doc.getText();
return "--- 摘要 ---\n" + summary + "\n--- 内容 ---\n" + content;
})
.collect(Collectors.joining("\n\n"));
}
}
结语
通过本次实战,我们不仅实现了一个功能完整的 RAG 系统,更重要的是掌握了 Spring AI 的整合能力——它让 Java 开发者也能轻松构建 AI 原生应用。
AI 不是替代开发者,而是赋予我们更强的创造力。
更多推荐



所有评论(0)