LLM 工具调用完全指南:从一次"模型说正在查询其实什么都没做"看懂 Function Calling 的真正机制

2024 年我做一个 AI 助手想让它能回答我那个订单发货了吗明天杭州下不下雨这类问题这些问题的答案不在模型脑子里得去查订单系统查天气接口怎么让模型用上这些外部能力这件事我没多想就有了方案把工具告诉模型第一版我做得很顺手我在系统提示里写了一段话大意是你有一个查订单的工具和一个查天气的工具需要时就用本地一测我问查一下订单 A1001 模型回了一句好的正在为您查询订单 A1001 我心里一暖觉得它听懂了可等我真去看它到底干了什么一串问题冒了出来第一种最先把我打懵模型说正在查询可我的订单系统根本没收到任何请求它只是嘴上说查什么都没查第二种最难缠我换了个写法模型这次倒是吐出了一段像调用工具的文字可它调的工具名我从来没定义过是它自己编的第三种最头疼工具名对了参数却不对该传订单号它传了订单描述第四种最莫名其妙我好不容易把工具结果喂回去了模型却像没看见一样继续按它自己的想象答题甚至反复要求调用同一个工具我盯着这一连串问题想了很久才彻底想明白第一版错在一个根本的认知上我以为把工具描述写进 prompt 模型就会调用这个工具它会真的去执行那个函数拿到结果可大模型不会执行任何代码它从头到尾只会做一件事输出文本所谓的工具调用本质从来不是模型执行了工具而是模型输出一段结构化的文本表达我想调用某个工具参数是这些这个意图真正去执行工具的是你的代码执行完你再把结果作为新的输入喂回给模型它是一个模型提议你执行你回传模型继续的多轮循环本文从头梳理为什么把工具写进 prompt 不等于模型会调用工具该怎么定义模型输出的调用意图怎么解析和分发工具结果怎么回传给模型形成闭环多轮工具调用怎么用循环和上限管住以及一些把它做扎实要避开的工程坑

2024 年我做一个 AI 助手,想让它能回答"我那个订单发货了吗""明天杭州下不下雨"这类问题。这些问题的答案不在模型脑子里,得去查订单系统、查天气接口。怎么让模型用上这些外部能力?这件事我没多想,就有了方案:把工具告诉模型。第一版我做得很顺手——我在系统提示里写了一段话,大意是"你有一个查订单的工具和一个查天气的工具,需要时就用",还把每个工具能干什么、要什么参数都用中文描述清楚了。本地一测,我问"查一下订单 A1001",模型回了一句"好的,正在为您查询订单 A1001",我心里一暖,觉得它听懂了,这套助手稳了。可等我真去看它到底干了什么,一串问题冒了出来。第一种最先把我打懵:模型说"正在查询",可我的订单系统根本没收到任何请求——它只是嘴上说查,什么都没查。第二种最难缠:我换了个写法,模型这次倒是吐出了一段像调用工具的文字,可它调的工具名我从来没定义过,是它自己编的。第三种最头疼:工具名对了,参数却不对——该传订单号它传了订单描述,该是数字它给了字符串。第四种最莫名其妙:我好不容易把工具结果喂回去了,模型却像没看见一样,继续按它自己的想象答题,甚至反复要求调用同一个工具,绕不出来。我盯着这一连串问题想了很久,才彻底想明白:第一版错在一个根本的认知上。我以为把工具描述写进 prompt,模型就会"调用"这个工具——它会真的去执行那个函数、拿到结果。可大模型不会执行任何代码,它从头到尾只会做一件事:输出文本。它不可能联网,不可能访问你的数据库,不可能运行你的函数。所谓的工具调用,本质从来不是"模型执行了工具",而是:模型输出一段结构化的文本,表达"我想调用某个工具、参数是这些"这个意图;真正去执行工具的,是你的代码;执行完,你再把结果作为新的输入喂回给模型。它是一个"模型提议、你执行、你回传、模型继续"的多轮循环,模型在这个循环里只负责"决策",执行权和数据始终在你手里。本文从头梳理:为什么把工具写进 prompt 不等于模型会调用,工具该怎么定义,模型输出的调用意图怎么解析和分发,工具结果怎么回传给模型形成闭环,多轮工具调用怎么用循环和上限管住,以及一些把它做扎实要避开的工程坑。

