LangChain Tools 进阶:动态工具、Headless 客户端工具、MCP 协议集成与服务端工具

上一篇 tools 的基本定义,上下文访问到错误处理 是基础篇, 这一篇主要讲述动态工具来动态注入一些 tools 给 Agent 使用

本文代码参考

什么是动态工具

Agent 实现可以动态筛选传递给模型开放的 tools,而一次性开放全部定义 tools 给模型。因为并非所有工具都适用于所有场景。工具过多可能让模型负载过高(overload context 上下文过载)并增加出错概率;工具过少则会限制能力。动态工具选择支持根据认证状态、用户权限、功能开关或者对话阶段,对可用工具集进行适配调整。

中间件 hook/装饰器

使用 @wrap_model_call 装饰器可以实现 自动重试机制、故障回退、动态 Prompt 注入与上下文修剪或者是选择性开放 tools 给模型、调用次数限制与审计。

    1. wrap_model_call(中间件钩子 / 装饰器):
    • 作用:这是一个用于包装大模型调用的中间件钩子(Middleware Hook)。通过它,你可以在 Agent 每次准备把请求发给大模型之前、大模型返回结果之后,插入自定义的拦截逻辑。
    • 工作方式:它定义的 function 接收一个 **request(ModelRequest)**和一个 handler 回调。你可以在调用 handler(request) 前后编写任意逻辑(例如:修改输入、实现重试、记录日志、故障回退等)。
    1. ModelRequest(模型请求对象):
    • 作用:封装了每次向大模型发起调用时所包含的所有输入数据
    • 输入的数据包含:
      • 传给模型的消息历史(messages)。
      • 当前的Agent 状态(state)和运行时上下文(runtime / context)。
      • 当前使用的模型实例、系统提示词(System Prompt)等配置。
    1. ModelResponse(模型响应对象):
    • 作用:封装了大模型返回的输出结果。
    • 输出结果包含的主要内容:
      • 模型的生成输出(通常是 AIMessage,包含文本回复或工具调用请求)。
      • 模型的 token 消耗、响应元数据(response_metadata)等
    1. handler(执行器):
    • 主要作用:控制请求是否继续向下执行,以及获取执行结果,比如:
      • 触发真正的模型调用:在中间件里,你可以做各种前置处理(比如打印日志、检查权限、修改 Prompt)。但做完之后,必须通过执行 handler(request) 把请求真正发送给大模型。
      • 控制执行流(甚至可以拦截或替换):
        • 正常放行:调用 response = handler(request),让大模型正常生成回复。
        • 中断或修改:你甚至可以选择不调用 handler(request)(例如触发了某种安全拦截,直接返回一个错误响应),或者在 handler 执行前后去捕获异常、实现重试。
      • 获取响应并进行后处理:当 handler(request) 执行完毕后,它会返回大模型的 response。你可以在它返回给 Agent 之前,对响应内容进行审计、过滤或记录日志。

比如自定义一个日志

python 复制代码
    # 定义一个自定义的模型拦截中间件
    @wrap_model_call
    def log_and_modify_model_call(
        request: ModelRequest, handler: Callable[[ModelRequest], ModelResponse]
    ) -> ModelResponse:
      """拦截大模型调用,打印请求信息,并获取响应"""
      print(
          f"\n[Middleware] 准备请求大模型,当前消息数量: {len(request.messages)}"
      )

      # 你可以在这里修改 request(例如临时追加系统提示词)

      # 执行真正的模型调用并获取响应
      response = handler(request)

      print("[Middleware] 已成功收到大模型的响应!")
      return response


    # 创建带有该中间件的 Agent
    agent = create_agent(
        model=model,  # 假设已初始化好 model
        tools=[],
        middleware=[log_and_modify_model_call],  # 注册模型中间件
        system_prompt=SystemMessage(content="You are a helpful assistant."),
    )

选择需要开放的 tools 暴露给模型

在 agent 创建阶段就已经定义好了所有的 tools,你可以根据状态、权限或上下文动态筛选向模型开放的 tools

展示如何使用 @wrap_model_call 中间件根据当前对话 State(状态) 动态过滤和控制可用工具(Tools),如下定义了两个不同权限级别的工具:一个公开工具(public_get_faq)和一个敏感/高级工具(advanced_search)。中间件会检查状态中的 authenticated 和消息数量,动态决定将哪些工具暴露给大模型

python 复制代码
# 1. 定义自定义状态 (继承 AgentState)
class CustomState(AgentState):
    authenticated: bool

# 2. 定义工具
@tool
def public_get_faq() -> str:
    """获取公开的常见问题解答 (Public FAQ)"""
    return "FAQ: 本系统提供基础问答和高级搜索服务。"

