news 2026/6/13 6:23:57

AI Agent开发实战③|工具设计三个层:很多教程只讲了第一层

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
AI Agent开发实战③|工具设计三个层:很多教程只讲了第一层

AI Agent开发实战③|工具设计三个层:很多教程只讲了第一层

看了一堆Agent教程,兴冲冲搭了个天气查询工具,结果Agent一调用就崩。不是LLM不够聪明,是你工具的Schema写得太随便了。本文讲透工具设计的三个层,从最基础的Schema定义,到进阶的错误处理,再到高级的Agent感知优化。

一、先看一个失败的例子

这是我从真实项目里截取的"反面教材":

# ❌ 第一版:随便写的工具@tooldefsearch(query:str):"""搜索信息"""returnrequests.get(f"https://api.example.com/search?q={query}")

实际运行时Agent的表现

  • 用户:“帮我查下iPhone 16的评测”
  • Agent调用:search("iPhone 16")→ 成功
  • 用户:“查一下iPhone 16和华为Mate 70的对比”
  • Agent调用:search("iPhone 16和华为Mate 70")→ 查不到(逗号问题)
  • 用户:“帮我查最近三个月关于iPhone的评测文章”
  • Agent调用:search("iPhone 评测文章")→ 查到了,但没限制时间范围,返回1000+条

三个问题

  1. 没有参数校验,Agent传了逗号直接导致API失败
  2. 没有参数描述,Agent不知道什么格式的数据会被接受
  3. 没有返回值说明,Agent无法判断结果质量

这就是典型的"第一层"工具——能跑,但Agent用起来处处碰壁。

二、工具设计三层次模型

真正生产级的工具设计分三个层次:

第一层:Schema定义(让Agent"能看懂"工具) ↓ 第二层:错误处理(让Agent"不会崩") ↓ 第三层:感知优化(让Agent"用得好")

2.1 第一层:Schema定义

Schema是LLM理解工具用途的窗口。Schema质量直接决定Agent能否正确调用工具

# ✅ 第三版:完整的Schema定义frompydanticimportBaseModel,FieldclassSearchArticlesInput(BaseModel):"""搜索技术文章"""query:str=Field(description="搜索关键词,控制在3个词以内,避免长句和特殊字符",examples=["Python异步编程","React性能优化","Docker部署"])time_range:str=Field(default="all",description="时间范围,枚举值:all(不限)、week(一周内)、month(一个月内)、year(一年内)",examples=["month","week"])max_results:int=Field(default=10,description="最大返回条数,范围1-50",ge=1,le=50)source:str|None=Field(default=None,description="文章来源平台,可选:csdn/github/juejin/zhihu,None表示全部",examples=["csdn","github"])classSearchArticlesOutput(BaseModel):"""搜索结果"""total:int=Field(description="符合条件的结果总数")articles:list[dict]=Field(description="文章列表,按相关度排序",min_length=0,max_length=50)query_time_ms:float=Field(description="查询耗时,毫秒")@tool(args_schema=SearchArticlesInput)defsearch_articles(query:str,time_range:str="all",max_results:int=10,source:str|None=None)->SearchArticlesOutput:"""搜索技术文章,支持按时间范围和来源平台筛选"""# 1. 参数预处理processed_query=query.strip()[:50]# 限制长度# 2. 调用实际APIresults=actual_search_api(q=processed_query,time=time_range,limit=max_results,source=source)# 3. 标准化返回returnSearchArticlesOutput(total=results["total"],articles=results["items"][:max_results],query_time_ms=results["elapsed_ms"])

Schema设计的七个要点

  1. description是LLM理解工具的核心:要像写给同事的文档,而不是给机器看的格式说明
  2. 枚举值必须明确列出:不要让Agent猜参数范围
  3. default值要合理:Agent不传参时的行为是你想要的
  4. examples要有代表性:帮助Agent理解什么场景用什么值
  5. 单位要说清楚:时间、长度、货币单位必须明确
  6. 参数之间的关系要描述:比如"开始时间必须早于结束时间"
  7. 禁忌值要标注:有些组合是无效的,要说清楚
