1. 引言
😄 上节从 传统AI应用开发 的 "痛点 " (需手动管理上下文、多步骤任务处理复杂、外部集成工具需大量样板代码、扩展性差) 引出了功能强大的 LangChain ,然后系统讲解了 "七大核心组件 " 中的前四个:Models (LLM模型配置)、Prompts (提示词模板)、Tools (工具函数) 和 Chains (链式调用-LCEL),本节把剩余几个组件过完~
2. Memory - 记忆
🤔 在多轮对话系统中,LLM模型 本身是 无状态的 ,这意味着它不会记住之前的对话内容,也无法维持对话的连贯性和上下文。如果每次都要重新介绍背景信息,用户体验也太差了。Memory 组件就是为了解决这个问题而设计的,它的主要作用:
- 存储对话历史:保留用户和AI之间的交互记录。
- 提供上下文:为模型提供必要的历史信息。
- 维持连贯性:让对话具有连续性和一致性。
- 优化性能:智能管理历史信息,平衡效果和成本。
记忆问题 & 解决链条:
bash
无记忆 → 完整保存 → 成本过高 → 滑动窗口 → 丢失重要信息 → LLM智能压缩
↓
配置复杂 → 现代化API → 多用户混淆 → Session隔离 → 特殊需求 → 自定义Memory
↓
业务适配 → 角色提示 → 重启丢失 → 数据库存储 → 记忆质量参差 →
智能管理 (重要性评估 + 时间衰减) → 性能瓶颈 → 优化器 (压缩 + 关键信息提取 + 最近消息)
2.1. 演进历程
2.1.1. 传统Memory系统
LangChain 的早期版本中 (v0.0.x) 存在多种专门的Memory类用于管理对话历史:
- ConversationBufferMemory:最基础的记忆实现,直接存储完整的对话历史。这种方式简单直观,但会快速消耗大量tokens,容易超出模型的上下文限制。
- ConversationBufferWindowMemory :通过设置 "窗口大小k" 来限制保留的对话轮数,只保留最近的2k条消息。这种方式能控制上下文长度,但会丢失早期的重要信息。
- ConversationSummaryMemory :将对话历史总结为"摘要"后存储,需要额外的LLM调用来生成摘要。这种方式节省tokens但可能丢失重要细节。
- ConversationSummaryBufferMemory:结合缓冲和摘要机制,在token限制内保留最新消息,超出部分生成摘要,一种平衡上下文保留和性能的混合策略。
2.1.2. 过渡期
LangChain v0.1 引入了更灵活的接口 (与Chain对象紧密联合):
- RunnableWithMessageHistory:为任意Runnable添加消息历史管理功能,支持自定义会话历史获取函数。
- BaseChatMessageHistory:抽象基类,定义了消息历史存储的标准接口。开发者可以基于此创建自定义的历史存储实现。
2.1.3. 现代化解决方案
LangChain v0.3 正式推荐使用 LangGraph Persistence 作为主要的记忆管理方案:、
- LangGraph Persistence :基于 checkpointer 的状态管理系统,支持多用户、多会话场景,具备错误恢复、人工干预和时间旅行等高级功能。
- Memory Store:提供跨会话的长期记忆存储,支持JSON文档存储、命名空间组织和内容过滤。
虽然官方推荐新项目使用 LangGraph,但过渡期的解决方案在简单应用中仍然非常实用,故先展开讲讲~
2.2. RunnableWithMessageHistory
替代了旧版本的 ConversationChain,功能更强大更灵活,自动管理消息历史的读取、存储和注入。
python
conversation = RunnableWithMessageHistory(
runnable=chain, # 对话链
get_session_history=get_history, # 获取会话历史的函数
input_messages_key="input", # 输入消息的键名
history_messages_key="history", # 历史消息的键名
)
# 优势
# 自动注入: 在调用前自动将历史消息注入到提示模板中
# 自动保存: 在调用后自动保存新的用户输入和AI回复
# 会话隔离: 通过session_id实现多用户/多会话的完全隔离
# 无需手动管理: 不再需要手动调用save_context等方法
2.2.1. BaseChatMessageHistory
所有聊天消息历史存储类的抽象父类,定义了统一的接口标准,确保不同存储后端的接口一致性(内存、文件、数据库等),核心方法:
python
def add_message(self, message: BaseMessage) -> None:
"""添加单条消息到历史记录"""
pass
def clear(self) -> None:
"""清空历史记录"""
pass
@property
def messages(self) -> List[BaseMessage]:
"""获取所有历史消息列表"""
pass
2.2.2. ChatMessageHistory
最基础的 内存存储实现,简单直接,适用于短时间的对话会话或不需要持久化的简单应用
python
from langchain_community.chat_message_histories import ChatMessageHistory
history = ChatMessageHistory()
history.add_user_message("你好")
history.add_ai_message("你好!很高兴见到你")
2.2.3. MessagesPlaceholder
消息占位符,在提示模板中为历史消息预留位置,实现动态消息注入。
python
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
prompt = ChatPromptTemplate.from_messages([
("system", "你是一个友好的AI助手"),
MessagesPlaceholder(variable_name="history"), # 历史消息占位符
("human", "{input}")
])
2.2.4. Session管理
:通过 session_id 实现多用户、多会话的独立记忆管理,代码示例:
python
store = {}
def get_session_history(session_id: str) -> BaseChatMessageHistory:
if session_id not in store:
store[session_id] = ChatMessageHistory()
return store[session_id]
# 使用时指定session_id
response = conversation.invoke(
{"input": "你好"},
config={"configurable": {"session_id": "user_123"}}
)
2.2.5. 简单示例-多用户会话管理
python
from langchain_openai import ChatOpenAI
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.chat_history import BaseChatMessageHistory
import os
# 1. 初始化LLM
llm = ChatOpenAI(
temperature=0,
api_key= os.getenv("OPENAI_API_KEY"),
base_url=os.getenv("OPENAI_BASE_URL"),
model=os.getenv("DEFAULT_LLM_MODEL")
)
# 2. 创建会话存储(Session管理)
store = {}
def get_session_history(session_id: str) -> BaseChatMessageHistory:
"""实现BaseChatMessageHistory接口规范"""
if session_id not in store:
store[session_id] = ChatMessageHistory() # 使用ChatMessageHistory
return store[session_id]
# 3. 创建包含MessagesPlaceholder的提示模板
prompt = ChatPromptTemplate.from_messages([
("system", "你是专业的客服代表,请友好、耐心地帮助客户解决问题。"),
MessagesPlaceholder(variable_name="history"), # 关键:历史消息占位符
("human", "{input}")
])
# 4. 创建对话链
chain = prompt | llm
# 5. 包装成具有记忆功能的RunnableWithMessageHistory
conversation = RunnableWithMessageHistory(
chain,
get_session_history,
input_messages_key="input",
history_messages_key="history",
)
# 6. 多用户对话演示
def chat_with_customer(customer_id: str, message: str):
"""与特定客户对话"""
response = conversation.invoke(
{"input": message},
config={"configurable": {"session_id": customer_id}}
)
return response.content
# 测试多个客户
customers = [
("customer_001", "你好,我的订单有问题"),
("customer_002", "我想退换商品"),
("customer_001", "订单号是ABC123"), # 客户001继续对话
("customer_002", "商品质量不好"), # 客户002继续对话
]
# 分别处理两个客户的对话
print("🌟 客户001的对话记录:")
print("="*50)
for customer_id, message in customers:
if customer_id == "customer_001":
response = chat_with_customer(customer_id, message)
print(f"【客户{customer_id}】: {message}")
print(f"【客服】: {response}")
print("-" * 30)
print("\n🌈 客户002的对话记录:")
print("="*50)
for customer_id, message in customers:
if customer_id == "customer_002":
response = chat_with_customer(customer_id, message)
print(f"【客户{customer_id}】: {message}")
print(f"客服: {response}")
print("-" * 30)
运行输出结果:

