OpenAI Agents SDK 中两个同名 lookup FunctionTool 被同时注册,而本地分发只保留后注册工具 - XBSTACK

OpenAI Agents SDK 重复 Tool 名称:为什么后注册工具会覆盖前一个?

Release Date
2026-08-03
Reading Time
11分钟
Content Size
5,201 chars
OpenAI Agents SDK
FunctionTool
Tool Calling
Python
AI Agent
name_override
Tool Namespace
Production Engineering
Xiaobai's Note / 实验室笔记

实验使用 Python 3.10.2 与 openai-agents 0.19.2,只调用 SDK 的工具注册、列表解析和 lookup map 构建逻辑;不需要 OpenAI API Key,不调用模型,不验证真实 Provider 的错误文案。OpenAI API 返回重复函数名 400 的行为来自官方仓库 Issue #4116 的生产复现。

适合谁读

  • 正在用 OpenAI Agents SDK 组合多个 FunctionTool、插件或 Agent 工具的 Python 开发者。
  • 需要在 CI、Worker 启动和生产发布前验证 Tool Registry 的平台团队。
  • 遇到 duplicate function name 400、错误工具分发或 name_override 冲突的 Agent 工程团队。

OpenAI Agents SDK 重复 Tool 名称:为什么后注册工具会覆盖前一个?

先说结论:openai-agents==0.19.2 中,两个普通 FunctionTool 使用相同公共名称时,SDK 不会在组装 Agent 时主动报错。 本次离线实验中,两个不同 Python 函数都通过 name_override="lookup" 暴露为 lookup;SDK 的校验函数返回 NoneAgent.get_all_tools() 仍返回两个同名工具,而内部用于分发 Tool Call 的 lookup map 只保留后注册工具。结果是:使用 OpenAI API 时,请求可能因重复函数名被 Provider 返回 400;使用容忍重复名称的兼容 Provider 时,运行可能看似成功,却执行了错误实现。

这个问题来自 OpenAI Agents SDK 官方仓库 2026 年 8 月 2 日创建的 Issue #4116上游状态已经变化: Issue #4116 现已关闭,PR #4145 于 8 月 4 日合并了 collision-policy 相关改动并把 #4116 列为已解决。本文实验仍固定在 openai-agents==0.19.2,所以它证明的是 0.19.2 的行为,不自动代表每个后续版本仍然相同。

本文只解决这个搜索任务:

OpenAI Agents SDK 为什么允许同名 FunctionTool 进入 Agent,为什么分发时变成后注册工具覆盖前一个,以及在官方修复发布前怎样让生产系统提前失败?

它与 RunState Tool Approval 跨进程恢复 不是同一问题。RunState 文章处理暂停、审批、序列化和恢复;本文处理的是模型调用发生之前的 Tool Registry 唯一性。

最容易出现冲突的代码

两个函数名称本来不同,但都被显式改成 lookup

from agents import function_tool


@function_tool(name_override="lookup")
def lookup_customers(query: str) -> str:
    """Look up customers."""
    return f"customer:{query}"


@function_tool(name_override="lookup")
def lookup_orders(query: str) -> str:
    """Look up orders."""
    return f"order:{query}"

官方文档说明,@function_tool 默认使用 Python 函数名作为工具名称,也允许通过 name_override 指定对模型公开的名称。真正需要唯一的不是 Python 标识符,而是最终的 FunctionTool.name

这类冲突在生产代码里并不罕见:

  • CRM 模块和订单模块都把搜索工具叫 lookup
  • 两个插件各自导出 search
  • Agent.clone() 后又把原工具列表拼接一次;
  • 根据租户或 Feature Flag 动态追加工具;
  • 将子 Agent 转成 Tool 时手工指定了重复 tool_name
  • 多个团队都使用 name_override="execute"query
  • 重构期间旧工具与新工具同时注册。

单看每个模块都没有错误,冲突只会在最终工具列表合并时出现。

OpenAI Agents SDK 中两个 FunctionTool 注册为同名 lookup,进入 Agent 工具列表后,本地分发映射只保留后注册的 lookup_orders

离线复现环境

为了排除模型选择、API Key、网络和 Provider 差异,实验只调用 SDK 本地代码:

