前言:Agent 翻车,90% 死在工具系统上
上一篇我们搭好了 Agent 的骨架(ReAct 循环 + 记忆架构 + 反思机制)。但真正让 Agent 从"能说"变成"能做"的,是工具系统。
我在三个真实项目中追踪了 Agent 的故障原因分布:
| 故障类型 | 占比 | 根因 |
|---|---|---|
| 工具调用死循环 | 31% | 工具返回格式不一致,Agent 反复重试 |
| 参数错误 | 28% | Schema 定义模糊,LLM 传了错误参数 |
| 工具不存在 | 12% | 工具注册名和 LLM 理解的功能对不上 |
| 危险操作 | 9% | 没有沙箱,Agent 直接执行了 rm -rf |
| 其他 | 20% | API 超时、网络错误等 |
71% 的故障可以通过改进工具系统设计来避免。这篇文章把工具系统的每一个环节拆开,给出一套生产级的实现方案。
一、工具系统的设计目标
一个靠谱的工具系统需要满足六条要求:
| 目标 | 说明 | 做不到的后果 |
|---|---|---|
| 通用注册 | 任何函数一行代码就能注册为工具 | 每加一个工具都要改 Agent 代码 |
| 自动校验 | 参数类型、必填、枚举值自动检查 | LLM 传了错误参数,执行失败 |
| 精准描述 | 工具描述直接影响 LLM 调用准确率 | 工具误调用率高达 30% |
| 安全隔离 | 危险操作拦截 + 用户确认机制 | Agent 执行破坏性命令 |
| 失败恢复 | 超时重试、错误提示、降级策略 | 一个工具失败,整个任务挂掉 |
| 可观测 | 每次调用的输入/输出/耗时/费用 | 出问题不知道是哪一步 |
二、工具注册框架:一个装饰器搞定
2.1 为什么不用 LangChain 的 Tool 类?
LangChain 定义一个工具需要继承BaseTool,重写_run和_arun,然后注册到ToolRegistry。一个发邮件的工具要写 40 行样板代码。
我们用一个装饰器,一行@register_tool搞定:
importinspectimportjsonimporttimeimportsignalimportrefromtypingimportCallable,Any,Optional,get_type_hintsfromdataclassesimportdataclass,fieldfromfunctoolsimportwraps# ====== 全局工具注册表 ======TOOL_REGISTRY:dict[str,'ToolDefinition']={}@dataclassclassToolParameter:"""工具参数定义——给 LLM 看的描述直接决定调用准确率"""name:strtype:str# string / number / boolean / objectdescription:str# 这个描述是给 LLM 看的,写清楚传什么required:bool=Truedefault:Any=Noneenum:Optional[list]=None# 限定可选值,大幅降低参数错误率@dataclassclassToolDefinition:"""工具的完整定义"""name:strdescription:str# 包含"做什么"和"不要做什么"parameters:list[ToolParameter]func:Callable category:str="general"# 工具分类,方便组织require_confirmation:bool=False# 危险操作需用户确认timeout:int=30# 超时秒数max_retries:int=1# 失败重试次数risk_level:str="safe"# safe / dangerous / destructivedefregister_tool(name:str,description:str,parameters:list[dict]=None,category:str="general",require_confirmation:bool=False,timeout:int=30,max_retries:int=1,risk_level:str="safe",):"""装饰器:将任意 Python 函数注册为 Agent 工具。 用法: @register_tool( , description="发送邮件。参数 to 为收件人邮箱...", parameters=[ {"name": "to", "type": "string", "description": "收件人邮箱地址", "required": True}, {"name": "subject", "type": "string", "description": "邮件主题", "required": True}, {"name": "body", "type": "string", "description": "邮件正文(支持 Markdown)", "required": True}, ], category="communication", require_confirmation=True, risk_level="dangerous", ) def send_email(to: str, subject: str, body: str) -> str: ... """ifparametersisNone:parameters=[]defdecorator(func:Callable)->Callable:# 从函数签名自动推断参数类型(如果没在 parameters 里显式声明)hints=get_type_hints(func)ifhasattr(func,'__annotations__')else{}param_list=[]forpinparameters:param_type=p.get("type","string")# 如果类型没指定,从 type hints 推断if"type"notinporp["type"]=="string":py_type=hints.get(p["name"])ifpy_type==int:param_type="number"elifpy_type==bool:param_type="boolean"elifpy_typein(list,dict):param_type="object"param_list.append(ToolParameter(name=p["name"],type=param_type,description=p.get("description",""),required=p.get("required",True),default=p.get("default"),enum=p.get("enum"),))tool_def=ToolDefinition(name=name,description=description,parameters=param_list,func=func,category=category,require_confirmation=require_confirmation,timeout=timeout,max_retries=max_retries,risk_level=risk_level,)TOOL_REGISTRY[name]=tool_defreturnfuncreturndecoratordefget_tools_for_llm(category:str=None)->str:"""生成给 LLM 看的工具描述。格式直接影响调用准确率。"""tools=TOOL_REGISTRY.values()ifcategory:tools=[tfortintoolsift.category==category]lines=[