Agent 开发入门(二):让模型学会用工具 — Function Calling
Agent 开发入门(二):让模型学会用工具 — Function Calling
模型不只能说,还能做。Function Calling 让模型告诉你它想调什么函数,你来执行。
上一篇,我们跑通了第一次大模型 API 调用。模型能回答问题,能聊天,但它只能"说"——它不知道今天天气怎么样,不能帮你查数据库,不能发邮件。
这篇,我们给它装上"手"。
先聊聊历史:从 functions 到 tools
在正式动手之前,先花两分钟理清一个容易混淆的概念。
2023 年 6 月,OpenAI 在 Chat Completions API 中首次引入了 functions 参数和 function_call 参数,让模型可以声明"我想调用某个函数"。这是大模型工具调用的起点。
但 functions 有个明显的局限:它只支持函数调用这一种工具类型。OpenAI 很快意识到,模型需要的"工具"不只是函数——未来可能还有代码解释器、文件检索等。
于是 2023 年 11 月,OpenAI 发布了新的 tools 参数和 tool_choice 参数,替代了原来的 functions 和 function_call。tools 是一个数组,每个元素有 type 字段,目前 type 只有 "function" 一种,但这个设计为未来扩展留了口子。同时,模型返回的字段也从 function_call 变成了 tool_calls(注意是复数),支持一次返回多个工具调用。
旧的 functions 参数随后被标记为 deprecated。
| 时间 | 变化 | 关键参数 |
|---|---|---|
| 2023.06 | 首次引入函数调用 | functions + function_call |
| 2023.11 | 升级为工具调用 | tools + tool_choice + tool_calls |
| 之后 | functions 被废弃 |
旧参数仍可用但不推荐 |
现在几乎所有平台(包括百炼、DeepSeek、智谱等)都直接实现了 tools 版本的接口。所以我们这篇文章直接用 tools,不再提旧的 functions。
参考:OpenAI Function calling and other API updates (2023.06)、百炼 Function Calling 文档
Function Calling 的本质
概念理清了,再说说它到底在干什么。
模型不是真的在调用你的函数。
Function Calling 的实际流程是这样的:
- 你告诉模型:“我有这些工具,每个工具能干什么,需要什么参数”
- 用户提问,模型判断需不需要用工具
- 如果需要,模型返回:“我想调用
get_weather这个函数,参数是{"city": "北京"}” - 你拿到这个信息,去真正执行函数,拿到结果
- 你把结果塞回 messages,再发一次请求
- 模型根据结果生成最终回复
模型做的只是"决策"——它告诉你该调什么、传什么参数。真正的执行在你这边。
这个设计很合理:模型是语言模型,不是执行引擎。它擅长理解意图、生成结构化输出,但它没有权限访问你的数据库、调用你的内部 API。
用一张表对比一下:
| 普通对话 | Function Calling |
|---|---|
| 用户问 → 模型答 → 结束 | 用户问 → 模型决策 → 你执行 → 模型总结 |
| 模型只能用训练数据 | 模型可以用实时数据 |
| 一轮结束 | 可能多轮 |
完整流程:查天气示例
我们做一个"查天气"的例子,用模拟数据,重点是走通整个流程。
第一步:定义工具
工具用 JSON Schema 描述,告诉模型这个函数叫什么、干什么、需要什么参数:
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市的当前天气",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,如:北京、上海"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "温度单位,默认 celsius"
}
},
"required": ["city"]
}
}
}
description 字段非常重要——模型就是靠这个判断什么时候该用这个工具。写得越清楚,模型判断越准。
第二步:发送请求,模型返回 tool_calls
把工具定义加到请求里,发给模型:
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class FunctionCallingDemo {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();
String requestBody = """
{
"model": "qwen-plus",
"messages": [
{"role": "user", "content": "北京今天天气怎么样?"}
],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市的当前天气",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,如:北京、上海"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "温度单位,默认 celsius"
}
},
"required": ["city"]
}
}
}
],
"tool_choice": "auto"
}
""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(
"https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions"))
.header("Authorization", "Bearer YOUR_BAILIAN_API_KEY")
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
}
}
模型返回的不是普通文本,而是 tool_calls:
{
"choices": [
{
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\": \"北京\", \"unit\": \"celsius\"}"
}
}
]
},
"finish_reason": "tool_calls"
}
]
}
注意几个关键字段:
finish_reason是tool_calls,不是stop,说明模型没有直接回答,而是要调工具tool_calls[0].id:这个调用的唯一 ID,后面回传结果时要用tool_calls[0].function.arguments:参数是 JSON 字符串,不是对象,需要你自己解析
第三步:执行函数,回传结果
你拿到 tool_calls 后,执行对应的函数,然后把结果以 role: tool 的消息加回 messages,再发第二轮请求:
// 第二轮:把工具执行结果回传给模型
String secondRequest = """
{
"model": "qwen-plus",
"messages": [
{"role": "user", "content": "北京今天天气怎么样?"},
{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\\"city\\": \\"北京\\", \\"unit\\": \\"celsius\\"}"
}
}
]
},
{
"role": "tool",
"tool_call_id": "call_abc123",
"content": "北京今天晴,气温 22°C,湿度 45%,东南风 3 级"
}
],
"tools": [...]
}
""";
模型拿到工具结果后,生成最终回复:
{
"choices": [
{
"message": {
"role": "assistant",
"content": "北京今天天气不错,晴天,气温 22°C,湿度适中,东南风 3 级,适合出行。"
},
"finish_reason": "stop"
}
]
}
整个流程走完了。
完整可运行示例
把三步串起来,一个完整的 Java 程序:
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class WeatherAgentDemo {
static final String API_URL =
"https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions";
static final String API_KEY = "YOUR_BAILIAN_API_KEY";
// 模拟天气查询,实际项目里换成真实 API 调用
static String getWeather(String city) {
return "{\"city\": \"" + city + "\", \"temperature\": 22, "
+ "\"unit\": \"celsius\", \"condition\": \"晴\", "
+ "\"humidity\": \"45%\", \"wind\": \"东南风 3 级\"}";
}
static String toolsJson() {
return """
[
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市的当前天气",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,如:北京、上海"
}
},
"required": ["city"]
}
}
}
]
""";
}
static String sendRequest(HttpClient client, String body) throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(API_URL))
.header("Authorization", "Bearer " + API_KEY)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
return client.send(request, HttpResponse.BodyHandlers.ofString()).body();
}
static String extract(String json, String key) {
String search = "\"" + key + "\":";
int start = json.indexOf(search);
if (start == -1) return "";
start += search.length();
while (start < json.length() && json.charAt(start) == ' ') start++;
char first = json.charAt(start);
if (first == '"') {
int end = json.indexOf('"', start + 1);
return json.substring(start + 1, end);
}
int end = start;
while (end < json.length() && json.charAt(end) != ',' && json.charAt(end) != '}') end++;
return json.substring(start, end).trim();
}
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();
// ---- 第一轮:发送用户问题 + 工具定义 ----
String firstBody = """
{
"model": "qwen-plus",
"messages": [
{"role": "user", "content": "北京今天天气怎么样?"}
],
"tools": %s,
"tool_choice": "auto"
}
""".formatted(toolsJson());
String firstResponse = sendRequest(client, firstBody);
System.out.println("=== 第一轮响应 ===");
System.out.println(firstResponse);
if (!firstResponse.contains("\"finish_reason\":\"tool_calls\"")
&& !firstResponse.contains("\"finish_reason\": \"tool_calls\"")) {
System.out.println("模型直接回答,无需调用工具");
return;
}
String toolCallId = extract(firstResponse, "id");
String city = "北京"; // 简化处理,实际应解析 arguments
String weatherResult = getWeather(city);
System.out.println("\n=== 工具执行结果 ===");
System.out.println(weatherResult);
// ---- 第二轮:把工具结果回传给模型 ----
String secondBody = """
{
"model": "qwen-plus",
"messages": [
{"role": "user", "content": "北京今天天气怎么样?"},
{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "%s",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\\"city\\": \\"%s\\"}"
}
}
]
},
{
"role": "tool",
"tool_call_id": "%s",
"content": "%s"
}
],
"tools": %s
}
""".formatted(
toolCallId, city, toolCallId,
weatherResult.replace("\"", "\\\"").replace("\n", ""),
toolsJson()
);
String secondResponse = sendRequest(client, secondBody);
System.out.println("\n=== 最终回复 ===");
System.out.println(secondResponse);
}
}
这个示例为了零依赖,JSON 解析用了简单字符串查找。实际项目里建议引入 Jackson 或 Gson,解析会更可靠。
tool_choice 参数
tool_choice 控制模型是否必须调用工具:
| 值 | 含义 |
|---|---|
"auto" |
模型自己判断要不要用工具(默认) |
"none" |
禁止调用工具,强制直接回答 |
"required" |
必须调用至少一个工具 |
{"type": "function", "function": {"name": "get_weather"}} |
强制调用指定函数 |
大多数场景用 auto 就够了。required 适合你明确知道需要工具的场景,比如数据提取任务。指定函数名适合测试或强制走某个分支。
常见坑
坑 1:模型不调用工具
最常见的原因是 description 写得不够清楚。模型是靠描述判断要不要用工具的。
// 差
"description": "天气"
// 好
"description": "查询指定城市的实时天气,包括温度、天气状况、湿度和风力"
另一个原因是问题本身不需要工具。"今天天气怎么样"会触发工具,"天气预报是什么意思"不会。
坑 2:arguments 解析失败
arguments 是字符串,不是 JSON 对象。模型偶尔会生成格式不规范的 JSON,比如多余的逗号、单引号代替双引号。
建议加 try-catch,解析失败时记录原始 arguments,方便排查。
坑 3:第二轮忘记带 assistant 消息
第二轮请求的 messages 里,必须包含第一轮模型返回的 assistant 消息(带 tool_calls 的那条)。很多人只加了 tool 消息,忘了 assistant 消息,会报错或得到奇怪的结果。
正确的 messages 顺序:
user → assistant(tool_calls) → tool → [assistant 最终回复]
坑 4:多次工具调用
模型可能在一次响应里返回多个 tool_calls,比如同时查北京和上海的天气。这时候你需要遍历 tool_calls 数组,每个都执行,每个都回传一条 role: tool 消息,tool_call_id 分别对应。
[
{"role": "user", "content": "..."},
{"role": "assistant", "tool_calls": [{"id": "call_1", ...}, {"id": "call_2", ...}]},
{"role": "tool", "tool_call_id": "call_1", "content": "北京天气..."},
{"role": "tool", "tool_call_id": "call_2", "content": "上海天气..."}
]
这和 Agent 有什么关系
Function Calling 就是 Agent 的"手"。
没有 Function Calling,模型只能在语言空间里打转,它能分析、能建议,但做不了任何实际的事。
有了 Function Calling,模型可以:
- 查数据库,拿到实时数据再回答
- 调内部 API,执行业务操作
- 搜索网页,获取最新信息
- 写文件、发邮件、触发工作流
Agent 的核心循环就是:模型决策 → 工具执行 → 结果反馈 → 模型再决策,一直循环直到任务完成。Function Calling 是这个循环的基础设施。
你现在已经掌握了这个循环的完整实现方式。
动手试试
- 把完整示例跑一遍,观察两轮请求和响应的结构
- 加一个
get_time工具,让模型能回答"现在几点了" - 试试
tool_choice: "required",看模型在不需要工具的问题上会怎么处理 - 故意把
description写得很模糊,观察模型是否还能正确判断
下一篇,我们给模型装上记忆——对话上下文管理。模型本身没有记忆,但我们可以用 messages 数组模拟出"记住了"的效果,同时处理上下文窗口超限的问题。
觉得有用的话点个收藏,后续系列持续更新。
本文 API 格式基于百炼 OpenAI 兼容接口文档和百炼 Function Calling 文档,如有变动请以官方最新文档为准。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)