工具调用与 Agent 智能体循环
30 秒导读: 模型只会「说」——它输出一段结构化的
tool_call("我要调用bash,参数command="ls"")。本章讲 Inspect 怎么给模型装上手脚:把一个普通 Python 函数变成模型能看懂的工具(抽 JSON schema)、把模型说的那段调用精确、可容错地落到真实函数上(查找 / 审批 / 校验 / 反序列化 / 执行 / 收结果),再把这些工具编成一个会自己转的 Agent 循环(react),最后讲多个 Agent 怎么互相移交(handoff)与嵌套(as_tool)。
本章在全书里的位置:
- 模型如何产出
tool_call(把工具列表发给 OpenAI/Anthropic、解析回来的函数调用)——归 03 统一模型层。 - 工具真正跑在哪(docker/沙箱、RPC 到容器)——归 06 日志与沙箱。
- 本章只管中间那一段:工具的定义、
tool_call的落地执行、以及 Agent 的编排与循环。
1. 这是什么(零基础也能懂)
一句话定义: 工具(Tool)是一个模型可以请求调用的异步 Python 函数;Agent 是一个"读对话、调工具、再读对话"反复循环直到交答案的自动程序。
1.1 为什么需要"手脚"
模型本身是个纯文本函数:给它一段对话,它回一段话。它不能真的读文件、跑命令、查数据库。
要让它干实事,得给它工具。但难点从来不是"调用模型",而是这三件事:
| 要解决的问题 | 白话 | 本章对应 |
|---|---|---|
| 让模型知道有哪些工具、每个怎么用 | 把 Python 函数翻译成模型能读的 JSON schema | §3 工具定义 |
| 把模型说的调用落到真实函数上 | 查找函数、校验参数、容错、跑、收结果 | §4 执行循环 |
| 让这一切自动转起来、还能多智能体协作 | 循环 + 移交 | §5–§7 Agent |
1.2 用起来什么样
定义一个工具,就是写个带 @tool 的工厂函数,返回内部的 execute:
from inspect_ai.tool import tool, Tool, ToolResult
@tool
def add() -> Tool: # 外层:工具工厂
async def execute(x: int, y: int) -> int: # 内层:真正被调用的实现
"""把两个整数相加。
Args:
x: 第一个加数
y: 第二个加数
"""
return x + y
return execute
把它交给一个 react Agent,模型就会在需要时自己调用它:
from inspect_ai.agent import react
agent = react(tools=[add()]) # 一个会自主循环调工具的 Agent
# agent 内部:generate → 模型说"调 add(x=2,y=3)" → 执行得 5 → 再 generate → submit 答案
关键直觉:@tool 只是把函数注册并附上元信息;模型真正看到的是从这个函数 的类型签名 + docstring 自动抽出来的一份 JSON schema。你写 Python,Inspect 负责翻译。
1.3 一句话类比
- 工具 = 给模型的一根遥控器按钮:按钮上印着名字和说明(schema),按下去(
tool_call)真的会动(execute)。 - react Agent = 一个不知疲倦的操作员:看一眼屏幕(对话)、按一个按钮(工具)、再看屏幕、再按……直到按下"提交"(submit)。
本节不出现底层细节。记住一件事:模型说的和真实世界之间隔着一层"翻译 + 落地",这层就是本章的主角。
2. 顶层全景(它大概怎么转)
2.1 一次工具调用的生命周期
下面这张图从左到右是时间顺序,一次跑完停在"结果回到对话":
你写的 Python 函数 模型侧 真实执行
┌──────────────────┐ ①抽schema ┌──────────────────┐ ③tool_call ┌──────────────────┐
│ @tool def add(): │ ────────────▶ │ 模型看到工具清单 │ ───────────▶ │ execute_tools │
│ async execute │ ToolInfo │ 决定调用哪个 │ (函数名+参数)│ 逐个落地 │
└──────────────────┘ (JSON schema)└──────────────────┘ └───────┬──────────┘
│ ④call_tool
▼
⑥ ChatMessageTool ┌───────────────────────────────────┐
┌──────────────────┐ (结果回到对话) │ 找函数→审批→schema校验→反序列化参数 │
│ react 循环 │ ◀────────────────────────────── │ →调用 execute()→截断→包成结果 │
│ 再 generate… │ ⑤结果 └───────────────────────────────────┘
└──────────────────┘
怎么读:①②是定义(§3),③由 03 模型层 产出,④⑤⑥是执行(§4),最外圈的"react 循环"是 Agent(§6)。
2.2 部件一句话职责
| 部件 | 干什么 | 在哪 |
|---|---|---|
@tool / Tool | 注册工具、挂元信息(并行性、viewer 等) | src/inspect_ai/tool/_tool.py:163 / :80 |
ToolInfo | 一份 JSON-Schema 兼容的工具规格,直接发给模型 API | src/inspect_ai/tool/_tool_info.py:24 |
_parse_tool_info_shared | 从签名 + docstring 反射抽出 schema | src/inspect_ai/tool/_tool_info.py:103 |
ToolDef | 统一载体:把 Tool/裸函数规范成 name+desc+params+行为 | src/inspect_ai/tool/_tool_def.py:35 |
execute_tools | 执行最后一条 assistant 消息里的所有 tool_call | src/inspect_ai/model/_call_tools.py:103 |
call_tool | 单个调用的落地:查找→审批→校验→反序列化→调用 | src/inspect_ai/model/_call_tools.py:606 |
Agent / AgentState | Agent 协议 + 它读写的对话状态 | src/inspect_ai/agent/_agent.py:92 / :35 |
react | 内置的 ReAct 循环 agent(本章核心) | src/inspect_ai/agent/_react.py:50 |
handoff / as_tool | 把一个 Agent 包成"移交工具" / "普通工具" | src/inspect_ai/agent/_handoff.py:19 / _as_tool.py:22 |
2.3 主线走一遍(高层)
- 你
react(tools=[add(), bash()])。react 把工具收集好,还会自动加一个submit工具。 - 循环第一轮:react 调模型生成(
_agent_generate),把工具的ToolInfo清单一并发过去。 - 模型回来一条 assistant 消息,里面可能带
tool_calls。 - react 调
execute_tools(state.messages, tools):对每个tool_call走call_tool,把结果包成ChatMessageTool追加进对话。 - 若模型调了
submit,react 把答案写进output.completion,退出循环;否则回到第 2 步继续转。
3. 工具定义体系:从 Python 函数到模型能读的 schema
这一节讲图里的①②:怎么把一个函数变成模型看得懂的工具规格。 由浅入深分五层。
3.1 Tool 协议 与 @tool 装饰器
Tool 是什么? 就是一个"可 await、返回 ToolResult 的可调用对象"这一约定(Protocol),不是基类。任何 async def execute(...) 都天然满足它(src/inspect_ai/tool/_tool.py:80,Tool)。
ToolResult 限定了合法返回类型:str | int | float | bool | Content...,模型 API 只吃这些(src/inspect_ai/tool/_tool.py:35)。
@tool 做的三件事(src/inspect_ai/tool/_tool.py:163,tool):
- 确定工具名(显式
name=或函数名)。 - 用
@wraps包一层tool_wrapper