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

相关推荐
码途漫谈7 小时前
Easy-Vibe开发篇阅读笔记(四)——前端开发之结合 Agent Skills 美化界面
人工智能·笔记·ai·开源·ai编程
冬奇Lab7 小时前
RAG 系列(二):用 LangChain 搭建你的第一个 RAG Pipeline
人工智能·langchain·llm
Flying pigs~~8 小时前
Agent 完整面试指南:原理、框架、架构模式
大模型·prompt·agent·rag·agent架构·人工只能
Mr_sst8 小时前
Claude Code 部署与使用保姆级教程(2026 最新)
python·ai
@PHARAOH9 小时前
WHAT - cursor cli 开发范式
前端·ai·ai编程
Flying pigs~~9 小时前
RAG 完整面试指南:原理、优化、幻觉解决方案
人工智能·prompt·rag·智能体·检索增强生成·rag优化
企业架构师老王10 小时前
2026制造业安全生产隐患识别AI方案:从主流产品对比看企业级AI Agent的非侵入式落地路径
人工智能·安全·ai
cd_9492172111 小时前
融美系质感与欧系纯净 蒂曼蒂重构高品涂料行业新标杆
重构
xixixi7777711 小时前
三重筑基:5G-A超级上行提速千兆,电联低频共享扫平盲点,800V HVDC算电协同破局
人工智能·5g·ai·大模型·算力·通信·信通院
金融小师妹11 小时前
4月30日多因子共振节点:鲍威尔“收官效应”与权力结构重塑的预期重构
大数据·人工智能·重构·逻辑回归