Files
Nelson 514723dbe7 init
2026-04-06 12:36:04 +08:00

565 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 第04章自定义工具开发
## 📌 本章目标
- 理解 ADK 工具系统的工作原理
- 掌握函数工具Function Tool的开发方法
- 学习 MCP 工具的集成方式
- 了解 OpenAPI 工具的生成方法
- 掌握工具间数据传递的技巧
---
## 4.1 工具系统概述
### 4.1.1 工具是什么?
在 ADK 中,**工具Tool** 是 Agent 与外部世界交互的桥梁。Agent 通过调用工具来执行特定任务,如查询数据库、调用 API、执行计算等。
```
┌─────────────────────────────────────────────┐
│ Agent │
│ │
│ 用户: "帮我查一下北京的天气" │
│ ↓ │
│ LLM 分析: 需要调用天气查询工具 │
│ ↓ │
│ 调用工具: get_weather(city="北京") │
│ ↓ │
│ 工具返回: {"status": "success", │
│ "report": "晴天25°C"} │
│ ↓ │
│ LLM 生成回复: "北京今天天气晴朗气温25°C" │
└─────────────────────────────────────────────┘
```
### 4.1.2 工具类型
| 类型 | 说明 | 适用场景 |
|------|------|----------|
| **Function Tool** | Python 函数自动转换 | 自定义逻辑、API 调用 |
| **MCP Tool** | 连接 MCP 服务器 | 标准化工具集成 |
| **OpenAPI Tool** | 从 OpenAPI 规范生成 | REST API 集成 |
| **Agent Tool** | 将 Agent 作为工具使用 | Agent 复用 |
| **内置工具** | Google 提供的预置工具 | 搜索、代码执行等 |
---
## 4.2 函数工具Function Tool
### 4.2.1 基本原理
ADK 会自动检查 Python 函数的以下信息来生成工具 Schema
- **函数名**:工具的名称
- **docstring**:工具的描述(发送给 LLM
- **参数类型**:参数的类型提示
- **默认值**:区分必选和可选参数
- **返回值**:工具的输出
### 4.2.2 基础示例
```python
"""
函数工具基础示例
演示如何创建简单的自定义工具
"""
from google.adk.agents import Agent # 导入 Agent 类
# ========================================
# 示例一:简单的天气查询工具
# ========================================
def get_weather(city: str) -> dict:
"""
获取指定城市的天气信息
Args:
city (str): 城市名称,例如"北京""上海"
Returns:
dict: 包含天气状态的字典
"""
# 模拟天气数据(实际应用中应调用真实天气 API
weather_data = { # 模拟天气数据库
"北京": {"temp": "25°C", "condition": "晴天"}, # 北京天气
"上海": {"temp": "28°C", "condition": "多云"}, # 上海天气
"广州": {"temp": "32°C", "condition": "雷阵雨"}, # 广州天气
}
# 查找城市天气数据
data = weather_data.get(city) # 从字典中获取天气数据
# 如果找不到该城市的天气
if not data: # 检查数据是否存在
return { # 返回错误信息
"status": "error", # 状态:错误
"error_message": f"未找到'{city}'的天气信息" # 错误描述
}
# 返回成功结果
return { # 返回天气信息
"status": "success", # 状态:成功
"city": city, # 城市名称
"temperature": data["temp"], # 温度
"condition": data["condition"], # 天气状况
}
# ========================================
# 示例二:带可选参数的工具
# ========================================
def search_restaurant(
city: str, # 必选参数:城市
cuisine: str = "中餐", # 可选参数:菜系,默认中餐
price_range: str = "中等", # 可选参数:价格范围,默认中等
) -> dict:
"""
搜索指定城市的餐厅
Args:
city (str): 城市名称
cuisine (str, optional): 菜系类型,默认为"中餐"
price_range (str, optional): 价格范围,默认为"中等"
Returns:
dict: 包含搜索结果的字典
"""
# 模拟餐厅搜索结果
return { # 返回搜索结果
"status": "success", # 状态:成功
"city": city, # 城市
"cuisine": cuisine, # 菜系
"price_range": price_range, # 价格范围
"results": [ # 餐厅列表
{"name": "美味餐厅", "rating": 4.5}, # 餐厅1
{"name": "佳肴轩", "rating": 4.2}, # 餐厅2
],
}
# ========================================
# 创建使用工具的 Agent
# ========================================
agent = Agent(
name="tool_agent", # Agent 名称
model="gemini-2.0-flash", # 使用 Gemini 模型
instruction=( # 系统指令
"你是一个生活助手。\n"
"你可以查询天气和搜索餐厅。\n"
"根据用户需求使用合适的工具。"
),
tools=[ # 注册工具列表
get_weather, # 天气查询工具
search_restaurant, # 餐厅搜索工具
],
)
```
### 4.2.3 参数类型详解
```python
"""
工具参数类型详解
展示不同类型的参数定义方式
"""
from typing import Optional, List # 导入类型提示
from google.adk.agents import Agent # 导入 Agent 类
# ========================================
# 必选参数 vs 可选参数
# ========================================
def calculate_loan(
principal: float, # 必选:贷款本金
annual_rate: float, # 必选:年利率
years: int, # 必选:贷款年限
extra_payment: float = 0.0, # 可选额外月供默认0
) -> dict:
"""
计算贷款月供和总利息
Args:
principal (float): 贷款本金(元)
annual_rate (float): 年利率(百分比,如 4.5 表示 4.5%
years (int): 贷款年限
extra_payment (float, optional): 每月额外还款金额,默认为 0
Returns:
dict: 包含月供、总利息等信息的字典
"""
monthly_rate = annual_rate / 100 / 12 # 计算月利率
total_months = years * 12 # 计算总月数
# 计算等额本息月供
monthly_payment = principal * monthly_rate * (1 + monthly_rate) ** total_months / ((1 + monthly_rate) ** total_months - 1)
total_amount = monthly_payment * total_months + extra_payment * total_months # 总还款额
total_interest = total_amount - principal # 总利息
return { # 返回计算结果
"status": "success", # 状态
"monthly_payment": round(monthly_payment, 2), # 月供
"total_interest": round(total_interest, 2), # 总利息
"total_amount": round(total_amount, 2), # 总还款额
}
# ========================================
# 使用 Optional 标记可选参数
# ========================================
def create_user_profile(
username: str, # 必选:用户名
bio: Optional[str] = None, # 可选:个人简介
avatar_url: Optional[str] = None, # 可选:头像 URL
) -> dict:
"""
创建用户档案
Args:
username (str): 用户名
bio (str, optional): 个人简介
avatar_url (str, optional): 头像图片 URL
Returns:
dict: 创建结果
"""
profile = { # 创建用户档案字典
"username": username, # 用户名
"bio": bio or "这个人很懒,什么都没写。", # 简介(默认值)
"avatar_url": avatar_url or "", # 头像 URL
}
return { # 返回结果
"status": "success", # 状态
"profile": profile, # 用户档案
}
# ========================================
# 使用 List 类型参数
# ========================================
def compare_cities(cities: List[str]) -> dict:
"""
比较多个城市的信息
Args:
cities (List[str]): 城市名称列表
Returns:
dict: 城市比较结果
"""
city_info = { # 城市信息数据库
"北京": {"population": "2189万", "area": "16410km²"}, # 北京
"上海": {"population": "2487万", "area": "6341km²"}, # 上海
"广州": {"population": "1881万", "area": "7434km²"}, # 广州
}
results = [] # 初始化结果列表
for city in cities: # 遍历城市列表
info = city_info.get(city) # 获取城市信息
if info: # 如果信息存在
results.append({ # 添加到结果列表
"city": city, # 城市名
**info, # 展开城市信息
})
return { # 返回比较结果
"status": "success" if results else "error", # 状态
"results": results, # 比较结果
}
# 创建 Agent
agent = Agent(
name="param_agent", # Agent 名称
model="gemini-2.0-flash", # 模型
instruction="你是一个多功能助手。", # 指令
tools=[ # 工具列表
calculate_loan, # 贷款计算器
create_user_profile, # 用户档案创建
compare_cities, # 城市比较
],
)
```
### 4.2.4 工具返回值最佳实践
```python
"""
工具返回值最佳实践
返回值会被 LLM 读取,因此需要清晰、结构化
"""
# ❌ 不好的返回值:信息不清晰
def bad_tool(query: str) -> str:
"""不好的返回值示例"""
return "0" # LLM 不知道 0 代表什么
# ✅ 好的返回值:结构化、有状态码
def good_tool(query: str) -> dict:
"""
好的返回值示例
包含状态码和详细描述
"""
return {
"status": "success", # 状态码success / error / pending
"query": query, # 原始查询
"result": "查询结果详情", # 具体结果
"metadata": { # 可选的元数据
"source": "database", # 数据来源
"timestamp": "2025-04-05T10:00:00Z", # 时间戳
},
}
# ✅ 错误处理:返回有意义的错误信息
def robust_tool(query: str) -> dict:
"""
健壮的工具:包含完善的错误处理
"""
if not query or not query.strip(): # 检查空输入
return { # 返回参数错误
"status": "error", # 状态:错误
"error_code": "INVALID_INPUT", # 错误码
"error_message": "查询内容不能为空,请提供有效的查询参数。" # 人类可读的错误信息
}
try: # 尝试执行操作
result = do_something(query) # 执行实际操作
return { # 返回成功结果
"status": "success", # 状态:成功
"result": result, # 操作结果
}
except Exception as e: # 捕获异常
return { # 返回异常信息
"status": "error", # 状态:错误
"error_code": "INTERNAL_ERROR", # 错误码
"error_message": f"处理请求时发生错误:{str(e)}" # 错误描述
}
def do_something(query: str) -> str:
"""模拟操作函数"""
return f"处理结果: {query}" # 返回处理结果
```
---
## 4.3 工具间数据传递
```python
"""
工具间数据传递
使用 session.state 的 temp: 前缀在工具间传递数据
"""
from google.adk.agents import Agent # 导入 Agent 类
from google.adk.tools import FunctionTool # 导入 FunctionTool
from google.adk.tools import ToolContext # 导入工具上下文
# ========================================
# 使用 ToolContext 访问会话状态
# ========================================
def step1_fetch_data(
query: str, # 搜索关键词
ctx: ToolContext, # 工具上下文(自动注入)
) -> dict:
"""
第一步:获取原始数据
Args:
query (str): 搜索关键词
ctx (ToolContext): 工具上下文,可用于访问会话状态
Returns:
dict: 原始数据
"""
# 模拟获取数据
raw_data = f"关于'{query}'的原始数据..." # 模拟数据
# 将数据保存到临时状态temp: 前缀)
# temp: 状态只在当前调用中有效,调用结束后自动清除
ctx.state["temp:raw_data"] = raw_data # 保存到临时状态
return { # 返回结果
"status": "success", # 状态
"message": f"已获取关于'{query}'的数据", # 消息
"data_length": len(raw_data), # 数据长度
}
def step2_analyze_data(ctx: ToolContext) -> dict:
"""
第二步:分析第一步获取的数据
Args:
ctx (ToolContext): 工具上下文
Returns:
dict: 分析结果
"""
# 从临时状态中读取第一步保存的数据
raw_data = ctx.state.get("temp:raw_data") # 读取临时状态
if not raw_data: # 如果没有数据
return { # 返回错误
"status": "error",
"error_message": "没有找到原始数据,请先执行数据获取步骤。"
}
# 模拟数据分析
analysis = f"对数据进行了深度分析:{raw_data[:50]}..." # 分析结果
return { # 返回分析结果
"status": "success", # 状态
"analysis": analysis, # 分析结果
}
# 创建 Agent
agent = Agent(
name="pipeline_agent", # Agent 名称
model="gemini-2.0-flash", # 模型
instruction=( # 指令
"你是一个数据分析助手。\n"
"当用户需要分析数据时,先使用 step1_fetch_data 获取数据,\n"
"然后使用 step2_analyze_data 进行分析。"
),
tools=[ # 工具列表
step1_fetch_data, # 数据获取工具
step2_analyze_data, # 数据分析工具
],
)
```
---
## 4.4 Agent-as-a-Tool智能体作为工具
```python
"""
Agent-as-a-Tool
将一个 Agent 包装为工具,供其他 Agent 调用
"""
from google.adk.agents import Agent # 导入 Agent 类
from google.adk.tools import AgentTool # 导入 AgentTool 类
# ========================================
# 定义一个专门的翻译 Agent
# ========================================
translator_agent = Agent(
name="translator", # 翻译 Agent 名称
model="gemini-2.0-flash", # 使用 Gemini 模型
instruction="你是一个专业翻译,将用户输入的文本翻译成目标语言。只输出翻译结果。", # 指令
)
# ========================================
# 将翻译 Agent 包装为工具
# ========================================
translation_tool = AgentTool( # 创建 AgentTool
agent=translator_agent, # 传入要包装的 Agent
)
# ========================================
# 在主 Agent 中使用翻译工具
# ========================================
main_agent = Agent(
name="main_agent", # 主 Agent 名称
model="gemini-2.0-flash", # 使用 Gemini 模型
instruction=( # 系统指令
"你是一个多语言助手。\n"
"当用户需要翻译时,使用 translation_tool 工具。\n"
"其他情况直接回答用户问题。"
),
tools=[translation_tool], # 注册翻译工具
)
# AgentTool 与 sub_agents 的区别:
# - sub_agents通过 LLM Transfer 机制委派任务Agent 切换执行
# - AgentTool作为工具被调用在当前 Agent 上下文中执行
```
---
## 4.5 MCP 工具集成
```python
"""
MCPModel Context Protocol工具集成
连接外部 MCP 服务器作为 Agent 的工具
"""
from google.adk.agents import Agent # 导入 Agent 类
from google.adk.tools.mcp_tool.mcp_toolset import McpToolset # 导入 MCP 工具集
# ========================================
# 连接本地 MCP 服务器
# ========================================
async def create_mcp_agent():
"""创建使用 MCP 工具的 Agent"""
# 创建 MCP 工具集
# 需要先启动 MCP 服务器(如 filesystem、database 等)
mcp_tools = McpToolset(
connection_params={ # MCP 服务器连接参数
"url": "http://localhost:3000", # MCP 服务器地址
},
tool_names=[ # 要导入的工具名称列表
"read_file", # 读取文件工具
"write_file", # 写入文件工具
"list_directory", # 列出目录工具
],
)
# 获取 MCP 工具列表
tools = await mcp_tools.get_tools() # 异步获取工具
# 创建使用 MCP 工具的 Agent
agent = Agent(
name="mcp_agent", # Agent 名称
model="gemini-2.0-flash", # 使用 Gemini 模型
instruction="你是一个文件管理助手,可以读写文件和浏览目录。", # 指令
tools=tools, # 注册 MCP 工具
)
return agent # 返回 Agent
```
---
## 4.6 完整代码示例
详见 `code/custom_tools_demo.py` — 包含完整的自定义工具开发示例。
---
## 📌 本章小结
- ADK 自动将 Python 函数转换为工具,通过函数签名生成 Schema
- 好的 docstring 是 LLM 理解工具的关键
- 返回值推荐使用字典,包含 `status` 字段
- `ToolContext` 允许工具访问会话状态,实现工具间数据传递
- `AgentTool` 可以将 Agent 包装为工具供其他 Agent 使用
- MCP 工具集支持连接外部标准化工具服务器
**下一章**[第05章 - 多智能体系统](./05-multi-agent-systems.md) → 学习如何构建多 Agent 协作系统。