跳到主要内容

工具系统

本章讲 SDK 怎么把一个普通 Python 函数变成 LLM 能调用的工具:从「读函数签名生成 schema」到「拿 LLM 给的 JSON 参数、校验、执行、容错」的完整链路。

1. 它要解决的小问题

LLM 调工具靠的是「function calling」:你得先告诉模型有哪些工具、每个工具叫什么、参数是什么样的 JSON schema;模型返回一段 JSON 参数;你得把这段 JSON 校验、解析、调用真实函数、把返回值喂回模型。

手写这套很烦。SDK 的目标是:你只写一个带类型注解和 docstring 的普通 Python 函数,加个 @function_tool,剩下全自动。

2. 思路 / 直觉:函数即 schema

核心洞察:一个带类型注解的 Python 函数签名,本身就包含了生成 JSON schema 所需的全部信息。

def get_weather(city: str, days: int = 1) -> str:
"""查天气。
Args:
city: 城市名。
days: 预报天数。
"""
│ │ │
函数名→工具名 参数+类型→schema docstring→工具描述+参数描述

所以 @function_tool 做的就是:用 inspect 读签名、用 griffe 解析 docstring、用 pydantic 把参数变成一个校验模型,最后吐出 JSON schema。

3. 第一步:function_schema —— 把函数变成 schema

function_schema(function_schema.py:224)产出一个 FuncSchema(function_schema.py:23),包含工具名、描述、参数 JSON schema、一个 pydantic 校验模型、以及「函数是否需要 context 作首参」等元数据。

几个关键动作:

取 docstring 信息(function_schema.py:256):自动检测 docstring 风格(Google/Numpy/Sphinx),抽出每个参数的描述。

识别 context 参数(function_schema.py:292):如果函数第一个参数的类型是 RunContextWrapperToolContext,标记 takes_context=True 并把它从「LLM 可见参数」里剔除——这个参数由 SDK 注入,不暴露给模型:

# function_schema.py:296 —— 首参是 context 就标记并跳过
if ann != inspect._empty:
origin = get_origin(ann) or ann
if origin is RunContextWrapper or origin is ToolContext:
takes_context = True # 由 SDK 注入,不进 schema
else:
filtered_params.append((first_name, first_param))

强制约束: context 参数只能在第一位,出现在其他位置直接抛 UserError(function_schema.py:306-312)。

4. 第二步:function_tool 装饰器 —— 包成 FunctionTool

function_tool(tool.py:1899)是个可带参/不带参两用的装饰器。它内部定义一个 _on_invoke_tool_impl,这就是「LLM 调这个工具时实际执行的逻辑」(tool.py:1978):

# 示意,提炼自 tool.py:1978 的 _on_invoke_tool_impl
async def _on_invoke_tool_impl(ctx, input_json): # input_json 是 LLM 给的 JSON 字符串
json_data = _parse_function_tool_json_input(...) # 1. 解析 JSON(失败→ModelBehaviorError)
try:
parsed = schema.params_pydantic_model(**json_data) # 2. pydantic 校验参数
except ValidationError as e:
raise ModelBehaviorError(f"Invalid JSON input ...") from e
args, kwargs = schema.to_call_args(parsed) # 3. 转成位置/关键字参数
if takes_context: # 4. 按需注入 context、按同步/异步分别调用
result = await the_func(ctx, *args, **kwargs)
return result

同步函数怎么办? SDK 用 asyncio.to_thread 把同步工具函数丢到线程池跑,避免阻塞事件循环(tool.py:2004):

# tool.py:2002 —— 同步工具丢线程池,异步工具直接 await
if not is_sync_function_tool:
result = await the_func(ctx, *args, **kwargs_dict) if schema.takes_context else await the_func(*args, **kwargs_dict)
else:
result = await asyncio.to_thread(the_func, ctx, *args, **kwargs_dict) ...

最终它调 _build_wrapped_function_tool(tool.py:599)产出 FunctionTool 这个 dataclass(tool.py:381)。

5. FunctionTool 的字段:工具的全部旋钮

FunctionTool(tool.py:381)就是工具的运行时表示。值得记的字段:

