跳到主要内容

工具系统:从 @tool 到并发执行与 MCP

30 秒导读: 大模型本身不能"做事",它只能在回复里说一句"我要调用 weather(city='Tokyo')"——这在协议里叫 toolUse(一段带名字和入参的 JSON)。本章讲清 Strands 怎么把这句话变成一次真实的 Python 函数调用:先在启动时把你的函数抽成模型能读懂的 ToolSpec(名字 + 描述 + JSON schema),运行时再校验、配对、并发执行,最后把返回值包成 toolResult 喂回模型。循环调度本身不在这里讲——上一章 01-agent-loop.md_handle_tool_execution 已经讲过"什么时候该调工具";本章讲的是"调用这件事本身怎么落地"。


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

一句话定义: 工具系统 = 把普通 Python 函数"翻译"成模型能理解、框架能安全执行的可调用单元。

它要解决的问题。 模型是个只会说话的大脑,没有手脚。你想让它查天气、读文件、调数据库,就得:

  1. 事先告诉模型"你有哪些工具、每个工具要什么参数"——这是 描述 问题。
  2. 模型说"我要调 X(参数…)"之后,真的去调、还要防它乱传参数——这是 执行 + 校验 问题。

用起来什么样。 对使用者,门槛低到只有一个装饰器:

from strands import Agent, tool

@tool
def weather(city: str, unit: str = "celsius") -> str:
"""查询某个城市的当前天气。

Args:
city: 城市名,例如 "Tokyo"。
unit: 温度单位,celsius 或 fahrenheit。
"""
return f"{city}: 22°{unit[0].upper()}"

agent = Agent(tools=[weather])
agent("东京今天多少度?") # ① 模型自己决定调 weather
agent.tool.weather(city="Tokyo") # ② 你也能像普通方法一样直接调

注意这里 同一个 weather 有两种调用形态:模型驱动的自动调用(①),和你手动的直接调用(②)。装饰器让它俩共用一套校验与执行逻辑,这是本章第一个要拆开的魔法。

一句话直觉:@tool 想成一台"双向翻译机"——正向把 Python 的类型注解 + docstring 翻译成模型能读的 JSON schema;反向把模型吐出的 JSON 翻译回一次带类型校验的真实函数调用。


2. 顶层全景(一次工具调用怎么流动)

先看一次工具调用从模型输出到结果回流的主线。怎么读这张图:从上往下是时间顺序,左边是"启动时一次性做的事",右边是"每次调用做的事"。

启动时(一次性) 运行时(每一轮)
───────────────── ─────────────────────────────────
@tool 装饰函数 模型输出 message(含 toolUse[])
│ 抽取 name/描述/schema │
▼ ▼
DecoratedFunctionTool ──register──► ① validate_and_prepare_tools
(一个 AgentTool) ToolRegistry 校验名字 / 剔除非法 id
│ │
│ get_all_tool_specs ▼
└────────────► ② agent.tool_executor._execute
(喂给模型的 │ 并发 or 顺序
工具清单) ▼
③ ToolExecutor._stream(每个 toolUse)
├ 查注册表拿到 AgentTool
├ Before 钩子(可取消/改参/拦截)
├ selected_tool.stream(...)
│ └ 校验入参→调函数→包成 toolResult
└ After 钩子(可重试)


toolResult[] 回填进 messages,喂回模型

部件一句话职责:

部件干什么文件(相对 clone 根)
@tool / DecoratedFunctionTool把函数抽成 ToolSpec,并实现执行入口 streamstrands-py/src/strands/tools/decorator.py
AgentTool(抽象基类)所有工具的统一接口:tool_name / tool_spec / streamstrands-py/src/strands/types/tools.py:212
ToolRegistry注册、查找、把 spec 清单暴露给模型strands-py/src/strands/tools/registry.py:32
validate_and_prepare_tools从模型消息里抽出 toolUse,校验名字,剔非法 idstrands-py/src/strands/tools/_validator.py:8
ToolExecutor(基类)单个工具的完整执行流程(查找/钩子/追踪/重试/错误)strands-py/src/strands/tools/executors/_executor.py:32
ConcurrentToolExecutor默认执行器:多个工具并发跑strands-py/src/strands/tools/executors/concurrent.py:19
MCPAgentTool / MCPClient把远端 MCP 工具包成本地 AgentToolstrands-py/src/strands/tools/mcp/
StructuredOutputTool + 上下文用"强制工具调用"逼模型输出结构化结果strands-py/src/strands/tools/structured_output/

