融会贯通:打造完整的 RAG 问答链

走过了前面四篇文章,我们已经把 RAG 的每一块积木都摸透了:从加载文档、拆分文本,到向量化嵌入、存入数据库,再到精准检索。这些步骤单独拿出来,你都能讲得头头是道。但 RAG 从来不是单一步骤的炫技,而是一条首尾相连的流水线

今天,我们就把这五道工序串成一条优雅的 LCEL 链------你向它丢一个问题,它自动完成检索、拼接、生成,最后吐出一个有据可依的回答。你即将亲手打造出本系列第一个真正意义上的"智能问答系统"。


一、把积木摆上桌面:回顾我们都有哪些零件

在动手组装之前,先快速清点一下已经掌握的"标准件":

  1. 文档加载TextLoaderPyPDFLoaderDirectoryLoader 把各种文件变成 Document 对象。
  2. 文本拆分RecursiveCharacterTextSplitter 把长文档切成语义完整的小块(chunk)。
  3. 嵌入模型HuggingFaceEmbeddings(本地免费)或 DashScopeEmbeddings(阿里云,中文更优)把文本块变成向量。
  4. 向量存储:Chroma、Redis、Pinecone 等,用于快速语义搜索。
  5. 检索器vectorstore.as_retriever() 负责根据查询返回最相关的文档片段。
  6. 聊天模型ChatDeepSeek 根据检索到的内容生成自然语言回答。
  7. 提示词模板ChatPromptTemplate 把检索结果和用户问题编织成一个完整的指令。
  8. 输出解析器StrOutputParser 把模型回复整理成纯文本。

这八块积木,任何一块你都能独立使用。现在,我们要用 LCEL 的管道符 |,把它们拼成一条没有焊点的流水线。


二、第一条全流程 RAG 链:从问题到答案

我们假设你已经按照前面几篇文章的指导,把文档加载、拆分、嵌入、存入 Chroma 数据库这一步完成了。如果你还没有现成的向量库,我们可以先"快进"一下------用一个最小化的脚本,把所有准备工作一次性跑通。

2.1 一分钟备料:准备好向量库

python 复制代码
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_chroma import Chroma

# 1. 加载文档(假设你有一个公司规章的 txt 文件)
loader = TextLoader("company_rules.txt", encoding="utf-8")
docs = loader.load()

# 2. 拆分
splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=50,
    separators=["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""]
)
chunks = splitter.split_documents(docs)

# 3. 嵌入模型(本地免费)
embedder = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")

# 4. 存入 Chroma
vectorstore = Chroma.from_documents(
    documents=chunks,
    embedding=embedder,
    persist_directory="./rag_demo_db"
)

print(f"知识库就绪,共 {len(chunks)} 个片段。")

这段脚本运行一次后,本地的 ./rag_demo_db 目录就存储了你的知识库。后面再次启动时,只需加载,不必重复处理。

2.2 用 LCEL 串起 RAG 链

现在进入正题------构建那条梦寐以求的链。我们将用到 RunnablePassthrough 这个"透明管道",它能把用户的原始问题原封不动地传给下一个环节,同时我们又能在它身上"挂载"上检索结果。

python 复制代码
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
from langchain_deepseek import ChatDeepSeek

# 1. 加载已有向量库
vectorstore = Chroma(
    embedding_function=embedder,
    persist_directory="./rag_demo_db"
)

# 2. 从向量库创建检索器
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})

# 3. 准备提示词模板:将检索到的上下文 {context} 和用户问题 {question} 融合
prompt = ChatPromptTemplate.from_messages([
    ("system", """你是一个严谨的企业内部问答助手。
请严格根据以下参考资料回答问题。如果资料不足以回答问题,请如实说"根据现有资料无法回答"。
不要编造任何信息。

参考资料:
{context}"""),
    ("user", "{question}")
])

# 4. 创建聊天模型
model = ChatDeepSeek(model="deepseek-chat", temperature=0)

# 5. 输出解析器
parser = StrOutputParser()

# 6. 用 LCEL 组装全链
rag_chain = (
    {"context": retriever, "question": RunnablePassthrough()}
    | prompt
    | model
    | parser
)

这条链的逻辑非常清晰:

  • {"context": retriever, "question": RunnablePassthrough()}:输入是个字符串(用户问题)。RunnablePassthrough 把问题原样放进 question 字段;同时,同一个问题传给 retriever,检索结果放进 context 字段。最终这个 Runnable 吐出一个 {"context": [Document,...], "question": "原问题"} 的字典。
  • 这个字典直接喂给 prompt,模板把检索到的文本块拼接好,替换 {context} 占位符。
  • 格式化后的提示词发给 model 生成回答。
  • parserAIMessage 变成纯文本。

2.3 测试你的 RAG 系统

python 复制代码
# 问一个知识库里应该有的问题
question = "员工请假需要提前几天申请?"
answer = rag_chain.invoke(question)
print(f"问:{question}")
print(f"答:{answer}")

你会得到一个基于文档内容的回答,而不是模型的"自由发挥"。如果问题超出知识库范围,模型会按要求说"无法回答"。


