从函数到工具,LangChain Tools 与 Tool Calling 完整入门

很多人第一次看到大模型调用天气查询、数据库搜索或计算器时,会产生一种错觉,模型好像突然获得了执行代码的能力。

其实不是。

模型只是在对话中生成了一条结构化的「调用请求」,真正执行函数、访问网络、读取数据库的人,仍然是你的应用程序。LangChain Tools 的价值,就是把普通 Python 函数包装成模型能够理解的工具契约,再把模型的调用请求接回运行时,形成一条可观察、可控制的执行链路。

这件事听起来简单,但生产系统中最常见的故障,恰恰发生在边界上。有人把工具调用请求当成工具执行结果,有人漏掉了 ToolMessage,有人让模型一次拿到几十个权限过大的工具,最后才发现 Agent 的行为无法解释。

本文从一个完整的消息循环开始,带你理解 Tool Calling 的数据结构、LangChain 的核心 API、多工具调用和 tool_choice,最后再讨论重试、权限、幂等和人工确认。示例可以独立运行,工具使用内存中的演示数据,不依赖真实天气或搜索服务。

本文解决什么,理解工具描述、tool_callsToolMessage 与模型循环之间的关系,并能写出一个有轮数上限的基础工具调用程序。

本文不展开什么,不展开 Agent 的状态图编排、LangSmith 的观测配置和具体业务系统的鉴权实现,这些内容需要结合自己的运行时设计。

读完能完成什么,读者可以注册两个演示工具,验证模型是否提出调用请求,执行工具并把结果送回模型,同时知道哪些地方必须由应用侧兜底。

除非段落明确写出「完整示例」,其余代码块都是解释某个 API 的局部片段,依赖条件会在片段前说明。

先回答一个关键问题,工具到底是什么

在 LangChain 中,工具是一个拥有明确输入和输出契约的可调用函数。它通常包含四类信息。

信息 作用 示例
名称 让模型在多个工具中选择目标 get_weather
描述 解释何时使用、能解决什么问题 查询指定城市的当前天气
参数模式 规定参数名、类型、是否必填 city: str
执行函数 在应用侧完成真实动作 查询缓存或调用 HTTP API

普通函数只需要让 Python 调用者看懂。工具还要让模型看懂,所以描述和参数模式不是装饰品,而是模型决策的一部分。

工具也不是数据库、搜索引擎或支付系统本身。它只是一个受控入口。真正的外部副作用发生在函数体里,应该由应用代码负责鉴权、超时、审计和错误转换。

Tool Calling 的完整消息回路

一次典型的工具调用至少包含四个阶段。

  1. 用户发送自然语言问题。
  2. 模型返回 AIMessage,其中的 tool_calls 描述工具名称和参数。
  3. 应用根据 tool_calls 找到对应函数并执行,生成 ToolMessage
  4. 应用把完整消息列表再次交给模型,模型根据工具结果生成最终回答。

可以把它画成一条消息序列。注意,图里的「执行工具」不是模型内部行为,而是你的 Python 进程或后端服务在做的事。
工具函数 聊天模型 应用运行时 用户 工具函数 聊天模型 应用运行时 用户 #mermaid-svg-9qdw81WDIWUunSam{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-9qdw81WDIWUunSam .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-9qdw81WDIWUunSam .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-9qdw81WDIWUunSam .error-icon{fill:#552222;}#mermaid-svg-9qdw81WDIWUunSam .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-9qdw81WDIWUunSam .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-9qdw81WDIWUunSam .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9qdw81WDIWUunSam .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9qdw81WDIWUunSam .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-9qdw81WDIWUunSam .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9qdw81WDIWUunSam .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9qdw81WDIWUunSam .marker{fill:#333333;stroke:#333333;}#mermaid-svg-9qdw81WDIWUunSam .marker.cross{stroke:#333333;}#mermaid-svg-9qdw81WDIWUunSam svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9qdw81WDIWUunSam p{margin:0;}#mermaid-svg-9qdw81WDIWUunSam .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-9qdw81WDIWUunSam text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-9qdw81WDIWUunSam .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-9qdw81WDIWUunSam .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-9qdw81WDIWUunSam .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-9qdw81WDIWUunSam .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-9qdw81WDIWUunSam #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-9qdw81WDIWUunSam .sequenceNumber{fill:white;}#mermaid-svg-9qdw81WDIWUunSam #sequencenumber{fill:#333;}#mermaid-svg-9qdw81WDIWUunSam #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-9qdw81WDIWUunSam .messageText{fill:#333;stroke:none;}#mermaid-svg-9qdw81WDIWUunSam .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-9qdw81WDIWUunSam .labelText,#mermaid-svg-9qdw81WDIWUunSam .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-9qdw81WDIWUunSam .loopText,#mermaid-svg-9qdw81WDIWUunSam .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-9qdw81WDIWUunSam .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-9qdw81WDIWUunSam .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-9qdw81WDIWUunSam .noteText,#mermaid-svg-9qdw81WDIWUunSam .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-9qdw81WDIWUunSam .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-9qdw81WDIWUunSam .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-9qdw81WDIWUunSam .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-9qdw81WDIWUunSam .actorPopupMenu{position:absolute;}#mermaid-svg-9qdw81WDIWUunSam .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-9qdw81WDIWUunSam .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-9qdw81WDIWUunSam .actor-man circle,#mermaid-svg-9qdw81WDIWUunSam line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-9qdw81WDIWUunSam :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 发送问题 HumanMessage AIMessage(tool_calls) 按名称和参数调用函数 返回结果或错误 Human + AI(tool_calls) + ToolMessage 最终 AIMessage 展示自然语言答案

