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

相关推荐
阿里云云原生几秒前
阿里云的 Agent Infra 长什么样
阿里云·云计算·agent
幂律智能几秒前
从AI使用风险到合同智能审查重构企业风控能力
人工智能·重构
fruge2 分钟前
数字人从演示到场景落地:突破交互瓶颈,走进真实服务
microsoft·ai·交互
caicongyang8 分钟前
开源项目OpenCLI 扫盲
agent·cdp·opencli
小歪不歪我是AI16 分钟前
Pi 源码拆解:当一个极简主义的 agent harness 只有 4 个 tool
开源·agent
Ai.den26 分钟前
Windows 安装 MinerU 3.x 实现本地批量解析 PDF
人工智能·windows·ai
元思未来27 分钟前
Hermes Agent 源码探秘 (4):工具系统 — Agent 的"双手"
agent
studentliubo29 分钟前
重生之点亮Agent技术栈--agent
agent·ai编程
鼎道开发者联盟1 小时前
跳出传统 RAG!用 LLM Wiki 构建闭环式产品 Agent 协作体系
agent·rag·hermes·llmwiki