第 3 章 Google ADKToolsFunctionTool

第 3 章 工具与上下文:Agent 的能力来源

第 3 章 工具与上下文:Agent 的能力来源

上一章的云销客服 v0.1 能查订单、查退款政策——但那只是靠两个 mock 函数撑起来的。这一章我们要回答两个问题:第一,Agent 的能力还能从哪里来?第二,多轮对话里,Agent 凭什么"记得"之前说过什么?答案分别是「工具」和「上下文」。这两样东西,决定了你的 Agent 是玩具还是生产系统。

3.1 工具:Agent 的双手

第 2 章我们看到了最简单的工具形态——一个被 FunctionTool 包装的普通 Python 函数。这一节把"工具"这件事讲透。

3.1.1 为什么 Agent 需要工具

一个没有工具的 LLM,本质上是一个"只会说话的专家"。它知道很多事,但它做不了任何事——不能查数据库、不能调 API、不能操作文件、不能下单。

工具就是把这些"能力"交给 Agent 的通道。有了工具,Agent 才能从"会说"变成"会做"。

ADK 里工具的核心模型非常统一:工具就是一个带清晰签名的函数。Agent 在推理时决定"要不要调用某个工具、传什么参数",框架负责真正执行这个函数并把结果还给模型。

3.1.2 函数工具(Function Tool):最常用的工具

函数工具就是把普通 Python 函数包装成工具。第 2 章的 get_order_status 就是例子。这里我们系统讲一下它的三个要点。

要点一:函数签名就是工具的说明书。

ADK 会根据函数的类型注解(type hints)和 docstring 自动生成工具的 JSON Schema——这个 schema 就是给模型看的"这个工具怎么用"的说明书。

def get_order_status(order_id: str) -> dict:
    """查询订单当前状态。

    Args:
        order_id: 订单号,例如 'ORD-20260901-001'

    Returns:
        包含订单状态和物流信息的字典。
    """
    ...

这段代码里,order_id: str 告诉模型"这个工具需要一个字符串参数";docstring 里的 Args: order_id: ... 告诉模型"这个参数应该传订单号"。你的类型注解和 docstring 写得多清楚,模型调用工具就有多准。

要点二:返回结构要稳定。

工具的返回值会被模型读取并用于下一步推理。所以返回值最好是结构清晰的字典,而且成功和失败要有区分。我们云销客服的工具就是这种模式:

# 成功
{"status": "success", "order_id": "ORD-20260901-001", "status": "已发货", ...}
# 失败
{"status": "error", "order_id": "ORD-999999", "message": "未找到订单 ORD-999999"}

这种"带 status 字段的返回"是一种经过实战检验的模式:模型一眼就能判断这次调用成没成功,不会把错误结果当成正常数据继续推理。

要点三:用 FunctionTool 包装,或者直接传函数。

ADK 的 Agent 构造函数里,tools 参数既可以直接接收普通函数(如第 2 章快速入门),也可以用 FunctionTool 显式包装:

from google.adk.tools import FunctionTool

weather_tool = FunctionTool(func=get_weather_report)

agent = Agent(
    model='gemini-flash-latest',
    name='weather_agent',
    instruction="You are a helpful weather assistant.",
    tools=[weather_tool],
)

FunctionTool 的显式包装让你可以进一步定制工具(比如加描述、控制是否启用)。日常开发,两种写法都常见。

3.1.3 MCP 工具:接入整个工具生态

函数工具很好用,但它有一个局限:你只能使用自己写的函数。如果我想用一个别人已经写好的工具——比如一个查询地图的工具、一个操作 GitHub 的工具——怎么办?

答案就是 MCP(Model Context Protocol,模型上下文协议)。MCP 是 Anthropic 提出的开放标准,现在已经成了 AI 工具生态的事实协议。一个 MCP server 就是一个"工具服务器",任何支持 MCP 的客户端都能用它提供的工具。

ADK 的 McpToolset 让 Agent 以 MCP 客户端的身份接入外部 MCP server:

