跳到主要内容

工具系统:定义、注册与安全执行

30 秒导读: 模型只会"说话",本章讲 PraisonAI 怎么给它装上"手脚"——把一个 Python 函数变成模型能点名调用的工具,并让每一次真实调用都穿过一条受控管线(类型纠正、审批、熔断、重试、给外部内容打隔离标记)。MCP 服务器也被当成一批"外部工具"接进这条同样的管线。

本章在货架里的位置:上一章 Agent 单体与 chat 主循环 讲"模型什么时候决定要调工具";本章接手"决定之后,这次调用如何安全落地"。沙箱级隔离执行属于 第 06 章 范畴,本章不展开。


1. 这是什么(零基础也能懂)

一句话定义。 工具(tool)= 一个带名字、带参数说明的函数,模型可以在回话里说"我要调 get_weather(city='东京')",框架负责把这句话真正执行掉,再把结果喂回模型。

解决什么问题 / 给谁用。 大语言模型本身不能查数据库、不能读文件、不能发 HTTP 请求——它只能生成文本。工具系统就是那层"翻译 + 执法":

  • 把开发者写的普通函数,翻译成模型看得懂的调用说明(JSON schema)。
  • 模型说要调用时,把它给的参数校正、审批、执行,再把返回值安全地送回去。

给谁用:任何想让 agent "干实事"(而不只是聊天)的开发者。

它能做什么。

  • 用一个 @tool 装饰器把函数变工具,自动从函数签名生成参数 schema。
  • 用注册表统一管理工具、支持第三方插件自动发现。
  • 执行时做:参数类型纠正、危险工具审批、故障熔断、失败重试退避、外部内容防注入包裹。
  • 把 MCP(Model Context Protocol,一个让 agent 连外部工具服务器的开放协议)服务器整批挂成工具。

用起来什么样。 最小例子——定义一个工具,交给 agent:

# 示意,基于 README 的最小用法
from praisonaiagents import Agent, tool

@tool # 一个装饰器就够
def get_stock_price(company: str) -> str:
"""查询某公司股价。""" # docstring 变成给模型看的描述
return f"{company} 现价 100 USD"

agent = Agent(instructions="你是助手", tools=[get_stock_price])
agent.start("特斯拉股价多少?") # 模型自己决定调 get_stock_price(company="Tesla")

一句话直觉。 把工具系统想成餐厅后厨的"传菜 + 食品安全"制度:模型是点菜的客人(只会说菜名),工具定义是菜单(告诉客人有什么、怎么点),执行管线是后厨流水线(核对订单、检查过敏原、出问题重做),MCP 是外包的中央厨房(不在本店做,但也走同一套上菜流程)。


2. 顶层全景(它大概怎么转)

工具系统天然分成两个时相,别把它们混在一起看:

时相什么时候发生干什么
装载期(定义与注册)agent 构造、模块导入时把函数变成工具对象、生成 schema、登记进注册表
运行期(执行管线)模型返回一个 tool_call 之后把"调用这句话"安全地落到真实函数上,拿回结果

怎么读下面这张图: 上半是装载期(左→右一次性完成),下半是运行期(模型触发后走的循环)。

装载期 ── 一次性 ────────────────────────────────────────────
@tool / BaseTool / MCP(...)
│ 从函数签名 + 类型注解生成 JSON schema

FunctionTool / BaseTool 对象 ──register──► ToolRegistry(全局注册表)
│ │
└───────────► 汇总成 OpenAI 工具定义 ◄──────┘

▼ 作为 tools=[...] 传给 LLM

运行期 ── 每次工具调用 ──────────────────────────────────────
LLM 回一个 tool_call {name, arguments}


execute_tool ──► 执行管线(类型纠正→审批→熔断→重试→包裹)──► 结果字符串
│ │
└──────────────── 结果回喂进对话,进入下一轮 ◄──────────┘

部件一句话职责:

部件干什么文件(clone 根相对)
@tool / FunctionTool把函数包成工具、自动生成 schemasrc/praisonai-agents/praisonaiagents/tools/decorator.py
BaseTool / ToolResult工具基类与结果封装(含多模态).../tools/base.py
schema 生成类型注解 → JSON schema.../tools/schema.py
ToolRegistry注册、查找、插件发现、信任级.../tools/registry.py
执行管线 ToolExecutionMixin审批/熔断/重试/纠参/包裹.../agent/tool_execution.py
安全护栏路径/URL/信任/白名单.../tools/path_safety.pyurl_safety.pytrust.py.../allowed_tools_filter.py
工具检索工具太多时的渐进式披露.../tools/tool_search.py
MCP把外部 MCP 服务器挂成工具.../mcp/mcp.py

