第22回 工具调用——从 Prompt JSON 到 Function Calling

手里无锤难钉钉,纸上千言不落地。
要让书生真办案,先把规矩立分明。

上回我们搭了“智能体最小循环”,看官已知:
智能体要办事,必得调用工具。

可江湖里偏偏最常翻车的,也在“工具”二字:

  • 参数乱填,API 立刻报错
  • 该并行的不并行,白白耗时
  • 该拒绝的也乱调用,越界又危险
  • 工具返回了错误,模型却装作没看见,继续编

所以这一回不讲花架子,专讲“规矩”。
把规矩立起来,工具调用才能稳。


一、工具调用到底是什么:把一句话翻译成一次函数调用

把工具当成一个函数:

  • 名字:get_weather
  • 参数:{"city": "上海"}
  • 返回:{"temp_c": 12, "rain": true}

“工具调用能力”就是让模型能做三件事:

  1. 选对工具
  2. 填对参数
  3. 读懂返回,继续下一步

2024 年已有综述把这条能力归纳成“任务规划→工具选择→工具调用→响应生成”的链条,并讨论了数据、训练与评估。1

你看,它其实很像第11回强化学习的“策略”:

  • 状态:用户请求 + 当前上下文
  • 动作:选择哪个工具、填什么参数
  • 结果:工具返回是否满足目标

二、三条硬规矩:格式、边界、错误回传

规矩一:格式要可校验

最怕的不是模型不会选工具,而是:

  • 生成了一个“看似 JSON 但其实不是”的东西
  • 少了字段、字段类型错了、数组写成了字符串

所以工具调用必须“可校验”:
你给它一个 schema,它得按 schema 来。

规矩二:边界要可拒绝

工具是手脚,手脚要有边界:

  • 不允许访问敏感数据
  • 不允许执行危险操作
  • 不允许调用不存在的工具

边界不是靠一句“你要安全”就能保证的,必须靠约束与审计

规矩三:错误要可回传

工具调用失败很正常,关键是失败要“被看见”:

  • 失败原因是什么?
  • 可否重试?是否换参数?是否换工具?
  • 还是应该直接向用户说明无法完成?

能把错误回传到推理链里,智能体才会“越办越稳”,而不是“越办越编”。


三、从 Prompt JSON 到 Function Calling:差别不在形式,在可控性

早期很多人用“Prompt 让模型输出 JSON”。
它也能跑,但常见毛病是:

  • JSON 不稳定
  • 字段名乱写
  • 夹杂自然语言

后来出现更结构化的“Function Calling”接口:
把工具名与参数放进专门字段里,让系统层做校验与执行。

看官记住一句:
越结构化,就越容易做校验、审计与回滚。
这也是为什么 2024–2026 的 Agent 系统越来越工程化:
它们把“会说话”拆成了可控模块,而不是让模型一口气写到天明。


四、并行调用与拒绝调用:聪明不在多用工具,而在用得恰当

工具调用有两个“高级动作”,常被忽略:

  1. 并行调用:互不依赖的工具,应该一起叫
  2. 拒绝调用:信息不足、越界、或工具无意义时,应该不叫

这两点决定了智能体像不像一个“老练办案人”:

  • 老练的人,会先问清楚缺的参数,再动手
  • 老练的人,会把独立的子任务并行推进

而不是“想到哪儿叫哪儿”,把系统拖成慢吞吞的流水账。


五、极简可跑代码:一个可校验的 Function Calling 小框架

下面代码做三件事:

  • 定义两个工具(searchadd
  • 用非常朴素的“模型输出”(我们手写模拟)
  • 在执行前做校验:工具是否存在、参数字段是否齐、类型是否对
import json


TOOLS = {
    "search": {
        "schema": {"q": str},
        "fn": lambda q: [s for s in ["RAG", "Agent", "MCTS"] if q.lower() in s.lower()],
    },
    "add": {
        "schema": {"a": int, "b": int},
        "fn": lambda a, b: a + b,
    },
}


def validate_call(call):
    if not isinstance(call, dict):
        return False, "call not dict"
    if call.get("type") != "tool_call":
        return False, "bad type"
    name = call.get("name")
    if name not in TOOLS:
        return False, "unknown tool"
    args = call.get("args")
    if not isinstance(args, dict):
        return False, "args not dict"
    schema = TOOLS[name]["schema"]
    for k, t in schema.items():
        if k not in args:
            return False, "missing field: " + k
        if not isinstance(args[k], t):
            return False, "bad type: " + k
    return True, ""


def run_call(call):
    ok, msg = validate_call(call)
    if not ok:
        return {"ok": False, "error": msg}
    name = call["name"]
    args = call["args"]
    out = TOOLS[name]["fn"](**args)
    return {"ok": True, "name": name, "output": out}


if __name__ == "__main__":
    model_outputs = [
        '{"type":"tool_call","name":"add","args":{"a":3,"b":5}}',
        '{"type":"tool_call","name":"search","args":{"q":"ag"}}',
        '{"type":"tool_call","name":"add","args":{"a":"3","b":5}}',
        '{"type":"tool_call","name":"rm","args":{"path":"/"}}',
    ]
    for s in model_outputs:
        call = json.loads(s)
        print(run_call(call))

你会看到:

  • 前两个调用通过校验并成功执行
  • 第三个因为类型不对被拒绝
  • 第四个因为工具不存在被拒绝

这就是“规矩”的力量:
不是指望模型永远不犯错,而是让错变得可拦截、可回传、可修正


六、小结:工具调用要像做账,不能像写诗

写诗可以“意会”,做账必须“可核对”。
工具调用是智能体从“能说”到“能做”的门槛:

  • 结构化输出(便于校验)
  • 参数约束(便于安全)
  • 错误回传(便于纠错)
  • 并行与拒绝(便于效率与稳健)

下一回(第23回),我们就把“工具”换成最常用的一把:检索
RAG 为什么能救幻觉?它又为什么会引入新的幻觉?
关键在“证据”二字:证据怎么切、怎么找、怎么拼。

欲知后事如何,且听下回分解。


幻觉核查

  • 工具学习链条(规划→选择→调用→生成):可在综述中核对其分阶段表述与讨论范围。1
  • 本回代码为教学用最小框架,不等同于任何厂商实现;其目的在于展示“校验与拒绝”的工程要点。

逻辑审计

  • 与第21回衔接:第21回给出智能体循环,本回补上“行动”环节的约束,使循环可控。
  • 与导读一致:导读强调“工作流与可验证执行”,本回把“可验证”落到 schema 校验与错误回传。
  • 为后续铺路:第23回开始进入检索;检索结果同样要做“引用与核对”,本回的“可校验”思路会贯穿后续所有 RAG 章节。

引用与溯源

Footnotes

  1. Qu, C., et al. Tool Learning with Large Language Models: A Survey (Frontiers of Computer Science, 2024) https://arxiv.org/abs/2405.17935 2