开头想先聊个现象:我把一个 Agent 从"写好规则让它跑"改成"让它自己选模型、自己选工具"的时候,身边好几个朋友都劝我冷静,说模型自己选不靠谱,工具自己选更是不受控。结果我硬着头皮把五个自制件做完之后,最大的感受反而是——问题从来不在"让 Agent 选",而在于你把选择的规则、描述和反馈做得够不够清楚。标题里这五个自制件,是模型选择器、工具注册表、工具决策器、参数注入器和结果评估器。这篇博文会把每一件的设计取舍、实现思路和踩过的坑摊开讲。如果你是正在搭 Agent 系统、卡在"模型乱选""工具乱调"这个阶段的人,这篇应该能帮你省不少弯路。
1. 为什么非要让 Agent 自己选:从配置地狱到运行期自决
1.1 写死规则的路,走到后面全是组合爆炸
最早一版我的实现很简单:在入口处写一个路由函数,判断用户输入里含不含"天气""日历""发送邮件"等关键词,然后往对应的工具函数里扔。单看 demo 确实没问题,可一旦任务变成"帮我把这周的会议纪要按照公司模板转成周报,再发给项目组成员",关键词规则就失灵了。这个需求里同时出现了"会议纪要"、"模板"、"周报"、"发送"四个信息,每个信息对应一个或者多个工具,规则耦合很快就变成一层又一层的 if-else。
你会发现,实际问题不是"要不要加规则",而是规则覆盖不住这些组合。用户的一句话里可能同时包含查询、计算、格式化、发送四个动作,动作之间还有顺序关系。用写死规则维护这种组合,配置量是爆炸的,而且每个新工具上线都得回去改路由逻辑,改完还要担心影响旧场景。
1.2 我的答案:把选择权交给运行期,但外面加约束框
既然规则维护不起来,那就换方向:让 Agent 在运行期自己看任务、自己选。我做的第一个关键决策是"不把选择权完全交给模型裸奔,而是给模型提供一份清晰的候选清单和选择依据"。模型不直接调工具,它只负责做选择;真正的执行链路仍然由代码控制。
这个思路落到架构上就成了五个件:模型选择器负责决定这个任务该由哪个模型来理解;工具注册表维护所有可用工具的结构化描述;工具决策器根据注册表决定调哪个工具、按什么顺序调;参数注入器解决"模型给的是自然语言,工具要的是强类型参数"这个断层;最后结果评估器检查工具执行结果是不是真的解决了用户的问题。五件连起来就是一条完整的自主闭环。
1.3 明确优先级:精度第一位,成本第二位,延迟第三位
设计任何一件之前,都得先把目标定清楚。我在这次项目里把优先级定为:结果精度 > 调用成本 > 响应延迟。原因很朴素——一个工具选错了,后续所有步骤都建立在错误基础上,返工成本远高于多付几次模型调用的钱。这个优先级直接影响后面每一件的实现方式,比如模型选择器里会更倾向给复杂任务分配更强的模型,而不是最便宜的模型;结果评估器宁可多花一次调用来确认,也不接受一个"看起来成功其实全错"的返回。
2. 模型选择器:让 Agent 判断"该用谁",而不是永远用最贵的
2.1 为什么不能一刀切用旗舰模型
搭 Agent 早期最贵的教训,就是让所有任务都走同一个最强模型。当时想的是"反正模型强一点,工具调用准确率就高一点",结果一个月下来的账单很难看,而且部分简单任务用旗舰模型并没有带来精度提升。举个例子,像"把这段文本里的日期提取出来"这种简单结构化任务,用轻量模型和旗舰模型的准确率差距不到两个百分点,成本却差了近十倍。
所以就做了第一个自制件:模型选择器。它的输入是任务分析结果(任务类型、输入长度、是否需要视觉、是否需要长上下文、涉及工具复杂度),输出是选用哪个候选模型。它是第一个被调用件,但逻辑不应该复杂——前面先用硬性规则把肯定不合适的模型过滤掉,比如任务输入超过 32k 上下文、必须支持图像输入、需要特定模态输出,这类场景直接锁定候选模型,不做智能判断。
2.2 让模型选模型的正确姿势:先建模型档案,再做匹配
规则过滤完之后,剩下的候选集通常有两到三个模型,这时候让一个轻量的决策模型做匹配就够了。我给每个候选模型都建了结构化的"档案",字段大致是这样:
{ "model_id": "light_model", "capabilities": ["text_generation", "function_calling", "extraction"], "max_context": 32000, "cost_per_1k_tokens": 0.002, "strengths": ["结构化抽取", "工具调用参数生成"], "weaknesses": ["复杂推理", "长文总结"], "recommended_for": ["信息提取", "参数填充", "短文本分类"], "not_recommended_for": ["多步推理", "长文档综述"] }关键就在recommended_for和not_recommended_for这两个字段。让模型选择模型的时候,它不是靠"谁更强"来选,而是靠"谁更适合当前任务"来选。决策提示词里明确告诉它:你是调度员,不是使用者,你的目标是性价比和成功率最高,而不是选择最强的模型。实测下来,这个引导非常有效,模型自选时消费金额能降一半左右,错误率没有明显上升。
2.3 踩坑记录:模型自评置信度完全不可信
第一次把模型选择器放上线,我犯过一个错误——让候选模型先自评"我这个模型能不能处理这个任务",给出 0 到 1 的置信度,然后选置信度最高的那个。这个设计看着合理,实际跑起来简直是灾难。几乎所有模型都会给自己打 0.8 以上的高分,即使任务根本不是它的强项。后来想明白了,模型没有"自我认知",它的自评本质上还是语言生成,它会倾向于生成"我可以"的文本,这不是它骗你,是训练目标决定的。
这个坑的解法是放弃自评,改成"让一个小模型给任务打标签,标签直接映射到推荐模型"。比如任务标签是"extraction",就推荐轻量模型;标签是"multi_step_reasoning",推荐复杂推理模型。这本质上是把模型选择从"感性的自我评价"改成了"基于文本分类的确定性映射",效果稳定得多。
2.4 成本护栏:就算模型想选贵的,也要有兜底
还有一个细节容易被忽略:模型选择器本身也是模型调用,它可能受输入影响产生随机性。为了不让随机性影响预算,我加了一个简单的成本护栏——如果本次任务估算 token 数超过预设阈值,强制降级到成本更低的候选模型,并记录一条降级日志。这里有个经验:护栏条件宁可写宽松一点,也不要写死,因为有些任务确实是需要高成本的,强行降级会导致结果不可用。护栏的作用是"挡住极端情况",不是"限制正常路径"。
3. 工具注册表:把工具描述成"人话",Agent 才不会选错
3.1 工具描述是给模型看的"产品说明书"
第二个自制件是工具注册表。一开始我觉得工具列表不就是把函数名传进 prompt 里嘛,但真要落地就发现完全不是这样。模型没有能力看你的函数源码,它只能靠注册表里的一段文字和参数结构来理解这个工具是干嘛的。你的字段写得越清楚,它选错的概率就越低;写糊了,再强的模型也救不回来。
我用的工具注册字段经过几次迭代,稳定下来的版本大致是这样的:
- name:唯一标识,不能和别的工具重名
- description:描述工具"做什么、典型使用场景、不做什么"
- parameters:JSON Schema 格式的参数定义,必须有类型和必填标记
- output_schema:工具返回值的预期结构
- tags:能力标签,方便决策器做候选过滤
- constraints:调用约束,比如"仅在有用户明确授权时调用""不支持批量"
3.2 踩坑记录:description 的"措辞陷阱"导致语义抢占
最经典的一个错,我给它起名叫"语义抢占"。当时系统里有两个工具:一个叫"发送邮件",另一个叫"发送通知"。为了省 token,我把"发送通知"的描述写成了"给用户或群组发送一条消息内容"。结果模型在处理"给项目组发送周报通知"这种任务时,选通知不选邮件,因为它看到"发送消息内容"就觉得匹配上了,哪管你是邮件还是 IM 通知。
这类问题的根因,不是模型笨,而是描述里缺少区分性信息。后来我统一改成了"描述模板 + 负面提示"的结构:每个工具的描述都分成"用途""典型场景""不适用场景"三段。比如"发送邮件"的不适用场景写明"如果需求是即时聊天通知,请使用发送通知工具",反过来也一样。加了负面提示之后,语义抢占的问题基本消失。
3.3 token 长度和描述质量的拉锯
你可能会想:描述写得越详细当然越好。但这里有个硬约束——工具多了以后,注册表全部塞进上下文里是要花 token 的。我实际项目里接了十几个工具,完整描述塞进去要几千 token,每轮调用都带着,成本压力很大。做过一轮实验,把描述精简掉一半,工具选择准确率直接掉了接近十个点。
最后还是找到了一个平衡点:完整描述只给到"决策阶段",一旦工具被选定进入执行阶段,就不再重复加载所有工具描述,只加载被选中那一个的结构化参数。这样主要是为了节省上下文长度,效果也挺好。对参数校验需求高的工具,描述写完整一点;对非常简单、一眼就懂的工具,描述压到一行。
3.4 注册表上线前的自检清单
这个清单是我后来每次接新工具都要过一遍的,列出来供你参考。
- 工具的 description 是否包含"不适用场景"?
- 参数是否都是强类型?枚举字段是否写全了所有合法值?
- 工具之间是否有描述语义重叠?如果有,是否加了互相指向的排除说明?
- 该工具是否需要有前置工具输出作为输入?如果有,是否在约束里写明白了?
这套自检看着基础,但每一条我都真实踩出过问题。尤其最后一条,工具依赖如果不在注册表里声明,决策器再聪明也只能靠猜。
4. 工具决策器:候选工具怎么排序,才能从"能用"变"该用"
4.1 决策不是选一个,而是排一条链路
很多第一版 Agent 的设计是:让模型直接选工具。我也这么做过,结果模型在一个需求里同时输出"调用工具 A、B、C、D",完全不管顺序。比如先发邮件再生成周报,等周报生成完,邮件已经发出去了,里面还是空的。后来我把"工具决策器"从主 Agent 里拆了出来,它负责两件事:选哪些工具,以及按什么顺序执行。
工具决策器的输入是用户目标解析结果,输出是工具链,它是一个有序列表,每个元素包含工具 ID 和预期输入来源。决策器本身用模型实现,但它不直接生成工具调用参数,只输出 JSON 格式的计划,比如[{"tool": "generate_report", "input_from": "user_data"}, {"tool": "send_email", "input_from": "generate_report.output"}]。这个结构让后面的参数注入器有了明确的依赖依据。
4.2 排序策略:读操作永远排在写操作前面
在给工具链排序的时候,我发现了一个相对普适的经验:只读工具优先,副作用工具靠后。这个逻辑有点类似于操作系统的权限设计——查询类工具不会改状态,先执行没关系;但发送邮件、修改记录这类写操作,必须等所有数据准备完成之后才能执行。
把动作类型放进了注册表的 constraints 里,标注为read/write/append/delete。工具决策器在生成计划时,会默认把 write 和 delete 类安排在所有 read 类的后面,同时禁止删除类工具在没有二次确认的情况下出现在自动链路里。这条规则从设计上拦截了一大批潜在事故。
4.3 踩坑记录:模型把"查询"当"创建"的幂等性灾难
说一个印象特别深的线上问题:系统里有一个"创建任务"工具和一个"查询任务"工具,两者并行作用在同一个项目里。某次用户说"帮我看看昨天的任务列表",决策器在解析时把"看"这个意图匹配到了"创建任务"的 description 上,原因是我在创建任务描述里写了"创建一个新任务"而不是"向系统添加一条新的待办记录"。模型把"看"理解为"新建一个视图"之类的动作,随即真的调用了创建接口,往系统里写了一条脏数据。
这个问题的教训是双层的:注册表描述要包含"本工具的副作用是什么",决策器在生成计划前要增加一个意图-动作一致性校验。我后来在校验逻辑里写了一条硬规则:用户提问如果是疑问句或查询性质,工具链里不允许出现 write/delete 类工具。这个规则不需要模型判断,用简单的句式分类就能实现,但它会挡住绝大多数误写场景。
4.4 互斥工具与依赖工具
工具之间还有两类关系需要决策器特别处理:互斥与依赖。互斥的典型例子是"使用模板 A 生成周报"和"使用模板 B 生成周报",两者只能选其一;依赖的典型例子是"发送邮件"需要"生成周报"的输出作为正文来源。我在注册表 models 之外加了一张工具关系表,专门声明exclusive_with和depends_on。决策器生成计划之前,代码会先对这层关系做一次硬校验,如果不满足就直接让模型重写计划,而不是让错误计划带到下一环节。这里我强调一下,凡是可以用确定性代码校验的关系,就不要丢给概率性的模型判断,这是整个系统稳定性的基础。
5. 参数注入器:九成调用失败出在这一层,别怪模型笨
5.1 自然语言和强类型接口之间的断层
工具链确定之后,面临的是最后一个也比较容易出问题的一步:把用户需求转成工具需要的实际参数。模型天然输出自然语言,工具接口却要求严格的类型、枚举和格式。就拿日期字段来举例,用户说"帮我查一下上周的数据",模型给你的参数可能直接是"上周",而工具接口要的是"2024-12-02"这样的具体日期字符串,直接传上去必炸。
这个件做的东西很具体:接收工具链计划 + 用户原始上下文,按 JSON Schema 生成符合格式的参数,对每个参数做类型转换、枚举合法值校验、必填项检查,校验不通过则自动把错误信息拼接回上下文,让模型重新生成,限制最多重试三次。它的核心价值是:把"模型生成参数"从自由文本变成了受约束填充,强类型边界的兜底全部落在代码上,而不是寄托于模型的临场发挥。
5.2 几种最普遍的类型陷阱
我列几个反复踩到的参数类型问题,你可以直接对照自己的场景看。
| 异常情况 | 场景还原 | 处理方式 |
|---|---|---|
| 日期相对表达 | 用户说"下周一",模型传 "下周" | 抽取后交给日期解析函数,结合"今天"推断具体日期,再转目标格式 |
| 空值沉默 | 用户没说收件人,模型生成空字符串 | 必填参数缺失时,回传错误信息要求模型补充,三次后转人工澄清 |
| 字符串和数组混淆 | 工具要数组,模型传 "a, b, c" | 按 schema 检测类型不匹配,尝试安全转换,无法转换则报错重试 |
| 枚举值幻觉 | 状态字段只允许 pending/done,模型传 "completed" | 做合法值映射表,能映射的映射,映射不了的回传重试 |
这些陷阱光靠 prompt 是止不住的,因为它们源于模型对世界理解的模糊性,而不是指令理解不到位。参数注入器最大的意义,就是把这部分模糊性在运行时显式暴露出来,再通过重试机制收敛。
5.3 重试风暴必须掐住
必须重点说一下重试循环。第一次做参数注入器的时候,我让它"校验失败就回到模型重新生成",结果有一个任务因为某个枚举值一直生成不合法,模型快速重试了十几次,token 费用飙升,任务还是没通过。主要原因是我没设最大重试次数,模型每次都轻微调整措辞、换各种近义词,就是不敢承认"我不确定这个字段该怎么填"。
后来给重试逻辑加了两个收敛手段:一是最大重试次数为三次,超过就短路,直接进入"向用户澄清"通道;二是把上一次失败的具体原因拼进重试 prompt 里,告诉模型"具体是哪个字段为什么失败,不要改动其他字段,只修正这个字段"。第二个手段特别有效,准确率显著提升,因为模型的精力被聚焦到一个具体错误上了。
5.4 参数注入器要和工具执行器解耦
最后还有一个架构上的经验:参数注入器只负责"生成并校验参数",不负责"调用工具"。工具调用放在一个独立的执行器里,执行器拿到的已经是校验过的强类型参数。做这个解耦的原因很现实:一旦工具调用失败,你才能明确区分是"参数错了"还是"工具本身错了"。如果是参数的问题,重试参数注入流程;如果是工具的错误,考虑换工具或者标记工具不可用。这两者混在一起的时候,排查会非常痛苦,我第一版就是混在一起的,线上问题定位花了好几次。
6. 结果评估器:让 Agent 知道自己搞砸了,才有机会纠错
6.1 HTTP 200 不代表任务完成
这一件是我最早觉得没必要、最后发现是最值得做的。最开始的系统里,工具调用只要返回没有抛异常,链路就往下走了。直到有一次"发送邮件"工具返回成功,但实际上收件人地址被模型拼错了,邮件发到了一个不存在的邮箱地址,调用接口照样返回 200。用户不会说你接口调成功了,用户只会说"周报没收到"。
所以第五个自制件——结果评估器的任务,是检查工具执行结果是否真的满足用户原始需求,而不是只看有没有异常。它的输出是结构化的状态:success、partial_success、failed、need_clarification四种。每个状态后面必须跟一段原因说明,这段说明会回到决策器,供它决定下一步走"换工具重试"、"换模型重试"还是"向用户确认"。
6.2 评估信号从哪里来
做结果评估器过程中会最先遇到的问题通常是:怎么判断结果好不好?完全靠大模型做开放式判断肯定不行。我实际用的信号分三层:第一层是硬校验,比对输出结构是否完整,比如周报生成结果里有没有标题、有没有日期范围、有没有正文段落;第二层是启发式规则,比如返回体为空、返回时长异常、包含错误关键字、数据条数为 0;第三层才是模型判断——把用户原始需求和工具输出一起交给评估模型,让它做一致性打分。
这三层的执行顺序是从便宜到昂贵。硬校验和启发式规则是代码实现,几乎没有成本;模型判断放在最后,只处理前两层没判断出来的模糊情况。这样设计的原因依然是成本控制,不是所有调用都需要模型重新读一遍用户需求。
6.3 踩坑记录:把结果评估逻辑写进主 Agent 的 prompt
最早一版我想省事,直接在主 Agent 的 prompt 里加了一句"调用完工具后,请检查结果是否满足用户需求"。结果非常讽刺:模型几乎总是回答"已经满足",哪怕结果明显不对。后来我意识到,这不是模型不听话,而是它把"检查"当成了一种对话礼节,它倾向于给你一个让你满意的肯定答复。就像你问朋友"我这件事办得怎么样",大部分人会先说"挺好",然后才补充细节。
独立性是这个件的核心。评估器我单独用一个轻量模型跑,prompt 里明确说:"你的任务是质检,不是执行。你必须指出不留情面的事实错误。"即便是同一个底层模型能力,独立上下文也确实比混在主链路里的判断要客观得多,这一点经过多轮对比验证。
6.4 失败状态怎么驱动下一步
评估器输出failed之后,后续路径值得说一下。我实际跑下来发现,评估失败之后不存在一个通用的最优方案,得细分场景:如果失败原因是参数错误,回参数注入器修正;如果是工具本身返回异常或者内容明显不对,回工具决策器换候选工具;如果两次重试还是失败,降级到简单模型重新跑一遍完整链路,这个动作在部分场景下反而能成功,大概率是因为简单模型生成结果更保守,不会写一些自以为合理的内容。这一整条失败处理链路,是让 Agent 系统从"demo 可用"变成"真实可用"的关键分水岭。
7. 五件联动复盘:一次"会议纪要转周报并发送"的完整链路
7.1 场景设定与预期路径
前面几件都是单独讲的,合在一起看它们怎么配合可能更有体感。这里用一个最常见的场景走一遍完整链路。用户输入是:"把昨天的项目例会纪要按照周报模板整理一下,发给项目群里所有人。没有模板的话用默认格式就行。"这个需求在旧规则系统里要写好几条 if-else,在自决链路里会拆得很清晰。
预期路径是:模型选择器判定这是一个中等复杂任务,需要文本理解和工具调用,不需要视觉,分配给中档推理模型;工具决策器从注册表里锁定"会议纪要解析""周报生成""联系人查询""消息发送"四个工具,并且把联系人查询放在消息发送前面;参数注入器从用户上下文里抽出时间范围"昨天"、模板偏好"默认格式"、收件人范围"项目群所有人";执行器依次调用;最终评估器检查周报完整性和发送状态。
7.2 每步流转的具体状态变化
实际跑起来,每步之间流转的是带状态的数据结构,而非游离的自然语言。模型选择器输出selected_models;工具决策器输出tool_chain,包含每个工具的前置依赖;参数注入器为每个工具生成injected_arguments,附上校验状态;执行器返回每个工具的raw_output;评估器最后输出final_status和issues。数据结构的标准化,是后面能顺利排查问题的基础,每步都有日志可回溯。
这次 demo 过程中故意埋了一个坑:用户说"昨天",但项目例会实际是"上周五",昨天是周末。参数注入器严格按字面把日期解析成了昨天,周报生成出来内容自然是空的。如果是第一版系统,到这里就算完成,但评估器在做完整性检查时,发现周报主体为空,于是判断为partial_success,并把原因回传。工具决策器收到反馈后,决定追加一次"会议纪要列表查询",发现有上周五的会议记录,于是推翻原日期参数,用新日期重新生成,整个链路又走了一遍,这次评估通过。
7.3 从结果倒推:线上问题应该从哪个环节查
这轮演练也让我总结了一套排查顺序。线上出现 Agent 行为异常时,别一上来就看模型输出,我现在的习惯是从后往前查:先看评估器给的final_status和issues,它里面已经区分了失败类别;再看参数注入器的校验日志,比如参数缺了哪些、哪些字段是重试出来的;再看工具决策器的计划,检查工具顺序和依赖是否合理;最后才回到模型选择器,看是不是模型分配错了导致推理能力不足。这个顺序能帮你快速定位八成问题,也避免把一个参数问题反复归因到模型能力上。
顺带说一下排查时需要重点关注的日志字段:每个环节都记录input_source(输入来源)、decision_reason(决策原因)、revision_count(重试次数)、degraded_flag(是否降级)。decision_reason尤其重要,因为模型决策是概率性的,只有把决策原因落下来,你事后才有办法分析它为什么选错。我在第一版里没记这个字段,导致很多问题只能靠猜。
8. 一点实操体会:先跑通闭环,再加判断力
整套五件自制件做完之后,如果再让我重来一次,我大概率会反过来做。第一版系统其实没必要一上来就把模型选择器和评估器做得那么完善,完全可以先用固定模型、固定工具链把闭环跑通,然后逐步放开自主决策。这样做的最大好处是,你可以先积累一批正常路径的日志,再拿这些日志去调模型选择器和工具决策器,比一上来就全自主、全靠肉眼盯日志要踏实得多。
8.1 给 Agent 留一条"逃走通道"
再提一个容易被忽视的设计:要允许 Agent 在拿不准的时候主动停下来问人,而不是硬着头皮继续跑。我给系统加了一个need_clarification状态,一旦参数注入器三轮重试失败,或者评估器发现多个冲突信号,系统会直接向用户提问,而不是继续消耗 token 瞎猜。表面上看这会打断流程,实际上它大幅提高了整体成功率,因为很多失败链路的起点,就是模型在一个本当澄清的地方做了一个自信的错误假设。
8.2 日志系统是你唯一可信赖的同事
如果你也要做类似系统,我建议从第一天就把决策日志当作第一等公民来对待——不要等出了线上事故再补。我后来复盘时,发现大多数坑都非常直白,就一个原因:某个环节的决策理由没有记录下来。只要理由落盘了,坑就不再是坑,顶多是下一次要改进的参数。加上这五个件本身是解耦的,日志清晰之后,每次迭代都变成局部的,不会把整个系统重写一遍。
如果你正准备让 Agent 自己挑模型、自己挑工具,我的建议很简单:先做工具注册表和参数注入器,它们是整个地基;再做结果评估器和工具决策器,它们负责堵住模型的盲目自信;最后加模型选择器,做成本优化。按这个顺序来,每一层都是在给下一层铺路,而不是推倒重来。