ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Agent工具选择策略:从硬编码到动态决策

Agent工具选择策略:从硬编码到动态决策 做Agent开发的朋友大概率都遇到过这种场景你辛辛苦苦给Agent接好了七八个工具功能逻辑都跑通了结果一上线就发现它老选错工具。明明该查数据库的时候去调了外部API明明该发邮件的时候跑去写文件。我前阵子在一个销售线索处理项目里就踩了一整周的坑最后终于把工具选择策略从一堆if-else重构成了动态决策链路。今天就把这套从硬编码到动态决策的完整思路拆开聊一聊包括为什么硬编码会失效、动态决策有哪几种主流方案、具体落地时要盯住哪些细节以及我实测中遇到的那些坑。这篇文章适合正在做Agent应用开发、或者用LangChain、Dify、CrewAI这类框架但总感觉工具调度不受控的朋友希望能帮你少走点弯路。1. 工具选择策略的本质与硬编码的局限1.1 硬编码的本质一切判断都写在代码里所谓硬编码Hardcoding的工具选择策略指的就是在Agent的代码逻辑中用固定的条件分支、规则映射或顺序调用来决定“调哪个工具”。最原始也最常见的方式就是直接在主函数里写一连串的if-elsedef decide_tool(user_input: str) - str: if 天气 in user_input: return weather_api elif 股票 in user_input or 股价 in user_input: return stock_api elif 翻译 in user_input or 英文 in user_input: return translate_api else: return default_fallback更精细一点的会做一个“意图识别工具映射”的查表逻辑TOOL_MAP { query_sales_data: 从数据库查询销售额数据, query_customer_info: 从CRM系统查询客户信息, send_email: 发送邮件给指定联系人, generate_report: 生成销售周报并保存为Markdown文件 } def route_by_intent(intent: str) - str: return TOOL_MAP.get(intent, unknown_tool)还有的干脆把工具调用写成了固定的编排顺序比如“先查数据库-再调分析服务-最后生成报告”一套流水线跑到底。这类实现在Agent工具数量少、场景单一的时候确实管用我也用这种方式快速验证过很多原型毕竟它思路直白、调试方便出了问题直接在代码里打断点就能定位。但硬编码最大的问题是你提前预设了所有可能的场景而真实的用户输入和业务状态是千变万化的。一旦工具数量增长、调用条件不再是单纯的“关键词命中”这套策略就开始失控。1.2 硬编码的三大致命问题说致命可能有点夸张但对我那个项目来说这三点确实都是卡脖子的。第一语义覆盖严重不足。用户的输入方式是无限的。用户不会只说“查一下股价”他可能说“帮我看看今天xx公司行情怎么样”“最近这基金要不要抛”表达里完全没有“股票”这两个字。如果只靠关键词和if-else匹配要么误判到别的工具要么直接走进兜底分支。我试过在if-else分支里加关键词表越加越长还是有漏网的而且分支之间的冲突也变多了。第二工具扩张后维护成本爆炸。假设你原本有5个工具两两之间可能有冲突场景你要写C(5,2)10种规避规则当工具涨到15个冲突组合变成C(15,2)105种这还没算三个工具同时被触发的复杂场景。靠人肉枚举完全没有可行性代码也变成了一团乱麻。第三与Agent的动态推理逻辑脱节。大模型本身就是高度动态的它会在多轮对话里自主理解用户需求可你的工具调度却是死板的。这就好比你请了个聪明的大脑却给它配了一只不听指挥的手。模型在思考链路里明明打算“先查数据再发邮件”落到代码层面却被硬编码规则强行拉到“只查数据”的分支。模型能力越强这种错位感就越明显。1.3 什么时候硬编码还是可选的我不是说硬编码一无是处有些场景下甚至最优。工具量极少1-3个且意图边界非常清晰比如一个只有“查天气”和“设提醒”的小助手。流程完全固定比如每日数据同步任务根本没有用户交互。作为动态决策的“兜底预案”在模型选择置信度极低时落到一个固定规则分支上。所以更准确的说法是硬编码适合做“最后的保险丝”不适合做“唯一的调度器”。当我们把硬编码定位为兜底层以后才能放心地往上搭建动态决策能力。2. 动态决策策略全景三类主流方案动态决策的目标很明确让工具选择这件事不再由开发者在编码期就拍死而是由Agent在运行时基于当前上下文决定。但“让Agent决定”不是非黑即白的我把它分成三个层次来聊。2.1 方案一模型完全自主决策LLM as Router这是当下最主流、也最容易上手的思路把工具列表通常以JSON Schema的形式和用户请求一起丢给大模型模型自己决定要调用哪个工具、参数填什么然后由代码侧负责执行。OpenAI的Function Calling、Claude的Tool Use都是这个机制。from openai import OpenAI client OpenAI() tools [ { type: function, function: { name: query_sales_data, description: 查询指定日期范围内的销售额数据支持按产品线、区域维度聚合, parameters: { type: object, properties: { start_date: {type: string, description: 开始日期格式YYYY-MM-DD}, end_date: {type: string, description: 结束日期格式YYYY-MM-DD}, product_line: {type: string, enum: [手机, 电脑, 配件]} }, required: [start_date, end_date] } } } ] resp client.chat.completions.create( modelgpt-4o, messages[{role: user, content: 帮我查一下上周手机的销售额}], toolstools, tool_choiceauto )这种方案的好处是省事大模型对自然语言的理解能力远远超过关键词规则语义匹配和参数抽取都能一步到位。但它也有明显的代价模型在每次决策时都要把全部工具描述塞进上下文Token开销会随工具数量线性上涨而且模型的选择存在不确定性偶发会臆造不存在的参数。2.2 方案二可编程路由语义检索 规则过滤模型自主决策像是在大海里捞针那我们就先帮它缩小捞针的范围。可编程路由的思路是先建立一套工具索引在每次请求到来时通过向量检索或关键词匹配的方式找出最相关的Top-K个候选工具只把这几个工具的Schema送给模型做最终裁决。def candidate_tools(user_input: str, tool_embeddings: dict, k: int 3) - list: from sentence_transformers import SentenceTransformer import numpy as np model SentenceTransformer(BAAI/bge-small-zh-v1.5) query_emb model.encode(user_input) scores { name: cosine_sim(query_emb, emb) for name, emb in tool_embeddings.items() } ranked sorted(scores.items(), keylambda x: x[1], reverseTrue) return [name for name, _ in ranked[:k]]在此基础上还能叠加规则层。比如当前用户是普通访客就把所有需要管理员权限的工具从候选列表里剔除当前是夜间时段就把“发送营销短信”这类需要人工复核的工具置后。规则层不负责决策它只负责“否决”和“排序”把决策空间进一步压缩。为什么推荐这个方案因为它兼顾了动态性和可控性。LLM的决策质量高度依赖候选范围你给它20个选项它容易挑花眼你给它3个高质量的选项它的准确率会明显提升。而且候选范围缩小以后Token成本也能控制在比较理想的水位。2.3 方案三混合决策召回-过滤-裁决-兜底把上面两种方案串成一条流水线就是我最终在自己的项目里采用的架构召回层用向量检索召回Top-K工具同时用关键词规则补充召回确保某些高频触发词直接命中。过滤层根据权限、环境状态、用户历史偏好做硬性过滤把不可用的工具剔除。裁决层把剩余的候选工具Schema交给LLM让它选择并填充参数。兜底层如果LLM没有成功触发工具或裁决结果被校验层否决则回退到硬编码规则或要求用户澄清。这四层各司其职召回层负责“看全”过滤层负责“别乱来”裁决层负责“读懂”兜底层负责“兜住”。我把它做成了一个简单表格来对比三种决策模式维度模型自主决策可编程路由混合决策响应速度中全量工具Schema进上下文快先检索后面试中检索有开销但裁决范围小Token 成本高工具越多成本越高低低-中可控性弱模型可能选错或乱填参数中规则只做过滤不负责理解强多层约束叠加实现复杂度低中高适用场景工具数量少、原型验证工具较多、意图相对明确生产环境、工具丰富、业务复杂我个人强烈建议如果项目只是一个Demo用方案一快速跑通就够了但如果你要上线到生产环境、要接几十个工具混合决策是抗住复杂度的底线。3. 核心实现细节从“会选”到“选对”工具选择策略不是把工具列表发给模型就完事了。很多人在这一步踩坑就是因为只搭了骨架没抠细节。这节我挑几个最影响成败的点展开说。3.1 工具描述工程模型靠“说明书”做选择工具选择质量的70%取决于工具描述写得怎么样。LLM不是人它对工具的理解完全来自于名称、描述、参数约束这三段文本。我见过很多人写工具描述极其敷衍比如{ name: query_sales, description: 查询销售数据, parameters: {} }这种描述模型根本分不清它和query_sales_by_region有什么区别。真正有用的工具描述应该具备这几个要素触发场景什么情况下该用、什么情况下不该用。比如“当用户询问历史销售业绩或销售额汇总时使用不要用于查询客户联系方式”。参数语义每个参数是什么含义、格式要求、边界值。比如日期的格式、是否包含当天。反面示例容易混淆的相似工具是什么。可以在描述里直接写“这是唯一能查询销售数据的工具不要使用xxx工具来查询销售数据”。输出说明工具执行后返回什么。模型在执行完成后还会读到返回值明确输出结构可以帮助它判断结果是否需要继续加工。我自己习惯的做法是写“工具卡”每个工具维护一个富描述文档再定期评估工具描述的命中率。是的工具描述也需要迭代把它当作一个小型知识库工程来做。3.2 参数校验与闭环反馈别把选择权完全交给模型模型选完工具后它会按JSON Schema输出一个工具调用请求。这时候你绝对不能“模型说什么就是什么”必须做一层参数校验和修正。我项目里的校验规则是类型校验字符串、数字、布尔类型必须匹配不符合的直接置空或抛错让模型重填。枚举校验参数值必须在预设枚举内否则选最接近的同义词。必填校验必填字段缺失时优先从对话上下文中抽取值抽不到就让模型补充。权限校验工具对当前用户是否可用不可用直接否决。工具执行完以后要把执行结果、错误信息、耗时都包装成tool message返回给模型让模型能看得到“刚才那次调用是否成功、结果长什么样”。这一步特别关键它是整个动态决策闭环里的反馈信号。没有这个反馈模型就只能闭着眼睛选下一次工具很容易陷入重复调用同一个失败工具的循环。3.3 上下文感知动态决策的“动态”体现在哪里动态决策的“动态”不只是每次请求动态选工具还包括在决策过程中感知足够的上下文信息。我总结了三层上下文对话上下文用户前几轮说过什么、有没有提到过某个实体、上一轮工具返回的结果是什么。这些信息决定了当前工具选择的连续性。比如用户第一轮说“我想了解上季度销售情况”第二轮说“详细点”这时候Agent应该关联到销售数据工具而不是去随便选别的工具。状态上下文当前系统处于什么状态、有什么限制条件。比如用户权限是只读那所有写操作工具发送邮件、写入数据库都要被过滤掉比如某个外部API当前处于降级状态那就应该优先切换到备用工具。偏好上下文用户历史使用过哪些工具、对哪种报告格式有偏好。这个可以做成一个轻量的用户画像在过滤层动态调整工具排序权重。三层上下文加在一起工具选择就从一个“单点分类问题”变成了“序列决策问题”。这也更接近Agent的本质工具选择不是一次性的而是随着推理过程逐步展开的。4. 实操过程在真实项目里搭一条工具选择链路前面讲的都是原理和策略这节我用自己的销售线索周报Agent项目作为案例完整展示工具选择链路的搭建过程。4.1 场景定义与工具清单这个Agent要解决的问题是销售主管每天早上发送一句语音或文字指令Agent自动生成昨日销售周报并通过邮件发送给团队。我最初列了五个工具工具名功能权限query_sales_daily查询日维度销售数据金额、单量、转化率可读query_sales_trend查询最近N日销售趋势可读query_team_performance查询团队成员个人业绩排名可读generate_markdown_report把结构化数据渲染成Markdown报告可写send_email发送邮件给指定收件人可写这五个工具之间有明显的语义关联但也有区分度。我的目标是无论用户怎么表达Agent都能在“查询数据-生成报告-发送邮件”这条主链路上正确选到工具。4.2 第一步硬编码版本对照组我先花半天时间写了硬编码版本做基线def handle_report_request(text): intent classify_intent(text) if intent gen_report: sales_data query_sales_daily(datetoday) trend_data query_sales_trend(days7) report generate_markdown_report(sales_data, trend_data) send_email(report, tosales_teamexample.com) return report_sent elif ...这个版本在测试集上的成功率只有61%。我分析了那39%的失败案例发现集中在下面几类情况用户说“数据看起来不太对帮我复查一下”Agent无法理解应该重新走查询链路用户说“只看手机线就行了”Agent无法动态调整参数用户说“顺便也发给李经理”Agent直接发送失败了。这些案例清楚地告诉我硬编码死于表达多样性。于是我开始重构。4.3 第二步检索召回层我给五个工具写了一套详细的描述文档核心是触发场景和参数语义。然后为每个工具生成Embedding向量用一个轻量级的向量库Chroma存起来。from chromadb import Client client Client() collection client.create_collection(sales_tools) tool_docs [ (query_sales_daily, 查询指定日期的销售额、订单量、转化率等核心指标按日期精确查询主要用于回答昨天的业绩怎么样昨天卖了多少等问题), (query_sales_trend, 查询连续多日销售趋势数据用于观察走势、判断增长或下滑主要用于回答最近走势如何这周比上周怎么样等问题), # ... ] for name, doc in tool_docs: collection.add(documents[doc], ids[name])召回层在每次请求到来时对用户输入做同样编码后计算向量相似度取Top-3作为候选。4.4 第三步过滤层与权限控制过滤层的核心是不信任模型把硬性限制前置。我这个项目里主要是两类过滤权限过滤只有被授权的管理员才能触发send_email普通场景下直接把它从候选列表里删掉。语义排他query_sales_daily和query_sales_trend在多数场景下只需要一个如果检索结果同时命中这俩我会根据用户输入是否包含“趋势”“走势”“最近”等词或者让LLM在两者间做一次快速二选一。过滤层不需要做很重的逻辑但它能极大概率避免“生成报告前先发了邮件”这类灾难性操作。4.5 第四步LLM裁决与参数填充候选工具只剩2-3个我就可以放心地把它们塞给LLM做最终决策。def final_select(candidate_names: list[str], user_input: str): candidate_schemas [ TOOL_SCHEMAS[name] for name in candidate_names ] resp client.chat.completions.create( modelgpt-4o-mini, messages[{role: user, content: user_input}], toolscandidate_schemas, tool_choicerequired ) tool_call resp.choices[0].message.tool_calls[0] return tool_call.function.name, json.loads(tool_call.function.arguments)注意我用了tool_choicerequired这很重要。它强制模型必须选一个工具避免出现模型“聊闲天”不调用工具的情况。在候选集已经很小且都有意义的情况下强制选择是合理的。4.6 第五步可观测性与成本监控动态决策最怕“黑盒”。所以我在链路里埋了详细的日志结构{ user_input: 帮我查一下昨日的销售情况并生成报告, candidates: [query_sales_daily, generate_markdown_report, send_email], final_selection: query_sales_daily, params: {date: 2025-06-17}, latency_ms: 421, tokens_used: 438, call_chain: [query_sales_daily, generate_markdown_report, send_email] }每次请求的决策路径、候选集、最终选择、Token开销、链路时延都记下来。为什么要做这件事因为动态决策的调优不是靠感觉的你得能回答“今天为什么这个场景选错了”和“哪类请求的Token消耗异常高”这些具体问题。最后我还验证了一下成本和延迟。召回阶段用本地Embedding模型耗时只有几十毫秒LLM裁决阶段用gpt-4o-mini处理2-3个候选Schema每次决策大概消耗400-500 Token成本很低。路由链路加上工具执行总体延迟比纯硬编码多了大概150ms完全可接受。重构完成之后这个Agent在同样测试集上的成功率从61%提升到了94%。剩下的6%主要是模型把“销售趋势”和“销售数据”搞混或者参数日期格式填错。这些错误已经进入了我的评估清单是下一轮迭代的方向。5. 常见问题与排查技巧实录动态决策上线以后真正的问题是层出不穷的。我挑了四个遇到最多、也最典型的整理成速查表每个都说清楚表象、根因和解决路径。5.1 工具选择困惑模型在高度相似的描述中乱选现象候选工具里有两个都涉及销售数据模型经常选错比如该查趋势的时候查了单日数据。根因工具描述区分度太低描述里都出现了“销售数据”“查询”这样的高频词模型也很迷茫。解法互斥描述在A的描述里写“该工具仅用于单日精确查询不适用多日趋势分析”在B的描述里写“该工具仅用于多日趋势分析不适用单日汇总”明确划清边界。检索重排序如果两个工具都有召回在过滤层加一条规则看用户输入是否包含“趋势”“最近几天”“走势”等信号词有就保留趋势工具。模型再确认在裁决层让模型输出一句“选择该工具的理由”把这个理由也记进日志便于事后分析。5.2 循环调用Agent反复调用同一个失败工具现象工具返回错误后模型不死心反复调用同一个工具浪费Token且拉长延迟。根因工具执行的错误信息没有有效传回给模型或者错误信息太隐晦模型没理解失败原因。解法把错误信息格式化清楚直接告诉模型“这次调用失败原因xxx不建议再次调用该工具”。给模型注入“已尝试过”的状态记忆在后续决策时把历史工具执行结果作为上下文的一部分。代码层设置最大连续调用次数比如同一工具连续失败3次就中断链路转人工兜底。5.3 参数幻觉模型编造不存在的枚举值现象参数Schema里明明枚举只有“手机”“电脑”“配件”模型偏偏填了“耳机”导致接口报错。根因模型对枚举约束遵循能力不稳尤其当工具描述里出现过别的实体词时容易混淆。解法枚举校验属于必须做的硬校验不通过就回抛给模型重新生成参数。在参数描述里把每个枚举值都给出中文释义例如“手机包括智能手机及配件类终端设备”。可在Schema里对枚举字段增加更严格的描述“必须且只能从给定列表中选择不要使用任何未列出的值”。5.4 评估问题怎么衡量工具选择策略好不好动态决策做得好不好不能靠感觉。我后来给项目做了一个轻量级的评估集大概60条请求覆盖正常指令、模糊指令、相似工具歧义指令、越权指令四类。EVAL_CASES [ {input: 帮我查下昨天的销售额, expect_tool: query_sales_daily}, {input: 看看最近一周的销售发展趋势, expect_tool: query_sales_trend}, {input: 把销售报告发给陈经理, expect_tool: send_email, expect_reject: False}, {input: 帮我删掉所有历史订单数据, expect_tool: None, expect_reject: True}, ]每轮跑完统计工具选择准确率和误调用率。工具选择准确率选择正确且参数正确的请求数/总请求数误调用率不该调用工具却触发了工具的请求数/总请求数。这两个指标一个看召回质量一个看越权风险都很关键。评估集要在每次调整描述或路由规则后重跑不然你很难判断这次的优化是变好了还是变坏了。6. 工具选择策略与Agent系统演进的联动聊到这儿得把视野拉高一点。工具选择策略不是孤立的模块它和Agent系统的其他能力Skills、Harness、多Agent、记忆紧密耦合。尤其是“Agent Skills”这个概念火了以后工具选择策略又有了新玩法。6.1 从“选工具”到“选技能”的层级化单个工具是最细粒度的能力单元但有些任务天然需要组合多个工具完成这就是Skills技能存在的意义。动态决策的第一层是让Agent先选“用哪个技能”选定了技能以后技能内部再动态编排具体调用哪些工具。这相当于把工具选择从“一层路由”升级成“两级路由”。比如我的Agent里“生成销售周报”就是一个技能它内部包含“查询单日数据”“查询趋势数据”“渲染为Markdown”“发送邮件”四个工具的编排规则。Agent只需要决定“用户要我生成周报吗”至于生成周报要不要先查趋势、要不要附带环比由技能内部根据参数决定。这种分层让顶层决策信号变得更粗粒度也更容易精准决策。6.2 Harness与工具选择的边界很多人分不清Harness和Agent的区别。简单来说Harness是那个“骨架”或“容器”它负责Agent的推理循环、工具执行的I/O、消息历史管理等基础设施而工具选择策略是Harness内部最核心的“调度决策”机制。如果Harness设计得好工具选择模块就是可插拔的你可以随时把动态决策策略从“模型全选”换成“检索模型”而不影响Agent的其他部分。我在重构时就把路由决策做成了一个独立的中间件模块对外只暴露decide(user_input, context, tools)接口。这样每次换策略改一个模块就够了。6.3 记忆与多Agent带来的动态决策复杂度动态决策的搜索空间会随着系统复杂度指数上升。多Agent场景下每个子Agent都有自己的工具域父Agent需要先路由到正确的子Agent再由子Agent选择自己的工具。这时候“先选Agent再选工具”的决策链路需要额外设计。我目前的做法是在父Agent规划阶段就把子Agent及其工具域做成候选集让父Agent一次性决策“目标子Agent该子Agent域内的工具”。这比“先找子Agent再进子Agent内部找工具”少一次往返、少一轮LLM调用。记忆方面用户的长期偏好可以被编码成工具排序的偏置权重比如某个用户总是要求报告附带环比数据那“生成报告”技能里就优先选择含环比参数的调用路径。这套机制跑起来以后Agent的系统能力边界会明显往上走一层。它的工具调用不再是一个个孤立的单点请求而是有策略、有层次、有反馈的连续决策过程更像是一个人拿着工具箱在干活而不是一台自动售货机根据按钮掉商品。我最后想说的是动态决策不是万能的过度动态同样会翻车。工具选择策略的关键不是“放弃所有代码逻辑、全凭模型发挥”而是找到“确定性规则”和“模型推理”之间的平衡点。高置信度的常规路径用规则稳准快地处理长尾模糊场景才交给模型发挥这种混合思路在我所有实战项目里都跑得比纯规则或纯模型要好。等你把描述工程、检索召回、过滤兜底这套基本功练扎实了再回头看工具选择策略就会从“填代码”变成“做设计”那个主动权才是真正握在自己手里的。
返回列表