承上 :上一篇我们把文档碎片变成了向量,存进了Redis,现在能通过语义检索找到最相关的文档块了。但这只是"查到了资料",还没到"回答问题"。今天,我们要用Spring AI原生的
QuestionAnswerAdvisor,一行代码跑通完整RAG链路。
1. RAG的最后一公里:从检索到生成
1.1. 目前我们做到了哪一步?
javascript
用户提问:"年假怎么申请?"
│
▼
向量检索 → 找到3个最相关的文档块
│
▼
返回给用户:一堆文档片段(JSON格式)
用户看到的是:
css
{
"results": [
{"content": "年假:入职满1年享有5天带薪年假..."},
{"content": "病假:每年3天带薪病假..."},
{"content": "事假:每年5天无薪事假..."}
]
}
这不是用户想要的。用户要的是:"根据公司规定,年假需要在OA系统提交申请,审批通过后即可休假。"
2. Spring AI的答案:QuestionAnswerAdvisor
还记得第9篇的 MessageChatMemoryAdvisor 吗?一行代码搞定记忆管理。Spring AI用同样的Advisor模式封装了RAG:
arduino
// 第9篇:记忆管理
.advisors(new MessageChatMemoryAdvisor(chatMemory, conversationId))
// 本篇:RAG问答
.advisors(new QuestionAnswerAdvisor(vectorStore))
QuestionAnswerAdvisor 自动完成了:检索文档 → 拼接上下文 → 注入Prompt → 发给AI生成回答。 我们只需要告诉它用哪个VectorStore。
2.1. 引入依赖
Spring AI 2.0 对模块命名进行了规范化。注意: 旧版的 spring-ai-advisors-vector-store 已更名为 spring-ai-vector-store-advisor。
xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-vector-store-advisor</artifactId>
</dependency>
2.2. QuestionAnswerAdvisor配置
java
package com.yunxi.ai.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
@Slf4j
@Service
public class ChatService {
private final ChatClient chatClient;
private final VectorStore vectorStore;
public ChatService(ChatClient.Builder builder, VectorStore vectorStore) {
this.chatClient = builder
.defaultAdvisors(new SimpleLoggerAdvisor())
.build();
this.vectorStore = vectorStore;
}
/**
* 同步接口
* @param message
* @return
*/
public String chat(String message) {
SearchRequest searchRequest = SearchRequest.builder()
.topK(3)
.build();
return chatClient.prompt()
.system("你是一个专业的助手,可以回答用户的问题")
.user(message)
.advisors(QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(searchRequest)
.build())
.call()
.content();
}
}
一行 new QuestionAnswerAdvisor(vectorStore) ,RAG链路全部搞定。
2.3. 测试结果

2.4. 日志分析:QuestionAnswerAdvisor做了什么?
ini
用户问题:"如何请假呀?"
│
▼
QuestionAnswerAdvisor 执行流程:
│
├── 1. 向量检索
│ vectorStore.similaritySearch("如何请假呀", topK=3)
│ → 返回3个最相关的文档块
│
├── 2. 拼接上下文
│ 将检索到的文档块拼成 {context} 字符串
│
├── 3. 注入Prompt模板
│ → 最终Prompt = "参考资料:... 用户问题:如何请假呀?"
│
└── 4. 发给AI生成回答
AI基于参考资料生成自然语言回答