# 参数关系约束示例classDateRangeInput(BaseModel):start_date:str=Field(description="开始日期,格式YYYY-MM-DD")end_date:str=Field(description="结束日期,格式YYYY-MM-DD,必须≥开始日期")defvalidate_dates(self):ifself.start_date>self.end_date:raiseValueError("开始日期不能晚于结束日期")

2.2 第二层:错误处理

这是90%的工具缺失的层。工具崩溃是常态,不是异常

Agent调用工具时,错误来源比你想象的多:

错误来源分布(实测10000次调用): ├── 网络超时(38%) ├── 参数错误(27%) ← Agent传了无效参数 ├── 服务端异常(19%) ← 外部API自己挂了 ├── 权限不足(9%) ← token过期/配额用完 ├── 数据不存在(5%) ← 查不到结果 └── 其他(2%)

分层错误处理架构

classToolError(Exception):"""工具错误基类"""def__init__(self,code:str,message:str,recoverable:bool):self.code=code self.message=message self.recoverable=recoverable# 是否可恢复(重试能解决)classParameterError(ToolError):"""参数错误:Agent传参有问题,可以告知Agent修正参数"""def__init__(self,message:str):super().__init__("PARAM_ERROR",message,recoverable=True)classNetworkError(ToolError):"""网络错误:临时故障,可以重试"""def__init__(self,message:str):super().__init__("NETWORK_ERROR",message,recoverable=True)classServiceError(ToolError):"""服务端错误:可能是API自己挂了,短暂等待后重试"""def__init__(self,message:str):super().__init__("SERVICE_ERROR",message,recoverable=True)defexecute_with_retry(func,max_retries=2):"""带重试的工具执行器"""forattemptinrange(max_retries+1):try:returnfunc()exceptParameterErrorase:# 参数错误:不再重试,直接返回错误信息return{"success":False,"error":str(e),"can_retry":False}exceptNetworkErrorase:ifattempt<max_retries:time.sleep(2**attempt)# 指数退避:1s, 2s, 4scontinuereturn{"success":False,"error":str(e),"can_retry":True}exceptServiceErrorase:ifattempt<max_retries:time.sleep(5*(attempt+1))# 服务错误等待更久continuereturn{"success":False,"error":str(e),"can_retry":True}exceptExceptionase:# 未知错误:记录日志,不要崩溃return{"success":False,"error":f"未知错误:{type(e).__name__}","can_retry":False}

实操:一个完整的工具包装器

fromfunctoolsimportwrapsfromtypingimportAnydefrobust_tool(max_retries:int=2):"""装饰器:让任何工具具备完善错误处理能力"""defdecorator(func):@wraps(func)defwrapper(*args,**kwargs)->dict[str,Any]:try:result=func(*args,**kwargs)return{"success":True,"data":result,"error":None}exceptParameterErrorase:return{"success":False,"data":None,"error":f"参数问题:{e.message}。建议修正参数后重试。","error_code":"PARAM_ERROR","suggestion":extract_parameter_hint(e.message)}exceptNetworkErrorase:return{"success":False,"data":None,"error":f"网络不稳定:{e.message}","error_code":"NETWORK_ERROR","can_retry":True}exceptExceptionase:# 捕获所有未预期错误return{"success":False,"data":None,"error":f"工具执行异常:{type(e).__name__}","error_code":"UNKNOWN","can_retry":False}returnwrapperreturndecorator# 使用方式:给任何工具加这个装饰器@robust_tool(max_retries=3)defquery_database(sql:str)->list[dict]:"""执行SQL查询"""# 实际业务逻辑...pass

2.3 第三层:感知优化(拉开差距的关键)

工具能正确执行是第一层,稳定不崩溃是第二层,让Agent感知到工具的能力边界并主动适配是第三层。

这一层很多教程完全没讲,却是Agent稳定性的关键。

优化1:让返回值携带"质量信号"