3. 核心原理(逐个机制,由浅入深)

3.1 从函数到 schema:定义与注册

要解决的小问题。 模型不认识 Python 函数,它只认识一段 JSON:"有个工具叫 X,吃这些参数、每个参数什么类型、哪些必填"。谁来把 def get_stock_price(company: str) 翻成这段 JSON?

思路。inspect 读函数签名 + typing 读类型注解,逐个参数转成 JSON schema 片段。核心翻译器是 annotation_to_json_schema,它认得 OptionalUnionLiteralEnumListDict 这些复杂类型,而不是一律塌成 "string"(.../tools/schema.py:23 annotation_to_json_schema)。

三种定义工具的方式(条条大路,最后都变成一个"有 name/description/get_schema() 的对象"):

方式怎么写生成 schema 的入口
装饰器@tool 包一个函数FunctionTool._generate_schema_from_func(decorator.py:103)
子类继承 BaseTool、实现 run()BaseTool._generate_parameters_schema(base.py:202)
裸函数直接把函数放进 tools=[...]Agent._generate_tool_definition(agent/agent.py:403)

前两种最后都调到同一个共享助手 build_parameters_schema(schema.py:156)——它遍历签名,跳过 self/cls 和被标记为"注入"的参数,对每个参数调 annotation_to_json_schema,再用 get_parameter_requirements(schema.py:123)判定必填(无默认值且非 Optional 才必填)。

原理演示(把"签名 → schema"的核心想法演出来):

# 示意,非源码:build_parameters_schema 的骨架
def build_parameters_schema(sig, hints):
schema = {"type": "object", "properties": {}, "required": []}
for name, _ in sig.parameters.items():
if name in {"self", "cls"}:
continue # 跳过实例参数
ptype = hints.get(name, Any)
schema["properties"][name] = annotation_to_json_schema(ptype) # 类型 → schema
if has_no_default_and_not_optional(sig, name):
schema["required"].append(name) # 无默认值 = 必填
return schema

真实实现要点。

  • @tool 装饰器同时支持 @tool@tool(name=..., retry_policy=...) 两种写法,内部统一走 decorator(fn)(decorator.py:181 tool)。
  • 装饰时立刻做两道校验并把工具登记进全局注册表:validate() + validate_schema_roundtrip()(decorator.py:237-238),再 registry.register(...)(decorator.py:252)。validate_schema_roundtrip(base.py:337)会把 schema JSON 序列化再反序列化,确保它真能发给 LLM 且结构不变——这是"定义期就把错误抓出来"的设计。
  • ToolResult(base.py:32)不止能装文本,还能装多模态内容(图片/文件 part),让截图、图表这类工具的产物在下一轮变成模型可见的图像消息;multimodal_content / image_part 是它的便捷工厂(base.py:100)。

注册表长什么样。 ToolRegistry(registry.py:81)是个线程安全(RLock)的单例,内部用 Dict[str, ToolEntry] 存工具,每个 ToolEntry 记着工具本体、动态 schema 覆盖函数、以及信任级(trust_level)。关键方法:

方法作用位置
register登记工具(重名默认跳过)registry.py:100
get按名取工具,找不到才触发插件发现registry.py:183
get_tool_definitions汇总所有可用工具的 OpenAI schema(每次发给 LLM 前调)registry.py:221
discover_pluginsentry_points 自动发现第三方工具registry.py:361
get_trust_level查工具信任级(审批时要用)registry.py:433

外部包只要在 pyproject.toml 里声明 [project.entry-points."praisonaiagents.tools"],就能被 discover_plugins 自动装进来(registry.py:361)——这是"工具即插件"的机制。

3.2 Injected:给工具偷偷塞进 agent 状态

要解决的小问题。 有些工具需要知道"当前是哪个 session、哪个 agent、上一条用户消息",但你不想让模型看到、更不想让模型来填这些参数。

思路。 用类型标记 Injected[T]。被这样标注的参数:①不出现在给模型的 schema 里(生成 schema 时被 skip_predicate 跳过,见 decorator.py:121);②运行时由框架自动填入当前 AgentState