组件环境
Python3.10.2
OpenAI Agents SDK0.19.2
OpenAI API Key不需要
模型调用不发生
验证对象Tool 校验、Agent 工具列表、分发 lookup map

复现目录:

experiments/openai-agents-duplicate-tool-names-repro/
├── repro.py
├── requirements.txt
├── results/verification.json
├── RESEARCH.md
└── README.md

运行:

python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/python repro.py

第一层问题:SDK 校验函数没有拒绝

SDK 内部存在一个名称非常明确的函数:

validate_function_tool_lookup_configuration(tools)

从用途看,它应该拒绝无法唯一映射的 Function Tool 配置。但传入两个普通同名工具时:

result = validate_function_tool_lookup_configuration([
    lookup_customers,
    lookup_orders,
])

assert result is None

没有 UserError,没有 warning,也没有去重。

Issue #4116 指出的代码路径是:校验器已经发现相同 qualified name 的已有 owner,但当两个工具都没有显式 namespace 时,分支直接 continue。换句话说,它识别到了最常见的冲突,却选择继续执行。

这与 SDK 对其他工具类型的策略不一致。Issue 中提到:

  • 多个 MCP Server 产生重复工具名时已有冲突检查;
  • Codex Tool 重名时也有明确验证;
  • 普通 Function Tool 反而缺少等价门禁。

第二层问题:两个同名工具仍然发给模型

构造 Agent 后读取全部工具:

from agents import Agent, RunContextWrapper

agent = Agent(
    name="Support",
    tools=[lookup_customers, lookup_orders],
)

tools = await agent.get_all_tools(
    RunContextWrapper(context=None),
)

print([tool.name for tool in tools])

实验输出:

['lookup', 'lookup']

这意味着 SDK 没有在 Agent 层自动选择一个、重命名或删除重复项。Provider 请求构造阶段仍可能看到两个同名函数定义。

Issue #4116 的真实 Provider 复现指出,OpenAI Responses 或 Chat Completions API 会拒绝这类 payload,因此生产表现通常是一个不够直观的 Provider 400。开发者看到的是“模型调用失败”,而真正问题发生在本地 Tool Registry。

第三层问题:本地分发变成 last-wins

即使某个 OpenAI-compatible Provider 没有拒绝重复名称,问题也不会消失。SDK 还需要在模型返回:

{
  "name": "lookup",
  "arguments": {"query": "A-100"}
}

之后找到本地函数执行。

实验直接构建 SDK 的 Function Tool lookup map:

from agents._tool_identity import build_function_tool_lookup_map

lookup_map = build_function_tool_lookup_map([
    lookup_customers,
    lookup_orders,
])

结果只有一个键:

('bare', 'lookup')

而该键指向后注册的 lookup_orders

selected = lookup_map[("bare", "lookup")]

assert selected is lookup_orders
assert selected is not lookup_customers

完整验证结果:

{
  "sdk_validator_returned_none": true,
  "advertised_tool_names": ["lookup", "lookup"],
  "dispatch_lookup_keys": [["bare", "lookup"]],
  "dispatch_selected_python_function": "lookup_orders",
  "first_tool_reachable_by_bare_name": false,
  "second_tool_reachable_by_bare_name": true
}

这就是 last-wins:后加入字典的工具覆盖前一个。

OpenAI Agents SDK 0.19.2 离线验证结果:SDK validator 不报错,Agent 暴露两个 lookup,而本地分发选择 lookup_orders

为什么 silent last-wins 比 400 更危险

Provider 400 会阻止请求,虽然影响可用性,至少不会执行错误操作。兼容 Provider 如果接受重复工具名,模型只返回 lookup,SDK 无法知道它意图的是客户查询还是订单查询,只能按本地映射执行后注册函数。

假设两个工具分别是:

lookup_customer_account
lookup_refund_order

却都对模型暴露成 lookup。如果第二个工具涉及退款、删除、发送消息或写数据库,错误分发可能产生真实副作用。

因此风险不只是“命名不规范”,而是:

  • 模型看到两个无法区分的 Tool Schema;
  • Provider 可能拒绝请求;
  • 本地执行器可能选择错误实现;
  • 日志只记录公共名称 lookup,难以解释原本想调用哪个;
  • 重试不会修复确定性冲突;
  • 更换 Provider 后,失败模式可能从 400 变成静默错误执行。

