1. 为什么需要理解 tool-call
简单来说,传统的 Web 应用遵循“用户点了按钮——我们调用函数”的范式。在 ChatGPT Apps 的世界中则不同:用户说了些什么,模型思考之后,如果认为有必要,就会生成一次结构化的工具调用(tool-call)。
也就是说你不再编写:
onClick={() => callSuggestGiftsApi(formData)}
而是改为:
- 为工具 suggest_gifts 编写描述(名称、说明、参数模式)。
- 在 system-prompt 中向模型解释该工具的用途。
- 把决策权交给模型:由它自行决定何时以及如何调用。
因此有两点要尽早明确:
- GPT 看不到你的后端代码。它只能看到工具的“抬头”:名称、说明与参数模式。
- 模型能否“聪明地”使用你的 App,几乎直接取决于你写的这些描述。优秀的描述就是你的“工具专用提示词”。
今天这节课讨论的,正是位于用户与服务器之间的这个“中枢”。
2. tool-call 的心智模型:到底发生了什么
先看全局。以 GiftGenius 的典型场景为例:
- 用户:“给一位30岁的朋友挑选礼物,预算100美元,他喜欢电子游戏。”
- GPT 读取该消息并查看有哪些工具。在我们的 App 中,例如有 suggest_gifts。
- GPT 决定:“为了更好地回答,我需要调用这个工具。”
- 它不会返回普通文本,而是生成一段结构:工具名称 + JSON 参数。
- ChatGPT 客户端识别到“这是一次 tool-call”,并把它发送到你的 MCP/服务器。
- 你的服务器执行业务逻辑并返回结构化输出。
- GPT 接收结果、阅读内容,然后基于工具的响应为用户形成清晰的答案,和/或更新小部件。
从 OpenAI API 的角度看,这与 LLM-function-calling 机制相同:模型的回复中不会是普通文本,而是包含工具的 name 和 arguments 的对象,同时 finish_reason 会标记为 tool_calls。模型本身并不会执行代码——它只是“提议”调用哪个工具,实际的调用由客户端(ChatGPT/Apps SDK)完成。
大致流程如下(简化版):
sequenceDiagram
participant U as 用户
participant G as GPT(模型)
participant C as ChatGPT 客户端
participant S as 你的 MCP/Backend
U->>G: "给朋友挑选礼物..."
G->>C: tool-call: { name: "suggest_gifts", args: {...} }
C->>S: HTTP /mcp tools/call (suggest_gifts, args)
S-->>C: 结果(包含礼物列表的 JSON)
C-->>G: 工具结果
G-->>U: 回复 + 已更新的小部件
关键结论:你不再编写 if (userAskedAboutGifts) callSuggestGifts()。你要做的是创建工具及其描述,而由模型来做决策。
3. 模型能看到什么:System Prompt + 工具列表
要理解 GPT 如何做决定,得先清楚它在选择时能获得哪些信息。
简化来看,模型能看到:
- 你的 App 的 system‑prompt(我们会在第 5 个模块详细讲解);
- 对话历史:用户消息、它自己的回复、此前的工具调用结果;
- 可用工具列表(tools),包含它们的名称、描述与参数模式;
- 工具的附加注解(readOnly/destructive 等)。
它看不到:
- 函数实现;
- SQL 查询;
- 你的数据表结构;
- 私有仓库中服务的代码。
稍后我们会详细聊 MCP。此处只需知道,在 MCP 层面,工具以描述符的形式声明:每个工具都有 name、description 和 inputSchema(JSON Schema)。在握手时,ChatGPT 会向 MCP 服务器请求工具列表,并将它们视为可用的“动作”。
GiftGenius 的一个此类描述符示例(简化版 JSON):
{
"name": "suggest_gifts",
"description": "根据年龄、兴趣和预算给出礼物创意",
"inputSchema": {
"type": "object",
"properties": {
"age": { "type": "integer" },
"budget": { "type": "number" }
},
"required": ["age", "budget"]
}
}
模型在这里“读取”的只有文本与结构:age 是什么、budget 是什么、工具整体做什么。下一节我们将专讲如何恰当地编写 inputSchema。现在先关注:模型如何基于这些描述得出“我来调用 suggest_gifts 吧”的决策。
4. 从 API 视角看 tool-call
ChatGPT 调用你的 MCP 服务器中的工具(tools)与 OpenAI Agent 在你的后端调用函数的方式类似。在 ChatGPT Apps SDK 中会再包一层,但基本机制是一样的。
想象我们在后端对 OpenAI API 发起一个普通请求,同时传入工具 suggest_gifts,让模型可以在回复中调用它:
const response = await openai.responses.create({
model: 'gpt-5-mini',
messages: [
{
role: 'user',
content: '需要给一位30岁的朋友准备礼物,预算100美元'
}
],
tools: [ // 在这里我们传入 LLM 可“调用”的函数列表
{
name: 'suggest_gifts',
description: '根据年龄、预算和兴趣推荐礼物',
parameters: {
type: 'object',
properties: {
age: { type: 'integer' },
budget: { type: 'number' }
},
required: ['age', 'budget']
}
}
]
});
如果模型决定调用工具,你会收到的不是文本,而是一个类似这种的 assistant 消息:
{
"role": "assistant",
"tool_calls": [
{
"id": "call_1",
"name": "suggest_gifts",
"arguments": "{\"age\":30,\"budget\":100}"
}
],
"content": []
}
通过这种方式,LLM 告诉你的后端它需要调用函数 suggest_gifts(30,100)。
这里有三点很重要:
- 工具名(name)——模型会把你在 tools 描述中提供的名字原样填入。
- 参数(arguments)——基于 parameters/inputSchema 组装出的 JSON 字符串。
- 暂时没有普通文本回复——你先收到的是一个用来调用工具的结构。
在 ChatGPT 应用中亦然:模型返回“我想以这些参数调用 suggest_gifts”,而客户端(ChatGPT)会向你的 MCP/服务器发起 HTTP 请求:tools/call,附上工具名称与参数。
5. 模型如何决策:调用工具还是文本回答
接下来是最有意思的:GPT 何时会想到你的工具?
简化的机制如下:
- 模型看到用户的最新消息与当前上下文。
- 内部有一个“层”去生成下一条 assistant 消息,但它并不总是产出普通文本,模型可以选择不同的结束方式:
- 普通文本回复(finish_reason: "stop");
- 一次或多次 tool-call(finish_reason: "tool_calls");
- 有时还有其他选项(例如“还需要用户再发一条消息”)。
- 影响这个选择的因素包括:
- 用户请求与工具描述中的任务的相似度;
- 你的工具描述是否明确地说明了“在这种情况下请使用我”;
- Apps SDK 中配置的 app system prompt 所提供的信息。
用更直白的话说,模型会把你的工具与当前请求“对齐匹配”。如果描述是“根据年龄和兴趣挑选礼物”,而用户请求是“分析国家预算”,模型甚至不会尝试调用它。若描述过于模糊——“做很酷的事”——模型就不知道在什么请求下应该用它。
一个有趣的细节是:即便你声明了工具,模型也没有义务一定调用。GPT 可以判断“这里我自己回答就够了,不用 tool‑call”。因此接下来我们会反复练习如何撰写让模型“更愿意、也更应该”调用的工具描述。
6. 工具命名:为什么 tool1 是个坏主意
工具名本质上是一个标识符,模型会在调用中使用它。看似只是技术字段,但实践中名字对模型行为影响很大。
如果你把工具命名为 tool1,模型从中读不出任何含义;它只是字符序列。如果命名为 suggest_gifts、search_products 或 fetch_user_orders,名字本身就传达了强烈的语义信号。
想想你读陌生代码的体验。看到函数 calculateCartTotal,大概就知道它会做什么。模型同样需要这样的“语义锚点”。
对于 GiftGenius,合理的工具名可能是:
suggest_gifts
search_products
get_product_details
create_order
好名字应当:
- 简短但有信息量;
- 风格统一(snake_case、拉丁字母、动词_名词);
- 聚焦一个明确动作。
把多种动作混在一个工具里是个坏主意,比如 do_all_gift_stuff。模型更难理解何时使用它,在后续课程中你会看到这会破坏参数模式并加大调试难度。
7. 工具描述:写给模型的提示词
如果说名字是标题,那么 description 就是“迷你文档”,但不是写给人类开发者,而是写给 GPT 的。开发者能读代码;模型不能。它在选择是否调用工具、以及如何填充参数时,会依赖描述文本。
建议用“使用说明书”的风格来写描述:
- 何时使用该工具;
- 它的限制是什么;
- 它不应该做什么。
以我们的 suggest_gifts 为例,下面给出三种描述。
过于宽泛:
"推荐礼物。"
模型无法理解针对谁、在什么场景、需要哪些参数。这个工具可能会与模型对“礼物”这一常识性知识“竞争”,它往往会直接用文本回答。
过于狭窄:
"只为弟弟的生日推荐礼物。"
这等于把大多数情境都排除了。其他场景——妈妈、同事、纪念日——都“不适用”,模型会回避调用。
较为理想:
"当需要根据年龄、关系类型(朋友、伴侣、同事等)、预算和兴趣为某人挑选礼物时,使用此工具。
非礼物相关的问题(如政治或天气)不要调用该工具。"
这里清晰说明了工具做什么、有哪些参数、何时调用,并增加了否定条件——哪些请求不应调用。
模型“偏爱”这种清晰的边界。你越明确该工具适用于哪些用户表述(意图),App 的行为就越可预测。
小练习
现在就可以拿你正在构思的 App(不一定是礼物主题),为其中一个工具写三段描述:非常宽泛、非常狭窄以及折中平衡。然后测试 GPT 在不同版本下的行为。
8. 参数模式:它如何帮助模型决策
关于 JSON Schema 的细节我们会在下一讲深入讨论,但理解 tool-call 至少需要一个“高层次感觉”。
当模型决定调用工具时,它需要:
- 知道这个工具期望哪些参数。
- 从用户文本(或上下文)中抽取这些值。
- 用这些参数组装出 JSON。
为此,工具描述里的参数模式(inputSchema)会告诉模型:
- 有哪些字段(age、budget、relationship_type、interests 等);
- 哪些字段是必填的(required);
- 字段类型有哪些(integer、number、string、数组等);
- 有时还包括允许值(enum)以及字段说明(description)。
一个最简单的 suggest_gifts 参数 TypeScript 接口可能是:
interface SuggestGiftsParams {
age: number;
relationship_type: 'friend' | 'partner' | 'colleague';
budget: number;
interests?: string[];
}
在模型层面它会变成 JSON Schema,而模型会根据每个字段的名称与描述推断:
- age 应该从“30 岁”“给青少年”等表述中获取;
- budget 从“预算 100 美元”“不超过 50 欧元”等表述中获取;
- relationship_type 从“朋友”“同事”等表述中获取;
- interests 从“喜欢电子游戏”等表述中获取。
如果你提供的模式没有描述,字段名又很抽象(比如 a、b、c),模型在填充参数时会更容易出错。我们会在本课程中关于本地化与 UX 提示的模块再次回到这一点。此处的关键想法是:参数模式不仅是后端校验,更是对模型的提示:什么应该放到哪里。
上面讲的是模式如何帮助模型正确组装参数。除了“怎么调用”,还有“现在是否能调用、是否安全”。这就涉及到工具的权限与元信息。
9. 权限与上下文:并非所有工具随时可用
除了名称、描述和参数模式,工具还有另一个重要维度——安全与访问性。真实 App 中的工具在“危险等级”上差别很大。查询公开商品目录是一回事,从用户卡里扣钱又是另一回事。
Apps SDK 和 MCP 允许在工具描述与注解中反映这些差异——例如将工具标记为 read-only 或 destructive。
核心思路如下:
- 只读取公共数据的工具(search_products、get_weather)可在无需额外确认的情况下调用。
- 会修改数据的工具(create_order、cancel_order、charge_user)应标记为“破坏性”。ChatGPT 的 UI 可能会向用户请求额外确认(“你确定要下单吗?”),而模型在没有明确请求时也会更少主动建议调用。
在后续关于 MCP 的模块里,你会看到这些注解(_meta、destructiveHint、readOnlyHint)在真实 JSON 描述符中的样子、它们如何影响 UX,以及 ChatGPT 在调用前如何生成 “Are you sure?” 确认对话。当前只需理解:
- GPT 不仅考虑描述文本,也会考虑与安全相关的元信息。
- 需要认证的工具,在用户未登录(或 App 未获得所需 token)前不会被使用。
这又是影响“是否调用工具”的一个因素:即使从语义上适用,该工具也可能因为权限不可用而被模型绕过,模型会改走其他路径。
10. 工具如何进入 ChatGPT
从架构上看,工具进入模型的路径主要有两种。
其一,来自你的 ChatGPT App 配置。当你注册 App 时,你会声明与之绑定的 MCP 服务器(及其工具),或应用自身的内置 tools。会话启动时,ChatGPT 会获取这份配置并了解有哪些工具可用。
其二,直接来自 MCP。MCP(Model Context Protocol)定义了客户端(此处是 ChatGPT/Apps SDK)获知你的服务器能力的标准方式:客户端发起 tools/list 请求,获得包含工具描述的 JSON,并将其作为能力缓存。具体机制我们会在 MCP 专题模块中详解,这里把握总体思路即可。
示意如下:
flowchart LR A[ChatGPT Client] -->|handshake| B[MCP Server] B -->|tools/list| A A -->|传递列表| G[GPT Model]
之后,工具列表会成为模型上下文的一部分。如果你在服务器上修改了工具的模式或描述并重启 App,新的描述符会在下一次握手时送达 ChatGPT,模型会据此重新做调用决策。
还有一个重要的实践结论:当你只修改后端(工具的实现),模型并不知道。但当你修改 name/description/schema 时,你实际上在改变 App 的“中枢”。有时改动 description 中的一句话,比写 200 行带启发式的后端代码更有效。
11. 应用于 GiftGenius:打造一个模型愿意调用的工具
现在把上述内容与我们的教学应用 GiftGenius 结合。假设我们已经有一个 MCP 服务器或后端层,在其中注册工具。用 server.registerTool(...) 注册工具 suggest_gifts。
下面是一个 TypeScript 的原型草稿(先不实现真实逻辑):
// pseudo-mcp-server/tools/suggestGifts.ts
server.registerTool(
'suggest_gifts', // 工具名称
{
title: '礼物推荐',
description:
'当需要根据年龄、关系类型与预算来挑选礼物创意时使用该工具;' +
'非礼物相关问题不要调用。',
inputSchema: { // 工具参数的描述
type: 'object',
properties: {
age: { type: 'integer', description: '收礼人的年龄(单位:岁)' },
relationship_type: {
type: 'string',
description: '关系类型:friend, partner, colleague'
},
budget: {
type: 'number',
description: '礼物的最高预算(使用用户的货币)'
}
},
required: ['age', 'budget']
}
},
async ({ age, relationship_type, budget }) => { // 工具/函数的代码
// 实际逻辑稍后再实现
return { suggestions: [] };
}
);
请注意我们在逻辑还是“占位”的阶段就已经考虑的细节:
- 名称:用 suggest_gifts,而非 tool1。
- 描述:明确说明何时调用、何时不应调用。
- 字段描述:帮助模型把用户文本正确映射到参数。
结果是,当用户写下“给同事挑一个 50 美元左右的礼物”时,模型会看到:
- 有一个名为 suggest_gifts 的工具,描述与“挑选礼物”相关;
- 它有字段 age、relationship_type、budget;
- budget 表示“礼物的最高预算”,relationship_type 表示“关系类型:friend、partner、colleague”。
即便用户的表述不够精确(“不超过五十”“项目搭档”),模型也有足够上下文去构造合理的参数 JSON。
当我们的工具在后续(后端与 MCP 模块)真正跑起来时,你已经能得心应手:GPT 会以更可预测的方式调用它,因为我们设计了良好的接口与描述。
12. 给你的一个小实践
为了避免主题停留在理论层面,建议在本讲之后立刻做个小实验。
先从 GiftGenius 的某个场景着手,或设想一个新的 App。写下你想明确交给模型的一项功能——比如 search_products、find_hotels、calculate_shipping。
然后为同一个工具构思三组“名称 + 描述”:
- 非常抽象的名称和描述。
- 过于具体(几乎是特殊情况)的版本。
- 平衡良好的名称 + 描述,清楚写明何时调用、以及它不该做什么。
接着(可选)你可以用常规 OpenAI SDK 做一个简单请求,分别带上这些版本,观察模型行为如何变化:是否会调用工具、如何填充参数。在相关研究中,正是用 suggest_gifts 做了类似练习。
13. 设计 tool-call 与描述时的常见错误
错误 №1:给工具起名 tool1、handler、doStuff。
这类命名对模型毫无帮助。GPT 不会从文件名中“猜测开发者意图”;它需要语义清晰的名称。如果你只给出 tool1、tool2、tool3 且没有描述,工具几乎不会被调用:模型根本搞不清每个工具做什么,要么忽略,要么随机选择。
错误 №2:把 description 当做人类注释。
很多人只写很形式化的一句“用于推荐礼物的函数”,以为细节反正代码里都有。但模型看不到代码;它只能看到描述文本与参数模式。模糊的描述会成为幻觉来源:该调用工具时模型可能自行回答,或在奇怪的场景下调用工具。
错误 №3:描述过宽或过窄。
如果你写“做很酷的事情”,模型就不知道适用边界;如果你写“只为弟弟的 18 岁生日挑礼物”,基本就是把工具禁用了。理想的描述应当明确任务范围(例如按若干参数挑选礼物)、关键参数列表(年龄、关系、预算、兴趣),并说明哪些类别的问题不应使用该工具。
错误 №4:忽视参数模式是“提示词”的一部分。
有的开发者只把 JSON Schema 看作服务端校验手段。实际上,模型会积极利用字段名、类型与描述来判断需要从用户文本中抽取哪些数据。如果你把字段命名为 x 且不给描述,还把它设为可选,GPT 可能会胡乱填充或干脆不填。清晰的模式(清楚的命名与简洁描述)能显著减少无效的 tool-call。
错误 №5:以为模型“必须”调用工具。
有的开发者会困惑:“为什么 GPT 没有调用我的工具,明明它存在?”答案几乎总是:从描述或 system‑prompt 中并不能推导出该场景应调用该工具,或者该请求落在了模型自认为“直接回答更容易”的区域。
错误 №6:把多种不同动作混在一个工具里。
有时会想做个万能的 manage_orders,既查订单、又创建订单、还取消订单。对人来说也许勉强能解释,但对模型来说就是边界不清的工具。GPT 会更难理解何时调用,也更难填充参数——内部会堆满大量可选字段。最好把这些动作拆分成多个窄工具(get_order、create_order、cancel_order),配以清晰的描述与参数模式。
错误 №7:在工具设计中忽视权限与安全。
如果你描述了能做破坏性动作的工具(扣费、删数据),却没有标记为 destructive,也没有在描述中限定使用范围,你就埋下了风险。ChatGPT 的 UI 不会多问确认,而模型也可能在“边界场景”中建议调用。正确的注解与谨慎的描述(“仅在用户明确同意后使用”)可以在 tool‑call 层面就降低风险。
GO TO FULL VERSION