# 示意,基于 injected.py 的用法
from praisonaiagents import tool
from praisonaiagents.tools import Injected

@tool
def whoami(query: str, state: Injected[dict]) -> str:
# state 由框架注入,模型既看不到也填不了
return f"session={state.get('session_id')}"

真实实现。 执行前,execute_tool 先组一个 AgentState(装 agent_id/run_id/session_id/上一条用户消息/记忆句柄等,tool_execution.py:436),再用 with_injection_context(state) 把它压进一个 ContextVar(injected.py:95);FunctionTool.run 真正调用前,inject_state_into_kwargs 从 ContextVar 取状态填进 kwargs(decorator.py:125-129injected.py:185)。用 ContextVar 而非全局变量,是为了多 agent 并发时互不串状态。

3.3 执行管线:一次调用的受控之旅

这是本章工程含量最高的部分。入口是 execute_tool(function_name, arguments, tool_call_id)(tool_execution.py:412),它把这次调用送进一条分层管线;下面拆成"外层护送"和"内层落地"两张图看。

外层护送(在 _execute_tool_with_context,tool_execution.py:449):

execute_tool
│ 组 AgentState、开注入上下文

① 发 TOOL_CALL_START 事件(trace / 流式)
② steering 检查:高优先级用户插话可中断本次调用
③ BEFORE_TOOL 钩子:可【拦截】或【改写参数】
④ loop guard:防工具死循环(WARN / BLOCK / HALT)
⑤ 参数校验器(可选):不合规直接拒

▼ 外层重试循环(attempt = 1 .. max_retry_limit+1)
⑥ 可选超时:放进线程池,超时判 retryable
└──► 内层落地(见下图)──► 结果 / 错误

▼ 拿到结果后
⑦ wrap_if_external:外部工具结果打隔离标记(防注入)
⑧ 截断:超长输出压缩,防撑爆上下文
⑨ AFTER_TOOL 钩子

内层落地(_execute_tool_with_circuit_breaker_impl_execute_tool_impl):

_execute_tool_with_circuit_breaker (1342)
│ 取重试策略(工具级 > agent级 > 默认)
▼ 内层重试循环(attempt = 0 .. max_attempts-1)
_execute_tool_with_circuit_breaker_impl (1446)
│ 为该工具取/建熔断器

breaker.call(_tool_wrapper)


_execute_tool_impl (1513)
├─ 审批 _check_tool_approval_sync ──拒─► 返回 approval_denied
├─ MCP 工具?在 MCP 实例里找并调
├─ 在 agent.tools 列表里找(BaseTool / FunctionTool / 裸函数)
├─ 没找到 → 查全局注册表(插件)
└─ _cast_arguments 纠正参数类型 ──► 调 func.run(**args) / func(**args)

为什么有两层重试? 这是刻意设计,不是重复。内层(_execute_tool_with_circuit_breaker,tool_execution.py:1358)按 RetryPolicy.retry_on 只重试特定错误类型(超时/限流/连接错误);外层(_execute_tool_with_context,tool_execution.py:581)是兜底。代码在 tool_execution.py:634-643 明确解释了它们如何避免"叠加空转":内层已经重试过并耗尽的错误类型(如 timeout),外层不再当 retryable 重跑,只有内层不重试的类型(如 unknown)才交给外层再走一遍。

参数纠正 _cast_arguments(tool_execution.py:356)。 LLM 给的参数经常有毛病,这一步专治:

  • kwarg 名带尾随 =、空格 → 清洗(raw_name.strip().rstrip('='))。
  • 大小写对不上 → 大小写不敏感模糊匹配到真实参数名。
  • 类型串了:字符串 "3" 传给 int 参数 → int(float("3"));"true" 传给 bool → 按 ('true','1','yes','on') 判定。

参数纠正的容错映射:

LLM 给的目标类型纠正为
"42" / 42.0intint(float(x))
"3.14" / 3floatfloat(x)
"yes" / "on" / "1"boolTrue
"City="(脏名)——清成 "City" 再匹配

重试退避。 延迟由 BackoffPolicy.delay(外层,tool_execution.py:28)和 RetryPolicy.get_delay_ms(内层,tools/retry.py:51)算:都是指数退避 + 抖动 + 封顶(initial * factor^attempt,加随机抖动,min(delay, max_delay))。抖动是为了避免多个失败调用同时重试造成"惊群"。