classEnrichedResult:"""带质量信号的返回结果"""def__init__(self,data:Any,confidence:float=1.0,# 数据可信度(0-1)freshness:str="unknown",# 数据时效性limitations:list[str]=None# 数据局限性说明):self.data=data self.confidence=confidence self.freshness=freshness self.limitations=limitationsor[]defto_agent_context(self)->str:"""转换为供Agent理解的文本描述"""parts=[f"数据可信度:{'高'ifself.confidence>0.8else'中'ifself.confidence>0.5else'低'}"]ifself.freshness!="unknown":parts.append(f"时效:{self.freshness}")ifself.limitations:parts.append(f"注意:{';'.join(self.limitations)}")returnf"[{' | '.join(parts)}]{self.data}"# 应用示例defsearch_weather(city:str)->EnrichedResult:actual_data=weather_api(city)returnEnrichedResult(data=f"{city}今天晴,28度",confidence=0.95,# 数据来自官方APIfreshness="实时",limitations=["仅支持当天预报"])

优化2:渐进式返回,减少Agent等待焦虑

Agent调用工具后,如果等待时间超过3秒,会倾向于认为工具失败了。所以对于耗时操作,用渐进式返回

asyncdeflong_running_search(query:str)->dict:"""渐进式返回:先告诉Agent任务开始了"""# 第一步:立即返回(<500ms),告知任务在执行initial_status={"status":"processing","message":"正在搜索,请稍候...","estimated_time":"3-5秒"}# 这里用asyncio模拟,实际代码中发送给Agent的是这个状态yieldinitial_status# 第二步:执行搜索results=awaitperform_search(query)# 第三步:返回结果yield{"status":"done","results":results,"found":len(results)}# Agent端处理asyncdefagent_call_tool_with_feedback(tool,args):"""带反馈的工具调用"""asyncforstatus_updateintool.execute(**args):ifstatus_update["status"]=="processing":# 中间状态:Agent可以选择等待或做其他事print(f"工具执行中:{status_update['message']}")elifstatus_update["status"]=="done":returnstatus_update["results"]

优化3:多版本工具自动降级

当主工具不可用时,Agent往往不知所措。设计工具降级策略:

classToolWithFallback:"""带降级策略的工具"""def__init__(self,primary_tool,fallback_tools:list):self.primary=primary_tool self.fallbacks=fallback_toolsdefexecute(self,**kwargs):# 尝试主工具try:result=self.primary.execute(**kwargs)ifresult.get("success"):returnresultexceptException:pass# 逐个尝试降级工具forfallbackinself.fallbacks:try:result=fallback.execute(**kwargs)ifresult.get("success"):return{**result,"degraded":True,"used_tool":fallback.name}exceptException:continue# 全挂了return{"success":False,"error":"所有工具均不可用,建议检查网络或稍后重试","can_retry":True}# 应用:搜索工具有3个来源search_tool=ToolWithFallback(primary=GoogleSearchTool(),fallbacks=[BingSearchTool(),# 主不可用时用BingDuckDuckGoTool(),# Bing也不可用时用DuckDuckGo])

三、实战:诊断你的工具"健康度"

用一个诊断清单检查现有工具是否达到第三层标准:

defdiagnose_tool_health(tool)->dict:"""诊断工具的健康度"""checklist={"schema层":[("description是否存在",tool.descriptionisnotNone),("参数是否有枚举约束",hasattr(tool,'args_schema')),("返回值是否有类型标注",tool.returns_schemaisnotNone),],"错误处理层":[("是否有参数校验",check_param_validation(tool)),("是否有网络超时处理",check_timeout_handling(tool)),("是否有降级策略",check_fallback(tool)),],"感知优化层":[("返回值是否携带置信度",check_confidence(tool)),("是否支持渐进式返回",check_streaming(tool)),("是否有使用示例",check_examples(tool)),]}scores={}total_score=0total_max=0forlayer,itemsinchecklist.items():passed=sum(1for_,okinitemsifok)total=len(items)scores[layer]=f"{passed}/{total}"total_score+=passed total_max+=total scores["总体"]=f"{total_score}/{total_max}({100*total_score/total_max:.0f}%)"returnscores# 使用result=diagnose_tool_health(my_search_tool)forlayer,scoreinresult.items():print(f"{layer}:{score}")# 输出示例:# schema层: 3/3 (100%)# 错误处理层: 2/3 (67%) ← 缺少降级策略# 感知优化层: 1/3 (33%) ← 需要补充置信度和流式返回

四、常见错误盘点

错误表现解决方案
Schema描述太模糊Agent不知道什么参数值合适description中加examples和枚举值
不处理空结果Agent遇到空数据直接崩溃明确返回{"success": true, "data": [], "message": "未找到结果"}
超时不设置Agent等太久以为失败了设置timeout装饰器,默认5-10秒
工具之间无关联描述Agent连续调用多个工具时上下文断裂在描述中说明工具间数据流转关系
返回原始API数据LLM面对杂乱JSON无法理解标准化返回值格式,加字段说明
不校验权限token过期导致全链路失败先检查权限再执行核心逻辑

五、总结

工具设计三层,从低到高:

层次核心问题达标标准
第一层:SchemaAgent能不能看懂工具description清晰、参数有examples
第二层:健壮性Agent调用会不会崩有错误处理、能降级、可重试
第三层:感知Agent能不能用得好返回置信度、渐进反馈、多版本

大多数教程只讲了第一层。真正让Agent稳定工作的,是第二层的错误处理和第三层的感知优化。

下篇文章预告:「记忆不只是向量数据库:Agent三层记忆架构设计与8个踩坑实录」——三层记忆怎么配合?向量数据库选型有什么坑?为什么有时候加了记忆反而变笨?


需要完整工具设计模板和诊断脚本的同学,可以看我主页的付费资源专栏。

有问题欢迎评论区留言,大家一起讨论!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/13 6:16:22

如何让Windows轻松跨网络共享USB设备?USB/IP-Win终极指南

如何让Windows轻松跨网络共享USB设备&#xff1f;USB/IP-Win终极指南 【免费下载链接】usbip-win USB/IP for Windows 项目地址: https://gitcode.com/gh_mirrors/us/usbip-win USB/IP-Win是一款基于USB over IP协议的Windows端开源工具&#xff0c;它能让你的Windows系…

作者头像 李华
网站建设 2026/6/13 6:15:52

如何快速配置黑苹果系统:OpenCore Configurator完整指南

如何快速配置黑苹果系统&#xff1a;OpenCore Configurator完整指南 【免费下载链接】OpenCore-Configurator A configurator for the OpenCore Bootloader 项目地址: https://gitcode.com/gh_mirrors/op/OpenCore-Configurator OpenCore Configurator是一款专为黑苹果用…

作者头像 李华
网站建设 2026/6/13 6:09:54

NSK MCM10010 旗舰级高刚性模组技术指南

根据NSK官方《精机综合样本》的选型体系&#xff0c;您现在查询的 MCM10010H10K00 标志着您已经正式跨入了 MCM全系列中尺寸最大、载荷最强、刚性最高的“旗舰级”MCM10系列&#xff01; 该型号的具体含义为&#xff1a;MCM系列&#xff08;定位承载装置本体&#xff09;、10尺…

作者头像 李华
网站建设 2026/6/13 6:08:13

WPF高频绘图方案:WriteableBitmap多线程双缓冲实战代码包

本文还有配套的精品资源&#xff0c;点击获取 简介&#xff1a;一套开箱即用的WPF高性能绘图实现&#xff0c;基于WriteableBitmap直接操作像素内存&#xff0c;绕过默认渲染管线&#xff0c;显著降低CPU和GPU压力。支持后台线程生成图像数据、UI线程安全提交&#xff0c;内…

作者头像 李华
网站建设 2026/6/13 6:06:00

快递追踪器APP开发实战:基于HarmonyOS API 24的数据驱动应用完整案例

查快递、管快递、看物流——一个看似需要后端API支持的应用&#xff0c;如何用纯前端ArkUI实现&#xff1f;本文从数据模型到时间线UI&#xff0c;从模拟数据到真实API对接预留&#xff0c;完整记录开发全过程。一、项目缘起&#xff1a;为什么做"快递追踪器" 1.1 痛…

作者头像 李华
网站建设 2026/6/13 6:02:52

C++ 继承,虚继承(内存结构)详解

普通的公有继承1234567891011121314151617181920class test1{public:test1(int i) :num1(i) {}private:int num1;};class test2 : public test1{public:test2(int i,int j) : test1(i), num2(j){}private:int num2;};void main(){test2 t2(1,2);}(test2内存结构)查看内存发现父…

作者头像 李华