import os
from google.adk.agents import LlmAgent
from google.adk.tools.mcp_tool import McpToolset
from google.adk.tools.mcp_tool.mcp_session_manager import StdioConnectionParams
from mcp import StdioServerParameters

root_agent = LlmAgent(
    model='gemini-flash-latest',
    name='filesystem_assistant_agent',
    instruction='Help the user manage their files. You can list files, read files, etc.',
    tools=[
        McpToolset(
            connection_params=StdioConnectionParams(
                server_params=StdioServerParameters(
                    command='npx',
                    args=[
                        "-y",
                        "@modelcontextprotocol/server-filesystem",
                        os.path.abspath("./your_folder"),
                    ],
                ),
            ),
        )
    ],
)

这个例子接入的是官方的 filesystem MCP server——它提供"列目录、读文件、写文件"等工具。McpToolset 在初始化时会自动完成:连接 MCP server、发现它提供了哪些工具、把工具的 schema 转成 ADK 能用的格式。Agent 调用这些工具时,McpToolset 透明地把请求转发给 MCP server 并取回结果。

MCP 支持两种连接方式:

  • stdio(本地子进程):本地开发常用,McpToolset 会启动一个子进程作为 MCP server
  • Streamable HTTP(远程服务器):生产环境推荐,连接远程的 MCP 服务,可扩展性好
from google.adk.tools.mcp_tool.mcp_session_manager import StreamableHTTPConnectionParams

connection_params = StreamableHTTPConnectionParams(
    url="https://your-mcp-server.example.com/mcp",
    headers={"Authorization": "Bearer YOUR_TOKEN"},
)

MCP 的意义远超"能接外部工具"。它代表了一种生态哲学:工具不再是某个框架私有的,而是开放的、可复用的。到第 7 章讲开放协议时,我们会把 MCP 放到更大的图景里看。

3.1.4 OpenAPI 工具:把 REST API 变成工具

第三种工具类型是 OpenAPI 工具——把现有的 REST API(只要它有 OpenAPI/Swagger 规范)自动变成 Agent 可用的工具。

这非常实用:你的公司已经有一堆 REST API?不需要为每个接口写一个 Python 函数,直接把 OpenAPI 规范喂给 ADK,它就能自动生成对应的工具集。这相当于把整个存量 API 体系直接暴露给 Agent

from google.adk.tools.openapi_tool import OpenApiToolset

openapi_toolset = OpenApiToolset(
    spec_path="./openapi.json",  # 或 spec_url="https://..."
)

agent = Agent(
    model='gemini-flash-latest',
    name='api_agent',
    instruction="You can call the company's internal APIs to help users.",
    tools=[openapi_toolset],
)

3.1.5 三种工具怎么选

工具类型 适用场景 特点
函数工具 自己写的逻辑、内部函数 最简单、最可控、schema 自动生成
MCP 工具 接入第三方/开源工具生态 开放协议、生态丰富、即插即用
OpenAPI 工具 把已有 REST API 暴露给 Agent 零代码接入存量 API 体系

三种工具可以混用——一个 Agent 的 tools 列表里可以同时放函数工具、MCP 工具、OpenAPI 工具。这就是 ADK"连接一切"的能力起点。

3.2 技能(Skills)入门:可复用的能力包

3.2.1 从工具到技能:多了一层"指令"

工具解决的是"能做什么",但现实中的任务往往需要"怎么做"的知识。比如"处理售后"这件事,不只是调用几个工具——它有一套完整的流程、判断标准、话术。

技能(Skill) 就是这样一个"自包含的功能单元":它把指令 + 资源 + 工具打包在一起,让 Agent 能执行特定任务。如果说工具是"一双手",技能就是"一份带手的手册"——既告诉 Agent 怎么做,又给了它做事的工具。

3.2.2 Skill 的三层结构

一个 Skill 本质上是一个目录,核心文件是 SKILL.md

skills/
  refund-handling/          # 一个技能目录
    SKILL.md                # 必需:技能的定义和指令
    references/             # 可选:扩展指令、参考文档
    assets/                 # 可选:模板、API 文档等资源
    scripts/                # 可选:可执行脚本(.py / .js / .ts)