2.3. LangGraph
2.3.1. 为什么需要它?
🤔 传统Memory方案的局限性:
- 线性思维:只能处理简单的对话序列。
- 状态割裂:无法管理复杂的中间状态。
- 难以调试:执行过程不透明。
- 扩展困难:难以支持分支、循环等复杂逻辑。
架构:
bash
输入 → Memory读取 → LLM处理 → 输出 → Memory保存
↑___________________________|
LangGraph 的革命性改进:
- 图即逻辑 & 可观测:支持复杂的执行流程,可视化的执行流程,支持条件分支、循环和递归。
- 状态即一切:统一的状态管理模型,自动的状态持久化,灵活的状态恢复。
- 检查点即安全:自动保存执行状态、支持任意点回复、分布式环境友好。
架构:
bash
[状态存储]
↕
输入 → [节点A] → [节点B] → [节点C] → 输出
↓ ↓ ↓
[检查点1] [检查点2] [检查点3]
2.3.2. 核心概念
① 状态 (State ) ------ 贯穿整个执行过程的数据容器
通常是一个 Python字典 或 Pydantic类型 ,在 LangGraph 中,定义 State 最常用和推荐的方式是使用 Python 的 TypedDict ,这能为你提供代码补全和类型检查等好处。一个关键的特性是使用 Annotated 和 add_messages 来让 LangGraph 自动处理聊天消息的累积。
python
from typing import TypedDict, List, Annotated
from langchain_core.messages import BaseMessage
from langgraph.graph.message import add_messages
class ConversationState(TypedDict):
"""
对话状态定义
'messages' 字段经过特殊注解,以实现消息的累积性追加,
而不是每次都被新消息覆盖。
"""
# 消息列表:使用 Annotated 和 add_messages 来累积对话历史
messages: Annotated[List[BaseMessage], add_messages]
# 以下字段将采用默认的覆盖更新策略
user_info: dict # 用户信息
context: str # 当前上下文
step: str # 当前步骤
metadata: dict # 元数据
② 节点 (Node ) ------ 执行具体逻辑的最小单元
代表工作流程中的一个个具体 "步骤" 或 "任务",负责执行具体的工作,在 LangGraph 中,节点本质上就是 Python 函数 或 LCEL Runnable 对象,它们接收当前的 State 作为输入,执行相应的逻辑(如:调用大语言模型、执行工具、访问数据库等),然后返回对状态的更新。从功能和约定的角度,可以归纳为以下几种:
【入口节点】
- 定义:图的起始节点,是整个工作流程的开端,当用户输入被送入图时,入口节点是第一个被调用的节点。
- 作用:通常负责接收初始输入,初始化状态,并启动整个工作流程。
- 用法:在构建图时,使用 set_entry_point("节点名称") 来指定哪个节点是入口。
python
from langgraph.graph import StateGraph
# 假设 agent_node 是我们定义的一个节点函数
workflow = StateGraph(ConversationState)
workflow.add_node("agent", agent_node)
workflow.set_entry_point("agent")
【常规节点】
- 定义:执行核心业务逻辑的节点。,如调用LLM进行推理、执行一个具体的工具 (如搜索)、或自定义的Python 函数。
- 作用:完成图中的一项具体任务,并根据任务结果更新共享的"状态"。
- 用法:使用 add_node("节点名称", 节点函数) 将一个函数或 Runnable 添加为图中的节点。
python
def my_logic_node(state: ConversationState):
# ... 执行一些逻辑 ...
updated_state = {"context": "new_value"}
return updated_state
workflow.add_node("my_node", my_logic_node)
【工具节点】
- 定义:特殊的常规节点,专门用于执行一个或多个 "工具 " (Tools),在 LangChain 生态中,工具可以是搜索引擎、计算器、API 调用等任何外部功能。
- 作用:当上一个节点 (通常是 LLM) 决定需要使用某个工具时,流程会转到工具节点来执行该工具。
- 用法:LangGraph 提供了便捷的 ToolNode 来简化工具的执行。
python
from langgraph.prebuilt import ToolNode
# 'tools' 是一个工具列表
tool_node = ToolNode(tools)
workflow.add_node("tools", tool_node)
【结束节点】
- 定义:一个特殊的、内置的节点,代表整个图的执行流程结束。
- 作用:当流程走到 END 时,图的执行就会停止,并最终返回结果。确保你的图在所有可能的分支下最终都能到达 END 是非常重要的,这样可以 "避免无限循环"。
- 用法:在添加边,特别是条件边时,可以将 END 作为一个合法的目标节点。
python
# 在条件边的逻辑中,如果满足某个条件,就返回 END
def should_continue(state: AgentState):
if some_condition:
return "end" # 'end' 会被映射到 END 节点
else:
return "continue_node"
workflow.add_conditional_edges(
"start_node",
should_continue,
{"continue": "continue_node", "end": END}
)
③ 边 (Edge ) ------ 节点之间的执行流程
定义了节点之间的连接关系和流程的走向,是 LangGraph 实现复杂控制流的关键。类型有这几种:
【常规边】
- 定义:最简单的边,用于定义一个节点在执行完毕后,总是应该流向的下一个节点。
- 作用:构建线性的、确定性的流程。如:在一个 Agent 的执行流程中,当工具节点执行完工具后,通常总是应该返回给 Agent 节点,让其根据工具结果进行下一步决策。
- 用法:使用 add_edge("起始节点", "目标节点") 来创建一条常规边。
python
# 从 'node_A' 执行完后,总是流向 'node_B'
workflow.add_edge("node_A", "node_B")
【条件边】
- 定义:条件边允许你根据当前的状态,动态地决定下一步应该走向哪个节点。
- 作用:实现流程的分支和循环,这是构建真正 "智能" 的 Agent 的核心,因为它允许 Agent 根据当前情况做出判断和选择。
- 用法:使用 add_conditional_edges("起始节点", 条件函数, 路径映射) 来创建。
- 起始节点:条件判断发生的节点。
- 条件函数:一个接收当前状态作为输入的函数,它的返回值(通常是一个字符串)将决定走哪条路径。
- 路径映射:一个字典,将条件函数的返回值映射到具体的下一个节点名称。[4]
python
#【条件函数】一个接收当前状态作为输入的函数,返回值 (通常是一个字符串) 将决定走哪条路径。
def router_function(state: AgentState):
if "tool_calls" in state["messages"][-1].additional_kwargs:
return "execute_tools"
else:
return "end_process"
workflow.add_conditional_edges(
"agent_node", #【起始节点】条件判断发生的节点
router_function,
#【路径映射】一个字典,将条件函数的返回值映射到具体的下一个节点名称
{
"execute_tools": "tools_node",
"end_process": END
}
)
【入口条件边】
- 定义:与条件边类似,但是它作用于图的入口。它允许在整个流程开始时,就根据初始输入的状态来决定第一个要执行的节点。
- 作用:根据不同的用户输入类型或初始状态,启动不同的工作流程。
- 用法:使用 set_conditional_entry_point(条件函数, 路径映射) 。
python
def initial_router(state: AgentState):
if state["is_simple_question"]:
return "chatbot_node"
else:
return "agent_with_tools_node"
workflow.set_conditional_entry_point(
initial_router,
{
"chatbot_node": "chatbot_node",
"agent_with_tools_node": "agent_node"
}
)
④ 检查点 (Checkpoint ) ------ 状态在特定时刻的快照
指在图的每个执行步骤之后,将当前的 "State快照" 保存到持久化存储 (如:数据库) 中的机制。它的好处包括:
- 对话历史与状态恢复:当一个用户关闭了聊天窗口,下次再回来时,应用程序可以通过加载最新的检查点,完美地恢复之前的对话状态,继续交流。
- 容错与弹性:如果你的应用程序在执行一个长流程时意外崩溃,你可以从上一个成功的检查点恢复执行,而不需要从头开始,这对于生产环境至关重要。
- 调试与审计:你可以查看和分析保存在检查点中的每一步 State,这对于理解和调试复杂的 Agent 行为非常有帮助。
- 异步与长任务:对于需要很长时间才能完成的任务,你可以触发任务,保存检查点,然后让其他进程在后台完成它,并在完成后更新检查点。
LangGraph 内置了多种检查点后端:
- MemorySaver: 一个内存中的存储,主要用于测试和原型设计 (程序关闭后数据会丢失)。
- SqliteSaver: 使用 SQLite 数据库文件进行存储,简单易用,适用于单机部署。
- PostgresSaver / RedisSaver: 更强大的、适用于生产环境的数据库后端。
核心要点 在于在 compile 时加入 checkpointer ,并在 invoke 时提供一个唯一的 thread_id ,这样,你就开启了LangGraph 的 "记忆 " 功能。简单代码示例:
python
from langgraph.checkpoint.sqlite import SqliteSaver
# 1. 定义一个检查点后端
# 'conn' 是一个数据库连接对象
memory_saver = SqliteSaver.from_conn_string(":memory:") # 使用内存中的 SQLite 进行演示
# 2. 在编译 (compile) 图时,将 checkpointer 传入
# workflow = ... (你已经定义好的 StateGraph)
app = workflow.compile(checkpointer=memory_saver)
# 3. 在调用图时,提供一个可配置的 'thread_id'
# 'thread_id' 就像是每个独立对话的"存档文件名"
# 同一个 'thread_id' 的调用会共享同一个历史记录
user_input = "你好吗?"
config = {"configurable": {"thread_id": "user_123"}}
# 第一次调用
response = app.invoke({"messages": [("human", user_input)]}, config=config)
print(response)
# 第二次调用,LangGraph 会自动加载 'user_123' 的历史状态
user_input_2 = "我刚才问了你什么?"
response_2 = app.invoke({"messages": [("human", user_input_2)]}, config=config)
print(response_2) # 模型将能够回答出 "你好吗?"
2.3.3. 简单应用
先 pip install langgraph langgraph-checkpoint 安装下依赖,导入必要模块:
python
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.sqlite import SqliteSaver
from typing import TypedDict, List
from langchain_openai import ChatOpenAI
import os
① 定义状态
python
class SimpleConversationState(TypedDict):
"""简单对话状态"""
messages: List[str]
user_name: str
step_count: int
② 创建节点
python
def greet_node(state: SimpleConversationState) -> SimpleConversationState:
"""问候节点"""
print(f"🎉 欢迎 {state['user_name']}!")
return {
**state,
"messages": state["messages"] + ["你好!很高兴认识你!"],
"step_count": state["step_count"] + 1,
}
def chat_node(state: SimpleConversationState) -> SimpleConversationState:
"""对话节点"""
llm = ChatOpenAI(
temperature=0,
api_key=os.getenv("OPENAI_API_KEY"),
base_url=os.getenv("OPENAI_BASE_URL"),
model=os.getenv("DEFAULT_LLM_MODEL"),
)
# 获取最新消息
latest_message = state["messages"][-1] if state["messages"] else ""
# 生成回复
response = llm.invoke(f"回复用户:{latest_message}")
return {
**state,
"messages": state["messages"] + [response.content],
"step_count": state["step_count"] + 1,
}
def end_node(state: SimpleConversationState) -> SimpleConversationState:
"""结束节点"""
print(f"💫 对话结束,共进行了 {state['step_count']} 步")
return state
③ 构建图
python
def create_simple_chat_graph():
"""创建简单聊天图"""
# 初始化图
graph = StateGraph(SimpleConversationState)
# 添加节点
graph.add_node("greet", greet_node)
graph.add_node("chat", chat_node)
graph.add_node("end", end_node)
# 设置入口点
graph.set_entry_point("greet")
# 添加边
graph.add_edge("greet", "chat")
graph.add_edge("chat", "end")
graph.add_edge("end", END)
# 配置检查点
checkpointer = MemorySaver()
# 编译图
app = graph.compile(checkpointer=checkpointer)
return app
④ 运行图
python
def demo_simple_chat():
"""演示简单聊天"""
# 创建应用
app = create_simple_chat_graph()
# 初始状态
initial_state = {
"messages": ["你好,我是新用户"],
"user_name": "张三",
"step_count": 0,
}
# 执行图
config = {"configurable": {"thread_id": "user_001"}}
result = app.invoke(initial_state, config=config)
print("📋 最终状态:")
print(f"消息数量: {len(result['messages'])}")
print(f"执行步骤: {result['step_count']}")
print(f"最后消息: {result['messages'][-1]}")
运行输出结果:

