LangChain 知识总结详解(2)

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 的两大局限

  1. 有限上下文:无法一次性处理所有信息
  2. 静态知识:训练数据有截止日期,无法获取最新信息

RAG 通过"先检索,再生成"的方式解决这两个问题。

8.2 完整 RAG 流程(7 步)

复制代码
┌────────────┐    ┌────────────┐    ┌────────────┐    ┌────────────┐
│ 1.文档加载  │───→│ 2.文档分块  │───→│ 3.文本向量化│───→│ 4.存入向量库│
│  Loaders   │    │  Splitters │    │ Embeddings │    │  FAISS等   │
└────────────┘    └────────────┘    └────────────┘    └─────┬──────┘
                                                          │
┌────────────┐    ┌────────────┐    ┌────────────┐        │
│ 7.LLM生成  │←───│ 6.语义检索  │←───│ 5.查询向量化│←───────┘
│   答案     │    │  Retriever │    │ Embeddings │
└────────────┘    └────────────┘    └────────────┘
  1. 原始文档加载 → Document Loaders
  2. 文档分块(Chunking) → Text Splitters
  3. 向量化(Embedding) → Embedding Models
  4. 存入向量数据库 → Vector Store (FAISS/Chroma/Milvus)
  5. 用户查询向量化 → Embedding Models
  6. 语义相似度检索 → Retriever
  7. 拼接上下文 + 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 进行内容审查