第 1 周·第 3 讲|工具如何交给模型:工具定义、参数与结构化调用

本讲解决三个问题:

  1. 模型怎么知道有哪些工具?
  2. 模型怎样表达"我要调用这个工具"?
  3. 程序怎么把这份表达变成真正的执行?

仍然使用订单查询作为贯穿案例,再对应到仓库中的 web_search 实现。

一、本地有一个函数,模型就会使用它吗?

假设你的 Python 程序中有:

复制代码
def query_order(order_id):
    ...

它能根据订单编号查询订单。

但是,远程模型不会因为你的文件里出现了这个函数,就自动知道:

  • 函数叫什么;
  • 用于解决什么问题;
  • 接受哪些参数;
  • 什么时候应该使用;
  • 怎样让你的程序执行它。

因此,需要先把函数的使用方式整理成工具定义,随模型请求发送。

整个过程是:

复制代码
程序准备工具定义
        ↓
模型读取定义,生成调用请求
        ↓
程序检查调用请求
        ↓
找到对应实现并执行
        ↓
把结果送回模型

工具定义解决的是"让模型知道怎么请求使用",工具实现解决的是"实际把事情做出来"。

二、先分清四个容易混在一起的对象

对象 内容 订单案例
工具定义 名称、用途、参数规则 query_order 用来查询订单,必须提供字符串订单编号
调用请求 本次调用的工具名和具体参数 查询订单 A102
工具实现 实际执行操作的代码或服务 向订单数据库或订单服务发起查询
工具结果 执行后得到的信息 A102 已发货,或者查询失败

这四者的关系,可以类比普通函数:

复制代码
# 函数定义
def query_order(order_id):
    ...

# 具体调用
result = query_order("A102")

# 返回结果
print(result)

Agent 工具调用多了一层:

具体调用的名称与参数由模型生成,再由程序识别和执行。

模型生成的请求必须先被当作数据处理,不能直接当成任意 Python 代码运行。

三、工具定义具体长什么样?

下面是采用本课接口风格的教学示例:

复制代码
{
  "type": "function",
  "function": {
    "name": "query_order",
    "description": "根据订单编号查询当前订单状态,不修改订单。",
    "parameters": {
      "type": "object",
      "properties": {
        "order_id": {
          "type": "string",
          "description": "要查询的订单编号,例如 A102"
        }
      },
      "required": ["order_id"],
      "additionalProperties": false
    }
  }
}

先不要被层数吓住,它主要说明三件事:

复制代码
叫什么:query_order
做什么:查询订单状态,不修改订单
怎么传参:提供 order_id,类型必须是字符串

逐层解释如下。

type: "function"

表示这里声明的是函数工具。

它是一项协议字段,不表示模型收到了 Python 函数对象。

name

工具的机器名称。模型生成调用请求时,会使用这个名称:

复制代码
query_order

程序也需要用它识别应该执行哪个工具。

description

告诉模型工具有什么用途,以及重要边界。

例如:

复制代码
根据订单编号查询当前订单状态,不修改订单。

比单纯写"订单工具"更清楚,因为后者无法区分查询、取消、退款等操作。

parameters

描述调用参数的结构规则。

这里采用 JSON Schema------JSON 数据结构的描述与校验规则。

它相当于一份参数契约。

四、Schema 中的字段分别控制什么?

看参数定义:

复制代码
{
  "type": "object",
  "properties": {
    "order_id": {
      "type": "string"
    }
  },
  "required": ["order_id"],
  "additionalProperties": false
}
字段 含义
"type": "object" 参数整体应是一个 JSON 对象,例如 {"order_id": "A102"}
properties 声明允许的参数及其规则
"type": "string" order_id 的值必须是字符串
required 列出的参数必须存在
additionalProperties: false 不允许出现未声明的额外参数

按这个定义:

复制代码
{"order_id": "A102"}

符合结构要求。

下面这个缺少必填参数:

复制代码
{}

下面这个参数类型不符合要求:

复制代码
{"order_id": 102}

下面这个多了未声明的参数:

复制代码
{"order_id": "A102", "cancel": true}

还有一个细节:

required 只要求字段存在,不自动保证字符串非空、订单存在或当前用户有权访问。

如果要限制字符串长度,需要进一步定义;如果要确认订单存在,需要查询业务系统。

Schema 能表达和约束一部分结构,但不能代替所有业务判断

五、工具定义怎样进入模型请求?

在本课使用的接口中,工具定义通过 tools 参数发送:

复制代码
tools = [query_order_definition]

