🛠️ 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()
|
1 2 3 4 5 6 7 8 9 10
| from crewai.tools import BaseTool
class BaiduSearchTool(BaseTool): name: str = "search_web" 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: api_key = os.getenv("BAIDU_API_KEY") if not api_key: return "错误:缺少API认证密钥,请配置环境变量BAIDU_API_KEY" 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}} 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() ...
|
🔧 工程化细节:
- ✅ 敏感信息(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
| { "references": [ { "id": "1", "title": "Python 3.12新特性", "url": "https://example.com", "content": "Python 3.12引入了..." } ] }
结果1: [ Python 3.12新特性 ] ( https://example.com ) 内容摘要: Python 3.12引入了...
结果2: [ ... ]
|
🎯 格式化原则:
- 结构清晰:用固定模板,方便Agent解析关键信息
- 信息完整:标题+链接+摘要,三位一体
- 长度可控:避免返回超长文本导致Token浪费
- 带统计信息:
找到{N}条结果,帮助Agent判断信息充分性
💡 进阶技巧:让工具更”智能”
技巧1:动态调整top_k策略
1 2 3 4 5
| 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
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实战 #百度千帆