字段作用行号
name / description / params_json_schema暴露给 LLM 的三件套tool.py:386-393
on_invoke_tool实际执行逻辑(上一节那个)tool.py:395
strict_json_schema是否用 OpenAI 的 strict schema(强烈建议 True)tool.py:408
is_enabledbool 或回调,运行时动态开关工具(关掉的工具对 LLM 不可见)tool.py:412
needs_approvalbool 或回调,True 则执行前中断等人类审批(见 04 章)tool.py:426
tool_input_guardrails / tool_output_guardrails工具级护栏tool.py:420-423
timeout_seconds / timeout_behavior单次调用超时 + 超时处理(返回错误字符串 or 抛异常)tool.py:436-439

is_enabledAgentBase.get_all_tools(agent.py:246)里被求值:每次 run 都并发检查所有工具是否启用,只把启用的暴露出去。

6. 容错:工具报错不一定让 run 崩

一个重要设计:工具抛异常时,默认不让整个 run 失败,而是把错误信息当成工具输出喂回给 LLM,让模型自己看到「这个工具报错了」并重试或换路。

这由 failure_error_function 控制(tool.py:1938):默认用 default_tool_error_function 生成一句给模型看的错误消息;如果你显式传 None,则改为直接抛异常让 run 失败。这个权衡——「对模型容错 vs 对调用者暴露」——是 agent 框架里很关键的一个旋钮。

7. agent 即工具(as_tool)

Agent.as_tool()(agent.py:508)把一个 agent 包装成一个 FunctionTool。它和 handoff 的区别(agent.py:533):

  • handoff: 新 agent 接管整个对话历史,接管控制权。
  • as_tool: 新 agent 像被调用的一个函数——父 agent 生成它的输入,它跑完把结果返回,父 agent 继续掌控对话

实现上,as_tool 内部的 _run_agent_impl(agent.py:599)就是「在工具执行里再起一个 Runner.run」,把嵌套 run 的 final_output 当工具结果返回。它还细致处理了嵌套审批:如果被当工具的 agent 内部触发了工具审批中断,父 run 的审批决定会被镜像到嵌套 run(agent.py:694_apply_nested_approvals)。

8. 其它工具类型(托管工具)

除了 FunctionTool(本地 Python 函数),SDK 还支持一批托管工具——这些工具的执行发生在模型服务端,SDK 只负责声明和接收结果:

工具类干什么行号
FileSearchTool向量检索文件tool.py:662
WebSearchTool联网搜索tool.py:688
ComputerTool控制电脑(截图/点击/输入)tool.py:715
HostedMCPTool托管的 MCP 服务器工具tool.py:960
CodeInterpreterTool / ImageGenerationTool代码解释器 / 生图tool.py:981 / tool.py:993
LocalShellTool / ShellTool / ApplyPatchToolshell 执行 / 打补丁(sandbox 相关)tool.py:1020

process_model_response(见 01 章)里,这些类型各有专门的分拣分支;托管工具大多「已经在服务端跑完了」,本地只是把结果纳入历史。

9. 巧妙之处

  • 签名即契约。 用 pydantic + 类型注解,既生成了给 LLM 的 JSON schema,又复用同一个模型在运行时校验 LLM 返回的参数——schema 和校验逻辑永远一致,不会漂移(tool.py:1969 同一个 schema 既出 params_json_schema 又出 params_pydantic_model)。
  • context 注入对模型透明。 首参是 RunContextWrapper 就自动从 schema 里抹掉、运行时注入,让你能在工具里访问运行上下文而不污染给模型的接口。
  • 默认对模型容错。 工具报错变成「给模型看的消息」而非崩溃,贴合 agent「试错—重试」的工作方式。

10. 代码地图

主题文件路径符号名
装饰器入口src/agents/tool.pyfunction_tool
工具运行时表示src/agents/tool.pyFunctionToolFunctionToolResult
实际执行逻辑src/agents/tool.py_on_invoke_tool_impl(在 function_tool 内)、_build_wrapped_function_tool
签名→schemasrc/agents/function_schema.pyfunction_schemaFuncSchema
工具启用检查src/agents/agent.pyAgentBase.get_all_tools
agent 即工具src/agents/agent.pyAgent.as_tool(内部 _run_agent_impl)
工具执行(并发跑)src/agents/run_internal/tool_execution.pyexecute_function_tool_calls
托管工具src/agents/tool.pyWebSearchToolComputerToolHostedMCPTool