问题背景

先把概念理清楚。一个大模型,本质是一个文本生成器:给它一段输入文本,它生成一段输出文本。它没有手脚,不能点鼠标、不能发网络请求、不能读文件。它"知道"的一切,来自训练数据和你当次给它的 prompt;训练数据有截止日期,而你的订单状态、明天的天气,既不在训练数据里,也不可能凭空出现在它的输出里。

错误认知是:把工具的说明文字写进 prompt,模型就获得了使用工具的能力,会替我去执行。真相是:模型永远只输出文本,它唯一能做的,是输出一段"表示它想调用某工具"的文本;这段文本要变成真实的工具执行,中间必须有你的代码。把这层认知摆正,第一版的几类问题就都能解释了:

  • 嘴上说调、其实没调:你只在 prompt 里用自然语言描述工具,模型就用自然语言回应"正在查询",因为它以为你要的就是一句回应,没有任何机制让它产生可被解析的调用。
  • 工具名是模型编的:工具不是以结构化、受约束的方式提供的,模型就只能"想象"一个工具名,想象出来的东西自然可能不存在。
  • 参数对不上:没有明确的参数 schema 约束,模型给的参数字段、类型、必填项都可能和你的函数签名对不上。
  • 无视工具结果或陷入死循环:工具结果没有以模型能识别的角色和格式回传,或者整个调用循环没有终止条件,模型就会忽略结果、或反复要求同一个调用。

所以工具调用要做对,要解决的根本不是"怎么把工具描述写得更清楚",而是"怎么搭一套机制,让模型的文本意图能被可靠地解析、执行,再把结果可靠地回传"。下面六节就来一步步搭这套机制。

一、为什么"把工具描述写进 prompt"不等于模型会调用工具

第一版的写法,本质是把工具当成了"说明书"——在 prompt 里用一段话告诉模型有什么工具。问题在于,模型读完这段话之后,它能做的还是只有一件事:生成文本。它生成什么文本,取决于它觉得你想要什么。你给它的是一段自然语言描述,它最自然的回应,就是另一段自然语言——"好的,我来帮您查询"。这句话对你的程序毫无用处:它不是一个可以被代码解析、能触发真实函数执行的指令,它就是一句话。

# 反面教材:以为把工具写进 prompt,模型就会"执行"工具

SYSTEM_PROMPT = """你是一个助手。你有以下工具可以使用:
1. 查订单:输入订单号,返回订单状态
2. 查天气:输入城市,返回天气
需要时请使用这些工具。"""

def ask(user_input, client):
    messages = [
        {"role": "system", "content": SYSTEM_PROMPT},
        {"role": "user", "content": user_input},
    ]
    resp = client.chat(messages=messages)
    # 模型只会返回一段文本,比如"好的,正在为您查询订单 A1001"
    # 这段文本里没有任何东西能让你的代码去真正查订单
    return resp["content"]

# 你的订单系统从头到尾没收到任何请求
# 模型"说"它查了,但它根本没有、也不可能去查

要让模型的意图变成可执行的东西,缺的是一个"约定"。模型必须以一种你的代码能稳定解析的、结构化的格式,来表达"我要调用哪个工具、参数是什么";同时,可选的工具清单必须以结构化的方式提供给模型,而不是夹在一段自然语言里。这正是各家大模型 API 提供的 Function Calling(也叫 Tool Use)能力:你用结构化的 schema 把工具传给模型,模型在需要时不再返回普通文本,而是返回一个结构化的 tool call 对象,里面明确写着工具名和参数。

这一节要建立的认知是:工具调用不是给模型"增加了执行能力",而是给你和模型之间"增加了一种结构化的协议"。模型的能力边界一寸都没变——它依然只会输出文本。变的是输出的形式:从一段随意的自然语言,变成一个有固定结构、能被代码可靠解析的对象。理解了这一点,你就会明白后面所有工作的分工:模型负责"决策"(该不该调工具、调哪个、传什么参数),你的代码负责"执行"(真正去查数据库、调接口)和"裁决"(校验、回传、控制循环)。模型是大脑,你的代码是手脚和神经系统。第一版的错,就是误以为把说明书递给大脑,大脑就长出了手脚。

