:Retriever 与 RAG 实战,从检索到生成完整串起来)
LangChain 学习笔记六Retriever 与 RAG 实战从检索到生成完整串起来这是 LangChain 学习笔记系列的第六篇。前面几篇已经整理了模型调用、工具调用、结构化输出、Few-shot、文档加载、文本切分、Embedding、Redis 向量存储和 RediSearch Index。这一篇把这些组件串起来重点讲 Retriever 和完整 RAG Chain用户提问后系统如何检索相关文档再把文档交给模型生成答案。1. 这一篇要解决什么问题前面已经知道文档可以被切成 chunk chunk 可以通过 embedding 变成向量 向量和 metadata 可以存进 Redis / Chroma / FAISS / Pinecone RediSearch Index 可以根据 query vector 找到相似文档。但真实应用里还差最后一步怎么把检索结果交给模型 怎么组织 prompt 怎么让模型只根据资料回答 怎么把整条流程封装成一个可运行的 RAG chain这一篇的主线是VectorStore - Retriever - format_docs - Prompt - ChatModel - OutputParser - final answer一句话Retriever 负责取资料LLM 负责基于资料生成答案。2. 从第五篇衔接到这里第五篇讲的是 Redis 向量存储Document - text metadata - embedding - Redis hash - RediSearch Index - similarity_search / MMR search也就是说第五篇重点在数据怎么存怎么搜。这一篇重点在搜出来之后怎么交给模型回答。如果用一张流程图表示用户问题 | v Retriever 检索相关文档 | v format_docs 格式化上下文 | v PromptTemplate 组装问题和上下文 | v ChatModel 生成答案 | v OutputParser 提取结果3. Retriever 是什么Retriever 可以理解成检索器。它的职责很单一输入一个查询字符串 输出一组相关 Document。例如docsretriever.invoke(数据库表怎么设计)返回[Document(page_content数据库MySQL MyBatis...,metadata{...}),Document(page_content分库分表按 user_id 哈希分库...,metadata{...}),]注意Retriever 不负责生成最终答案。 Retriever 只负责找资料。最终自然语言回答还是由模型完成。4. VectorStore 和 Retriever 的区别VectorStore 是向量存储。它负责存向量 存文本 存 metadata 提供相似性搜索、MMR 搜索等能力。Retriever 是对搜索能力的封装。它负责把“根据 query 查文档”这件事包装成 Runnable。可以这样理解VectorStore 更像数据库 Retriever 更像查询接口。例如docsvector_store.similarity_search(数据库表怎么设计,k4)这是直接调用向量库搜索。而retrievervector_store.as_retriever()docsretriever.invoke(数据库表怎么设计)是先把向量库转成 Retriever再用标准 Runnable 方式调用。5. 为什么 Retriever 也是 RunnableLangChain 里很多组件都是 Runnable。Retriever 也可以retriever.invoke(数据库表怎么设计)这很重要因为它能接入 LCELretriever|format_docs意思是先检索文档 再把文档格式化成字符串。这和前面学过的prompt|model|parser是同一套组合方式。所以 Retriever 的价值不只是“能查文档”而是它能作为 RAG chain 的一个标准组件参与编排。6.as_retriever基本用法最简单写法retrievervector_store.as_retriever()调用docsretriever.invoke(数据库表怎么设计)默认情况下很多向量库会使用相似性搜索。也就是把用户问题转成 query embedding 在向量库里找最相似的 k 个 document embedding 返回对应 Document。7.search_type是什么as_retriever可以指定搜索类型retrievervector_store.as_retriever(search_typesimilarity,search_kwargs{k:4})常见search_typesimilarity mmr similarity_score_threshold分别对应similarity 返回最相似的 k 个文档。 mmr 返回相关但尽量不重复的文档。 similarity_score_threshold 只返回相似度达到阈值的文档。8. similarity 搜索写法retrievervector_store.as_retriever(search_typesimilarity,search_kwargs{k:4})含义找和 query 最相似的 4 个文档。适合问题明确 知识库质量较好 希望直接找最相关内容。缺点可能返回重复内容。比如用户问Redis 向量索引是什么可能返回 4 个都在讲 RediSearch Index 的相似片段。9. MMR 搜索写法retrievervector_store.as_retriever(search_typemmr,search_kwargs{k:4,fetch_k:20,})含义先取 20 个相似候选 再从里面选 4 个既相关又不重复的文档。fetch_k是候选池大小。k是最终返回数量。适合文档内容容易重复 问题需要多个角度 希望上下文覆盖更全面。比如问数据库表怎么设计MMR 可能返回MySQL 表结构 分库分表 索引设计 数据访问层而不是只返回一堆 MySQL 表结构片段。10. similarity_score_threshold 搜索写法retrievervector_store.as_retriever(search_typesimilarity_score_threshold,search_kwargs{score_threshold:0.75,k:10,})含义只返回相似度达到阈值的文档。适合宁愿少返回也不要返回明显不相关内容 知识库里噪声较多 希望降低幻觉风险。注意不同向量库的 score 方向可能不同。有些是分数越高越相似。有些是距离越小越相似。所以阈值不要凭空设应该先观察实际返回结果。11.search_kwargs常见参数search_kwargs是传给底层搜索方法的参数。常见k 最终返回多少个文档。 fetch_k MMR 初始候选数量。 score_threshold 相似度阈值。 filter metadata 过滤条件。例如retrievervector_store.as_retriever(search_typemmr,search_kwargs{k:4,fetch_k:20,filter:filter_condition,})这表示先按 filter 限定文档范围 再做 MMR 检索。12. metadata 过滤和 Retriever如果向量库支持 metadata 过滤可以把过滤条件放到search_kwargs里。以 Redis / RedisVL 风格为例fromredisvl.query.filterimportTag,Num category_is_qaTag(category)QAnum_is_under_50Num(num)50filter_conditioncategory_is_qanum_is_under_50 retrievervector_store.as_retriever(search_typemmr,search_kwargs{k:4,fetch_k:20,filter:filter_condition,})意思是只在 categoryQA 且 num50 的文档中检索。metadata 过滤在真实项目里很重要。比如只查某个用户有权限看的文档 只查某个课程 只查某类资料 只查某个时间范围 只查某个文件来源。13. 先单独测试 Retriever在写完整 RAG chain 之前建议先测试 Retriever。docsretriever.invoke(数据库表怎么设计)fori,docinenumerate(docs):print(*40)print(doc,i)print(doc.page_content[:300])print(doc.metadata)先确认能不能召回相关文档 metadata 是否完整 返回数量是否合适 是否有明显重复 是否有无关结果。不要一上来就接 LLM。因为如果最终回答不好你要先判断是检索错了 还是 prompt 没写好 还是模型总结错了Retriever 单独测试就是为了定位问题。14. RAG Prompt 应该怎么写RAG prompt 的核心是把用户问题 检索上下文 回答规则放在一起。示例fromlangchain_core.promptsimportChatPromptTemplate promptChatPromptTemplate.from_messages([(human,你是负责回答问题的助手。 请只基于给定的上下文回答问题。 如果上下文中没有答案就说不知道。 回答要简洁不要编造。 问题 {question} 上下文 {context} 答案)])这里有几个关键约束只基于上下文回答 没有答案就说不知道 不要编造 回答简洁。这能降低模型凭空发挥的概率。15.format_docs是什么Retriever 返回的是 Document 列表。但 PromptTemplate 通常需要字符串。所以要把 Document 列表格式化成字符串。defformat_docs(docs):return\n\n.join(doc.page_contentfordocindocs)比如 Retriever 返回[Document(page_content数据库MySQL MyBatis...,metadata{...}),Document(page_content分库分表按 user_id 哈希分库...,metadata{...}),]format_docs后变成数据库MySQL MyBatis... 分库分表按 user_id 哈希分库...然后填到 prompt 的{context}16. 带来源的format_docs真实项目里最好把来源也带上。defformat_docs(docs):formatted[]fori,docinenumerate(docs,1):sourcedoc.metadata.get(source,unknown)pagedoc.metadata.get(page,)formatted.append(f[资料{i}] 来源{source}页码{page}\n{doc.page_content})return\n\n.join(formatted)这样 prompt 里会有[资料1] 来源redis-vector.md 页码3 Redis 支持向量搜索...好处模型更容易引用来源 用户更容易追溯答案 调试时更容易知道召回了哪篇文档。17. RunnablePassthrough 是什么RAG chain 里常见fromlangchain_core.runnablesimportRunnablePassthrough然后{context:retriever|format_docs,question:RunnablePassthrough()}这行最容易让人懵。它的意思是同一个用户输入要被分成两路处理。假设输入是数据库表怎么设计第一路输入 - retriever - docs - format_docs - context第二路输入 - RunnablePassthrough - question也就是context 用来放检索资料 question 用来保留原始问题。18. 完整 RAG Chain现在可以串完整链路fromlangchain_core.runnablesimportRunnablePassthroughfromlangchain_core.output_parsersimportStrOutputParserfromlangchain_core.promptsimportChatPromptTemplatefromlangchain_openaiimportChatOpenAI modelChatOpenAI(modelgpt-4o-mini)promptChatPromptTemplate.from_messages([(human,你是负责回答问题的助手。 请只基于给定的上下文回答问题。 如果上下文中没有答案就说不知道。 最多只用三句话回答要简明扼要。 问题 {question} 上下文 {context} 答案)])defformat_docs(docs):return\n\n.join(doc.page_contentfordocindocs)rag_chain({context:retriever|format_docs,question:RunnablePassthrough(),}|prompt|model|StrOutputParser())answerrag_chain.invoke(数据库表怎么设计)print(answer)这条链完整执行顺序输入问题 - retriever 检索相关文档 - format_docs 把文档变成上下文字符串 - RunnablePassthrough 保留原问题 - prompt 组装 question context - model 生成答案 - StrOutputParser 提取字符串19. 这条链里每一步的输入输出假设输入数据库表怎么设计第一步retriever.invoke(数据库表怎么设计)输出[Document(page_content数据库MySQL MyBatis...,metadata{...}),Document(page_content分库分表按 user_id 哈希分库...,metadata{...}),]第二步format_docs(docs)输出数据库MySQL MyBatis... 分库分表按 user_id 哈希分库...第三步RunnablePassthrough()输出原问题数据库表怎么设计第四步prompt.invoke({question:数据库表怎么设计,context:数据库MySQL MyBatis...\n\n分库分表...})输出HumanMessage(...)第五步model.invoke(messages)输出AIMessage第六步StrOutputParser()输出最终字符串答案20. 如何保留检索到的来源上面的rag_chain最终只返回字符串答案。但真实项目里通常还想返回答案 引用文档 来源链接 页码最直接的方式是分两步写question数据库表怎么设计docsretriever.invoke(question)contextformat_docs(docs)answer(prompt|model|StrOutputParser()).invoke({question:question,context:context,})print(answer)fordocindocs:print(doc.metadata.get(source))print(doc.metadata.get(page))这样更容易调试也更容易把来源展示到前端。工程上不一定所有东西都要硬塞进一条 LCEL 链。有时候分步骤写更清楚。21. 流式输出 RAG 答案如果希望模型边生成边显示可以用forchunkinrag_chain.stream(数据库表怎么设计):print(chunk,end,flushTrue)这里的流式输出发生在模型生成阶段。注意Retriever 通常不是流式的 它会先一次性检索出文档 然后模型再流式生成答案。所以用户体验上是先等待检索完成 再看到模型逐字/逐块输出。22. Web 项目里的 SSE如果接前端页面可以用 SSE。流程是后端 rag_chain.stream() - 一边收到 chunk - 一边通过 SSE 推给浏览器 - 前端实时显示答案关系是LangChain stream 后端从 chain 里流式拿结果。 SSE 后端把结果流式推给前端。它们不是同一个东西但经常一起用。23. LangSmith 怎么调试 RAGRAG 出问题时不能只看最终答案。要看每一步用户问题是什么 Retriever 检索到了哪些文档 文档内容是否相关 metadata 是否正确 prompt 最终长什么样 模型是否遵守“只基于上下文回答” token 消耗多少 哪一步耗时最多LangSmith 可以追踪整条链路。比如retriever step 输入 query输出 docs。 prompt step 输入 question/context输出 messages。 model step 输入 messages输出 AIMessage。 parser step 输入 AIMessage输出字符串。这对定位问题很关键。24. RAG 常见问题检索不到正确文档如果回答不对第一步先看 Retriever。可能原因文档切分不合理 embedding 模型不适合 query 表达和文档表达差距太大 k 太小 metadata filter 太严格 向量库里根本没有相关内容 文档入库时 source 或 category 搞错 中文标点/换行导致切分效果差。排查方式docsretriever.invoke(question)fordocindocs:print(doc.page_content)print(doc.metadata)先确认检索质量再看模型回答。25. RAG 常见问题检索到了但模型没用上如果检索结果是对的但模型回答还是不准确可能是 prompt 问题。常见原因context 太长重点被淹没 prompt 没明确要求基于上下文 没有要求不知道就说不知道 资料格式太乱 多个 chunk 之间顺序混乱 模型被问题里的错误假设带偏。可以优化压缩 context 给每段资料编号 要求引用资料编号 明确“不得使用上下文之外的信息” 必要时先 rerank 再生成。26. RAG 常见问题返回内容重复如果返回的 chunk 很重复可以考虑使用 MMR 调大 fetch_k 调小 chunk_overlap 对入库文档去重 减少相似重复 chunk 增加 metadata 过滤。例如retrievervector_store.as_retriever(search_typemmr,search_kwargs{k:4,fetch_k:20,})27. RAG 常见问题回答出现幻觉如果模型编造内容可以从几个方向处理prompt 明确只基于上下文 上下文不足时回答不知道 提高检索质量 返回来源并要求引用 使用 similarity_score_threshold 对低置信度问题拒答 不要把无关文档塞进 context。RAG 不能完全消灭幻觉但可以显著降低。前提是检索到的上下文足够准确。28. RAG 参数怎么调常见调参思路chunk_size 影响文档粒度。 chunk_overlap 影响边界语义保留和重复率。 k 影响上下文数量。 fetch_k 影响 MMR 候选池大小。 score_threshold 影响结果相关性门槛。 prompt 影响模型是否忠于上下文。不要只调一个参数。建议按顺序排查1. 文档解析是否完整 2. chunk 是否合理 3. retriever 是否召回正确内容 4. prompt 是否清楚 5. model 是否适合29. 一个完整 RAG 应用可以长什么样以课程资料问答系统为例离线入库阶段 PDF / Markdown - Loader - Document - Splitter - chunks - Embedding - VectorStore在线查询阶段 用户问题 - Retriever - docs - format_docs - Prompt - LLM - Answer - Sources如果再加 WebFastAPI 接收问题 - RAG Chain - stream 输出 - SSE 推给前端 - 前端展示答案和来源如果再加调试LangSmith 记录每一步输入输出这就是一个完整的 RAG 应用闭环。30. 容易混淆的点30.1 Retriever 会生成答案吗不会。Retriever 只检索文档。 LLM 才生成答案。30.2 VectorStore 和 Retriever 是一个东西吗不是。VectorStore 是存储和搜索能力。 Retriever 是把搜索能力封装成 Runnable。30.3 RunnablePassthrough 是不是没用不是。它在 RAG chain 里负责保留原始问题。同一个输入会同时走两路一路去检索 context 一路保留为 question。30.4 format_docs 是不是必须不一定叫这个名字但需要这个步骤。因为Retriever 返回 Document 列表 Prompt 需要字符串 context。所以需要把 docs 转成文本。30.5 RAG 有了就不会幻觉吗不会。RAG 只是给模型提供外部上下文。如果检索错了 上下文不完整 prompt 不严格 模型不遵守上下文仍然可能幻觉。31. 工程实践先检索评估再生成优化很多 RAG 项目一开始就盯着最终回答。但更有效的方式是拆开评估第一步评估 Retriever 问题能不能召回正确文档 第二步评估 Prompt 正确文档给到模型后模型能不能答对 第三步评估最终链路 检索 生成整体效果如何也就是说RAG 调优不要一锅端。应该分层看入库质量 检索质量 生成质量 最终体验如果检索已经错了后面的模型再强也很难稳定答对。32. 本篇总结这一篇把完整 RAG 查询链路串起来了。核心流程用户问题 - Retriever 检索 Document - format_docs 格式化上下文 - Prompt 组合 question 和 context - ChatModel 基于上下文生成答案 - OutputParser 提取最终字符串几个关键结论Retriever 输入 query输出 Document 列表。 VectorStore 可以通过 as_retriever 变成 Retriever。 search_type 控制检索策略。 similarity 返回最相似文档。 MMR 返回相关但不重复的文档。 similarity_score_threshold 可以过滤低相关文档。 RunnablePassthrough 用于保留原始输入。 format_docs 把 Document 列表转成 prompt 可用的 context。 LangSmith 能帮助调试 RAG 每一步。 RAG 效果要分层排查入库、检索、生成。一句话概括RAG 的本质不是让模型记住知识而是在回答前帮模型找对资料再让模型基于资料组织答案。33. 系列总结从调用模型到构建 RAG 应用到第六篇为止这个系列已经形成一条完整路线第一篇 ChatModel、Message、PromptTemplate解决如何组织输入并调用模型。 第二篇 Tool Calling、ToolMessage、结构化输出解决模型如何连接外部工具和稳定返回对象。 第三篇 Few-shot、示例选择器、RAG 入门解决如何给模型补充示例和外部知识。 第四篇 文档加载、文本切分、Embedding解决外部知识如何被加工成可检索内容。 第五篇 Redis 向量存储、RediSearch Index解决向量和 metadata 如何被存储和搜索。 第六篇 Retriever 与 RAG Chain解决如何从检索走到最终生成答案。对应到一个真实项目文档导入 - 文本切分 - 向量化 - 向量存储 - Retriever - RAG Chain - 流式问答 - 来源追踪 - LangSmith 调试这已经是一个比较完整的 LangChain RAG 应用开发闭环。34. 下一步可以做什么接下来最适合进入项目实战。比如基于 LangChain 的课程资料 RAG 问答系统可以实现PDF / Markdown 文档导入 RecursiveCharacterTextSplitter 切分 OpenAIEmbeddings 向量化 Redis / Chroma / FAISS 存储 Retriever 检索 MMR 去重 ChatPromptTemplate 组织上下文 ChatOpenAI 生成答案 StrOutputParser 输出结果 stream / SSE 流式返回 LangSmith 调试链路 答案展示来源。这个项目能把前六篇的内容全部串起来。相比单独说“学过 LangChain”更有说服力的是我基于 LangChain 实现了一个课程资料 RAG 问答系统支持文档解析、文本切分、向量检索、MMR 去重、流式输出、来源追踪和链路调试。这就是从学习笔记走向项目能力的下一步。当然也可以将自己之前学过的其他技术栈结合起来实现一个集合多个技术栈加上这个LangChain的agent相关技术组合成的一个新项目。