系列导读:本系列共 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 篇的手写代码,新增了两个重要功能:
- TextSplitter:把长文档切成适合检索的小片段
- 多轮对话 :通过
MessagesPlaceholder注入历史记录,让模型记住上下文
LCEL 的 | 管道语法是 LangChain 最核心的设计,理解它就理解了 LangChain 的工作方式。
下一篇引入 LangGraph,把 RAG 从单一链条升级为有条件分支的工作流,加入问题分析、检索质量评估、自动重试等高级能力。