二、工具定义:用结构化 schema 描述,而不是自然语言

既然要走结构化协议,第一步就是把工具用结构化的方式定义出来。主流 API 的约定高度一致:每个工具是一个对象,包含工具名、一段给模型看的功能描述、以及一份用 JSON Schema 写的参数定义。这份参数定义至关重要——它不只是说明,更是对模型输出的一种约束:它规定了参数有哪些字段、每个字段什么类型、哪些是必填。

# 用结构化 schema 定义工具,而不是写一段自然语言

TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "get_order_status",
            "description": "根据订单号查询订单的发货状态。"
                           "当用户询问某个具体订单时调用。",
            "parameters": {
                "type": "object",
                "properties": {
                    "order_id": {
                        "type": "string",
                        "description": "订单号,形如 A1001",
                    }
                },
                "required": ["order_id"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "查询某个城市某一天的天气。",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string",
                             "description": "城市名,如 杭州"},
                    "date": {"type": "string",
                             "description": "日期 YYYY-MM-DD,默认今天"},
                },
                "required": ["city"],
            },
        },
    },
]

# 把 TOOLS 作为 tools 参数传给模型,而不是塞进 system prompt
def ask(user_input, client):
    messages = [{"role": "user", "content": user_input}]
    resp = client.chat(messages=messages, tools=TOOLS)
    return resp

这里有几个细节决定了模型用得好不好。description 是写给模型看的,要说清楚"这个工具是干什么的"以及"什么时候该用它"——模型正是靠它来判断该不该调、调哪个。参数的 description 同样重要,它帮模型理解每个字段该填什么。required 列出必填项,模型会尽量保证这些字段都给。这套 schema 写得越清楚,模型选错工具、漏参数、填错类型的概率就越低。

这一节的认知是:工具的 schema,是你和模型之间那份协议的正文,它的清晰程度直接决定了协议执行的质量。很多人把工具调用做不稳,归咎于"模型不够聪明",但很多时候,真正的问题是 schema 写得含糊——description 一句话带过,没说清适用场景;参数没有 description,模型只能猜。要记住,模型是在"读你的 schema 来决策"的,你给的信息越精确,它的决策就越精确。把 schema 当成 API 文档来写——名字准确、描述完整、参数类型明确、适用场景写清楚——这是让工具调用稳定的地基,也是性价比最高的一笔投入。

三、模型不执行工具:它输出意图,你的代码负责分发执行

把工具 schema 传给模型后,当模型判断需要用工具时,它的返回就不再是一段普通文本了,而是带着一个或多个 tool call 的结构化对象。每个 tool call 里有工具名,和一段 JSON 格式的参数字符串。注意:到这一步,工具仍然没有被执行——模型只是把"我想这样调用"这个意图,以结构化的形式交了出来。真正去执行的,是你的代码。你需要做的,是解析这个意图,根据工具名找到对应的真实函数,把参数喂进去,执行。

# 解析模型的 tool call 意图,分发到真实函数去执行
import json

# 工具名到真实函数的注册表
def get_order_status(order_id: str) -> dict:
    # 这里才是真正访问订单系统的地方
    return {"order_id": order_id, "status": "已发货",
            "carrier": "顺丰", "tracking_no": "SF123456"}

def get_weather(city: str, date: str = "today") -> dict:
    return {"city": city, "date": date,
            "weather": "多云", "temp": "18-25C"}

TOOL_REGISTRY = {
    "get_order_status": get_order_status,
    "get_weather": get_weather,
}