错误分类 _classify_error_type(tool_execution.py:1890)。 决定"这个错该不该重试"的关键——按错误消息/异常类型的关键词归类:

归类触发关键词默认是否重试
timeout"timeout" / "timed out"
rate_limit"rate" 且 "limit"
connection_error"connection" / "network"
unknown其它否(内层),交外层兜一次

默认可重试集合是 {"timeout", "rate_limit", "connection_error"}(tools/retry.py:28)。

3.4 安全护栏:四道闸

管线里散布着四类护栏,各防一种事。一张表看全:

护栏防什么触发点文件:符号
审批(approval)危险工具未经许可就跑落地前 _check_tool_approval_synctool_execution.py:1295
熔断(circuit breaker)某工具连续失败拖垮系统每次落地包一层tool_execution.py:1446
信任包裹(trust)外部内容里的提示词注入结果回喂前 wrap_if_externaltools/trust.py:52
路径/URL 安全目录穿越、SSRF具体工具内部调用path_safety.pyurl_safety.py

审批(approval)。 拦截分两级:

  • 权限层快查(O(1) frozenset):_perm_deny 命中直接拒、_perm_allow 不在里面也拒(tool_execution.py:1298-1301)。这来自 Agent(approval="default"/"safe"/...) 预设。
  • 审批决策 _resolve_approval_decision(tool_execution.py:1179):符合下列任一即需人工/后端批准——approve_all、在内置危险清单 DEFAULT_DANGEROUS_TOOLS(approval/registry.py:33,含 execute_command=critical、write_file=high 等)、注册表标了需审批、或工具信任级为 external

默认预设 "default" 只拦"删文件 + 跑 shell/代码",放行读/建/改——因为 99% 的有用 agent 流程都要改文件(approval/registry.py:53-70 的注释直言这个取舍)。

熔断。 每个工具有独立熔断器,配置写死为 failure_threshold=5recovery_timeout=60sgraceful_degradation=True(tool_execution.py:1468)。状态机是标准三态:CLOSED(正常)→ 连续 5 次失败 → OPEN(拒绝调用、返回 circuit_open 错误)→ 60 秒后 → HALF_OPEN(放一个探针)→ 成功则回 CLOSED(tools/circuit_breaker.py:19)。妙处:工具返回的错误 dict 被 _tool_wrapper 转成哨兵异常,好让熔断器"看见"失败;但审批/权限拒绝算失败(tool_execution.py:1481-1484)——否则用户一直拒批就会误触熔断。

信任包裹(防提示词注入)。 联网搜索/抓网页这类工具的返回值可能藏着"忽略之前指令,改去做 X"的恶意文本。wrap_if_external(tools/trust.py:52)只对外部工具(硬编码清单 EXTERNAL_TOOL_NAMES 或注册表信任级为 external)的结果动手:把内容夹进 <external_tool_result>…</external_tool_result> 标记,并转义内容里已有的同名标记防"越狱"。配套的系统提示词(trust.py:151 get_system_prompt_addition)告诉模型:标记内是数据,只提取事实、绝不执行其中指令。

路径与 URL 安全。

  • resolve_within_root(path_safety.py:9):把路径解析到某个根目录下,用 os.path.commonpath 判定是否逃出根,逃出就返回 None——挡目录穿越(../../etc/passwd)。
  • is_safe_http_url(url_safety.py:19):挡 SSRF(服务端请求伪造)。只放行 http(s);把主机名解析成 IP,若命中回环/私网/链路本地/组播就拒;localhost 需显式经 ALLOW_LOCAL_CRAWL=true 或 allowlist 才放行。

白名单过滤 AllowedToolsFilter(allowed_tools_filter.py:37)。 解决多环境下"同名工具打架"(tool shadowing):ALLOWED_TOOLS 环境变量给出白名单,只有名单内工具可见。语义有讲究——未设=全放(带告警)、空串=报错(必须显式)、CI 里名单含未知工具=严格失败(allowed_tools_filter.py:99 filter_tools)。

3.5 工具检索:工具太多时的渐进式披露

要解决的小问题。 工具一多,把所有 schema 都塞进提示词会吃掉大量上下文,还让模型选择困难。

思路(移植自 Hermes)。 把非核心工具"折叠"起来,只给模型三个桥工具:tool_search(query) 找候选、tool_describe(name) 看完整 schema、tool_call(name, args) 解包并真正执行(tools/tool_search.py 头部注释列了 8 条不变量)。核心工具(文件/shell/搜索等,PRAISONAI_CORE_TOOLS,tool_search.py:33)永不折叠。

