【LangChain系列十二】RAG全流程下:从零搭建知识问答系统

文章目录

【LangChain系列十二】RAG全流程下:从零搭建知识问答系统

前面两篇分别讲了文档加载分割、嵌入模型和向量存储。这篇把它们串起来,搞一个完整的 RAG 知识问答系统。代码不多,但每一步都很关键。

整体流程长啥样

先看全貌,心里有数再写代码:

复制代码
用户提问
  ↓
嵌入模型 + 聊天模型(初始化)
  ↓
知识库检索(向量库 → 检索器)
  ↓
构建提示词(把检索到的文档和问题塞进模板)
  ↓
发送给 LLM
  ↓
流式输出结果(输出解析器)

核心思路就一句话:先从知识库里找到相关内容,拼到 prompt 里,再让 LLM 基于这些内容回答。 这样 LLM 的回答就有据可依,不会凭空编造。

为什么不直接把所有文档都塞进 prompt?因为文档量大了,token 数会撑爆上下文窗口,而且无关信息太多反而会干扰 LLM 的判断。所以先检索再生成,是目前最实用的方案。

第一步:初始化模型和向量库

python 复制代码
from langchain_community.embeddings import DashScopeEmbeddings
from langchain_openai import ChatOpenAI
from langchain_redis import RedisConfig, RedisVectorStore

# 嵌入模型
embeddings = DashScopeEmbeddings(model="text-embedding-v4")

# 聊天模型
model = ChatOpenAI(model="gpt-4o", temperature=0)

# Redis 向量库
config = RedisConfig(
    index_name="knowledge_base",
    redis_url="redis://localhost:6379",
)
vector_store = RedisVectorStore(embeddings, config=config)

temperature=0 是为了让回答尽量稳定。知识问答场景不希望模型"发挥创造力",要的是准确、忠实于原文的回答。你设成 0.7 试试,同样的问题每次回答可能都不一样,这在知识问答里是减分项。

第二步:加载文档到向量库

如果你还没有往 Redis 里灌数据,需要先做这一步:

python 复制代码
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter

# 加载文档
loader = TextLoader("your_docs.txt", encoding="utf-8")
docs = loader.load()

# 分割
splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_size_overlap=50,
)
chunks = splitter.split_documents(docs)

# 给每个 chunk 加元数据
for chunk in chunks:
    chunk.metadata["source"] = "your_docs.txt"

# 存入向量库
vector_store.add_documents(chunks)

chunk_sizeoverlap 的选择很关键。太大会塞太多无关信息给 LLM,太小会丢失上下文。500 左右是经验值,具体看你文档的结构------段落清晰的文档可以大一点,流水账式的文档要小一点。

overlap=50 的作用是让相邻 chunk 之间有 50 个字符的重叠,避免在句子中间切断导致语义不完整。

第三步:定义检索器

python 复制代码
retriever = vector_store.as_retriever(
    search_type="similarity",
    search_kwargs={"k": 4}
)

k=4 表示每次检索返回最相似的 4 个文档块。k 太大,prompt 会被撑爆,而且无关内容会干扰 LLM;k 太小,可能漏掉关键信息。4-6 是个不错的起点,实际项目里根据回答质量再调。

search_type 还支持 "mmr"(最大边际相关性),可以在相似度和多样性之间做平衡。如果你发现检索出来的内容太雷同,可以试试 MMR。

第四步:定义提示词模板

python 复制代码
from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_template("""
你是一个知识问答助手。请根据以下检索到的上下文来回答问题。
如果上下文中没有相关信息,请直接说"根据现有资料,我无法回答这个问题",不要编造答案。

上下文:
{context}

问题:
{question}

回答:
""")

这个模板有几个要点:

  • 明确告诉 LLM 基于上下文回答,别自由发挥。这是 RAG 和普通聊天的核心区别
  • 不知道就说不知道,别编。RAG 最怕的就是幻觉------LLM 一本正经地胡说八道,用户还以为是真的
  • 角色设定简洁就行,不需要花哨的 system prompt。把精力放在上下文的质量上,比写一大段角色描述有用得多

第五步:format_docs 函数

检索器返回的是 Document 对象列表,但 prompt 需要的是纯文本字符串。这个转换函数很简单:

python 复制代码
def format_docs(docs):
    return "\n\n".join(doc.page_content for doc in docs)

每个文档块之间用两个换行隔开,让 LLM 能区分不同段落。为什么不加序号或者标签?因为实测下来,简单的换行分隔效果最好,太多格式标记反而会干扰 LLM 的理解。

第六步:组装 Chain

LangChain 的链式调用,把各环节串起来:

python 复制代码
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough

chain = (
    {
        "context": retriever | format_docs,   # 检索 → 格式化
        "question": RunnablePassthrough()      # 原样传递问题
    }
    | prompt      # 填充模板
    | model       # 调用 LLM
    | StrOutputParser()  # 提取纯文本
)

拆解一下这个 chain 的数据流:

  1. retriever | format_docs:拿到问题 → 检索相关文档 → 转成文本字符串
  2. RunnablePassthrough():把原始问题透传过去
  3. 两个结果拼成 {"context": "...", "question": "..."} 喂给 prompt
  4. prompt 渲染后发给 model
  5. model 返回的 Message 对象经过 StrOutputParser 提取成纯文本