@tool
def advanced_search() -> str:
    """执行高权重的敏感/高级搜索(需要认证且达到一定对话轮次)"""
    return "高级搜索结果:成功查阅到关于的机密数据。"

# 3. 定义基于 State 过滤工具的模型中间件
@wrap_model_call
def state_based_tools(
    request: ModelRequest, handler: Callable[[ModelRequest], ModelResponse]
) -> ModelResponse:
    """根据对话 State 过滤工具"""
    # 从 request.state 中获取状态数据,request.runtime.store,request.runtime.context 也可以在这里获取
    state = request.state
    is_authenticated = state.get("authenticated", False)
    message_count = len(state.get("messages", []))

    print(
        f"\n[Middleware 拦截] 认证状态: {is_authenticated}, 当前消息数: {message_count}"
    )

    # 规则 1:未认证时,只允许使用以 "public_" 开头的工具
    if not is_authenticated:
        tools = [t for t in request.tools if t.name.startswith("public_")]
        print(f"[Middleware] 未认证,已过滤工具,仅保留: {[t.name for t in tools]}")
        request = request.override(tools=tools)

    # 规则 2:已认证但消息数少于 3 条时,暂时禁用高级搜索
    elif message_count < 3:
        tools = [t for t in request.tools if t.name != "advanced_search"]
        print(
            f"[Middleware] 已认证但对话较短,暂时禁用 advanced_search,保留: {[t.name for t in tools]}"
        )
        request = request.override(tools=tools)
    else:
        print("[Middleware] 条件满足,开放所有工具!")

    # 放行,继续执行大模型调用
    return handler(request)

Headless Tools(无头工具 / 客户端工具)

Headless Tools 允许你在服务器端(Server)的 Agent 中注册一个"只有外壳、没有实现"的纯 Schema 工具(只包含名称、描述和参数结构),而该工具的具体执行逻辑(Implementation)则放在客户端(Client,如浏览器、移动端或用户本地环境)。

当大模型决定调用这个工具时,服务端的 Agent 不会直接在本地执行它,而是会触发一个中断(Interrupt),把工具调用请求发送给客户端;客户端在本地环境执行完后,再把结果"恢复(Resume)"传回服务器,继续后续流程。

Headless Tools 主要目的是什么

  • 突破服务端环境限制(利用客户端专属能力): 有些操作必须在用户的本地或浏览器环境中才能运行(例如:调用浏览器的 Geolocation 定位、操作 IndexedDB 本地数据库、读取剪贴板、使用 Canvas 2D 绘图或调起本地文件选择器等)。Headless Tools 让服务端大模型能够"指挥"客户端去完成这些操作。

  • 隐私与数据安全(Data Locality): 敏感数据或本地私密内容无需上传到服务器,可以直接存储和处理在用户的设备本地(例如存放在浏览器的 IndexedDB 中),实现数据不出端。

  • 降低延迟(Reduce Latency): 对于纯粹依赖本地环境的轻量级操作,不需要在服务端和客户端之间来回折腾额外的网络请求,能显著降低响应延迟。

  • 安全且结构化的本地执行(避免危险的远程代码执行): 相比于把一段动态代码(Code / Eval)发送到客户端去执行的安全隐患,Headless Tools 采用的是结构化、强类型约束的小型工具定义(比如专门定义一个画矩形的 Canvas 工具),更加安全可控。

假设你的公司正在开发一款网页端的"智能文档与客户管理系统"(类似 Notion 或 Salesforce 的结合体)。用户在网页上使用 AI 助手来处理业务数据,但是遇到了一些挑战:

  • 环境隔离:大模型运行在后端服务器(Server)上,无法直接调用用户的浏览器 API(例如:读取或写入用户的本地剪贴板、操作浏览器的 IndexedDB 本地缓存、或者唤起本地的文件下载/选择器)。

  • 隐私与安全:用户的某些敏感草稿数据或临时缓存,出于隐私合规考虑,不希望无条件全部上传到后端服务器。

此时实现 Headless Tools(无头工具/客户端工具) 的核心,是利用 LangGraph 的中断与恢复机制(interrupt() 与 Command(resume=...)),当大模型决定调用一个需要在客户端(如浏览器)执行的工具时,服务端的 Python 代码无法直接运行它,因此通过 interrupt() 主动暂停图的执行,将工具调用的参数下发给前端;当前端执行完毕后,再通过 Command(resume=...) 把结果回传给服务端并恢复执行

python 复制代码
from langchain_core.messages import HumanMessage
from langchain_core.tools import tool
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import MessagesState, START
from langgraph.types import Command, interrupt

