60个工具下Agent挑花眼?工具路由与动态检索三招解决
发布时间:2026/9/28 15:52:02来源:尧图网络
六十个工具堆在 Agent 面前的时候问题不是它“不知道选哪个”而是它开始乱选、反复横跳、甚至干脆不干活。这段时间我在折腾一个内部办公助手把各类接口从 PDF 处理、表格解析、定时任务、图片压缩到会议纪要全挂上去前前后后凑了 61 个工具。结果原本 10 秒能跑完的 PDF 转图片任务变成了模型先在十几个相似工具之间反复推理然后调了一个参数格式完全不对的接口报错后又重试一遍最后告诉我“暂不支持”。不是模型变笨了是工具选择的上下文压力把模型逼疯了。这个现象在很多 Agent 项目里都藏着。工具少的时候Function Calling 几乎是指哪打哪工具一多模型要在一个超长的系统提示词里做分类、排序、纠错难度完全不亚于让一个人在一本 500 页的产品手册里找一句话。这篇文章就把我在 60 工具场景下踩过的坑、拆过的方案、调过的参数一次性说透。不管你是刚入门搭第一个 Agent还是已经维护着几十个工具的线上服务下面这套“工具路由 动态检索 描述重构”的组合拳应该都能给你些参考。1. 六十个工具Agent 为什么会挑花眼1.1 一个再常见不过的现场先说个我实际遇到的例子。当时系统里有几个功能上有重叠的工具比如convert_pdf_to_png、pdf_to_image、image_converter还有office_doc_convert。从设计者的角度这些工具各自服务的文件类型和场景略有差别但对于模型来说它们就是“把 PDF 变成图片”的三张几乎一样的脸。用户问“帮我把这个合同的 PDF 转成图片发我”Agent 在那轮推理里先分析了三个工具的描述接着又比较了半天参数最后调用了pdf_to_image但传参用的是convert_pdf_to_png的字段格式直接报校验错误。这背后的问题就是当工具数量从 10 个涨到 60 个模型面对的不再是“该不该调用工具”而是“60 个候选里哪个才是最佳选项”。Function Calling 的原理说到底就是让大模型在给定工具列表上做条件生成模型要把用户 query 和工具描述做语义匹配还要在 JSON Schema 里构建出合法的参数。60 个工具全部塞进 context等于让模型在巨大的候选空间里同时做检索、分类、信息抽取和格式生成任何一个环节出错表现就是乱调、假调、拒调。1.2 工具注册表膨胀对推理的三重压力第一重压力是上下文窗口被工具描述占了太多。一个工具若写了 name、description、parameters平均要吃掉 150 到 300 个 token60 个工具加起来就是 9000 到 18000 个 token这些 token 还集中在系统提示词里。模型做推理的时候每轮都要重新“读”一遍这堆描述能留给对话历史和中间推理的注意力就更少了反应速度和准确率一起下降。第二重压力是长上下文里的注意力稀释。业界有大量“lost in the middle”研究模型对长文本中间部分的记忆和理解明显弱于开头和结尾。按我注册工具的习惯新建的工具往往追加在提示词末尾老工具挤在中间结果老工具慢慢成了“隐形人”明明早就挂好的image_resize被模型忘得干干净净。测试时我试过把某个高频工具的描述放在不同位置放中间时调用率掉了将近四成这数据相当吓人。第三重压力是相似工具之间的语义干扰。工具多了名称和描述难免撞车模型在生成 tool_name 时logits 分数会被这些相似项拉平最终选出来的不一定是最合适的而是描述和当前 query 表面重合度最高的。它会把“生成 PDF”和“编辑 PDF”搞混会把“定时提醒”和“日程查询”搞混本质上不是模型能力不行是工具描述本身没有给到足够强的区分度。2. 工具过多导致的典型故障表现2.1 幻觉调用与参数错乱幻觉工具调用是最常见也最头疼的问题表现形式就是模型调用了工具可这个工具的入参和真实 schema 对不上。比如send_email明明只接受to、subject、body模型却自作主张加了个cc或者把attachments传成字符串而不是数组。工具少的时候模型还能靠例子里的模式兜住工具一多参数模板在上下文里被其他工具的描述冲散生成 JSON 时就容易张冠李戴。我遇到过一次特别离谱的Agent 要查一个订单状态居然去调了create_invoice还煞有介事地传了一整套开票参数。之所以这么选是因为工具描述里都有“order”这个词模型在做语义匹配时把“查”理解成了“建”。这类错误在单次对话里很难通过重试解决因为模型每次重新审视工具列表时面对的还是同样的六十份描述、同样的模糊匹配压力。应对这种幻觉调用光靠 prompt 里写“请仔细选择工具”完全无效模型的高概率 token 已经落错位置了。我在实践中更认可的做法是给 Agent 配一个可编程的“校验层”在 Function Calling 返回之后、真正执行之前用一个轻量级规则引擎检查参数 schema 是否合法非法就直接打回重生成而不是拿着坏参数去调真实接口。很多框架自带的“tool validation”就是干这个的但默认开关往往没打开。2.2 试探性绕路与拒绝执行工具多了以后Agent 会表现得特别“怂”。遇到一个任务它先调 A 工具失败再试 B 工具又失败然后就开始自言自语“这个任务无法完成”主动放弃。真实场景里一个“把 Excel 表格里的数据提取出来并生成图表”的任务明明excel_reader和chart_generator两个工具都现成Agent 却先去调了document_intelligence因为它的描述里有“document”“data”这些词结果返回的格式不对又绕去调csv_parser最后还是没拼出图表。这类试探性绕路的根源是缺少一条快速失败的信号。工具多了模型不知道哪个能赢于是只能采用一种“试错”策略用越来越高的 token 成本去换取一个不确定的结果。更可怕的是这种失败会“传染”模型在连续失败后会怀疑整个系统的能力出现“tool execution terminated due to error”之后直接拒绝继续干活。我后来在系统里加了一个“工具失败原因反馈”机制把失败的 JSON、校验错误的具体字段名、HTTP 状态码全部塞回给模型并附一句“请换一个工具或者修正参数”。这个简单的做法把最终任务完成率从 67% 拉到了 81%效果立竿见影。对比模型自己反复试探不如给它一个明确的路障提示。2.3 上下文爆炸带来的成本飙升工具多了还有一笔隐性账单token 成本。60 个工具每个哪怕只算 180 token那就是 10800 token 的基础开销。很多 Agent 框架每轮对话都会重新注入系统提示词也就是说用户哪怕只问一句“几点开会”这 10800 token 也要一进一出算两遍费用。如果模型因为选择困难再来回推理几轮成本轻松翻倍。我做过一个粗略统计采用动态工具注入之前单次完整任务的平均 token 消耗大概是 28000其中 40% 都砸在工具描述和无效试探上。改用动态注入之后每轮进上下文的工具只有 8 到 12 个单次任务平均 token 掉到 15000 左右成本降了差不多一半响应时间也从 15 秒压到了 6 秒。对于高频调用场景这笔账算下来非常可观。所以在工具多起来的早期就要有成本意识不能把所有工具一股脑全塞进去。工具注册表不应该是“模型每轮必须阅读的图书馆目录”而应该像一个“检索式抽屉”只把最近用到的、和当前任务语义相关的几把工具推到前台。3. 我的三个落地解法路由、检索、合并3.1 工具分组与子代理路由第一个解法是给六十个工具按领域分层而不是拉平成一个巨型列表。我按业务模块把工具分成四组文档处理组、数据分析组、沟通协作组、系统管理组。然后设定了一个“总调度 Agent”它不做具体执行只负责根据用户请求选择进入哪个子代理再由子代理在其专属工具集内执行任务。这样一来真正进入模型推理上下文的工具数量从 60 个降到了每个分组内的 8 到 15 个。这方案的原理是分治。总调度模型面对的是一个 5 选 1 甚至 4 选 1 的小分类问题分类精度远高于 60 选 1子代理模型面对的是自己领域内的小工具列表任务目标清晰干扰项大幅减少。我用的是 OpenAI 的 Assistants 风格子代理其实用 LangGraph 的 subgraph 或 Dify 的工作流节点都能实现关键是“路由决策”和“工具全集”要做物理隔离不能只是逻辑分组后还堆在同一个 system prompt 里。实际调试中我给总调度 Agent 写了一份极小但明确的路由表比如“凡是涉及 PDF、Word、图片格式转换的一律走文档处理代理凡是涉及数据库查询、报表统计的一律走数据分析代理”。路由描述越具体模型选错分组的概率越低。这个方案上线后工具调用准确率从 58% 提到了 79%是最立竿见影的一步。3.2 语义检索动态注入工具第二个解法是给工具建索引然后按用户 query 动态召回相关工具。做法很简单把所有工具的 name、description、关键参数说明汇总成文本用向量模型我用的text-embedding-3-small生成工具向量存到本地向量库比如 Chroma、LanceDB 甚至就是 NumPy 数组里。每次用户发起请求时把用户的 query 也向量化做一次余弦相似度检索只把 top_k 个工具注入系统提示词。这个方案的核心逻辑是“不在推理时检索而在注册时索引”。工具描述是静态的提前向量化没有任何成本query 向量化只需要多调一次 embedding API耗时可以忽略。关键是 top_k 和相似度阈值要调试得当。我默认取 top_k10相似度阈值 0.3。测试中发现阈值太严会漏召回到常用工具太松又会让无关工具混进来。有一类特殊情况是用户问“你有哪些能力”query 和任何一个工具都不太像这种就触发一个兜底机制不做检索直接把全部工具按分组目录发给用户。这个方案还解决了“工具顺序敏感性”问题。动态检索后最相关的工具永远排在最前面模型一眼就能看到最高频的选项。即使某次召回的 top_k 里混入一两个不相干的工具排在后面的位置也不至于干扰模型产生错误调用。实测下来动态注入在相似工具较多、query 意图明确的场景下效果最好。如果你对 embedding 成本敏感也可以考虑用 BM25 关键词召回做平替体感差距不大。3.3 描述重写与复合工具合并第三个解法是对工具描述本身做瘦身和合并。很多工具描述写得像产品说明书堆满了设计词汇、动词、同义词模型根本抓不住重点。我给工具模板定了一个硬性要求description 必须包含三个部分——这个工具干什么、大概适用什么输入、典型触发词。比如把convert_pdf_to_png描述写成“将 PDF 文件转为 PNG 图片。输入 PDF 路径输出图片路径。常见于合同扫描件、纸质文件的图片化需求。”模型看到这个描述匹配成本极低。同时把功能重叠的工具合并成复合工具。比如原系统里resize_image、crop_image、rotate_image三个小工具合并成一个image_editor通过action参数区分 resize/crop/rotate。这样工具数量减少每个工具的 schema 变大了一点但总 token 开销明显下降且模型在做“选工具”时只需要选一个而不是三个。合并的原则是“执行逻辑可以收敛在同一服务里”如果两个工具的底层实现完全不同合并反而会让参数校验复杂化不建议硬并。这里有一个比较反直觉的细节工具名要起得足够口语化。fetch_latest_stock_prices和get_stock_data对工程师来说都能理解但模型对短小、动词开头的名称更敏感我后来统一把工具名改成了get_stock_prices、convert_pdf_to_png这类最直白的格式调用准确率又提了几个点。工具名不是变量名不需要追求语义长度越接近自然语言越好。还有一个容易被忽略的点写描述时避免过度使用否定句。比如“此工具不用于查询天气”这种描述模型的注意力会被“查询天气”吸引走反而更容易误调用。要正向引导写“此工具用于查询航班动态”而不是“此工具不处理天气”。这在模型眼里就是两条完全不同的注意力路径。4. 实操记录与参数调优4.1 工具路由模块的搭建记录路由模块我用的是最朴素的方式一个RouteDecision函数接收用户 query返回目标分组名。一开始想用模型去判定路由测试发现多一层模型调用就多一层延迟和误差后来改成了“关键词规则优先 模型兜底”的混合模式。规则层用一组词表做粗筛比如 query 里含“PDF/转换/图片”就直发文档处理组规则没命中时再调模型做一次轻量分类选分组而不是选工具分类难度低准确率很高。这套逻辑的代码骨架大致是这样def route_to_group(query: str) - str: rules { document: [pdf, word, 图片, 转换, ocr, ppt], data: [表格, 报表, 统计, sql, 数据库, 图表], communication: [邮件, 会议, 日程, 通知, 提醒], system: [日志, 部署, 监控, 配置, 权限], } for group, keywords in rules.items(): if any(k in query for k in keywords): return group # 兜底让轻量模型做分组 llm_resp call_model(f请将用户请求分入以下组别: ...) return parse_group(llm_resp)规则层的目的是拦截绝大多数明确意图模型兜底只处理模糊 query。这样既保证速度又不会因为规则太死板而漏掉新说法。测试时注意一点关键词不能写得过长过长的词容易互相覆盖比如“图片”命中文档组“图表”命中数据组如果 query 是“将图片数据做成图表”两个规则都命中此时要靠规则优先级或模型兜底来仲裁。路由模块上线后我统计了 200 条真实用户请求规则层直接命中约七成剩下三成走模型兜底总路由准确率在 94% 以上。这个结果说明路由决策不需要很聪明的模型关键是决策空间要小、规则要清晰把复杂留给子代理执行层。4.2 语义检索模块的参数选择动态工具检索这块我重点调了三个参数embedding 模型、top_k、阈值。embedding 模型从text-embedding-3-small换到text-embedding-3-large之后相似度排序确实更合理但延迟和成本上去了单次任务多那几十毫秒还能忍成本增加则要考虑调用量。如果每天几千次调用还是 small 版本更划算中文场景下 small 的排序质量也够用。top_k 我做了几轮对比结果如下top_k工具调用准确率平均响应时间上下文占用572.1%5.1s约 1.5k token1084.6%6.3s约 2.6k token1583.9%8.0s约 3.8k token全部6058.3%16.2s约 11k tokentop_k10 是最稳的点top_k15 准确率反而略降因为混入了一些低相关的工具干扰判断。这个结果也再次印证了“少即是多”编造一个不存在的参数。这类问题最有效的办法是在执行前端加 JSON Schema 校验用项目里现成的jsonschema库或者自己写一个字段核对函数一次校验不过就打回重新调用。具体可以这么处理捕获模型输出的 JSON逐个字段和注册表里的 schema 做对比遇到缺失字段或多余字段就返回一个错误描述并把“上次失败原因”附加到下一次 prompt 里让模型自己修正。加了这层防护后参数错误率从 16% 降到 4%是投入产出比最高的一个修复。第三种现象工具“看见了却不去用”。常见于用户用口语化表达提出需求比如“帮我把这个搞成文件发出去”工具列表里明明有create_file_and_send但模型没识别出来反而说“我无法完成发送操作”。这是描述和用户语言之间存在语义鸿沟。解决办法是在工具描述里补充一个“触发场景”字段用两三个用户原话级别的例子来标注比如“当用户说把内容做成文件/发出去/存档时使用”。这比增加工具数量更有效——工具不是不够用是描述不够贴近真实口语。5.2 几条测出来的经验最后分享几条我在实战中验证过的通用经验谈不上系统方法论但每一条都是拿失败换来的。第一工具的“分层”比“分类”更重要。分类只是把工具打上标签分层则是把决策树搭好让模型永远面对一个小候选集。我最后给所有工具都分配了层级号L1 是路由 AgentL2 是分组子代理L3 是工具本体。任何一次工具调用都只能发生在 L3而 L3 的候选集永远不超过 15 个。这套层级设计不挑框架LangGraph、Semantic Kernel、自研 loop 都能套用。第二失败信息要还给模型不要吞掉。很多 Agent 项目在工具执行异常后只返回“Error”二字等于让模型在黑暗中摸索。我在异常处理器里统一做结构化输出包含错误类型、参数校验明细、本地排查建议。模型看到“参数target_format只允许 png/jpg你传的是 webp”后常常能自己纠正并重新调用成功。错误信息越具体模型的自愈能力越强这在工具多的场景下尤其重要。第三定期清理和合并工具应该是常态。工具数量增长是自然趋势但每加一个新工具都要回头看看有没有旧的可以合并、下线。我给自己定了条规矩工具总数超过 40 就必须做一轮“减重”优先合并 action 维度的工具。把time相关的所有工具合并成一个datetime_tool把文件操作合并成一个file_ops这比让 Agent 去 60 个名字里找一个更现实。第四不要迷信“更聪明的模型会自动处理更多工具”。即使换成更强的大模型60 个工具全量注入后的准确率可能高一点但 token 成本和响应延迟依然是硬伤。工具架构的优化在任何模型上都成立正确率提升和成本下降是同时发生的值得投入时间做这件事。回头再看“Agent 给到六十个工具开始挑花眼”这个问题我现在的感受是60 个工具本身不是坏事坏的是把它们一股脑塞进同一个模型上下文里。工具系统设计得像人的工具箱常用的伸手就够到不常用的锁在抽屉里有标签、有隔层、有使用说明。别奢求模型自己在 60 个工具里选出最佳答案把问题拆成路由、检索、校验三个环节每一层只做一点点简单的判断整体效果就会稳定很多。如果你也正在被工具膨胀折磨先从给工具分组开始这可能是整个优化里最简单也最有效的一步。
网站建设高端定制企业官网