LangChain 知识总结详解(二)--- 编排与检索
Memory · Chains (LCEL) · Retrieval (RAG)
📂 LangChain 知识总结详解系列
目录
- [六、Memory 记忆组件](#六、Memory 记忆组件)
- [七、Chains 链路组件(LCEL)](#七、Chains 链路组件(LCEL))
- [八、Retrieval 检索组件(RAG)](#八、Retrieval 检索组件(RAG))
六、Memory 记忆组件
6.1 为什么 LLM 会"失忆"
LLM 本身是无状态 的 --- 每次 model.invoke() 都是独立的调用,AI 不知道上一轮说了什么。
第1次调用: model.invoke("我叫小明") → AI: "你好小明"
第2次调用: model.invoke("我叫什么?") → AI: "我不知道" ← 失忆了!
6.2 手动实现记忆
用 Python 列表保存历史,每次调用时把完整历史传给模型:
python
from langchain_core.messages import HumanMessage, AIMessage
history = []
# 第1轮
history.append(HumanMessage(content="我叫小明,今年10岁。"))
response = model.invoke(history)
history.append(AIMessage(content=response.content))
# 第2轮 --- 带完整历史
history.append(HumanMessage(content="我叫什么名字?"))
response = model.invoke(history) # AI 能记住"小明"
history.append(AIMessage(content=response.content))
6.3 短期记忆(线程级别)
LangChain 通过 Checkpointer + thread_id 实现线程级别的短期记忆:
python
from langgraph.checkpoint.memory import InMemorySaver
from langchain.agents import create_agent
agent = create_agent(
model=model,
tools=[get_weather],
system_prompt="你是一个有用的助手。",
checkpointer=InMemorySaver(), # 添加记忆存储
)
config = {"configurable": {"thread_id": "1"}}
# 第1轮
r = agent.invoke({"messages": [HumanMessage("我叫小明,我在北京。")]}, config)
# 第2轮 --- Agent 记住了"北京"
r = agent.invoke({"messages": [HumanMessage("我所在城市天气怎么样?")]}, config)
# 不同线程 --- 记忆隔离
other_config = {"configurable": {"thread_id": "2"}}
r = agent.invoke({"messages": [HumanMessage("还记得我的名字吗?")]}, other_config)
# AI 不记得"小明"(不同线程)
关键概念:
| 概念 | 说明 |
|---|---|
InMemorySaver |
内存存储,重启丢失(开发用) |
thread_id |
线程标识,同一线程共享历史,不同线程互不干扰 |
context |
每次运行携带的上下文数据(不持久化) |
生产环境 Checkpointer:
| 存储 | 类 | 适用场景 |
|---|---|---|
| 内存 | InMemorySaver |
开发测试 |
| PostgreSQL | PostgresSaver |
生产环境(推荐) |
| SQLite | SqliteSaver |
轻量级生产 |
| Azure Cosmos DB | CosmosDBSaver |
Azure 云环境 |
6.4 自定义 AgentState
通过 state_schema 扩展 Agent 状态,添加自定义字段:
python
from typing import TypedDict, Annotated
from langgraph.graph.message import add_messages
class MyAgentState(TypedDict):
messages: Annotated[list, add_messages] # 消息历史(必须)
user_id: str # 自定义:用户ID
preferences: dict # 自定义:用户偏好
current_task: str # 自定义:当前任务
agent = create_agent(
model=model,
tools=[...],
state_schema=MyAgentState,
)
6.5 长对话的三种处理模式
对话过长时,Token 消耗增大且可能超出上下文限制。三种常见处理策略:
模式1:修剪消息(Trim Messages)
保留第一条(系统提示)+ 最近 K 条消息,中间的丢弃:
python
from langchain.agents.middleware import before_model, AgentState
from langchain_core.messages import RemoveMessage
KEEP = 3 # 保留最近3条
@before_model
def trim_messages(state: AgentState, runtime) -> dict | None:
messages = state["messages"]
if len(messages) <= KEEP + 1: # +1 是系统提示
return None
first = messages[0] # 保留系统提示
recent = messages[-KEEP:] # 保留最近K条
return {"messages": [RemoveMessage(id=m.id) for m in messages[1:-KEEP]]}
模式2:删除消息(Delete Messages)
删除特定 ID 的消息或清空所有消息:
python
from langchain_core.messages import RemoveMessage
from langgraph.graph.message import REMOVE_ALL_MESSAGES
# 删除特定消息
RemoveMessage(id="msg_abc123")
# 清空所有消息
{"messages": [RemoveMessage(id=REMOVE_ALL_MESSAGES)]}
# 清空后重新添加系统提示
{"messages": [RemoveMessage(id=REMOVE_ALL_MESSAGES), system_prompt]}
模式3:摘要消息(Summarize Messages)
用 AI 自动总结历史对话,保留关键信息(不像修剪/删除会丢失信息):
python
from langchain.agents.middleware import SummarizationMiddleware
agent = create_agent(
model=model,
tools=[...],
middleware=[
SummarizationMiddleware(
model=summary_model, # 用于生成摘要的模型
max_tokens_before_summary=4000, # 触发摘要的 Token 阈值
messages_to_keep=10, # 摘要后保留的最近消息数
),
],
)
6.6 在工具中访问记忆
工具函数可以通过 runtime 访问和修改 Agent 状态:
python
from langchain_core.tools import tool
from langchain.agents import ToolRuntime
from langchain.types import Command
@tool
def save_preference(key: str, value: str, runtime: ToolRuntime) -> str:
"""保存用户偏好设置。"""
# 读取短期记忆
current_state = runtime.state
user_id = current_state.get("user_id", "unknown")
# 写入短期记忆(通过 Command 更新状态)
preferences = current_state.get("preferences", {})
preferences[key] = value
return Command(
update={"preferences": preferences}, # 更新 Agent 状态
)
@tool
def get_user_info(runtime: ToolRuntime) -> str:
"""获取当前用户信息。"""
# 读取短期记忆
state = runtime.state
return f"用户ID: {state.get('user_id')}, 偏好: {state.get('preferences')}"
6.7 旧版 Memory 组件(Legacy)
以下组件属于 LangChain 旧版 API,新项目建议使用 Checkpointer + Middleware 方案:
ConversationBufferMemory --- 完整记忆
保存所有对话,一字不漏。适合短对话,缺点是对话长了消耗大量 Token:
python
from langchain_classic.memory import ConversationBufferMemory
memory = ConversationBufferMemory(return_messages=True)
memory.save_context({"input": "你好,我叫小明。"}, {"output": "你好小明!"})
memory.save_context({"input": "我今年10岁。"}, {"output": "10岁正是学习的好年纪!"})
# 取出所有历史
history = memory.load_memory_variables({})["chat_history"]
ConversationBufferWindowMemory --- 窗口记忆
只保留最近 K 轮对话,旧的自动丢弃,节省 Token:
python
from langchain_classic.memory import ConversationBufferWindowMemory
memory = ConversationBufferWindowMemory(k=3, return_messages=True)
# 保存5轮,但只保留最近3轮
6.8 记忆策略对比
| 策略 | 比喻 | 保留内容 | 信息丢失 | 适用场景 |
|---|---|---|---|---|
| Buffer(完整记忆) | 录像机 | 全部对话 | 无 | 短对话、需完整上下文 |
| Window(窗口记忆) | 行车记录仪 | 最近 K 轮 | 早期对话 | 长对话、节省 Token |
| Trim(修剪) | 剪辑 | 首条 + 最近 K 条 | 中间部分 | 保留系统提示 + 近期 |
| Delete(删除) | 格式化 | 可选 | 被删除的部分 | 重置对话 |
| Summarize(摘要) | 读书笔记 | 摘要 + 最近消息 | 最少 | 长对话、需保留关键信息 |
七、Chains 链路组件(LCEL)
7.1 什么是 LCEL
LCEL(LangChain Expression Language)是 LangChain 的核心编排语法,用 | 操作符把组件串联成管道(Pipeline)。
Pipeline = "流水线" = 用 | 操作符把多个组件串起来,数据自动流转
7.2 核心 LCEL 组件
| 组件 | 作用 | 示例 |
|---|---|---|
RunnablePassthrough |
透传输入,不做任何变换 | 传递原始输入到下游 |
RunnableLambda |
包装任意 Python 函数为 Runnable | 自定义处理逻辑 |
RunnableParallel |
并行执行多个 Runnable | 同时执行多个任务 |
python
from langchain_core.runnables import RunnablePassthrough, RunnableLambda, RunnableParallel
# RunnablePassthrough --- 透传
chain = RunnablePassthrough()
chain.invoke("hello") # → "hello"
# RunnableLambda --- 自定义函数
chain = RunnableLambda(lambda x: x.upper())
chain.invoke("hello") # → "HELLO"
# RunnableParallel --- 并行
chain = RunnableParallel({
"original": RunnablePassthrough(),
"upper": RunnableLambda(lambda x: x.upper()),
"length": RunnableLambda(lambda x: len(x)),
})
chain.invoke("hello")
# → {"original": "hello", "upper": "HELLO", "length": 5}
7.3 三段式 Pipeline
最经典的 LCEL 模式:Prompt → Model → Parser
python
from langchain_core.prompts import PromptTemplate
from langchain_ollama import ChatOllama
from langchain_core.output_parsers import StrOutputParser
prompt = PromptTemplate.from_template("请用一句话解释{concept}。")
model = ChatOllama(model="qwen3.5:2b")
parser = StrOutputParser()
# 用 | 串成 Pipeline
chain = prompt | model | parser
# 投入数据,自动完成所有步骤
result = chain.invoke({"concept": "人工智能"})
# result 已经是纯字符串
数据流图:
{"concept": "人工智能"}
│
▼
┌──────────────────┐
│ PromptTemplate │ 填充模板变量
│ from_template() │ → "请用一句话解释人工智能。"
└────────┬─────────┘
│
▼
┌──────────────────┐
│ ChatOllama │ 调用 LLM
│ invoke() │ → AIMessage(content="人工智能是...")
└────────┬─────────┘
│
▼
┌──────────────────┐
│ StrOutputParser │ 提取纯文本
│ │ → "人工智能是..."
└──────────────────┘
7.4 顺序链(Sequential Chain)
多步骤串联,上一步的输出作为下一步的输入:
python
# Chain 1: 生成大纲
outline_chain = (
PromptTemplate.from_template('为"{topic}"生成3个要点大纲。\n大纲:')
| model | parser
)
# Chain 2: 根据大纲写内容
content_chain = (
PromptTemplate.from_template("根据大纲写一段内容:\n{outline}\n\n内容:")
| model | parser
)
# Chain 3: 润色
polish_chain = (
PromptTemplate.from_template("润色以下内容,使其更流畅:\n{content}\n\n润色后:")
| model | parser
)
# 手动串联
outline = outline_chain.invoke({"topic": "人工智能"})
content = content_chain.invoke({"outline": outline})
polished = polish_chain.invoke({"content": content})
7.5 路由链(Router Chain)
根据输入类型,自动选择不同的处理路径:
方式1:if-else 路由(手动指定任务类型)
python
def router(text: str, task: str) -> str:
chains = {
"translate": translate_chain,
"summarize": summarize_chain,
"explain": explain_chain,
}
return chains.get(task, explain_chain).invoke({"text": text})
# 使用
result = router("Hello World", "translate")
方式2:智能路由(AI 自动判断类型)
python
classifier = (
PromptTemplate.from_template(
"判断输入类型(translate/summarize/explain):\n{input}\n类型:"
) | model | StrOutputParser()
)
def smart_router(user_input: str) -> str:
task = classifier.invoke({"input": user_input})
if "translate" in task:
return translate_chain.invoke({"text": user_input})
elif "summarize" in task:
return summarize_chain.invoke({"text": user_input})
else:
return explain_chain.invoke({"text": user_input})
# 使用 --- AI 自动判断应该翻译、总结还是解释
result = smart_router("Please translate this to Chinese: Hello World")
7.6 Chain 工厂函数
封装成函数,方便复用和参数化:
python
def create_translator(source_lang: str, target_lang: str):
"""翻译链工厂函数"""
prompt = PromptTemplate.from_template(f"""
你是一位专业翻译,请将文本从{source_lang}翻译成{target_lang}。
原文:{{{{text}}}}
译文:""")
model = ChatOllama(model="qwen3.5:2b")
return prompt | model | StrOutputParser()
# 创建不同的翻译链
en_to_zh = create_translator("英语", "中文")
zh_to_en = create_translator("中文", "英语")
# 使用
en_to_zh.invoke({"text": "Hello World"})
zh_to_en.invoke({"text": "你好世界"})
7.7 Chain 的 invoke/stream/batch
所有 Chain(本质是 Runnable)都支持三种调用方式:
python
chain = prompt | model | parser
# 单次调用
result = chain.invoke({"concept": "AI"})
# 流式输出
for chunk in chain.stream({"concept": "AI"}):
print(chunk, end="", flush=True)
# 批量调用
results = chain.batch([
{"concept": "AI"},
{"concept": "ML"},
{"concept": "DL"},
])
八、Retrieval 检索组件(RAG)
8.1 什么是 RAG
RAG = Retrieval-Augmented Generation(检索增强生成)
| 模式 | 比喻 | 特点 |
|---|---|---|
| 普通 LLM | 闭卷考试 | 凭记忆回答,可能编造(幻觉) |
| RAG | 开卷考试 | 先查资料再回答,更准确、可溯源 |
LLM 的两大局限:
- 有限上下文:无法一次性处理所有信息
- 静态知识:训练数据有截止日期,无法获取最新信息
RAG 通过"先检索,再生成"的方式解决这两个问题。
8.2 完整 RAG 流程(7 步)
┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐
│ 1.文档加载 │───→│ 2.文档分块 │───→│ 3.文本向量化│───→│ 4.存入向量库│
│ Loaders │ │ Splitters │ │ Embeddings │ │ FAISS等 │
└────────────┘ └────────────┘ └────────────┘ └─────┬──────┘
│
┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ 7.LLM生成 │←───│ 6.语义检索 │←───│ 5.查询向量化│←───────┘
│ 答案 │ │ Retriever │ │ Embeddings │
└────────────┘ └────────────┘ └────────────┘
- 原始文档加载 → Document Loaders
- 文档分块(Chunking) → Text Splitters
- 向量化(Embedding) → Embedding Models
- 存入向量数据库 → Vector Store (FAISS/Chroma/Milvus)
- 用户查询向量化 → Embedding Models
- 语义相似度检索 → Retriever
- 拼接上下文 + LLM 生成 → Chain
8.3 Document Loaders(文档加载器)
从外部数据源加载文档:
python
from langchain_community.document_loaders import TextLoader, PyPDFLoader, WebBaseLoader
# 文本文件
loader = TextLoader("knowledge.txt")
docs = loader.load()
# PDF 文件
loader = PyPDFLoader("report.pdf")
docs = loader.load()
# 网页
loader = WebBaseLoader("https://example.com/article")
docs = loader.load()
8.4 Text Splitters(文档分块)
原始文档可能几万字,直接塞给 LLM 会超出上下文限制,必须先分块:
python
from langchain_text_splitters import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # 每块最多500字符
chunk_overlap=50, # 块之间重叠50字符(保持上下文连贯)
separators=["\n\n", "\n", "。", ",", " "], # 按优先级切分
)
chunks = text_splitter.split_text(long_text)
# 也可以直接分割 Document 对象
chunks = text_splitter.split_documents(docs)
参数说明:
| 参数 | 说明 |
|---|---|
chunk_size |
每块最大字符数 |
chunk_overlap |
相邻块重叠字符数(保持语义连贯) |
separators |
分隔符优先级列表,优先用前面的分隔 |
RecursiveCharacterTextSplitter 按优先级尝试切分:段落 → 行 → 句子 → 字级别,尽量保持语义完整。
8.5 Document 对象
LangChain 中文档的标准数据结构:
python
from langchain_core.documents import Document
doc = Document(
page_content="Python 是一种高级编程语言", # 正文(会被向量化和检索)
metadata={ # 附加信息(不会被向量化)
"source": "python_intro",
"topic": "programming",
"page": 1,
},
)
| 字段 | 类型 | 说明 |
|---|---|---|
page_content |
str |
文档正文,会被向量化和检索 |
metadata |
dict |
附加元数据,可用于过滤和溯源 |
8.6 Embedding Models(向量化模型)
Embedding 把文字转成数字向量,语义相近 → 向量相近:
"苹果" → [0.8, 0.2, -0.5, 0.3, ...]
"香蕉" → [0.7, 0.3, -0.4, 0.2, ...] ← 向量相近(都是水果)
"手机" → [0.3, 0.8, 0.5,-0.2, ...] ← 向量差异大(不同类别)
python
from langchain_community.embeddings import DashScopeEmbeddings
from langchain_openai import OpenAIEmbeddings
# 阿里云 Embedding
embeddings = DashScopeEmbeddings(model="text-embedding-v3")
# OpenAI Embedding
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
# 向量化文本
vector = embeddings.embed_query("什么是人工智能?")
# vector: [0.0023, -0.0145, 0.0089, ...] (高维浮点数组)
8.7 Vector Stores(向量数据库)
存储和检索向量:
| 向量库 | 特点 | 适用场景 |
|---|---|---|
| FAISS | Facebook 开源,纯内存,速度快 | 开发测试、中小规模 |
| Chroma | 开源,支持持久化 | 中等规模、本地部署 |
| Milvus | 分布式,生产级 | 大规模生产环境 |
python
from langchain_community.vectorstores import FAISS
# 从文档创建向量库
store = FAISS.from_documents(docs, embeddings)
# 语义搜索
results = store.similarity_search("人工智能相关的技术", k=3)
for doc in results:
print(doc.page_content)
print(doc.metadata)
# 保存和加载
store.save_local("faiss_index")
loaded_store = FAISS.load_local("faiss_index", embeddings, allow_dangerous_deserialization=True)
8.8 Retrievers(检索器)
Retriever 是文档搜索的统一接口,比直接使用 Vector Store 更灵活:
python
# 从 Vector Store 创建 Retriever
retriever = store.as_retriever(search_kwargs={"k": 3})
# 使用
docs = retriever.invoke("什么是量子计算?")
8.9 最简 RAG 实现(关键词匹配)
不依赖向量数据库,用关键词匹配实现基础 RAG:
python
# 知识库
knowledge = [
{"content": "Qwen 是阿里云开发的大语言模型系列。", "source": "qwen"},
{"content": "Ollama 是本地运行大模型的工具。", "source": "ollama"},
{"content": "LangChain 是构建 AI 应用的框架。", "source": "langchain"},
]
# 检索(关键词匹配)
def retrieve(query: str, k: int = 2):
scored = []
for doc in knowledge:
score = sum(1 for w in query if w in doc["content"])
if score > 0:
scored.append((score, doc))
scored.sort(key=lambda x: x[0], reverse=True)
return [doc for _, doc in scored[:k]]
# RAG Chain
rag_prompt = PromptTemplate.from_template("""基于以下资料回答问题。
资料:
{context}
问题: {question}
回答:""")
rag_chain = rag_prompt | model | StrOutputParser()
# 使用
question = "Qwen 是谁开发的?"
docs = retrieve(question)
context = "\n".join(d["content"] for d in docs)
answer = rag_chain.invoke({"context": context, "question": question})
8.10 三种 RAG 架构
| 架构 | 流程 | 特点 | 适用场景 |
|---|---|---|---|
| 2-Step RAG | 检索 → 生成(单次 LLM 调用) | 快速、简单 | 简单查询、事实性问答 |
| Agentic RAG | Agent 自主决定何时/如何检索 | 灵活、可多轮检索 | 复杂推理、多源检索 |
| Hybrid RAG | 结合多种检索策略 | 综合效果最好 | 高质量要求场景 |
2-Step RAG
用户问题 → 检索相关文档 → [文档+问题] → LLM → 答案
最简单的 RAG 架构,一次检索 + 一次生成:
python
def two_step_rag(question: str) -> str:
docs = retriever.invoke(question)
context = "\n".join(d.page_content for d in docs)
return rag_chain.invoke({"context": context, "question": question})
Agentic RAG
Agent 自主决定是否需要检索、检索什么、检索几次:
python
@tool
def search_knowledge_base(query: str) -> str:
"""搜索知识库获取相关信息。"""
docs = retriever.invoke(query)
return "\n".join(d.page_content for d in docs)
agent = create_agent(
model=model,
tools=[search_knowledge_base],
system_prompt="你是一个研究助手。如果需要信息,请使用搜索工具。",
)
# Agent 会根据问题复杂度自主决定是否检索、检索几次
Hybrid RAG
结合关键词检索、语义检索、网页搜索等多种策略:
python
@tool
def keyword_search(query: str) -> str:
"""关键词检索,适合精确匹配。"""
...
@tool
def semantic_search(query: str) -> str:
"""语义检索,适合模糊匹配。"""
...
@tool
def web_search(query: str) -> str:
"""网页搜索,获取最新信息。"""
...
agent = create_agent(
model=model,
tools=[keyword_search, semantic_search, web_search],
system_prompt="你是一个研究助手,请根据需要选择合适的搜索方式。",
)
8.11 安全:间接提示注入风险
RAG 系统面临间接提示注入的安全风险:恶意文档可能包含指令,诱导 LLM 执行非预期操作。
用户查询 → 检索到恶意文档(包含隐藏指令) → LLM 执行了恶意指令
防护措施:
- 对检索到的文档进行内容过滤
- 在 System Prompt 中明确告知 AI 不要执行文档中的指令
- 使用 Middleware 进行内容审查