如果第三步没有发生,模型不会凭空知道工具结果。如果第四步没有发生,用户通常只能看到一条空内容的 AIMessage 或「我已经查询」之类的半成品。

版本、安装与环境变量

本文按 LangChain 1.x 的统一 API 编写。LangChain 仍在快速迭代,模型供应商对工具调用能力和参数格式的支持也不完全相同。发布或上线前,应将依赖固定在经过测试的版本,并重新阅读对应集成包的变更记录。

安装最小依赖。

shell 复制代码
pip install -U "langchain>=1.0,<2" "langchain-openai>=1.0,<2" python-dotenv

在运行环境中配置密钥。示例使用 OpenAI 兼容模型,模型名可以通过环境变量替换。

dotenv 复制代码
OPENAI_API_KEY=replace_with_your_key
MODEL_NAME=gpt-4o-mini

不要把密钥写进代码、工具参数、Trace metadata 或错误日志。若使用其他供应商,请安装对应的 LangChain 集成包,并按照官方文档修改模型初始化方式。

一个可以跑通的最小示例

下面的脚本实现了一个小型城市助手。它拥有天气查询和城市时区两个工具,工具返回的是固定演示数据,因此读者不需要先申请第三方天气 API。模型仍然负责判断是否调用工具、调用哪个工具和传递什么参数。

python 复制代码
import os
from typing import Any

from dotenv import load_dotenv
from langchain.chat_models import init_chat_model
from langchain.messages import HumanMessage, ToolMessage
from langchain.tools import tool


load_dotenv()


@tool
def get_weather(city: str) -> str:
    """查询演示天气数据。

    Args:
        city: 城市名称,例如北京、上海或杭州。
    """
    weather = {
        "北京": "晴,22 摄氏度,西北风 2 级",
        "上海": "多云,25 摄氏度,东南风 3 级",
        "杭州": "小雨,21 摄氏度,东风 2 级",
    }
    return weather.get(city, f"暂时没有{city}的演示天气数据")


@tool
def get_timezone(city: str) -> str:
    """查询城市所属时区。

    Args:
        city: 城市名称,例如北京、上海或纽约。
    """
    zones = {
        "北京": "Asia/Shanghai,UTC+8",
        "上海": "Asia/Shanghai,UTC+8",
        "杭州": "Asia/Shanghai,UTC+8",
        "纽约": "America/New_York,通常为 UTC-5 或 UTC-4",
    }
    return zones.get(city, f"暂时没有{city}的演示时区数据")


def build_model():
    """按环境变量创建聊天模型,避免把模型实例藏在调用函数的局部作用域。"""
    model_name = os.getenv("MODEL_NAME", "gpt-4o-mini")
    return init_chat_model(f"openai:{model_name}", temperature=0)