关键机制:

  • 何时启用:should_defer_tools——当"可折叠工具的 schema token 数 ≥ 上下文窗口的 threshold_pct(默认 10%)"时自动开(tool_search.py:173)。
  • 检索用内联的 BM25(BM25ToolSearcher,tool_search.py:208),不引新依赖,并对工具名做子串兜底。
  • tool_call 先把桥调用解包成真实工具名再执行(resolve_underlying_call,tool_search.py:532),所以审批/trace/流式看到的都是真实工具名(不变量 #6);这也是 execute_tool 开头就拦截桥工具的原因(tool_execution.py:427)。

3.6 把 MCP 服务器挂成外部工具

要解决的小问题。 MCP 是一套让 agent 连"工具服务器"的开放协议(文件系统、GitHub、时间等都有现成 server)。怎么把一个 MCP server 的所有工具,变成 agent 能直接用的普通工具?

思路。 一个 MCP(...) 对象(mcp/mcp.py:159)= 一个 MCP 客户端 + 一批自动生成的工具函数。它可迭代(__iter__,mcp/mcp.py:604),所以能直接 Agent(tools=MCP("npx ..."))

传输自动识别。 构造时按字符串判定用哪种传输(mcp/mcp.py:263 起、独立函数 get_transport_type,mcp/mcp_transport.py:115):

目标字符串传输客户端
ws:// / wss://WebSocketWebSocketMCPClient
http(s)://…/sseSSE(旧式)SSEMCPClient
其它 http(s)://Streamable HTTPHTTPStreamMCPClient
命令字符串(如 npx …)stdioMCPToolRunner(独立线程)

stdio 是怎么跑的。 MCPToolRunner(mcp/mcp.py:25)是个守护线程,内部跑一个 asyncio 事件循环,initializelist_tools 拉到工具清单,然后从队列里收"调用请求"逐个 session.call_tool。用独立线程 + 队列,是为了把 MCP 的异步世界桥接到同步的工具调用世界。

动态造函数(最巧的一处)。 stdio 工具没有 Python 签名,_create_tool_wrapper(mcp/mcp.py:403)读 MCP 工具的 inputSchema,把 JSON 类型映射回 Python 类型(string→str、integer→int…),用 inspect.Parameter 手工拼出一个函数签名贴到 wrapper 上(mcp/mcp.py:465-487)。这样 3.1 节那套"从签名生成 schema"的机制就能无差别地处理 MCP 工具。

执行路径。 运行期在 _execute_tool_impl 里,先检测 self.tools 是不是 MCP 实例、或列表里含 MCP 实例,是就按传输类型在对应客户端里找同名工具并调用(tool_execution.py:1527-1581)——即 MCP 工具和本地工具共用同一条执行管线,一样过审批/熔断/包裹。

MCP 侧安全。 mcp/mcp_security.py 提供服务端护栏:Origin 校验防 DNS 重绑定(is_valid_origin :30is_potential_dns_rebinding :110)、默认只绑 localhost(SecurityConfig :215)、Bearer/Basic 鉴权头、密码学安全的 session id(generate_secure_session_id :184)。


4. 内置工具族(只列不逐个讲)

tools/ 目录自带一大批开箱工具,风格统一(纯函数 + docstring),需要哪族按文件去看:

工具族文件大致能力
文件读写tools/file_tools.pyedit_tools.py读/写/编辑/打补丁
Shell / 系统tools/shell_tools.py执行命令、进程管理
Python 执行tools/python_tools.py跑 Python 代码
Web 搜索tools/web_search.pyduckduckgo_tools.pytavily_tools.pyexa_tools.pysearxng_tools.py各家搜索
网页抓取tools/crawl4ai_tools.pyspider_tools.pyweb_crawl_tools.py爬取/抓取
协作类tools/github_tools.pyjira_tools.pyemail_tools.pymessaging_tools.py对接外部服务
agent 内务tools/memory.pytodo_tools.pyschedule_tools.pyclarify.pysubagent_tool.py记忆/待办/调度/澄清/子 agent

这些大多被 3.4 的护栏点过名(如 web_search 属外部工具、execute_command/write_file 属危险工具)。


5. 巧妙之处(可借鉴的技术)

  • 定义期就 round-trip 校验 schema。 装饰工具那一刻就把 schema JSON 序列化再反序列化比对,把"发不出去的 schema"在开发时抓死,而非运行时炸(base.py:337 validate_schema_roundtrip)。
  • 参数容错前置。 认定"LLM 一定会把参数写脏",专门用 _cast_arguments 洗 kwarg 名、纠类型(tool_execution.py:356),把脆弱性挡在真实函数之外。
  • 两层重试明确分工、避免空转。 内层按错误类型精准重试,外层兜底,并用注释级说明协调二者(tool_execution.py:634-643),这是很多重试实现踩过的坑。
  • 外部内容打隔离标记 + 系统提示配套。<external_tool_result> 围栏 + 转义 + 提示词三件套一起防提示词注入,而不是指望模型自觉(tools/trust.py:52:151)。
  • 给无签名的 MCP 工具手工造签名。 让协议来的工具复用同一套 schema/执行机制,避免为 MCP 单开一条分支(mcp/mcp.py:403)。
  • 注入用 ContextVar 而非全局。 多 agent 并发时状态不串台(injected.py:81-95)。

6. 边界与局限(诚实)

  • 多 MCP server 的工具名前缀防撞尚未实现。 load_mcp_toolsprefix_tools 参数和 mcp.py 里的前缀逻辑都留着 TODO,多 server 同名工具会撞(mcp/loader.py:79-88)。
  • 裸函数解析会回落到 globals()/__main__ 老路径 _generate_tool_definition(agent/agent.py:432-440)会去全局和 __main__ 找函数;但新执行路径 _execute_tool_impl 刻意不回落 globals,找不到就拒(tool_execution.py:1609-1612),两条路径的解析范围不一致。
  • 危险工具清单是硬编码的启发式。 DEFAULT_DANGEROUS_TOOLS 靠名字匹配(approval/registry.py:33);自定义的危险工具若不主动登记信任级/审批要求,不会被自动拦。
  • 错误分类靠关键词。 _classify_error_type 用消息里的 "timeout"/"connection" 等子串判类型(tool_execution.py:1890),措辞不含这些词的错误会被归为 unknown、少一轮重试。
  • 熔断参数写死。 failure_threshold=5/recovery_timeout=60s_impl 里硬编码(tool_execution.py:1468),不随工具区分。

7. 横向对比(同货架兄弟)


8. 代码地图(导航索引)

按符号名 grep 比按行号更抗漂移。路径均相对 clone 根 src/praisonai-agents/praisonaiagents/

主题文件符号
工具装饰器tools/decorator.pytoolFunctionTool
工具基类/结果tools/base.pyBaseToolToolResultvalidate_toolvalidate_schema_roundtrip
签名→schematools/schema.pyannotation_to_json_schemabuild_parameters_schemaget_parameter_requirements
注册表tools/registry.pyToolRegistryregister_toolget_registryget_tool_definitions
状态注入tools/injected.pyInjectedAgentStatewith_injection_contextinject_state_into_kwargs
执行入口agent/tool_execution.pyexecute_tool_execute_tool_with_context
参数纠正agent/tool_execution.py_cast_arguments
审批agent/tool_execution.py_check_tool_approval_sync_resolve_approval_decision
危险工具/预设approval/registry.pyDEFAULT_DANGEROUS_TOOLSPERMISSION_PRESETS
熔断agent/tool_execution.pytools/circuit_breaker.py_execute_tool_with_circuit_breakerCircuitState
重试策略tools/retry.pyagent/tool_execution.pyRetryPolicyBackoffPolicy_get_tool_retry_policy_classify_error_type
信任包裹tools/trust.pywrap_if_externalToolTrustLevelget_system_prompt_addition
路径/URL 安全tools/path_safety.pytools/url_safety.pyresolve_within_rootis_safe_http_url
白名单过滤allowed_tools_filter.pyAllowedToolsFilterfilter_tools_with_allowed_tools
工具检索tools/tool_search.pyshould_defer_toolsBM25ToolSearcherresolve_underlying_callPRAISONAI_CORE_TOOLS
MCP 集成mcp/mcp.pyMCPMCPToolRunner_create_tool_wrapperto_openai_tool
MCP 加载/传输mcp/loader.pymcp/mcp_transport.pyload_mcp_toolsget_transport_type
MCP 安全mcp/mcp_security.pyis_valid_originSecurityConfiggenerate_secure_session_id