def execute_tool_call(tool_call: dict) -> dict:
    name = tool_call["function"]["name"]
    raw_args = tool_call["function"]["arguments"]

    # 模型给的工具名可能是它编的,先校验存在
    func = TOOL_REGISTRY.get(name)
    if func is None:
        return {"error": f"未知工具 {name}"}

    # 参数是一段 JSON 字符串,解析可能失败
    try:
        args = json.loads(raw_args)
    except json.JSONDecodeError:
        return {"error": "参数不是合法 JSON"}

    # 找到真实函数,真正执行的是这一步
    try:
        return func(**args)
    except TypeError as e:
        return {"error": f"参数与函数签名不匹配:{e}"}

这段代码里有三个"可能出错"的点被显式处理了:工具名可能是模型编的(校验注册表),参数可能不是合法 JSON(捕获解析异常),参数可能和函数签名对不上(捕获 TypeError)。这三个点,正好对应第一版里"工具名是编的""参数对不上"这两类问题。它们不是小心翼翼的过度防御,而是必须的——因为这一侧的输入,来自一个概率性的模型,它给的东西天然就可能不规整。

这一节的认知是:模型输出的工具调用意图,本质上是一份"不可信的外部输入",你的执行代码必须像对待用户提交的表单一样去校验它。这和处理网络请求里的用户输入,是同一个道理。你不会假设用户提交的表单字段一定齐全、类型一定正确,同样地,你也不能假设模型给的工具名一定存在、参数一定能对上函数签名。模型是在"尽力"按 schema 输出,但它是概率性的,不保证每次都对。把工具的执行入口,设计成一个"先校验、校验过了才执行、校验不过就返回一个明确的错误"的关卡,工具调用才有了稳的基础。这个关卡返回的错误,下一节会看到,本身也是要回传给模型的有用信息。

四、把工具结果回传给模型,完成一轮闭环

工具执行完了,拿到了结果——比如订单状态是"已发货"。但这个结果现在还在你的代码里,模型并不知道。如果就此打住,模型那边等于话说了一半。必须把工具结果送回给模型,模型才能基于这个真实数据,生成最终给用户的回答。回传的方式也是协议的一部分:你把工具结果包装成一条特殊角色(role 为 tool)的消息,带上对应的 tool call id,追加到对话里,再调用一次模型。

# 把工具结果以 tool 消息回传,再调一次模型生成最终回答
import json

def run_one_round(user_input: str, client) -> str:
    messages = [{"role": "user", "content": user_input}]

    # 第一次调用:模型决定要不要用工具
    resp = client.chat(messages=messages, tools=TOOLS)
    msg = resp["message"]

    # 模型没要求调工具,它的回复就是最终答案
    if not msg.get("tool_calls"):
        return msg["content"]

    # 模型要求调工具:先把模型这条带 tool_calls 的消息存进对话
    messages.append(msg)

    # 逐个执行工具,把结果作为 role=tool 的消息追加回去
    for tc in msg["tool_calls"]:
        result = execute_tool_call(tc)
        messages.append({
            "role": "tool",
            "tool_call_id": tc["id"],          # 对应上是哪次调用
            "content": json.dumps(result, ensure_ascii=False),
        })

    # 第二次调用:模型拿到真实结果,生成给用户的最终回答
    final = client.chat(messages=messages, tools=TOOLS)
    return final["message"]["content"]

注意这里的消息序列:用户问题、模型的工具调用请求、工具执行结果、模型的最终回答。这四条消息构成了一次完整的工具调用闭环。其中那条 role 为 tool 的消息是关键——它用模型认识的格式、带着能对应上的 id,把真实数据交还给了模型。第一版里"模型无视工具结果",很多时候就是因为结果没有用这种规范的方式回传,模型根本没把它当成"工具的结果"来看待。

这一节的认知是:工具调用是一个"闭环",而不是一次"单向的请求"。很多人脑子里的模型是"我问、它答",是一来一回。但带工具的模型不是这样,它是"我问、它提议调工具、我执行、我回传、它再答",至少两次模型调用、一来二去。这个闭环里,信息的流向是有去有回的:工具的意图从模型流向你的代码,工具的结果再从你的代码流回模型。少了"流回"这半程,模型就是在凭空作答。所以实现工具调用时,心里要始终装着这个闭环的完整形状,而不能停在"拿到 tool call 就完事了"。把工具结果规范地、带 id 地回传,是这个闭环能合上的关键一步。