def run_assistant(question: str) -> str:
    """运行一次受控的工具调用循环。

    模型每轮最多提出一次或多次调用,应用执行后把 ToolMessage 放回消息列表。
    max_rounds 防止异常模型或异常工具结果导致无限循环。
    """
    model = build_model()
    tools = [get_weather, get_timezone]
    tool_map = {item.name: item for item in tools}
    model_with_tools = model.bind_tools(tools)

    messages: list[Any] = [HumanMessage(content=question)]
    max_rounds = 4

    for _ in range(max_rounds):
        response = model_with_tools.invoke(messages)
        messages.append(response)

        if not response.tool_calls:
            return str(response.content)

        for call in response.tool_calls:
            tool_name = call.get("name")
            tool = tool_map.get(tool_name)
            if tool is None:
                # 未知工具不能静默忽略,否则模型会一直等待一个不存在的结果。
                messages.append(
                    ToolMessage(
                        content=f"工具{tool_name}不存在,无法执行。",
                        tool_call_id=call["id"],
                        name=tool_name,
                    )
                )
                continue

            try:
                # 传入完整 tool call,LangChain 会保留 tool_call_id 并生成 ToolMessage。
                tool_message = tool.invoke(call)
            except Exception as exc:
                # 工具异常应转换成模型可读的结果,同时由日志系统记录原始异常。
                tool_message = ToolMessage(
                    content=f"工具执行失败,请基于失败信息重新判断。错误类型:{type(exc).__name__}",
                    tool_call_id=call["id"],
                    name=tool_name,
                )
            messages.append(tool_message)

    return "工具调用轮数超过上限,已停止本次请求。"


if __name__ == "__main__":
    print(run_assistant("请告诉我北京的天气和时区。"))

这段代码里有几个值得停下来看的细节。

@tool 把函数注册成 BaseTool 对象。函数名、docstring 和类型注解会参与生成工具 Schema。bind_tools 不会执行函数,它只会把工具描述发送给模型,并返回一个绑定了工具能力的可运行对象。

模型返回的 AIMessage 可能没有文本内容,但有 tool_calls。每一项通常至少包含 nameargsidid 用于把后续的 ToolMessage 和原始请求对应起来,不能随意丢弃或重新生成。

tool.invoke(call) 才是应用侧执行函数的动作。函数返回值会被包装成 ToolMessage,然后和原始 AIMessage 一起再次传给模型。模型看到完整上下文后,才有机会把「天气数据」翻译成面向用户的回答。

直接调用工具和让模型选择工具

本节的代码是局部片段,依赖上方完整脚本已经定义的 get_weatherget_timezonebuild_model。需要直接运行某个片段时,先执行完整脚本中的定义,再在片段开头写 model = build_model();模型实例不再依赖某个函数的局部变量。

工具对象仍然可以像普通函数一样直接调用。

python 复制代码
print(get_weather.invoke({"city": "北京"}))

这种方式适合单元测试、后台定时任务或开发阶段验证函数本身。它不涉及模型,也没有自然语言理解成本。

开发中的主流方式是绑定工具。

python 复制代码
model = build_model()
model_with_tools = model.bind_tools([get_weather, get_timezone])
response = model_with_tools.invoke("北京今天适合出门吗?")
print(response.tool_calls)

这里的返回值仍然是 AIMessage。是否真的调用了工具,要看 response.tool_calls。不要因为绑定了工具,就假设每次请求都会执行工具。

