LangChain 学习笔记(六):Retriever 与 RAG 实战,从检索到生成完整串起来
2026/7/11 6:45:18 网站建设 项目流程

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。

例如:

docs=retriever.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 更像查询接口。

例如:

docs=vector_store.similarity_search("数据库表怎么设计?",k=4)

这是直接调用向量库搜索。

而:

retriever=vector_store.as_retriever()docs=retriever.invoke("数据库表怎么设计?")

是先把向量库转成 Retriever,再用标准 Runnable 方式调用。

5. 为什么 Retriever 也是 Runnable

LangChain 里很多组件都是 Runnable。

Retriever 也可以:

retriever.invoke("数据库表怎么设计?")

这很重要,因为它能接入 LCEL:

retriever|format_docs

意思是:

先检索文档; 再把文档格式化成字符串。

这和前面学过的:

prompt|model|parser

是同一套组合方式。

所以 Retriever 的价值不只是“能查文档”,而是:

它能作为 RAG chain 的一个标准组件参与编排。

6.as_retriever基本用法

最简单写法:

retriever=vector_store.as_retriever()

调用:

docs=retriever.invoke("数据库表怎么设计?")

默认情况下,很多向量库会使用相似性搜索。

也就是:

把用户问题转成 query embedding; 在向量库里找最相似的 k 个 document embedding; 返回对应 Document。

7.search_type是什么

as_retriever可以指定搜索类型:

retriever=vector_store.as_retriever(search_type="similarity",search_kwargs={"k":4})

常见search_type

similarity mmr similarity_score_threshold

分别对应:

similarity: 返回最相似的 k 个文档。 mmr: 返回相关但尽量不重复的文档。 similarity_score_threshold: 只返回相似度达到阈值的文档。

8. similarity 搜索

写法:

retriever=vector_store.as_retriever(search_type="similarity",search_kwargs={"k":4})

含义:

找和 query 最相似的 4 个文档。

适合:

问题明确; 知识库质量较好; 希望直接找最相关内容。

缺点:

可能返回重复内容。

比如用户问:

Redis 向量索引是什么?

可能返回 4 个都在讲 RediSearch Index 的相似片段。

9. MMR 搜索

写法:

retriever=vector_store.as_retriever(search_type="mmr",search_kwargs={"k":4,"fetch_k":20,})

含义:

先取 20 个相似候选; 再从里面选 4 个既相关又不重复的文档。

fetch_k是候选池大小。

k是最终返回数量。

适合:

文档内容容易重复; 问题需要多个角度; 希望上下文覆盖更全面。

比如问:

数据库表怎么设计?

MMR 可能返回:

MySQL 表结构 分库分表 索引设计 数据访问层

而不是只返回一堆 MySQL 表结构片段。

10. similarity_score_threshold 搜索

写法:

retriever=vector_store.as_retriever(search_type="similarity_score_threshold",search_kwargs={"score_threshold":0.75,"k":10,})

含义:

只返回相似度达到阈值的文档。

适合:

宁愿少返回,也不要返回明显不相关内容; 知识库里噪声较多; 希望降低幻觉风险。

注意:

不同向量库的 score 方向可能不同。

有些是:

分数越高越相似。

有些是:

距离越小越相似。

所以阈值不要凭空设,应该先观察实际返回结果。

11.search_kwargs常见参数

search_kwargs是传给底层搜索方法的参数。

常见:

k: 最终返回多少个文档。 fetch_k: MMR 初始候选数量。 score_threshold: 相似度阈值。 filter: metadata 过滤条件。

例如:

retriever=vector_store.as_retriever(search_type="mmr",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_qa=Tag("category")=="QA"num_is_under_50=Num("num")<50filter_condition=category_is_qa&num_is_under_50 retriever=vector_store.as_retriever(search_type="mmr",search_kwargs={"k":4,"fetch_k":20,"filter":filter_condition,})

意思是:

只在 category=QA 且 num<50 的文档中检索。

metadata 过滤在真实项目里很重要。

比如:

只查某个用户有权限看的文档; 只查某个课程; 只查某类资料; 只查某个时间范围; 只查某个文件来源。

13. 先单独测试 Retriever

在写完整 RAG chain 之前,建议先测试 Retriever。

docs=retriever.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 prompt=ChatPromptTemplate.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):source=doc.metadata.get("source","unknown")page=doc.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 model=ChatOpenAI(model="gpt-4o-mini")prompt=ChatPromptTemplate.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())answer=rag_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="数据库表怎么设计?"docs=retriever.invoke(question)context=format_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="",flush=True)

这里的流式输出发生在模型生成阶段。

注意:

Retriever 通常不是流式的; 它会先一次性检索出文档; 然后模型再流式生成答案。

所以用户体验上是:

先等待检索完成; 再看到模型逐字/逐块输出。

22. Web 项目里的 SSE

如果接前端页面,可以用 SSE。

流程是:

后端 rag_chain.stream() -> 一边收到 chunk -> 一边通过 SSE 推给浏览器 -> 前端实时显示答案

关系是:

LangChain stream: 后端从 chain 里流式拿结果。 SSE: 后端把结果流式推给前端。

它们不是同一个东西,但经常一起用。

23. LangSmith 怎么调试 RAG

RAG 出问题时,不能只看最终答案。

要看每一步:

用户问题是什么? 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 搞错; 中文标点/换行导致切分效果差。

排查方式:

docs=retriever.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 过滤。

例如:

retriever=vector_store.as_retriever(search_type="mmr",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

如果再加 Web:

FastAPI 接收问题 -> 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相关技术,组合成的一个新项目。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询