五、多轮工具调用要有循环和上限

上一节的闭环假设了"调一次工具就够"。但真实任务常常不是。用户问"我最近那个发往杭州的订单,到货那天天气怎么样",模型可能要先调 get_order_status 拿到订单和到货日期,再调 get_weather 查那天的天气——这是连续两次、且后一次依赖前一次结果的工具调用。所以工具调用的正确形态,不是"调一次",而是一个循环:调模型,如果它要调工具就执行并回传,然后再调模型,如此往复,直到模型不再要求调工具、给出最终答案为止。

# 工具调用的正确形态:一个带上限的循环
import json

def run_agent(user_input: str, client, max_iterations: int = 6) -> str:
    messages = [{"role": "user", "content": user_input}]

    for step in range(max_iterations):
        resp = client.chat(messages=messages, tools=TOOLS)
        msg = resp["message"]
        messages.append(msg)

        # 模型不再要求工具,说明它给出了最终答案,退出循环
        if not msg.get("tool_calls"):
            return msg["content"]

        # 执行这一轮所有的工具调用,结果逐条回传
        for tc in msg["tool_calls"]:
            result = execute_tool_call(tc)
            messages.append({
                "role": "tool",
                "tool_call_id": tc["id"],
                "content": json.dumps(result, ensure_ascii=False),
            })

    # 跑满 max_iterations 仍未收敛:必须有这个兜底
    return "抱歉,这个问题处理起来比较复杂,暂时没能完成。"

这段代码里,max_iterations 不是一个可有可无的保险,它是必需品。模型是概率性的,它有可能陷入一种病态:反复要求调用同一个工具、或在两个工具之间来回横跳,就是不收敛到最终答案。如果循环没有上限,这种情况会让一次请求无限地调用模型、无限地烧钱,最后还可能因为对话太长撞上上下文窗口而崩。一个明确的迭代上限,加上跑满之后的兜底回答,是把这种失控关进笼子的唯一办法。

把整个流程画出来,工具调用的闭环加循环,就是下面这张图:

[mermaid]
flowchart TD
A[用户提出问题] --> B[把对话发给模型]
B --> C{模型要求调用工具吗}
C -->|不要 直接给答案| D[把回答返回给用户]
C -->|要调用工具| E[校验工具名与参数]
E --> F[你的代码执行真实工具]
F --> G[把工具结果作为 tool 消息回传]
G --> H{迭代次数是否超过上限}
H -->|未超过| B
H -->|已超过| I[返回兜底回答 结束]

这一节的认知是:带工具的模型调用,本质是一个需要你的代码来驱动和管控的循环,模型自己不会管这个循环。模型在每一步只做局部决策——"现在这一步,我要不要调工具、调哪个"。它没有全局视角,不知道自己已经调了几次,也不会主动判断"我是不是绕进死胡同了"。这个全局的控制——什么时候继续、什么时候因为次数超限而强制停止——必须由你的循环代码来负责。这也是"模型负责决策、你的代码负责管控"这个分工的延伸:决策是一步一步的、局部的,管控是贯穿全程的、全局的。把循环和上限写好,工具调用才既能完成多步任务,又不会失控。

六、把工具调用做扎实,要避开的工程坑

前面五节搭出了一套能定义工具、能解析意图、能执行回传、能多轮循环的工具调用机制。但要在生产里稳住,还有几个坑得专门讲。第一个是工具参数的严格校验。第三节捕获了 JSON 解析失败和签名不匹配,但还不够——模型可能给出类型对、但取值非法的参数(比如日期格式不对、订单号明显不合规)。用 JSON Schema 对参数做一次完整校验,能在工具真正执行前把这些挡掉。

# 用 JSON Schema 对模型给的参数做完整校验
from jsonschema import validate, ValidationError

# 工具名到其参数 schema 的映射(取自 TOOLS 定义)
PARAM_SCHEMAS = {
    "get_order_status":
        TOOLS[0]["function"]["parameters"],
    "get_weather":
        TOOLS[1]["function"]["parameters"],
}

