ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

从硬编码到动态决策:Agent工具选择策略的演进与落地

从硬编码到动态决策:Agent工具选择策略的演进与落地

上周有个朋友过来找我聊,说他负责的 Agent 项目要从 20 多个工具里做选择,代码里全是 if-else,每次加新需求都得把整棵规则树翻一遍,改完还要担心把旧分支打坏。他说想换成动态决策,但又怕结果不可控。这个纠结我太熟悉了,我自己做 Agent 的早期也是这么过来的,从硬编码开始,再一步一步过渡到动态选择。

这里要说的工具选择策略,是 Agent 开发里最基础、也最容易被低估的一个环节:用户一句话进来,Agent 到底该调哪个工具、传什么参数、按什么顺序执行。选错了,后面所有动作全白费。市面上聊 Agent 框架、多 Agent 编排的文章已经很多了,但专门把"工具选择"拎出来讲透的反而少见。这篇博文我就从硬编码讲起,分析它为什么成了大多数人的默认选择,再讲动态决策背后的原理、落地实现、性能优化和踩坑记录,适合正在做 Agent 产品、想把手头工具编排方式优化的开发者参考。

1. 先聊聊硬编码:为什么大家一开始都这么干

1.1 硬编码工具选择的四种常见形态

在 Agent 项目里,硬编码不是一上来就写得很丑,它其实有几种递进的形态。

第一种是最朴素的 if-else / switch 分支。根据用户输入里的关键词或者意图标记,直接路由到固定工具。比如检测到"查天气"就去调天气 API,检测到"订闹钟"就去调闹钟工具。逻辑直白,新手上路基本都先这么写。

第二种是正则 / 规则映射。比 if-else 更进一步,先抽取出实体,再把命中的规则映射到工具。例如用户说"我要订明天早上 8 点去北京的机票",先用正则抓日期、时间、目的地,再按模板拼出订票工具的调用参数。

第三种是状态机路由。把 Agent 会话切成"待确认身份 → 查询订单 → 处理售后"这样的状态,每个状态下面只开放一个或几个工具。这种形态适合流程固定的场景,本质上还是人为规定好了路该怎么走。

第四种是干脆在 prompt 里写死调用剧本,要求模型"先调 A 工具,如果结果为空再调 B 工具"。看起来是模型在决策,但调用顺序和条件都已经被写死了,改起来照样要改 prompt,还是硬编码的思路。

这四种形态我早期都写过,最伤的其实是第一类和第四类。第一类是纯维护灾难,每加一个分支都要担心和其他分支打架;第四类是"假动态",看起来模型在做决定,实际上一点容错和泛化能力都没有。

1.2 硬编码的开局优势与后期代价

硬编码之所以能成为几乎所有 Agent 项目的默认起点,是因为开局优势太明显了。

  • 开发快,一个分支对应一个工具,几小时就能跑通主流程。
  • 完全可解释,出问题能立刻定位是哪个分支、哪个条件出了问题。
  • 没有额外推理延迟,不需要等模型做二次决策。
  • 初始效果稳定,工具集合小的时候,规则很容易覆盖大多数用户说法。

但工具数量一旦超过某个阈值,代价就开始快速放大。先是最直观的维护成本:每加一个新工具,都得把旧分支全看一遍,怕命中冲突,加完还要补测试用例。其次是覆盖不全的痛点:用户说法千变万化,关键词规则永远追不齐,同一个"查订单"能说出十几种变体,规则漏一个就挂一个场景。再就是上下文浪费:如果你把整套路由规则都写进 prompt,每次请求都背着厚厚一叠指令,token 费用上去了,首字延迟也上去了。

我当时项目里有 13 个工具时,硬编码还能扛得住。等工具涨到 20 多个,并且出现了"同一个意图可能命中多个工具"的情况,规则树就彻底失控了,漏匹配、误匹配轮番出现,修完一个又冒出来两个。

硬编码和动态决策之间不是"谁绝对更好"的关系。如果你手里只有四五个工具,流程又高度确定,硬编码就是最优解,又快又稳。真正逼着你升级的是规模,而不是某个单点问题。

1.3 什么时候硬编码依然是正确答案

虽然我把硬编码吐槽了一顿,但如果你满足下面任一条件,我建议你继续用硬编码,别跟风上动态决策。

  • 工具数量少,五六个以内,规则覆盖得了。
  • 流程是确定性的,每个分支都有明确触发条件,比如表单校验、数据回填。
  • 对延迟和成本极度敏感,希望尽量不走模型推理。
  • 场景完全封闭,用户输入变化不大,比如内部系统的固定指令。

