LangChain Agent 中间件完全指南:六种钩子函数从入门到生产(附完整教学代码)

1. 钩子中间件全景图:Agent 执行流程中的六种钩子

在开始写代码之前,我们先通过一张完整的流程图来理解 Agent 执行过程中,每个钩子在哪里被触发。这张图来自原始笔记,是理解整个中间件体系的核心。

下面我们构建一个完整的 Agent,依次使用 before_agentbefore_modelwrap_model_callafter_modelwrap_tool_callafter_agent 六种钩子,同时附带 dynamic_prompt 作为特殊形式。每个钩子都实现了典型的生产级功能,并配有详细注释。完整的敲完代码之后能够熟悉地掌握一个完整的 Agent 的钩子函数的运作过程。

1.1 流程图:六种钩子的执行顺序

text 复制代码
用户输入
    │
    ▼
┌─────────────────────────────────────────────────────────────────────┐
│  [before_agent]                                                     │
│  作用:全局初始化、权限校验、注入初始状态                             │
│  可修改:state                                                       │
└─────────────────────────────────────────────────────────────────────┘
    │
    ▼
┌─────────────────────────────────────────────────────────────────────┐
│  [动态提示词](dynamic_prompt)                                      │
│  作用:根据 state/context 生成 system prompt                         │
│  注:实际是 wrap_model_call 的一种特例,但语义上独立                  │
└─────────────────────────────────────────────────────────────────────┘
    │
    ▼
┌─────────────────────────────────────────────────────────────────────┐
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │              Agent 主循环(直到停止条件满足)                 │   │
│  │                                                             │   │
│  │  ┌─────────────────────────────────────────────────────┐   │   │
│  │  │  [before_model]                                     │   │   │
│  │  │  作用:动态修改 messages、请求限速、注入上下文        │   │   │
│  │  │  可修改:state, ModelRequest                         │   │   │
│  │  └─────────────────────────────────────────────────────┘   │   │
│  │                          │                                  │   │
│  │                          ▼                                  │   │
│  │  ┌─────────────────────────────────────────────────────┐   │   │
│  │  │  【wrap_model_call】                                 │   │   │
│  │  │  作用:模型替换、缓存、重试、超时控制、错误处理       │   │   │
│  │  │  可拦截:可跳过 handler,直接返回响应                 │   │   │
│  │  │                                                      │   │   │
│  │  │     handler(request) ──┐                            │   │   │
│  │  │         │              │                            │   │   │
│  │  │         ▼              │                            │   │   │
│  │  │    实际模型调用         │                            │   │   │
│  │  │    (OpenAI/Anthropic)│                            │   │   │
│  │  │         │              │                            │   │   │
│  │  │         ▼              │                            │   │   │
│  │  │    ┌──────────────────┴─────────────────────────┐  │   │   │
│  │  │    │           返回 ModelResponse                │  │   │   │
│  │  │    └────────────────────────────────────────────┘  │   │   │
│  │  └─────────────────────────────────────────────────────┘   │   │
│  │                          │                                  │   │
│  │                          ▼                                  │   │
│  │  ┌─────────────────────────────────────────────────────┐   │   │
│  │  │  [after_model]                                      │   │   │
│  │  │  作用:响应验证、敏感词过滤、结果转换、条件路由       │   │   │
│  │  │  可修改:ModelResponse                               │   │   │
│  │  └─────────────────────────────────────────────────────┘   │   │
│  │                          │                                  │   │
│  │                          ▼                                  │   │
│  │             响应中是否包含工具调用?                         │   │
│  │                    │          │                             │   │
│  │         ┌─────────┘          └─────────┐                   │   │
│  │         │ 是                            │ 否               │   │
│  │         ▼                               ▼                   │   │
│  │  ┌─────────────────────┐         ┌──────────────┐         │   │
│  │  │ 【wrap_tool_call】   │         │  结束循环    │         │   │
│  │  │ 包裹每个工具调用     │         │  退出到      │         │   │
│  │  │                     │         │  after_agent │         │   │
│  │  │  handler(request) ──┐         └──────────────┘         │   │
│  │  │      │              │                                  │   │
│  │  │      ▼              │                                  │   │
│  │  │   实际工具执行       │                                  │   │
│  │  │   (API/DB/计算)   │                                  │   │
│  │  │      │              │                                  │   │
│  │  │      ▼              │                                  │   │
│  │  │ 返回 ToolMessage    │                                  │   │
│  │  │                     │                                  │   │
│  │  │  作用:参数校验、    │                                  │   │
│  │  │  错误恢复、重试、    │                                  │   │
│  │  │  执行监控、审计      │                                  │   │
│  │  └─────────────────────┘                                  │   │
│  │                          │                                  │   │
│  │         ┌────────────────┘                                  │   │
│  │         ▼                                                   │   │
│  │    将 ToolMessage 加入 state["messages"]                     │   │
│  │    继续下一轮循环(回到 before_model)                       │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘
    │
    ▼
┌─────────────────────────────────────────────────────────────────────┐
│  [after_agent]                                                      │
│  作用:最终结果处理、数据落库、清理资源、汇总指标                     │
│  可修改:最终输出 state                                              │
└─────────────────────────────────────────────────────────────────────┘
    │
    ▼
最终输出(最终回答)

1.2 关键流转说明

  • before_agent 只执行一次,在最开始。
  • dynamic_prompt 本质上是 wrap_model_call 的封装,在每次模型调用前动态生成系统提示。
  • 模型调用循环(before_modelwrap_model_call → 实际模型 → after_model)可能执行多次,直到没有工具调用或达到停止条件。
  • 工具调用(wrap_tool_call)每次执行一个工具,可能多次执行(并行或串行)。
  • after_agent 只执行一次,在所有循环结束后。

LangChain 的设计一致性 :所有 Node-style 钩子(before_agent, before_model, after_model, after_agent)都接收 stateruntime 两个参数。


2. 完整教学代码:六种钩子中间件的综合应用(显式类版本)

以下代码使用 TypedDict 定义状态,实现类型安全的中间件开发。请确保已安装 langchainlanggraphpython-dotenv 等依赖。

2.1 导入依赖和配置日志

python 复制代码
"""
完整教学代码:六种钩子中间件的综合应用(显式类版本)
使用 TypedDict 定义状态,实现类型安全的中间件开发
"""

import asyncio
import time
import logging
from typing import Any, Dict, Optional, Annotated, TypedDict, List
from langchain_core.messages import BaseMessage, ToolMessage, HumanMessage, AIMessage
from langchain.agents import create_agent, AgentState
from langchain.agents.middleware import (
    before_agent, before_model, after_model, after_agent,
    wrap_model_call, wrap_tool_call, dynamic_prompt,
    ModelRequest, ModelResponse
)
from langchain.tools import tool
from dotenv import load_dotenv
from langchain.agents.middleware import ModelResponse

# 加载 .env 文件中的环境变量
load_dotenv()

# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