# 1. 定义无头工具(Headless Tool)
@tool
def copy_to_clipboard(text_content: str) -> str:
    """将文本内容复制到用户的本地剪贴板。"""
    # 服务端无法直接操作浏览器的剪贴板
    # 调用 interrupt() 暂停图的执行,并将客户端需要的动作和参数以 JSON 传输出去
    client_result = interrupt(
        {
            "type": "headless_tool_call",
            "tool_name": "copy_to_clipboard",
            "args": {"text_content": text_content},
        }
    )

    # 当客户端执行完毕并通过 resume 传回结果后,这里会收到该返回值
    return client_result

# 2. 创建带内存检查点的 Agent(支持中断恢复必须配置 checkpointer)
memory = InMemorySaver()
agent = create_agent(
    model=model,  # 假设已初始化好大模型
    tools=[copy_to_clipboard],
    checkpointer=memory,
)

config = {"configurable": {"thread_id": "session_user_123"}}

# ==========================================
# 阶段 1:服务端第一次运行(直到触发中断)
# ==========================================
print("=== 阶段 1:AI 决定调用无头工具 ==px")
events = agent.stream(
    {"messages": [HumanMessage(content="帮我把这段合同摘要复制到剪贴板:谁怕?一蓑烟雨任平生!")]},
    config=config,
    stream_mode="updates",
)

# 捕获图的执行状态,看是否遇到了 interrupt
interrupted_payload = None
for event in events:
    print("执行事件:", event)
    # 如果图因为 interrupt 暂停,可以从状态中提取中断负载
    if "__interrupt__" in event:
        interrupted_payload = event["__interrupt__"]

print("\n🚨 服务端检测到客户端中断请求,准备下发给前端的 Payload:")
print("interrupted_payload::: ", interrupted_payload)


# ==========================================
# 模拟网络传输:将 payload 发给前端浏览器...
# 前端执行: navigator.clipboard.writeText(text_content)
# 模拟前端执行成功后,将结果回传给服务端
# ==========================================
client_execution_result = "Successfully copied to client clipboard."

# ==========================================
# 阶段 2:收到客户端结果,恢复图的执行(Resume)
# ==========================================
print("\n=== 阶段 2:收到客户端执行结果,使用 Command(resume=...) 恢复 Agent 执行 ===")

# 通过 Command(resume=...) 将客户端的执行结果注入进之前中断的地方
resumed_response = agent.invoke(
    Command(resume=client_execution_result),
    config=config,
)

print("最终 AI 回复: ", resumed_response["messages"][-1].content)

预置工具(Prebuilt Tools)

LangChain 提供了大量 预置工具(Prebuilt Tools),涵盖网页搜索、代码解释、数据库查询等常见任务。使用这些现成的工具,你完全不需要自己编写具体的函数实现逻辑,直接拿来注册进 Agent 即可

