1. 从一次线上事故说起:为什么工具选择策略值得单独拎出来讲
去年冬天我接手了一个内部运维助手的项目,功能不复杂:让 Agent 根据用户自然语言指令,去调用一系列内部工具完成查询、变更、通知等操作。上线第一周就炸了。用户说“帮我看看昨天订单服务的错误日志”,Agent 老老实实调了日志查询工具,没问题;用户说“把 staging 环境重启一下”,Agent 也调了重启工具,也没问题。但有个用户说了句“帮我确认下线上服务是不是都正常”,Agent 一口气串行调了十几个健康检查工具,每个工具超时 30 秒,用户等了六分多钟,最后返回一句“部分服务异常”,用户直接投诉。
问题出在哪?不是模型不够聪明,也不是工具本身有问题,而是工具选择策略太粗糙了。当时我的实现是最原始的硬编码路由:用一堆 if-else 和关键词匹配来决定调哪个工具。这种方案在工具数量少于 5 个、用户表达高度规范的时候能跑,一旦工具数量上到二三十个、用户表达千变万化,就彻底失控。
这件事之后我花了大概两个月时间,把工具选择策略从硬编码一路迭代到动态决策,中间踩了无数坑。这篇文章就是把这套演进路径完整拆开讲清楚:硬编码到底什么时候能用、什么时候必须换;动态决策的核心机制是什么;MCP 协议在这个环节扮演什么角色;以及在实际工程中怎么平衡准确率、延迟和成本。如果你正在做 Agent 开发,或者正准备把原型推向生产,这些经验应该能帮你少走不少弯路。
2. 工具选择策略的本质:一个被低估的决策问题
2.1 工具选择到底在解决什么问题
很多人把 Agent 的工具选择理解成“让模型选一个函数调用”,这个理解太浅了。工具选择策略真正要解决的是一个在不确定输入下做多约束决策的问题。约束条件至少包括四层:
第一层是语义匹配。用户说“查一下昨天的错误日志”,系统里可能有query_logs、search_events、fetch_metrics三个工具都能沾边,选哪个取决于用户真实意图。第二层是参数完备性。有些工具需要必填参数,比如时间范围、服务名,用户没说全的时候,是追问还是用默认值,这本身就是策略。第三层是执行约束。工具可能有权限要求、速率限制、依赖关系,比如重启工具必须先通过健康检查。第四层是成本与延迟。同样能完成任务的工具,有的走缓存 50ms 返回,有的要查冷存储 3 秒,选哪个直接影响用户体验。
硬编码策略通常只处理第一层,而且是用关键词匹配这种极其脆弱的方式。动态决策则要把四层约束都纳入考量。这就是为什么工具数量一多、场景一复杂,硬编码就撑不住了。
2.2 硬编码、规则引擎、动态决策的边界在哪里
我整理了一张对照表,这是我实际项目中用来判断该用哪种策略的依据:
| 维度 | 硬编码 if-else | 规则引擎 | 动态决策(模型驱动) |
|---|---|---|---|
| 适用工具数量 | 1-5 个 | 5-20 个 | 20 个以上 |
| 用户表达规范度 | 高度规范 | 中等规范 | 任意自然语言 |
| 维护成本 | 低但脆弱 | 中等 | 前期高后期低 |
| 准确率上限 | 约 70% | 约 85% | 约 95% |
| 延迟 | 极低(微秒级) | 低(毫秒级) | 中(百毫秒到秒级) |
| 可解释性 | 完全可解释 | 可解释 | 需要额外日志 |
这张表不是绝对的,但边界感很重要。我见过太多项目在工具只有 3 个的时候就上动态决策,结果引入了一堆不必要的复杂度和延迟;也见过工具已经 40 多个还在用 if-else 硬扛,每次加工具都要改核心路由代码,改一次崩一次。
判断标准其实很简单:当你发现新增一个工具需要修改工具选择逻辑本身时,就该考虑升级策略了。硬编码的问题不在于它简单,而在于它把“工具定义”和“选择逻辑”耦合在了一起。
2.3 MCP 协议带来的结构性变化
MCP(Model Context Protocol)这两年被讨论得很多,但很多人只把它当成一个“工具接入标准”。在我看来,MCP 对工具选择策略最大的价值是解耦。在 MCP 之前,每个 Agent 框架都有自己的工具定义格式,工具选择逻辑和工具实现绑死。MCP 把工具的描述、参数 schema、调用方式标准化之后,工具选择策略可以独立演进。
具体来说,MCP 提供了几个关键能力:工具的能力描述是结构化的(name、description、inputSchema),这意味着动态决策可以直接消费这些元数据;工具可以动态注册和发现,意味着策略不需要硬编码工具列表;工具调用有统一的错误语义,意味着策略可以根据错误类型做重试或降级。
我实际用下来,MCP 让工具选择策略的迭代速度至少快了一倍。以前加一个工具要改三处代码,现在只需要在 MCP server 里注册,Agent 侧的策略自动就能感知到。这个结构性变化是后面所有动态决策方案的基础。
3. 硬编码策略:什么时候它是对的,什么时候是灾难
3.1 硬编码的三种典型写法与各自的坑
我见过和写过的硬编码策略大概有三种形态,每一种都有它特定的适用场景和隐藏的坑。
第一种是纯关键词匹配。代码大概长这样:
def select_tool(user_input): if "日志" in user_input or "log" in user_input: return query_logs elif "重启" in user_input or "restart" in user_input: return restart_service elif "监控" in user_input or "metric" in user_input: return fetch_metrics else: return None这种写法在 demo 阶段极其高效,十分钟就能跑通。但它的坑在于关键词冲突和覆盖不全。用户说“帮我看看服务状态”,既没有“日志”也没有“监控”,直接返回 None。用户说“重启前先看下日志”,同时命中两个关键词,走第一个分支,但用户真实意图是先查日志再重启,需要多工具编排,硬编码根本处理不了。
第二种是意图分类器 + 映射表。先训一个小分类模型或者用规则做意图识别,再把意图映射到工具。这种比纯关键词稳一些,但坑在于意图和工具不是一一对应的。同一个意图可能需要不同工具组合,取决于上下文。比如“查服务状态”这个意图,在故障排查场景下要调健康检查工具,在日常巡检场景下要调指标查询工具,分类器没法区分。
第三种是决策树。把选择逻辑写成一棵显式的树,每个节点是一个条件判断。这种可解释性最好,但坑在于树的维护成本随工具数量指数增长。我维护过一棵 30 个工具的决策树,深度到了 7 层,每次加工具都要重新审视整棵树的分支,改一次要回归测试两天。
3.2 硬编码的合理使用场景
说了这么多坑,但硬编码不是一无是处。在下面这些场景里,硬编码反而是最优解:
- 工具数量极少且稳定:比如只有“查询”和“写入”两个工具,硬编码清晰直接,引入动态决策纯属过度设计。
- 高频路径需要极致延迟:有些场景对延迟极其敏感,比如实时对话中的简单指令,硬编码的微秒级响应是动态决策给不了的。我的做法是高频路径硬编码兜底,长尾路径走动态决策。
- 安全敏感操作需要强管控:涉及资金、权限变更的操作,硬编码的显式判断反而更安全,因为每一条路径都是人工审核过的,不存在模型幻觉导致误调用的风险。
- 作为动态决策的降级方案:动态决策服务挂了或者超时了,硬编码兜底能保证核心功能可用。这个后面会详细讲。
我的经验是:硬编码不是要被淘汰的旧方案,而是动态决策体系里的一个必要组件。把它放在正确的位置,它就能发挥价值。
3.3 从硬编码迁移的时机判断
迁移时机判断错了,要么过早引入复杂度,要么过晚导致线上事故。我总结了一个简单的判断清单,命中任意两条就该考虑迁移:
- 工具数量超过 10 个,且还在持续增加。
- 新增工具平均需要修改工具选择逻辑 2 处以上。
- 线上出现因工具选择错误导致的用户投诉,且每月超过 3 次。
- 用户输入的自然语言多样性明显增加,关键词匹配覆盖率低于 80%。
- 需要支持多工具编排(一个请求触发多个工具按序执行)。
我自己的项目是在工具数量到 15 个、月投诉 5 次的时候开始迁移的。回头看,其实在工具到 10 个的时候就该动了,多撑的那段时间纯粹是在给自己挖坑。
4. 动态决策的核心机制:让 Agent 自己学会选工具
4.1 动态决策的三种实现范式
动态决策不是单一技术,而是一类方案的统称。我实际用过和评估过的有三种范式,各有取舍。
第一种是纯 Prompt 驱动。把所有工具的 name、description、inputSchema 塞进 system prompt,让模型直接输出要调用的工具和参数。这种实现最简单,MCP 标准化之后尤其方便,因为工具描述是结构化的,直接序列化进 prompt 就行。坑在于工具数量一多,prompt 就爆炸。我试过 40 个工具全塞进去,光工具描述就占了 8000 token,模型注意力被稀释,选择准确率反而下降。
第二种是检索增强的工具选择。先用向量检索从工具库里召回 Top-K 个候选工具,再把候选塞进 prompt 让模型选。这种解决了 prompt 爆炸问题,但引入了新的坑:检索召回率决定了准确率上限。如果正确的工具没被召回,模型再聪明也没用。我的做法是检索召回 Top-10,再让模型从 10 个里选,实测准确率比全量塞 prompt 高 8 个百分点。
第三种是微调专用选择模型。用历史调用数据微调一个小模型专门做工具选择。这种准确率最高,但成本也最高,而且工具一变就要重新微调。我只在工具集极其稳定的场景下推荐这种。
我最终采用的是检索增强 + Prompt 驱动的混合方案,兼顾了准确率、成本和灵活性。下面详细拆解这套方案的实现。
4.2 工具描述的写法直接决定选择准确率
这一点被严重低估。很多人写工具 description 就是一句话“查询日志”,然后抱怨模型选不准。工具描述是模型做决策的唯一依据,它的质量直接决定准确率。
我总结了一套工具描述的写法规范,实测能把选择准确率提升 15 个百分点以上:
- 描述要写“什么时候用”,而不是“这是什么”。差的写法:“查询日志工具”。好的写法:“当用户需要排查服务错误、查看历史请求记录、分析异常行为时使用。不适用于查询实时指标(用 fetch_metrics)或查看服务健康状态(用 health_check)”。
- 显式写出排除场景。模型很容易在相似工具间混淆,主动告诉它“这个工具不用于什么”比只告诉它“用于什么”有效得多。
- 参数描述要包含示例值。
time_range参数描述写成“时间范围,格式如 2024-01-01 到 2024-01-02,默认最近 1 小时”,比“时间范围”四个字强太多。 - 标注工具的副作用和成本。比如“此工具会触发实际重启操作,调用前需确认”、“此工具查询冷存储,延迟约 3 秒”,这些信息能帮模型在多个候选间做更合理的权衡。
我维护了一个工具描述模板,每个新工具都按这个模板写,团队里谁来写都保证质量下限。
4.3 多工具编排的决策逻辑
单工具选择只是入门,真正的难点是多工具编排。用户说“重启前先看下日志”,这需要先调日志工具,根据结果决定是否重启。用户说“把所有异常服务重启”,这需要先调健康检查,拿到异常列表,再对每个异常服务调重启。
动态决策处理编排有两种思路。一种是让模型输出完整的执行计划,比如输出一个工具调用序列,Agent 按序执行。这种适合步骤确定的场景,但模型很难预判中间结果对后续步骤的影响。另一种是ReAct 式的逐步决策,模型每次只决定下一步调什么工具,根据上一步结果再决定下一步。这种更灵活,但延迟更高,因为每步都要过一次模型。
我的实践是混合:对于步骤数少于 3 步的编排,让模型一次性输出计划;超过 3 步的,走 ReAct 逐步决策。这个阈值是根据实际延迟数据调出来的,3 步以内一次性规划的总延迟更低,超过 3 步逐步决策的准确率优势更明显。
5. 实操落地:一套可复现的动态决策实现
5.1 整体架构与数据流
先讲清楚整体架构,再拆细节。我的实现分四层:
第一层是工具注册层,基于 MCP 协议,所有工具通过 MCP server 注册,提供标准化的 name、description、inputSchema。第二层是检索层,把工具描述向量化存入向量库,用户输入来了先做向量检索召回 Top-K 候选。第三层是决策层,把候选工具和用户输入组装成 prompt,调模型输出工具选择和参数。第四层是执行与反馈层,执行工具调用,记录结果,把成功和失败案例回流到检索层和决策层做优化。
数据流是这样的:用户输入 → 向量检索召回候选 → 组装 prompt → 模型决策 → 参数校验 → 执行 → 结果回流。每一层都有降级和兜底机制,后面细讲。
5.2 检索层的实现细节
检索层看着简单,但有几个关键参数直接影响最终效果。
向量模型的选择。我试过三种:通用文本向量模型、代码向量模型、以及专门微调的工具检索模型。通用模型在工具描述这种半结构化文本上表现一般,代码向量模型反而更好,因为工具描述里有很多参数名、格式说明,和代码语义接近。最终我用的是一个在工具调用数据上微调过的向量模型,召回率比通用模型高 12 个百分点。
Top-K 的取值。K 太小召回不全,K 太大 prompt 变长。我实测下来 K=10 是个甜点,召回率能到 96%,prompt 长度也可控。如果工具总数少于 15 个,直接全量塞 prompt 就行,不用检索。
检索的 query 构造。不要直接拿用户原始输入去检索,先做一次轻量的意图改写。比如用户说“服务挂了帮我看看”,改写成“查询服务健康状态和错误日志”,检索命中率明显提升。这个改写可以用一个小模型做,成本很低。
5.3 决策层的 Prompt 工程
决策层的 prompt 我迭代了十几版,最终稳定下来的结构是这样的:
你是一个工具选择助手。根据用户请求,从候选工具中选择最合适的工具并填充参数。 候选工具: {tools_json} 用户请求:{user_input} 决策规则: 1. 优先选择描述中明确匹配用户意图的工具 2. 如果多个工具都匹配,选择参数最完备、副作用最小的 3. 如果用户请求需要多个工具协作,输出工具调用序列 4. 如果没有任何工具匹配,输出 {"tool": null, "reason": "..."} 5. 参数缺失时,使用工具描述中的默认值,不要编造 输出格式(严格 JSON): {"tool": "工具名", "params": {...}, "reason": "选择理由"}几个关键点:强制 JSON 输出保证可解析;要求输出 reason方便排查和回流;明确参数缺失的处理规则避免模型编造参数;给出无匹配时的输出格式避免模型硬选一个。
实测下来,这套 prompt 在 40 个工具的场景下,单工具选择准确率能到 93%,多工具编排准确率约 85%。剩下的误差主要来自工具描述本身的歧义,需要持续优化描述。
5.4 参数校验与执行兜底
模型输出的参数不能直接信。我在执行前加了一层校验:
- 类型校验:参数类型必须匹配 inputSchema,字符串、数字、布尔、数组、对象都要检查。
- 必填校验:必填参数缺失直接拒绝,触发追问流程。
- 枚举校验:枚举类型参数必须在允许值范围内。
- 范围校验:数值参数检查上下界,比如时间范围不能超过 30 天。
- 权限校验:当前用户是否有权限调用该工具。
校验不通过的处理策略分两种:如果是参数格式问题,把校验错误信息回传给模型,让它重新生成一次;如果是权限或范围问题,直接返回用户可读的错误提示,不重试。
执行层还要有超时和重试。我给每个工具设了独立的超时时间,默认 10 秒,查询类工具 30 秒,变更类工具 60 秒。超时后根据工具类型决定是否重试,查询类可以重试一次,变更类绝不自动重试,避免重复操作。
6. 常见问题与排查技巧实录
6.1 工具选择准确率上不去的排查路径
准确率上不去是最常见的问题。我整理了一套排查路径,按顺序检查:
| 排查项 | 检查方法 | 典型问题 |
|---|---|---|
| 工具描述质量 | 人工审阅所有 description | 描述模糊、缺少排除场景 |
| 检索召回率 | 统计正确工具是否在 Top-K 内 | K 太小或向量模型不匹配 |
| Prompt 结构 | 检查决策规则是否清晰 | 规则冲突、输出格式不明确 |
| 参数 schema | 检查 inputSchema 是否完整 | 缺少默认值、类型不明确 |
| 模型能力 | 换更强的模型对比 | 模型本身推理能力不足 |
| 数据分布 | 统计错误案例的输入特征 | 某些表达方式训练数据少 |
我遇到最多的问题是工具描述质量。有一次准确率卡在 80% 上不去,排查一圈发现是两个工具的 description 几乎一样,模型根本没法区分。改完描述当天准确率就到 91%。
6.2 延迟优化的几个实操手段
动态决策的延迟主要来自三块:向量检索、模型推理、工具执行。优化手段分别说:
向量检索通常几十毫秒,优化空间不大,但可以加缓存。相同或相似的 query 直接命中缓存,省掉检索。我用的是语义缓存,相似度超过 0.95 就复用结果,命中率约 30%。
模型推理是大头,几百毫秒到几秒。优化手段有三个:一是用更小的模型做初筛,大模型只处理复杂 case;二是流式输出,边生成边解析,不用等完整输出;三是 prompt 压缩,把工具描述精简到必要信息,减少 token 数。
工具执行的优化取决于工具本身,但策略层可以做的是并行化。多个独立工具调用并行执行,总延迟从累加变成取最大值。我有个场景串行执行 5 个工具要 15 秒,并行后 3.5 秒。
6.3 动态决策的安全边界
动态决策最大的风险是模型幻觉导致误调用。我的防护措施分三层:
第一层是工具分级。把工具分成只读、写入、危险三类。只读工具随便调,写入工具需要参数校验,危险工具(重启、删除、资金操作)必须二次确认。第二层是调用频率限制。同一用户单位时间内调用危险工具的次数有上限,超过直接拒绝。第三层是审计日志。所有工具调用记录完整的输入、决策理由、参数、结果,出问题可以回溯。
注意:危险工具的二次确认不要做成简单的“是/否”,要让用户看到具体的操作对象和影响范围。我见过因为确认信息太模糊,用户误点确认导致误操作的事故。
6.4 从硬编码迁移的实操步骤
如果你决定从硬编码迁移到动态决策,我建议按这个顺序来,不要一步到位:
- 先标准化工具定义。把所有工具的描述、参数 schema 补齐,这一步不管迁不迁移都该做。
- 加一层检索。先不改决策逻辑,只在硬编码前面加检索,观察召回效果。
- 灰度切换决策层。新老策略并行,按流量比例灰度,对比准确率和延迟。
- 建立回流机制。记录所有决策结果,定期分析错误案例,持续优化。
- 保留硬编码兜底。动态决策超时或失败时,降级到硬编码,保证核心功能可用。
我整个迁移过程花了六周,其中前两周都在做工具定义标准化。回头看,这两周是最值得的投入,后面所有优化都建立在这个基础上。
7. 一些踩坑之后的个人体会
工具选择策略这件事,我最大的体会是不要追求一步到位的完美方案。我一开始想直接上最先进的动态决策,结果工具定义一团糟,再好的策略也发挥不出来。后来退回去先把工具描述和 schema 做扎实,动态决策的效果自然就上来了。
另一个体会是延迟和准确率永远在打架。想要准确率高就得多检索、多推理,延迟就上去了;想要延迟低就得简化决策,准确率就下来。我的做法是分层:高频简单请求走快速路径,低频复杂请求走精细路径,用请求特征做路由。这个分层策略让我的 P95 延迟降了 60%,同时整体准确率没掉。
最后说一个容易被忽略的点:决策日志的价值远超你的想象。我一开始只是为了排查问题才记日志,后来发现这些日志是优化策略的金矿。哪些工具经常被混淆、哪些参数经常缺失、哪些用户表达模型处理不好,全在日志里。我现在的优化流程就是每周拉一次决策日志,分析 Top 错误模式,针对性优化。这个习惯让我的准确率在过去半年里从 85% 稳步爬到了 94%。
如果你也在做 Agent 工具选择,建议从第一天就把日志和回流机制建起来,后面会省很多事。