😄 对图结构感兴趣,可以调用 get_graph() 获取图结构详情:
python
print(json.dumps(app.get_graph().to_json(), indent=2, ensure_ascii=False))
# 输出结果
{
"nodes": [
{
"id": "__start__",
"type": "runnable",
"data": {
"id": [
"langgraph",
"_internal",
"_runnable",
"RunnableCallable"
],
"name": "__start__"
}
},
{
"id": "greet",
"type": "runnable",
"data": {
"id": [
"langgraph",
"_internal",
"_runnable",
"RunnableCallable"
],
"name": "greet"
}
},
{
"id": "chat",
"type": "runnable",
"data": {
"id": [
"langgraph",
"_internal",
"_runnable",
"RunnableCallable"
],
"name": "chat"
}
},
{
"id": "end",
"type": "runnable",
"data": {
"id": [
"langgraph",
"_internal",
"_runnable",
"RunnableCallable"
],
"name": "end"
}
},
{
"id": "__end__"
}
],
"edges": [
{
"source": "__start__",
"target": "greet"
},
{
"source": "chat",
"target": "end"
},
{
"source": "greet",
"target": "chat"
},
{
"source": "end",
"target": "__end__"
}
]
还可以安装 grandalf 依赖库,然后以 ASCII文本图 的可视化形式展示图结构:
python
print(app.get_graph().draw_ascii())
运行输出结果:

😄 还可以安装 Graphviz软件 + pygraphviz库 来实现更精美的可视化效果。
3. Indexes - 索引
🤔 LLM 的知识来源于其庞大的、固定的训练数据,这导致了两个局限性:
- 知识陈旧:LLM不了解在它训练截止日期之后发生的任何事情。
- 知识局限:LLM不了解你公司内部的、私有的数据,比如产品文档、技术手册或内部知识库。
😶 为了解决这个问题,检索增强生成 (RAG ,Retrieval Augmented Generation) 应运而生,它的核心思想非常简单------"开卷考试 ",当用户提出一个问题时,不直接把问题丢给LLM,而是先从 "私有知识库 " 中 "检索相关信息 ",再连同 "原始问题 " 一起作为 "上下文增强 " 后的 "提示词",再交给 LLM 去生成答案。这样做的好处:
- 答案更准确 :LLM的回答基于我们提供的实时、准确的资料,大大减少了 "幻觉" 的产生。
- 知识可更新:只需要更新我们的知识库,而不需要重新训练昂贵的模型。
💁♂️ LangChain 的 Indexes 组件正式实现 RAG流程 的 "基石 ",它提供了一整套工具将原始数据处理成一种结构化的、便于LLM高效查询和利用的形式。这个处理过程,就是 "索引的构建过程 ",通常包含四大核心环节:文档加载、文本分割、向量存储、文档检索。接下来,逐一深入讲解这四个环节~
bash
文档源 → Document Loaders → Text Splitters → Embeddings → Vector Stores → Retrievers
↓ ↓ ↓ ↓ ↓ ↓
PDF/网页 加载文档 文本分割 向量化 向量存储 相似度检索
3.1. 文档加载器 (Document Loaders)
😐 将各种不同来源和格式的数据 (如PDF、网页、数据库、CSV文件等) 加载进来,并转换成 LangChain 能同意处理的标准化格式------ Document 对象。
一个标准的 Document 对象包含两个核心部分:
- page_content:str,代表了文档的主要文本内容。
- metadata:dict,包含关于文档的元数据,如:来源文件名、页码、URL等,这些信息在后续的筛选和检索中非常有用。
python
from langchain_core.documents import Document
# Document包含两个主要属性
doc = Document(
page_content="这是文档的主要内容", # 文本内容
metadata={ # 元数据
"source": "example.pdf",
"page": 1,
"author": "张三"
}
)
LangChain 为不同的数据源提供了相应的加载器,如:文本文件 → TextLoader ,PDF文件 → PyPDFLoader 、网页内容 → WebBaseLoader 、CSV文件 → CSVLoader。简单代码示例:
python
# 示例:加载一个PDF文件
from langchain_community.document_loaders import PyPDFLoader
loader = PyPDFLoader("path/to/your/document.pdf")
documents = loader.load()
# 'documents' 现在是一个 Document 对象的列表
# 列表中的每个对象通常对应PDF的一页
print(documents[0].page_content[:200]) # 打印第一页的前200个字符
print(documents[0].metadata) # 打印第一页的元数据
3.2. 文本分割器 (Text Splitters)
😐 加载完文档后,通常会得到 "篇幅很长的文本 ",由于 LLM 的 "上下文窗口 " 有限 (一次能处理的文本长度),我们不能直接将整篇长文都丢给模型。需要先用 "文本分割器 " 将长文档切分成更小的、语义完整 的块 (chunks)。有效的分割是RAG成功的关键。理想的分割应该:
- 克服模型限制:确保每个文本块的大小都在模型的处理范围内。
- 保留语义完整性:分割点应尽可能选在段落、句子等自然边界上,避免将一个完整的语义单元切得支离破碎。
- 提升检索精度:更小、更聚焦的文本块更容易与特定的用户查询精准匹配。
LangChain 中最常用且推荐的分割器是 RecursiveCharacterTextSplitter,它的工作方式非常智能:
尝试按照一个 字符列表 (默认为["", " ","\n","\n\n"]) 进行递归分割。先尝试按 "段落 " (双换行符) 分割,如果分割后的块仍然太大,,就在这个块的基础上,尝试按 "句子" (单换行符) 分割,以此类推,直到块的大小符合要求。
分割时的两个关键参数:
- chunk_size:每个块的最大长度 (通常按字符数计算)。
- chunk_overlap:相邻块间的重叠字符数。设置一定的重叠可以确保块与块之间的语义连续性,避免上下文信息的丢失。
简单代码示例:
python
# 示例:分割已加载的文档
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, # 每个块的最大字符数
chunk_overlap=200 # 相邻块的重叠字符数
)
chunks = text_splitter.split_documents(documents)
# 'chunks' 现在是一个新的 Document 对象列表,但内容是分割后的小块
print(f"原始文档数量: {len(documents)}")
print(f"分割后块的数量: {len(chunks)}")
3.3. 嵌入与向量存储 (Embeddings & Vector Stores)
😐 现在我们有了一堆 "文本块",如何快速地从中找到与用户查询最相关的内容呢?
这就需要借助这两样东西了:
- 嵌入 (Embeddings):一种将文本转换成数值向量 (一串数字) 的技术。其神奇之处在于,语义上相似的文本,在数学空间中的向量也更"接近"。这项工作通常由专门的嵌入模型,如 OpenAI 的 text-embedding-3-small 模型。
- 向量存储 (Vector Stores):专门为存储和高效查询这些文本向量而设计的数据库,如:适合本地开发的轻量级库 (Chroma, FAISS),生产级的云服务 (如Pinecone, Weaviate)。
简单代码示例:
python
# 示例:将文本块嵌入并存储到 Chroma 向量数据库中
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
# 1. 初始化嵌入模型
embeddings_model = OpenAIEmbeddings()
# 2. 从文本块创建向量存储
# 这会处理好所有的嵌入计算和存储过程
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings_model
)
3.4. 检索器 (Retrievers)
😐 连接用户查询和向量存储的桥梁,封装底层的复杂检索逻辑 (向量搜索、传统的关键词搜索、混合搜索),为上层应用 (如RAG链) 提供一个简单、统一的调用方式:接收一个 字符串查询 ,返回一个相关的 Document对象列表 。最常见的用法:直接从已经构建好的向量存储中派生出检索器。
简单代码示例:
python
# 示例:从向量存储创建一个检索器并使用它
# 1. 创建检索器
retriever = vectorstore.as_retriever(
search_type="similarity", # 相似度搜索
search_kwargs={"k": 3} # 返回top-3结果
)
# 2. 使用检索器进行查询
query = "LangChain的索引组件包含哪些部分?"
relevant_docs = retriever.invoke(query)
# 'relevant_docs' 是一个根据查询的语义相似度排序的 Document 列表
print(relevant_docs[0].page_content)
上面调 as_retriever() 创建的是 "最基础的检索器 ",它执行的是 "语义相似度搜索", 在真实复杂的业务场景下,单纯的语义相似度可能并不够用,LangChain为此提供了多种更先进的检索策略。
3.4.1. 自查询检索器
很多时候,用户的查询中不仅包含了语义信息,还可能隐含了结构化的元数据过滤条件,如:给我找一下2023年之后,关于LangChain V0.1.0版本的更新文档。
- 语义部分:LangChain更新文档
- 元数据过滤部分:"年份 > 2023" 且 "版本号 = V0.1.0"
如果用简单的向量搜索,它可能会找回所有关于LangChain更新的文档,而无法精确满足年份和版本的限制。而 自查询检索器非常巧妙:
😶 利用LLM,先将用户的自然语言查询转换成一个 结构化的查询 ,包含:一个用于向量搜索的查询字符串 和 一组元数据过滤器,然后将这个结构化查询应用于底层的向量存储。
概念代码示例:
python
from langchain.retrievers.self_query.base import SelfQueryRetriever
from langchain.chains.query_constructor.base import AttributeInfo
# 1. 定义你的元数据字段
metadata_field_info = [
AttributeInfo(name="year", description="文档发布的年份", type="integer"),
AttributeInfo(name="version", description="文档对应的LangChain版本", type="string"),
]
# 2. 创建自查询检索器
# retriever = SelfQueryRetriever.from_llm(
# llm, vectorstore, document_content_description, metadata_field_info
# )
# 3. 执行查询
# relevant_docs = retriever.invoke(
# "关于LangChain V0.1.0版本,2023年之后的更新文档"
# )
3.4.2. 父文档检索器
🤔 在做文本分割时常常面临一个两难的困境:
- 块太小:有利于精准匹配用户查询,但块本身可能缺乏足够的上下文信息来让LLM生成好的答案。
- 块太大:上下文信息充分,但可能会因为包含太多无关信息而降低检索的精准度。
父文档检索器 则优雅地解决了这个问题。它的索引构建分为两步:
- 先将文档分割成小的"子块",这些小块非常适合用来做精准的向量搜索。
- 同时也保留了这些子块所属的、更大的 "父块" 或完整的原始文档。
在检索时,它先用用户查询去匹配最相关的小 "子块 ",但最终返回给用户或LLM的,是这些子块所对应的、拥有完整上下文的"父块"。 这就实现了 "用小块精准检索,用大块生成答案" 的理想效果。
3.4.3. 集成检索器
🤔 单一的检索算法往往各有优劣,如:向量搜索 (如FAISS) 擅长捕捉语义关系,而传统的 关键词搜索 (如BM25) 在匹配精确的术语或ID时表现更佳。如何将两者结合,取长补短?
😄 集成检索器 可以将多个不同的检索器组合在一起。当用户查询时,它会分别调用内部的每一个检索器,然后使用一种特定的算法 (如 Reciprocal Rank Fusion) 来重新排序和融合所有检索结果,最终返回一个综合了多种算法优势的最佳结果列表。概念代码示例:
python
# 概念代码示例
from langchain.retrievers import BM25Retriever, EnsembleRetriever
# 1. 准备你的文档和块
# ...
# 2. 初始化两个不同的检索器
bm25_retriever = BM25Retriever.from_documents(docs)
faiss_vectorstore = FAISS.from_documents(docs, embeddings_model)
faiss_retriever = faiss_vectorstore.as_retriever()
# 3. 创建集成检索器
ensemble_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, faiss_retriever],
weights=[0.5, 0.5] # 可以为不同检索器的结果设置权重
)
# 4. 执行查询
# relevant_docs = ensemble_retriever.invoke("你的查询")
3.5. 索引 API
😶 在之前的讲解中,索引构建流程(加载->分割->嵌入->存储)是一个一次性的、同步的过程,但在实际生产环境中,往往会面临更复杂的需求,如:
- 增量更新:知识库是动态变化的,需要不断添加新文档、更新旧文档、删除过期文档。
- 避免重复处理:不希望每次更新时都从头重新处理所有文档,这既浪费时间也浪费金钱 (嵌入API调用是收费的)。
- 后台处理与容错:索引构建可能是个耗时任务,需要异步执行,并能处理中间可能发生的错误。
LangChain 提供了一个强大的 indexing API 来解决以上问题,它通过一个 "记录管理器" (Record Manager) 来跟踪哪些文档已经被处理和索引。其核心逻辑:
- 哈希计算:在处理文档前,API会计算文档内容的哈希值(一种指纹)。
- 内容比对:当新的一批文档到来时,API会检查这些文档的哈希值是否已经存在于记录管理器中。
- 智能同步:哈希值不存在的新文档 → 执行完整的索引流程;哈希值已存在但内容未改变 → 跳过;内容已改变的文档 (哈希值变化) → 执行更新;在数据源中已消失的文档 → 执行删除。
Indexing API 确保了我们的向量存储与原始数据源能够高效、低成本地保持同步,是构建生产级RAG应用不可或缺的一环。
3.6. 简单实践案例
3.6.1. 知识库问答系统
python
from langchain_community.document_loaders import DirectoryLoader, TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_community.vectorstores import FAISS
from langchain.chains import RetrievalQA
def build_knowledge_qa_system():
"""构建知识库问答系统的函数"""
# 1. 加载文档
print("📖 正在加载文档...")
loader = DirectoryLoader(
"knowledge_base/",
glob="*.txt",
loader_cls=TextLoader,
loader_kwargs={"encoding": "utf-8"}
)
documents = loader.load()
print(f"加载了 {len(documents)} 个文档")
# 2. 分割文本
print("✂️ 正在分割文本...")
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
length_function=len,
)
splits = text_splitter.split_documents(documents)
print(f"分割成 {len(splits)} 个文本块")
# 3. 创建向量存储
print("🔢 正在创建向量索引...")
embeddings = OpenAIEmbeddings()
vectorstore = FAISS.from_documents(splits, embeddings)
# 4. 创建检索器
retriever = vectorstore.as_retriever(
search_type="mmr",
search_kwargs={"k": 3, "fetch_k": 6}
)
# 5. 创建问答链
llm = ChatOpenAI(temperature=0, model="gpt-3.5-turbo")
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=retriever,
return_source_documents=True,
verbose=True
)
return qa_chain
def test_qa_system():
"""测试问答系统的函数"""
qa_chain = build_knowledge_qa_system()
questions = [
"什么是机器学习?",
"深度学习有哪些应用?",
"如何选择合适的算法?"
]
for question in questions:
print(f"\n❓ 问题: {question}")
result = qa_chain.invoke({"query": question})
print(f"🤖 答案: {result['result']}")
print("📚 参考来源:")
for doc in result['source_documents']:
print(f" - {doc.metadata.get('source', 'unknown')}")
3.6.2. 电商产品推荐提供
python
import pandas as pd
from langchain_core.documents import Document
def build_product_recommendation_system():
"""构建产品推荐系统的函数"""
# 1. 准备产品数据
def prepare_product_documents():
"""准备产品文档的内部函数"""
# 模拟产品数据
products_data = [
{
"id": "P001",
"name": "iPhone 15 Pro",
"category": "手机",
"brand": "Apple",
"price": 8999,
"description": "配备A17 Pro芯片的高端智能手机,拥有钛金属机身和48MP主摄像头",
"features": ["A17 Pro芯片", "钛金属", "48MP摄像头", "5G网络"]
},
{
"id": "P002",
"name": "MacBook Air M2",
"category": "笔记本电脑",
"brand": "Apple",
"price": 9499,
"description": "搭载M2芯片的轻薄笔记本电脑,13.6英寸Liquid视网膜显示屏",
"features": ["M2芯片", "13.6英寸", "轻薄设计", "全天候电池"]
},
{
"id": "P003",
"name": "小米13 Ultra",
"category": "手机",
"brand": "小米",
"price": 5999,
"description": "专业摄影旗舰手机,徕卡光学镜头,骁龙8 Gen2处理器",
"features": ["徕卡镜头", "骁龙8 Gen2", "专业摄影", "快充技术"]
}
]
documents = []
for product in products_data:
# 将产品信息组合成文本
content = f"""
产品名称: {product['name']}
品牌: {product['brand']}
分类: {product['category']}
价格: ¥{product['price']}
描述: {product['description']}
特性: {', '.join(product['features'])}
"""
doc = Document(
page_content=content.strip(),
metadata={
"product_id": product['id'],
"name": product['name'],
"category": product['category'],
"brand": product['brand'],
"price": product['price']
}
)
documents.append(doc)
return documents
# 2. 构建产品向量索引
documents = prepare_product_documents()
embeddings = OpenAIEmbeddings()
vectorstore = FAISS.from_documents(documents, embeddings)
# 3. 创建推荐检索器
retriever = vectorstore.as_retriever(
search_type="mmr",
search_kwargs={"k": 3, "lambda_mult": 0.8} # 更注重多样性
)
return retriever
def product_recommendation_demo():
"""产品推荐演示函数"""
retriever = build_product_recommendation_system()
# 模拟用户查询
user_queries = [
"我想要一个拍照好的手机",
"推荐一个适合办公的笔记本电脑",
"有什么苹果的产品推荐吗?",
"性价比高的手机有哪些?"
]
for query in user_queries:
print(f"\n🔍 用户查询: {query}")
docs = retriever.invoke(query)
print("📱 推荐产品:")
for i, doc in enumerate(docs):
product_name = doc.metadata['name']
price = doc.metadata['price']
print(f" {i+1}. {product_name} - ¥{price}")
3.6.3. 多模态文档检索
python
from langchain_community.document_loaders import PyPDFLoader, CSVLoader
from langchain.text_splitter import CharacterTextSplitter
def build_multimodal_document_system():
"""构建多模态文档检索系统的函数"""
def load_multiple_document_types():
"""加载多种类型文档的内部函数"""
all_documents = []
# 加载PDF文件
pdf_loader = PyPDFLoader("reports/annual_report.pdf")
pdf_docs = pdf_loader.load()
for doc in pdf_docs:
doc.metadata["doc_type"] = "PDF报告"
all_documents.extend(pdf_docs)
# 加载CSV文件
csv_loader = CSVLoader("data/sales_data.csv")
csv_docs = csv_loader.load()
for doc in csv_docs:
doc.metadata["doc_type"] = "销售数据"
all_documents.extend(csv_docs)
# 加载文本文件
text_loader = DirectoryLoader(
"documents/",
glob="*.txt",
loader_cls=TextLoader
)
text_docs = text_loader.load()
for doc in text_docs:
doc.metadata["doc_type"] = "文本文档"
all_documents.extend(text_docs)
return all_documents
# 1. 加载所有文档
print("📁 正在加载多种格式文档...")
documents = load_multiple_document_types()
print(f"总共加载了 {len(documents)} 个文档")
# 2. 智能分割
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=800,
chunk_overlap=100,
separators=["\n\n", "\n", "。", ",", " ", ""]
)
splits = text_splitter.split_documents(documents)
# 3. 创建向量存储
embeddings = OpenAIEmbeddings()
vectorstore = Chroma.from_documents(
splits,
embeddings,
persist_directory="./multimodal_db"
)
# 4. 创建智能检索器
retriever = vectorstore.as_retriever(
search_type="similarity_score_threshold",
search_kwargs={
"score_threshold": 0.7,
"k": 5
}
)
return retriever
def multimodal_search_demo():
"""多模态检索演示函数"""
retriever = build_multimodal_document_system()
queries = [
"2023年的销售额是多少?", # 可能来自CSV或PDF
"公司的发展战略是什么?", # 可能来自文本文档
"最新的产品发布情况", # 可能来自多种文档类型
]
for query in queries:
print(f"\n🔍 查询: {query}")
docs = retriever.invoke(query)
print("📄 相关文档:")
for doc in docs:
doc_type = doc.metadata.get('doc_type', 'unknown')
source = doc.metadata.get('source', 'unknown')
content_preview = doc.page_content[:100] + "..."
print(f" 类型: {doc_type}")
print(f" 来源: {source}")
print(f" 内容: {content_preview}")
print(" ---")
4. Agents - 智能体
🤖 Agent 是一个能够利用 LLM 进行 思考和决策 ,并利用一系列工具 (Tools) 来执行具体任务的系统。
想象一下你有一个非常聪明的助理🤖,你告诉它 "帮我查一下明天深圳的天气怎么样,如果下雨的话,就提醒我带伞"。在这个场景中:
- 你 → 任务的提出者。聪明的助理 → Agent。
- 思考和决策 :助理会理解你的意图,首先它需要 "查询天气",然后根据查询结果 "判断是否下雨",最后根据判断结果决定是否 "提醒你带伞"。这个思考和决策的过程,在LangChain中就是由 LLM 来驱动的。
- 工具 :为了 "查询天气",助理可能会访问某个"天气网站",这是它能利用的工具。在LangChain中,工具可以是任何东西,比如:搜索引擎、计算器、数据库查询接口,甚至是你自己编写的任何函数。
除了 LLM 和 Tools 外,还需要一个 Agent Executor ,这是Agent的 "执行器 " 或者说 "运行时环境",它负责协调LLM 和 Tools之间的交互,循环地执行以下步骤,直到任务完成:
- 接收用户的输入。
- 将输入和历史交互信息传递给LLM。
- LLM进行思考,决定下一步是直接回答用户,还是调用一个工具。
- 如果LLM决定调用工具,执行器会负责调用该工具,并将执行结果返回给LLM。
- LLM接收到工具的执行结果,再次进行思考,决定下一步行动。
- 重复以上步骤,直到LLM认为任务已经完成,然后将最终答案返回给用户。
这个循环的过程,通常被称为 "ReAct" (Reasoning and Acting) 循环。
bash
用户输入 → Agent → Tool Selection → Tool Execution → Result Analysis → 下一步决策
↓ ↓ ↓ ↓ ↓ ↓
"计算题" 思考工具 选择计算器 执行计算 分析结果 返回答案
4.1. ReAct 框架
LangChain 中的许多 标准Agent 都遵循 "ReAct 框架 " → 让 LLM 通过 "思考-行动-观察" 的循环来解决问题。通过一个简单的例子来解释这个过程,假如问Agent:"苹果公司现在的CEO是谁?他今年的年龄是多少?",它的执行流程可能是这样:
第一轮:
- 用户输入: "苹果公司现在的CEO是谁?他今年的年龄是多少?"
- LLM (思考): "我需要先找出苹果公司现在的CEO是谁。我可以使用搜索工具来查找这个信息。"
- LLM (行动): 调用 Search 工具,输入查询 "who is the current CEO of Apple"。
- Agent Executor: 执行搜索工具。
- 工具 (观察): 返回搜索结果 "Tim Cook is the current CEO of Apple."
第二轮:
- Agent Executor: 将上一轮的观察结果("Tim Cook is the current CEO of Apple.")返回给LLM。
- LLM (思考): "好的,我已经知道CEO是Tim Cook。现在我需要找出他的年龄。我需要先知道他的出生日期,然后用当前年份减去出生年份。我还是需要使用搜索工具。"
- LLM (行动): 调用 Search 工具,输入查询 "Tim Cook birth date"。
- Agent Executor: 执行搜索工具。
- 工具 (观察): 返回搜索结果 "Tim Cook was born on November 1, 1960."
第三轮:
- Agent Executor: 将观察结果("Tim Cook was born on November 1, 1960.")返回给LLM。
- LLM (思考): "我知道了他的出生日期是1960年11月1日。现在是2025年。我可以用计算器来计算他的年龄。2025 - 1960 = 65。任务完成了,我可以给出最终答案了。"
- LLM (最终答案): "苹果公司现在的CEO是蒂姆·库克,他今年的年龄是65岁。"
通过这个例子,我们可以清晰地看到,Agent通过LLM的推理能力,将一个复杂问题分解成了多个可执行的子任务,并利用工具一步步地找到答案,这就是Agent的强大之处👏。
4.2. 不同类型的标准Agent
4.2.1. zero-shot-react-description
基础也最经典的Agent类型,完全基于上面讲过的 ReAct 框架,命名解读:
- zero-shot:在没有示例的情况下,LLM仅凭其通用能力和工具的描述就能决定如何使用工具。
- react:工作流程是 "Reasoning and Acting" (思考与行动)。
- description:Agent在选择工具时,完全依赖于你为每个工具提供的描述文本,描述的好坏直接决定了Agent的性能。
工作流程:
- initialize_agent 会加载一个精心设计的 提示模板(Prompt) ,告诉LLM "你有以下工具可以使用:[工具A的描述]、[工具B的描述]... 你必须严格按照'思考(Thought)'、'行动(Action)'、'行动输入(Action Input)'、'观察(Observation)'的格式来回应。"
- LLM 会生成一段类似这样的文本:Thought: 我需要找出LangChain的作者是谁。我应该用搜索工具。Action : Search Action Input: who is the author of LangChain。
- Agent Executor 中的解析器会捕获 Action 和 Action Input 字段,然后去调用名为 "Search" 的工具,并传入 "who is the author of LangChain" 作为参数。
- 工具执行后返回结果,这个结果会被标记为 Observation,并与之前的历史一起再次提交给LLM,开始下一轮循环。
优点:通用性强,是理解Agent工作原理的最佳范例。
缺点: 严重依赖LLM的格式遵循能力,有时LLM可能会"忘记" 输出特定格式,导致解析失败。
4.2.2. conversational-react-description
对话增强版 ,在 ReAct 框架的基础上集成了 记忆(Memony) 组件,提示模板 中额外增加了一个用于 存放聊天历史的变量 (如 chat_history)。在每一轮决策时,LLM不仅能看到当前的用户问题和工具,还能看到整个对话的上下文。适用于需要进行多轮交互的聊天机器人或对话式应用。如:先问"LangChain是什么?",然后接着问 "它有什么主要特点?",Agent能够理解第二个问题中的"它"指代的是LangChain。
4.2.3. self-ask-with-search
通过 "自问自答 " 的方式将复杂问题分解,它专门设计用于一个单一但强大的工具:搜索 (Search)。工作流程:
面对复杂问题,LLM不会直接去搜这个问题,而是先提出一个 "中间问题",然后调用搜索工具来回答这个中间问题。得到答案后,基于这个答案提出下一个中间问题... 如此往复,知道所有子问题都得到解答,最终综合信息给出最终答案。
😄 对于需要通过多次搜索、层层递进才能解决的问题非常有效,思考路径清晰。不过应用场景相对受限,因为它被设计为只使用搜索这一个工具。
4.2.4. react-docstore
😶 专门为与 文档知识库 (如Wikipedia) 交互而设计的Agent,通常配备两个专属工具:
- Search: 在知识库中搜索一个词条或页面。
- Lookup: 在一个已经找到的页面中查找具体的关键词,以获取更精确的信息。
工作流程:
先搜再查,面对一个问题,它会先 Search 找到相关的文档,如果文档很长,它会接着 Lookup 关键信息,避免将冗长的全文都读一遍。
4.3. create_tool_calling_agent
🤔 上面的 "标准Agent " 是 "构建Agent的旧范式 ",其核心工作方式可以概括为 "基于纯文本的指令模拟 ",即用软件工程的 "补丁 " (复杂的Prompt和Parser ) 去弥补 旧LLM 的能力不足,"假装 " LLM能调用工具。像 GPT-4、Gemini、Claude 4等现代LLM,它们本身就被训练和设计成能够理解 "工具 " 和 "函数 " ****的改变。这时就不需要我们去 "教 " 了,这是它们的 "原生能力 "。create_tool_calling_agent (llm,tools,prompt) 就是 LangChain 中利用并统一了这种原生能力的 "标准化接口"。新范式的工作流程:
- 开发者:用标准方式定义好工具 (如用@tool装饰器,写好函数签名、类型注解和文档字符串),然后把这些工具列表直接传递给LLM。
- LLM :当收到任务和工具列表时,不再是"扮演"一个角色去生成文本。它在内部思考后,会直接生成一个 "结构化的、机器可读的指令 " (通常是一个JSON对象)。这个过程被称为 Function Calling 或 Tool Calling。
- LLM输出的直接就是代码可以完美理解的 JSON对象,不需要任何模糊的文本解析,程序拿到这个对象,直接调用对应的函数并传入参数即可。
json
# LLM输出不再是Action: Search... 这样的文本,而是类似这样的数据结构:
{
"tool_calls": [
{
"name": "search_web",
"arguments": {
"query": "LangChain latest version",
"engine": "google"
}
}
]
}
其它 Agent create_xxx() 工厂方法:
- create_react_agent:创建基于 ReAct 框架的 Agent,适合需要推理和行动结合的场景,使用 "思考-行动-观察" 的循环模式。
- create_structured_chat_agent:创建结构化聊天 Agent,适合需要复杂工具调用的场景,支持多参数工具,能处理结构化输入。
- create_openai_functions_agent:创建 OpenAI Functions Agent,专门适配 OpenAI 的 Function Calling 功能,支持函数模式的工具调用,与 OpenAI API 深度集成。
- create_openai_tools_agent:创建 OpenAI Tools Agent,使用 OpenAI 的 Tools API,支持并行工具调用,create_openai_functions_agent 的升级版。
- create_conversational_retrieval_agent:创建对话检索 Agent,支持对话记忆,结合检索功能,适合问答场景。
4.4. AgentExecutor
💁♂️ LangChain 中负责驱动 Agent 执行任务的 "运行时环境 ",扮演 "总指挥" 的角色,接收用户输入后:
- 思考:指挥内部 LLM 进行思考和决策,判断下一步应该做什么。
- 行动:如果 LLM 决定使用工具,调用相应的工具。
- 观察:获取工具返回的结果。
- 重复:将工具的结果再次交给 LLM 去思考,判断任务是否完成。没完成就继续循环前三步,直到任务结束。
简单使用示例:
python
# ① 准备LLM
llm = ChatOpenAI(temperature=0, model="gpt-4") # temperature=0 确保输出更稳定
# ② 准备Tools
tools = load_tools(["serpapi", "llm-math"], llm=llm)
# ③ 初始化 AgentExecutor
# initialize_agent 是一个便捷的函数,它将 LLM, Tools 和一个预设的 Agent 封装在一起
# AgentType.ZERO_SHOT_REACT_DESCRIPTION 是最常用的一种 Agent 类型
# verbose=True 可以让我们看到 Agent 的完整思考过程
agent_executor = initialize_agent(
tools,
llm,
agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
verbose=True
)
# ④ 调用 invoke() 运行 AgentExecutor
response = agent_executor.invoke({
"input": "目前英伟达的股价是多少?如果我买15股,需要多少美元?"
})
print(response)
4.5. 简单实践案例
😄 逐步实现一个 "获取单词长度+计算" 的 Agent,如 "单词 'LangChain' 的长度乘以 5 是多少?"
4.5.1. 选 LLM
python
from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.tools import tool
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
import os
# ================================
# 💡 ① 选择支持工具调用的LLM
# ================================
llm = ChatOpenAI(
temperature=0, # 设置为0可以使模型的输出更具确定性,对于需要精确执行工具的Agent任务来说通常是更好的选择。
api_key= os.getenv("OPENAI_API_KEY"),
base_url=os.getenv("OPENAI_BASE_URL"),
model=os.getenv("DEFAULT_LLM_MODEL")
)
4.5.2. 定义 Tools
两个工具:一个用于计算乘法,一个用于获取单词长度。
python
# ================================
# 💡 ② 定义 Tools
# 关键点:
# 1. 清晰的函数名:`multiply`,让LLM一眼就能看懂其功能。
# 2. 详细的文档字符串(docstring):`"""计算两个整数的乘积。"""`,这是最重要的!LLM将依赖这个描述来决定何时以及如何使用该工具。
# 3. 明确的类型注解:`(a: int, b: int) -> int`,这帮助LLM理解输入和输出的数据类型,生成正确的参数。
# ================================
@tool
def multiply(a: int, b: int) -> int:
"""计算两个整数的乘积。"""
print(f"--- 调用工具 [multiply] --- 参数: a={a}, b={b}")
return a * b
@tool
def get_word_length(word: str) -> int:
"""返回一个单词的长度。"""
print(f"--- 调用工具 [get_word_length] --- 参数: word='{word}'")
return len(word)
# 将所有定义好的工具放入一个列表中,以便后续提供给Agent
tools = [multiply, get_word_length]
4.5.3. 设计 Prompt
python
# ================================
# 💡 ③ 设计 Prompt
# ================================
prompt = ChatPromptTemplate.from_messages([
("system", "你是一个强大的助手,可以使用工具来回答问题。"),
("human", "{input}"), # 用户输入
("placeholder", "{agent_scratchpad}"), # 填充Agent的中间步骤的占位符 (工具调用记录、工具输出等)
])
4.5.4. 创建 Agent
python
# ================================
# 💡 ④ 使用工厂函数创建Agent
# `create_tool_calling_agent`是一个高级函数,它将LLM、工具和提示组合在一起,
# 创建出一个遵循原生工具调用逻辑的Agent"大脑"。
# 这个函数内部封装了处理工具调用请求和响应的复杂逻辑。
# 返回的`agent`是一个Runnable对象,定义了决策逻辑,但还不能独立运行。
# ================================
agent = create_tool_calling_agent(llm, tools, prompt)
4.5.5. 创建 Agent Executor
python
# ================================
# 💡 ⑤ 创建 AgentExecutor
# 设置 `verbose=True` 后,它会以非常详细的方式打印出Agent的每一步思考和行动,
# 让你能清晰地看到其内部工作流程。
# ================================
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
4.5.6. 调用 Agent 并获取结果
python
# ================================
# 💡 ⑥ 调用Agent并获取结果
# 使用`.invoke()`方法来运行Agent。输入必须是一个字典,其键与提示中的占位符相对应。
# ================================
print("\n\n--- 开始执行Agent ---")
question = "单词 'LangChain' 的长度乘以 5 是多少?"
response = agent_executor.invoke({
"input": question
})
print("\n--- Agent执行完毕 ---")
# 响应`response`是一个字典,最终的答案通常在`output`键中
print("\n[最终答案]:")
print(response["output"])
运行输出结果:
