第 37 章 · Agno Tools 与 MCP

从“会聊天”到“能干活”

那个跨境卖家最初对 Agent 的期待很简单:帮我盯着欧盟的玩具合规动态。可他很快发现,光靠模型“记忆里的知识”根本不够——模型不知道昨天欧盟刚发的新指令,不知道他自家后台今天的销量,更没法替他去 GitHub 上拉一份竞品的开源 SDK 看看更新了什么。

问题不在模型聪不聪明,而在于它没有手。一个只会说话的 Agent,充其量是个博学的顾问;一个能搜索、能查库、能读文件、能调外部服务的 Agent,才开始“像系统”。第 3、4、5 章已经把原理讲透了:工具调用让 Agent 能动手,MCP 让工具可复用,结构化输出让结果能被程序消费。这一章要做的,是把这三件事落到可运行的 Agno 代码上——同一套模式,看 Agno 怎么落地。

概念与主书映射:三种能力,三章对应

在 Agno 里,“给 Agent 装上手”主要靠三组能力,每一组都精确对应主书的一章:

  • Tools(工具,对应第 4 章):用 tools=[...] 给 Agent 接入动作能力。内置工具(如 DuckDuckGoToolsYFinanceTools)拿来即用;业务独有的能力则封装成普通 Python 函数。模型自己决定何时调用,Agno 负责执行并把结果(Observation)回填——这就是主书讲的 ReAct 循环,只是你不用亲手写循环。
  • MCP(对应第 5 章):MCP 是“AI 工具的 USB 标准”。Agno 支持接入 MCP Server,让 GitHub、文件系统、浏览器、数据库这类通用能力免于重复封装。
  • response_model(结构化输出,对应第 6 章):用 Pydantic 模型约束 Agent 的输出结构,让结果不再是“一段话”,而是能被下游 Workflow、评测和前端直接消费的对象。

还有一组进阶能力也在本章:human-in-the-loop(人在回路)——用 @tool(requires_confirmation=True) 让高风险工具在执行前暂停、等人确认。这对应第 4 章“工具是能力也是风险”的原则,也为第 42 章的治理埋下伏笔。

内置工具:先用现成能力

给 Research Agent 加联网搜索,是最快看到效果的一步:

from agno.agent import Agent
from agno.models.openai import OpenAIChat
from agno.tools.duckduckgo import DuckDuckGoTools

agent = Agent(
    model=OpenAIChat(id="gpt-4o"),
    tools=[DuckDuckGoTools()],          # 内置搜索工具,开箱即用
    instructions=[
        "你是跨境电商研究助手。",
        "涉及最新政策、竞品动态或市场新闻时,先搜索再回答。",
        "回答必须列出来源链接,不确定就说明未查到。",
    ],
    show_tool_calls=True,               # 打印工具调用过程,便于调试和观测
    markdown=True,
)

agent.print_response("最近欧盟玩具合规有什么变化?", stream=True)

这对应第 4 章的工具调用:模型判断需要搜索,Agno 执行 DuckDuckGoTools,再把搜索结果回填给模型二次生成。你不需要自己写 ReAct 循环,但仍要在 instructions 里写清“什么时候该用工具”——否则模型可能该搜的不搜,凭记忆硬答。

内置工具通常是一个工具包(toolkit),一个类里打包了多个相关函数。比如 YFinanceTools 一次性提供股价、财报、分析师评级等多个工具:

from agno.tools.yfinance import YFinanceTools

agent = Agent(
    model=OpenAIChat(id="gpt-4o"),
    # 只开需要的子能力,其余不暴露给模型——这就是"最小权限"
    tools=[YFinanceTools(stock_price=True, analyst_recommendations=True)],
    instructions=["分析上市竞品时,用财务数据佐证观点。"],
    show_tool_calls=True,
)

自定义工具:业务独有的能力

通用能力交给内置工具或 MCP,业务独有的能力必须自己封装。在 Agno 里,自定义工具就是一个带清晰 docstring 的普通 Python 函数——不需要任何装饰器,直接放进 tools=[...] 即可:

