agno 实战指南:Agent 动态工具集(Callable Tools)、Tool Choice 与调用次数限制
本文基于 agno 官方 Cookbook 中的 02_agents/04_tools 示例目录,系统讲解 Agent 工具层进阶能力的六个实战主题:用可调用工厂函数按用户角色动态装配工具集、以 session_state 直接作为工厂参数并控制缓存、动态组建 Team 成员、typing.Literal 限定工具参数取值、tool_call_limit 限制单次运行的工具调用次数,以及 tool_choice 控制模型选哪个工具。读完本文,你可以掌握 agno 中工具从"静态列表"升级为"运行时按需装配"的完整机制,并结合源码定位各参数的实际生效位置。
环境与运行方式
示例目录 cookbook/02_agents/04_tools 覆盖六类场景,文件与主题一一对应:
| 文件 | 主题 |
|---|---|
| 01_callable_tools.py | 用可调用工厂按用户角色变换工具集 |
| 02_session_state_tools.py | 以 session_state 为参数,关闭缓存让工厂每次新鲜执行 |
| 03_team_callable_members.py | 运行时动态组装 Team 成员 |
| 04_tools_with_literal_type_param.py | 用 typing.Literal 定义预定义取值的工具参数 |
| tool_call_limit.py | 限制每次运行的工具调用次数 |
| tool_choice.py | 控制 Agent 选择哪个工具 |
按 README 的说明,运行前置条件为:
- 使用
direnv allow加载环境变量(包括OPENAI_API_KEY); - 执行
./scripts/demo_setup.sh创建演示环境(对应仓库中的 scripts/demo_setup.sh),之后用.venvs/demo/bin/python运行 Cookbook; - 部分示例需要可选的本地服务(如 pgvector)或特定提供商的 API Key。
运行命令统一为:
.venvs/demo/bin/python cookbook/02_agents/04_tools/<file>.py
目录内还附有 TEST_LOG.md,记录了各示例的实测结果(如 03 示例约 87s 完成、04 示例验证了 JSON schema 为 Literal 类型正确生成 enum 约束),可作为运行预期参考。
可调用工具工厂:按用户角色装配工具集
静态 tools=[...] 列表对所有用户一视同仁,而 agno 允许把一个函数直接传给 Agent(tools=...)。该函数在每次运行的开始阶段被调用,从而让工具集可以随用户或会话变化。
01_callable_tools.py 的完整思路是:先定义三个普通函数工具——search_web(通用)、search_internal_docs(仅 admin)、get_account_balance(admin/finance),然后定义工厂函数:
from agno.run import RunContext
def tools_for_user(run_context: RunContext):
"""Return different tools based on the user's role stored in session_state."""
role = (run_context.session_state or {}).get("role", "viewer")
base_tools = [search_web]
if role == "admin":
base_tools.append(search_internal_docs)
if role in ("admin", "finance"):
base_tools.append(get_account_balance)
return base_tools
agent = Agent(
model=OpenAIResponses(id="gpt-5-mini"),
tools=tools_for_user, # 注意:传的是函数,不是函数结果
)
示例通过两次 print_response 演示了缓存语义:第一次以 user_id="viewer_user"、session_state={"role": "viewer"} 运行,模型只能看到 search_web;第二次换用 user_id="admin_user"、session_state={"role": "admin"},由于 user_id 变化,工厂函数会再次被调用并以新上下文重新装配出全部工具。
从源码注释与文件头说明看,工厂的参数是按签名名称注入的,支持三种形参:agent(Agent 实例)、run_context(当前的 RunContext,含 user_id、session_id 等字段)、session_state(当前会话状态字典)。默认情况下,工厂结果会按 user_id(或 session_id)缓存,即同一用户后续运行不会重复调用工厂——这正是示例中 viewer/admin 两次运行各自独立解析工具的原因。
以 session_state 为直接参数,并用 cache_callables 关闭缓存
02_session_state_tools.py 演示了工厂函数的另一种更简洁的写法:把形参命名为 session_state,即可直接收到会话状态字典,无需解包 run_context:
def get_tools(session_state: dict):
"""Pick tools based on the 'mode' key in session_state."""
mode = session_state.get("mode", "greet")
if mode == "greet":
return [get_greeting]
else:
return [get_farewell]
agent = Agent(
model=OpenAIResponses(id="gpt-5-mini"),
tools=get_tools,
cache_callables=False, # 关键:工厂每次运行都重新执行
instructions=["Use the available tool to respond."],
)
这里的核心参数是 cache_callables=False。示例头注释明确指出:默认缓存下,同一用户第二次运行会复用第一次的工具集;关闭缓存后,工厂每次运行都会以最新的 session_state 重新执行,从而捕捉到两次运行之间 mode 从 "greet" 变为 "farewell" 的变化。示例连续两次 print_response(分别传入 {"mode": "greet"} 与 {"mode": "farewell"})验证了这一点:第一次模型只能调用 get_greeting,第二次则切换到 get_farewell。
从源码看,cache_callables 是 Agent 的字段,默认值为 True(见 agent.py 中 cache_callables: bool = True 的定义),因此"每次新鲜执行"必须显式关闭缓存。此外,_storage.py 在序列化 Agent 配置时会把非默认的 cache_callables=False 一并写入配置,说明该设置可随 Agent 配置持久化。
动态组装 Team 成员
同样的"可调用工厂"机制也适用于 Team:把函数传给 Team(members=...),团队成员的组合在运行时依据 session_state 决定。03_team_callable_members.py 中,writer 与 researcher 两个 Agent 已预先创建,工厂函数只负责按需挑选:
def pick_members(session_state: dict):
"""Include the researcher only when needed."""
needs_research = session_state.get("needs_research", False)
if needs_research:
return [researcher, writer]
return [writer]
team = Team(
name="Content Team",
model=OpenAIResponses(id="gpt-5-mini"),
members=pick_members,
cache_callables=False,
instructions=["Coordinate the team to complete the task."],
)
运行部分用 {"needs_research": False} 与 {"needs_research": True} 各跑一次,分别得到"仅 Writer"和"Researcher + Writer"两种团队形态。这里同样设置了 cache_callables=False,确保两次运行都重新执行工厂、按最新的 session_state 组装成员。可以推断,Team 的可调用成员与 Agent 的可调用工具共享同一套签名注入与缓存机制,只是返回值从工具列表换成了成员 Agent 列表。
用 typing.Literal 限定工具参数取值
当工具参数只接受有限枚举值时,typing.Literal 是最简洁的约束方式。04_tools_with_literal_type_param.py 同时覆盖了两种工具形态:Toolkit 方法工具与独立函数工具。
from typing import Literal
from agno.tools import Toolkit
class FileOperationsToolkit(Toolkit):
def __init__(self):
super().__init__(name="file_operations", tools=[self.manage_file])
def manage_file(
self,
filename: str,
operation: Literal["create", "read", "update", "delete"] = "read",
priority: Literal["low", "medium", "high"] = "medium",
) -> str:
"""Manage a file with the specified operation."""
return f"Performed '{operation}' on '{filename}' with {priority} priority"
def standalone_tool(
action: Literal["start", "stop", "restart"],
service_name: str,
) -> str:
"""Control a service with the specified action."""
return f"Service '{service_name}' has been {action}ed"
两个要点:
- 两种工具形态都支持:Toolkit 内的实例方法(
manage_file)和模块级独立函数(standalone_tool)都可以使用 Literal 参数,Agent 通过tools=[FileOperationsToolkit(), standalone_tool]同时挂载两者; - schema 自动生成枚举约束:根据 TEST_LOG.md 的实测记录,agno 生成的 JSON schema 会正确地为 Literal 类型产生
enum约束,即模型在函数调用时只能从给定集合中选值,而非自由发挥字符串。参数上的默认值(如operation="read")也会体现在生成的 schema 中。
这对"操作类型"、"优先级"、"服务动作"这类有限取值参数非常实用,既减少了模型幻觉出非法值的可能,也让工具的文档字符串(docstring)成为参数说明的主要载体。
tool_call_limit:限制单次运行的工具调用次数
如果希望约束 Agent 在单次运行中最多执行多少次工具调用,可以使用 tool_call_limit 参数。tool_call_limit.py 给出了最小示例:
from agno.tools.yfinance import YFinanceTools
agent = Agent(
model=OpenAIResponses(id="gpt-5-mini"),
tools=[YFinanceTools()],
tool_call_limit=1, # 每次运行只允许一次工具调用
)
agent.print_response(
"Find me the current price of TSLA, then after that find me the latest news about Tesla.",
stream=True,
)
示例的验证意图写在注释里:提示词要求"先查 TSLA 当前股价,再查特斯拉最新消息",理想情况下模型会发起两次工具调用;在 tool_call_limit=1 下,它只能完成第一个工具调用,第二个调用会被框架拒绝,从而演示限额生效的行为。
从源码链路看,tool_call_limit 是 Agent 上的 Optional[int] 字段(默认 None 即不限制)。它在运行阶段被逐层透传到模型请求中:_response.py 与 _run.py 多处均将 agent.tool_call_limit 传入底层执行上下文,意味着该限制作用于运行循环层面,而不是依赖模型自行克制。同时 _storage.py 在 tool_call_limit 非空时会将其写入 Agent 配置实现持久化。
tool_choice:控制模型选择哪个工具
tool_choice 用于显式控制模型的选工具行为,tool_choice.py 用一个 get_weather 工具对比了三种取值:
no_tools_agent = Agent(
name="No-Tools Agent",
model=OpenAIResponses(id="gpt-5.2"),
tools=[get_weather],
tool_choice="none", # 完全禁止工具调用
)
auto_tools_agent = Agent(
name="Auto-Tools Agent",
model=OpenAIResponses(id="gpt-5.2"),
tools=[get_weather],
tool_choice="auto", # 模型自行决定是否调用工具
)
forced_tool_agent = Agent(
name="Forced-Tool Agent",
model=OpenAIResponses(id="gpt-5.2"),
tools=[get_weather],
tool_choice={"type": "function", "name": "get_weather"}, # 强制调用指定函数
)
三种取值的语义:
"none":本轮完全禁用工具。即使挂了get_weather,模型也只能靠自身回答(示例中它面对"What is the weather in San Francisco today?"将给出不带工具数据的回答);"auto":默认行为,由模型根据提示词自主判断是否调用工具;{"type": "function", "name": "get_weather"}:强制模型调用指定函数,适用于"这一步必须走某个工具"的流程控制场景(如强制先查天气再组织回答)。
需要说明的是,TEST_LOG.md 记录了该示例在某次自动测试中因 API 调用耗时超过了 120s 超时(示例需连续运行三个 Agent),属于运行环境/网络因素,示例逻辑本身与上述参数语义一致。
关键参数小结
| 参数 | 所在对象 | 默认值 | 作用 |
|---|---|---|---|
tools |
Agent | — | 传列表挂载固定工具;传函数则按签名注入 agent/run_context/session_state 动态装配 |
members |
Team | — | 传函数可在运行时按 session_state 动态组装成员 |
cache_callables |
Agent / Team | True(见 agent.py) |
控制可调用工厂结果是否按 user_id/session 缓存;False 时每次运行重新执行 |
tool_call_limit |
Agent | None(不限制,见 agent.py) |
限制单次运行允许的工具调用次数 |
tool_choice |
Agent | — | "none" / "auto" / {"type": "function", "name": ...} 三种控制模式 |
小结
02_agents/04_tools 这组示例展示的是 agno 工具层从"静态配置"到"运行时策略"的完整进阶路径:可调用工厂(tools、members 传函数)+ 签名注入(session_state 直传)+ cache_callables 缓存开关,构成按用户/会话动态定制能力面的机制;typing.Literal 从参数 schema 层面收敛模型取值;tool_call_limit 与 tool_choice 则分别在"调用次数"和"选哪个工具"两个维度上施加运行时控制。所有示例均可按上述环境步骤在仓库内直接复现,各参数在 Agent 源码 与运行链路(_run.py、_response.py)中的透传位置也为进一步排查行为提供了代码级依据。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00