本讲解决三个问题:
- 模型怎么知道有哪些工具?
- 模型怎样表达"我要调用这个工具"?
- 程序怎么把这份表达变成真正的执行?
仍然使用订单查询作为贯穿案例,再对应到仓库中的 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
参数类型:字符串
不允许额外参数
请判断以下三个场景,并说明检查应发生在哪里:
-
模型返回的参数是:
{"order_id": 102}它能通过 JSON 解析吗?能通过本讲的参数结构校验吗?
答:不能 因为不符合参数类型的标准 需要字符串 这个是int -
模型返回工具名
query_order,但本地注册表只有:tool_registry = { "get_order": query_order }程序能直接通过
tool_registry[name]找到函数吗?模型是否会自动知道这两个名字实际想表达同一个能力?
答:不能 因为需要的是tool_registryget_order, 肯定不知道 -
参数是合法的
{"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。权限不通过,就不能把订单详情返回给模型或用户。
实际实现可以直接在查询条件中限制用户范围,也可以通过业务权限逻辑检查;不应只依赖提示词要求模型"不要查询别人的订单"。