🛠️ CrewAI自定义工具实战:五步封装百度搜索工具,让Agent真正”联网”!

💡 导读:想让AI Agent具备实时搜索能力?本文带你从零封装一个基于百度千帆API的CrewAI工具,掌握自定义工具的标准SOP,附完整代码+避坑指南,建议收藏!


🔍 为什么需要自定义工具?

CrewAI官方提供了基础工具集,但真实业务场景中,我们往往需要:

  • ✅ 对接企业内部API
  • ✅ 集成第三方服务(如搜索、地图、天气)
  • ✅ 定制业务逻辑和输出格式

自定义工具 = Agent的”超能力扩展包” 🚀

今天我们就以百度搜索工具为例,拆解自定义工具的五步标准SOP


📋 五步标准SOP详解

第1️⃣步:定义输入Schema(Pydantic验证)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pydantic import BaseModel, Field, field_validator
from typing import Optional, List, Literal

class BaiduSearchInput(BaseModel):
"""百度搜索工具的输入参数模式"""
query: str = Field(
...,
description="搜索查询内容,不能为空"
)
top_k: Optional[int] = Field(
20,
description="返回结果数量,推荐:精确搜索≤5,广泛调研≥10"
)
recency_filter: Optional[Literal["week", "month", "semiyear", "year"]] = Field(
None,
description="时间筛选:week(7天)/month(30天)/semiyear(180天)/year(365天)"
)
sites: Optional[List[str]] = Field(
None,
description="指定搜索站点,最多20个,如['github.com', 'zhihu.com']"
)

🎯 关键技巧

  • 使用Field(...)标记必填项,Optional标记可选项
  • description不仅是注释,更是告诉Agent何时使用该工具的关键!
  • @field_validator做业务规则校验,提前拦截非法输入
1
2
3
4
5
@field_validator('query')
def validate_query(cls, v: str) -> str:
if not v or not v.strip():
raise ValueError("查询内容不能为空,请提供有效关键词")
return v.strip()

第2️⃣步:继承BaseTool,声明工具元信息

1
2
3
4
5
6
7
8
9
10
from crewai.tools import BaseTool

class BaiduSearchTool(BaseTool):
name: str = "search_web" # ⚠️ 必须是英文!CrewAI会过滤中文工具名
description: str = (
"使用百度搜索引擎查找相关信息,支持时间/站点筛选。\n"
"触发时机:查找最新信息、特定网站内容、按时间筛选结果时使用。\n"
"适用边界:仅搜索公开通用信息,专业领域请用专用工具。"
)
args_schema: Type[BaseModel] = BaiduSearchInput # 关联输入验证器

📝 Description编写心法

1
2
3
4
5
6
7
8
✅ 包含三要素:
1. 功能说明:工具能做什么
2. 触发时机:什么时候该用(帮助Agent决策)
3. 适用边界:什么时候不该用(避免误调用)

❌ 避免:
- 过于技术化的实现细节
- 模糊的"可以搜索信息"描述

第3️⃣步:实现_run方法(核心业务逻辑)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
def _run(
self,
query: str,
top_k: int = 20,
recency_filter: Optional[str] = None,
sites: Optional[List[str]] = None,
) -> str:
# 1️⃣ 获取鉴权信息
api_key = os.getenv("BAIDU_API_KEY")
if not api_key:
return "错误:缺少API认证密钥,请配置环境变量BAIDU_API_KEY"

# 2️⃣ 构建请求参数
payload = {
"messages": [{"content": query, "role": "user"}],
"search_source": "baidu_search_v2",
"resource_type_filter": [{"type": "web", "top_k": top_k}],
}
if recency_filter:
payload["search_recency_filter"] = recency_filter
if sites:
payload["search_filter"] = {"match": {"site": sites}}

