第22回 工具调用——从 Prompt JSON 到 Function Calling
手里无锤难钉钉,纸上千言不落地。
要让书生真办案,先把规矩立分明。
上回我们搭了“智能体最小循环”,看官已知:
智能体要办事,必得调用工具。
可江湖里偏偏最常翻车的,也在“工具”二字:
- 参数乱填,API 立刻报错
- 该并行的不并行,白白耗时
- 该拒绝的也乱调用,越界又危险
- 工具返回了错误,模型却装作没看见,继续编
所以这一回不讲花架子,专讲“规矩”。
把规矩立起来,工具调用才能稳。
一、工具调用到底是什么:把一句话翻译成一次函数调用
把工具当成一个函数:
- 名字:
get_weather - 参数:
{"city": "上海"} - 返回:
{"temp_c": 12, "rain": true}
“工具调用能力”就是让模型能做三件事:
- 选对工具
- 填对参数
- 读懂返回,继续下一步
2024 年已有综述把这条能力归纳成“任务规划→工具选择→工具调用→响应生成”的链条,并讨论了数据、训练与评估。1
你看,它其实很像第11回强化学习的“策略”:
- 状态:用户请求 + 当前上下文
- 动作:选择哪个工具、填什么参数
- 结果:工具返回是否满足目标
二、三条硬规矩:格式、边界、错误回传
规矩一:格式要可校验
最怕的不是模型不会选工具,而是:
- 生成了一个“看似 JSON 但其实不是”的东西
- 少了字段、字段类型错了、数组写成了字符串
所以工具调用必须“可校验”:
你给它一个 schema,它得按 schema 来。
规矩二:边界要可拒绝
工具是手脚,手脚要有边界:
- 不允许访问敏感数据
- 不允许执行危险操作
- 不允许调用不存在的工具
边界不是靠一句“你要安全”就能保证的,必须靠约束与审计。
规矩三:错误要可回传
工具调用失败很正常,关键是失败要“被看见”:
- 失败原因是什么?
- 可否重试?是否换参数?是否换工具?
- 还是应该直接向用户说明无法完成?
能把错误回传到推理链里,智能体才会“越办越稳”,而不是“越办越编”。
三、从 Prompt JSON 到 Function Calling:差别不在形式,在可控性
早期很多人用“Prompt 让模型输出 JSON”。
它也能跑,但常见毛病是:
- JSON 不稳定
- 字段名乱写
- 夹杂自然语言
后来出现更结构化的“Function Calling”接口:
把工具名与参数放进专门字段里,让系统层做校验与执行。
看官记住一句:
越结构化,就越容易做校验、审计与回滚。
这也是为什么 2024–2026 的 Agent 系统越来越工程化:
它们把“会说话”拆成了可控模块,而不是让模型一口气写到天明。
四、并行调用与拒绝调用:聪明不在多用工具,而在用得恰当
工具调用有两个“高级动作”,常被忽略:
- 并行调用:互不依赖的工具,应该一起叫
- 拒绝调用:信息不足、越界、或工具无意义时,应该不叫
这两点决定了智能体像不像一个“老练办案人”:
- 老练的人,会先问清楚缺的参数,再动手
- 老练的人,会把独立的子任务并行推进
而不是“想到哪儿叫哪儿”,把系统拖成慢吞吞的流水账。
五、极简可跑代码:一个可校验的 Function Calling 小框架
下面代码做三件事:
- 定义两个工具(
search与add) - 用非常朴素的“模型输出”(我们手写模拟)
- 在执行前做校验:工具是否存在、参数字段是否齐、类型是否对
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
-
Qu, C., et al. Tool Learning with Large Language Models: A Survey (Frontiers of Computer Science, 2024) https://arxiv.org/abs/2405.17935 ↩ ↩2