临时修复:请求前执行唯一名称门禁

在上游 SDK 主动拒绝同名工具前,最稳的方案不是等待 Provider 报错,而是在应用组装完最终工具集合后立即检查:

from collections import Counter
from collections.abc import Iterable

from agents import FunctionTool
from agents.exceptions import UserError


def find_duplicate_function_tool_names(
    tools: Iterable[object],
) -> list[str]:
    names = [
        tool.name
        for tool in tools
        if isinstance(tool, FunctionTool)
    ]

    return sorted(
        name
        for name, count in Counter(names).items()
        if count > 1
    )


def require_unique_function_tool_names(
    tools: Iterable[object],
) -> None:
    duplicates = find_duplicate_function_tool_names(tools)
    if duplicates:
        quoted = ", ".join(repr(name) for name in duplicates)
        raise UserError(
            "Duplicate FunctionTool names are not allowed: "
            f"{quoted}. Use a unique Python function name, "
            "name_override=, or a tool namespace."
        )

使用:

tools = load_static_tools()
tools += load_plugin_tools()
tools += await load_tenant_tools(tenant_id)

require_unique_function_tool_names(tools)

agent = Agent(
    name="Support",
    tools=tools,
)

实验中的重复配置会在模型请求前得到明确错误:

Duplicate FunctionTool names are not allowed: 'lookup'.
Use a unique Python function name, name_override=, or a tool namespace.

修复名称后,验证不再歧义

将订单工具改为唯一名称:

@function_tool(name_override="lookup_orders")
def lookup_orders(query: str) -> str:
    """Look up orders."""
    return f"order:{query}"

最终列表变成:

['lookup', 'lookup_orders']

校验通过,lookup map 也有两个不同键。

更清晰的命名通常比 lookup_1lookup_2 更好:

customer_lookup
order_lookup
invoice_lookup
knowledge_search
shipment_track

名称本身应该帮助模型区分业务动作,而不仅是满足唯一约束。

工具较多时使用 namespace

OpenAI Agents SDK 官方工具文档建议,在工具较多时优先使用 namespace。命名空间可以把相关 Function Tool 组织成:

crm.lookup_customer
orders.lookup_order
billing.lookup_invoice

它解决两个问题:

  1. 降低不同模块争抢 lookupsearchcreate 这类通用名称的概率;
  2. 给模型更明确的高层工具边界。

但 namespace 不是替代测试的理由。最终暴露给模型和执行器的可调用标识仍必须唯一,CI 应检查组装后的真实工具列表,而不是只检查源代码文件名。

应该在哪些阶段检查

单元测试

每个 Agent Factory 都应断言工具名唯一:

def test_support_agent_tool_names_are_unique():
    tools = build_support_tools()
    require_unique_function_tool_names(tools)

插件注册

插件加载完成后统一校验。不要让每个插件只保证自己内部唯一,因为冲突通常发生在两个插件之间。

多租户配置

不同租户可能启用不同工具组合。至少对所有合法组合执行预计算,或者在 Worker 启动/请求入口进行缓存后的组合校验。

Agent.clone 与动态追加

Clone、列表拼接、Feature Flag 和 A/B 测试最容易重复加入同一工具。校验必须发生在最终列表,而不是初始常量定义处。

发布门禁

CI 可以遍历所有生产 Agent Factory:

for agent_name, tools in all_production_toolsets():
    try:
        require_unique_function_tool_names(tools)
    except UserError as exc:
        raise AssertionError(f"{agent_name}: {exc}") from exc

OpenAI Agents SDK 重复工具名的修复与发布门禁:汇总最终工具集、唯一名称预检、修复冲突并加入 Agent Factory 与 CI 自动化检查

仅检查重复名称还不够

名称唯一只是第一层。完整 Tool Registry 门禁还应检查:

检查项失败风险
FunctionTool.name 唯一Provider 400、错误分发
Tool Schema 稳定Prompt 缓存失效、模型调用参数漂移
描述差异足够模型在不同工具间选择不稳定
高风险工具有审批/Guardrail未授权副作用
Tool ID 与审计字段稳定日志无法关联具体实现
动态启用结果可重现同一版本不同 Worker 工具集不一致