三层结构对应三种信息:

  • L1 元数据SKILL.md 的 frontmatter,包含 namedescription,用于技能发现。规则:name 用 kebab-case 小写,不超过 64 字符;description 非空、不超过 1024 字符。
  • L2 指令SKILL.md 的正文,是技能的核心指令,在 Agent 触发这个技能时才加载(增量加载,节省上下文窗口)。
  • L3 资源references/(附加文档)、assets/(模板素材)、scripts/(可执行脚本),按需加载。

3.2.3 加载并挂载一个 Skill

从文件系统加载一个技能,然后通过 SkillToolset 挂载到 Agent:

import pathlib
from google.adk import Agent
from google.adk.skills import load_skill_from_dir
from google.adk.tools import skill_toolset

# 加载技能
refund_skill = load_skill_from_dir(
    pathlib.Path(__file__).parent / "skills" / "refund_handling"
)

# 用 SkillToolset 挂载,可附带额外工具
my_skill_toolset = skill_toolset.SkillToolset(
    skills=[refund_skill],
    # additional_tools=[get_order_tool],  # 可选
)

root_agent = Agent(
    model="gemini-flash-latest",
    name="refund_agent",
    description="一个能处理售后退款的专业客服智能体。",
    instruction="你是一个专业的售后客服,使用退款处理技能来帮助用户。",
    tools=[my_skill_toolset],
)

SkillToolset 会给 Agent 注入一套默认系统指令,要求它:先调用 load_skill 读取技能指令、严格按指令执行、用 load_skill_resource 查看技能资源、用 run_skill_script 运行脚本。这套机制保证了"技能不是摆设,而是真的被遵循"。

本节是 Skills 的入门。技能的完整设计(从代码内联定义、与 Agent Skill 规范的关系、在多智能体和图工作流中的集成使用)我们放到第 7 章深入。现在你只需要知道:技能 = 指令 + 资源 + 工具 的打包,是比工具更高一层的复用单元。

3.3 Session 与 State:多轮对话怎么记住

3.3.1 没有记忆的 Agent 是失忆的

先做个实验:跟第 2 章的云销客服聊两句。

You: 我叫小明,帮我查一下订单
Agent: 好的,请提供订单号。
You: ORD-20260901-001
Agent: 订单 ORD-20260901-001 已发货...
You: 那这个订单能退吗?
Agent: ...

问题来了:Agent 怎么知道"这个订单"指的是 ORD-20260901-001?它怎么记得你叫小明?如果 Agent 每一次对话都是"重新开始",它根本没法接上话茬。

这就是 Session(会话) 要解决的问题。

3.3.2 Session:一次持续的交互

ADK 官方对 Session 的定义是:"用户和你的 Agent 系统之间一次持续进行的交互"。

一个 Session 包含两部分:

  1. Events(事件):对话按时间排列的记录——用户说了什么、Agent 说了什么、调用了什么工具、结果如何。这是会话的"流水账"。
  2. State(状态):会话范围内的临时数据——比如"当前用户想退的订单号"、"用户偏好的语言"。这是会话的"便签纸"。

Session 提供了多轮对话的连续性:Agent 可以读取之前的 Events 来理解上下文,可以读写 State 来记住会话内的临时信息。

3.3.3 State:会话内的便签纸

State 由 SessionService 管理,在代码里通过 context.state 访问。最典型的用法是在工具调用之间传递数据

from google.adk.tools import ToolContext

# 工具一:记录当前用户要处理的订单号
def set_current_order(tool_context: ToolContext, order_id: str) -> dict:
    tool_context.state["temp:current_order_id"] = order_id
    return {"status": "success", "message": f"已记录订单 {order_id}"}

# 工具二:读取之前记录的订单号
def get_current_order(tool_context: ToolContext) -> dict:
    order_id = tool_context.state.get("temp:current_order_id")
    if not order_id:
        return {"status": "error", "message": "还没有记录订单"}
    return {"status": "success", "order_id": order_id}

