本文对应参考文档:代理https://docs.langchain.org.cn/oss/python/langchain/agents#structured-output详细教程请参照上述链接。下述内容仅为个人理解,无法保证即时性。

前述章节

LangChain 学习与拆解(1)Agent 最小系统 | 此章节介绍了Agent最小系统包括:大模型、系统提示词、工具集、记忆机制。本章节是介绍在最小系统基础上,引入大模型、系统提示词、工具集的动态配置。

最小系统的问题

最小系统是静态的,模型配置、系统提示词、工具集等相关配置在定义 Agent 时就已经固化。因此,它无法在运行时根据任务需求或环境变化,对模型、工具等核心要素进行动态调整。

本文讲述的动态系统就是实现对模型配置系统提示词工具集3部分的动态配置,即实现在Agent运行时,动态更换调用模型、工具、系统提示词。

动态系统基础

Agent invoke调用过程——React循环

上图为Agent调用过程的基本循环,官方称为React循环。

在将用户指令通过invoke传递给Agent后,Agent执行如下步骤:

  1. 读取模型配置、工具集、上下文(包括系统提示词、长期记忆、短期记忆、状态等)和用户指令一起组织为ModelRequest
  2. 调用大模型进行推理,大模型推理内容一般包括文本回答、工具选择两部分
  3. 接收ModelResponse,解析是否存在ToolCallRequest(包括模型选择执行的工具、工具参数、id等)
  4. 解析ToolCallRequest
    1. 不存在ToolCallRequest,则输出大模型的文本回复
    2. 存在ToolCallRequest,则通过执行器执行选择的工具,并将执行结果添加到上下文中重新组织ModelRequest,调用大模型,重复步骤前述步骤