下面是一个使用 LangChain 官方预置的网页搜索(Web Search)工具,在 LangChain 中通常可以使用 DuckDuckGo(免 API Key、开箱即用,适合测试

python 复制代码
    # uv add duckduckgo-search langchain-community ddgs
    from langchain.agents import create_agent
    from langchain.messages import HumanMessage, SystemMessage

    # 导入 DuckDuckGo 搜索工具
    from langchain_community.tools import DuckDuckGoSearchRun

    # 1. 初始化网页搜索预置工具
    search_tool = DuckDuckGoSearchRun()

    # 2. 将搜索工具直接注册到 Agent 中
    agent = create_agent(
        model=model,
        tools=[search_tool],  # 替换为搜索工具
        system_prompt=SystemMessage(
            content=(
                "You are a helpful research assistant. "
                "Use the search tool to look up factual information when needed."
            )
        ),
    )

    # 3. 运行 Agent
    response = agent.invoke(
        {"messages": [HumanMessage(content="请帮我查一下什么是大语言模型(LLM)?")]}
    )

    print("Agent 最终回复:\n", response["messages"][-1].content)

从 MCP servers 中获取 tool 和使用工具

模型上下文协议(MCP)是一项开放协议,它对应用程序向大语言模型暴露工具的方式进行标准化。你无需手动编写工具,只需连接到一台 MCP 服务器,将它公布的工具适配为 LangChain 工具,就可以像其他工具一样提供给智能代理使用。

MCPAdapter能够发现服务器上的工具,并将其转换为 LangChain 工具。启用适配器,调用 adapter.list_tools(),再把返回结果传给create_agent:

python 复制代码
# 1. 定义本地 MCP 服务端 (2.x 标准写法)
mcp_server = MCPServer("Notebook-Local-MCP-Server")

@mcp_server.tool()
def add_numbers(a: int, b: int) -> int:
  """计算两个整数的和。"""
  return a + b

@mcp_server.tool()
def get_system_status() -> str:
  """获取本地系统的运行状态。"""
  return "Local Notebook 2.x Server Status: Healthy, CPU: 5%, Memory Free: 12.1GB"

# 2. 在后台线程中使用 2.x 内置的 run 方法启动
def run_mcp_server():
  # mcp 2.x 的 MCPServer 提供了直接运行 SSE 的标准方法
  # 注意:如果在同一个进程/线程运行,通常使用 stdio 或正确的 sse 传输配置
  try:
    mcp_server.run(transport="sse", port=8001)
  except Exception as e:
    print(f"MCP Server Error: {e}")

server_thread = threading.Thread(target=run_mcp_server, daemon=True)
server_thread.start()
print("✅ 本地 MCP 2.x 服务端已在后台线程启动 (http://127.0.0.1:8001/sse)")

编写异步客户端与 Agent 测试逻辑

python 复制代码
async def test_notebook_mcp_agent():
  mcp_url = "http://127.0.0.1:8001/sse"
  print(f"\n正在连接本地 MCP 服务: {mcp_url} ...")

  # 使用 MCPAdapter 连接本地服务
  async with MCPAdapter(mcp_url) as adapter:
    # 自动获取服务端提供的工具
    tools = await adapter.list_tools()
    print(f"🎉 成功从本地 MCP 加载工具: {[t.name for t in tools]}")

    # 创建 Agent (确保你前面已初始化好 model 对象)
    agent = create_agent(model=model, tools=tools)

    # 测试任务 1
    print("\n--- 测试任务 1: 加法计算 ---")
    res1 = await agent.ainvoke({
        "messages": [HumanMessage(content="帮我计算一下 789 加 123 等于多少")]
    })
    print("AI 回复:", res1["messages"][-1].content)

    # 测试任务 2
    print("\n--- 测试任务 2: 系统状态查询 ---")
    res2 = await agent.ainvoke({
        "messages": [HumanMessage(content="请帮我检查一下当前系统状态")]
    })
    print("AI 回复:", res2["messages"][-1].content)

# 4. 在 Jupyter 中执行
await test_notebook_mcp_agent()

服务端工具调用(Server-side Tool Use)

服务端工具调用需要注意:

  • 成本与计费风险:通常不是免费的,往往会对每次工具调用(如一次搜索请求)或代码解释器的容器运行时间进行单独计费
  • 数据隐私与合规性:敏感数据不外发
  • 模型绑定与可移植性(服务端工具是各家大模型厂商的"私有能力"): 服务端工具是各家大模型厂商的"私有能力",来想要无缝切换到底层其他开源模型或竞品模型会变得非常困难,迁移成本高。
  • 黑盒控制与定制局限: 工具逻辑完全运行在服务商的黑盒中,你无法修改其内部实现

部分服务商支持服务端工具调用循环:模型可以在单次对话轮次中与网页搜索、代码解释器以及其他工具交互,并对返回结果进行分析。

如果模型在服务端调用工具,响应消息的内容会包含代表工具调用动作与调用结果的信息。读取响应的内容块,将以与服务商无关的统一格式返回服务端工具调用信息和结果:

python 复制代码
tool = {"type": "web_search"}
# 此处使用的模型是 "deepseek-v4.1-flash-1017"
model_with_tools = model.bind_tools([tool])

res = model_with_tools.invoke("今天杭州有什么新闻,并给出相应的简介和链接")
print("res:", res.content_blocks)
相关推荐
LMT流氓兔3 小时前
第五单元——第11课:自动测试“拒绝后绝不能写入”
数据库·人工智能·python·langchain
ratel️3 小时前
小墨析 · Office Agent 日常办公不烧心
python·langchain·word·excel·fastapi·js·ppt
一千柯橘4 小时前
LangChain Tools:从基础定义、上下文访问到错误处理
langchain
LMT流氓兔5 小时前
第五单元——第10课:用自动化测试验证工具逻辑
数据库·ai·oracle·langchain
云和数据.ChenGuang7 小时前
Python 集合(set)常用函数详解与实战案例
开发语言·python·深度学习·langchain
吴凡wudianmei1 天前
线上出问题不知道卡在哪?全链路追踪 + 三端统一部署
架构·langchain·llm
DGT1 天前
不用 LangChain,200 行 TypeScript 手搓一个能联网、能写文件的 Agent
前端·langchain·agent
梦影_1 天前
Langchain简单快速上手教程(九)—— 核心组件之少样本提示
人工智能·python·自然语言处理·中间件·langchain
BD_Marathon1 天前
【RAG开发】CSVLoader
langchain