工具授权与业务策略可以继续看 AI Agent Tool Authorization Policy Gate。本文不把名称冲突扩写成完整授权系统。

三种错误处理方式

依赖 Provider 返回 400

这会把本地可确定的问题推迟到网络请求之后,增加延迟、费用、重试噪声和错误排查成本。更换 Provider 后,甚至可能不再报错而是错误执行。

用列表顺序决定“正确工具”

顺序变化可能来自 import、插件发现、配置合并或 Python 数据结构。把后注册覆盖当成配置机制,没有可读性,也没有稳定保证。

只检查函数 __name__

两个函数可以有不同 __name__,却通过相同 name_override 暴露同一公共名称。应检查 FunctionTool.name

上游修复发布后怎么验

截至 2026 年 8 月 3 日,Issue #4116 没有关联 PR。合理的上游行为应该是在工具解析阶段直接抛出类似:

Ambiguous function tool configuration:
the tool name `lookup` is used by multiple tools.
Pass a unique name_override= or namespace.

升级后不要只删除应用代码。先运行回归测试:

import pytest
from agents.exceptions import UserError


def test_sdk_rejects_duplicate_bare_function_tools():
    with pytest.raises(UserError):
        validate_function_tool_lookup_configuration([
            lookup_customers,
            lookup_orders,
        ])

同时检查:

  1. Agent.get_all_tools() 不再返回两个同名可发送工具;
  2. 动态工具与静态工具冲突也能被发现;
  3. namespace 内外的名称规则符合官方实现;
  4. 错误信息能指出冲突名称和修复动作;
  5. 不同 Provider 的请求前行为一致。

与 RunState 文章如何分工

站内已经发布的 OpenAI Agents SDK RunState 实战 关注的是:

  • Tool Approval 暂停;
  • RunState 序列化;
  • 跨进程恢复;
  • 重复投递与业务幂等;
  • Context 脱敏和版本治理。

本文发生得更早:Agent 还没调用模型,工具注册表已经存在歧义。一个系统可以把 RunState 做得很可靠,但如果两个高风险工具同名,恢复后仍可能调用错误实现。

最终结论

OpenAI Agents SDK 0.19.2 的重复 Function Tool 名称问题具有两种失败模式:

  • 严格 Provider 在请求阶段拒绝重复函数名;
  • 宽松 Provider 接受请求,但 SDK 本地分发以后注册工具覆盖前一个。

XBSTACK 的离线实验确认:

SDK validator       -> 不报错
Agent tools          -> ['lookup', 'lookup']
Local dispatch map   -> 只保留后注册 lookup_orders
First tool reachable -> False

在上游修复发布前,生产系统应该:

  1. 在最终 Tool 列表组装完成后检查 FunctionTool.name 唯一;
  2. 使用明确的业务名称或 name_override
  3. 工具较多时使用 namespace;
  4. 把校验放进 Agent Factory 单测、插件注册和发布门禁;
  5. 升级 SDK 后用回归测试确认官方已真正拒绝冲突,再决定是否简化应用层保护。

这类问题最适合在本地启动阶段失败,而不是等模型请求之后才通过 400 或错误副作用暴露。

参考资料

专题入口 / AI Agent Hub

从单个 Agent 问题继续进入完整生产体系

AI Agent 专题统一组织架构、记忆、工具调用、评测、安全、部署和多智能体协作,让每篇文章都回到明确的主题主页面。

下一步阅读

返回专题入口 →
小白

小白

Full-Stack AI Engineer

小白,全栈 AI 工程师,持续构建生产级 Agent 系统、产品工具与独立软件资产。

了解小白与 XBSTACK →

喜欢这篇文章?
加入小白实验室的周刊

每期只整理 AI 工程变化、真实故障、可复现实验、值得尝试的工具和 XBSTACK 新资产,不做泛新闻汇总,也不为周更凑数。

Comments

参与讨论

问题、验证与勘误

登录后可发表评论。所有新评论先进入审核;审核期间仅评论者本人和管理员可见,通过后才公开。

登录评论 审核后公开
正在加载评论区…