pydantic-ai 中用 PrefixTools 给 Capability 工具加命名空间前缀:原理、用法与测试验证

📅 发布时间:2026/9/13 5:24:49
pydantic-ai 中用 PrefixTools 给 Capability 工具加命名空间前缀:原理、用法与测试验证
pydantic-ai 中用 PrefixTools 给 Capability 工具加命名空间前缀原理、用法与测试验证【免费下载链接】pydantic-aiHow Python does AI. Agents, realtime voice, image generation, embeddings. Every model, every interface, typed end to end.项目地址: https://gitcode.com/GitHub_Trending/py/pydantic-ai本文介绍 pydantic-ai 的PrefixToolscapability它包裹另一个 capability 并为其所有工具名加上统一前缀用于在组合多个可能产生工具名冲突的 capability如多个 MCP 服务器时做命名空间隔离。读完本文你将掌握PrefixTools的直接构造与prefix_tools()便捷方法两种用法、底层PrefixedToolset的前缀改写与调用还原机制、在 Agent 声明式 spec 中的写法以及它与 toolset 层prefixed()的分工关系。为什么需要 PrefixToolspydantic-ai 的 capability 机制允许把 MCP、WebSearch、Toolset 等功能以能力的形式组合挂到 Agent 上。当同一个 Agent 挂上多个来源相似的能力时很容易出现工具名冲突——例如两个 MCP 服务器都暴露了search工具模型侧就会收到两个同名工具导致歧义甚至报错。PrefixTools的解决方式非常直接它包裹wrap另一个 capability并给该 capability 贡献的每个工具名加一个前缀例如mcp使search变为mcp_search从而为每个能力源划定独立的命名空间。其核心语义是只有被包裹 capability 的工具会被加前缀Agent 上的其他工具不受影响。基本用法直接构造 PrefixToolsPrefixTools从pydantic_ai.capabilities导出见 导出定义构造时接收两个关键参数wrapped被包裹的 capability继承自基类WrapperCapability的wrapped字段prefix加到工具名前的前缀字符串最终工具名为{prefix}_{原名}。下面的示例展示了它的典型应用场景为两个不同 MCP 服务器分别加api1、api2前缀避免两侧同名工具互相覆盖from pydantic_ai import Agent from pydantic_ai.capabilities import MCP, PrefixTools agent Agent( openai:gpt-5.2, capabilities[ PrefixTools(MCP(urlhttps://api1.example.com, nativeTrue), prefixapi1), PrefixTools(MCP(urlhttps://api2.example.com, nativeTrue), prefixapi2), ], )这里内层的MCPcapability 通过url指定 MCP 服务器地址nativeTrue表示优先走提供商的原生 MCP 支持源码中MCP.__init__明确要求nativeTrue时必须提供url见 mcp.py L76-L83。你也可以用PrefixTools包裹任何返回工具集的能力例如Toolset(...)from pydantic_ai import Agent from pydantic_ai.capabilities import PrefixTools, Toolset from pydantic_ai.toolsets import FunctionToolset toolset FunctionToolset() agent Agent( openai:gpt-5, capabilities[ PrefixTools( wrappedToolset(toolset), prefixns, ), ], )便捷方法任意 capability 上的 prefix_tools()每个AbstractCapability都提供了prefix_tools便捷方法返回一个以自身为wrapped的PrefixTools包装器避免手写构造参数MCP(urlhttps://mcp.example.com/api, nativeTrue).prefix_tools(mcp)其实现就是简单地把self传给包装器见 abstract.py L1365-L1372def prefix_tools(self, prefix: str) - PrefixTools[AgentDepsT]: Returns a new capability that wraps this one and prefixes its tool names. Only this capabilitys tools are prefixed; other agent tools are unaffected. from .prefix_tools import PrefixTools return PrefixTools(wrappedself, prefixprefix)对应的回归测试test_prefix_tools_convenience_method验证了Toolset(toolset).prefix_tools(ns)的结果确实是PrefixTools实例。实现原理从 PrefixTools 到 PrefixedToolsetPrefixTools本身是一个数据类源码只有 67 行核心逻辑集中在get_toolset()中见 prefix_tools.py L59-L67def get_toolset(self) - AgentToolset[AgentDepsT] | None: toolset super().get_toolset() if toolset is None: return None if isinstance(toolset, AbstractToolset): return PrefixedToolset(toolset, prefixself.prefix) # ToolsetFunc callable — wrap in DynamicToolset so PrefixedToolset can delegate return PrefixedToolset(DynamicToolsetAgentDepsT, prefixself.prefix)从源码结构看这里处理了三种情况被包裹能力没有工具集get_toolset()返回None例如纯指令/模型设置类 capabilityPrefixTools原样返回None不做任何前缀处理——测试test_prefix_tools_returns_none_when_no_toolset验证了这一点被包裹能力返回的是标准AbstractToolset直接包一层PrefixedToolset被包裹能力返回的是 callable 形式的动态工具集ToolsetFunc先包一层DynamicToolset使其可委托再套PrefixedToolset——测试test_prefix_tools_with_callable_toolset验证了此时模型侧看到的工具名是dyn_dynamic_tool。PrefixedToolset改写名字与还原调用真正干活的是 toolset 层的PrefixedToolset它做两件事暴露工具时改写名字get_tools()把内层每个工具重命名为{prefix}_{name}并同步替换工具定义中的name字段执行调用时还原名字call_tool()用name.removeprefix(self.prefix _)剥掉前缀同时把RunContext.tool_name和工具定义恢复为原始名再委托执行保证内层工具及其钩子、审批逻辑感知到的始终是未加前缀的名字async def call_tool(self, name, tool_args, ctx, tool): original_name name.removeprefix(self.prefix _) ctx replace(ctx, tool_nameoriginal_name) tool replace(tool, tool_defreplace(tool.tool_def, nameoriginal_name)) return await super().call_tool(original_name, tool_args, ctx, tool)这个模型侧看到带前缀名、执行侧还原原名的双向映射使得前缀完全对底层工具透明。测试test_prefix_tools_tool_call_strips_prefix构造了一个模型返回ToolCallPart(ns_greet, ...)的场景确认带前缀的调用能正确落到原始greet工具上。边界验证只前缀被包裹能力的工具PrefixTools的作用范围严格限定在wrapped能力内部。测试test_prefix_tools_prefixes_wrapped_capability_tools同时注册了 capability 内的inner_tool和 Agent 级工具outer_tool断言模型侧看到的名字是ns_inner_tool,outer_tool——只有前者被加前缀。声明式 specPrefixTools.from_specpydantic-ai 支持用 JSON 风格的 spec 声明 AgentAgent.from_specPrefixTools在其中有专门的序列化名PrefixToolsget_serialization_name()返回该名见 prefix_tools.py L42-L44和配套的from_spec工厂L46-L57classmethod def from_spec(cls, *, prefix: str, capability: CapabilitySpec) - PrefixTools[Any]: from pydantic_ai.agent.spec import load_capability_from_nested_spec wrapped load_capability_from_nested_spec(capability) return cls(wrappedwrapped, prefixprefix)capability参数接收与其他capabilities列表条目相同格式的嵌套 spec既可以是带参数字典也可以是裸类名。测试test_prefix_tools_from_spec覆盖了两种形态agent Agent.from_spec( { model: test, capabilities: [ { PrefixTools: { prefix: search, capability: {NativeTool: {kind: web_search}}, } }, ], }, )此外PrefixTools.from_spec也可以脱离Agent.from_spec单独使用此时走默认注册表见test_prefix_tools_from_spec_directcap PrefixTools.from_spec(prefixws, capability{WebSearch: {local: duckduckgo}})注册身份与延迟加载的继承PrefixTools继承自WrapperCapability。这类包装器对运行期钩子run/node/model/tool/output 各阶段的 before/after/wrap/error 回调、事件流、handle_deferred_tool_calls等全部默认委托给内层能力只有get_toolset()被PrefixTools覆写——这正是最小侵入的包装模式除工具改名外被包裹能力的一切行为原样保留。值得注意的是注册身份id、defer_loading、description的处理。WrapperCapability.__adopt_wrapped_identity见 wrapper.py L77-L86的规则是包装器自身没有显式id时采用被包裹能力的id和defer_loading。这带来两个实际后果包装器可以直接坐在一个 deferred capability 之上而不丢失其延迟加载身份和加载目录load catalog中的位置——测试test_prefix_tools_inherits_wrapped_metadata_for_registration验证了PrefixTools会继承内层idleaf-tools、defer_loadingTrue与描述并以内层 id 注册进 capability map包装器也可以显式覆盖这些元数据如PrefixTools(github, prefixgithub, idgithub_prefixed)并让自身成为 deferred capability 参与延迟加载见test_prefix_tools_can_be_deferred。另外从源码结构看WrapperCapability.apply特意采用单次遍历、结果重放而非每层递归两次的设计wrapper.py L88-L98注释中明确说明若每层遍历两棵子树n层包装链会导致2**n次遍历一堆prefix_tools()调用将无法正常在合理时间内完成解析。也就是说PrefixTools支持任意层数的嵌套堆叠且遍历开销随深度线性增长。与 toolset 层 PrefixedToolset 的分工PrefixedToolset并非PrefixTools的私有实现它同时也是 toolsets 层的公共能力在 Toolsets 文档 中任意 toolset 都可以通过prefixed()便捷方法链式改名例如weather_toolset.prefixed(weather)使temperature_celsius变为weather_temperature_celsius。两者的分工可以这样理解维度PrefixedToolset/.prefixed()PrefixTools/.prefix_tools()作用对象单个 toolset整个 capability及其贡献的工具集典型场景用CombinedToolset组合多个本地 toolset 时消歧组合多个 capability如多个 MCP 服务器时做命名空间隔离附加影响无纯名字变换继承被包裹能力的 id/描述/延迟加载等注册元数据spec 中可声明嵌套能力当加前缀生成的名字过长或可能让模型困惑时还可以改用 RenamedToolset 做字典式精确重命名而不是机械前缀。小结PrefixTools是 pydantic-ai capability 组合中的命名空间工具PrefixTools(capability, prefixapi1)或更简洁的capability.prefix_tools(api1)只改写被包裹能力的工具名Agent 其他工具不受影响底层链路为PrefixTools.get_toolset()→PrefixedToolsetget_tools()负责把名字改为{prefix}_{name}call_tool()负责在还原RunContext.tool_name与工具定义后委托执行对底层工具完全透明包装器自动继承被包裹能力的id与defer_loading可叠加延迟加载也支持通过Agent.from_spec的PrefixTools声明嵌套能力关键源码位于 prefix_tools.py、wrapper.py、prefixed.py行为边界可由 tests/test_capabilities.py 中的test_prefix_tools_*系列用例逐条核对。【免费下载链接】pydantic-aiHow Python does AI. Agents, realtime voice, image generation, embeddings. Every model, every interface, typed end to end.项目地址: https://gitcode.com/GitHub_Trending/py/pydantic-ai创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考