三、不止一问一答:让 RAG 链更强大

现在这条链已经能跑通,但我们可以让它更聪明、更友好。

3.1 带上来源引用

用户不只想得到答案,还想知道"这说法从哪来的"。我们只需稍微改造提示词,让模型在回答时引用来源。

python 复制代码
prompt_with_source = ChatPromptTemplate.from_messages([
    ("system", """你是一个严谨的企业内部问答助手。
请根据以下参考资料回答问题。每条回答后,列出你依据的参考来源(文档名称或片段标号)。

参考资料:
{context}"""),
    ("user", "{question}")
])

rag_chain_with_source = (
    {"context": retriever, "question": RunnablePassthrough()}
    | prompt_with_source
    | model
    | parser
)

answer = rag_chain_with_source.invoke("请假流程")
print(answer)

如果 metadata 里带有 source 字段,检索结果中就自然携带了出处信息,模型可以利用它们生成引用。

3.2 流式输出:让回答一字一字蹦出来

在第 11 篇文章中我们学过流式传输。把 rag_chain 最后的 parser 去掉,链会输出 AIMessage,然后我们可以用 stream 获得流式效果。但即使保留 parser,由于 LCEL 的智能传递,链本身就支持 stream

python 复制代码
# 流式输出回答
for chunk in rag_chain.stream("解释一下加班的调休规定"):
    print(chunk, end="", flush=True)

用户将看到答案逐字生成,体验拉满。

3.3 异步查询:不阻塞你的 FastAPI

如果你的 RAG 链要部署为 Web 服务,记得用异步版本:

python 复制代码
import asyncio

async def ask_rag(question: str) -> str:
    return await rag_chain.ainvoke(question)

# 在异步环境中直接 await
# answer = await ask_rag("年假如何计算?")

从模型调用到检索,整个链条全线支持异步,搭配 FastAPI 流式响应,便是生产级 AI 问答的后端骨架。


四、完整的项目蓝图:从脚本到服务

现在你的 RAG 系统还只是一个 Python 脚本。但只需几步,它就能进化成一个持续的在线服务:

  1. 配置管理:把嵌入模型选择、数据库连接、k 值等参数放到 YAML 或环境变量里。
  2. API 封装 :用 FastAPI 包裹 rag_chain,暴露 /chat/ask 接口。
  3. 日志与监控:接入 LangSmith,查看每次检索了哪些片段、模型看到了什么上下文。
  4. 文档更新:定期扫描新文件,触发增量加载与嵌入,更新向量库。
  5. 安全与权限:根据用户身份动态设置元数据过滤,实现知识库的权限隔离。

这条从原型到产品的进化路线,正是 LangChain 赋予我们的工程化能力。


五、今日收获与下篇预告

今天,我们亲手把 RAG 的完整流程落地为一条优雅的链:

  • 你复习了 RAG 的八块积木,并看到了它们如何各就各位。
  • 你用 LCEL 的 RunnablePassthrough 和管道符把检索、提示词组装、模型生成、输出解析串成了一条自动化流水线。
  • 你体验了如何为回答附上来源、实现流式输出以及异步调用,让 RAG 系统更接近生产形态。

至此,一个完整的、可运行的 RAG 智能问答系统已经在你手中诞生。但它还不完美------也许检索偶尔不准,也许切块的粒度不对,也许面对多轮对话会失忆,也许速度还有优化空间。

下一篇也是本系列的收官之作------《打磨与展望:RAG 的进阶技巧与避坑指南》。我们将直面这些现实问题,讨论检索精度调优、对话历史集成、数据持久化策略,并展望更复杂的 Agent 和工具生态。所有的"最后一公里",都将在那里打通。

相关推荐
deephub6 小时前
构建一个可自我改进的多 Agent RAG 系统:架构、评估,以及带人工审核的 Prompt 反馈闭环
人工智能·python·大语言模型·rag
信竞星球_少儿编程题库6 小时前
2026年全国信息素养大赛算法应用主题赛 丝路新城 Python 模拟卷(三)
开发语言·python·算法
进击切图仔6 小时前
python 工程使用 .env getenv 安全加载环境变量(备忘)
chrome·python·安全
TechWayfarer7 小时前
出海APP本地化实战:基于IP归属地API的网关路由与多语言自动切换方案
网络·python·网络协议·tcp/ip
wj3055853787 小时前
课程 5:将官方 LTX-2.3 工作流改造成 GGUF 主模型工作流
python·cuda·comfyui
Muyuan19987 小时前
31.Cursor 初体验:用 AI Agent 给 PaperPilot 做一次最小工程重构
人工智能·python·重构·django·fastapi·faiss
范范@7 小时前
python基础-5大容器
开发语言·python
RSTJ_16257 小时前
PYTHON+AI LLM DAY FOURTY-NINE
人工智能·python·深度学习
测试员周周7 小时前
【AI测试路线图2】功能测试转 AI 测试:4~5 个月,一条最稳的路
开发语言·人工智能·python·功能测试·测试工具·单元测试·pytest