主线一句话:注册表在启动时收集工具、把 spec 清单给模型 → 模型点名要调哪些 → 校验器筛掉非法的 → 执行器逐个/并发地把它们跑起来、包成结果回流。


3. @tool 装饰器:从函数到 ToolSpec

3.1 它要解决的小问题

模型看不懂 Python 函数。它只认一份 ToolSpec——{name, description, inputSchema}。所以装饰器的第一份工作,是把函数签名"逆向"成这份 spec。

Strands 不自己手写 schema 生成器,而是借道 Pydantic:先用函数的参数和类型注解动态造一个 Pydantic 模型,再让 Pydantic 吐 JSON schema,最后清洗掉 Pydantic 特有的杂质。

3.2 元数据抽取的三个来源

FunctionToolMetadata(decorator.py:79)在装饰那一刻就把三样东西挖出来:

抽什么从哪来关键代码
工具名函数名(可被 @tool(name=...) 覆盖)extract_metadata decorator.py:283
描述docstring 去掉 Args 段之后的全部_extract_description_from_docstring decorator.py:233
参数 schema类型注解 + docstring 里每个参数的说明_create_input_model decorator.py:190

描述抽取有个细节容易忽略:它保留 Returns / Raises / Examples 段,只砍掉 Args 段(decorator.py:256-269)。因为参数说明已经进 schema 了,再塞进描述是重复;但返回值、示例对模型选工具有用,得留着。

参数说明的优先级链(_extract_annotated_metadata decorator.py:161-165):Annotated 里的字符串 > docstring 里的 Args: 描述 > 兜底 "Parameter x"。这解释了为什么你既能写 docstring,也能写 param: Annotated[str, "城市名"]

一个刻意留下的坑:Annotated 里塞 pydantic.Field(想加 ge=0 这类约束)会直接抛 NotImplementedError(decorator.py:155-159)。注释里解释了原因——Pydantic v2 的 Core Schema 一旦构建就冻结,事后改 FieldInfo 不可靠,所以干脆封掉。想加约束请写在 docstring 里。

3.3 生成并清洗 JSON schema

Pydantic 吐出的 schema 带一堆框架自己不需要、甚至会干扰模型的字段(title$defsadditionalProperties)。_clean_pydantic_schema(decorator.py:314)把它们删掉,并做一个关键简化:

Optional[X](Pydantic 表示成 anyOf: [X, null])在"非必填"时压平成裸 X 但如果字段是必填的可空类型,就保留 anyOf——否则模型没法合法地传 null(decorator.py:344-358)。这个"必填才留 anyOf"的判断是容易读漏但很要紧的正确性细节。

