走过了前面四篇文章,我们已经把 RAG 的每一块积木都摸透了:从加载文档、拆分文本,到向量化嵌入、存入数据库,再到精准检索。这些步骤单独拿出来,你都能讲得头头是道。但 RAG 从来不是单一步骤的炫技,而是一条首尾相连的流水线。
今天,我们就把这五道工序串成一条优雅的 LCEL 链------你向它丢一个问题,它自动完成检索、拼接、生成,最后吐出一个有据可依的回答。你即将亲手打造出本系列第一个真正意义上的"智能问答系统"。
一、把积木摆上桌面:回顾我们都有哪些零件
在动手组装之前,先快速清点一下已经掌握的"标准件":
- 文档加载 :
TextLoader、PyPDFLoader、DirectoryLoader把各种文件变成Document对象。 - 文本拆分 :
RecursiveCharacterTextSplitter把长文档切成语义完整的小块(chunk)。 - 嵌入模型 :
HuggingFaceEmbeddings(本地免费)或DashScopeEmbeddings(阿里云,中文更优)把文本块变成向量。 - 向量存储:Chroma、Redis、Pinecone 等,用于快速语义搜索。
- 检索器 :
vectorstore.as_retriever()负责根据查询返回最相关的文档片段。 - 聊天模型 :
ChatDeepSeek根据检索到的内容生成自然语言回答。 - 提示词模板 :
ChatPromptTemplate把检索结果和用户问题编织成一个完整的指令。 - 输出解析器 :
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生成回答。 parser把AIMessage变成纯文本。
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 脚本。但只需几步,它就能进化成一个持续的在线服务:
- 配置管理:把嵌入模型选择、数据库连接、k 值等参数放到 YAML 或环境变量里。
- API 封装 :用 FastAPI 包裹
rag_chain,暴露/chat或/ask接口。 - 日志与监控:接入 LangSmith,查看每次检索了哪些片段、模型看到了什么上下文。
- 文档更新:定期扫描新文件,触发增量加载与嵌入,更新向量库。
- 安全与权限:根据用户身份动态设置元数据过滤,实现知识库的权限隔离。
这条从原型到产品的进化路线,正是 LangChain 赋予我们的工程化能力。
五、今日收获与下篇预告
今天,我们亲手把 RAG 的完整流程落地为一条优雅的链:
- 你复习了 RAG 的八块积木,并看到了它们如何各就各位。
- 你用 LCEL 的
RunnablePassthrough和管道符把检索、提示词组装、模型生成、输出解析串成了一条自动化流水线。 - 你体验了如何为回答附上来源、实现流式输出以及异步调用,让 RAG 系统更接近生产形态。
至此,一个完整的、可运行的 RAG 智能问答系统已经在你手中诞生。但它还不完美------也许检索偶尔不准,也许切块的粒度不对,也许面对多轮对话会失忆,也许速度还有优化空间。
下一篇也是本系列的收官之作------《打磨与展望:RAG 的进阶技巧与避坑指南》。我们将直面这些现实问题,讨论检索精度调优、对话历史集成、数据持久化策略,并展望更复杂的 Agent 和工具生态。所有的"最后一公里",都将在那里打通。