from agno.agent import Agent
from agno.models.openai import OpenAIChat

def query_sales(start: str, end: str, limit: int = 10) -> dict:
    """查询指定时间段内销量最高的 SKU。只读,不修改任何数据。

    Args:
        start: 起始日期,格式 YYYY-MM-DD
        end: 结束日期,格式 YYYY-MM-DD
        limit: 返回条数,默认 10

    Returns:
        含 rows(SKU 列表)和 source(数据来源)的字典
    """
    rows = sales_db.readonly_top_skus(start=start, end=end, limit=limit)
    return {"rows": rows, "source": "sales_read_replica"}

agent = Agent(
    model=OpenAIChat(id="gpt-4o"),
    tools=[query_sales],                # 普通函数直接当工具
    instructions=[
        "回答销量、库存、选品问题时,优先调用只读业务工具。",
        "涉及金额、销量、日期时必须说明口径。",
    ],
    show_tool_calls=True,
    markdown=True,
)

这里的关键不是 Python 语法,而是第 4 章的工具设计原则,一条都不能少:

  • 工具只做一件事——查销量就不要顺带改库存。
  • 函数名和 docstring 要清楚:Agno 直接把 docstring 当作模型看到的工具说明,写得含糊模型就会用错。
  • 参数要具体、可校验:日期用字符串格式约定,limit 给默认值。
  • 读写权限在工具内部锁死:这里连的是只读副本 readonly_top_skus,模型无论如何都改不了生产数据。
  • 错误要结构化返回,不要让异常裸奔吞掉上下文。

最后一条尤其容易被忽略。工具内部一旦抛出未捕获的异常,模型拿到的往往是一段面目全非的堆栈,既没法据此纠错,也污染了上下文。更稳的写法是把可预期的失败也包成结构化结果:

def query_sales(start: str, end: str, limit: int = 10) -> dict:
    """查询指定时间段内销量最高的 SKU。只读,不修改任何数据。"""
    try:
        rows = sales_db.readonly_top_skus(start=start, end=end, limit=limit)
    except ValueError as e:                 # 日期格式等可预期错误
        return {"ok": False, "error": f"参数错误:{e}", "source": "sales_read_replica"}
    return {"ok": True, "rows": rows, "source": "sales_read_replica"}

模型看到 ok: False 和明确的 error 文案,就能选择改参数重试或如实告诉用户“查不到”,而不是把一段异常当成数据继续往下编。这正是第 4 章强调的“工具的失败也是一种可被推理的 Observation”。

MCP:把外部能力接进来

第 5 章讲过,MCP 是“AI 工具的 USB 标准”。Agno 支持接入 MCP Server 的意义在于:你不必为 GitHub、文件系统、浏览器、数据库这类通用能力重复造轮子,而是把现成的 MCP Server 接进来,它暴露的工具就自动注册进 Agent。

到底哪些能力自己写、哪些走 MCP,可以按这张表判断:

能力推荐做法
自家订单、销量、库存自定义工具(业务逻辑和权限只有你清楚)
GitHub、浏览器、文件系统、通用 SaaSMCP Server(现成生态,无需重复封装)
一个完整的远端 Agent不要伪装成普通工具,按第 4、23 章判断“工具 vs Agent 服务”

接了 MCP 之后必须记住一句话:MCP 只管接入,不管安全。它把外部能力标准化地送进来,但“这个 Server 能做什么、用什么凭据、会不会写生产数据”,仍然要你自己审查。一个能删仓库的 GitHub MCP Server,和一个只读的,风险天差地别。接入前先问清楚它暴露的工具边界,再决定给不给、给哪些凭据。

结构化输出:别让结果只是一段话

Agno 用 Pydantic 模型约束输出,正好对应第 6 章的结构化输出。把 response_model 指向一个 Pydantic 类,Agent 就会返回一个填好字段的对象,而不是自由文本:

from typing import List

from agno.agent import Agent
from agno.models.openai import OpenAIChat
from pydantic import BaseModel, Field

