跳到主要内容

第 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_parserparse(),把每个 Args: 项变成参数 description(tools/function.py:328-339)。
  • 必填判定:无默认值的参数才进 required(tools/function.py:366-381)。
  • 剔除框架注入参数:agentteamrun_contextimagesvideosaudiosfilesself 这些会被自动从 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=[...] 既能放裸函数、也能放 ToolkitFunction 或 dict(见 agent/agent.py:174 的类型签名)。

2.4 执行:在 Model 循环里跑工具

回到第 1 章的 Model while 循环。当模型回了 tool_calls,Model.response 就:

  1. _prepare_function_calls 把模型给的 JSON 配对到真实 Function(models/base.py:742)。
  2. run_function_calls 逐个执行(models/base.py:751、定义在 :2311)。
  3. 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 代码地图

主题文件符号
函数→工具 schemalibs/agno/agno/tools/function.pyFunction.from_callableFunction.process_entrypoint
工具集合libs/agno/agno/tools/toolkit.pyToolkitToolkit.register
HITL 字段libs/agno/agno/tools/function.pyrequires_confirmationrequires_user_inputexternal_executionstop_after_tool_call
工具执行libs/agno/agno/models/base.pyrun_function_calls_prepare_function_calls
暂停判定libs/agno/agno/models/base.pyModel.response(循环尾部 break)
续跑libs/agno/agno/agent/_run.py_resolve_continue_from_fork_run_find_regenerate_checkpoint
暂停标记libs/agno/agno/models/response.py(ToolExecutionrun/agent.py:11 import)/ run/requirement.py(RunRequirement)ToolExecutionRunRequirement