注意工具函数通过声明 tool_context: ToolContext 参数来接收上下文——这是 ADK 的依赖注入机制,框架会自动把上下文注入这个参数。

State 有个命名约定,用前缀区分作用域:

前缀 作用域 说明
temp: 仅当前 invocation 一次性数据,调用结束后失效
user: 跨 session 的用户级 需配合持久化 SessionService
app: 应用级 全局共享数据

这个约定看起来简单,但它强制你思考"这份数据该活多久"——这是生产级 Agent 状态管理的第一课。

3.3.4 SessionService:会话的管家

Session 由 SessionService 管理,负责创建、读取、更新、删除 Session。ADK 自带内存实现 InMemorySessionService(数据在应用重启后丢失),生产环境可以换成持久化实现(如存数据库)。

后面我们会看到 Runner 如何用 SessionService 管理对话——那是第 4 章多 Agent 和部署时的重点。

3.4 事件流模型:每个决策都可追踪

3.4.1 什么是事件流

这一节讲 ADK 最优雅的设计之一——事件流(Event Stream)

ADK 把 Agent 执行过程中的每一个动作都建模成一个 Event(事件)

  • 用户发送了一条消息 → 一个 Event
  • Agent 调用了某个工具 → 一个 Event
  • 工具返回了结果 → 一个 Event
  • Agent 切换到了子 Agent → 一个 Event
  • Agent 生成了最终回复 → 一个 Event

整个执行过程就是一条有序的事件流

3.4.2 为什么事件流重要

事件流的价值在于可观测性。传统的"黑盒"Agent 你只看到输入和输出,中间发生了什么全靠猜。而在 ADK 里,Agent 的每一步决策都是显式的事件——这意味着:

  • 可以调试:出问题时,翻看事件流就能定位是模型判断错了、工具返回错了、还是路由选错了
  • 可以回放:把一条事件流重放一遍,就能复现问题
  • 可以接入观测系统:事件流可以输出到日志、指标、追踪系统(第 9 章专门讲)

3.4.3 在代码里看事件流

用 Runner 跑 Agent 时,你会看到事件流:

import asyncio
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.genai import types

async def run_agent():
    session_service = InMemorySessionService()
    session = await session_service.create_session(
        state={}, app_name='yunxiao_app', user_id='user_001'
    )

    runner = Runner(
        app_name='yunxiao_app',
        agent=yunxiao_agent,
        session_service=session_service,
    )

    content = types.Content(
        role='user',
        parts=[types.Part(text="帮我查一下 ORD-20260901-001")],
    )

    events_async = runner.run_async(
        session_id=session.id,
        user_id=session.user_id,
        new_message=content,
    )
    async for event in events_async:
        print(f"Event: {event.type} | {event}")

asyncio.run(run_agent())

这里的 runner.run_async() 返回一个异步事件流,async for 逐个处理事件。你会看到:模型调用事件 → 工具调用事件 → 工具结果事件 → 最终回复事件。整个决策链透明可见。

3.4.4 事件与 Session 的关系

事件是 Session 的一部分——Session 就是"按时间排列的事件集合"。这意味着,只要 Session 还在,Agent 的完整历史就在;而 Agent 理解上下文,靠的就是读取 Session 里的历史事件。

3.5 记忆、压缩、缓存:上下文管理的完整方案

3.5.1 上下文窗口是有限的

LLM 的上下文窗口是有限的(比如几万到百万 token)。一个长期运行的客服 Agent,对话历史会越来越长,迟早撑爆窗口。上下文管理就是解决这个问题。

3.5.2 记忆(Memory):跨会话的长期知识

Session 的 State 是会话内的。但有些信息需要跨会话保留——比如"小明是金牌会员,喜欢用简体中文"。这是 记忆(Memory) 的职责。

ADK 的 MemoryService 管理长期知识存储,支持摄入(ingest)和搜索(search)。Agent 可以在需要时搜索相关记忆:

async def search_customer_memory(tool_context) -> dict:
    results = await tool_context.search_memory("小明 会员 偏好")
    return {"memory": results}

记忆和 Session 的区别:

  • Session:一次对话的完整流水账(Events)+ 临时状态(State)
  • Memory:跨对话的可搜索长期知识档案

