OpenAI Agents SDK 重复 Tool 名称:为什么后注册工具会覆盖前一个?
实验使用 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 的校验函数返回 None,Agent.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; - 重构期间旧工具与新工具同时注册。
单看每个模块都没有错误,冲突只会在最终工具列表合并时出现。

离线复现环境
为了排除模型选择、API Key、网络和 Provider 差异,实验只调用 SDK 本地代码:
| 组件 | 环境 |
|---|---|
| Python | 3.10.2 |
| OpenAI Agents SDK | 0.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:后加入字典的工具覆盖前一个。

为什么 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_1、lookup_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
它解决两个问题:
- 降低不同模块争抢
lookup、search、create这类通用名称的概率; - 给模型更明确的高层工具边界。
但 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

仅检查重复名称还不够
名称唯一只是第一层。完整 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,
])
同时检查:
Agent.get_all_tools()不再返回两个同名可发送工具;- 动态工具与静态工具冲突也能被发现;
- namespace 内外的名称规则符合官方实现;
- 错误信息能指出冲突名称和修复动作;
- 不同 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
在上游修复发布前,生产系统应该:
- 在最终 Tool 列表组装完成后检查
FunctionTool.name唯一; - 使用明确的业务名称或
name_override; - 工具较多时使用 namespace;
- 把校验放进 Agent Factory 单测、插件注册和发布门禁;
- 升级 SDK 后用回归测试确认官方已真正拒绝冲突,再决定是否简化应用层保护。
这类问题最适合在本地启动阶段失败,而不是等模型请求之后才通过 400 或错误副作用暴露。
参考资料
- OpenAI Agents SDK Issue #4116:Reject duplicate function tool names
- OpenAI Agents SDK:Tools
- OpenAI Agents SDK:Agents
从单个 Agent 问题继续进入完整生产体系
AI Agent 专题统一组织架构、记忆、工具调用、评测、安全、部署和多智能体协作,让每篇文章都回到明确的主题主页面。
下一步阅读
返回专题入口 →
OpenAI Agents SDK Tool Approval 如何恢复?RunState 跨进程与 v0.19.3 流式 Resume 实测
OpenAI Agents SDK RunState 如何恢复 Tool Approval?本文对比 openai-agents 0.18.3 与 0.19.3,实测跨进程批准/拒绝、流式 Resume 丢失已批准 Tool Output 的回归与修复,并验证重复投递、业务幂等和 Context 秘密边界。
AI Agent 记忆系统实现:解决智能体“断片”的 3 层架构与实战代码
AI Agent 记忆系统实现:AI Agent 记忆系统实战。对比向量数据库与图数据库在长期记忆存储中的表现。本文进一步说明先给结论:Agent 记忆系统要分清“上下文、事实、状态”、本文解决的问题:Query 意图锁定。
AutoGen 实战教程:多智能体对话协作、工具调用与生产化边界
AutoGen 实战教程:系统拆解 AutoGen 在多智能体对话协作中的实战用法与生产化边界,覆盖 AgentChat、GroupChat、Planner / Executor / Critic 模式、工具调用、Human-in-the-loop、对话轮次控制、评估指标、成本监控和 Microsoft Agent Framework 迁移风险。
LangChain 实战教程:手把手构建具备工具调用能力的智能体
LangChain 实战教程:基于 LangChain 框架的 AI Agent 构建指南。涵盖 Pydantic 工具定义、AgentExecutor 运行机制、持久化记忆集成及工业级错误处理实战。
小白
Full-Stack AI Engineer
小白,全栈 AI 工程师,持续构建生产级 Agent 系统、产品工具与独立软件资产。
了解小白与 XBSTACK →
参与讨论
问题、验证与勘误
登录后可发表评论。所有新评论先进入审核;审核期间仅评论者本人和管理员可见,通过后才公开。