056、为大模型设计高效的工具库 📅 发布时间:2026/9/15 7:48:01 👁 浏览次数: 056 为Agent设计高效的工具库一次半夜的线上事故教会我的事凌晨两点十七分我被手机震醒。生产环境里那个负责处理客户工单的Agent突然开始疯狂循环调用工具日志里全是同一条错误——tool_result_parse_error。我登上服务器看了一眼发现Agent在一个叫get_order_status的工具上反复重试了14次每次返回的JSON都带一个多余的逗号而我们的工具解析器用的是json.loads于是直接把整个对话上下文炸了。那晚我蹲在机房改代码到天亮才意识到一个残酷的事实我们花了大把时间调prompt、调模型参数却没人认真想过——给Agent用的工具库根本不该是“能跑就行”的接口集合。那之后我花了整整两周重构工具库踩了无数坑也总结出一些真正值得写进代码里的经验。这篇笔记就当是给自己做个mark也希望能帮你绕开我走过的弯路。工具不是函数是Agent的“感官”很多同学给Agent设计工具时脑子里想的是“把Python函数注册进去”。你写一个def get_weather(city): return requests.get(...)然后挂到工具列表里完事。但真实线上Agent跑起来你会发现它完全不知道这个工具什么时候该用、参数怎么填、返回结果该怎么解读。模型不是程序员它不会去看你的docstring——即便看了也可能被你那句“参数city为城市名”带偏给你传一个北京/朝阳区进来然后你的API炸了Agent还一脸无辜地继续尝试。我的经验是每个工具的描述必须写清楚“触发条件”和“典型请求示例”。不是给人类看的注释是给模型看的“感官说明书”。比如{name:get_order_status,description:当用户询问订单物流、配送进度、是否发货时使用。仅在用户明确提供订单号时调用不要猜测订单号。,parameters:{order_id:{type:string,description:订单号形如 20250516-XXXX用户没给就问他。}}}注意那个“用户没给就问他”——这句话能避免Agent瞎编订单号。别觉得啰嗦模型真会拿123456去试。这里的坑我踩过疼。返回结构越“笨”越好工具返回给Agent的数据决定了它下一步思考的质量。很多工程师习惯直接返回原始API的JSON比如{status:200,data:{order:{id:123,state:3,logistics:[{time:2025-05-16 10:00,info:已揽收}]}},message:success}看着很标准但对模型极不友好。state: 3是什么意思模型不知道它只能猜。猜错就乱。我建议把返回内容“半加工”成自然语言的结论同时保留结构化字段供程序分支判断。像这样{status:success,summary:订单123已于2025-05-16发货当前最新状态是【运输中】最近一条物流记录是10:00 已揽收。,structured:{order_id:123,state:shipping,last_event:已揽收}}summary字段直接告诉模型发生了什么它就不用从一堆嵌套里做语义推理。structured字段留给你的代码做条件分支比如判断是否要触发异常重试。这里踩过坑——一开始我只给summary结果Agent把“运输中”误解成“已签收”差点给用户发错误短信。后来加上structured做程序侧校验才堵住这个洞。错误处理别把异常扔给模型思考最让Agent崩溃的不是你工具返回了错误而是你的工具抛异常。很多框架把Python的exception原样塞给模型模型一看KeyError: order_id它能怎么办它只能瞎猜重试或者一本正经地跟用户说“系统内部错误”。这体验跟屎一样。所以工具库的第一层防线捕获所有异常并转化为Agent能理解的“友好错误”——同时给出恢复建议。defsafe_call_tool(tool_name,params):try:returnactual_tool(tool_name,params)exceptExceptionase:# 别把堆栈丢给模型它看不懂只会吓死它return{status:error,error_type:type(e).__name__,message:f工具{tool_name}执行失败。可能原因{guess_reason(e)}。建议请告诉用户稍后重试或询问用户是否提供其他有效参数。,recoverable:True}别这样写exceptExceptionase:return{status:error,detail:str(e)}# 模型看了一脸懵关键点是recoverable字段。如果这个错误是临时的比如网络超时让Agent可以重试或转人工如果是永久性的比如参数缺失明确告诉Agent“别重试去问用户要正确信息”。这一招能把你Agent的无效调用次数降低70%以上不夸张。工具命名和参数设计别让模型做阅读理解你有两个工具一个叫fetch_user_info一个叫get_user_account_details。请问模型怎么区分它只知道都是“拿用户信息”可能随机选或者干脆乱调。为了省事我后来统一命名规则领域_动作_对象比如user_query_balance、order_cancel。动词固定用query/create/cancel/update不要用fetch/get/delete/remove混着来。模型不是不懂英文是它在一次对话里要记住几十个工具名你越像“系统命名规范”它越少犯错。参数设计更是重灾区。有些人喜欢把所有参数塞到一个object里让模型自由发挥。比如parameters:{type:object,properties:{query:{type:object,description:所有查询条件放在这里}}}这个query里该放什么模型需要自己猜。我宁可把参数扁平化一个工具最多四五个字段每个字段都给枚举值或正则示例。比如parameters:{type:object,properties:{start_date:{type:string,format:YYYY-MM-DD,description:查询起始日例如 2025-05-01},end_date:{type:string,format:YYYY-MM-DD,description:查询结束日必须晚于start_date},page_size:{type:integer,enum:[10,20,50],default:10}},required:[start_date,end_date]}不要写那些description: end_date 结束日期要写“必须晚于start_date”因为模型真的会传一个比开始日期还早的结束日期。这种约束你必须写进描述里否则它根本不会做日期比较。工具注册顺序与“近道”策略在一次对话中如果Agent有20个工具可用它每次决策都要在这20个里挑。我们得帮它降低选择成本。我通常会把工具分成两组核心工具和扩展工具。核心工具放在列表最前面并且用priority字段标记。很多开源框架支持tool_choice或forced_tool但更通用的办法是把高频工具的描述写得更详细低频工具描述写“仅在XXX且XXX时使用”直接劝退模型。还有一个trick如果两个工具经常配合使用就合并成一个复合工具。比如“查询订单获取物流轨迹”你做成一个order_query_with_tracking减少一次模型决策调用。代价是工具逻辑变复杂但换来的是Agent的稳定性和速度。在线上场景多一次模型调用就多几百毫秒延迟和多一笔token费也多了出错的可能。我宁可让工具复杂一点也别让模型在中间环节自由发挥。幂等性和重试机制把Agent的“手贱”变成无害Agent天生爱重试。你定义了order_refund它可能因为用户的一句“再试一次”就调两次退款接口。所以写工具库时必须默认“所有写操作都是非幂等的”然后在工具内部加防护。给每个生产型工具加一个request_id参数这个request_id由Agent生成或者由你框架注入一个UUID后端根据request_id做去重。别指望模型自己记住“我刚才调过了”——它记不住的它有幻觉。我自己的代码里每个写操作工具第一行就检查ifredis.exists(fdedup:{request_id}):return{status:duplicate,message:这个请求已经被处理过了请不要重复提交}redis.set(fdedup:{request_id},1,ex3600)这样Agent就算疯狂重试也不会产生两笔订单。这招救了我的命真的。工具库的“可观测性”你得知道它在干什么调试Agent最痛苦的是什么是你不知道它为什么调那个工具参数是什么返回了什么。所以工具库一定要有日志而且是结构化日志。每个工具调用前后都打一条带上tool_name、params、response_summary、latency_ms、session_id。我在本地开发时会把整个工具调用链打印成树状图一眼看出Agent在哪一步开始跑偏。更绝的是我加了一个“重放”机制把某次会话的工具调用序列保存下来然后在固定测试集上反复回放。只要工具输出变了就说明后面改代码影响了之前的行为。这个对快速迭代特别有用——改了某个工具的描述会不会导致Agent不再调用另一个工具跑一遍回放对比就知道。给模型留“退路”承认无知比瞎猜强最后一个经验每个工具库都必须有一个agent_ask_to_user的“元工具”。什么意思就是允许Agent在不确定的时候反问用户而不是硬着头皮调工具。很多人觉得这多此一举但实际线上用户的需求经常是模糊的。比如“查一下我那个订单” —— 哪个订单没有订单号。如果你没有反问工具Agent只能随机挑一个最近的订单查然后返回错误信息。有了反问工具它就学会了说“请问您的订单号是多少”。这个工具本质上是一个“软护栏”让Agent在工具的诱惑面前保持克制。我甚至把agent_ask_to_user放在工具列表的第一位权重拉满宁可让它多问一句也别让它瞎猜。最后分享一点个人的执念工具库不是一锤子买卖。你上线第一版运行一周然后去看Agent的调用日志找出那些频繁出错、频繁重试、频繁被模型误解的工具逐个优化描述和返回结构。这个过程叫“工具调优”跟prompt调优同等重要但被很多人忽略。我见过太多团队把工具库当REST API封装写完就不管了结果Agent智商被工具拖累成弱智。反过来你把工具库打磨得足够“钝” —— 每条描述都像给实习生写的操作手册每个返回都像客服话术 —— 你的Agent就会表现得像个老练的工程师。记住模型很聪明但它没见过你的业务工具库就是它摸象的那只手你把手伸得越准它就越不会把大象摸成绳子。那次线上事故之后我给工具库加了一个“熔断器”如果同一个工具在一分钟里连续失败5次直接禁止Agent再次调用并强制它走人工兜底流程。这个设计让我再也没在半夜被叫起来过。你也许觉得简单粗暴但对付一个固执的Agent简单粗暴往往就是最优雅的解法。