模型选择工具大致经过这样的判断。
#mermaid-svg-dmm4ziYEanloqiBD{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-dmm4ziYEanloqiBD .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dmm4ziYEanloqiBD .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dmm4ziYEanloqiBD .error-icon{fill:#552222;}#mermaid-svg-dmm4ziYEanloqiBD .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dmm4ziYEanloqiBD .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dmm4ziYEanloqiBD .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dmm4ziYEanloqiBD .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dmm4ziYEanloqiBD .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dmm4ziYEanloqiBD .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dmm4ziYEanloqiBD .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dmm4ziYEanloqiBD .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dmm4ziYEanloqiBD .marker.cross{stroke:#333333;}#mermaid-svg-dmm4ziYEanloqiBD svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dmm4ziYEanloqiBD p{margin:0;}#mermaid-svg-dmm4ziYEanloqiBD .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-dmm4ziYEanloqiBD .cluster-label text{fill:#333;}#mermaid-svg-dmm4ziYEanloqiBD .cluster-label span{color:#333;}#mermaid-svg-dmm4ziYEanloqiBD .cluster-label span p{background-color:transparent;}#mermaid-svg-dmm4ziYEanloqiBD .label text,#mermaid-svg-dmm4ziYEanloqiBD span{fill:#333;color:#333;}#mermaid-svg-dmm4ziYEanloqiBD .node rect,#mermaid-svg-dmm4ziYEanloqiBD .node circle,#mermaid-svg-dmm4ziYEanloqiBD .node ellipse,#mermaid-svg-dmm4ziYEanloqiBD .node polygon,#mermaid-svg-dmm4ziYEanloqiBD .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-dmm4ziYEanloqiBD .rough-node .label text,#mermaid-svg-dmm4ziYEanloqiBD .node .label text,#mermaid-svg-dmm4ziYEanloqiBD .image-shape .label,#mermaid-svg-dmm4ziYEanloqiBD .icon-shape .label{text-anchor:middle;}#mermaid-svg-dmm4ziYEanloqiBD .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-dmm4ziYEanloqiBD .rough-node .label,#mermaid-svg-dmm4ziYEanloqiBD .node .label,#mermaid-svg-dmm4ziYEanloqiBD .image-shape .label,#mermaid-svg-dmm4ziYEanloqiBD .icon-shape .label{text-align:center;}#mermaid-svg-dmm4ziYEanloqiBD .node.clickable{cursor:pointer;}#mermaid-svg-dmm4ziYEanloqiBD .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-dmm4ziYEanloqiBD .arrowheadPath{fill:#333333;}#mermaid-svg-dmm4ziYEanloqiBD .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-dmm4ziYEanloqiBD .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-dmm4ziYEanloqiBD .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dmm4ziYEanloqiBD .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-dmm4ziYEanloqiBD .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dmm4ziYEanloqiBD .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-dmm4ziYEanloqiBD .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-dmm4ziYEanloqiBD .cluster text{fill:#333;}#mermaid-svg-dmm4ziYEanloqiBD .cluster span{color:#333;}#mermaid-svg-dmm4ziYEanloqiBD div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-dmm4ziYEanloqiBD .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-dmm4ziYEanloqiBD rect.text{fill:none;stroke-width:0;}#mermaid-svg-dmm4ziYEanloqiBD .icon-shape,#mermaid-svg-dmm4ziYEanloqiBD .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dmm4ziYEanloqiBD .icon-shape p,#mermaid-svg-dmm4ziYEanloqiBD .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-dmm4ziYEanloqiBD .icon-shape .label rect,#mermaid-svg-dmm4ziYEanloqiBD .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dmm4ziYEanloqiBD .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-dmm4ziYEanloqiBD .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-dmm4ziYEanloqiBD :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否

用户问题
模型读取消息与工具 Schema
是否需要外部信息或动作
直接生成 AIMessage 文本
选择工具与参数
返回 tool_calls
运行时校验并执行
ToolMessage 结果
模型生成最终回答

多工具调用不是多了一行代码

当问题同时涉及天气和时区时,模型可能在同一个 AIMessage 中返回多个调用。应用需要遍历 response.tool_calls,逐个执行,并按原顺序把结果加入消息列表。

如果工具之间互不依赖,可以在运行时并发执行来降低延迟,但并发会带来速率限制、结果排序、取消和异常聚合等问题。初学阶段先使用串行循环,确认消息协议正确后再引入异步并发。

下面是多工具循环的核心形态。

python 复制代码
response = model_with_tools.invoke(messages)
messages.append(response)

for call in response.tool_calls:
    selected = tool_map[call["name"]]
    messages.append(selected.invoke(call))

final_response = model_with_tools.invoke(messages)

上面仍是循环的核心片段,messages 应包含 HumanMessagetool_map 应由已注册工具按名称建立。不要把它当成脱离上下文的完整脚本;完整的未知工具、异常转换和轮数限制已经包含在本文前面的最小示例中。

一轮工具调用结束后,如果最终 AIMessage 仍然包含 tool_calls,就继续循环。生产代码必须设置轮数、时间或预算上限,不能把「直到模型满意」当成可靠的停止条件。

用 tool_choice 约束模型行为

bind_tools 支持 tool_choice,常见取值如下。

取值 行为 适用情况
auto 模型自行决定是否调用工具 通用问答,默认选择
none 禁止调用工具 只允许知识解释或离线模式
required 必须调用至少一个工具 必须经过查询或分类的流程
指定工具 强制调用某一个工具 已由业务路由确定工具

不同模型供应商对 required、并行调用和指定工具的支持细节可能不同,应该以对应集成包文档为准。

python 复制代码
model = build_model()

# 只允许模型回答,不允许访问天气工具
text_only_model = model.bind_tools(tools, tool_choice="none")

# 强制至少调用一个工具,适合必须先经过外部查询的流程
query_model = model.bind_tools(tools, tool_choice="required")

# 具体格式由供应商决定,常见形式是指定函数名称
weather_only_model = model.bind_tools(
    tools,
    tool_choice={"type": "function", "function": {"name": "get_weather"}},
)