def validate_args(name: str, args: dict) -> str:
    schema = PARAM_SCHEMAS.get(name)
    if schema is None:
        return f"未知工具 {name}"
    try:
        validate(instance=args, schema=schema)
        return ""             # 空字符串表示校验通过
    except ValidationError as e:
        # 这条错误信息会回传给模型,让它知道哪里错了
        return f"参数校验失败:{e.message}"

第二个坑,是工具出错时怎么办。工具执行可能失败——订单系统超时、天气接口返回错误、参数校验没过。一个重要原则是:不要把这些错误直接抛出去中断整个流程,而要把错误信息也当成一种"工具结果",规范地回传给模型。模型拿到"参数校验失败:订单号格式不对"这样的反馈,往往能自己纠正,用对的参数再调一次。这就是让错误信息走和正常结果一样的回传通道。

# 工具出错也要回传给模型,给它自我纠正的机会
import json

def execute_with_feedback(tool_call: dict) -> str:
    name = tool_call["function"]["name"]
    try:
        args = json.loads(tool_call["function"]["arguments"])
    except json.JSONDecodeError:
        return json.dumps({"error": "参数不是合法 JSON,请重新生成"},
                          ensure_ascii=False)

    # 执行前先做 schema 校验,错误信息回传给模型
    err = validate_args(name, args)
    if err:
        return json.dumps({"error": err}, ensure_ascii=False)

    func = TOOL_REGISTRY.get(name)
    try:
        result = func(**args)
        return json.dumps(result, ensure_ascii=False)
    except Exception as e:
        # 工具内部异常也作为结果回传,不要直接 raise 中断
        return json.dumps({"error": f"工具执行失败:{e}"},
                          ensure_ascii=False)

还有几个坑值得点一下。其一,有些工具是"危险"的——下单、退款、删除数据这类有副作用的操作,模型决定要调用时,不该让代码闷头执行,而应该先停下来,由人确认或由更严格的规则裁决后再放行。模型的决策是概率性的,有副作用的操作绝不能完全交给概率。其二,模型有时会一次返回多个工具调用,如果它们之间没有依赖,可以并行执行以省时间;但要注意把每个结果和对应的 tool_call_id 配对回传。其三,要警惕模型该调工具时偷懒不调、自己编答案——可以在 schema 的 description 里把适用场景写得更明确,必要时在系统提示里强调"涉及订单和天气的问题必须用工具,不要凭记忆回答"。下面把一次典型的多轮工具调用,它的消息序列摊开看一眼:

一次多轮工具调用的消息序列

  序号  role        内容要点
  ------------------------------------------------------
  1     user        我发往杭州那个订单到货那天天气如何
  2     assistant   tool_calls: get_order_status(order_id)
  3     tool        {订单已发货, 预计到货 2026-05-25}
  4     assistant   tool_calls: get_weather(杭州, 2026-05-25)
  5     tool        {杭州 2026-05-25 多云 18-25C}
  6     assistant   到货那天杭州多云,18 到 25 度(最终回答)

  要点:assistant 与 tool 消息交替出现,直到 assistant
        不再发起 tool_calls,第 6 条才是给用户的答案。

这一节这几个坑,串起来是同一个道理:工具调用这套机制,信任的边界要划清楚。模型负责决策,这件事可以信任它做得不错;但"模型的决策一定正确""模型给的参数一定合法""模型一定会用工具""有副作用的操作可以直接照做"——这些都不能信。你的代码要在模型和真实世界之间,当好那个"裁决者":校验它的参数、回传它的错误让它纠正、对危险操作设卡、给循环设上限。把该信任的交给模型,把不能信任的交给你的代码去兜底——工具调用做到这个程度,才算扎实。

关键概念速查