completion = client.chat.completions.create(
    model=model_name,
    messages=messages,
    tools=tools,
)

这里:

复制代码
messages

主要组织本次任务与已有信息。

复制代码
tools

提供可请求使用的工具定义。

虽然工具定义通常以独立请求字段传入,但它同样是模型做决策时可以利用的信息。

发送工具定义,不等于执行工具,也不等于修改模型参数。

模型可能根据任务选择调用工具,也可能直接回答;具体还受接口配置和模型能力影响。不能因为请求中有 tools,就断言一定会发生工具调用。

六、模型生成的调用请求是什么样?

假设用户说:

查询订单 A102。

模型可能返回如下结构。这里是教学示例,不是真实调用结果:

复制代码
{
  "id": "call_01",
  "type": "function",
  "function": {
    "name": "query_order",
    "arguments": "{\"order_id\":\"A102\"}"
  }
}

与工具定义对照:

复制代码
工具定义:
query_order 可以接收什么参数

调用请求:
这一次 query_order 的参数是 A102

其中:

  • id:标识这一次调用;
  • name:请求使用哪个工具;
  • arguments:本次调用参数。

为什么需要调用 ID?

因为一次任务可能多次调用同一个工具:

复制代码
call_01:查询 A102
call_02:查询 B208

只写工具名不足以区分"这份结果属于哪次查询"。后面回填结果时,需要对应具体调用。

七、为什么 arguments 看起来是一段字符串?

这是本课接口中需要注意的一个表示方式:

复制代码
"arguments": "{\"order_id\":\"A102\"}"

外层的调用请求是结构化数据,但 arguments 字段的值是包含 JSON 文本的字符串。

其中 \" 表示字符串内部的双引号。

程序取得这个字段后,得到的文本是:

复制代码
{"order_id":"A102"}

在 Python 中可以这样表示:

复制代码
raw_arguments = '{"order_id":"A102"}'

现在它还是字符串,不能直接按字典使用。需要解析:

复制代码
import json

arguments = json.loads(raw_arguments)

解析后:

复制代码
arguments == {"order_id": "A102"}

这时才能取值:

复制代码
order_id = arguments["order_id"]

对应关系是:

复制代码
JSON 字符串
    ↓ json.loads()
Python 字典
    ↓ 按键取值
具体参数

反过来:

复制代码
json.dumps(arguments)

是将 Python 数据结构转换成 JSON 字符串。

但必须区分:

JSON 能解析成功,只说明文本格式可解析,不说明它符合工具参数规则。

例如:

复制代码
json.loads('{"order_id": 102}')

可以成功解析,但得到的订单编号是整数,不符合前面的字符串要求。

甚至:

复制代码
json.loads('["A102"]')

也能成功解析,但结果是列表,而工具要求的参数整体是对象。

八、工具名称怎样对应到真正的 Python 函数?

模型返回:

复制代码
query_order

这只是一个字符串。程序必须明确建立名称与实现之间的关系。

一种常见方式是工具注册表:

复制代码
tool_registry = {
    "query_order": query_order
}

这里左右两边不同:

复制代码
左边 "query_order":字符串,工具名称
右边 query_order:函数对象

注意右边没有括号,因此这里没有执行函数。

复制代码
query_order

表示引用这个函数。

复制代码
query_order("A102")

才表示调用这个函数。

当调用请求通过参数和权限检查之后,程序可以执行:

复制代码
handler = tool_registry[name]
result = handler(**arguments)

假设:

复制代码
name = "query_order"
arguments = {"order_id": "A102"}

那么过程可以展开为:

复制代码
handler = query_order
result = query_order(order_id="A102")

这里的 **arguments,就是上一讲学过的字典展开:

复制代码
handler(**{"order_id": "A102"})

相当于:

复制代码
handler(order_id="A102")

工具名称不会自动变成函数调用,中间需要程序完成映射。

注册表也限定了可执行范围。遇到未登记的名称,应进入错误处理,而不是尝试执行模型给出的任意代码。

工具实现不一定是本地函数。这个映射也可以指向一个适配器,由它再请求远程服务。

九、执行前到底要检查什么?

至少要分清三层:

层次 检查问题 示例
JSON 解析 文本能否解析成数据? 引号或括号是否正确
参数结构校验 数据是否符合工具契约? 是否有 order_id,是否为字符串
业务与权限校验 这次操作是否允许、是否满足条件? 订单是否属于当前用户

这三层不能相互替代。

例如:

复制代码
{"order_id": "A102"}

可能格式正确、结构正确,但 A102 属于另一个用户。