强制工具并不等于工具结果一定可信。工具函数仍然可能超时、返回空数据或被错误参数触发。tool_choice 只解决「要不要提出调用请求」,不能替代权限和业务校验。

常见错误,按消息协议排查

只调用一次模型就返回给用户

症状是输出为空,或者回答「我会帮你查询」却没有具体结果。

原因是代码拿到 AIMessage(tool_calls) 后直接结束,没有执行工具,也没有把 ToolMessage 发回模型。修复方法是实现完整循环,并在没有 tool_calls 时才把文本交给用户。

手工拼接 ToolMessage 时丢失 call id

每个 ToolMessage 都应该使用对应调用的 tool_call_id。如果 id 错位,部分供应商会拒绝请求,或者模型无法把结果和工具请求配对。

docstring 和类型注解不完整

没有参数类型时,Schema 缺少有用的类型约束。使用 parse_docstring=True 却没有按支持的 Google 风格写 Args,可能在创建工具时直接抛出解析异常。工具描述应说明数据范围、单位、何时使用和失败时的返回语义。

把模型的工具调用当成安全授权

模型说要调用 delete_order,不代表用户已经授权删除订单。高风险工具必须在执行前再次检查当前用户、资源归属、审批状态和幂等键。模型输出只能是意图,不是权限令牌。

工具返回复杂对象导致上下文膨胀

工具返回值最终会进入下一次模型上下文。不要把数据库整表、完整 HTML 或原始堆栈直接返回。优先返回稳定、精简、可解释的字符串或小型 JSON,并在服务端保留详细日志。

生产环境的工具边界

工具系统真正难的地方不在装饰器,而在执行边界。建议把一次调用拆成四层。
#mermaid-svg-Itys6EavzOl5bZeS{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Itys6EavzOl5bZeS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Itys6EavzOl5bZeS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Itys6EavzOl5bZeS .error-icon{fill:#552222;}#mermaid-svg-Itys6EavzOl5bZeS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Itys6EavzOl5bZeS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Itys6EavzOl5bZeS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Itys6EavzOl5bZeS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Itys6EavzOl5bZeS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Itys6EavzOl5bZeS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Itys6EavzOl5bZeS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Itys6EavzOl5bZeS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Itys6EavzOl5bZeS .marker.cross{stroke:#333333;}#mermaid-svg-Itys6EavzOl5bZeS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Itys6EavzOl5bZeS p{margin:0;}#mermaid-svg-Itys6EavzOl5bZeS .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Itys6EavzOl5bZeS .cluster-label text{fill:#333;}#mermaid-svg-Itys6EavzOl5bZeS .cluster-label span{color:#333;}#mermaid-svg-Itys6EavzOl5bZeS .cluster-label span p{background-color:transparent;}#mermaid-svg-Itys6EavzOl5bZeS .label text,#mermaid-svg-Itys6EavzOl5bZeS span{fill:#333;color:#333;}#mermaid-svg-Itys6EavzOl5bZeS .node rect,#mermaid-svg-Itys6EavzOl5bZeS .node circle,#mermaid-svg-Itys6EavzOl5bZeS .node ellipse,#mermaid-svg-Itys6EavzOl5bZeS .node polygon,#mermaid-svg-Itys6EavzOl5bZeS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Itys6EavzOl5bZeS .rough-node .label text,#mermaid-svg-Itys6EavzOl5bZeS .node .label text,#mermaid-svg-Itys6EavzOl5bZeS .image-shape .label,#mermaid-svg-Itys6EavzOl5bZeS .icon-shape .label{text-anchor:middle;}#mermaid-svg-Itys6EavzOl5bZeS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Itys6EavzOl5bZeS .rough-node .label,#mermaid-svg-Itys6EavzOl5bZeS .node .label,#mermaid-svg-Itys6EavzOl5bZeS .image-shape .label,#mermaid-svg-Itys6EavzOl5bZeS .icon-shape .label{text-align:center;}#mermaid-svg-Itys6EavzOl5bZeS .node.clickable{cursor:pointer;}#mermaid-svg-Itys6EavzOl5bZeS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Itys6EavzOl5bZeS .arrowheadPath{fill:#333333;}#mermaid-svg-Itys6EavzOl5bZeS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Itys6EavzOl5bZeS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Itys6EavzOl5bZeS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Itys6EavzOl5bZeS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Itys6EavzOl5bZeS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Itys6EavzOl5bZeS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Itys6EavzOl5bZeS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Itys6EavzOl5bZeS .cluster text{fill:#333;}#mermaid-svg-Itys6EavzOl5bZeS .cluster span{color:#333;}#mermaid-svg-Itys6EavzOl5bZeS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Itys6EavzOl5bZeS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Itys6EavzOl5bZeS rect.text{fill:none;stroke-width:0;}#mermaid-svg-Itys6EavzOl5bZeS .icon-shape,#mermaid-svg-Itys6EavzOl5bZeS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Itys6EavzOl5bZeS .icon-shape p,#mermaid-svg-Itys6EavzOl5bZeS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Itys6EavzOl5bZeS .icon-shape .label rect,#mermaid-svg-Itys6EavzOl5bZeS .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Itys6EavzOl5bZeS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Itys6EavzOl5bZeS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Itys6EavzOl5bZeS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 模型提出意图
参数 Schema 校验
权限与业务规则
执行外部副作用
审计、指标、结果裁剪