# 3️⃣ 发送HTTP请求
headers = {
"X-Appbuilder-Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
response = requests.post(API_URL, json=payload, headers=headers, timeout=30)
response.raise_for_status()

# 4️⃣ 解析并格式化结果(见第5步)
...

🔧 工程化细节

  • ✅ 敏感信息(API Key)通过环境变量注入,永不硬编码
  • ✅ 请求体构建采用”按需添加”策略,避免冗余参数
  • ✅ 设置合理timeout,防止Agent卡死

第4️⃣步:错误处理 + 日志记录(稳定性保障)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
try:
response = requests.post(...)
response.raise_for_status()
result = response.json()

# 检查业务错误码
if result.get("code") not in [0, None, ""]:
return f"API错误:{result.get('message')}"

except requests.exceptions.Timeout:
logger.error("请求超时")
return "错误:服务器响应超时,请稍后重试"

except json.JSONDecodeError:
logger.error("JSON解析失败")
return "错误:响应格式异常,请重试"

except Exception as e:
logger.exception(f"未预期错误: {e}")
return f"错误:系统异常,请稍后重试"

🛡️ 错误处理三层设计

层级 处理方式 返回给Agent的内容
🔹 业务错误 解析API错误码 友好提示+解决建议
🔹 网络异常 捕获requests异常 “请稍后重试”类通用提示
🔹 未知异常 全局Exception捕获 兜底提示+日志记录

📊 日志最佳实践

1
2
3
4
5
6
# ✅ 记录关键节点,但隐藏敏感信息
logger.info(f"搜索关键词: {query},top_k: {top_k}") # 可记录
logger.info(f"请求体:\n{json.dumps(safe_payload)}") # 脱敏后记录

# ❌ 避免记录
logger.info(f"API Key: {api_key}") # 🔥 绝对禁止!

第5️⃣步:格式化输出(让Agent”看懂”结果)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 原始API返回
{
"references": [
{
"id": "1",
"title": "Python 3.12新特性",
"url": "https://example.com",
"content": "Python 3.12引入了..."
}
]
}

# ✅ 格式化为Agent友好的纯文本
结果1: [ Python 3.12新特性 ] ( https://example.com )
内容摘要: Python 3.12引入了...

结果2: [ ... ]

🎯 格式化原则

  1. 结构清晰:用固定模板,方便Agent解析关键信息
  2. 信息完整:标题+链接+摘要,三位一体
  3. 长度可控:避免返回超长文本导致Token浪费
  4. 带统计信息找到{N}条结果,帮助Agent判断信息充分性

💡 进阶技巧:让工具更”智能”

技巧1:动态调整top_k策略

1
2
3
4
5
# 在_run方法开头添加
if "最新" in query or "新闻" in query:
top_k = min(top_k, 10) # 资讯类结果少而精
elif "教程" in query or "指南" in query:
top_k = min(top_k, 30) # 教程类需要更多参考

技巧2:结果去重 + 质量排序

1
2
3
4
5
6
7
8
# 简单去重示例
seen_urls = set()
unique_refs = []
for ref in references:
if ref["url"] not in seen_urls:
seen_urls.add(ref["url"])
unique_refs.append(ref)
references = unique_refs[:top_k] # 截断到期望数量

技巧3:缓存高频查询(生产环境必备)

1
2
3
4
5
6
7
8
9
10
11
from functools import lru_cache

@lru_cache(maxsize=100)
def _cached_search(query_hash: str, top_k: int) -> str:
# 实际搜索逻辑
pass

# 在_run中调用
import hashlib
query_hash = hashlib.md5(f"{query}|{recency_filter}".encode()).hexdigest()
return _cached_search(query_hash, top_k)

🚨 常见坑点排查清单

问题现象 可能原因 解决方案
Agent不调用该工具 description写得太模糊 重写description,明确”触发时机”
工具名被忽略 使用了中文工具名 改为英文,如search_web
参数验证失败 Pydantic字段类型不匹配 检查Optional/List等类型注解
API调用超时 未设置timeout或网络问题 添加timeout=30,增加重试机制
返回结果被截断 输出文本过长 限制返回条数,或分段返回

🎓 学习总结:自定义工具核心心法

1
2
3
4
5
6
7
8
9
10
11
12
13
🔑 一个中心:以Agent理解为中心设计工具
├─ 输入:用Pydantic严格约束+清晰description
├─ 输出:结构化纯文本,避免复杂嵌套
└─ 错误:返回"可操作"的错误提示,而非堆栈

🔑 两个基本点:
1️⃣ 稳定性:完善的异常处理+日志记录
2️⃣ 可维护性:配置外置+代码注释+类型提示

🔑 三个不要:
❌ 不要硬编码敏感信息
❌ 不要返回原始JSON给Agent
❌ 不要忽略边界情况(空结果/超时/限流)

🔖 标签:#CrewAI #AI Agent #工具封装 #Python实战 #百度千帆