Agent工具越加越慢?用Tool Search按需加载函数定义
凌晨一点,一个订单Agent的测试结果突然开始变得奇怪。
用户只是问了一句:
“帮我查一下客户CUST-1001还有哪些未完成订单。”
Agent却先后尝试了退款、物流、发票和会员积分工具,最后才选中订单查询函数。请求虽然完成了,但输入Token明显增加,首次响应也比原来慢。
检查代码后发现,业务侧已经给这个Agent配置了七十多个函数。每次发起请求,所有函数的名称、说明和JSON参数Schema都会一起提交给模型。
这正是工具型Agent进入工程阶段后很容易遇到的问题:
• 工具数量越来越多;
• 每个工具的参数定义越来越长;
• 大部分工具在当前任务中根本用不到;
• 模型需要从大量相似工具中完成选择;
• 工具定义占用上下文,影响Token、缓存和延迟。
解决办法并不是简单删除工具,而是改变工具进入上下文的方式。
一、工具越多,为什么请求会越来越重?
Function Calling中的工具并不只是一个函数名。一个完整定义通常包括:
{
"type": "function",
"name": "list_open_orders",
"description": "查询指定客户尚未完成的订单",
"parameters": {
"type": "object",
"properties": {
"customer_id": {
"type": "string",
"description": "客户唯一编号"
},
"status": {
"type": "string",
"enum": ["pending", "processing", "shipped"]
}
},
"required": ["customer_id"],
"additionalProperties": false
}
}
如果一个Agent只有5个工具,这点开销通常不明显。
当工具增加到50个、100个,甚至接入多个MCP Server后,模型在看到用户问题之前,还要先读取大量工具描述和参数结构。
传统方式的上下文大致如下:
系统指令
+ 工具1的完整Schema
+ 工具2的完整Schema
+ 工具3的完整Schema
...
+ 工具100的完整Schema
+ 用户问题
这里至少会产生三个工程问题。
- 输入Token增加
即使某个请求只需要调用一个订单查询工具,其他几十个工具的定义也可能进入输入上下文。
- 相似工具更容易被混淆
例如下面几个函数:
get_order
get_order_detail
search_orders
list_open_orders
get_order_status
如果描述不够准确,模型可能选错工具,或者需要额外推理才能判断。
- 修改工具可能影响缓存
工具定义属于请求上下文的一部分。频繁调整工具集合、顺序或Schema,可能降低稳定复用缓存的效果。
Tool Search的思路,就是先让模型看到“工具目录”,等确定需要某一类能力时,再载入对应工具的完整定义。
根据OpenAI当前文档,Tool Search支持延迟加载Function、命名空间或MCP Server;目前需要使用gpt-5.4及后续支持该能力的模型。具体兼容范围应以Tool Search官方文档为准:
https://developers.openai.com/api/docs/guides/tools-tool-search
二、Tool Search不是执行工具,而是搜索工具
这两个步骤很容易混淆。
一次完整流程实际上包含:
用户提出任务
↓
模型判断需要哪类工具
↓
Tool Search加载相关工具定义
↓
模型发出Function Call
↓
应用程序执行本地函数
↓
把执行结果交回模型
↓
模型生成最终回答
Tool Search解决的是“该把哪些工具定义放入上下文”,并不会替你执行业务函数。
例如用户要查询订单时:
- 模型先搜索订单相关命名空间;
- API把list_open_orders载入上下文;
- 模型生成该函数的调用参数;
- 你的Python程序查询订单系统;
- 程序将查询结果传回Responses API。
这个边界非常重要。数据库权限、业务鉴权、参数检查和真实执行仍然由开发者控制。
三、准备Python环境
示例环境:
• Python 3.10及以上;
• 当前版本OpenAI Python SDK;
• 支持Tool Search的模型;
• 已配置API Key。
安装依赖:
python -m pip install -U openai
Linux或macOS设置环境变量:
export OPENAI_API_KEY="你的API_Key"
Windows PowerShell:
$env:OPENAI_API_KEY="你的API_Key"
不要把真实API Key直接写进源码,也不要提交到Git仓库。
四、先建立一个订单工具命名空间
下面模拟一个CRM Agent。为了让示例能够直接阅读和修改,订单数据暂时放在内存中。
import json
from openai import OpenAI
client = OpenAI()
ORDERS = {
"CUST-1001": [
{"order_id": "ORD-9001", "status": "processing"},
{"order_id": "ORD-9002", "status": "shipped"},
],
"CUST-1002": [
{"order_id": "ORD-9010", "status": "pending"},
],
}
def list_open_orders(customer_id: str) -> dict:
"""返回客户尚未完成的订单。"""
orders = ORDERS.get(customer_id, [])
unfinished = [
order for order in orders
if order["status"] in {"pending", "processing"}
]
return {
"customer_id": customer_id,
"orders": unfinished,
"count": len(unfinished),
}
crm_namespace = {
"type": "namespace",
"name": "crm",
"description": "客户资料、订单查询和售后处理相关工具。",
"tools": [
{
"type": "function",
"name": "list_open_orders",
"description": "根据客户编号查询尚未完成的订单。",
"defer_loading": True,
"parameters": {
"type": "object",
"properties": {
"customer_id": {
"type": "string",
"description": "客户编号,例如CUST-1001",
}
},
"required": ["customer_id"],
"additionalProperties": False,
},
}
],
}
这里最关键的是:
"defer_loading": True
它应该写在需要延迟加载的Function中,而不是写在命名空间对象上。
然后在请求的tools数组中增加:
{"type": "tool_search"}
两者缺一不可。
五、发送一次支持Tool Search的请求
response = client.responses.create(
model="gpt-5.6",
input="查询客户CUST-1001尚未完成的订单,并用中文简要回答。",
tools=[
crm_namespace,
{"type": "tool_search"},
],
parallel_tool_calls=False,
)
for item in response.output:
print(item.type, item)
如果模型判断订单命名空间与当前任务相关,返回结果中通常会出现这些项目:
tool_search_call
tool_search_output
function_call
它们分别表示:
输出类型:tool_search_call
含义:模型发起工具搜索
输出类型:tool_search_output
含义:API返回已加载的工具定义
输出类型:function_call
含义:模型决定调用具体业务函数
输出类型:function_call_output
含义:应用程序返回函数执行结果
Hosted Tool Search由API在服务端完成工具搜索。开发者仍然要处理后续的function_call,否则程序只会停在“准备调用函数”阶段。
六、补齐Function Call执行闭环
下面加入一个安全的函数分发器。
def dispatch_function(namespace: str, name: str, arguments: str) -> str:
args = json.loads(arguments)
if namespace == "crm" and name == "list_open_orders":
result = list_open_orders(
customer_id=args["customer_id"]
)
return json.dumps(result, ensure_ascii=False)
raise ValueError(f"不允许调用的工具:{namespace}.{name}")
不要根据模型返回的函数名使用eval(),也不要动态执行未经验证的Python代码。白名单分发更容易控制权限。
处理第一次响应:
tool_outputs = []
for item in response.output:
if item.type != "function_call":
continue
try:
output = dispatch_function(
namespace=item.namespace,
name=item.name,
arguments=item.arguments,
)
except (KeyError, ValueError, json.JSONDecodeError) as exc:
output = json.dumps(
{"error": str(exc)},
ensure_ascii=False,
)
tool_outputs.append(
{
"type": "function_call_output",
"call_id": item.call_id,
"output": output,
}
)
把执行结果交回模型:
if not tool_outputs:
print(response.output_text)
else:
final_response = client.responses.create(
model="gpt-5.6",
previous_response_id=response.id,
input=tool_outputs,
)
print(final_response.output_text)
在模拟数据下,最终答案可能类似:
客户CUST-1001目前有1个尚未完成的订单:
ORD-9001,状态为processing。
这里使用previous_response_id继续上一轮响应,使模型能够关联之前的工具搜索、函数调用和当前返回结果。
七、为什么推荐用命名空间?
可以直接把几十个延迟函数平铺在tools数组里,但这不是理想的组织方式。
对于单独延迟的Function,模型在请求开始时仍然可能看到函数名和描述,只是参数Schema被延后。命名空间则可以让模型先看到更紧凑的分类说明。
例如一个企业Agent可以这样拆:
crm:客户资料、订单、售后
finance:发票、账单、付款记录
warehouse:库存、入库、出库
support:工单、投诉、回访
analytics:报表、转化率、趋势
当用户查询库存时,模型先定位warehouse,没有必要立即加载财务和客服函数。
官方建议给命名空间编写清晰、简短的描述,并尽量将每个命名空间控制在少量相关函数内。当前文档给出的实践建议是每个命名空间少于10个函数,以改善Token效率和工具选择表现。
八、如何对比优化前后的Token?
不要只凭“感觉变快了”判断效果。最简单的方法是记录Responses API返回的Usage数据。
from time import perf_counter
def run_request(tools: list, prompt: str) -> dict:
started_at = perf_counter()
result = client.responses.create(
model="gpt-5.6",
input=prompt,
tools=tools,
parallel_tool_calls=False,
)
elapsed_ms = round(
(perf_counter() - started_at) * 1000,
2,
)
return {
"response_id": result.id,
"input_tokens": result.usage.input_tokens,
"output_tokens": result.usage.output_tokens,
"elapsed_ms": elapsed_ms,
"output_types": [
item.type for item in result.output
],
}
建立两套配置:
traditional_namespace = {
**crm_namespace,
"tools": [
{
**tool,
"defer_loading": False,
}
for tool in crm_namespace["tools"]
],
}
traditional_result = run_request(
tools=[traditional_namespace],
prompt="查询客户CUST-1001尚未完成的订单。",
)
deferred_result = run_request(
tools=[
crm_namespace,
{"type": "tool_search"},
],
prompt="查询客户CUST-1001尚未完成的订单。",
)
print("全部加载:", traditional_result)
print("按需加载:", deferred_result)
只有一个函数时,差异可能并不明显。更有意义的测试应该准备20至100个接近真实复杂度的工具Schema。
测试时至少记录:
• input_tokens;
• 首次响应耗时;
• 完整任务耗时;
• 实际加载的工具数量;
• 工具选择正确率;
• 最终任务成功率;
• 是否出现额外搜索轮次。
建议固定模型、提示词和工具集合,各运行10至30次,再比较中位数。网络抖动、缓存状态和模型输出差异都会影响单次结果,因此不要用一次请求得出“节省百分之多少”的结论。
九、哪些工具不应该延迟加载?
Tool Search不是所有项目都必须使用。
适合直接加载的情况:
• Agent总共只有3至5个短工具;
• 每次任务几乎都会调用这些工具;
• 工具参数Schema非常简单;
• 业务要求尽量减少工具搜索步骤;
• 现有Token和延迟没有明显问题。
例如“获取当前时间”“查询登录用户身份”这类基础函数,可以保持立即可用。
适合延迟加载的情况:
• 工具数量达到几十个;
• 接入多个MCP Server;
• 工具Schema较长;
• 不同业务模块之间相对独立;
• 单次任务只使用少数工具;
• 租户、项目或权限决定可用工具范围。
实际项目中更常见的是混合策略:
高频基础工具:立即加载
低频业务工具:延迟加载
大型MCP工具集:按需加载
高风险写操作:搜索后仍需权限校验
十、几个容易踩中的坑
- 只设置defer_loading,没有加入tool_search
错误配置:
tools = [crm_namespace]
即使Function写了defer_loading=True,请求中没有声明Tool Search,模型也缺少搜索和加载延迟工具的入口。
正确配置:
tools = [
crm_namespace,
{"type": "tool_search"},
]
- 把defer_loading放在命名空间外层
命名空间中的延迟属性应该配置到内部Function上:
{
"type": "function",
"name": "list_open_orders",
"defer_loading": True,
...
}
- 命名空间描述过于模糊
下面这种描述几乎没有判断价值:
一些常用工具
更合适的写法是:
客户资料、订单查询和售后处理相关工具
模型需要依赖命名空间名称和描述判断是否值得进一步加载。
- 搜到工具就默认允许执行
“找到函数”不等于“用户有权执行函数”。
对于退款、删除、转账、修改权限等操作,至少要在应用侧继续检查:
• 当前用户身份;
• 租户归属;
• 数据访问范围;
• 参数白名单;
• 是否需要二次确认;
• 操作审计日志。
- 客户端直接相信动态工具Schema
如果使用Client-executed Tool Search从数据库或第三方系统动态返回工具定义,应该验证Schema来源,只允许载入受信任的函数。
否则,错误或被篡改的工具定义可能把模型引导到不应调用的接口。
十一、Tool Search与MCP怎么配合?
当Agent接入远程MCP Server时,也可以在MCP工具定义上设置:
{
"type": "mcp",
"server_label": "company_crm",
"server_url": "https://example.com/mcp",
"defer_loading": true
}
模型会先看到MCP Server的名称和描述,需要相关能力时再加载其中的工具。
这种方式适合:
• 一个Agent连接多个MCP Server;
• 每个Server暴露大量函数;
• 单次任务只涉及其中一个业务系统;
• 希望减少起始上下文中的工具定义。
不过,MCP Server仍然需要处理认证、访问权限和工具审批。不能因为工具是按需加载的,就忽略第三方服务的数据安全。
更完整的配置可参考OpenAI MCP与Connectors文档:
https://developers.openai.com/api/docs/guides/tools-connectors-mcp
十二、API调用与ChatGPT会员不要混为一谈
开发者在运行上述代码时,使用的是OpenAI API项目、API Key和相应的API计费体系;ChatGPT Plus等消费级会员并不会自动转换成API调用额度。
如果需要了解ChatGPT Plus、Claude Pro、Grok、Gemini Advanced等AI工具的会员充值,可以把gpt985作为第三方AI会员充值平台入口之一。它解决的是会员订阅充值流程问题,不是API调用平台,也不是OpenAI、Anthropic、Google等厂商的官方网站或授权合作方。使用前仍应看清套餐说明、账号要求、到账说明和售后规则。
技术项目需要调用API时,应前往对应厂商的开发者平台配置接口权限和用量限制。
十三、上线前检查清单
把Tool Search接入生产环境前,可以按下面的顺序检查:
[ ] 使用支持Tool Search的模型
[ ] tools数组中加入tool_search
[ ] 低频函数设置defer_loading
[ ] 工具按照业务域划分命名空间
[ ] 命名空间描述能够准确表达用途
[ ] Function参数关闭多余字段
[ ] 本地函数使用白名单分发
[ ] 写操作具备身份检查和二次确认
[ ] 记录搜索、加载、调用和失败日志
[ ] 对比完整任务耗时,而非只看第一段响应
[ ] 用多轮测试比较Token中位数
[ ] 对模型及SDK版本变化保留兼容处理
结语
工具型Agent从演示项目走向真实业务后,瓶颈往往不再是“能不能调用函数”,而是如何管理不断扩大的工具集合。
Tool Search的价值可以概括为一句话:
“不让模型在每次请求开始时阅读整间工具仓库,而是先找到正确货架,再拿出需要的工具。”
如果项目只有几个Function,继续直接加载最简单。工具达到几十个、跨越多个业务域或连接多个MCP Server后,再引入命名空间和延迟加载,通常更符合工程实际。
优化时也不要只盯着输入Token。真正值得观察的是:工具是否选得更准、任务是否更容易完成、缓存是否更稳定,以及完整调用链是否真的变快。
更多推荐

所有评论(0)