参数 Schema 负责类型和范围,例如城市不能为空、金额必须大于零。权限层负责当前用户是否能访问订单或发送邮件。执行层负责超时、重试、连接池和事务。审计层记录调用名、耗时、结果状态和追踪 ID,但不记录密钥和未经脱敏的敏感数据。

涉及付款、删除、发信、改库或发布内容的工具,最好采用「模型提出请求,应用展示摘要,用户确认,服务执行」的两阶段模式。对于会重试的网络操作,工具必须设计幂等键,否则一次超时重试可能变成两次扣款。

同步工具适合纯计算或短小的内存操作。网络、数据库、文件和队列通常是 IO 密集型,应提供异步实现,并设置连接和整体超时。重试要区分可重试错误和业务拒绝,不能对参数错误无限重试。

调试时应该记录什么

至少记录以下字段。

  • 请求追踪 ID 和会话 ID
  • 模型名称、工具列表版本和提示词版本
  • AIMessage.tool_calls 的工具名与脱敏参数
  • 工具耗时、状态码、重试次数和结果摘要
  • 最终回答与用户反馈

LangSmith 等追踪平台可以把模型调用、工具调用和最终回答串在同一条 Trace 中。追踪解决的是「发生了什么」,并不自动解决「是否允许发生」。权限、审计和脱敏仍由应用负责。

一份发布前检查清单

工具是否只有一个清晰职责,名称和描述是否能让模型区分相似工具。参数是否有类型、范围、单位和必填规则。未知工具、缺少参数、超时和第三方错误是否都有明确结果。是否限制了每轮工具数量、最大循环轮数和总耗时。高风险副作用是否需要人工确认,重试是否具备幂等性。日志和 Trace 是否经过脱敏,是否能根据调用 ID 复现一次失败。

这些问题都回答清楚后,Tools 才不只是一个能在演示中跑通的装饰器,而是一组可以被测试、审计和演进的应用接口。

官方文档与进一步阅读

本文示例基于 LangChain 1.x 的 API 习惯。具体模型是否支持并行工具调用、tool_choice 的完整取值和参数格式,请在锁定依赖版本后以供应商及 LangChain 集成包的官方文档为准。

相关推荐
未若君雅裁2 小时前
工具描述决定调用质量,LangChain 工具 Schema 与参数校验
langchain
艾醒(AiXing-w)3 小时前
LangChain 1.0 入门(二):LangChain 全模型标准化接入最佳实践(小白参数详解版)
前端·javascript·langchain
invicinble7 小时前
langchain --rage了解(周末版本)
langchain
qy2016skq7 小时前
OpenClaw 源码解读——入门与破局9 双插件协同:Quota Guard 负责“停“,Model Router 负责“绕“
langchain·prompt·aigc·embedding·ai编程·llama·agi
这就是佬们吗8 小时前
治幻觉,先治检索:RAG 系统防幻觉的完整工程指南
python·langchain·embedding
爱奥尼欧8 小时前
14.输出解析器-Pydantic与JSON
人工智能·学习·langchain·json
qy2016skq9 小时前
OpenClaw 源码解读——入门与破局8 从“报错不切换“到“秒级自动切换“:模型路由插件在 2026.4.14 上的五次迭代实录
langchain·prompt·aigc·embedding·ai编程·ai-native
AI_小站19 小时前
刚面完百度的 Agent 开发岗,我才发现:世界就是个巨大的草台班子
java·开发语言·人工智能·spring·百度·langchain
swipe1 天前
RabbitMQ 实战:AI Agent 里异步处理的标配方案(手把手 + 4 种交换机全解析)
后端·面试·langchain