还有一个经常被人忽略的点:硬编码最适合当"骨架"用。一个复杂的 Agent 主流程,主干可以硬编码,比如必须先登录、再填参数、最后提交;但主干上的"这一步该选哪个工具",可以交给动态决策。硬编码负责确定性,动态决策负责不确定性,两者不是二选一,而是可以混合的。

这套"规则骨架 + 动态决策"的形态,是我现在做 Agent 方案时的默认推荐,后面第 3 节我还会结合实现展开讲。

2. 从硬编码到动态决策:核心痛点与设计思路

2.1 动态决策的本质是什么

工具选择本质上是路由问题,动态决策说白了,就是把路由权从程序员手里移交到大模型手里。模型读你的工具描述,结合用户当前意图,输出"该用哪个工具、参数是什么"。这个决策过程不再需要你维护规则树,取而代之的是你维护工具描述的质量。

我刚开始很不适应让权,总觉得不可控,模型选的工具凭什么信得过。后来想明白一件事:动态决策不是放弃控制,而是把控制的粒度上移。硬编码控制的是"每一种输入具体怎么走",动态决策控制的是"工具怎么描述、候选集怎么给、结果怎么校验"。控制点变了,但控制一直都在。

而且动态决策天然适配大模型的泛化能力,同一套机制能覆盖你完全没预写过的用户说法。比如你先写了"查订单状态"的工具,用户某天说"我的快递怎么还没到",模型依然有可能通过语义相关性把它路由到正确的工具上,这在硬编码时代几乎不可能。

2.2 动态决策的三个关键维度

做动态决策的时候,我一般会盯紧三个维度。

相关性是第一位的。模型选工具,靠的是工具描述和用户意图之间的语义重合度。描述写得烂,选择必然烂,这个没有任何捷径,只能靠描述工程去补。

复杂度排在第二。候选工具越多,模型的选择准确率越低。这不是玄学,而是有明确趋势的:工具在 10 个以内时准确率比较高,20 个以上明显下滑,50 个以上就必须要做预筛。你可以理解成模型在长列表尾部容易注意力稀释,目标工具被埋在了几十个工具描述里。

可靠性排第三。同一个用户意图,多轮对话、多人重复触发时,要尽量保持选择结果一致,减少随机抖动。平时 demo 看不出来,一旦你接上自动化流水线,抖动问题会非常折磨人。

这三个维度是我判断一套动态决策方案是否合格的硬指标,后续所有调优动作基本都是围绕它们展开的。

2.3 动态决策快照:让每次选择都可复现

先介绍一个我在项目里非常受益的概念:动态决策快照。听起来复杂,其实就是把"选择过程"做成一条条结构化记录,包含当前意图、候选工具、评分依据、参数草案和最终选择结果。这样每次工具调用都能追溯到一个决策现场,调试和审计都有据可查。

实操上,我会把一次选择写成类似下面这样的快照:

{ "intent": "query_order_status", "candidates": ["search_orders", "query_logistics", "refund_process"], "scores": [0.82, 0.51, 0.12], "selected": "search_orders", "params": {"order_no": "20240101"}, "model": "gpt-4o", "latency_ms": 320 }

快照最大的价值不在当天复盘,而是能回流成测试集。把误选的样本沉淀下来,下次迭代工具描述的时候拿来当回归用例,改完就知道有没有修好,有没有打坏别人。我现在项目里任何工具选择的 bug,都不是靠口头争论修的,而是先补一条快照用例,再改描述、跑回归、看结果。

3. 动态工具选择的落地实现:从注册到执行的完整链路

3.1 工具注册表与 Schema 设计

动态决策的第一个基础设施是工具注册表。你不该把工具列表散落在代码各处,而应该在一个地方集中注册,并且给每个工具提供结构化元数据。这里我按生产环境的常见实践给你一个可复用模板:

tools = [ { "name": "search_orders", "description": "按订单号或客户手机号查询订单状态,适用于用户询问订单进展、预计送达时间的场景。不要用于退款和投诉,退款请使用 refund_process。", "parameters": { "type": "object", "properties": { "order_no": { "type": "string", "description": "订单号,一般以字母开头,如 A20240101" }, "phone": { "type": "string", "description": "客户手机号,非必填,用于辅助定位订单" } }, "required": ["order_no"] } }, { "name": "refund_process", "description": "发起退款申请并跟踪退款进度,适用于用户明确提出退款、退货、不想买了等诉求。订单状态查询请改用 search_orders。", "parameters": { "type": "object", "properties": { "order_no": {"type": "string", "description": "订单号"}, "reason": {"type": "string", "description": "退款原因"} }, "required": ["order_no", "reason"] } } ]

