👦抠腚男孩的AI学习之旅 | 6、玩转 LangChain (二)

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 ,这能为你提供代码补全和类型检查等好处。一个关键的特性是使用 Annotatedadd_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的回答基于我们提供的实时、准确的资料,大大减少了 "幻觉" 的产生。
  • 知识可更新:只需要更新我们的知识库,而不需要重新训练昂贵的模型。

💁‍♂️ LangChainIndexes 组件正式实现 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):一种将文本转换成数值向量 (一串数字) 的技术。其神奇之处在于,语义上相似的文本,在数学空间中的向量也更"接近"。这项工作通常由专门的嵌入模型,如 OpenAItext-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中,工具可以是任何东西,比如:搜索引擎、计算器、数据库查询接口,甚至是你自己编写的任何函数。

除了 LLMTools 外,还需要一个 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。

通过 "自问自答 " 的方式将复杂问题分解,它专门设计用于一个单一但强大的工具:搜索 (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 CallingTool 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"])

运行输出结果:

相关推荐
洞窝技术7 小时前
洞窝基于RAG+Dify+钉钉快速搭建智能问答工具的落地实践
aigc·openai
大模型教程7 小时前
12天带你速通大模型基础应用(二)自动化调优Prompt
程序员·llm·agent
AI大模型8 小时前
无所不能的Embedding(02) - 词向量三巨头之FastText详解
程序员·llm·agent
AI大模型8 小时前
无所不能的Embedding(03) - word2vec->Doc2vec[PV-DM/PV-DBOW]
程序员·llm·agent
悟乙己8 小时前
使用 Python 中的强化学习最大化简单 RAG 性能
开发语言·python·agent·rag·n8n
YUELEI11812 小时前
langchain 缓存 Caching
缓存·langchain
roshy12 小时前
MCP(模型上下文协议)入门教程1
人工智能·大模型·agent
用户51914958484512 小时前
强大的OSINT情报工具:Blackbird用户名与邮箱搜索分析平台
人工智能·aigc
用户51914958484513 小时前
30条顶级APT与蓝队攻防单行命令:网络战场终极对决
人工智能·aigc