class MarketBrief(BaseModel):
    title: str = Field(..., description="简报标题")
    key_findings: List[str] = Field(..., description="3 到 5 条关键发现")
    risks: List[str] = Field(default_factory=list, description="识别到的风险")
    recommended_actions: List[str] = Field(default_factory=list, description="建议动作")

agent = Agent(
    model=OpenAIChat(id="gpt-4o"),
    response_model=MarketBrief,         # 输出直接是 MarketBrief 实例
    instructions="输出跨境市场简报,必须结构化。",
)

response = agent.run("分析德国玩具市场最近的合规风险")
brief = response.content                # 这是一个 MarketBrief 对象
print(brief.title)
for finding in brief.key_findings:
    print("-", finding)

这比在提示里写“请输出 JSON”可靠得多,因为 schema 是代码的一部分:字段缺了、类型不对,Pydantic 直接报错,而不是等下游解析时崩掉。它也让后续的 Workflow(第 40 章)、评测(附录 D)和前端展示都有了稳定的契约。

深入一层:这些选择背后的取舍

为什么自定义工具是“普通函数 + docstring”,而不是繁复的 schema 声明? 这正是 Agno 与主书 pi 最直观的分野。pi 作为 harness,要求你显式声明 parameters 的 JSON Schema(回顾第 4 章的 registerTool),因为它要在框架层做严格校验;Agno 作为应用框架,选择从函数签名和 docstring 自动推断 schema——写起来轻,代价是“docstring 写得好不好”直接决定模型用得对不对。所以在 Agno 里,docstring 不是注释,而是产品文档

内置工具包为什么要按需开子能力? YFinanceTools(stock_price=True, ...) 里每个布尔开关都是一次最小权限决策。全开固然省事,但每多暴露一个工具,模型的选择空间就大一分,误用和 token 开销也随之上升。只开这次任务真正需要的,是把第 4 章“工具即攻击面”的原则落到参数上。

response_model 和“工具返回结构化数据”是两回事。 前者约束的是 Agent 对用户的最终输出,后者约束的是工具喂给模型的中间数据。两者可以叠加:工具返回带 source/score 的字典(供模型推理),Agent 最终吐出一个 MarketBrief(供程序消费)。想清楚“谁消费这个结构”,就知道该用哪一个。

高风险动作要不要人来确认? 对写操作(改价、下单、发邮件),Agno 提供 @tool(requires_confirmation=True)。工具被调用时不会立即执行,而是让 response.is_paused 变为真、把待确认的调用挂起;你在应用层展示给人看,人确认后调用 agent.continue_run() 才真正执行。骨架大致如下:

from agno.tools import tool

@tool(requires_confirmation=True)           # 执行前暂停,等人确认
def adjust_price(sku: str, new_price: float) -> dict:
    """调整某 SKU 的售价。写操作,会影响生产数据。"""
    pricing_db.update_price(sku=sku, price=new_price)
    return {"ok": True, "sku": sku, "price": new_price}

response = agent.run("把 SKU-8842 降价到 19.9")
if response.is_paused:                       # 命中待确认的工具
    # 在应用层把 response 里挂起的调用展示给人,人点了"确认"后:
    response = agent.continue_run(run_id=response.run_id, updated_tools=response.tools)

这对应第 4 章“能力越大越要设闸”的思路。要注意的是:确认这一步的 UI、权限、审计谁来做,是应用层的责任,Agno 只提供暂停与续跑的机制。完整的治理设计——谁有权确认、超时怎么办、如何留痕——留到第 42 章展开。

动手看看

拿本章第一段的搜索 Agent 做起点,做三个小改动,直观感受每个参数的作用:

  1. show_tool_calls 关掉再跑一次——你会发现“模型到底搜了没、搜了什么”完全看不见了。这就是为什么调试和观测阶段它几乎必开。
  2. 给它加一个自定义工具 query_sales,然后问一个既需要联网、又需要查自家销量的问题(比如“我这款玩具在德国还能不能卖,最近卖得怎么样”),观察模型如何自己决定先调哪个、后调哪个。
  3. 把某个写操作工具(比如“调整某 SKU 售价”)加上 @tool(requires_confirmation=True),跑一次,看 response.is_paused 变为真、Agent 停在确认点等你——你就亲手触发了一次 human-in-the-loop。

