CodeGym /课程 /ChatGPT Apps /GPT 如何决定调用工具:tool-call 模型与描述的作用

GPT 如何决定调用工具:tool-call 模型与描述的作用

ChatGPT Apps
第 4 级 , 课程 0
可用

1. 为什么需要理解 tool-call

简单来说,传统的 Web 应用遵循“用户点了按钮——我们调用函数”的范式。在 ChatGPT Apps 的世界中则不同:用户说了些什么,模型思考之后,如果认为有必要,就会生成一次结构化的工具调用(tool-call)。

也就是说你不再编写

onClick={() => callSuggestGiftsApi(formData)}

而是改为:

  1. 为工具 suggest_gifts 编写描述(名称、说明、参数模式)。
  2. system-prompt 中向模型解释该工具的用途。
  3. 把决策权交给模型:由它自行决定何时以及如何调用。

因此有两点要尽早明确:

  1. GPT 看不到你的后端代码。它只能看到工具的“抬头”:名称、说明与参数模式。
  2. 模型能否“聪明地”使用你的 App,几乎直接取决于你写的这些描述。优秀的描述就是你的“工具专用提示词”。

今天这节课讨论的,正是位于用户与服务器之间的这个“中枢”。

2. tool-call 的心智模型:到底发生了什么

先看全局。以 GiftGenius 的典型场景为例:

  1. 用户:“给一位30岁的朋友挑选礼物,预算100美元,他喜欢电子游戏。”
  2. GPT 读取该消息并查看有哪些工具。在我们的 App 中,例如有 suggest_gifts
  3. GPT 决定:“为了更好地回答,我需要调用这个工具。”
  4. 它不会返回普通文本,而是生成一段结构:工具名称 + JSON 参数。
  5. ChatGPT 客户端识别到“这是一次 tool-call”,并把它发送到你的 MCP/服务器。
  6. 你的服务器执行业务逻辑并返回结构化输出。
  7. GPT 接收结果、阅读内容,然后基于工具的响应为用户形成清晰的答案,和/或更新小部件。

从 OpenAI API 的角度看,这与 LLM-function-calling 机制相同:模型的回复中不会是普通文本,而是包含工具的 namearguments 的对象,同时 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 层面,工具以描述符的形式声明:每个工具都有 namedescriptioninputSchema(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)

这里有三点很重要:

  1. 工具名(name)——模型会把你在 tools 描述中提供的名字原样填入。
  2. 参数(arguments)——基于 parameters/inputSchema 组装出的 JSON 字符串。
  3. 暂时没有普通文本回复——你先收到的是一个用来调用工具的结构。

在 ChatGPT 应用中亦然:模型返回“我想以这些参数调用 suggest_gifts”,而客户端(ChatGPT)会向你的 MCP/服务器发起 HTTP 请求:tools/call,附上工具名称与参数。

5. 模型如何决策:调用工具还是文本回答

接下来是最有意思的:GPT 何时会想到你的工具?

简化的机制如下:

  1. 模型看到用户的最新消息与当前上下文。
  2. 内部有一个“层”去生成下一条 assistant 消息,但它并不总是产出普通文本,模型可以选择不同的结束方式:
    • 普通文本回复(finish_reason: "stop");
    • 一次或多次 tool-callfinish_reason: "tool_calls");
    • 有时还有其他选项(例如“还需要用户再发一条消息”)。
  3. 影响这个选择的因素包括:
    • 用户请求与工具描述中的任务的相似度;
    • 你的工具描述是否明确地说明了“在这种情况下请使用我”;
    • Apps SDK 中配置的 app system prompt 所提供的信息。

用更直白的话说,模型会把你的工具与当前请求“对齐匹配”。如果描述是“根据年龄和兴趣挑选礼物”,而用户请求是“分析国家预算”,模型甚至不会尝试调用它。若描述过于模糊——“做很酷的事”——模型就不知道在什么请求下应该用它。

一个有趣的细节是:即便你声明了工具,模型也没有义务一定调用。GPT 可以判断“这里我自己回答就够了,不用 tool‑call”。因此接下来我们会反复练习如何撰写让模型“更愿意、也更应该”调用的工具描述。

6. 工具命名:为什么 tool1 是个坏主意

工具名本质上是一个标识符,模型会在调用中使用它。看似只是技术字段,但实践中名字对模型行为影响很大。

如果你把工具命名为 tool1,模型从中读不出任何含义;它只是字符序列。如果命名为 suggest_giftssearch_productsfetch_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 至少需要一个“高层次感觉”。

当模型决定调用工具时,它需要:

  1. 知道这个工具期望哪些参数。
  2. 从用户文本(或上下文)中抽取这些值。
  3. 用这些参数组装出 JSON。