3.5.3 压缩(Compaction):让历史"瘦身"

当对话历史太长时,可以用压缩把旧内容提炼成摘要。ADK 支持在会话层面对历史做压缩,把冗余的早期对话压缩成精炼摘要,释放上下文窗口给当前任务。这是"上下文放不下时"的兜底方案。

3.5.4 缓存(Caching):省成本又省延迟

上下文管理中还有一个容易忽略但非常省钱的点:缓存。很多场景下,Agent 的历史输入在多次调用间是不变的,ADK 可以利用模型的上下文缓存能力,避免重复计费、降低延迟。

缓存和压缩的区别:缓存是"同样的内容不重复算",压缩是"把内容变少"。一个省成本,一个省空间,配合使用效果最佳。

3.5.5 一套完整的上下文策略

把这一节串起来,一个生产级 Agent 的上下文管理应该是:

  1. 会话内:Session 管理 Events + State
  2. 跨会话:Memory 提供长期记忆
  3. 历史太长:Compaction 压缩旧内容
  4. 重复内容:Caching 省 token

这四层构成了完整的上下文管理方案。这也是为什么说"上下文是 Agent 的命门"——玩具 Agent 只会在一个 prompt 里塞所有东西,生产 Agent 用这套分层方案管理有限而昂贵的上下文。

3.6 主线项目:云销客服 v0.2

这一节我们把云销客服升级到 v0.2:给它加上会话记忆和状态管理,让它能处理真正的多轮对话。

3.6.1 升级目标

v0.1 的 Agent 能查订单、查政策,但它"记不住"对话。v0.2 的目标:

  1. 记住用户要处理的订单:用户说"帮我查订单"后给出订单号,Agent 能记住这个订单,后面追问"它到哪了"不用重复订单号
  2. 记住用户偏好:用户说"我喜欢用简洁的回答",后续回答保持简洁
  3. 结构化返回:用稳定的 status 模式,方便模型理解

3.6.2 带状态的工具

from google.adk.agents.llm_agent import Agent
from google.adk.tools import FunctionTool, ToolContext

def get_order_status(order_id: str, tool_context: ToolContext) -> dict:
    """查询订单当前状态,并把当前订单记录到会话状态中。"""
    mock_orders = {
        "ORD-20260901-001": {"status": "已发货", "logistics": "顺丰 SF1234567890"},
        "ORD-20260901-002": {"status": "待发货", "logistics": "仓库备货中"},
    }
    order = mock_orders.get(order_id)
    if order:
        # 记住当前订单,供后续追问使用
        tool_context.state["temp:current_order_id"] = order_id
        return {"status": "success", "order_id": order_id, **order}
    return {"status": "error", "message": f"未找到订单 {order_id}"}


def get_current_order_status(tool_context: ToolContext) -> dict:
    """获取会话中最近查询的订单的最新状态。"""
    order_id = tool_context.state.get("temp:current_order_id")
    if not order_id:
        return {"status": "error", "message": "会话中还没有查询过订单"}
    return get_order_status(order_id, tool_context)


yunxiao_agent = Agent(
    model='gemini-flash-latest',
    name='yunxiao_cs_agent',
    description="云销电商客服助手,支持多轮对话,能记住用户查询的订单。",
    instruction=(
        "你是云销电商的客服助手。\n"
        "规则:\n"
        "1. 用户给出订单号时,调用 get_order_status 查询并记录\n"
        "2. 用户追问'它到哪了/还在路上吗'等,如果没给新订单号,"
        "调用 get_current_order_status 查询会话中最近的订单\n"
        "3. 工具返回 error 时如实告知,不要编造\n"
    ),
    tools=[
        FunctionTool(func=get_order_status),
        FunctionTool(func=get_current_order_status),
    ],
)

3.6.3 跑起来验证多轮对话

You: 帮我查订单 ORD-20260901-001
Agent: 订单 ORD-20260901-001 已发货,顺丰 SF1234567890。

You: 它现在到哪了?
Agent: 订单 ORD-20260901-001 当前状态是已发货,物流:顺丰 SF1234567890。

You: 那 002 呢?
Agent: 订单 ORD-20260901-002 待发货,仓库备货中。

注意第二条对话——用户没有重复订单号,Agent 通过 temp:current_order_id 记住了上一个订单。这就是 Session State 在真实场景中的作用。

「为什么 ADK 这样设计」:结构化上下文 vs 把一切塞进 prompt

这一章我们见到了 ADK 的很多"上下文"概念:Session、State、Events、Memory、Compaction、Caching。你可能想问:为什么不干脆把一切都塞进 prompt 里让模型自己理解?

这其实是 Agent 框架设计的一个根本分歧:

把一切塞进 prompt 的路线(很多轻量框架的做法):

  • 把历史对话、工具结果、用户偏好全拼进一个大 prompt
  • 优点:实现简单、看起来"无脑"
  • 致命问题:
    • 不可控:历史越长 prompt 越大,迟早爆窗口
    • 不可检索:所有信息混在一起,模型要大海捞针
    • 不可结构化:没法区分"会话状态"和"长期记忆"、没法对某一部分做缓存或压缩
    • 成本爆炸:每次调用都要把全部历史重新发给模型

ADK 的结构化上下文路线

  • Session 把对话组织成事件流,按需读取
  • State 把关键数据独立存储,快速读写
  • Memory 单独管理长期知识,可搜索
  • Compaction / Caching 分别处理空间和时间成本

结构化上下文的核心洞察是:上下文不只是"喂给模型的文本",它是有结构的、可管理的资源。不同的信息有不同的生命周期(一次调用、一次会话、长期)、不同的访问方式(顺序读取、随机读写、搜索)、不同的成本特征(可缓存、可压缩)。

这就是为什么 ADK 的上下文管理能力远超"一个大的 context 变量"——它把上下文当成一个需要精心管理的系统,而不是一段可以随意拼接的字符串。生产级 Agent 和玩具 Agent 的分水岭,一半在这里。

本章小结

  • 工具是 Agent 的能力来源:函数工具(自己的函数)、MCP 工具(开放生态)、OpenAPI 工具(存量 API),可混用
  • 函数工具 = 普通函数 + FunctionTool:类型注解和 docstring 自动生成 schema,直接影响模型调用准确率
  • MCP 让 Agent 接入整个工具生态McpToolset 自动完成连接、发现、转发,支持 stdio 和远程 HTTP
  • 技能(Skill)是更高层的复用单元:指令 + 资源 + 工具打包,SKILL.md 三层结构,SkillToolset 挂载(深入内容见第 7 章)
  • Session 提供对话连续性:Events(流水账)+ State(便签纸),SessionService 管理
  • State 有作用域约定temp: / user: / app: 前缀,强制思考数据生命周期
  • 事件流让 Agent 可观测:每一步决策都是 Event,可调试、可回放、可接入观测系统
  • 上下文管理四件套:Session(会话)、Memory(记忆)、Compaction(压缩)、Caching(缓存)

练习

  1. 加一个 MCP 工具:给云销客服接一个官方 filesystem MCP server,让 Agent 能"读取本地文件"(比如读一份售后政策文档),验证 McpToolset 的接入流程。
  2. 实践 State 传递:模仿 3.6.2,写两个工具——一个记录用户偏好的回复风格,一个读取它;让 Agent 记住用户偏好并体现在回答里。
  3. 观察事件流:用 3.4.3 的 Runner 代码跑一次云销客服,把打印出来的事件流仔细看一遍,标注出"模型调用、工具调用、工具结果、最终回复"分别对应哪几行。
  4. 做一个最小 Skill:为"退款处理"建一个 skills/refund-handling/SKILL.md(写上退款流程指令),用 load_skill_from_dir + SkillToolset 挂载到 Agent,跑通。

下一章预告:第 4 章,我们把单兵变成团队。云销客服从"一个 Agent"进化成"订单/退款/物流三个子 Agent 协作的客服团队"——多智能体编排,是 ADK 2.0 的核心战场。