概念 说明
Function Calling 让模型以结构化方式表达"想调用某工具"的机制,也叫 Tool Use
模型只输出文本 模型不执行任何代码,工具调用是它输出的结构化意图,执行靠你的代码
工具 schema 用 JSON Schema 定义的工具描述,含工具名、功能描述、参数定义
tool call 模型返回的结构化调用意图对象,含工具名和 JSON 参数
工具分发 按工具名从注册表找到真实函数并执行,执行前需校验
tool 消息 把工具结果回传给模型的特殊角色消息,带 tool_call_id 对应
调用闭环 用户问到模型提议到代码执行到结果回传到模型作答的完整环路
迭代上限 多轮工具调用循环的最大次数,防止模型不收敛导致失控
参数校验 把模型给的参数当不可信输入,用 schema 校验后才执行
错误回传 工具出错时把错误也作为结果回传,给模型自我纠正的机会

避坑清单

  1. 不要只在 prompt 里用自然语言描述工具:模型只会用自然语言回应,产生不了可解析的调用。
  2. 不要以为模型会执行工具:模型只输出调用意图,真正执行的必须是你的代码。
  3. 不要用模糊的工具描述:description 和参数说明写得越清楚,模型选错、漏参的概率越低。
  4. 不要信任模型给的工具名:可能是它编的,执行前先校验注册表里是否存在。
  5. 不要信任模型给的参数:当成不可信外部输入,用 JSON Schema 完整校验后再执行。
  6. 不要漏掉结果回传:工具结果要以 role 为 tool 的消息带 id 回传,闭环才能合上。
  7. 不要把工具调用当一次性请求:它是多轮闭环,要写成带退出条件的循环。
  8. 不要让循环无上限:设 max_iterations,防止模型不收敛导致无限调用和烧钱。
  9. 不要让工具错误直接中断:把错误作为结果回传,模型常能据此自我纠正。
  10. 不要让模型直接触发危险操作:下单、删除等有副作用的工具应先经人工或规则确认。

总结

回头看第一版那个"把工具写进 prompt"的 AI 助手,它的错误很典型。它不在某一行代码,而在一个对大模型的根本误解:以为把工具的说明书递给模型,模型就长出了使用工具的手脚。真相是,模型从头到尾只会输出文本,它不可能执行你的函数、访问你的数据库。所谓工具调用,是模型输出一个结构化的"调用意图",由你的代码去执行,再把结果回传——模型负责决策,代码负责执行和管控。

而把这件事做对,工程量并不小。它不是"开启 Function Calling"就完事,而是要把一整套机制搭起来:用结构化 schema 定义工具,解析并校验模型给的调用意图,把工具结果规范地回传形成闭环,用带上限的循环驱动多轮调用,再加上参数校验、错误回传、危险操作确认这些工程上的关卡。一个能在生产里稳住的工具调用,是这些环节一个都不少地拼起来的。

这件事其实很像一位经验丰富的医生和他的检查科室。医生本人不会去做 CT、不会去化验血液——他坐在诊室里,凭专业判断"这个病人需要做一个 CT、查一下血常规",开出检查单。真正去做检查的,是影像科、检验科;做完了,报告单送回到医生手里,医生再根据报告单的真实数据下诊断。医生负责决策,科室负责执行,报告单是结果的回传。没有哪个医生会以为"我开了检查单,检查就自动做完了"。工具调用里的模型,就是这位医生:它做判断、开单子,但它不亲自动手,执行和把结果送回来,是整个系统的事。

这类问题还有一个共同的麻烦:它在本地很容易给你一种"已经成了"的错觉。你在本地问一句、模型回一句"好的正在查询",看起来对答如流,可它背后什么都没发生。工具到底有没有被调用、参数对不对、结果有没有真的影响最终回答——这些只有你去追踪每一轮的消息序列、去看工具函数有没有被真正执行,才会暴露。所以如果你正在做一个要接外部能力的 AI 应用,别被模型那句流畅的"正在为您查询"骗了。从一开始就把它当成一个"模型决策、代码执行、结果回传"的闭环来设计,把校验、回传、循环上限这些机制扎扎实实搭好——这是这篇文章最想留给你的一句话。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

数据库连接池完全指南:从一次"上线后数据库报 too many connections"看懂连接为什么不能即取即用

2026-5-22 20:05:31

技术教程

限流算法完全指南:从一次"限了每分钟一万却涌进两万"看懂固定窗口、滑动窗口、漏桶与令牌桶

2026-5-22 20:14:47

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索