为此,工具描述里的参数模式(inputSchema)会告诉模型:

  • 有哪些字段(agebudgetrelationship_typeinterests 等);
  • 哪些字段是必填的(required);
  • 字段类型有哪些(integernumberstring、数组等);
  • 有时还包括允许值(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 从“喜欢电子游戏”等表述中获取。

如果你提供的模式没有描述,字段名又很抽象(比如 abc),模型在填充参数时会更容易出错。我们会在本课程中关于本地化与 UX 提示的模块再次回到这一点。此处的关键想法是:参数模式不仅是后端校验,更是对模型的提示:什么应该放到哪里

上面讲的是模式如何帮助模型正确组装参数。除了“怎么调用”,还有“现在是否能调用、是否安全”。这就涉及到工具的权限与元信息。

9. 权限与上下文:并非所有工具随时可用

除了名称、描述和参数模式,工具还有另一个重要维度——安全与访问性。真实 App 中的工具在“危险等级”上差别很大。查询公开商品目录是一回事,从用户卡里扣钱又是另一回事。

Apps SDK 和 MCP 允许在工具描述与注解中反映这些差异——例如将工具标记为 read-onlydestructive

核心思路如下:

  • 只读取公共数据的工具(search_productsget_weather)可在无需额外确认的情况下调用。
  • 会修改数据的工具(create_ordercancel_ordercharge_user)应标记为“破坏性”。ChatGPT 的 UI 可能会向用户请求额外确认(“你确定要下单吗?”),而模型在没有明确请求时也会更少主动建议调用。

在后续关于 MCP 的模块里,你会看到这些注解(_metadestructiveHintreadOnlyHint)在真实 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 的工具,描述与“挑选礼物”相关;
  • 它有字段 agerelationship_typebudget
  • budget 表示“礼物的最高预算”,relationship_type 表示“关系类型:friend、partner、colleague”。

即便用户的表述不够精确(“不超过五十”“项目搭档”),模型也有足够上下文去构造合理的参数 JSON。

当我们的工具在后续(后端与 MCP 模块)真正跑起来时,你已经能得心应手:GPT 会以更可预测的方式调用它,因为我们设计了良好的接口与描述。

12. 给你的一个小实践

为了避免主题停留在理论层面,建议在本讲之后立刻做个小实验。

先从 GiftGenius 的某个场景着手,或设想一个新的 App。写下你想明确交给模型的一项功能——比如 search_productsfind_hotelscalculate_shipping

然后为同一个工具构思三组“名称 + 描述”:

  1. 非常抽象的名称和描述。
  2. 过于具体(几乎是特殊情况)的版本。
  3. 平衡良好的名称 + 描述,清楚写明何时调用、以及它不该做什么。

接着(可选)你可以用常规 OpenAI SDK 做一个简单请求,分别带上这些版本,观察模型行为如何变化:是否会调用工具、如何填充参数。在相关研究中,正是用 suggest_gifts 做了类似练习。

13. 设计 tool-call 与描述时的常见错误

错误 №1:给工具起名 tool1handlerdoStuff
这类命名对模型毫无帮助。GPT 不会从文件名中“猜测开发者意图”;它需要语义清晰的名称。如果你只给出 tool1tool2tool3 且没有描述,工具几乎不会被调用:模型根本搞不清每个工具做什么,要么忽略,要么随机选择。

错误 №2:把 description 当做人类注释。
很多人只写很形式化的一句“用于推荐礼物的函数”,以为细节反正代码里都有。但模型看不到代码;它只能看到描述文本与参数模式。模糊的描述会成为幻觉来源:该调用工具时模型可能自行回答,或在奇怪的场景下调用工具。

错误 №3:描述过宽或过窄。
如果你写“做很酷的事情”,模型就不知道适用边界;如果你写“只为弟弟的 18 岁生日挑礼物”,基本就是把工具禁用了。理想的描述应当明确任务范围(例如按若干参数挑选礼物)、关键参数列表(年龄、关系、预算、兴趣),并说明哪些类别的问题不应使用该工具。

错误 №4:忽视参数模式是“提示词”的一部分。
有的开发者只把 JSON Schema 看作服务端校验手段。实际上,模型会积极利用字段名、类型与描述来判断需要从用户文本中抽取哪些数据。如果你把字段命名为 x 且不给描述,还把它设为可选,GPT 可能会胡乱填充或干脆不填。清晰的模式(清楚的命名与简洁描述)能显著减少无效的 tool-call

错误 №5:以为模型“必须”调用工具。
有的开发者会困惑:“为什么 GPT 没有调用我的工具,明明它存在?”答案几乎总是:从描述或 system‑prompt 中并不能推导出该场景应调用该工具,或者该请求落在了模型自认为“直接回答更容易”的区域。

错误 №6:把多种不同动作混在一个工具里。
有时会想做个万能的 manage_orders,既查订单、又创建订单、还取消订单。对人来说也许勉强能解释,但对模型来说就是边界不清的工具。GPT 会更难理解何时调用,也更难填充参数——内部会堆满大量可选字段。最好把这些动作拆分成多个窄工具(get_ordercreate_ordercancel_order),配以清晰的描述与参数模式。

错误 №7:在工具设计中忽视权限与安全。
如果你描述了能做破坏性动作的工具(扣费、删数据),却没有标记为 destructive,也没有在描述中限定使用范围,你就埋下了风险。ChatGPT 的 UI 不会多问确认,而模型也可能在“边界场景”中建议调用。正确的注解与谨慎的描述(“仅在用户明确同意后使用”)可以在 tool‑call 层面就降低风险。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION