第 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,包含name和description,用于技能发现。规则: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 包含两部分:
- Events(事件):对话按时间排列的记录——用户说了什么、Agent 说了什么、调用了什么工具、结果如何。这是会话的"流水账"。
- 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 的上下文管理应该是:
- 会话内:Session 管理 Events + State
- 跨会话:Memory 提供长期记忆
- 历史太长:Compaction 压缩旧内容
- 重复内容:Caching 省 token
这四层构成了完整的上下文管理方案。这也是为什么说"上下文是 Agent 的命门"——玩具 Agent 只会在一个 prompt 里塞所有东西,生产 Agent 用这套分层方案管理有限而昂贵的上下文。
3.6 主线项目:云销客服 v0.2
这一节我们把云销客服升级到 v0.2:给它加上会话记忆和状态管理,让它能处理真正的多轮对话。
3.6.1 升级目标
v0.1 的 Agent 能查订单、查政策,但它"记不住"对话。v0.2 的目标:
- 记住用户要处理的订单:用户说"帮我查订单"后给出订单号,Agent 能记住这个订单,后面追问"它到哪了"不用重复订单号
- 记住用户偏好:用户说"我喜欢用简洁的回答",后续回答保持简洁
- 结构化返回:用稳定的 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(缓存)
练习
- 加一个 MCP 工具:给云销客服接一个官方 filesystem MCP server,让 Agent 能"读取本地文件"(比如读一份售后政策文档),验证
McpToolset的接入流程。 - 实践 State 传递:模仿 3.6.2,写两个工具——一个记录用户偏好的回复风格,一个读取它;让 Agent 记住用户偏好并体现在回答里。
- 观察事件流:用 3.4.3 的 Runner 代码跑一次云销客服,把打印出来的事件流仔细看一遍,标注出"模型调用、工具调用、工具结果、最终回复"分别对应哪几行。
- 做一个最小 Skill:为"退款处理"建一个
skills/refund-handling/SKILL.md(写上退款流程指令),用load_skill_from_dir+SkillToolset挂载到 Agent,跑通。
下一章预告:第 4 章,我们把单兵变成团队。云销客服从"一个 Agent"进化成"订单/退款/物流三个子 Agent 协作的客服团队"——多智能体编排,是 ADK 2.0 的核心战场。