这里用到了 LangChain 的 LCEL(LangChain Expression Language) ,管道符 | 就是把上一步的输出接到下一步的输入。第一次看可能觉得绕,多写几次就习惯了。

RunnablePassthrough() 是个透传组件,输入啥输出啥。这里用它是因为 prompt 需要两个变量(context 和 question),context 靠检索器生成,question 直接传原始输入。

第七步:流式输出

对于知识问答,流式输出体验好很多------用户不用等 LLM 全部生成完才看到结果:

python 复制代码
question = "LangChain是什么?"

for chunk in chain.stream(question):
    print(chunk, end="", flush=True)

chain.stream() 会逐 token 返回结果,end=""flush=True 保证实时打印。如果是 Web 应用,可以把每个 chunk 通过 SSE(Server-Sent Events)推给前端,体验和 ChatGPT 一样。

完整代码汇总

python 复制代码
from langchain_community.embeddings import DashScopeEmbeddings
from langchain_openai import ChatOpenAI
from langchain_redis import RedisConfig, RedisVectorStore
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough

# 初始化
embeddings = DashScopeEmbeddings(model="text-embedding-v4")
model = ChatOpenAI(model="gpt-4o", temperature=0)

config = RedisConfig(index_name="knowledge_base", redis_url="redis://localhost:6379")
vector_store = RedisVectorStore(embeddings, config=config)
retriever = vector_store.as_retriever(search_kwargs={"k": 4})

# 格式化函数
def format_docs(docs):
    return "\n\n".join(doc.page_content for doc in docs)

# 提示词模板
prompt = ChatPromptTemplate.from_template("""
根据以下上下文回答问题。如果没有相关信息,请说"无法回答"。

上下文:
{context}

问题:{question}
""")

# 组装 chain
chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | prompt
    | model
    | StrOutputParser()
)

# 流式输出
for chunk in chain.stream("LangChain是什么?"):
    print(chunk, end="", flush=True)

整个流程不到 40 行代码,这就是 LangChain 的优势------把复杂的 RAG 流程抽象成了简洁的管道。底层的向量检索、prompt 组装、LLM 调用、流式输出,全帮你封装好了。

几个踩坑经验

  1. 文档质量 > 一切:垃圾文档灌进去,检索出来的也是垃圾,LLM 再强也救不了。花时间打磨文档质量,比调模型参数有用 10 倍
  2. chunk_size 要调:没有万能值,得根据文档类型试。技术文档可以大一点(500-800),FAQ 类的要小(200-400)
  3. 元数据过滤很有用 :比如你的知识库有产品文档和内部文档,检索时加上 filter 可以只搜特定来源,避免检索到不相关的内容
  4. 别忘了异常处理:Redis 连不上、LLM 超时、文档为空......生产环境这些都得兜住,不然一个请求挂了整个服务跟着挂
  5. 评估很重要:搭完系统后,准备一批测试问题,看看回答质量,调参才有方向。瞎调不如不调

这个方案的局限性

说实话,基础 RAG 有几个明显短板:

  • 复杂问题拆解不了:如果用户问的是需要综合多个文档才能回答的问题,单次检索可能不够
  • 检索质量受 embedding 模型影响大:换一个嵌入模型,效果可能差很多,而且很难预判
  • 长文档处理不好:chunk 切割不可避免地会破坏文档结构,尤其是表格、代码块这些结构化内容
  • 没有推理能力:RAG 只是"找到相关内容然后让 LLM 总结",遇到需要多步推理的问题就力不从心

这些局限性,后面在 LangGraph 系列里会通过更复杂的 agent 架构来解决。比如多轮检索、自我反思、工具调用这些能力,都需要跳出简单的 RAG 管道。


觉得有帮助的话,点个赞👍收藏⭐支持一下吧!

相关推荐
Darling噜啦啦1 小时前
手写 mini-cursor 完整版:从 Promise 子进程到 ReAct 循环,30 轮迭代完成 TodoList 全流程
langchain·agent
飞猪~2 小时前
Langchain python版本 LLM 重要函数invoke,ainvoke,stream,astream,batch,abatch
python·langchain·batch
大流星5 小时前
LangChainJS之Runnable(三)
javascript·langchain
想要成为糕糕手5 小时前
从 0 到 1 打造 Coding Agent——mini cursor(终篇):装上命令行手脚,造一个会"干活"的 AI 程序员
langchain·agent·ai编程
BraveWang5 小时前
【LangChain 1.x】03、Models 模型层简介与 init_chat_model 模型统一初始化
langchain
先吃饱再说5 小时前
手写一个 mini-cursor:LangChain Agent 开发完全实战
langchain·llm·agent
风吹夏回6 小时前
LangChain Agent 入门实战:从报错到跑通带记忆的交互式智能体(初学者保姆级教程)
langchain
大鹅同志1 天前
Agent Runtime Evaluation Platform — AI Agent 运行时全维度质量评估平台
ai·langchain·agent·rag·langgraph·vibecoding·llm-as-judge
闲猫1 天前
深入理解 Skills:从 API 调用到智能体行为封装
人工智能·python·langchain