第 2 章 · 工具与人审(HITL)
这章讲两件事:一个普通 Python 函数 怎么变成模型能调用的工具(连 JSON schema 都自动生成),以及 Agno 怎么在工具调用处「踩刹车」让人介入——确认、补输入、交给外部系统执行。
2.1 工具的小问题:模型只会输出 JSON
模型不会真的「执行」函数,它只会按工具的 schema 吐一段 JSON(「我想调 get_weather,参数 city="北京"」)。所以框架要做两件事:(1) 把你的函数翻译成模型看得懂的 schema 发过去;(2) 收到模型的 JSON 后,找到对应函数、用解析出的参数调用它。
2.2 核心:Function.from_callable 自动造 schema
Agno 不要你手写 JSON schema。Function.from_callable(tools/function.py:278)用 inspect 读函数签名 + 解析 docstring,自动拼出 schema。
思路演示——你只要写正常的带类型注解和 docstring 的函数:
# 示意,非源码 —— 一个普通函数就能当工具
def get_weather(city: str, units: str = "celsius") -> str:
"""查询某城市的天气。
Args:
city: 城市名,如 "北京"
units: 温度单位,celsius 或 fahrenheit
"""
...
# Agno 会自动得出:
# name="get_weather"
# description="查询某城市的天气。" (取自 docstring 首段)
# parameters={ city: {type:string, ...}, units: {...} }
# required=["city"] (units 有默认值→非必填)
真实实现里几个关键动作(都在 Function.from_callable):
- 类型 → JSON schema:
get_type_hints拿注解,交给get_json_schema(tools/function.py:342)。 - docstring → 参数描述:用
docstring_parser的parse(),把每个Args:项变成参数 description(tools/function.py:328-339)。 - 必填判定:无默认值的参数才进
required(tools/function.py:366-381)。 - 剔除框架注入参数:
agent、team、run_context、images、videos、audios、files、self这些会被自动从 schema 里删掉(tools/function.py:290-324)——它们由框架在调用时注入,不该暴露给模型。这是个很贴心的细节:你的工具函数可以声明def my_tool(query: str, agent: Agent),模型只看到query。
2.3 Toolkit:一组相关工具打包
单个函数是 Function;一批相关函数(如「DuckDuckGo 搜索」的若干方法)用 Toolkit 打包(tools/toolkit.py:12)。Toolkit 内部维护一个 self.functions: Dict[str, Function](toolkit.py:60),通过 register() 把可调用对象注册进去(toolkit.py:155)。Agent 的 tools=[...] 既能放裸函数、也能放 Toolkit、Function 或 dict(见 agent/agent.py:174 的类型签名)。
2.4 执行:在 Model 循环里跑工具
回到第 1 章的 Model while 循环。当模型回了 tool_calls,Model.response 就:
_prepare_function_calls把模型给的 JSON 配对到真实Function(models/base.py:742)。run_function_calls逐个执行(models/base.py:751、定义在:2311)。format_function_call_results把结果塞回messages,然后continue再问模型(base.py:817)。
执行前后还有 tool hooks(中间件):Agent 上的 tool_hooks(agent/agent.py:187)会包在工具调用外面,可做日志、限流、改参数。
2.5 HITL:四种「踩刹车」
Agno 的人审不是另起一套机制,而是在同一个 Model 循环里 break 出来。一个 Function 上有四个开关(tools/function.py:159,171,174,181):
| 开关字段 | 含义 | 循环如何响应 |
|---|---|---|
requires_confirmation | 调用前需人确认 | 不执行,产出暂停标记,break |
requires_user_input | 需人补充参数 | 不执行,带上待填字段 schema,break |
external_execution | 交给外部系统执行 | 不在本地执行,break 交回控制权 |
stop_after_tool_call | 执行后立即停 | 执行,但结果产生后 break |
看 run_function_calls 里如何把「确认/补输入」变成 ToolExecution 暂停标记(models/base.py:2334):
# models/base.py:2334 —— 需要确认的工具不执行,只标记 paused(节选)
if fc.function.requires_confirmation:
paused_tool_executions.append(
ToolExecution(
tool_call_id=fc.call_id,
tool_name=fc.function.name,
tool_args=fc.arguments,
requires_confirmation=True,
approval_type=fc.function.approval_type,
)
)
而 Model.response 的循环尾部检查这些标记,命中就 break,不再 continue 问模型(models/base.py:836-865):
# models/base.py:836 —— 命中任一暂停条件就跳出循环(节选)
if any(m.stop_after_tool_call for m in function_call_results):
break
if any(tc.requires_confirmation for tc in model_response.tool_executions or []):
break
if any(tc.external_execution_required for tc in model_response.tool_executions or []):
break
if any(tc.requires_user_input for tc in model_response.tool_executions or []):
break
break 之后会怎样: 暂停的工具被记成 RunRequirement 挂到 RunOutput.requirements 上(models/base.py:802-804),整个 run 以「暂停」状态返回。人确认/补完输入后,通过 continue_run(agent/agent.py:1553)从断点续跑——_run.py 里有一整套 _fork_run(:3024)、_find_regenerate_checkpoint(:3088)、_resolve_continue_from(:3124)的逻辑支撑续跑与重生成。
用图把「正常 vs 暂停」两条出口画出来:
模型回了 tool_calls
│
▼
这些工具里有 HITL 标记吗?
├─ 没有 → 执行 → 结果喂回 → continue(继续问模型)
└─ 有 → 不执行(或执行后停)→ 记成 requirement → break
│
▼
run 以「暂停」返回,等人 → continue_run() 续跑
2.6 巧妙之处
- 零样板的工具定义。 一个带类型注解和 docstring 的普通函数即可,schema 全自动(
Function.from_callable)。和「手写 OpenAI function spec」相比省掉一大坨易错的 JSON。 - HITL 与正常工具共用一条循环。 没有「人审专用执行器」,只是循环尾部多了几个 break 条件(
models/base.py:836-865)——机制小、心智负担低。 - 框架参数自动隐藏。 工具函数能声明
agent/run_context等参数拿到运行时上下文,但这些不进模型看到的 schema(tools/function.py:290-324)。 - 工具调用上限有专门错误。 超过
tool_call_limit不是崩,而是给模型回一条「已达上限」的工具结果(create_tool_call_limit_error_result,models/base.py:2123,在循环里被调用于:2328)。
2.7 代码地图
| 主题 | 文件 | 符号 |
|---|---|---|
| 函数→工具 schema | libs/agno/agno/tools/function.py | Function.from_callable、Function.process_entrypoint |
| 工具集合 | libs/agno/agno/tools/toolkit.py | Toolkit、Toolkit.register |
| HITL 字段 | libs/agno/agno/tools/function.py | requires_confirmation、requires_user_input、external_execution、stop_after_tool_call |
| 工具执行 | libs/agno/agno/models/base.py | run_function_calls、_prepare_function_calls |
| 暂停判定 | libs/agno/agno/models/base.py | Model.response(循环尾部 break) |
| 续跑 | libs/agno/agno/agent/_run.py | _resolve_continue_from、_fork_run、_find_regenerate_checkpoint |
| 暂停标记 | libs/agno/agno/models/response.py(ToolExecution 经 run/agent.py:11 import)/ run/requirement.py(RunRequirement) | ToolExecution、RunRequirement |