别小看这个 schema,很多人在 description 上偷懒,写一两句"查询订单"就完事。我的经验是,工具描述必须回答三个问题:这个工具负责什么;什么场景下使用;什么场景下不要用。尤其"不要用"这一点很关键,能显著减少相似工具之间的误选,相当于你自己给模型画好了决策边界。

参数部分也要认真写类型和说明,否则模型只能靠猜。required 一定要严格,能必填就必填,否则模型经常漏参数,后面执行环节就开始报错。

3.2 选择的实现:三种主流策略

动态选择不是只有一种实现,完全取决于你的规模和成本。我按复杂度从低到高排个序,你对照自己的情况选。

策略一:单轮 LLM 函数调用。直接用 function calling 或 tool calling 接口,把工具列表塞给模型,让模型输出结构化的工具调用。适合工具在 10 到 20 个以内、对成本不敏感的场景。优点是实现最快,缺点是每次选择都要消耗不少 token,而且候选一多模型容易犯迷糊。

策略二:分类器预筛 + LLM 精排。先用轻量分类器从几十个工具里筛出 Top 5 到 8 个候选,再让模型在这些候选里做最终选择。分类器可以是规则、Embedding 相似度,也可以是小型微调模型。LLM 的决策负担小很多,准确率也更稳,这是我现在最常推荐的方案。

策略三:决策缓存加快照。命中高频意图时直接复用上次的选择快照,完全不走模型。缓存可以维护一份意图到工具选择的映射,对于高频场景,这一层能挡掉大半的模型调用。

把策略二和策略三结合起来的伪代码大概是这样的:

def select_tool(user_input, tools, cache): # 1. 查缓存:高频意图直接命中快照,不走模型 hit = cache.lookup(user_input) if hit: return hit # 2. 预筛:用 embedding 相似度取 Top N 候选 candidates = embedding_search(user_input, tools, top_n=6) # 3. LLM 精排:在候选集里生成最终选择与参数 chosen = llm_tool_choose(user_input, candidates) # 4. 写入快照:置信度够高才缓存,避免污染 if chosen and chosen.confidence > 0.8: cache.store(user_input, chosen) return chosen

这套流程我实测下来,模型调用成本能降一半以上,而且选择稳定性明显提升。核心思路就是"能不麻烦模型就不麻烦模型",把模型的价值集中在真正需要语义理解的决策环节。

3.3 从选择到执行:参数填充、校验与回退

选择只是起点,真正跑通业务需要把"选了哪个工具"变成"工具被正确调用"。这一段是工程化最容易翻车的地方,但很少被聊到。

首先是参数填充环节。模型给出的参数草案不能直接透传,必须先做一轮校验:必填项是否齐全、格式是否符合 schema、枚举值是否合法。校验失败的,可以让模型针对缺失字段补充,也可以直接返回提示让用户重新描述。我项目里有个搜索工具,模型经常漏传日期范围,加了参数校验之后,这类问题基本绝迹。

然后是执行环节的兜底。工具本身可能超时、报错、返回空结果,这些不能直接抛给用户。通常做法是:工具失败后先回退到同义工具,比如主搜索失败就试备份搜索服务;或者把失败原因整理成反馈,带回给模型,让模型换工具或换参数重试一次。

最后记得加一条保险丝。如果连续两次失败,就停止自动重试,转人工或给出兜底话术。否则模型容易陷入循环调用,疯狂消耗资源却没有业务产出。这是我见过的最常见的生产事故来源之一。

4. 动态选择策略的进阶调优:描述、评分与缓存

4.1 工具描述直接决定命中率

工具描述是最容易被低估的杠杆。同样一套工具,描述从"按订单号查订单状态"改成"用于处理用户查询订单进展、物流、签收状态的场景,也可按手机号辅助定位,退款请走 refund_process",命中率能肉眼可见地提升。

我在调描述时有一个固定套路:先收集误选样本,看模型为什么选错。比如发现它把订单查询和退款混淆,就在退款工具的描述里明确加一句"订单状态查询请改用 search_orders"。这种边界化描述比单纯罗列功能点有效得多,因为它直接消解了相似工具之间的语义重叠。

顺便说一句,现在很多 Agent 框架强调 harness 和 agent 的分离,工具描述这种核心资产其实是放在 harness 层的。选型的时候别只看框架功能花哨不花哨,要看它对工具注册、描述版本管理、测试回流的支持好不好,这些才是长期维护的关键。

