凌晨一点,一个订单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
+ 用户问题

这里至少会产生三个工程问题。

  1. 输入Token增加

即使某个请求只需要调用一个订单查询工具,其他几十个工具的定义也可能进入输入上下文。

  1. 相似工具更容易被混淆

例如下面几个函数:

get_order
get_order_detail
search_orders
list_open_orders
get_order_status

如果描述不够准确,模型可能选错工具,或者需要额外推理才能判断。

  1. 修改工具可能影响缓存

工具定义属于请求上下文的一部分。频繁调整工具集合、顺序或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解决的是“该把哪些工具定义放入上下文”,并不会替你执行业务函数。

例如用户要查询订单时:

  1. 模型先搜索订单相关命名空间;
  2. API把list_open_orders载入上下文;
  3. 模型生成该函数的调用参数;
  4. 你的Python程序查询订单系统;
  5. 程序将查询结果传回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工具集:按需加载
高风险写操作:搜索后仍需权限校验

十、几个容易踩中的坑

  1. 只设置defer_loading,没有加入tool_search

错误配置:

tools = [crm_namespace]

即使Function写了defer_loading=True,请求中没有声明Tool Search,模型也缺少搜索和加载延迟工具的入口。

正确配置:

tools = [
    crm_namespace,
    {"type": "tool_search"},
]
  1. 把defer_loading放在命名空间外层

命名空间中的延迟属性应该配置到内部Function上:

{
    "type": "function",
    "name": "list_open_orders",
    "defer_loading": True,
    ...
}
  1. 命名空间描述过于模糊

下面这种描述几乎没有判断价值:

一些常用工具

更合适的写法是:

客户资料、订单查询和售后处理相关工具

模型需要依赖命名空间名称和描述判断是否值得进一步加载。

  1. 搜到工具就默认允许执行

“找到函数”不等于“用户有权执行函数”。

对于退款、删除、转账、修改权限等操作,至少要在应用侧继续检查:

• 当前用户身份;
• 租户归属;
• 数据访问范围;
• 参数白名单;
• 是否需要二次确认;
• 操作审计日志。

  1. 客户端直接相信动态工具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。真正值得观察的是:工具是否选得更准、任务是否更容易完成、缓存是否更稳定,以及完整调用链是否真的变快。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