因此,不能仅因为模型生成了符合 Schema 的参数,就允许查询。

身份信息通常应来自可信的登录上下文等来源,不能让模型随意填写一个 user_id 就获得相应权限。

不同服务对 Schema 的支持和输出约束能力不同,实际执行层仍要落实必要检查。

十、回到 agent.py:当前仓库实际用了哪种方式?

刚才的订单注册表示例用于解释本地执行。当前仓库中的 web_search 则走远程服务路径。

这次已核对的源码可以分成四段。

第一段:获取工具定义。

在 \`_get_tools()\` (line 169)(/D:/learn/ai-agent-book/chapter1/web-search-agent/agent.py:169) 中:

复制代码
response = requests.get(...)
payload = response.json()
tools = payload.get("tools")

程序向 Formula 服务获取工具定义,再检查返回的工具列表中是否包含名为 web_search 的函数工具。

成功后:

复制代码
self._formula_tools = tools
return tools

将定义保存下来,供同一任务后续调用复用。

这里的网络请求是在获取工具说明,还不是执行搜索。

第二段:把定义交给模型。

在 _chat() 中:

复制代码
tools = self._get_tools()

if tools:
    kwargs["tools"] = tools

随后发起模型请求。模型这时才有机会根据任务和工具说明生成调用请求。

第三段:读取调用名称与参数。

在 \`search_and_answer()\` 的工具分支 (line 433)(/D:/learn/ai-agent-book/chapter1/web-search-agent/agent.py:433) 中:

复制代码
tool_call_name = tool_call.function.name

tool_call_arguments = json.loads(
    tool_call.function.arguments or "{}"
)

这里:

  • tool_call.function.name 取出工具名;
  • tool_call.function.arguments 取出原始参数字符串;
  • json.loads(...) 得到便于查看的参数数据。

or "{}" 表示:如果原始参数是空字符串或 None 等假值,就使用空对象的 JSON 文本。

它不能修复任意损坏的 JSON。

第四段:将请求交给远程执行器。

当前代码识别到 web_search 后:

复制代码
tool_result = self._execute_formula(
    tool_call_name,
    tool_call.function.arguments or "{}",
)

这里有一个与本地注册表示例不同的地方:

当前代码将原始参数字符串发送给远程 Formula 执行接口;解析后的参数主要用于可读轨迹。

在 \`_execute_formula()\` (line 232)(/D:/learn/ai-agent-book/chapter1/web-search-agent/agent.py:232) 中:

复制代码
body = {
    "name": name,
    "arguments": raw_arguments
}

response = requests.post(
    url,
    headers=...,
    json=body,
    timeout=...,
)

程序把工具名和参数交给远程服务,由服务执行搜索。

之后它还检查:

复制代码
payload.get("status") == "succeeded"

并读取输出。仅收到 HTTP 成功响应,还不足以说明工具执行成功。

所以当前仓库的实际链路是:

复制代码
获取远程工具定义
        ↓
把定义交给模型
        ↓
模型返回 web_search 调用请求
        ↓
本地程序识别名称并转交远程执行接口
        ↓
远程服务返回执行结果

源码中对无效 JSON 有降级成空字典来记录轨迹的处理,但执行分支仍转发原始字符串。这不是完整的参数校验,也不能说明无效参数已经被修复。 阅读实现时,要把"记录方便"和"执行正确"分开判断。

十一、本讲最容易混淆的地方

  • 工具定义不是函数源码。 模型通常只需要知道用途和调用规则。
  • 写了函数,不等于模型知道它存在。 需要将能力定义交给模型。
  • 模型返回工具名称,不等于函数自动执行。 需要程序映射和调度。
  • JSON 解析成功,不等于参数合规。 还要检查结构、类型和业务条件。
  • 参数合规,不等于有权执行。 权限来自可信系统规则。
  • 声明工具和执行工具是两个动作。 当前仓库甚至用不同的服务请求完成它们。
  • 工具名与调用 ID 不同。 工具名识别能力,调用 ID 识别某一次使用。

本讲集中练习

仍使用本讲的 query_order 定义:

复制代码
工具名:query_order
必填参数:order_id
参数类型:字符串
不允许额外参数

请判断以下三个场景,并说明检查应发生在哪里:

  1. 模型返回的参数是:

    复制代码
    {"order_id": 102}

    它能通过 JSON 解析吗?能通过本讲的参数结构校验吗?
    答:不能 因为不符合参数类型的标准 需要字符串 这个是int

  2. 模型返回工具名 query_order,但本地注册表只有:

    复制代码
    tool_registry = {
        "get_order": query_order
    }

    程序能直接通过 tool_registry[name] 找到函数吗?模型是否会自动知道这两个名字实际想表达同一个能力?
    答:不能 因为需要的是tool_registryget_order, 肯定不知道

  3. 参数是合法的 {"order_id": "A102"},但业务系统查明该订单属于其他用户。能否因为参数合规就继续查询并返回订单详情?应该由哪一层处理?

答:不能 应该是由程序或者说是后端用户校验这里就卡死

AI修改:

第 2、3 题判断正确。第 1 题中,你正确发现了参数类型不符,但需要把"JSON 能否解析"和"工具是否接受这个参数"分开。

1. {"order_id": 102}:能解析,但不符合本工具的参数规则

JSON 本身允许数字,因此:

复制代码
import json

arguments = json.loads('{"order_id": 102}')

可以成功执行,得到:

复制代码
{"order_id": 102}

其中 102 在 Python 中是整数。

json.loads() 负责理解 JSON 文本,它不知道你的工具要求订单编号必须是字符串。这项要求来自工具的 Schema,需要另外检查。

可以把两步理解成:

复制代码
JSON 解析:
这段文字是否符合 JSON 的语法,能转换成数据?

Schema 校验:
转换出来的数据,是否符合这个工具的参数要求?

对比四种情况:

参数文本 JSON 解析 本工具的 Schema 校验
{"order_id": "A102"} 通过 通过
{"order_id": 102} 通过 不通过:要求字符串
{"other_id": "A102"} 通过 不通过:缺少必填字段,并包含额外字段
{"order_id": } 不通过:冒号后缺少值 尚未进入这一步

所以第 1 题的准确答案是:

能通过 JSON 解析;不能通过参数结构校验,因为 order_id 必须是字符串,而这里是整数。

这里不是让你推翻"类型不对"的判断,而是把这个判断放到正确的处理阶段。


2. 名称不一致:你的判断正确

注册表是:

复制代码
tool_registry = {
    "get_order": query_order
}

它表达的是:

复制代码
字符串键 "get_order"
        ↓
对应函数对象 query_order

当:

复制代码
name = "query_order"

执行:

复制代码
tool_registry[name]

就是查找:

复制代码
tool_registry["query_order"]

这个键不存在,直接索引会产生 KeyError。

你写的查找方式需要补上引号:

复制代码
tool_registry["get_order"]

如果没有引号:

复制代码
tool_registry[get_order]

Python 会把 get_order 当作变量名,而不是字符串。

另外,模型可能从语义上猜测两个名称相关,但程序的字典查找不会自动建立别名关系。工具对外名称与程序映射需要明确保持一致。


3. 订单属于其他用户:你的判断正确

你说"由程序或者后端用户校验卡住",责任位置正确。更精确地说,是后端的资源权限校验。

这里还要区分:

检查 回答的问题
身份认证 你是谁?是否已登录?
资源授权 你能不能访问这一个订单?

用户已经登录,不代表可以查看所有订单。

因此,即使参数完全符合 Schema:

复制代码
{"order_id": "A102"}

后端也必须根据可信的当前用户身份,检查其是否有权访问 A102。权限不通过,就不能把订单详情返回给模型或用户。

实际实现可以直接在查询条件中限制用户范围,也可以通过业务权限逻辑检查;不应只依赖提示词要求模型"不要查询别人的订单"。

相关推荐
Yyyyyy~1 小时前
[Mysql] 数据类型
数据库·mysql
老歌老听老掉牙1 小时前
两个平面旋转平移坐标系间的坐标变换关系
python·算法·平面·旋转·平移
W.A委员会1 小时前
PID与PID整定
python·算法
slacker-kian1 小时前
SAP On-Premise 部署环境下 ABAP 开发对接 AI Agent 方案探讨
人工智能·ai·sap·agent·abap·mcp·odata
需要8261 小时前
缓存与数据库一致性:延迟双删与订阅 binlog 的取舍
数据库·缓存
蜗牛互联网1 小时前
Java HttpClient 调用 Gemini 图像理解与金额字段校验
java·开发语言·人工智能·后端·python
蜗牛互联网1 小时前
Python Responses API视觉输入与本地金额校验最小实现
java·开发语言·人工智能·后端·python
Python图像识别1 小时前
35-【2027毕设】YOLO11中草药检测识别系统 - Python完整源码+PyQt5界面+训练模型+数据集
python·qt·课程设计
马六六i1 小时前
市面上专业的IP驱动产业新场景新工具哪家好
网络·人工智能·python·tcp/ip