Agent invoke调用的钩子(Hooks

钩子(Hooks)就是在上述调用过程(React循环)的特定节点增加方法,以支持自定义处理(即中间件)。

以下是LangChain提供的钩子:

钩子方法 (Hook) 触发时机 典型应用场景
before_agent Agent 整体开始执行前 验证用户输入、加载长期记忆、初始化上下文
before_model 每次调用大模型(LLM)前 动态修改提示词(Prompt)、裁剪过长的历史消息
wrap_model_call 包裹每次大模型调用 动态切换模型、动态修改工具清单、请求/响应拦截
wrap_tool_call 包裹每次工具调用 动态绑定工具、工具权限检查、工具调用重试
after_model 每次大模型返回响应后 检查模型输出是否包含敏感词、应用安全护栏(Guardrails)
after_agent Agent 整体执行结束后 保存对话结果、清理临时资源、记录最终日志

wrap_model_call、wrap_tool_call称为包装式钩子,其他4个钩子称为节点式钩子

节点式钩子特点

before_agent、after_agent、before_model、after_model为节点式钩子,具有如下特点:

  1. 查看、修改数据,可以访问修改state(跨次全局共享)、context(模型单次调用),无法直接修改ModelRequest、ModelResponse,仅能通过修改state,间接调整ModelRequest部分属性
  2. 无法阻断流程,执行完自定义方法后,就进入正常流程,如想阻断只能通过抛出错误的方式阻断。

包装式钩子特点(动态系统实现基础)

wrap_model_call、wrap_tool_call为包装式钩子,具有如下特点:

  1. 可以直接访问修改state、context、ModelRequest、ModelResponse
  2. 拦截流程,选择放行、阻断或者其他特殊处理。

动态系统

动态系统的实现均是通过包装式钩子实现的。包装式钩子的使用方法包括两步:

  1. 定义钩子处理方法
    1. 通过@wrap_model_call、@wrap_tool_call实现
      from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse
      from typing import Callable
      
      # 如果底层调用的是异步大模型,记得加上 async 和 await
      @wrap_model_call
      async def my_wrap_model_middleware(
          request: ModelRequest, 
          handler: Callable[[ModelRequest], ModelResponse]
      ) -> ModelResponse:
          """自定义包装式钩子"""
          
          # --- 【放行前】处理 ---
          # 1. 读取或修改请求参数 (如 request.model, request.messages)
          # 2. 获取上下文信息 (request.state, request.runtime.context)
          print(f"准备调用模型,当前消息数: {len(request.state['messages'])}")
          
          # 3. 【核心控制】决定是否放行
          # 场景A:直接短路(例如命中缓存),不调用 handler,直接返回伪造的响应
          # if cache_hit:
          #     return cached_response 
          
          # 场景B:正常放行,将请求传递给下一个环节
          try:
              response = await handler(request)
          except Exception as e:
              # --- 【异常处理】 ---
              # 可以在这里捕获模型调用的报错,进行重试或返回兜底回复
              print(f"模型调用失败: {e}")
              raise e 
      
          # --- 【放行后】处理 ---
          # 修改模型的返回结果 (response)
          # 记录耗时、Token消耗等
          
          return response
    2. 通过继承AgentMiddleware类实现
      from langchain.agents.middleware import AgentMiddleware, ModelRequest, ModelResponse
      from typing import Callable
      
      class MyCustomMiddleware(AgentMiddleware):
          """自定义中间件类"""
          
          # 可以在这里定义一些中间件专属的配置或状态
          # state_schema = ...
          
          def wrap_model_call(
              self,
              request: ModelRequest,
              handler: Callable[[ModelRequest], ModelResponse]
          ) -> ModelResponse:
              """重写包装模型调用的方法"""
              
              # --- 【放行前】处理 ---
              # 同样可以访问 self 来获取类的内部属性
              print("通过类中间件拦截到了模型请求")
              
              # 执行实际的模型调用(如果是异步环境,这里同样需要配合 async/await)
              response = handler(request)
              
              # --- 【放行后】处理 ---
              # 对响应结果进行二次加工
              
              return response
      
      # 💡 使用时,直接将这个类实例化放入 create_agent 的 middleware 列表中即可
      # agent = create_agent(model=..., tools=[...], middleware=[MyCustomMiddleware()])
  2. 定义Agent时,将定义的方法作为中间件传入
    agent = create_agent(
        middleware=[my_wrap_model_middleware(),MyCustomMiddleware()],
    )

动态模型

与动态模型相对的静态模型是指:在最小系统中,模型的配置是在create_agent(model=model)时配置的,model的参数是固化的,只能访问单个模型,无法根据不同条件实现对不同模型的访问。

动态模型是通过wrap_model_call,修改Modelrequest,根据不同条件实现对不同模型的访问。一般使用场景是针对上下文大小、任务类型等实现对不同能力模型的动态访问,节约成本。

官方示例
from langchain_openai import ChatOpenAI
from langchain.agents import create_agent
from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse


basic_model = ChatOpenAI(model="gpt-5.4-mini")
advanced_model = ChatOpenAI(model="gpt-5.4")

@wrap_model_call
def dynamic_model_selection(request: ModelRequest, handler) -> ModelResponse:
    """Choose model based on conversation complexity."""
    message_count = len(request.state["messages"])

    if message_count > 10:
        # Use an advanced model for longer conversations
        model = advanced_model
    else:
        model = basic_model

    return handler(request.override(model=model))

agent = create_agent(
    model=basic_model,  # Default model
    tools=tools,
    middleware=[dynamic_model_selection]
)
  • 通过@wrap_model_call定义的中间件的入参为ModelRequest、Handler,返回值为ModelResponse
  • ModelRequest、ModelResponse无法直接修改,但是可以通过override方法生成新的ModelRequest、ModelResponse
  • handler功能是放行,入参是ModelRequest,返回值是ModelResponse

执行逻辑

动态模型实现包括如下部分:

  • 多模型定义
  • Agent创建指定默认模型
  • 中间件定义
    • 模型选用判断条件
    • 新ModelRequest生成
    • handler放行

动态工具

动态工具是对工具集的动态实现,与动态模型实现大致相同,也是通过wrap_model_call钩子定义中间件,实现对ModelRequest中Tools的替换。

与动态模型不同的是,ModelRequest中Tools是供模型选择的文本,不包括函数实例。通过ModelRequest中Tools替换,可以用来做已注册工具集的减法,增加新工具需要实现对函数实例的调用。

已注册工具集减法

已注册工具集减法与动态模型实现方式一致,不再赘述。主要使用场景是做权限区分,或者避免工具干扰。

官方示例
from langchain.agents import create_agent
from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse
from typing import Callable

@wrap_model_call
def state_based_tools(
    request: ModelRequest,
    handler: Callable[[ModelRequest], ModelResponse]
) -> ModelResponse:
    """Filter tools based on conversation State."""
    # Read from State: check if user has authenticated
    state = request.state
    is_authenticated = state.get("authenticated", False)
    message_count = len(state["messages"])

    # Only enable sensitive tools after authentication
    if not is_authenticated:
        tools = [t for t in request.tools if t.name.startswith("public_")]
        request = request.override(tools=tools)
    elif message_count < 5:
        # Limit tools early in conversation
        tools = [t for t in request.tools if t.name != "advanced_search"]
        request = request.override(tools=tools)

    return handler(request)

agent = create_agent(
    model="gpt-5.4",
    tools=[public_search, private_search, advanced_search],
    middleware=[state_based_tools]
)

新工具加法

新工具的加法关键在于实现对新工具的调用,包括两步:

  1. 通过wrap_model_call定义中间件,实现ModelRequest中的tools替换;
  2. 通过wrap_tool_call定义中间件,实现将新工具添加到ToolCallRequest.tool中。
工具调用逻辑

调用Agent时,Agent会将定义时注册的tools添加到ModelRequest的tools中,tools中的每个tool都包含名称、描述、参数规范。

模型会根据用户指令和工具的描述,选择一个工具,并生成参数,在存储在响应内容的tool_call中。

接收到模型响应后,Agent会在根据模型选择的工具名称,在工具集中加载函数实例,生成ToolCallRequest,由执行器执行。

通过wrap_model_call定义中间件添加tool,只能让大模型能够选择到工具。在组织ToolCallRequest是,无法找到工具实例,所以ToolCallRequest的tool为none,无法执行。所以需要通过wrap_tool_call定义中间件,将新工具添加到ToolCallRequest.tool中。

动态工具逻辑

官方示例
from langchain.tools import tool
from langchain.agents import create_agent
from langchain.agents.middleware import AgentMiddleware, ModelRequest, ToolCallRequest

# A tool that will be added dynamically at runtime
@tool
def calculate_tip(bill_amount: float, tip_percentage: float = 20.0) -> str:
    """Calculate the tip amount for a bill."""
    tip = bill_amount * (tip_percentage / 100)
    return f"Tip: ${tip:.2f}, Total: ${bill_amount + tip:.2f}"

class DynamicToolMiddleware(AgentMiddleware):
    """Middleware that registers and handles dynamic tools."""

    def wrap_model_call(self, request: ModelRequest, handler):
        # Add dynamic tool to the request
        # This could be loaded from an MCP server, database, etc.
        updated = request.override(tools=[*request.tools, calculate_tip])
        return handler(updated)

    def wrap_tool_call(self, request: ToolCallRequest, handler):
        # Handle execution of the dynamic tool
        if request.tool_call["name"] == "calculate_tip":
            return handler(request.override(tool=calculate_tip))
        return handler(request)

agent = create_agent(
    model="gpt-4o",
    tools=[get_weather],  # Only static tools registered here
    middleware=[DynamicToolMiddleware()],
)

# The agent can now use both get_weather AND calculate_tip
result = agent.invoke({
    "messages": [{"role": "user", "content": "Calculate a 20% tip on $85"}]
})

动态系统提示词

动态系统提示词实现逻辑与动态模型相同,只是官方提供了@dynamic_prompt,中间件直接返回提示词即可,无需再调用override生成新的ModelRequest,也无需调用handler放行。具体逻辑不再赘述。以下为官方示例:

from typing import TypedDict

from langchain.agents import create_agent
from langchain.agents.middleware import dynamic_prompt, ModelRequest


class Context(TypedDict):
    user_role: str

@dynamic_prompt
def user_role_prompt(request: ModelRequest) -> str:
    """Generate system prompt based on user role."""
    user_role = request.runtime.context.get("user_role", "user")
    base_prompt = "You are a helpful assistant."

    if user_role == "expert":
        return f"{base_prompt} Provide detailed technical responses."
    elif user_role == "beginner":
        return f"{base_prompt} Explain concepts simply and avoid jargon."

    return base_prompt

agent = create_agent(
    model="gpt-5.4",
    tools=[web_search],
    middleware=[user_role_prompt],
    context_schema=Context
)

# The system prompt will be set dynamically based on context
result = agent.invoke(
    {"messages": [{"role": "user", "content": "Explain machine learning"}]},
    context={"user_role": "expert"}
)

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