Day 19:RAG 问答链路 v1
学习目标
经过前三天的学习,我们已经掌握了 RAG 系统的各个组件:文档解析与清洗、文本切分、Embedding 和向量数据库。今天是把这些组件串起来的一天——构建一个端到端的 RAG 问答链路。
这个 v1 版本不追求完美,追求的是”跑通”。从用户提问到获得答案,整个链路能走通、能回答基本问题、能显示引用来源。有了这个基线版本,明天才能在上面做优化。
学完今天,你将拥有一个可以演示的 RAG Demo。它可能还不够好,可能有些问题回答不了,但它是一个真实的、可运行的系统。拿这个 Demo 给别人看,别人能理解 RAG 是什么、能做什么。
核心概念
一、用户问题处理
用户问题处理是 RAG 在线管线的第一个环节。它的任务不只是”拿到问题文本”,还包括一些必要的预处理。
问题清洗。 用户的输入可能包含多余的空格、特殊字符、表情符号等。虽然这些问题不影响 Embedding 的效果(模型本身有一定的鲁棒性),但在展示和日志记录时,干净的问题文本更易读。
问题分类。 不是所有用户输入都是适合 RAG 回答的问题。有些是闲聊(“你好”),有些是命令(“帮我生成一份报告”),有些是超出知识库范围的提问(“今天天气怎么样”)。在 MVP 阶段可以不做分类,但在生产系统中,需要判断用户输入是否是”知识库问答”类型的请求。如果不是,直接走其他处理路径。
问题补全。 在对话场景中,用户的提问可能包含代词或省略。比如用户先问了”退货政策是什么”,接着问”那换货呢?“。第二个问题的”换货”需要结合上下文理解。在 v1 版本中可以不处理多轮对话,但至少要意识到这个问题的存在。
工程建议。 v1 阶段保持简单:接收问题文本,做基本清洗(去除首尾空白、合并多余空格),然后直接进入向量化环节。对话上下文补全、问题分类等高级功能留到 v2。
二、问题向量化
把用户的问题通过 Embedding 模型转换为向量。这和 Day 18 离线阶段对 Chunk 做的向量化是同一个操作,使用同一个模型。
需要注意的点:
- 模型一致性。 必须使用和文档入库时相同的 Embedding 模型。这个在 Day 18 已经强调过了,但它是如此重要,值得再提醒一次。
- 单条 vs 批量。 在线查询通常一次只处理一个问题,不需要批量。但如果你要支持高并发(多个用户同时提问),需要考虑 Embedding API 的并发能力和速率限制。
- 缓存优化。 如果多个用户问了相同或非常相似的问题,可以缓存问题的向量,避免重复调用 Embedding API。这是一个可选优化,v1 可以不做。
- 延迟记录。 记录问题向量化的耗时,作为系统性能监控的基础数据。通常 Embedding API 的响应时间在 50-200ms。
三、检索 top-k
用问题向量在向量数据库中检索最相似的 k 个 Chunk。
参数选择。 v1 阶段建议 k=3 或 k=5。这个值不需要过度优化,先用一个中间值,后面根据测试效果调整。
相似度阈值。 top-k 检索总是会返回 k 个结果,即使这些结果和问题一点也不相关。你需要设定一个相似度阈值(比如余弦相似度 0.5),低于阈值的结果视为”未检索到相关内容”。这样当知识库中确实没有答案时,系统可以诚实地回答”未找到相关信息”,而不是硬凑一个不相关的答案。
检索结果的格式。 每个检索结果应该包含:
- Chunk 的原始文本
- 相似度分数
- 元数据(来源文档、章节、页码等)
这些信息在后续环节都会用到。
检索失败的判断。 什么情况算”检索失败”?
- 所有 top-k 结果的相似度分数都低于阈值
- 检索到的 Chunk 和问题明显不相关(可以通过人工抽样判断)
- 向量数据库返回错误(技术故障)
v1 阶段对检索失败的处理可以简单一些:告诉用户”未找到相关信息”。后续版本可以更智能地处理,比如扩大搜索范围、换一种检索方式等。
四、上下文拼接
把检索到的 k 个 Chunk 的文本拼接起来,作为 Prompt 的上下文部分。
拼接格式。 常见的做法是给每个 Chunk 加上序号和来源标注,然后用分隔符隔开。比如:
[参考资料 1](来源:《产品手册》第 3 章"退货政策")
退货申请提交后,客服团队将在 1 个工作日内审核...
[参考资料 2](来源:《售后服务指南》第 2 章"退款流程")
审核通过后,退款将在 3 个工作日内原路退回...
[参考资料 3](来源:《产品手册》第 3 章"退货政策")
特殊情况下,退款可能延长至 7 个工作日...
这样拼接的好处是:模型知道每段内容的来源(有助于引用标注),用户也能看到答案参考了哪些资料。
上下文长度控制。 检索到的 k 个 Chunk 的总字符数可能很长。如果超过模型上下文窗口的一半,需要截断。为什么是一半而不是全部?因为 Prompt 中还需要留空间给问题和生成指令,以及模型的回答本身。
截断策略:按相似度分数排序,从最相似的开始保留,直到总字符数达到上限。低相似度的 Chunk 被优先截断。
去重。 有时检索到的多个 Chunk 来自同一篇文档的相邻段落,内容高度重复(因为有 Overlap)。在拼接前可以做一次简单的去重:如果两个 Chunk 的文本重复率超过某个阈值(比如 80%),只保留相似度更高的那个。
五、Prompt 组装
把拼接好的上下文和用户问题组装成完整的 Prompt。
Prompt 的结构设计。 一个标准的 RAG Prompt 通常包含以下部分:
系统指令。 告诉模型它的角色和回答规则。比如:
“你是一个知识库问答助手。你的任务是基于提供的参考资料回答用户的问题。请遵循以下规则:1. 只基于参考资料中的信息回答,不要使用你自己的知识。2. 如果参考资料中没有相关信息,请明确说明。3. 在回答中标注信息来源。”
参考资料。 上一环节拼接好的上下文文本。
用户问题。 用户提出的问题。
回答指令。 引导模型按特定格式回答。
Prompt 设计的原则:
- 明确约束。 告诉模型”只基于参考资料回答”,这是减少幻觉的关键指令。虽然模型不一定 100% 遵守,但明确约束比不约束效果好很多。
- 明确失败处理。 告诉模型”如果没有相关信息,说’未找到相关信息’“。这比让模型自由发挥要好。
- 格式指引。 如果需要特定格式(比如分点回答、标注引用编号),在 Prompt 中给出格式模板和示例。
和 Week 2 的衔接。 Week 2 我们学了 Prompt Contract 和结构化输出。这些能力在 RAG Prompt 中同样适用。你可以要求模型以 JSON 格式输出,包含 answer(答案正文)和 citations(引用列表)两个字段。这样下游系统可以程序化地处理答案。
六、答案生成
调用大模型,传入组装好的 Prompt,获取答案。
模型选择。 RAG 答案生成对模型的要求:
- 需要好的中文理解能力
- 需要能严格遵循指令(“只基于参考资料回答”)
- 不需要特别强的推理能力(因为推理主要基于提供的上下文)
通常中等能力的模型就够用了(比如 GPT-4o-mini、Claude Haiku 级别)。用最强的模型当然更好,但成本也更高。在 v1 阶段先用性价比高的模型,后续根据效果决定是否升级。
Temperature 设置。 RAG 问答的答案应该基于事实,不需要创意发挥。建议 Temperature 设为 0 或很低(0.1-0.3)。低 Temperature 让模型的输出更确定、更稳定、更贴近参考资料。
流式输出。 如果前端支持,建议使用流式输出(Streaming)。RAG 的答案通常比较长,用户等待完整答案生成可能需要几秒。流式输出让用户可以逐步看到答案,体验更好。
七、引用来源
在每个答案中标注信息来源,是 RAG 系统区别于普通问答的重要特征。
引用的两种形式:
内联引用。 在答案正文中直接标注。比如:“退货申请提交后,客服团队将在 1 个工作日内审核[参考资料 1]。审核通过后,退款将在 3 个工作日内原路退回[参考资料 2]。”
引用列表。 在答案末尾列出所有引用的资料。比如:“参考来源:[1]《产品手册》第 3 章;[2]《售后服务指南》第 2 章。”
两种形式可以结合使用:答案正文中用编号引用,末尾附完整的来源列表。
引用的实现方式。
如果 Prompt 要求模型标注引用,大多数模型能在答案中加入引用标记。但引用的准确性不是 100%,模型可能标注了不相关的来源,或者遗漏了实际使用的来源。
对于 v1 版本,接受引用可能不完全准确的现实。后续可以通过后处理来校验引用的准确性。
八、答案保存
答案生成后需要保存,用于日志记录、效果评估和后续优化。
应该保存的内容:
- 用户问题
- 检索到的 top-k Chunk(文本、分数、元数据)
- 完整的 Prompt(包含上下文和指令)
- 模型生成的答案
- 引用来源
- 时间戳
- 处理耗时(每个环节的耗时)
保存的用途:
- 效果评估。 通过历史记录分析系统的回答质量,发现检索不准或生成不佳的案例。
- 问题诊断。 当用户反馈”回答不对”时,可以回溯查看是检索的问题还是生成的问题。
- Prompt 优化。 分析 Prompt 和答案的对应关系,找到 Prompt 可以改进的地方。
- 成本统计。 通过保存的 Token 使用记录,统计系统的运行成本。
存储方式。 v1 阶段可以用 JSON 文件或 SQLite 数据库。生产环境建议用正式的数据库(PostgreSQL 等)。
概念关系图
RAG 问答链路 v1 完整流程
=========================================================
[用户提问]
|
v
[问题预处理] -- 清洗、补全(v1 可选)
|
v
[问题向量化] -- Embedding API 调用
|
v
[向量检索] --- 向量数据库 top-k 查询
| |
| +-- 相似度分数
| +-- Chunk 文本
| +-- Chunk 元数据
v
[检索结果评估]
|
+-- 分数低于阈值? --> [返回:未找到相关信息]
|
+-- 分数合格? -------> 继续
|
v
[上下文拼接] -- Chunk 文本 + 来源标注
|
v
[Prompt 组装] -- 系统指令 + 上下文 + 问题 + 格式要求
|
v
[LLM 调用] ---- 模型生成答案
|
v
[后处理] ------ 引用标注 + 格式化
|
v
[返回答案] ---- 答案正文 + 引用来源
|
v
[保存记录] ---- 问答日志 + 检索记录 + 耗时统计
Prompt 组装结构
=========================================================
+-----------------------------------------------+
| System: 你是知识库问答助手 |
| 规则:只基于参考资料回答,标注来源 |
| |
| 参考资料: |
| [1] 来源:XX文档,第X章 |
| 退款流程如下... |
| [2] 来源:XX文档,第X章 |
| 退货条件是... |
| [3] 来源:XX文档,第X章 |
| 特殊情况... |
| |
| 用户问题:退款需要多久? |
| |
| 请基于以上参考资料回答,标注引用编号。 |
+-----------------------------------------------+
|
v
[模型生成]
|
v
"退款将在 3 个工作日内完成[2]。特殊情况可能延长至
7 个工作日[3]。
参考来源:
[2]《售后服务指南》第 2 章"退款流程"
[3]《产品手册》第 3 章"退货政策""
实战分析
任务一:完成最小 RAG 问答
把前三天的所有组件串起来,完成一个端到端的 RAG 问答。
需要的组件:
- Day 16 产出的:清洗后的文档文本
- Day 17 产出的:切分好的 Chunk(带元数据)
- Day 18 产出的:Chunk 向量(存入向量数据库)
- 今天新做的:检索 + Prompt + 生成的在线管线
端到端验证流程:
- 输入一个问题(比如”退货的流程是什么”)
- 问题经过向量化
- 在向量数据库中检索 top-3
- 检查检索到的 Chunk 是否包含相关信息
- 拼接上下文、组装 Prompt
- 调用大模型生成答案
- 检查答案是否正确、是否引用了正确的来源
如果一切顺利,你应该能获得一个基于文档内容的准确回答。第一次跑通的时候,你会真切感受到 RAG 系统的价值。
任务二:上传 3 篇文档
选择 3 篇不同类型的文档,完成从上传到可检索的全流程。
文档选择建议:
- 一篇产品手册或技术文档(有明确标题结构,适合按标题切分)
- 一份 FAQ 文档(问答对格式,天然适合 RAG)
- 一份制度文件或规范(可能比较枯燥,但是企业知识库的典型内容)
对每篇文档完成:
- 解析和清洗(Day 16 的流程)
- 切分(Day 17 的流程)
- 向量化和入库(Day 18 的流程)
- 验证检索效果(用 2-3 个问题测试)
任务三:提问 20 个问题
准备 20 个测试问题,覆盖不同的类型和难度。
问题类型分布建议:
- 5 个简单事实型问题(有明确答案,如”退款 SLA 是几天”)
- 5 个流程型问题(需要描述步骤,如”退货流程是什么”)
- 5 个条件判断型问题(需要结合多个信息判断,如”购买超过 30 天还能退货吗”)
- 3 个跨文档问题(答案可能在不同文档中)
- 2 个知识库范围外的问题(测试系统的失败处理能力)
记录每个问题的测试结果:
- 检索到了哪些 Chunk(文本摘要、相似度分数)
- 模型生成的答案
- 答案是否正确
- 引用是否准确
- 如果回答错了,分析是哪个环节出了问题
任务四:输出带引用答案
确保每个答案都包含引用来源。
检查引用的准确性。 对于每个答案中的引用,验证引用的 Chunk 确实包含了答案所表述的信息。如果答案说”退款需要 3 个工作日[2]“,检查参考资料 2 中是否确实说了 3 个工作日。
引用标注的完整性。 答案中的每个关键信息点都应该有引用。如果一个答案包含三个关键信息但只有两个有引用,说明有一个信息点可能是模型自行补充的(潜在的幻觉)。
当日产物说明
产物一:《RAG Demo v1》
这是一个可运行的 RAG 问答系统。
应该包含:
- 文档上传和索引功能(把文档处理成可检索的状态)
- 问答功能(输入问题,输出带引用的答案)
- 3 篇已索引的测试文档
- 20 个测试问题的完整记录
质量标准: Demo 可以演示给他人看。输入一个问题,能在 10 秒内返回带引用的答案。20 个测试问题中,简单事实型问题的正确率不低于 80%。
产物二:《20 个测试问题》
这是一份测试问题集和对应的答案记录。
应该包含:
- 每个问题的类型标注(事实型/流程型/条件判断型/跨文档型/范围外型)
- 每个问题的正确答案(人工标注,基于文档内容)
- 每个问题的系统回答
- 正确性判断(正确/部分正确/错误/无法回答)
- 错误分析(如果是错误的,分析哪个环节出了问题)
质量标准: 问题覆盖多种类型,正确性判断客观,错误分析能定位到具体环节。
产物三:《答案引用记录》
这是一份引用准确性的分析记录。
应该包含:
- 每个答案的引用列表
- 每个引用的准确性判断(引用的 Chunk 是否确实包含对应信息)
- 引用遗漏记录(答案中有信息点但没有引用的情况)
- 引用错误记录(引用的 Chunk 不包含对应信息的情况)
质量标准: 20 个问题中,至少 15 个问题的引用是准确的。不准确的情况有明确的分析。
常见误区与避坑
误区一:直接让模型回答,不管检索结果
有时候检索到的 Chunk 和问题关系不大,模型却还是会基于这些不相关的内容硬编一个答案。这就是”强行回答”的问题。
解决方案在 Prompt 中明确告诉模型:“如果参考资料中没有相关信息,请回答’根据现有资料未能找到相关信息’“。这比硬编一个答案好得多。
有些开发者不敢让系统回答”不知道”,怕影响用户体验。但实际上,一个诚实的”不知道”比一个自信的错误答案要好一万倍。用户可以换一种方式提问,但如果系统给出了错误答案,用户可能会基于错误信息做出决策。
误区二:Prompt 写一次就不管了
RAG Prompt 需要持续迭代。你写了第一版 Prompt,跑了几十个测试问题,会发现有些情况 Prompt 处理不好。比如模型没有严格基于参考资料回答、引用标注不准确、答案格式不统一等等。
每发现一个问题,就调整 Prompt,重新测试。建立一个 Prompt 版本记录,记录每次修改的内容和原因。这和 Week 2 学的 Prompt 管理是一脉相承的。
误区三:不记录中间过程
很多开发者只保存最终的答案,不保存检索到的 Chunk、组装的 Prompt、模型的原始输出。当需要分析问题(“为什么这个问题回答错了”)时,发现没有足够的信息来定位原因。
RAG 系统是一个多环节的管线,任何一个环节都可能是问题所在。只有记录了每个环节的输入和输出,才能有效地排查问题。
误区四:用同一个问题反复测试
用同一组问题反复测试 RAG 系统会让你产生”效果很好”的错觉,因为你可能会不自觉地针对这些问题优化 Prompt 和参数,导致系统在这些特定问题上表现很好,但在新问题上表现很差。
定期更换测试问题集,引入新的、未见过的问题来评估系统的真实水平。最好是让其他人(不了解系统实现的人)来提问,他们的问题表述方式可能和你完全不同。
误区五:忽略响应时间
RAG 问答链路涉及多个 API 调用:Embedding API(问题向量化)、向量数据库查询、LLM API(答案生成)。每个调用都有延迟,叠加起来可能好几秒。
用户对问答系统的响应时间有心理预期。通常 2-3 秒内是可以接受的,超过 5 秒就会觉得慢,超过 10 秒可能就不想等了。
在开发过程中关注每个环节的耗时:
- 问题向量化:通常 50-200ms
- 向量检索:通常 10-100ms(取决于数据规模和索引质量)
- Prompt 组装:几乎为 0(本地字符串操作)
- 答案生成:通常 2-10 秒(取决于答案长度和模型速度)
答案生成通常是瓶颈。如果总时间太长,可以考虑:用更快的模型、用流式输出、减少输入上下文的长度。
延伸思考
从 v1 到 v2 的优化方向
今天的 v1 版本是一个基线。跑通之后,你会清晰地看到它的不足:
- 有些问题检索不到相关内容(检索质量需要优化)
- 有些问题的答案不够准确(生成质量需要优化)
- 有些问题的答案缺少上下文(切分策略需要调整)
- 有些问题的检索结果包含太多噪声(需要 Rerank)
- 有些问题的表述不够清晰(需要 Query Rewrite)
明天的 Day 20 就是要解决这些问题。但优化之前,你需要有 v1 的基线数据:哪些问题回答对了、哪些回答错了、错在哪里。没有基线,就没有办法量化优化的效果。
RAG 问答和 Week 2 API 调用的关系
今天的 RAG 问答链路,本质上就是在 Week 2 学的 API 调用基础上,增加了一个”检索”环节。模型调用本身没有什么变化,变化的是 Prompt 的内容——不再是简单的”回答这个问题”,而是”基于这些参考资料回答这个问题”。
所以 Week 2 的所有知识(API 调用、错误处理、重试机制、结构化输出、日志记录)在 RAG 系统中全部用得上。RAG 不是全新的东西,而是在已有能力上的系统级组装。
企业 Demo 的演示策略
当你拿着这个 RAG Demo 去给客户演示时,有几个策略:
选好演示文档。 用和客户行业相关的文档做演示。如果客户是制造业,就用制造业的文档;如果是金融,就用金融的文档。相关性越强,说服力越大。
准备好演示问题。 提前准备 5-10 个能完美回答的问题,用来展示系统的能力。但同时准备 2-3个”故意刁钻”的问题,用来展示系统在遇到无法回答的问题时能诚实地说”不知道”。这比只展示成功案例更有说服力,因为它展示了系统的可靠性。
展示引用来源。 引用来源是 RAG 相比普通问答的核心优势。演示时强调”每个答案都可以追溯到具体的文档位置”,这对企业客户非常有吸引力。
自测问题
-
描述 RAG 问答链路 v1 的完整流程,从用户提问到返回答案,列出每个步骤的输入和输出。
-
为什么 Prompt 中要明确告诉模型”只基于参考资料回答”?不写这条指令会怎样?
-
上下文拼接时,如何处理检索到的 Chunk 总长度超过模型上下文窗口的情况?
-
引用来源的两种形式是什么?为什么引用来源对 RAG 系统特别重要?
-
答案保存应该包含哪些内容?这些内容分别用于什么目的?
-
如果检索到的 top-3 Chunk 的相似度分数都很低(低于 0.3),系统应该怎么处理?为什么?
-
RAG Prompt 的 Temperature 应该怎么设置?为什么和普通对话不同?
-
你用 20 个问题测试了 RAG 系统,其中 8 个回答不正确。你怎么判断是检索的问题还是生成的问题?
-
为什么说”一个诚实的’不知道’比一个自信的错误答案好一万倍”?
-
RAG 问答链路的响应时间主要花在哪些环节?怎么优化?
关键词
- 用户问题处理:对用户输入进行清洗、分类、补全的预处理环节
- 问题向量化:将用户问题通过 Embedding 模型转换为向量,用于检索
- 上下文拼接:将检索到的多个 Chunk 文本按格式组装成 Prompt 的上下文部分
- Prompt 组装:将系统指令、上下文、问题和格式要求组装成完整的 Prompt
- 答案生成:大模型基于增强后的 Prompt 生成回答的过程
- 引用来源(Citation):在答案中标注信息来自哪个文档的哪个部分
- 相似度阈值:用于判断检索结果是否足够相关的最低相似度分数
- 答案保存:记录问答过程的所有中间数据,用于评估和诊断
- RAG Demo v1:第一个端到端可运行的 RAG 问答系统,作为后续优化的基线
- 检索失败处理:当检索不到相关内容时,系统选择诚实告知而非强行回答