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 从单一链条升级为有条件分支的工作流,加入问题分析、检索质量评估、自动重试等高级能力。

相关推荐
1941s2 小时前
Google Agent Development Kit (ADK) 指南 第四章:Agent 开发与编排
人工智能·python·langchain·agent·adk
qq_452396232 小时前
【Python × AI】LangChain 深度剖析:从组件解耦到 LCEL 的逻辑美学
人工智能·python·ai·langchain
GISer_Jing2 小时前
两种AI交互方式深度解析——浏览器书签&插件
前端·人工智能·ai·prompt
科技新芯2 小时前
当AI龙虾接管购物车,Bidnex用CPS重构数字广告的信任基石
人工智能·重构
胡少侠72 小时前
ReAct Agent:手写 Thought-Action-Observe 循环,从工具调用到真正的 Agent
ai·agent·react·rag
一直会游泳的小猫2 小时前
CC-Switch使用指南
ai·claude code·ai配置管理工具
weixin_449290013 小时前
端到端智能对话系统架构文档
ai
夏白分享社3 小时前
OpenClaw 本地模型终极实战:vLLM 部署优化完整教程!
ai·开源软件·openclaw