# 示意,非源码:清洗前后对比
# 清洗前(Pydantic 原样):
{"unit": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Unit"}}
# 清洗后(非必填,压平):
{"unit": {"type": "string", "description": "..."}}

3.4 两种调用形态,一套逻辑

DecoratedFunctionTool(decorator.py:446)同时是"一个 AgentTool"和"一个还能当普通函数用的可调用对象"。它靠两个方法撑起两种形态:

  • __call__(decorator.py:518) —— 直接透传给原函数,weather("Tokyo") 就跟没装饰过一样跑。
  • stream(decorator.py:585) —— 模型驱动的入口,走"校验 → 注入 → 执行 → 包装"的完整管线(下一节)。

还有个 __get__(decorator.py:485)实现了描述符协议:当 @tool 装在类方法上、通过实例访问时,自动把 self 绑好,返回一个绑定了实例的新 DecoratedFunctionTool。这就是为什么工具既能是模块级函数,也能是类的方法。

至于 agent.tool.weather(city=...) 这条"直接调用"链路,入口其实在 _caller.py_ToolCaller.__getattr__(strands-py/src/strands/tools/_caller.py:49):它把方法名(下划线)映射回真实工具名(可能带连字符),伪造一个 toolUse,然后复用同一个 ToolExecutor._stream(_caller.py:119)。所以直接调用和模型调用最终跑的是同一段执行代码,只是入口不同。

3.5 stream:一次执行的四步

DecoratedFunctionTool.stream(decorator.py:585-669)是"模型说要调" → "函数真的跑完"的核心。四步:

toolUse{toolUseId, input}

├─① validate_input(input) ← Pydantic 校验+类型转换,失败抛 ValueError
├─② inject_special_parameters(...) ← 注入 agent / tool_context 等框架参数
├─③ 按函数类型执行:
│ async 生成器 → 逐个 yield ToolStreamEvent,最后一个当结果
│ async 函数 → await 拿结果
│ 普通同步函数 → asyncio.to_thread(...) 丢线程池,不堵事件循环
└─④ _wrap_tool_result(...) ← 把返回值包成标准 toolResult

几个值得记住的点:

  • 特殊参数注入(inject_special_parameters decorator.py:395):如果你的函数签名里写了 agenttool_context,框架会自动把当前 agent、工具上下文塞进去。这些参数不进 schema(_is_special_parameter decorator.py:419self/cls/agent 和配置的 context 参数排除掉),模型看不到也不用传。
  • 同步函数用 asyncio.to_thread(decorator.py:638):普通阻塞函数被丢到线程里跑,避免堵住整个 async 事件循环——这是"并发执行"能成立的前提之一。
  • 错误不抛、而是包成 error 结果(decorator.py:645-669):校验失败或函数抛异常,都被捕获、包成 status: "error" 的 toolResult 回给模型,让模型有机会自己纠错重试。唯一例外是 InterruptException,它走人类介入(human-in-the-loop)的中断分支。
  • 结果格式自适应(_wrap_tool_result decorator.py:671):已经是 {status, content} 形状就只补 toolUseId;否则字符串原样、Pydantic 模型 model_dump_json()、其它 json.dumps,兜底 str()

4. 工具注册表 ToolRegistry

4.1 它管什么

ToolRegistry(registry.py:32)是所有工具的中央登记处。两个字典:

  • registry —— 全部工具(名字 → AgentTool)。
  • dynamic_tools —— 其中"运行时动态加入 / 可热加载"的子集。

process_tools(registry.py:46)是入口,负责把五花八门的输入统一收编成 AgentTool:文件路径字符串、模块导入路径、导入好的模块、@tool 实例、ToolProvider(如 MCPClient),甚至另一个 Agent(自动 .as_tool() 包装)。

4.2 注册时的两道防重名

register_tool(registry.py:238)不只是往字典塞。它挡两种重名:

  1. 完全同名 —— 除非工具支持热加载(supports_hot_reload),否则抛错(registry.py:252)。
  2. 归一化同名 —— 把 -_ 都看成一样,my-toolmy_tool 不能共存(registry.py:257-271)。

第 2 道防线是为了配合 agent.tool.my_tool() 这种调用:Python 标识符不能带连字符,所以框架用下划线名去匹配连字符工具名。如果同时存在两个只差 -/_ 的工具,这个映射就会有歧义,于是干脆在注册时禁掉。

4.3 暴露给模型的 spec 清单

模型要看到的是 spec 列表,由 get_all_tools_config(registry.py:198)/ get_all_tool_specs(registry.py:573)生成。每个 spec 会先 normalize_tool_spec 归一化、再 validate_tool_spec 校验(registry.py:213-214);校验失败的工具被跳过而非报错(只 logger.warning),保证一个坏工具不拖垮整个 agent。

validate_tool_spec(registry.py:598)顺手补齐 schema 缺的字段:没有 type"object"、没有 properties{}、属性缺 type"string"(除非它用了 anyOf/oneOf/allOf/not 这类组合关键字,见 _COMPOSITION_KEYWORDS tools.py:20)。

schema 归一化的一处安全考量: normalize_schema(tools.py:116)和 _normalize_property 互相递归处理嵌套对象,靠 _MAX_SCHEMA_DEPTH = 64(tools.py:26)兜底——一个恶意 MCP 服务器塞来一份深度爆表的 schema,会得到 ValueError 而不是把解释器栈打爆(RecursionError)。


5. 校验与配对:validate_and_prepare_tools

模型给的 toolUse 不能直接信。validate_and_prepare_tools(_validator.py:8)在执行前做两件事:

  1. 抽取 —— 从模型 message 的 content 里把所有 toolUse 捞出来放进 tool_uses
  2. 校验名字 —— 对每个 toolUse 调 validate_tool_use(tools.py:41)。名字必须匹配 ^[a-zA-Z0-9_\-]{1,}$ 且不超过 64 字符(validate_tool_use_name tools.py:50-79)。

名字非法的怎么办?不丢弃,而是配对一条 error toolResult(_validator.py:39-45),并把该 id 记进 invalid_tool_use_ids。这背后是一条硬约束:每个 toolUse 必须有一个对应的 toolResult,否则下一轮对话消息结构非法、会被模型 API 拒掉。所以非法工具也得"假装执行"、返回一条错误结果占位。

回到 _handle_tool_execution(event_loop.py:698),校验完会把非法 id 从待执行列表里滤掉(event_loop.py:737),真正要跑的只剩合法工具,再交给执行器:

# 真实源码 event_loop.py:788,工具执行的统一入口
tool_events = agent.tool_executor._execute(
agent, tool_uses, tool_results, cycle_trace, cycle_span, invocation_state, structured_output_context
)

6. 执行器:从 _execute 到并发

6.1 分层:策略(怎么调度)与流程(单个怎么跑)分开

执行被拆成两层,这是本节最关键的设计:

  • ToolExecutor._stream(_executor.py:93) —— 单个 toolUse 的完整生命周期:查注册表、跑前后钩子、追踪打点、错误兜底、中断处理、重试。与"并发还是顺序"无关。
  • _execute(抽象方法 _executor.py:349) —— 调度策略:决定这批 toolUse 是并发跑还是排队跑。子类只需实现它。

这样并发/顺序两种执行器都只关心"怎么编排多个任务",而每个任务内部那套复杂流程完全复用。

6.2 单个工具的流程:_stream 里发生了什么

_stream(_executor.py:93-288)是个 async 生成器,按顺序:

查注册表(先 dynamic_tools 后 registry)拿到 tool_func


while True: ← 重试循环,钩子可把 after_event.retry=True 来重跑
├ Before 钩子 ─┬─ 有 interrupt? → yield ToolInterruptEvent,停(人类介入)
│ └─ cancel_tool? → 包一条 cancel 结果,停
├ selected_tool 为空? → "Unknown tool" error 结果
├ selected_tool.stream(tool_use, ...) 逐事件转发:
│ ToolStreamEvent → 直接 yield(流式中间输出)
│ ToolResultEvent → 记下 exception,取出 tool_result,break
├ After 钩子(可改结果 / 请求重试)
└ yield ToolResultEvent(最终结果),append 进 tool_results

要点:

  • 查找顺序 dynamic_tools 优先于 registry(_executor.py:127-128),让运行时动态工具能覆盖同名静态工具。
  • 钩子是干预点:Before 钩子能取消工具、改入参、甚至换掉选中的工具;After 钩子能改结果或请求重试。这套 hooks 机制是控制面的核心,详见 04-control-plane.md,本章只标出它们在执行流里的位置。
  • _stream_with_trace(_executor.py:290)_stream 外面再套一层 OpenTelemetry span + 指标(耗时、成败),两种执行器都通过它来跑单个工具。

6.3 默认并发:ConcurrentToolExecutor

ConcurrentToolExecutor._execute(concurrent.py:22)是默认策略。它给每个 toolUse 起一个 asyncio 任务,让它们同时跑;但事件要有序、单路地 yield 出去。协调靠一个队列 + 每任务一个 Event:

每个 _task: 主 _execute 循环:
产生一个 event 从 task_queue 取 (task_id, event)
put 进 task_queue │
等自己的 task_event 被 set ◄────────┤ 若是 stop 哨兵 → 该任务完成计数-1
(背压:主循环消费完才放行) │ 若是 Exception → 抛出
└ 否则 yield event,再 set 对应 task_event 放行

这个"put 一个 → 等 event 被 set → 再 put 下一个"(concurrent.py:125-128)是一种背压(backpressure):任务并发地算,但谁的事件被主循环消费之前不许抢跑,保证下游拿到的事件流单路、可控。收尾用标准的"task.cancel() + gather(..., return_exceptions=True)"清理(concurrent.py:87-90),异常从队列冒泡到主循环再 raise(concurrent.py:80-81)。

顺序执行器 SequentialToolExecutor(sequential.py:18)则简单得多:for 循环逐个跑,遇到中断(ToolInterruptEvent)就 break——因为人类介入必须停下等回应,不能继续跑后面的工具。

选哪个?agent 上的 tool_executor 属性决定,默认是并发。


7. MCP 集成:把远端工具当本地工具

7.1 思路

MCP(Model Context Protocol,模型上下文协议)让工具跑在另一个进程/服务器里。Strands 的做法是:把每个远端 MCP 工具包成一个普通的 AgentTool,这样注册表、执行器、钩子全都不用知道"它其实在远端"。

包装器是 MCPAgentTool(mcp/mcp_agent_tool.py:24)。它把 MCP 的工具描述翻译成 Strands 的 ToolSpec(mcp_agent_tool.py:65-85,含可选的 outputSchema),它的 stream(mcp_agent_tool.py:96)不自己算,而是把调用委托给 MCPClient:

# 真实源码 mcp_agent_tool.py:113,委托给客户端,拿回结果直接 yield
result = await self.mcp_client.call_tool_async(
tool_use_id=tool_use["toolUseId"],
name=self.mcp_tool.name, # 用原始 MCP 名跟服务器通信
arguments=tool_use["input"],
read_timeout_seconds=self.timeout,
)
yield ToolResultEvent(result)

注意 name_override:对外(给模型)可以用带前缀的名字消歧义,但跟服务器通信时始终用原始 MCP 名(mcp_agent_tool.py:52:115)。

7.2 MCPClient:一个后台线程里的 async 世界

MCPClient(mcp/mcp_client.py:105)本身是个 ToolProvider。它最有意思的是线程模型:MCP 的会话是 async 的,但 Strands 的很多调用点是同步的,于是它把整个 MCP async 事件循环塞进一个独立的后台守护线程里(start mcp_client.py:196_background_task mcp_client.py:876)。

所有对服务器的调用都通过 _invoke_on_background_thread(mcp_client.py:983)用 asyncio.run_coroutine_threadsafe 跨线程投递:

主线程(可能是 sync 也可能 async) 后台线程(MCP 的 async 事件循环)
call_tool_async / list_tools_sync ──► run_coroutine_threadsafe(coro)
│ wrap_future / .result() 实际的 session.call_tool(...)
◄──────────── 结果跨线程回传 ──────────┘

_invoke_on_background_thread 里还塞了一个巧思:把真正的调用和一个 close_future 一起 asyncio.wait(FIRST_COMPLETED)(mcp_client.py:995-1002)——万一会话中途关闭,所有挂起的调用会立刻以 RuntimeError 收场,而不是永久卡住。

工具列表由 list_tools_sync(mcp_client.py:410)拉取,每个远端工具 new 成一个 MCPAgentTool(mcp_client.py:456-459)。错误处理也做了翻译:MCP 的 elicitation-required 错误(code -32042)会被解析成结构化的 error 结果(_handle_tool_execution_error mcp_client.py:726),而不是裸异常逃逸。

长任务(task-augmented execution)是实验特性:call_tool_async 会根据服务器能力和工具的 taskSupport 自动决定走不走"创建任务→轮询→取结果"的流程。这条支线本章不展开。


8. 结构化输出:用"强制工具调用"逼出 Pydantic 对象

8.1 思路

你想让 agent 最后返回一个严格符合某 Pydantic 模型的对象(而不是自由文本)。Strands 的巧妙之处:不新造一套机制,而是复用工具调用——把"输出结构化结果"本身做成一个工具,逼模型调它。

核心是 StructuredOutputTool(structured_output/structured_output_tool.py:26):它用 convert_pydantic_to_tool_spec(structured_output_utils.py:260)把你的 Pydantic 模型转成一份 ToolSpec(展开嵌套模型、解析 $ref、区分必填/可空)。它的 stream(structured_output_tool.py:95)做的事就是拿模型传来的入参去实例化那个 Pydantic 模型:成功就存进上下文,失败就把 Pydantic 的校验错误列表包成 error 结果、让模型重试(structured_output_tool.py:124-146)。它的描述里还硬编码了一句"这应该是你返回前调用的最后一个工具"(structured_output_tool.py:38-42),引导模型正确使用。

8.2 StructuredOutputContext 与 force 模式

StructuredOutputContext(structured_output/_structured_output_context.py:19)是每次调用的上下文,管两件事:注册/清理这个临时工具(register_tool :134cleanup :144),以及协调"强制"逻辑。

关键在事件循环里(event_loop.py:357-366):如果开了结构化输出,但模型自己 end_turn 了却没调结构化工具,框架就进入 force 模式:

模型正常收尾(end_turn)但没输出结构化结果

├ 已经强制过一次了(force_attempted)? → 抛 StructuredOutputException(别死循环)

└ set_forced_mode() ← forced_mode=True, tool_choice={"any":{}}
│ 追加一条 user 提示("请把上面的回答格式化成结构化输出")

递归回事件循环:这一轮只暴露结构化工具这一个 spec、
并用 tool_choice 强制模型必须调它

set_forced_mode(_structured_output_context.py:77)把 tool_choice 设成 {"any": {}},于是下一次调模型时(event_loop.py:518-522)工具清单被替换成只有结构化工具一个,tool_choice 强制模型必须调工具——模型没有别的选择,只能吐出符合 schema 的结果。结果由 extract_result(_structured_output_context.py:113)从上下文里取出,并置 stop_loop=True 结束循环(event_loop.py:798-801)。

force_attempted 标志是防死循环的关键:强制过一次还失败,直接抛异常而不是无限重试。


9. 工具热加载(一句带过)

放在 ./tools/ 目录里的 .py 文件可以在运行时被发现和重载:ToolRegistry.discover_tool_modules(registry.py:337)扫目录,reload_tool(registry.py:362)按文件名重新 import 并重新 register_tool;ToolWatcher(watcher.py:19)用 watchdog 监听文件改动、自动触发重载;loader.py 提供从字符串路径 / 模块 / 函数目标加载工具的各种入口(load_tool_from_string loader.py:23);ToolProvider(tool_provider.py:11)是"工具来源"的抽象接口,MCPClient 就是它的一个实现。这些是外围便利设施,不影响前面讲的核心执行路径。


10. 巧妙之处(可借鉴)

  • 借 Pydantic 造 schema,而不是手写。 动态 create_model + model_json_schema() + 清洗,几十行就覆盖了类型注解到 JSON schema 的转换(decorator.py:190:314)。
  • 一个对象两种身份。 DecoratedFunctionTool 既是 AgentTool 又是可调用函数,__call__ 走原函数、stream 走工具管线,直接调用和模型调用还共用同一个 ToolExecutor._stream(_caller.py:119)——零重复。
  • 执行分两层:策略 vs 流程。 单工具的复杂流程(钩子/追踪/重试/错误)只写一遍,并发/顺序只写"怎么编排"。加新策略(比如带限流的执行器)只实现 _execute 一个方法。
  • 并发但有序的 yield。 用"每任务一个 Event"做背压(concurrent.py:125-128),兼顾并发算力和单路事件流。
  • 非法工具也配对结果。 宁可返回 error toolResult 也不丢弃(_validator.py:39),死守"toolUse 与 toolResult 一一对应"的协议约束。
  • 结构化输出 = 工具调用的再利用。 不造新机制,把"输出结构化"做成一个被强制调用的工具(structured_output_tool.py),复用整套校验/执行/重试。

11. 边界与局限(诚实)

  • Annotated[..., Field(...)] 里的 Pydantic 约束不支持,会直接抛 NotImplementedError(decorator.py:155);约束只能写进 docstring(且不会变成 schema 里的机器约束)。
  • schema 归一化有 64 层深度上限(tools.py:26),超深 schema 抛 ValueError
  • spec 校验失败的工具是静默跳过(只 warning),排查时要看日志才知道某工具没被暴露给模型(registry.py:217-218)。
  • MCP 客户端依赖后台线程存活;会话关闭时挂起调用会以 RuntimeError 收场(mcp_client.py:995-1002)。
  • 结构化输出强制一次仍失败即抛异常(event_loop.py:358-361),不会无限重试。

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

用符号名 grep 比行号抗漂移。路径相对 clone 根(strands-agents/)。

主题文件符号
@tool 装饰器入口strands-py/src/strands/tools/decorator.pytool / DecoratedFunctionTool
元数据/schema 抽取strands-py/src/strands/tools/decorator.pyFunctionToolMetadata / _create_input_model / _clean_pydantic_schema
装饰工具的执行入口strands-py/src/strands/tools/decorator.pyDecoratedFunctionTool.stream / _wrap_tool_result
工具统一接口strands-py/src/strands/types/tools.pyAgentTool
注册表strands-py/src/strands/tools/registry.pyToolRegistry / process_tools / register_tool / get_all_tool_specs
spec 归一化/校验strands-py/src/strands/tools/tools.pynormalize_schema / validate_tool_use_name / _MAX_SCHEMA_DEPTH
校验与配对strands-py/src/strands/tools/_validator.pyvalidate_and_prepare_tools
执行流程(单工具)strands-py/src/strands/tools/executors/_executor.pyToolExecutor._stream / _stream_with_trace / _execute
并发/顺序策略strands-py/src/strands/tools/executors/concurrent.py · sequential.pyConcurrentToolExecutor._execute / _task · SequentialToolExecutor
执行入口(来自循环)strands-py/src/strands/event_loop/event_loop.py_handle_tool_execution(见 agent.tool_executor._execute)
直接调用 agent.tool.xstrands-py/src/strands/tools/_caller.py_ToolCaller.__getattr__
MCP 工具包装strands-py/src/strands/tools/mcp/mcp_agent_tool.pyMCPAgentTool
MCP 客户端/线程模型strands-py/src/strands/tools/mcp/mcp_client.pyMCPClient / _background_task / _invoke_on_background_thread / call_tool_async
结构化输出工具strands-py/src/strands/tools/structured_output/structured_output_tool.pyStructuredOutputTool
结构化输出上下文/forcestrands-py/src/strands/tools/structured_output/_structured_output_context.pyStructuredOutputContext / set_forced_mode / extract_result
Pydantic→ToolSpecstrands-py/src/strands/tools/structured_output/structured_output_utils.pyconvert_pydantic_to_tool_spec
热加载/来源strands-py/src/strands/tools/watcher.py · loader.py · tool_provider.pyToolWatcher · load_tool_from_string · ToolProvider

同组其它章:index.md(总览)· 01-agent-loop.md(事件循环)· 03-models.md(模型抽象层)· 04-control-plane.md(hooks/中断/人类介入)· 05-context-and-durability.md · 06-sandbox-and-multiagent.md