这段代码在做什么?

  • 导入所有需要的钩子装饰器和类型
  • load_dotenv().env 文件读取 API Key(如 QWEN_API_KEY
  • 配置日志输出格式,方便我们观察每个钩子的触发顺序

3. 定义状态类(类型安全的关键)

3.1 为什么需要显式定义状态?

在 LangChain 中,Agent 的状态(state)是一个字典,包含 messages 对话历史以及自定义字段。如果不定义类型,IDE 无法提供自动补全,容易写错字段名。使用 TypedDict 可以让状态变得类型安全

但是,TypedDictAgentState 有什么区别?什么时候用哪个?原始笔记中有非常详细的辨析,我们完整保留如下。

辨析:TypedDict vs AgentState vs MessagesState

  • TypedDict(Python 原生)

    • 特点:Python 原生支持,IDE 有类型提示。没有特殊功能,就是普通字典。messages 字段每次更新都会覆盖!

    • 示例:

      python 复制代码
      class MyState(TypedDict):
          name: str
          age: int
  • AgentState(LangChain 定制版)

    • 特点:继承了 TypedDict 的所有能力,自动管理 messages 字段(不会覆盖,会追加),LangChain/LangGraph 能识别它,支持 reducer(状态合并策略)。

    • 示例:

      python 复制代码
      class UserMemory(AgentState):
          messages: Annotated[list, add_messages]  # 自动追加
          user_preferences: dict

覆盖问题说明

  • ❌ 不要这样:

    python 复制代码
    class MyState(TypedDict):
        messages: list  # 没有 add_messages 注解,会被覆盖!
  • ✅ 应该这样:

    python 复制代码
    class MyState(AgentState):
        messages: Annotated[list, add_messages]  # 自动追加

通俗比喻

  • TypedDict:📄 空白表格模板,告诉你有哪些字段要填,但没有任何特殊功能。
  • MessagesState:📋 带"历史记录"栏的表格,在空白表格基础上,预设了一个会自动追加内容的"历史记录"栏。
  • AgentState:🎯 智能助手专用表格,和 MessagesState 一样,但名字更贴切,专门给 Agent 用的。

使用场景建议

  • ✅ 定义 Agent 的状态 → 用 AgentState
  • ✅ 中间件扩展状态 → 可以用 TypedDict
  • ✅ 简单临时状态 → TypedDict 足够

总结

  • 核心差异:TypedDict = 普通字典 + 类型检查;AgentState = TypedDict + 自动管理 messages + LangGraph 优化。
  • 使用建议:做 Agent 项目 → 无脑用 AgentState;其他场景 → 用 TypedDict 就够。

3.2 定义 AppState

python 复制代码
class AppState(TypedDict):
    """Agent 的完整状态结构,使用 TypedDict 实现类型安全
    Python中的TypedDict:让字典更加的准确和安全
    Typedict    定义必需的键
                指定键的类型
                在静态检查时发现问题
                保持运行时灵活性
    """
    # messages 使用 add_messages reducer,自动合并而非覆盖
    messages: Annotated[List[BaseMessage], "add_messages"]
    """
    先说 Annotated(adj:带注解的,注释的) 是什么?
    Annotated 是 Python 的类型注解增强工具,它允许你给一个类型添加额外的元数据(metadata)。
    简单比喻:
    普通类型注解:messages: List[BaseMessage] → "这是一个消息列表"
    Annotated 注解:
    messages: Annotated[List[BaseMessage], "add_messages"] → "这是一个消息列表,并且要用 add_messages 规则来合并它"
  
    Annotated[基础类型,额外信息 1, 额外信息 2, ...]
    "add_messages": Merges two lists of messages, updating existing messages by ID.
    
    为什么需要 "add_messages"(是LangChain自带的硬编码规则)?
    这是 LangGraph/LangChain 的特殊机制!
    问题场景:
    假设有 3 个中间件都要修改 messages:
    # 中间件 1
    return {"messages": [新消息 1]}
    # 中间件 2
    return {"messages": [新消息 2]}
    # 中间件 3
    return {"messages": [新消息 3]}
    如果没有 add_messages:后面的返回值会覆盖前面的;最后只剩 [新消息 3],丢失了前两条消息 ❌
    有了 add_messages:LangChain 会自动合并所有消息
    最终结果:[新消息 1, 新消息 2, 新消息 3] ✅
    """

    # 自定义业务字段
    user_id: str
    start_time: float
    call_count: int
    before_model_triggered: bool
    after_agent_cleanup: bool
    # 可选字段(用 NotRequired 标记,Python 3.11+ 可用,这里用 Optional)
    intermediate_results: Optional[Dict[str, Any]]

这段代码在做什么?

  • messages 字段使用 Annotated + "add_messages" 告诉 LangChain:多个中间件返回的 messages合并而不是覆盖。
  • 自定义字段 user_idstart_timecall_count 等用于在钩子之间传递数据。
  • 这个 AppState 类会在 create_agent 中作为 state_schema 传入。

4. 定义工具

python 复制代码
# ========== 2. 定义工具 ==========
@tool
def get_weather(location: str) -> str:
    """获取指定城市的天气(模拟可能失败的 API)"""
    if location.lower() == "error":
        raise ValueError("模拟的 API 错误:无法获取天气")
    return f"{location} 天气晴朗,温度 25°C"

说明 :这个工具接受 location 参数,如果传入 "error" 会抛出异常,用于测试 wrap_tool_call 的错误恢复功能。


5. 定义钩子函数(核心内容)

以下每个钩子都配有:作用说明代码实现(带详细注释)执行流程解释

5.1 before_agent:Agent 开始前的全局初始化

作用:在 Agent 主循环开始前执行一次,常用于权限校验、初始化计时器、注入用户 ID 等。

python 复制代码
# ========== 3. 定义钩子(全部使用显式类型) ==========

# 3.1 before_agent: 全局初始化
@before_agent
def log_agent_start(state: AppState, runtime) -> Optional[Dict[str, Any]]:
    """
    Agent 开始前触发。
    参数 state 现在有完整的类型提示,IDE 会自动补全字段。
    """
    logger.info("=== before_agent: Agent 开始执行 ===")

    # 现在可以直接使用 state["user_id"],IDE 会提示这是 str 类型
    user_id = runtime.context.get("user_id", "anonymous")
    start_time = time.time()

    # 返回要更新的 state 字段(类型安全)
    return {
        "user_id": user_id,
        "start_time": start_time,
        "call_count": state.get("call_count", 0) + 1,
        "intermediate_results": {}
    }

执行流程解释

  • runtime.context 中获取调用时传入的 user_id(见后面 agent.invokecontext 参数)
  • 记录开始时间戳
  • 返回一个字典,这些字段会被合并 到当前 state
  • 后续的钩子(如 dynamic_prompt)可以通过 state["user_id"] 访问

5.2 答疑:runtime 是什么?request.statestate 的关系?

原始笔记中有一段非常详细的答疑,完整保留如下(已转为普通文本)。

答疑:Runtime 与 state 的关系

1. Runtime 是什么?有什么用?必备的吗?

在 LangChain v1 中,Runtime 是由 LangGraph 提供的运行时环境对象,用于在代理(Agent)、工具(Tool)和中间件(Middleware)中传递上下文、持久化存储以及流式输出等运行信息。它是 create_agent 和图编排的核心支撑。
核心组成

  • context:静态上下文信息(如 user_id、数据库连接等),通常用 dataclass 定义,替代旧的 RunnableConfig
  • storeBaseStore 实例,用于长期记忆和持久化数据。
  • stream_writer:自定义流写入器,用于 "custom" 流模式下推送进度或更新。
  • previous:上一个节点的返回值(仅在使用检查点时可用)。
  • config:运行时配置(如 thread_id)。
  • state:当前状态(在 Wrap-style 中间件中通过 request.state 访问)。

为什么 runtime 里有 state?包含关系?而不是并列关系?怎么还有 request.state?这么多 state?

runtime.stateAppStaterequest.state 是同一个东西,只是以不同形式存在。

  • request.state 就是你定义的 AppState 实例。
  • 访问路径不同:Node-style 直接给 state 参数,Wrap-style 通过 request.state 取。
  • 测试证明:
    • Node-style 中 state 的 id:140234567890
    • Node-style 中 runtime.state 的 id:140234567890
    • Wrap-style 中 request.state 的 id:140234567890
  • 本质是同一个数据的不同入口。

2. ToolRuntime[Context] 是什么?

这是泛型标注,表示 runtime 对象中 context 字段的类型是 ContextToolRuntime 是 LangGraph 内部的运行时类型,用于工具中间件。

所有 runtime 都是同一个类型,只是在不同钩子中通过不同方式访问:Node-style 钩子直接接收 runtime,Wrap-style 钩子通过 request.runtime 访问。


5.3 dynamic_prompt:动态系统提示

作用 :它不是真正的"中间件",而是一个"提示词生成器"------在每次模型调用前,根据当前状态动态生成 system prompt

python 复制代码
# 3.2 dynamic_prompt: 动态系统提示
@dynamic_prompt
def custom_system_prompt(request: ModelRequest) -> str:
    """
    根据状态动态生成系统提示。
    注意:dynamic_prompt 的参数是 ModelRequest,不是 state,
    但可以通过 request.state 访问状态。
    """
    # 这里 request.state 的类型是 Any,但实际是 AppState
    state: AppState = request.state  # 类型断言,让 IDE 识别
    user_id = state.get("user_id", "访客")
    call_count = state.get("call_count", 1)

    prompt = f"你是一个智能助手,正在为 {user_id} 服务。"
    if call_count > 3:
        prompt += " 用户已经多次提问,请提供更简洁的回答。"

    logger.info(f"dynamic_prompt 生成系统提示: {prompt[:50]}...")
    return prompt

补充说明(来自原始笔记)

dynamic_prompt 不是真正的"中间件",而是一个"提示词生成器"------它不处理请求/响应,只负责动态生成 system prompt。大部分情况只能返回字符串(prompt 文本),每次构建提示词前都调用,根据上下文动态生成提示词。参数为 ModelRequest

示例(根据对话内容动态切换 AI 角色):

python 复制代码
@dynamic_prompt
def role_based_prompt(request: ModelRequest):
    last_message = request.state["messages"][-1].content.lower()
    if "代码" in last_message or "编程" in last_message:
        return "你是一位资深程序员专家。提供清晰的代码示例,解释技术原理,指出最佳实践。"
    if "文案" in last_message or "写作" in last_message:
        return "你是一位创意文案专家。文字生动有趣,结构清晰,符合目标受众。"
    return "你是一个乐于助人的 AI 助手。"

执行流程解释

  • 每次模型调用前,LangChain 会执行这个函数,获取返回的字符串作为本次调用的系统提示。
  • 这里根据 user_idcall_count 动态改变提示内容。
  • 注意:dynamic_prompt 的参数是 ModelRequest,而不是 state,但可以通过 request.state 访问状态。

5.4 before_model:模型调用前的处理

作用 :在每次模型调用前执行,可以修改 messages 或注入额外信息。

python 复制代码
# 3.3 before_model: 模型调用前处理
@before_model
def before_model_hook(state: AppState, runtime) -> Optional[Dict[str, Any]]:
    """
    模型调用前触发,可以修改请求。
    现在 state 有完整类型,可以直接访问字段。
    """
    logger.info("=== before_model: 准备调用模型 ===")

    # 类型安全:state["messages"] 被 IDE 识别为 List[BaseMessage]
    messages = state.get("messages", [])

    # 如果消息过多,裁剪(但注意 messages 是 Annotated,这里直接操作可能影响 reducer)
    if len(messages) > 10:
        logger.warning(f"消息数量过多({len(messages)}),裁剪至最后 5 条")
        # 注意:这里直接修改 state 可能不通过 reducer,最好返回更新
        # 我们通过返回值来更新
        return {"messages": messages[-5:], "before_model_triggered": True}

    return {"before_model_triggered": True}

执行流程解释

  • 检查当前 messages 列表长度,如果超过 10 条,则裁剪到最近 5 条(防止上下文过长导致 token 浪费)。
  • 通过返回值更新状态,设置 before_model_triggered = True 用于调试。

5.5 wrap_model_call:包裹模型调用(实现缓存和重试)

作用 :这是最核心的钩子之一。它包裹 实际的模型调用函数(handler),允许你在调用前后插入逻辑,比如缓存、重试、超时控制

python 复制代码
# 3.4 wrap_model_call: 包裹模型调用(缓存 + 重试)
@wrap_model_call
def model_call_wrapper(request: ModelRequest, handler):
    """
    包裹模型调用,实现缓存和重试。
    request 包含 state,但类型是 Any,我们可以断言。
    """
    logger.info("=== wrap_model_call: 进入模型调用包装器 ===")
    # 类型断言,让 IDE 知道 state 的结构
    state: AppState = request.state
    # 简单缓存:使用消息内容作为键
    cache_key = str(request.messages)[:200]
    if not hasattr(model_call_wrapper, "cache"):
        model_call_wrapper.cache = {}
    cached = model_call_wrapper.cache.get(cache_key)
    if cached:
        logger.info("wrap_model_call: 缓存命中,直接返回")
        return cached
    # 重试机制
    max_retries = 2
    for attempt in range(max_retries + 1):
        try:
            logger.info(f"wrap_model_call: 第 {attempt + 1} 次尝试")
            response = handler(request)
            model_call_wrapper.cache[cache_key] = response
            logger.info("wrap_model_call: 调用成功")
            return response
        except Exception as e:
            logger.error(f"wrap_model_call: 失败 (尝试 {attempt + 1}): {e}")
            if attempt == max_retries:
                raise
            time.sleep(2 ** attempt)
    return None  # 不会执行到这里

答疑:hasattr 为什么总和 @wrap_model_call 一起出现?

hasattr 是什么?
hasattr 用于检查某个对象有没有某个属性/功能。就像你买手机时问:"这手机有无线充电吗?"

用法:hasattr(对象, 属性名),返回 TrueFalse

生活化示例:

python 复制代码
class Person:
    def __init__(self, name):
        self.name = name
        self.age = 25
person = Person("小明")
hasattr(person, "name")   # True
hasattr(person, "age")    # True
hasattr(person, "salary") # False

为什么总和 @wrap_model_call 一起出现?

核心原因:AI 模型的响应(AIMessage)有时候有工具调用,有时候没有!你需要先检查:"有工具调用吗?" → 有就处理,没有就跳过。

关键代码:

python 复制代码
if hasattr(response, "tool_calls") and response.tool_calls:
    # ✅ 有工具调用 → 需要特殊处理

例如 AI 回复:

python 复制代码
AIMessage(
    content="",
    tool_calls=[{"name": "get_weather", "args": {"location": "北京"}, "id": "call_123"}]
)
hasattr(ai_msg, "tool_calls")  # True
ai_msg.tool_calls              # [...] 有内容

hasattr 用于检查有无工具调用,避免访问不存在的属性导致报错。

执行流程解释

  • handler(request) 是真正的模型调用(或下一个中间件)。
  • 这里实现了两个功能:
    1. 缓存 :将请求的 messages 转为字符串作为 key,如果命中缓存则直接返回缓存的 ModelResponse,不再调用模型。
    2. 重试:如果调用失败(异常),最多重试 2 次,每次等待时间指数退避(2^attempt 秒)。
  • 注意 hasattr(model_call_wrapper, "cache") 用于给函数对象动态添加缓存字典。

5.6 重点解析:中间件函数参数的两种形态

原始笔记中有一段非常清晰的对比,完整保留如下(普通文本)。

中间件函数参数的两种形态

1. Node-style 钩子:(state, runtime)

适用钩子:before_agentafter_agentbefore_modelafter_model

python 复制代码
def hook(state: StateType, runtime) -> Optional[Dict[str, Any]]:
  • state:当前完整状态(字典)
  • runtime:运行时上下文
  • 特点:直接接收当前状态的完整快照;返回值(字典)会与现有状态合并;常用于观测和准备(日志、校验、状态修改)。

2. Wrap-style 钩子:(request, handler)

适用钩子:wrap_model_callwrap_tool_call

python 复制代码
def wrapper(request: ModelRequest | ToolCallRequest, handler: Callable) -> ModelResponse | ToolMessage:
  • request:包含请求的所有信息(messagesstatetool_calls 等)
  • handler:下一个中间件或实际执行函数
  • 特点request 中封装了 state(通过 request.state 访问);handler 是"继续执行"的函数引用;返回值直接是响应对象,不是状态更新;常用于控制和替换(缓存、重试、错误恢复)。

3. dynamic_prompt 的特殊性

python 复制代码
@dynamic_prompt
def my_prompt(request: ModelRequest) -> str:
  • 参数是 ModelRequest(与 wrap_model_call 相同)
  • 返回字符串(prompt),不是状态更新

返回值的意义与规则

  • Node-style 钩子 :返回 Optional[Dict[str, Any]]
    • None:不更新状态
    • {"key": value}:将字典与现有 state 合并(浅合并)。重要:返回的字典会被合并到当前 state,不会整体替换。这意味着你可以只返回要修改的字段。
  • Wrap-style 钩子 :返回响应对象
    • ModelResponse:直接作为模型调用结果
    • ToolMessage:直接作为工具执行结果
    • 调用 handler 的结果:执行后续中间件并返回结果
    • 关键:Wrap-style 钩子不返回状态更新,而是返回执行结果。

设计目的对比

  • Node-style 钩子:在目标前后插入逻辑 → 需要访问状态,可修改状态
  • Wrap-style 钩子:控制是否/如何执行目标 → 决定是否调用 handler,可替换结果

速查表

钩子 参数形式 返回值 访问 state 的方式
before_agent (state, runtime) Optional[Dict] 直接 state
before_model (state, runtime) Optional[Dict] 直接 state
after_model (response, runtime) Optional[ModelResponse] 通过 runtime.state
after_agent (state, runtime) Optional[Dict] 直接 state
wrap_model_call (request, handler) ModelResponse 通过 request.state
wrap_tool_call (request, handler) ToolMessage 通过 request.state
dynamic_prompt (request) str 通过 request.state

5.7 after_model:模型调用后的处理

作用:在模型返回响应后执行,可以过滤敏感词、添加元数据等。

python 复制代码
# 3.5 after_model: 模型调用后处理
@after_model
def after_model_hook(state: AppState, runtime) -> Optional[Dict[str, Any]]:
    """
    模型调用后触发,可以修改响应。
    注意:after_model 的参数是 state 字典,不是 ModelResponse。
    需要从 state 中获取最后一条消息(模型的回答)。
    """
    logger.info("=== after_model: 模型调用后处理 ===")

    # 从 state 中获取消息列表
    messages = state.get("messages", [])
    if messages:
        # 获取最后一条消息(模型的回答)
        last_message = messages[-1]
        content = last_message.content if hasattr(last_message, 'content') else str(last_message)
        
        # 过滤敏感词
        if "敏感词" in content:
            content = content.replace("敏感词", "[已过滤]")
            logger.warning("after_model: 检测到敏感词并过滤")
            # 修改消息内容
            if hasattr(last_message, 'content'):
                last_message.content = content
        
        # 添加自定义元数据
        if hasattr(last_message, "additional_kwargs"):
            last_message.additional_kwargs["after_model_processed"] = True
    
    return {"after_model_processed": True}

执行流程解释

  • state["messages"] 中取出最后一条消息(即模型刚刚生成的回答)
  • 检查内容是否包含敏感词,如果包含则替换
  • 在消息的 additional_kwargs 中添加标记,表示已经过 after_model 处理
  • 返回 {"after_model_processed": True} 更新状态

5.8 wrap_tool_call:包裹工具调用

作用:包裹每个工具的执行,实现参数校验、错误恢复、重试等。

python 复制代码
# 3.6 wrap_tool_call: 包裹工具调用
@wrap_tool_call
def tool_call_wrapper(request, handler):
    #request 的类型是 ToolCallRequest,不是 ModelRequest。!!!!!
    """
    包裹工具调用,实现参数校验和错误恢复。
    request 的类型是 ToolCallRequest,不是 ModelRequest。
    """
    tool_name = request.tool_call["name"]
    args = request.tool_call.get("args", {})
    logger.info(f"=== wrap_tool_call: 工具 {tool_name} 被调用,参数: {args} ===")

    # 参数校验
    if tool_name == "get_weather" and not args.get("location"):
        return ToolMessage(
            content="错误:location 参数不能为空。",
            tool_call_id=request.tool_call["id"]
        )

    # 重试逻辑
    max_retries = 2
    for attempt in range(max_retries + 1):
        try:
            logger.info(f"wrap_tool_call: 第 {attempt + 1} 次尝试")
            result = handler(request)
            logger.info(f"wrap_tool_call: 工具 {tool_name} 成功")
            return result
        except Exception as e:
            logger.error(f"wrap_tool_call: 失败 (尝试 {attempt + 1}): {e}")
            if attempt == max_retries:
                return ToolMessage(
                    content=f"工具执行失败:{str(e)}。请稍后重试。",
                    tool_call_id=request.tool_call["id"]
                )
            time.sleep(2 ** attempt)

wrap_tool_call 的调用链解释

text 复制代码
wrap_tool_call 中间件 A
    │
    └─ handler → wrap_tool_call 中间件 B
                      │
                      └─ handler → 实际工具执行函数

handler(request) 与工具调用的关系:
    代码位置	            实际执行的内容
    handler(request)	执行后续中间件(如果有),最终执行真正的工具函数
    真正的工具函数        	get_weather(location="北京") 等

执行流程解释

  • request.tool_call 包含了工具名称、参数和调用 ID。
  • 首先进行参数校验:如果工具是 get_weather 且没有提供 location,直接返回错误 ToolMessage,不再调用实际工具。
  • 然后进入重试循环:调用 handler(request) 执行实际工具(或下一个中间件),如果抛出异常则重试,最多 2 次。
  • 重试全部失败后,返回一个友好的错误 ToolMessage,而不是让 Agent 崩溃。

5.9 after_agent:Agent 执行结束后的清理

作用:在整个 Agent 主循环结束后执行一次,用于汇总指标、记录耗时、清理资源。

python 复制代码
# 3.7 after_agent: Agent 执行结束
@after_agent
def after_agent_hook(state: AppState, runtime) -> Optional[Dict[str, Any]]:
    """
    Agent 执行结束后触发。
    现在 state 有完整类型,可以直接访问所有字段。
    """
    logger.info("=== after_agent: Agent 执行结束 ===")

    start_time = state.get("start_time")
    if start_time:
        elapsed = time.time() - start_time
        logger.info(f"Agent 总耗时: {elapsed:.2f} 秒")
        logger.info(f"用户 {state.get('user_id')} 完成了 {state.get('call_count')} 次调用")

    # 返回清理标记
    return {"after_agent_cleanup": True}

执行流程解释

  • state 中读取 start_time(由 before_agent 设置),计算总耗时
  • 打印用户 ID 和调用次数
  • 返回 after_agent_cleanup = True 标记清理完成

6. 创建 Agent(传入 state_schema 和 middleware)

python 复制代码
# ========== 4. 创建 Agent(显式传入 state_schema) ==========
# 使用阿里云通义千问模型(根据你的环境配置)
from langchain_community.chat_models import ChatTongyi
import os

model = ChatTongyi(
    model="qwen3-max",
    api_key=os.getenv("QWEN_API_KEY"),  # 从环境变量读取
)

agent = create_agent(
    model=model,
    tools=[get_weather],
    state_schema=AppState,  # 关键:告诉 Agent 使用显式状态结构
    middleware=[
        log_agent_start,  # before_agent
        custom_system_prompt,  # dynamic_prompt
        before_model_hook,  # before_model
        model_call_wrapper,  # wrap_model_call
        after_model_hook,  # after_model
        tool_call_wrapper,  # wrap_tool_call
        after_agent_hook  # after_agent
    ]
)

说明

  • state_schema=AppState 让 Agent 知道状态的结构,IDE 可以在钩子中提供类型提示。
  • middleware 列表的顺序很重要:before_agent 最先执行,然后 dynamic_prompt,接着进入主循环(before_modelwrap_model_callafter_model → 如果需要工具则 wrap_tool_call),最后 after_agent

7. 执行调用(测试缓存和错误恢复)

python 复制代码
# ========== 5. 执行调用 ==========
async def main():
    # 第一次调用:正常
    print("\n========== 第一次调用(正常) ==========")
    result1 = agent.invoke(
        {
            "messages": [HumanMessage(content="北京天气如何?")],
            # 可选:初始状态字段
            "user_id": "user_123",
            "call_count": 0
        },
        context={"user_id": "user_123"}  # context 用于动态提示
    )
    print("最终回答:", result1["messages"][-1].content)

    # 第二次调用:测试缓存(相同消息)
    print("\n========== 第二次调用(测试缓存) ==========")
    result2 = agent.invoke(
        {
            "messages": [HumanMessage(content="北京天气如何?")],
            "user_id": "user_123"
        },
        context={"user_id": "user_123"}
    )
    print("最终回答:", result2["messages"][-1].content)

    # 第三次调用:测试工具错误恢复
    print("\n========== 第三次调用(工具错误恢复) ==========")
    result3 = agent.invoke(
        {
            "messages": [HumanMessage(content="error 天气如何?")],
            "user_id": "user_123"
        },
        context={"user_id": "user_123"}
    )
    print("最终回答:", result3["messages"][-1].content)

# 执行
asyncio.run(main())

执行效果预期

  • 第一次调用:正常调用模型,可能触发工具调用,wrap_model_call 会缓存结果。
  • 第二次调用(相同问题):wrap_model_call 命中缓存,直接返回,不实际调用模型(日志会显示"缓存命中")。
  • 第三次调用(location="error"):工具 get_weather 会抛出异常,wrap_tool_call 会重试 2 次后返回错误消息,Agent 不会崩溃。

8. 总结:中间件的实现方式辨析

原始笔记最后有一个总结,澄清了中间件实现方式的疑惑。

中间件的实现方法到底有多少种?

  1. 函数装饰器形式(@before_model, @wrap_tool_call 等)✓
  2. 继承基类形式(继承 AgentMiddleware)✓
  3. 框架内置中间件(如 ToolRetryMiddleware)✓

更准确来说,只有两种语法形式:函数装饰器类继承

text 复制代码
两种实现方式 + 一种使用来源
├── 方式 1: 函数装饰器 (Function-style Hooks)
│   ├── @before_agent
│   ├── @before_model
│   ├── @wrap_model_call
│   ├── @after_model
│   ├── @wrap_tool_call
│   └── @after_agent
│
├── 方式 2: 类继承 (Class-based Middleware)
│   └── class MyMiddleware(AgentMiddleware):
│       - state_schema
│       - tools
│       - before_model() 方法
│       - after_agent() 方法
│
└── (不准确的说)特殊的方式3:
    来源:框架提供的中间件实例 (开箱即用)
    - ToolRetryMiddleware()     ← 内部也是继承 AgentMiddleware
    - SummarizationMiddleware() ← 内部也是继承 AgentMiddleware
    - ModelFallbackMiddleware() ← 内部也是继承 AgentMiddleware

所有中间件最终都被放入 middleware=[...] 列表传给 create_agent()


写在最后

本文完整保留了原始 .py 笔记中 95% 以上的注释和讲解,并按照 "流程图 → 状态定义 → 工具 → 每个钩子的作用+代码+解释 → 创建 Agent → 执行测试 → 总结" 的顺序重新组织。希望通过这份详实的教学代码,你能彻底掌握 LangChain Agent 的钩子中间件体系。

关键 Takeaways

  1. 六种钩子 覆盖了 Agent 执行的完整生命周期:before_agentdynamic_prompt → 主循环(before_modelwrap_model_callafter_modelwrap_tool_call) → after_agent
  2. Node-style 钩子(before_agentbefore_modelafter_modelafter_agent)接收 (state, runtime),返回状态更新字典
  3. Wrap-style 钩子(wrap_model_callwrap_tool_call)接收 (request, handler),返回响应对象
  4. dynamic_prompt 是特殊的提示词生成器,参数为 ModelRequest,返回字符串
  5. 状态类型安全 :使用 TypedDictAgentState 定义状态,配合 state_schema 传入
  6. 缓存和重试wrap_model_call 的典型应用,能大幅提升性能和稳定性
  7. 工具错误恢复 通过 wrap_tool_call 实现,避免 Agent 因单个工具失败而崩溃

本文为个人学习笔记与实践总结,如有错误或疏漏,欢迎指正交流。

如果觉得本文对你有帮助,欢迎点赞、收藏、转发!

附录: 个人完整代码笔记如下:敲代码多多益善

python 复制代码
#(练习==========完整的钩子函数===========演示:完整教学代码:六种钩子中间件的综合应用
"""
下面我们构建一个完整的 Agent,依次使用
before_agent、before_model、wrap_model_call、after_model、
wrap_tool_call、after_agent 六种钩子,
同时附带 dynamic_prompt 作为特殊形式。每个钩子都实现了典型的生产级功能,并配有详细注释。
完整的敲完代码之后能够熟悉的掌握一个完整的Agent的钩子函数的运作过程!
"""
from langchain_classic.chains.question_answering.map_reduce_prompt import messages
from torch.compiler.config import cache_key_tag

from AI大模型RAG与智能体开发_Agent项目.agent.tools.agent_tools import user_ids

"""
用户输入
    │
    ▼
┌─────────────────────────────────────────────────────────────────────┐
│  [before_agent]                                                     │
│  作用:全局初始化、权限校验、注入初始状态                             │
│  可修改:state                                                       │
└─────────────────────────────────────────────────────────────────────┘
    │
    ▼
┌─────────────────────────────────────────────────────────────────────┐
│  [动态提示词](dynamic_prompt)                                      │
│  作用:根据 state/context 生成 system prompt                         │
│  注:实际是 wrap_model_call 的一种特例,但语义上独立                  │
└─────────────────────────────────────────────────────────────────────┘
    │
    ▼
┌─────────────────────────────────────────────────────────────────────┐
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │              Agent 主循环(直到停止条件满足)                 │   │
│  │                                                             │   │
│  │  ┌─────────────────────────────────────────────────────┐   │   │
│  │  │  [before_model]                                     │   │   │
│  │  │  作用:动态修改 messages、请求限速、注入上下文        │   │   │
│  │  │  可修改:state, ModelRequest                         │   │   │
│  │  └─────────────────────────────────────────────────────┘   │   │
│  │                          │                                  │   │
│  │                          ▼                                  │   │
│  │  ┌─────────────────────────────────────────────────────┐   │   │
│  │  │  【wrap_model_call】                                 │   │   │
│  │  │  作用:模型替换、缓存、重试、超时控制、错误处理       │   │   │
│  │  │  可拦截:可跳过 handler,直接返回响应                 │   │   │
│  │  │                                                      │   │   │
│  │  │     handler(request) ──┐                            │   │   │
│  │  │         │              │                            │   │   │
│  │  │         ▼              │                            │   │   │
│  │  │    实际模型调用         │                            │   │   │
│  │  │    (OpenAI/Anthropic)│                            │   │   │
│  │  │         │              │                            │   │   │
│  │  │         ▼              │                            │   │   │
│  │  │    ┌──────────────────┴─────────────────────────┐  │   │   │
│  │  │    │           返回 ModelResponse                │  │   │   │
│  │  │    └────────────────────────────────────────────┘  │   │   │
│  │  └─────────────────────────────────────────────────────┘   │   │
│  │                          │                                  │   │
│  │                          ▼                                  │   │
│  │  ┌─────────────────────────────────────────────────────┐   │   │
│  │  │  [after_model]                                      │   │   │
│  │  │  作用:响应验证、敏感词过滤、结果转换、条件路由       │   │   │
│  │  │  可修改:ModelResponse                               │   │   │
│  │  └─────────────────────────────────────────────────────┘   │   │
│  │                          │                                  │   │
│  │                          ▼                                  │   │
│  │             响应中是否包含工具调用?                         │   │
│  │                    │          │                             │   │
│  │         ┌─────────┘          └─────────┐                   │   │
│  │         │ 是                            │ 否               │   │
│  │         ▼                               ▼                   │   │
│  │  ┌─────────────────────┐         ┌──────────────┐         │   │
│  │  │ 【wrap_tool_call】   │         │  结束循环    │         │   │
│  │  │ 包裹每个工具调用     │         │  退出到      │         │   │
│  │  │                     │         │  after_agent │         │   │
│  │  │  handler(request) ──┐         └──────────────┘         │   │
│  │  │      │              │                                  │   │
│  │  │      ▼              │                                  │   │
│  │  │   实际工具执行       │                                  │   │
│  │  │   (API/DB/计算)   │                                  │   │
│  │  │      │              │                                  │   │
│  │  │      ▼              │                                  │   │
│  │  │ 返回 ToolMessage    │                                  │   │
│  │  │                     │                                  │   │
│  │  │  作用:参数校验、    │                                  │   │
│  │  │  错误恢复、重试、    │                                  │   │
│  │  │  执行监控、审计      │                                  │   │
│  │  └─────────────────────┘                                  │   │
│  │                          │                                  │   │
│  │         ┌────────────────┘                                  │   │
│  │         ▼                                                   │   │
│  │    将 ToolMessage 加入 state["messages"]                     │   │
│  │    继续下一轮循环(回到 before_model)                       │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘
    │
    ▼
┌─────────────────────────────────────────────────────────────────────┐
│  [after_agent]                                                      │
│  作用:最终结果处理、数据落库、清理资源、汇总指标                     │
│  可修改:最终输出 state                                              │
└─────────────────────────────────────────────────────────────────────┘
    │
    ▼
最终输出(最终回答)


关键流转说明:
    before_agent 只执行一次,在最开始。
    dynamic_prompt 本质上是 wrap_model_call 的封装,在每次模型调用前动态生成系统提示。
    模型调用循环(before_model → wrap_model_call → 实际模型 → after_model)可能执行多次,直到没有工具调用或达到停止条件。
    工具调用(wrap_tool_call)每次执行一个工具,可能多次执行(并行或串行)。
    after_agent 只执行一次,在所有循环结束后。



LangChain 的设计一致性:
    所有 Node-style 钩子(before_agent, before_model, after_model, after_agent)
    都接收 state 和 runtime 两个参数

"""

"""
完整教学代码:六种钩子中间件的综合应用(显式类版本)
使用 TypedDict 定义状态,实现类型安全的中间件开发
"""

import asyncio
import time
import logging
from typing import Any, Dict, Optional, Annotated, TypedDict, List
from langchain_core.messages import BaseMessage, ToolMessage, HumanMessage, AIMessage
from langchain.agents import create_agent, AgentState
from langchain.agents.middleware import (
    before_agent, before_model, after_model, after_agent,
    wrap_model_call, wrap_tool_call, dynamic_prompt,
    ModelRequest, ModelResponse
)
from langchain.tools import tool
from dotenv import load_dotenv
from langchain.agents.middleware import ModelResponse



# 加载 .env 文件中的环境变量
load_dotenv()

# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)


# ========== 1. 显式定义状态类(类型安全) ==========
from langgraph.graph import add_messages
#class AppState(TypedDict):......
#你可能会发现有时候状态继承的是TypeDict有时候却又是AgentState,这两哥们什么关系呢?什么时候用谁?其实还有MessageState
"""
AgentState 是 TypedDict 的"孙子类",专门为 LangChain Agent 定制的增强版。
1️⃣ TypedDict - Python 原生版本
    from typing import TypedDict
    
    class MyState(TypedDict):
        name: str
        age: int
        hobbies: list[str]
    
    # 使用
    person: MyState = {"name": "张三", "age": 25, "hobbies": ["编程"]}
特点:
✅ Python 原生支持
✅ IDE 有类型提示
❌ 没有特殊功能,就是普通字典
❌ messages字段没一簇更新都会覆盖!

2️⃣ AgentState - LangChain 定制版
    from langchain.agents import AgentState
    from typing import Annotated
    from langgraph.graph import add_messages
    
    class UserMemory(AgentState):  # ← 继承 AgentState
        messages: Annotated[list, add_messages]  # ← 自动累积消息
        user_preferences: dict
        interaction_count: int
特点:
✅ 继承了 TypedDict 的所有能力
✅ 自动管理 messages 字段(不会覆盖,会追加)
✅ LangChain/LangGraph 能识别它
✅ 支持 reducer(状态合并策略)


特别说明:覆盖问题:
    # ❌ 不要这样
    class MyState(TypedDict):  # 直接用 TypedDict
        messages: list  # 没有 add_messages 注解,会被覆盖!
    
    # ✅ 应该这样
    class MyState(AgentState):  # 继承 AgentState
        messages: Annotated[list, add_messages]  # 自动追加
    


辨析:
TypedDict:
📄 空白表格模板
告诉你有哪些字段要填,但没有任何特殊功能

MessagesState:
📋 带"历史记录"栏的表格
在空白表格基础上,预设了一个会自动追加内容的"历史记录"栏

AgentState:
🎯 智能助手专用表格
和 MessagesState 一样,但名字更贴切,专门给 Agent 用的

=======使用场景======:
# ✅ 场景 1:定义 Agent 的状态 → 用 AgentState
    from langchain.agents import AgentState
    
    class MyAppState(AgentState):
        messages: Annotated[list, add_messages]
        custom_field: str  # 你的自定义字段
    
    agent = create_agent(
        model=model,
        state_schema=MyAppState  # ← 传这个
    )
    
    # ✅ 场景 2:中间件扩展状态 → 可以用 TypedDict
    from typing import TypedDict
    
    class UserMemory(TypedDict, total=False):
        user_id: str
        preferences: dict
    
    class MemoryMiddleware(AgentMiddleware):
        state_schema = UserMemory  # ← 这里用 TypedDict 没问题
        
    # ✅ 场景 3:简单临时状态 → TypedDict 足够
    def some_hook(state: TypedDict, runtime):
        # 只是读取状态,不扩展
        pass


总结:
    核心差异:
    TypedDict = 普通字典 + 类型检查
    AgentState = TypedDict + 自动管理 messages + LangGraph 优化
    
使用建议:
    做 Agent项目 → 无脑用 AgentState
    其他场景 → 用 TypedDict 就够

"""


class AppState(TypedDict):
    """Agent 的完整状态结构,使用 TypedDict 实现类型安全
    Python中的TypedDict:让字典更加的准确和安全
    Typedict    定义必需的键
                指定键的类型
                在静态检查时发现问题
                保持运行时灵活性
    """
    # messages 使用 add_messages reducer,自动合并而非覆盖
    messages: Annotated[List[BaseMessage], "add_messages"]
    """
先说 Annotated(adj:带注解的,注释的) 是什么?
    Annotated 是 Python 的类型注解增强工具,它允许你给一个类型添加额外的元数据(metadata)。
    简单比喻:
    普通类型注解:messages: List[BaseMessage] → "这是一个消息列表"
    Annotated 注解:
    messages: Annotated[List[BaseMessage], "add_messages"] → "这是一个消息列表,并且要用 add_messages 规则来合并它"
  
    Annotated[基础类型,额外信息 1, 额外信息 2, ...]
    "add_messages":Merges two lists of messages, updating existing messages by ID.
    
    
为什么需要 "add_messages"(是LangChain自带的硬编码规则)?
    这是 LangGraph/LangChain 的特殊机制!
    问题场景:
    假设有 3 个中间件都要修改 messages:
    # 中间件 1
    return {"messages": [新消息 1]}
    # 中间件 2
    return {"messages": [新消息 2]}
    # 中间件 3
    return {"messages": [新消息 3]}
    如果没有 add_messages:后面的返回值会覆盖前面的;最后只剩 [新消息 3],丢失了前两条消息 ❌
    有了 add_messages:LangChain 会自动合并所有消息
    最终结果:[新消息 1, 新消息 2, 新消息 3] ✅
        
        
    """


    # 自定义业务字段
    user_id: str
    start_time: float
    call_count: int
    before_model_triggered: bool
    after_agent_cleanup: bool
    # 可选字段(用 NotRequired 标记,Python 3.11+ 可用,这里用 Optional)
    intermediate_results: Optional[Dict[str, Any]]


# ========== 2. 定义工具 ==========
@tool
def get_weather(location: str) -> str:
    """获取指定城市的天气(模拟可能失败的 API)"""
    if location.lower() == "error":
        raise ValueError("模拟的 API 错误:无法获取天气")
    return f"{location} 天气晴朗,温度 25°C"


# ========== 3. 定义钩子(全部使用显式类型) ==========

# 3.1 before_agent: 全局初始化
@before_agent
def log_agent_start(state: AppState, runtime) -> Optional[Dict[str, Any]]:
    """
    Agent 开始前触发。
    参数 state 现在有完整的类型提示,IDE 会自动补全字段。
    """
    logger.info("=== before_agent: Agent 开始执行 ===")

    # 现在可以直接使用 state["user_id"],IDE 会提示这是 str 类型
    user_id = runtime.context.get("user_id", "anonymous")
    start_time = time.time()

    # 返回要更新的 state 字段(类型安全)
    return {
        "user_id": user_id,
        "start_time": start_time,
        "call_count": state.get("call_count", 0) + 1,
        "intermediate_results": {}
    }




#todo :答疑:
"""
1.Runtime/runtime是什么?有什么用?必备的吗?
在 LangChain v1 中,Runtime 是由 LangGraph 提供的运行时环境对象,用于在代理(Agent)、工具(Tool)和中间件(Middleware)中
传递上下文、持久化存储以及流式输出等运行信息。它是 create_agent 和图编排的核心支撑。
核心组成:
    context:静态上下文信息(如 user_id、数据库连接等),通常用 dataclass 定义,替代旧的 RunnableConfig。
    store:BaseStore 实例,用于长期记忆和持久化数据。
    stream_writer:自定义流写入器,用于 "custom" 流模式下推送进度或更新。
    previous:上一个节点的返回值(仅在使用检查点时可用)。
    config:运行时配置(如 thread_id)
    state:当前状态(在 Wrap-style 中间件中通过 request.state 访问)


====疑?为什么runtime里有state?包含关系?而不是并列关系?我怎么记得上面函数是两个参数都要传进去的呀?还有一个request.state
怎么这么多state?

答:
    runtime.state 与 class AppState,request.state 的关系;是的,它们是同一个东西,但以不同形式存在。
    request.state 就是你定义的 AppState 实例。只是访问路径不同------Node-style 直接给 state,Wrap-style 通过 request.state 取。
    测试:
    Node-style 中 state 的         id: 140234567890
    Node-style 中 runtime.state 的 id: 140234567890
    Wrap-style 中 request.state 的 id: 140234567890
    本质是同一个数据的不同入口。
    
    
2.ToolRuntime[Context] 是什么?   
这是泛型标注,表示 runtime 对象中 context 字段的类型是 Context。ToolRuntime 是 LangGraph 内部的运行时类型,用于工具中间件。

所有 runtime 都是同一个类型,只是在不同钩子中通过不同方式访问。
    Node-style 钩子直接接收 runtime,Wrap-style 钩子通过 request.runtime 访问。    
"""




# 3.2 dynamic_prompt: 动态系统提示
"""
它不是真正的"中间件",而是一个"提示词生成器" - 
它不处理请求/响应,只负责动态生成 system prompt。

大部分情况只能返回字符串(prompt 文本),每次构建提示词前都调用,根据上下文动态生成提示词
参数:ModelRequest

@dynamic_prompt
def role_based_prompt(request: ModelRequest):
    "根据对话内容动态切换 AI 角色"
    
    last_message = request.state["messages"][-1].content.lower()
    
    # 场景 1: 用户要写代码
    if "代码" in last_message or "编程" in last_message:
        return "你是一位资深程序员专家。
        - 提供清晰的代码示例
        - 解释技术原理
        - 指出最佳实践"
    
    # 场景 2: 用户要写文案
    if "文案" in last_message or "写作" in last_message:
        return "你是一位创意文案专家。
        - 文字生动有趣
        - 结构清晰
        - 符合目标受众"
    
    # 默认场景
    return "你是一个乐于助人的 AI 助手。"



"""
@dynamic_prompt
def custom_system_prompt(request: ModelRequest) -> str:
    """
    根据状态动态生成系统提示。
    注意:dynamic_prompt 的参数是 ModelRequest,不是 state,
    但可以通过 request.state 访问状态。
    """
    # 这里 request.state 的类型是 Any,但实际是 AppState
    state: AppState = request.state  # 类型断言,让 IDE 识别
    user_id = state.get("user_id", "访客")
    call_count = state.get("call_count", 1)

    prompt = f"你是一个智能助手,正在为 {user_id} 服务。"
    if call_count > 3:
        prompt += " 用户已经多次提问,请提供更简洁的回答。"

    logger.info(f"dynamic_prompt 生成系统提示: {prompt[:50]}...")
    return prompt


# 3.3 before_model: 模型调用前处理
@before_model
def before_model_hook(state: AppState, runtime) -> Optional[Dict[str, Any]]:
    """
    模型调用前触发,可以修改请求。
    现在 state 有完整类型,可以直接访问字段。
    """
    logger.info("=== before_model: 准备调用模型 ===")

    # 类型安全:state["messages"] 被 IDE 识别为 List[BaseMessage]
    messages = state.get("messages", [])

    # 如果消息过多,裁剪(但注意 messages 是 Annotated,这里直接操作可能影响 reducer)
    if len(messages) > 10:
        logger.warning(f"消息数量过多({len(messages)}),裁剪至最后 5 条")
        # 注意:这里直接修改 state 可能不通过 reducer,最好返回更新
        # 我们通过返回值来更新
        return {"messages": messages[-5:], "before_model_triggered": True}

    return {"before_model_triggered": True}



# 3.4 wrap_model_call: 包裹模型调用(缓存 + 重试)
@wrap_model_call
def model_call_wrapper(request: ModelRequest, handler):
    """
    包裹模型调用,实现缓存和重试。
    request 包含 state,但类型是 Any,我们可以断言。
    """
    logger.info("=== wrap_model_call: 进入模型调用包装器 ===")
    # 类型断言,让 IDE 知道 state 的结构
    state: AppState = request.state
    # 简单缓存:使用消息内容作为键
    cache_key = str(request.messages)[:200]
    if not hasattr(model_call_wrapper, "cache"):
        model_call_wrapper.cache = {}
    cached = model_call_wrapper.cache.get(cache_key)
    if cached:
        logger.info("wrap_model_call: 缓存命中,直接返回")
        return cached
    # 重试机制
    max_retries = 2
    for attempt in range(max_retries + 1):
        try:
            logger.info(f"wrap_model_call: 第 {attempt + 1} 次尝试")
            response = handler(request)
            model_call_wrapper.cache[cache_key] = response
            logger.info("wrap_model_call: 调用成功")
            return response
        except Exception as e:
            logger.error(f"wrap_model_call: 失败 (尝试 {attempt + 1}): {e}")
            if attempt == max_retries:
                raise
            time.sleep(2 ** attempt)
    return None  # 不会执行到这里


#解惑:hasattr是什么?为什么我总看见它和@wrap_model_call在一起出现??
"""
太好了!你发现了一个经典组合!
让我给你通俗解释 hasattr 和它为什么总和 @wrap_model_call 一起出现。
hasattr = "检查某个对象有没有某个属性/功能"
就像你买手机时问:"这手机有无线充电吗?"

hasattr(对象,属性名)
# 返回 True 或 False

生活化示例:
    class Person:
        def __init__(self, name):
            self.name = name
            self.age = 25
    
    person = Person("小明")
    
    # 检查有没有这些属性
    hasattr(person, "name")   # True ✅ - 有名字
    hasattr(person, "age")    # True ✅ - 有年龄
    hasattr(person, "salary") # False ❌ - 没工资这个属性
    hasattr(person, "eat")    # False ❌ - 没定义吃饭方法

为什么总和 @wrap_model_call 一起出现?
核心原因:
    AI 模型的响应(AIMessage)有时候有工具调用,有时候没有!
    你需要先检查:"有工具调用吗?" → 有就处理,没有就跳过

    ⚠  ️关键!检查 AI 的回复中是否有工具调用
    if hasattr(response, "tool_calls") and response.tool_calls:
        # ✅ 有工具调用 → 需要特殊处理
        
    # AI 回复
    AIMessage
    (
        content="",
        tool_calls=
        [{
            "name": "get_weather",
            "args": {"location": "北京"},
            "id": "call_123"
        }]
    )
    
    hasattr(ai_msg, "tool_calls")  # True ✅
    ai_msg.tool_calls              # [...] 有内容

hasattr:检查有无工具调用+避免访问不存在的属性导致报错
"""




#todo:重点解析+答疑:
"""
1.中间件函数参数的两种形态(这个函数的参数我到底填写哪一种?):

1.1 Node-style 钩子:(state, runtime)
适用钩子:before_agent、after_agent、before_model、after_model
    def hook(state: StateType, runtime) -> Optional[Dict[str, Any]]:
        # state: 当前完整状态(字典)
        # runtime: 运行时上下文

特点:
    直接接收当前状态的完整快照
    返回值(字典)会与现有状态合并
    常用于观测和准备(日志、校验、状态修改)



1.2 Wrap-style 钩子:(request, handler)
适用钩子:wrap_model_call、wrap_tool_call
    def wrapper(request: ModelRequest | ToolCallRequest, handler: Callable) -> ModelResponse | ToolMessage:
        # request: 包含请求的所有信息(messages、state、tool_calls等)
        # handler: 下一个中间件或实际执行函数
特点:
    request 中封装了 state(通过 request.state 访问)
    handler 是"继续执行"的函数引用
    返回值直接是响应对象,不是状态更新
    常用于控制和替换(缓存、重试、错误恢复)


1.3 dynamic_prompt 的特殊性
@dynamic_prompt
def my_prompt(request: ModelRequest) -> str:
    # 参数是 ModelRequest(与 wrap_model_call 相同)
    # 返回字符串(prompt),不是状态更新




2.返回值的意义与规则
2.1 Node-style 钩子:返回 Optional[Dict[str, Any]]
    def before_agent(state, runtime) -> Optional[Dict[str, Any]]:
        return {"user_id": "123"}  # 与现有 state 合并

返回值             行为
None                不更新状态
{"key": value}      将字典与现有 state 合并(浅合并)
,重要:返回的字典会被合并到当前 state,不会整体替换。这意味着你可以只返回要修改的字段。



2.2 Wrap-style 钩子:返回响应对象
    def wrap_model_call(request, handler):
        return handler(request)  # 返回 ModelResponse
        # 或直接返回 ModelResponse(缓存命中时)

返回值类型           行为
ModelResponse       直接作为模型调用结果
ToolMessage         直接作为工具执行结果
调用 handler 的结果  执行后续中间件并返回结果
,关键:Wrap-style 钩子不返回状态更新,而是返回执行结果。



钩子类型            设计目的               需要什么
Node-style      在目标前后插入逻辑      访问状态,可修改状态
Wrap-style      控制是否/如何执行目标    决定是否调用 handler,可替换结果

Node-style 钩子位于图节点之间,可以安全地读写状态;Wrap-style 钩子直接包裹目标函数,需要访问请求详情和控制执行流


速查表
钩子              参数形式               返回值                    访问 state 的方式
before_agent    (state, runtime)   Optional[Dict]         直接 state
before_model    (state, runtime)   Optional[Dict]         直接 state
after_model     (response, runtime)    Optional[ModelResponse]    通过 runtime.state
after_agent     (state, runtime)   Optional[Dict]         直接 state
=============================================================================
wrap_model_call (request, handler) ModelResponse          通过 request.state
wrap_tool_call  (request, handler) ToolMessage                通过 request.state
dynamic_prompt  (request)          str                        通过 request.state


"""




# 3.5 after_model: 模型调用后处理
@after_model
def after_model_hook(state: AppState, runtime) -> Optional[Dict[str, Any]]:
    """
    模型调用后触发,可以修改响应。
    注意:after_model 的参数是 state 字典,不是 ModelResponse。
    需要从 state 中获取最后一条消息(模型的回答)。
    """
    logger.info("=== after_model: 模型调用后处理 ===")

    # 从 state 中获取消息列表
    messages = state.get("messages", [])
    if messages:
        # 获取最后一条消息(模型的回答)
        last_message = messages[-1]
        content = last_message.content if hasattr(last_message, 'content') else str(last_message)
        
        # 过滤敏感词
        if "敏感词" in content:
            content = content.replace("敏感词", "[已过滤]")
            logger.warning("after_model: 检测到敏感词并过滤")
            # 修改消息内容
            if hasattr(last_message, 'content'):
                last_message.content = content
        
        # 添加自定义元数据
        if hasattr(last_message, "additional_kwargs"):
            last_message.additional_kwargs["after_model_processed"] = True
    
    return {"after_model_processed": True}


# 3.6 wrap_tool_call: 包裹工具调用
@wrap_tool_call
def tool_call_wrapper(request, handler):
    #request 的类型是 ToolCallRequest,不是 ModelRequest。!!!!!
    """
    包裹工具调用,实现参数校验和错误恢复。
    request 的类型是 ToolCallRequest,不是 ModelRequest。
    """
    tool_name = request.tool_call["name"]
    args = request.tool_call.get("args", {})
    logger.info(f"=== wrap_tool_call: 工具 {tool_name} 被调用,参数: {args} ===")

    # 参数校验
    if tool_name == "get_weather" and not args.get("location"):
        return ToolMessage(
            content="错误:location 参数不能为空。",
            tool_call_id=request.tool_call["id"]
        )

    # 重试逻辑
    max_retries = 2
    for attempt in range(max_retries + 1):
        try:
            logger.info(f"wrap_tool_call: 第 {attempt + 1} 次尝试")
            result = handler(request)
            logger.info(f"wrap_tool_call: 工具 {tool_name} 成功")
            return result
        except Exception as e:
            logger.error(f"wrap_tool_call: 失败 (尝试 {attempt + 1}): {e}")
            if attempt == max_retries:
                return ToolMessage(
                    content=f"工具执行失败:{str(e)}。请稍后重试。",
                    tool_call_id=request.tool_call["id"]
                )
            time.sleep(2 ** attempt)
"""
wrap_tool_call 中间件 A
    │
    └─ handler → wrap_tool_call 中间件 B
                      │
                      └─ handler → 实际工具执行函数

handler(request) 与工具调用的关系:
    代码位置                实际执行的内容
    handler(request)    执行后续中间件(如果有),最终执行真正的工具函数
    真正的工具函数         get_weather(location="北京") 等                      
                          

                      
"""

# 3.7 after_agent: Agent 执行结束
@after_agent
def after_agent_hook(state: AppState, runtime) -> Optional[Dict[str, Any]]:
    """
    Agent 执行结束后触发。
    现在 state 有完整类型,可以直接访问所有字段。
    """
    logger.info("=== after_agent: Agent 执行结束 ===")

    start_time = state.get("start_time")
    if start_time:
        elapsed = time.time() - start_time
        logger.info(f"Agent 总耗时: {elapsed:.2f} 秒")
        logger.info(f"用户 {state.get('user_id')} 完成了 {state.get('call_count')} 次调用")

    # 返回清理标记
    return {"after_agent_cleanup": True}


# ========== 4. 创建 Agent(显式传入 state_schema) ==========
# 使用阿里云通义千问模型(根据你的环境配置)
from langchain_community.chat_models import ChatTongyi
import os

model = ChatTongyi(
    model="qwen3-max",
    api_key=os.getenv("QWEN_API_KEY"),  # 从环境变量读取
)

agent = create_agent(
    model=model,
    tools=[get_weather],
    state_schema=AppState,  # 关键:告诉 Agent 使用显式状态结构
    middleware=[
        log_agent_start,  # before_agent
        custom_system_prompt,  # dynamic_prompt
        before_model_hook,  # before_model
        model_call_wrapper,  # wrap_model_call
        after_model_hook,  # after_model
        tool_call_wrapper,  # wrap_tool_call
        after_agent_hook  # after_agent
    ]
)


# ========== 5. 执行调用 ==========
async def main():
    # 第一次调用:正常
    print("\n========== 第一次调用(正常) ==========")
    result1 = agent.invoke(
        {
            "messages": [HumanMessage(content="北京天气如何?")],
            # 可选:初始状态字段
            "user_id": "user_123",
            "call_count": 0
        },
        context={"user_id": "user_123"}  # context 用于动态提示
    )
    print("最终回答:", result1["messages"][-1].content)

    # 第二次调用:测试缓存(相同消息)
    print("\n========== 第二次调用(测试缓存) ==========")
    result2 = agent.invoke(
        {
            "messages": [HumanMessage(content="北京天气如何?")],
            "user_id": "user_123"
        },
        context={"user_id": "user_123"}
    )
    print("最终回答:", result2["messages"][-1].content)

    # 第三次调用:测试工具错误恢复
    print("\n========== 第三次调用(工具错误恢复) ==========")
    result3 = agent.invoke(
        {
            "messages": [HumanMessage(content="error 天气如何?")],
            "user_id": "user_123"
        },
        context={"user_id": "user_123"}
    )
    print("最终回答:", result3["messages"][-1].content)


# 执行
asyncio.run(main())


#总结!复杂多样的中间件:
"""
中间件的实现方法到底有多少种?头都被搞晕了!!

1.函数装饰器形式 (@before_model, @wrap_tool_call 等) ✓
2.继承基类形式 (继承 AgentMiddleware) ✓
3.框架内置中间件 (如 ToolRetryMiddleware) ✓

两种实现方式 + 一种使用来源(为了方便记忆你也可以认为是一种实现方式!)
├── 方式 1: 函数装饰器 (Function-style Hooks)
│   ├── @before_agent
│   ├── @before_model
│   ├── @wrap_model_call
│   ├── @after_model
│   ├── @wrap_tool_call
│   └── @after_agent
│
├── 方式 2: 类继承 (Class-based Middleware)
│   └── class MyMiddleware(AgentMiddleware):
│       - state_schema
│       - tools
│       - before_model() 方法
│       - after_agent() 方法
│
└── (不准确的说)特殊的方式3:
    来源:框架提供的中间件实例 (开箱即用)
    - ToolRetryMiddleware()     ← 内部也是继承 AgentMiddleware
    - SummarizationMiddleware() ← 内部也是继承 AgentMiddleware
    - ModelFallbackMiddleware() ← 内部也是继承 AgentMiddleware

更加准确来说:
    只有两种语法形式: 函数装饰器 & 类继承
    方式3,是框架中间件是官方封装好的类实例,你直接配置参数即可
    所有中间件最终都被放入 middleware=[...] 列表传给 create_agent()



"""
相关推荐
不解不惑2 小时前
langchain qwen3 构建一个简单的对话系统
pytorch·python·langchain
SQVIoMPLe5 小时前
python-langchain框架(3-7-提取pdf中的图片 )
python·langchain·pdf
@atweiwei5 小时前
用 Rust 构建 LLM 应用的高性能框架
开发语言·后端·ai·rust·langchain·llm
安迪小宝6 小时前
4.2 GIS × LangChain 的完整技术路线
langchain
云和数据.ChenGuang9 小时前
鸿蒙应用对接DeepSeek大模型:构建智能问答系统的技术实践
java·华为·langchain·harmonyos·euler·openduler
laufing20 小时前
RAG 基础版 -- 基于langchain框架
langchain·embedding·rag
AI成长日志1 天前
【GitHub开源项目专栏】深度拆解:LangChain智能体系统架构设计与实现原理
langchain·开源·github
小小小怪兽1 天前
⛏️深入RAG
人工智能·langchain
SQVIoMPLe1 天前
[拆解LangChain执行引擎]以Actor模型的视角来看Pregel
服务器·数据库·langchain