1604 字
约 5 分钟
20
FunctionCalling工具调用
2026-10-13
无标签
Function Calling:给模型装上手脚
系列:ToyAgent 智能体知识库 04/26
一句话核心:模型负责决定"用什么工具、传什么参数",执行永远在你手里——这是智能体安全性的基石。
为什么需要
ReAct 教学版用提示词约定"让模型输出一段 JSON 文本",能用但脆弱:模型偶尔输出格式漂移、无法表达参数 schema、没有专门的校验环节。Function Calling 把"模型想调用工具"这件事从自由文本升级为协议级结构:请求体里声明工具清单(含 JSON Schema),模型在响应里返回结构化的调用请求,你来执行并回填。分工由此清晰——模型只出主意,动手的永远是你。
核心机制:三步舞
// 第一步:声明工具清单(最简 Tool Schema,生产版放请求体 tools 数组)
String SYSTEM_PROMPT = """
你是一个可以调用工具的智能体。可用工具:
- get_weather(city): 查询指定城市的实时天气
- calculator(expression): 计算四则运算
需要工具时,只输出 JSON:
{"tool": "工具名", "arguments": {"参数名": "值"}}
不需要工具时,直接自然语言回答。
""";
// 第二步:模型返回结构化调用请求(只出主意,不执行)
String first = model.chat(messages);
// → {"tool": "get_weather", "arguments": {"city": "广州"}}
// 第三步:执行工具,把结果回填,模型基于事实作答
String result = execute(tool, args);
messages.add(Message.user("工具执行结果: " + result));
String answer = model.chat(messages);
完整链路:声明工具 → 模型决策调用 → 执行 + 回填 → 模型生成回答。
sequenceDiagram
participant A as 应用
participant M as 大模型
participant E as execute 执行层
A->>M: 请求体声明工具清单
M-->>A: tool_calls get_weather 广州
A->>E: 执行工具 工具名加参数
E-->>A: 返回结果 26 度多云
A->>M: 回填工具结果
M-->>A: 生成最终自然语言回答
图示:Function Calling 三步舞——声明、决策、执行回填;模型只出主意,副作用永远发生在你的执行层。
安全性基石:决定权在模型,执行权在你
模型输出的只是一段文本(工具名 + 参数),它永远无法直接触达你的数据库或文件系统。所有副作用都发生在你的 execute() 里——你可以校验参数、做权限检查、加白名单。这是 Function Calling 与"让模型直接执行代码"的本质区别。
举例:一条生产化的执行链
在 execute() 这一层做校验、权限与可读错误,就是教学版与生产版真正的分水岭:
private String execute(String tool, Map<String, Object> args) {
// 1. 工具白名单:模型幻觉出不存在的工具名时,返回可读错误而非异常
if (!registry.contains(tool)) {
return "错误:工具 " + tool + " 不存在。可用工具:" + registry.names();
}
// 2. 参数 schema 校验 + 权限检查(模型不能越过你的安全边界)
if (!registry.checkPermission(tool, currentUser())) {
return "错误:当前用户无权调用 " + tool;
}
try {
return registry.invoke(tool, args);
} catch (Exception e) {
// 3. 失败要可读:返回模型能理解并自我纠错的文本,而不是抛堆栈
return "执行失败:" + e.getMessage();
}
}
模拟轨迹:
用户:广州今天多少度?穿什么合适?
模型 → tool_calls: get_weather({"city": "广州"})
execute() → "26°C 多云"
回填后模型 → 广州今天 26°C 多云,建议薄外套或长袖。
LangChain4j 里的等价写法——工具就是一个带注解的方法,框架替你完成 schema 生成与回填:
class WeatherTools {
@Tool("查询指定城市的实时天气")
String getWeather(String city) { return weatherApi.fetch(city); }
}
// AiServices.builder(Agent.class).tools(new WeatherTools()).build();
教学版 vs 生产版
| 维度 | 教学版(本场景) | 生产协议(OpenAI/Claude/MCP) |
|---|---|---|
| 工具声明 | 提示词里写清单 | 请求体 tools 数组 + JSON Schema |
| 调用表达 | 模型输出 JSON 文本 | 响应 tool_calls 结构化字段 |
| 结果回填 | user 消息拼接 | role=tool 专用消息 |
形式不同,语义完全一致:先决策、再执行、后回填。读懂教学版再看原生协议,只是换了个"信封格式"。另有一个生产大坑:LangChain4j 默认对"模型幻觉出不存在的工具"抛异常断流,应改为返回可读错误或显式配置容错策略。
好的工具设计长什么样
- 单一职责:一个工具只做一件事,复杂动作组合成技能(见后续场景);
- 参数自描述:模型只见过 schema,参数名要"望文生义"(
city优于c); - 失败要可读:返回"城市未收录"而不是抛堆栈,模型才能自我纠错。
graph TD
A["模型输出的调用意图"] --> B{"工具在白名单?"}
B -->|否| C["返回可读错误"]
B -->|是| D{"有权限?"}
D -->|否| C
D -->|是| E["registry.invoke 执行"]
E -->|成功| F["结果回填模型"]
E -->|异常| G["翻译成模型能懂的失败文本"]
G --> F
图示:execute() 执行层切面——白名单、权限、失败可读化三道关卡都在这里,模型永远触达不到底层资源。
面试考点
- "模型能直接操作我的数据库吗?"——不能。模型只能输出调用意图(工具名 + 参数),所有副作用都发生在你的执行层,这是做权限控制和安全审计的天然切面。
- "工具抛异常给模型会发生什么?"——异常信息若不可读,模型无法纠错,可能重试放大问题;应把失败翻译成模型能理解的文本回填。
生产化路径
- 用原生协议替代提示词清单:LangChain4j
@Tool/ Spring AI@ToolCallback自动生成 schema,避免手写漂移。 - 执行层统一收口:白名单、参数校验、权限、审计日志、脱敏都做在
execute()这一个切面上(后续沙箱、审批、Hooks 场景都在这层扩展)。 - 工具命名空间化:多来源工具(内置 + MCP + 插件)用前缀区分,防止同名工具被模型误选。
FunctionCalling工具调用
https://clxhxhhr.top/posts/4704/ 评论
0 条
还没有评论,先写一条吧。