最终完整的Prompt:
markdown
{messages=[SystemMessage{textContent='你是一个专业的助手,可以回答用户的问题', messageType=SYSTEM, metadata={messageType=SYSTEM}}, UserMessage{content='如何请假呀?
Context information is below, surrounded by ---------------------
---------------------
未提前提交请假申请,或请假申请未获审批擅自不到岗的
2. 假期届满未按时返岗,也未提交续假申请的
3. 伪造病假单、外勤记录骗取休假的
4. 工作时间擅自脱岗外出办理私人事务超过2小时的
5. 考勤异常后3个工作日内未提交补登申请,也未向部门负责人说明情况的
5.2 旷工核算标准
- 单次旷工时长不足4小时的,按0.5天旷工核算
- 单次旷工时长超过4小时的,按1天旷工核算
5.3 旷工处罚规则
1. 旷工期间全额扣除对应时段工资,取消当月全勤奖
2. 月度内累计旷工达到2天的,扣除当月20%绩效工资
3. 月度内累计旷工达到3天,或年度内累计旷工达到10天的,属于严重违反公司规章制度,公司有权依法解除劳动合同,且不支付任何经济补偿金
第六章 各类假期管理
6.1 事假
员工因私人事务需占用工作时间的,可申请事假。事假最小申请单位为1小时,事假期间扣除对应时段的全额工资。事假审批权限:1天以内由部门负责人审批,1-3天由分管领导审批,3天及以上需提交总经理审批。
6.2 病假
员工因患病或非因公负伤需停工休息的,可申请病假。病假时长在2天以内的,需提供医院开具的诊断证明;病假时长超过7天的,需提供二级及以上医院的正式就医记录。病假工资按北京市最低工资标准的80%发放,最低不低于当地最低工资的80%。
6.3 年假
员工累计工作满1年不满10年的,每年享受5天带薪年假;满10年不满20年的,每年享受10天带薪年假;满20年及以上的,每年享受15天带薪年假。年假可在当年内集中安排或分段安排,不跨年度累计,确因工作需求当年无法休完的,经审批后可顺延至次年3月底前休完。
6.4 其他假期
婚假、产假、陪产假、丧假、工伤假等特殊假期,严格按照国家及北京市相关劳动法律法规执行,员工提交对应证明材料后,可按规定享受带薪假期。
第七章 外勤与出差管理
7.1 外勤管理
员工因工作需求需外出办理业务,无法到岗打卡的,需提前在钉钉提交外勤申请,注明外出事由、往返时段、对接客户信息,经部门负责人审批通过后生效。外勤结束后24小时内,需在申请单中上传相关工作凭证(如客户签到记录、沟通截图),未上传凭证的外勤申请自动作废,按旷工处理。
7.2 出差管理
员工需赴外地开展工作的,需提前提交出差申请,注明出差目的地、出差时长、工作内容、差旅预算,按审批流程完成报备。出差期间无需打卡,以出差审批单作为考勤依据,超出出差申请时段未返岗也未提交续期申请的,超出部分按旷工处理。长期出差超过30天的,需向人力资源部提交专项说明备案。
第八章 加班与调休管理
8.1 加班认定规则
公司不鼓励无效加班,员工在标准工作时间外自发延长工作时间的,不认定为加班。只有由部门统一安排、提前提交加班申请、有明确工作产出的超时工作,才可认定为加班。
8.2 加班核算标准
- 工作日延时加班:按1.5倍时长核算调休
- 休息日加班:按2倍时长核算调休
- 法定节假日加班:按国家规定核算3倍加班工资,不折算调休
8.
---------------------
Given the context and provided history information and not prior knowledge,
reply to the user comment. If the answer is not in the context, inform
the user that you can't answer the question.
', metadata={messageType=USER}, messageType=USER}], modelOptions=org.springframework.ai.openai.OpenAiChatOptions@a496bec3}
2.5. 实战测试
2.6. 测试1:知识库内的问题

2.7. 测试2:知识库外的问题

3. QuestionAnswerAdvisor 进阶配置
3.1. 召回数量 (topK)
- 作用:指定返回最相关的文档片段数量。
- 建议取值 :通常设置在 3~10 之间。
-
- 过少:容易遗漏关键信息,导致回答不完整。
- 过多会引入大量噪音,稀释语义重点,甚至超出大模型的上下文窗口限制。
3.2. 相似度阈值 (similarityThreshold)
- 作用:设定余弦相似度的最低门槛(范围 0.0 ~ 1.0只有相似度高于该值的文档才会被返回。
- 常见场景与取值建议:
-
- 0.1 ~ 0.3 :召回优先。适用于宁可多召回也不愿遗漏的场景,后续可由大模型或重排序模型筛选。
- 0.5 ~ 0.7 :平衡模式。在精度和召回率之间取得平衡适合大多数通用问答场景。
- 0.8+ :严格匹配。适用于对准确性要求极高、类似关键词精确检索的场景,但可能导致召回结果为空。
3.3. 元数据过滤 (filterExpression)
- 作用:基于文档的元数据(Metadata)进行预过滤,实现结构化筛选。
- 应用场景:
-
- 权限隔离:只检索当前用户有权限访问的文档。
- 范围限定 :仅检索特定来源(如
source in ['docA.pdf', 'docB.pdf'])、特定时间范围或特定类别的内容。 - 语法:通常使用类似 SQL 或 JSON Path 的表达式的语法构建。
css
Filter.Expression filterExpression = new Filter.Expression(
Filter.ExpressionType.IN,
new Filter.Key("source"),
new Filter.Value(source)
);
4. 本篇小结
这一篇我们用 QuestionAnswerAdvisor 一行代码跑通了完整RAG链路:
| 对比 | 手动拼接 | QuestionAnswerAdvisor |
|---|---|---|
| 代码量 | 30+行 | 1行 |
| 检索 | 手动调用 | 自动执行 |
| 模板 | 手动替换 | 自动填充 |
| 协作 | 手动编排 | 和其他Advisor无缝叠加 |
现在我们的公司制度问答机器人已经上线了。但你可能会发现一个尴尬的问题:有时候检索出来的东西和用户问的不太对得上。 比如问"怎么退钱",检索出来的却是"退款到账时间"。
下一篇,我们要解决这个问题------检索优化:Query改写、结果重排序,让RAG从"查到"升级为"查准"。
本文与DeepSeek协作完成