LangChain 重构 RAG:LCEL 管道语法 + 多轮对话记忆

系列导读:本系列共 6 篇,带你从零到一构建完整的 RAG + LangGraph + MCP 项目。

  • 第 1 篇:最小 RAG 实现,纯 numpy,无任何 AI 框架
  • 第 2 篇:接入 Ollama 本地大模型,实现真实语义检索
  • 第 3 篇:接入 ChromaDB 持久化向量数据库
  • 第 4 篇(本文):用 LangChain 重构 + 多轮对话
  • 第 5 篇:LangGraph 多步推理工作流
  • 第 6 篇:MCP 工具调用协议集成

一、为什么要用 LangChain

前 3 篇我们手写了 RAG 的所有组件,目的是理解原理

但在实际工程中,手写意味着:

  • 重复造轮子(每个项目都要写 VectorStore、generate 等)
  • 缺少标准接口(换个模型就要改很多地方)
  • 没有生态(无法复用社区插件)

LangChain 提供了这些标准组件,让代码更简洁,切换组件更容易:

手写版 LangChain 版
embed() OllamaEmbeddings
手写 VectorStore Chroma(LangChain 封装)
手写 generate() Ollama LLM + StrOutputParser
手写 Prompt 拼接 ChatPromptTemplate
手写串联逻辑 `
bash 复制代码
pip install langchain langchain-community chromadb

二、新增功能:文档切片(TextSplitter)

前 3 篇直接处理短文本,实际场景中文档往往很长(PDF 几十页),需要先切片:

python 复制代码
from langchain.text_splitter import RecursiveCharacterTextSplitter

def load_and_split(raw_docs: list) -> list:
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=200,      # 每片最多 200 字符
        chunk_overlap=30,    # 相邻片段重叠 30 字符,避免关键信息被切断
        separators=["\n\n", "\n", "。", ",", " "],  # 优先按段落切
    )
    from langchain_core.documents import Document
    docs = [Document(page_content=text.strip()) for text in raw_docs]
    chunks = splitter.split_documents(docs)
    print(f"原始 {len(docs)} 篇 → 切片后 {len(chunks)} 个 chunk")
    return chunks

切片策略的选择

分隔符优先级 说明
\n\n 段落边界,语义最完整
\n 行边界
句子边界
子句边界
词边界(最后手段)

RecursiveCharacterTextSplitter 会按优先级尝试,尽量在语义完整的地方切分。


三、LCEL:用 | 串联组件

LangChain Expression Language(LCEL)是 LangChain 的核心语法,用 | 把组件串成管道,类似 Linux 管道:

python 复制代码
from langchain_community.llms import Ollama
from langchain_community.embeddings import OllamaEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough

# 构建检索器
embeddings = OllamaEmbeddings(model="nomic-embed-text")
vectorstore = Chroma(persist_directory="./chroma_db_lc", embedding_function=embeddings)
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})

# 构建 LLM
llm = Ollama(model="qwen2.5:7b")

# 定义 Prompt 模板
RAG_PROMPT = ChatPromptTemplate.from_template("""你是企业知识库助手。
根据以下资料回答问题,如果没有相关信息请说明。

【检索到的资料】
{context}

【用户问题】
{question}

【回答】""")

def format_docs(docs):
    return "\n\n".join(doc.page_content for doc in docs)

# LCEL 管道:每个 | 连接一个步骤
# 数据流:question → {context, question} → prompt → llm → str
chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | RAG_PROMPT
    | llm
    | StrOutputParser()
)

# 调用(支持流式输出)
for token in chain.stream("我想请假怎么申请?"):
    print(token, end="", flush=True)

LCEL 的优势

  • 每个 | 两侧都是标准 Runnable 对象
  • 自动支持 invoke(同步)、stream(流式)、batch(批量)
  • 可以随时替换任意组件,接口统一

四、新增功能:多轮对话记忆

这是本篇的核心新功能。前 3 篇每次问答都是独立的,模型不记得之前说过什么。

加入对话历史后:

复制代码
[第1轮] 年假有几天?
→ 员工入职满一年后享有15天年假,满三年后20天。

[第2轮] 满三年之后呢?(引用上轮)
→ 满三年后享有20天年假,满五年后25天。  ← 模型知道"之后"指什么

实现原理

python 复制代码
from langchain_core.messages import HumanMessage, AIMessage
from langchain_core.prompts import MessagesPlaceholder

CONV_PROMPT = ChatPromptTemplate.from_messages([
    ("system", """你是企业知识库助手。
根据检索到的资料和对话历史回答问题。

【检索到的资料】
{context}"""),
    MessagesPlaceholder(variable_name="history"),  # 对话历史插入这里
    ("human", "{question}"),
])

class ConversationalRAG:
    def __init__(self, vectorstore):
        self.retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
        self.llm = Ollama(model="qwen2.5:7b")
        self.history = []  # [HumanMessage, AIMessage, HumanMessage, AIMessage, ...]

    def chat(self, question: str) -> str:
        # 检索相关文档
        docs = self.retriever.get_relevant_documents(question)
        context = "\n\n".join(doc.page_content for doc in docs)

        # 构造带历史的 Prompt
        messages = CONV_PROMPT.format_messages(
            context=context,
            history=self.history,  # 把历史记录插入 Prompt
            question=question,
        )

        # 流式生成
        full_response = ""
        for chunk in self.llm.stream(messages):
            print(chunk, end="", flush=True)
            full_response += chunk
        print()

        # 把本轮对话追加到历史
        self.history.append(HumanMessage(content=question))
        self.history.append(AIMessage(content=full_response))
        return full_response

每轮对话后,Prompt 会包含完整的历史记录,模型可以引用之前的上下文回答问题。


五、完整代码结构

python 复制代码
# step4_langchain_rag.py

# ── 1. 文档切片 ──
chunks = load_and_split(RAW_DOCUMENTS)

# ── 2. 建立向量数据库 ──
vectorstore = Chroma.from_documents(
    documents=chunks,
    embedding=OllamaEmbeddings(model="nomic-embed-text"),
    persist_directory="./chroma_db_lc",
)

# ── 3. 基础 RAG Chain(单轮)──
chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | RAG_PROMPT | llm | StrOutputParser()
)

# 流式调用
for token in chain.stream("我想请假怎么申请?"):
    print(token, end="", flush=True)

# ── 4. 多轮对话 RAG ──
rag = ConversationalRAG(vectorstore)
rag.chat("年假有几天?")
rag.chat("满三年之后呢?")    # 引用上轮对话
rag.chat("请假流程是什么?")  # 话题切换

运行:

bash 复制代码
python3 step4_langchain_rag.py

六、多轮对话的注意事项

Token 膨胀问题:随着对话轮次增加,历史记录越来越长,Prompt 的 token 数会不断增大,最终超过模型的上下文窗口限制。

解决方案

python 复制代码
# 方案1:只保留最近 N 轮
MAX_HISTORY = 5
if len(self.history) > MAX_HISTORY * 2:
    self.history = self.history[-(MAX_HISTORY * 2):]

# 方案2:用 LangChain 的 ConversationSummaryMemory
# 自动把早期对话压缩成摘要

七、LangChain 生态图

复制代码
LangChain 核心
├── LLMs         → Ollama, OpenAI, Anthropic, ...
├── Embeddings   → OllamaEmbeddings, OpenAIEmbeddings, ...
├── VectorStores → Chroma, Faiss, Pinecone, Milvus, ...
├── Retrievers   → VectorStore, BM25, Ensemble, ...
├── Prompts      → ChatPromptTemplate, FewShotPrompt, ...
├── Chains       → LCEL (|), Sequential, ...
└── Memory       → Buffer, Summary, Entity, ...

切换任意组件只需改一行:

python 复制代码
# 换模型:
llm = Ollama(model="llama3:8b")     # 换成 llama3
llm = ChatOpenAI(model="gpt-4o")    # 换成 GPT-4o

# 换向量库:
vectorstore = Chroma(...)   → vectorstore = Pinecone(...)
vectorstore = Chroma(...)   → vectorstore = Faiss(...)

总结

本文用 LangChain 重构了前 3 篇的手写代码,新增了两个重要功能:

  1. TextSplitter:把长文档切成适合检索的小片段
  2. 多轮对话 :通过 MessagesPlaceholder 注入历史记录,让模型记住上下文

LCEL 的 | 管道语法是 LangChain 最核心的设计,理解它就理解了 LangChain 的工作方式。

下一篇引入 LangGraph,把 RAG 从单一链条升级为有条件分支的工作流,加入问题分析、检索质量评估、自动重试等高级能力。

相关推荐
暮霭c16 分钟前
Al 帮我写交易策略,三道关决定能不能跑
agent·ai编程·vibecoding
沉默王二1 小时前
IDEA 爽用 Claude Code 的终极方案,太丝滑。
agent·ai编程·claude
大流星2 小时前
LangChainJs之基础模型(一)
javascript·langchain
AIOps打工人2 小时前
我以为 LangChain 就是调用大模型,直到我写出第一条 Chain
langchain
doiito3 小时前
【Agent Harness】Gliding Horse 上下文感知与智能压缩:让 Agent 的“注意力”永不偏移
ai·rust·架构设计·系统设计·ai agent
葫芦和十三3 小时前
图解 MongoDB 25|分片架构三件套:mongos、config server 和 shard
后端·mongodb·agent
葫芦和十三9 小时前
图解 MongoDB 26|片键设计:决定集群命运的一个决定
后端·mongodb·agent
Avan_菜菜11 小时前
使用 Docker + rclone 自建 WebDAV
后端·agent·claude
lxmjlove17 小时前
读懂 AgentFlow:让 Agent 在「执行过程中」学会规划和用工具
agent