4.2 参数约束与防抖:让决策结果可复现

在自动化链路里,选择抖动是最折磨人的问题之一。同一句话,可能上午选 A 工具,下午选 B 工具,结果还不一样。抖动的来源主要是三个:模型采样温度、候选工具顺序、描述模糊。

解决采样抖动,最简单的是调低 temperature 到 0 或 0.1,如果接口支持 seed 参数就固定 seed。候选顺序影响则可以这样处理:在提交给模型前,对候选排序做稳定化,比如按工具名或者历史成功率排序,而不是直接用 embedding 打分的原始顺序,避免因为微小的相似度波动导致顺序变化。描述模糊引起的抖动没有捷径,只能靠边界化描述去消除。

我特别建议给动态选择建一套回归测试集。挑 200 条真实用户输入,每条标注好期望工具,每次改完描述就全量跑一遍,看准确率是涨还是跌。没有这套回归,你改描述就是凭感觉,很容易修好一个工具又打坏另外两个。

4.3 并发与性能:动态选择怎么扛住流量

动态选择的性能瓶颈很明显,在 LLM 调用,而不是工具本身。要扛并发,不能指望每个请求都跑一轮模型推理。

第一层优化是缓存,高频意图直接命中快照,挡住大部分请求。第二层是预筛前置,Embedding 检索本身可以做成异步、并发友好,让请求先在轻量层被分流。第三层是对 LLM 调用做池化和限流,限制并发数、设置合理超时,避免被单次慢响应拖垮整条链路。

我实测过一个客服场景,全量走模型时平均选择延迟约 600ms;加上缓存和预筛后,平均降到 150ms 以内,并且约 70% 的请求完全不再消耗模型 token。动态选择的性能优化本质是想办法减少对模型推理的依赖,而不是盲目堆高并发上限。

5. 常见问题与排查实录

5.1 工具一多就选不准怎么办

先把工具数量降下来,核心思路是漏斗:Embedding 预筛 + LLM 精排,而不是让模型在 50 个工具里大海捞针。同时检查工具命名是否有冲突,名字和描述里不要出现 get_data、do_something 这类毫无业务含义的通用词,尽量改成带场景语义的名字,比如 search_orders_by_customer。

5.2 相似工具之间误选,怎么隔离

把相似工具的边界写死在描述里。比如"查询订单"和"查询物流"这两个工具,在各自的 description 里互相声明边界,明确写"物流查询请使用 query_logistics,本工具不处理物流信息"。模型在候选阶段看到这样的边界声明,误选率会显著下降。这个方法成本极低,效果却是立竿见影的。

5.3 把所有工具描述塞进 prompt,上下文爆炸怎么办

把工具全集变成候选集,先检索再拼接。工具注册表里可以存完整描述,但每个请求真正进入 prompt 的只保留 Top 5 到 8 个候选工具的描述。如果模型上下文本来就紧张,还可以进一步压缩描述,只保留名称、用途和必要参数,把详细说明留在注册表里。

5.4 动态选择会不会带来安全与权限问题

会,而且必须严肃对待。模型选出来的工具和参数,不等于业务允许的操作。实践中我会做三层防线:工具注册表里标注每个工具的最小权限;执行前校验参数的取值白名单;对高危工具,比如发消息、删数据、转账,强制人工确认或二次校验。动态决策是让模型更聪明,不是让模型拥有权限。

还有一点,凡涉及用户敏感数据,工具选择快照的日志要做脱敏,手机号、订单号这些字段不能在日志里明文存储,否则排查问题的时候容易惹出合规麻烦。

6. 一点真实的项目体会与迁移建议

说实话,工具选择从硬编码到动态决策,不是一次英雄主义式的彻底重写,而是一个渐进迁移的过程。我现在的默认建议是:小工具集直接硬编码;中等工具集用"规则骨架 + 动态决策"的混合架构;大工具集再上完整的检索、评分、缓存体系。

如果你要动手改造,我建议先从最容易变化的那批工具开始,接上动态选择,配上快照、缓存和回归测试集,跑稳两个星期再扩大范围。别一上来就把所有工具切换成全动态,出了错都找不到是从哪一步开始崩的。先把动态选择的路跑通,再逐步收编硬编码只管得住的那部分分支。

最后说一个我反复踩过的坑:优先把工具描述写好,比什么都强。模型选不准,九成情况下不是模型太笨,而是你的工具描述给它留了太多模糊地带。描述边界清晰,候选集给得合理,动态选择的效果就会远超你最初的预期。

返回列表