实战中的几个坑

坑一:docstring 写得含糊,模型用错工具。

  • 现象:明明有 query_sales,模型却去联网搜自家销量,或者传错了日期格式。
  • 原因:Agno 直接把函数名和 docstring 当作模型看到的说明,含糊的描述等于没说清。
  • 对策:docstring 写清“做什么、每个参数格式、返回什么、只读还是可写”,把它当产品文档写。

坑二:内置工具包全开,token 和误用都上去了。

  • 现象:接了个大工具包后响应变慢、偶尔调用了根本不该用的子工具。
  • 原因:一次性把工具包所有子能力都暴露给了模型。
  • 对策:用参数(如 YFinanceTools(stock_price=True))只开当前任务需要的子能力,最小权限。

坑三:MCP Server 权限没审,直接给了生产凭据。

  • 现象:接入某 MCP Server 后,Agent 具备了删除、写入生产数据的能力而你并不知情。
  • 原因:MCP 只标准化接入,不替你管安全;给的凭据权限过大。
  • 对策:接入前审查它暴露哪些工具,优先给只读凭据;写操作单独评估并配合 human-in-the-loop。

坑四:以为设了 response_model 就万无一失。

  • 现象:字段是齐了,但 key_findings 里塞的是模型编的、没经检索的内容。
  • 原因:response_model 只约束结构,不约束内容真伪
  • 对策:结构化输出要和“仅基于工具/检索结果回答”的指令配合,结构是壳,内容靠工具喂。

Agno vs 主书 pi 做法

能力Agno 怎么做pi(harness)怎么做各自适合谁
自定义工具普通 Python 函数 + docstring,自动推断 schemapi.registerTool 显式声明 parameters JSON SchemaAgno 适合快速落地;pi 适合要框架层严格校验的场景
内置工具DuckDuckGoTools/YFinanceTools 等即插即用无内置,通用能力靠 MCP 或自己封装Agno 开箱即用;pi 保持精简、按需接入
MCP 接入支持接 MCP Server,工具自动注册配置里连上 MCP,工具即注册进 Agent两者理念一致,都把 MCP 当“接入的能力”
结构化输出response_model= 指向 Pydantic 模型在提示/schema 层约定输出结构并校验Agno 用 Pydantic 更省心;pi 更贴近 harness 原语
人在回路@tool(requires_confirmation=True) + continue_run()在工具执行前插入审批钩子都能做,Agno 提供了现成开关

分野很清楚:Agno 是应用框架,把工具、内置能力、结构化输出都做成了开箱即用的组件,让你几行代码就能接上手;pi 是 harness,把这些都当作“你在其上接入的能力”,更灵活、也更贴合“每个 schema、每次校验都由我掌控”的工程需求。底层的模式是同一套——工具调用、MCP、结构化输出——只是抽象层次不同。

小结

  1. Tools(工具) 对应主书第 4 章:内置工具即插即用,自定义工具就是“普通函数 + 清晰 docstring”,docstring 即模型看到的产品文档。
  2. MCP 对应第 5 章:通用能力的接入标准,让 GitHub、文件系统等免于重复封装;但它只管接入、不管安全,凭据和边界仍要你审。
  3. response_model 对应第 6 章:用 Pydantic 固定输出结构,让结果能被下游程序消费;但它只约束结构、不保证内容真伪,须与“仅依据工具结果回答”配合。
  4. human-in-the-loop@tool(requires_confirmation=True) 配合 response.is_pausedagent.continue_run(),给高风险工具设闸。
  5. 企业级工具设计的重点从来不是“能调通”,而是最小权限、结构化错误、可观测、可评测——这些决定了 Agent 能不能真的托付业务。

工具让 Agent 能动手,但它动手的依据仍来自模型记忆或临时搜索。下一章,我们给 Research Agent 接入知识库,让它基于你自己的资料回答,而不是凭记忆猜。