跳转到内容

Agno 工具调用协议与中间件链

Agno 工具调用协议与中间件链

学习目标

本章将分析 Agno 的工具系统架构:

  • Toolkit + Function 两层封装的设计动机
  • @tool decorator 如何自动从 Python 函数生成 JSON Schema
  • Callable Factory 模式实现运行时动态工具
  • 工具中间件 Hook 的环绕执行

前置知识

建议先阅读:

下文假设你已理解上述概念,直接聚焦 Agno 的具体实现。


项目实践

Toolkit + Function 两层封装

Agno 的工具系统采用两层抽象:

# 伪代码:两层封装
class Function:
"""单点工具定义"""
name: str # 工具名称,对应 LLM tool_call.name
description: str # 工具描述,LLM 用于判断是否调用
parameters: dict # JSON Schema,LLM 用于生成参数
callable: Callable # 实际执行的 Python 函数
hooks: Optional[List] # 工具级 Hook
class Toolkit:
"""一组 Function 的容器"""
name: str # 命名空间
functions: List[Function] # 包含的工具

为什么两层? Toolkit 提供命名空间和批量操作(一次性注册多个工具),Function 是 LLM 工具调用协议的最小单位。这种设计使得:

  • 新增工具只需定义一个 Toolkit 子类
  • LLM 看到的是扁平的工具列表,不感知 Toolkit 结构
  • 不同 Toolkit 的工具可以混合使用

@tool 装饰器自动提取 Schema

Agno 的 @tool decorator 将普通 Python 函数转为 Function

# 伪代码:@tool decorator
@tool
def get_weather(city: str, unit: Literal["celsius", "fahrenheit"] = "celsius") -> str:
"""获取指定城市的天气信息。"""
...
# 自动生成的 Function
# name: "get_weather"
# description: "获取指定城市的天气信息。"
# parameters: {
# "type": "object",
# "properties": {
# "city": {"type": "string"},
# "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
# },
# "required": ["city"]
# }

参数 Schema 从函数签名和类型注解自动提取。Literal 类型转为 enumOptional 字段不加入 required

Callable Factory 模式

tools 字段接受三种类型:

类型行为示例
List[Toolkit/Function]静态工具列表tools=[WeatherToolkit(), CalculatorToolkit()]
Callable[..., List]运行时动态生成tools=lambda ctx: get_tools_for_user(ctx.user_id)
Dict直接传 JSON Schema手动定义工具定义

Callable Factory 在每次 run 时调用,注入 agentrun_context

# 伪代码:动态工具工厂
def get_db_tools(run_context):
db_name = run_context.session_state.get("db_name", "default")
return [QueryTool(db=db_name), InsertTool(db=db_name)]
agent = Agent(tools=get_db_tools, cache_callables=True)

cache_callables=True 缓存 factory 结果,避免同一 run 内重复调用。

工具中间件 Hook

tool_hooks 是环绕每个工具调用的函数列表:

# 伪代码:工具中间件
def log_tool_hook(function_name, args, result, **kwargs):
log.info(f"Calling {function_name} with {args}")
# 可以修改 result
return result
agent = Agent(tool_hooks=[log_tool_hook, pii_mask_hook])

每个 Hook 接收工具名称、参数和结果。与 LangChain 的责任链中间件不同,Agno 的工具 Hook 是简单的函数列表——每个 Hook 独立执行,不形成嵌套链。这种设计更轻量,适合不需要复杂状态流转的场景。

工具确定(Dynamic Tool Filtering)

在管线步骤 5,Agno 执行 determine_tools_for_model——不是所有声明的工具都发送给模型:

# 伪代码:工具确定
_tools = determine_tools_for_model(
agent,
model=agent.model,
processed_tools=processed_tools,
run_response=run_response,
session=agent_session,
run_context=run_context,
)

过滤逻辑考虑:

  • 模型的能力(某些模型不支持并行工具调用)
  • 当前 session 状态的权限
  • 工具之间的互斥关系

问题与规避

1. Callable Factory 的缓存陷阱

cache_callables=True 时,factory 结果在 Agent 生命周期内缓存。如果 factory 依赖的外部状态变化(如数据库切换),缓存的旧工具列表不会更新。

规避:在 factory 函数中使用 cache_callables=False,或使用自定义 callable_tools_cache_key 函数,在状态变化时返回不同的缓存键。

2. 工具数量过多导致上下文溢出

100+ 工具集成意味着单个 Agent 可能声明大量工具。每个工具的 JSON Schema 定义占用数百 token。

规避:使用 determine_tools_for_model 动态过滤,或在 Agent 配置中按场景拆分多个小 Agent,每个只携带相关工具。

3. @tool 装饰器的类型推断限制

@tool 从 Python 类型注解提取 JSON Schema,但不支持所有 Python 类型(如自定义 dataclass、泛型)。

规避:对复杂参数类型,使用 parameters 参数手动传 JSON Schema。

设计取舍

为什么工具 Hook 是函数列表而非责任链?

责任链中间件(如 LangChain)提供更强的控制力(可以拦截、跳过、重试),但实现复杂度高。Agno 的工具 Hook 选择简单函数列表

  • 优势:实现简单(几十行代码),用户容易理解
  • 代价:无法在 Hook 中跳过工具执行或修改执行顺序
  • 适用场景:日志、审计、简单的结果转换

替代方案:如果需要在 Hook 中做复杂控制流(如权限检查失败时跳过工具执行),使用 pre-hooks 的 guardrail 模式,在工具调用前拦截。

参考来源