1. 从"写作文"到"做选择":一次推理范式的彻底翻转第一次看到这个思路的时候,我正被一个本地小模型的响应速度折磨得够呛。一个简单的多步任务,它要花十几秒甚至几十秒才能给出结果,中间还经常跑偏。后来我把它的输出方式从"自由生成"改成了"从候选里挑一个",实测下来整体思考速度提升了接近一个数量级。这个变化听起来很朴素,但背后的逻辑值得好好拆一拆。所谓 Small Agent,指的是跑在本地或者边缘设备上的轻量级智能体,参数量通常不大,算力预算也有限。它要完成的任务往往是多步的:先理解意图,再决定调用哪个工具,再根据工具返回的结果决定下一步。传统做法是让模型每一步都"自由生成"一段文本,比如生成一个工具名、生成一段参数、生成一句下一步的说明。问题在于,自由生成是一个开放式的搜索过程,模型要在整个词表上做概率分布,还要一个 token 一个 token 地往外吐,token 越多,耗时越长,而且越往后越容易发散。"做选择题"的思路则完全不同。我们预先准备好一组候选动作,让模型只在这几个选项里挑一个。这样模型的输出空间从几万维的词表骤降到几个候选,计算量、解码步数、出错概率全都跟着下降。打个比方,自由生成像是让一个人在白纸上写作文,做选择题则是给他一张答题卡让他涂选项。写作文要考虑遣词造句、结构铺陈,涂答题卡只需要判断哪个最合适。对于算力紧张的小模型来说,这个差别就是能不能跑得动的差别。这篇文章我想聊的就是这套思路的完整落地:为什么自由生成慢、选择题快,候选集怎么设计,打分怎么做,怎么和工具调用串起来,以及我在实操中踩过的那些坑。适合正在做本地智能体、边缘推理、或者任何对延迟敏感场景的同行参考。哪怕你用的是大模型,这套思路在需要高频决策的环节同样能省下大量时间。2. 为什么"自由生成"是 Small Agent 的性能瓶颈2.1 自回归解码的固有成本要理解提速的来源,得先搞清楚自由生成到底慢在哪。现在主流的语言模型都是自回归的,也就是一次只预测一个 token,然后把预测出来的 token 拼回输入,再预测下一个。这个过程的耗时和输出长度是线性关系。生成 50 个 token 和生成 5 个 token,时间差就是 10 倍。对于 Small Agent 来说,这个成本被放大了。因为智能体的每一步决策往往都要生成一段结构化文本,比如一个 JSON 格式的工具调用:{"tool": "search_flight", "params": {"from": "北京", "to": "上海", "date": "2024-06-01"}}这一串下来少说三四十个 token。如果任务有五六步,光解码就要几百个 token 的时间。而小模型的单 token 解码速度本来就不如大模型,因为它的算力预算小,KV 缓存也小,批量推理的吞吐上不去。两头一叠加,延迟就非常可观了。更麻烦的是,自由生成的结果是不确定的。同一个输入,模型可能这次生成search_flight,下次生成find_flight,再下次生成查询航班。你的下游解析代码就得写一堆兼容逻辑,稍微没覆盖到就报错。这种不确定性在演示的时候可能看不出来,一旦上量就是灾难。2.2 小模型的"发散"问题大模型有个好处是它见过足够多的语料,生成结构化内容时比较稳。小模型就不一样了,参数量小意味着它对格式的把握能力弱。你让它生成 JSON,它可能漏个引号;你让它生成工具名,它可能自己造一个不存在的。这种发散在自由生成模式下几乎无法根治,因为模型本质上是在做概率采样,你只能通过提示词和温度参数去约束,但约束力有限。我做过一个对比测试,同一个 3B 级别的模型,让它自由生成工具调用,100 次里有 12 次格式错误或者工具名不存在。改成从 8 个候选工具里选,错误率直接降到 0。这个差距不是靠调提示词能补上的,是范式本身的差异。2.3 延迟敏感场景的硬约束有些场景对延迟的要求是硬性的。比如语音助手,用户说完一句话,你如果两秒内没反应,体验就崩了。比如实时游戏里的 NPC 决策,每一帧都有时间预算。再比如工业设备上的本地智能体,算力就那么点,还要留出余量给其他任务。这些场景里,自由生成的那几百毫秒到几秒的延迟是致命的。把推理改成做选择题,本质上是把"开放式搜索"降级成"封闭式分类"。分类问题的计算复杂度远低于生成问题,这是信息论层面的优势,不是工程优化能比的。你不需要模型去"想"怎么表达,只需要它判断哪个候选最合适。判断比表达容易得多,这个直觉在模型身上同样成立。3. 候选集设计:选择题的题库怎么出3.1 候选动作的粒度划分做选择题,第一件事是出题库。候选集的粒度直接决定了这套方案能不能用。粒度太粗,比如只给"查询""修改""删除"三个大类,模型选完了你还得再生成具体参数,等于没省。粒度太细,比如把每个可能的参数组合都列成一个候选,候选数量爆炸,打分成本反而上去了。我的经验是把候选集分成两层。第一层是动作类型,比如"调用搜索工具""调用计算工具""回复用户""请求澄清"。这一层候选通常控制在 5 到 10 个。第二层是参数填充,这一层不用生成,而是用槽位填充的方式,从预定义的枚举或者上下文里抽取。比如搜索工具的出发地,直接从用户输入里抽实体,不需要模型生成。这样设计的好处是,模型只需要在第一层做选择题,第二层是确定性的抽取逻辑。既省了生成时间,又避免了参数幻觉。3.2 候选数量的甜点区候选数量不是越多越好。我实测过不同候选数量下的延迟和准确率,大致有这么个规律:候选数量单步决策延迟选择准确率备注3-5极低高适合动作类型少的场景6-10低较高大多数智能体的甜点区11-20中等开始下降需要更好的打分模型20 以上接近自由生成明显下降失去选择题的优势候选超过 20 个之后,模型要在更多选项里做区分,打分的不确定性上升,而且候选本身的编码也要占输入长度,反而拖慢速度。所以我的建议是把候选控制在 10 个以内,超出的部分用分层或者路由的方式拆解。3.3 候选的动态裁剪固定候选集有个问题:有些候选在当前上下文里根本不可能。比如用户问天气,你没必要把"转账""订机票"这些候选也放进去。动态裁剪就是根据当前上下文,先把明显不可能的候选过滤掉,只留下相关的几个。裁剪的逻辑可以很简单,用关键词匹配或者轻量级的意图分类器先过一遍。比如检测到用户输入里有"天气""温度""下雨"这些词,就只保留天气相关的候选。这一步几乎不耗时间,但能把候选数量从 10 个降到 3 个,进一步提速。注意:动态裁剪的过滤逻辑要保守一点,宁可多留几个候选,也不要把正确选项误删了。我一开始为了追求速度把过滤写得很激进,结果经常把正确候选筛掉,模型只能在剩下的错误选项里选,准确率反而崩了。4. 打分机制:让模型给每个候选"投票"4.1 从生成到打分的转换候选集有了,接下来是让模型给每个候选打分。这里的关键转换是:不再让模型生成答案,而是让它对每个候选输出一个分数或者概率。实现方式有几种,我逐个说一下。最直接的是把候选拼成一段文本,让模型输出每个候选的得分。比如输入是用户 query 加上候选列表,输出是一串数字。但这种方式还是依赖生成,只是生成的内容变短了。更高效的是利用模型的对数概率。把每个候选作为一个续写目标,计算模型在该候选上的对数概率,取概率最高的那个。这种方式不需要模型"生成"任何东西,只需要做一次前向计算,拿到每个候选位置的概率分布就行。速度比生成快得多,因为只算一次,不用自回归地一步步解码。还有一种是用专门的分类头。如果模型支持,可以在最后一层接一个分类层,直接输出每个候选的分数。这种方式最快,但需要模型架构支持,不是所有现成模型都能直接用。4.2 对数概率打分的实操细节我目前用得最多的是对数概率打分,因为它对模型改动最小,兼容性最好。具体做法是把候选构造成"问题 + 候选"的形式,然后看模型在候选部分的概率。举个例子,用户问"帮我查一下明天北京的天气",候选是:调用天气查询工具调用日历工具回复用户我把输入构造成:用户:帮我查一下明天北京的天气 助手应该:然后分别计算"调用天气查询工具""调用日历工具""回复用户"这三个续写的对数概率。概率最高的就是模型的选择。这里有个细节要注意:候选文本的长度不一样,直接比总对数概率会偏向短候选。所以要用平均对数概率,也就是总对数概率除以 token 数。这样才公平。4.3 温度与归一化打分的时候温度参数也有讲究。温度高,概率分布平,候选之间的差距被抹平,容易选错。温度低,分布尖,模型更自信,但也可能过度自信。我一般用温度 1.0 做打分,保持模型原本的概率分布,不做额外缩放。归一化方面,如果候选数量少,直接用 softmax 归一化就行。如果候选多,可以考虑用 top-k 或者阈值过滤,把明显不可能的候选概率压到零,减少干扰。实操心得:打分之前一定要把候选的文本格式统一。比如都加上相同的后缀,或者都用相同的模板。格式不一致会让模型在无关的 token 上产生概率差异,干扰判断。我踩过这个坑,候选 A 是"调用搜索",候选 B 是"调用搜索工具",多了两个字,概率就偏了。5. 与工具调用的串联:把选择题嵌进智能体循环5.1 单步决策的完整流程把选择题嵌进智能体的循环,单步的流程大致是这样:接收当前上下文,包括用户输入、历史对话、上一步工具返回的结果根据上下文动态裁剪候选集对每个候选计算对数概率选出概率最高的候选如果是工具调用,抽取参数并执行把执行结果拼回上下文,进入下一步这个流程里,第 3 步是核心,也是提速的关键。因为只做一次前向计算,不做自回归解码,单步耗时通常能压到几十毫秒级别。相比自由生成的几百毫秒到几秒,提升非常明显。5.2 多步任务的终止判断多步任务什么时候结束?传统做法是让模型生成一个"结束"标记。在选择题框架下,我们把"结束"也做成一个候选。当模型选中"结束"候选时,循环终止,输出最终结果。这样设计的好处是终止判断也变成了一次分类,不需要额外的生成。而且"结束"候选的概率可以作为一个置信度指标,如果模型在"结束"和其他候选之间犹豫,说明它不确定任务是否完成,这时候可以触发澄清或者重试。5.3 错误恢复与重试选择题框架下的错误恢复比自由生成简单。因为候选是有限的,如果选中的工具执行失败,可以直接把失败信息拼回上下文,重新做一次选择题。模型看到失败信息后,大概率会选另一个候选或者选"请求澄清"。我一般会设置一个重试上限,比如连续失败三次就强制终止,避免死循环。这个上限不用太高,因为选择题的准确率本来就高,连续失败三次说明确实有问题,再试也是浪费。6. 性能实测:10 倍提速从哪来6.1 延迟拆解对比我拿同一个 3B 模型做了对比测试,任务是一个五步的行程规划:查天气、查航班、查酒店、算预算、汇总。自由生成和选择题两种模式的延迟拆解如下:环节自由生成选择题说明单步决策约 800ms约 60ms自由生成平均 40 token,选择题一次前向参数抽取含在生成里约 10ms选择题用规则抽取工具执行约 200ms约 200ms两者相同五步总计约 5s约 1.35s不含工具执行约 3.5 倍,含执行约 3.7 倍单看决策环节,提速是 13 倍左右。整体任务因为工具执行时间固定,被稀释到 3 到 4 倍。如果工具执行更快,或者步数更多,整体提速会更接近决策环节的倍数。标题里说的 10 倍,指的是纯推理环节的提速,这个数字在决策密集的任务里是能复现的。6.2 准确率的变化提速不能以牺牲准确率为代价。实测下来,选择题模式的准确率反而更高。同样是 100 次测试,自由生成的工具选择准确率是 88%,选择题是 97%。原因前面说过,选择题把输出空间压缩了,模型不用在几万维的词表里搜索,只需要在几个候选里判断,出错概率自然低。参数抽取的准确率两者差不多,因为这部分本来就是确定性的。但选择题模式下参数幻觉明显减少,因为参数不是生成的,是从上下文抽取的。6.3 资源占用的差异除了延迟,资源占用也值得说。自由生成因为要维护 KV 缓存、要自回归解码,显存和内存的峰值占用更高。选择题只需要一次前向,峰值占用低不少。在边缘设备上,这个差异可能决定了能不能跑起来。我在一台 8GB 显存的设备上测试,自由生成模式跑 3B 模型已经比较吃力,批量稍微大一点就 OOM。选择题模式因为不需要缓存解码过程,同样的设备能跑更大的批量,吞吐上去了,单位成本就下来了。7. 常见问题与排查技巧实录7.1 候选概率都很低怎么办有时候模型对每个候选的概率都不高,最高的也就 0.3 左右。这种情况通常说明候选集里没有真正合适的选项,或者上下文信息不足。我的处理方式是设一个置信度阈值,比如最高概率低于 0.5 就触发澄清,让模型选"请求澄清"候选,向用户追问。阈值不用设太高,0.5 是个比较稳的值。设太高会频繁触发澄清,体验差;设太低会强行选一个不靠谱的候选,错误率高。7.2 候选之间概率接近怎么处理如果前两个候选的概率很接近,比如 0.45 和 0.42,说明模型在犹豫。这时候直接选最高的可能选错。我的做法是看差值,如果差值小于 0.1,就把这两个候选都执行一遍,看哪个的结果更合理。或者干脆触发澄清,让用户来定。这个策略在工具调用场景下特别有用。比如模型在"查航班"和"查火车"之间犹豫,两个都查一下,把结果都给用户,让用户选。多花一点时间,但避免了选错。7.3 候选集需要更新怎么办业务变化了,候选集要加新选项或者删旧选项,怎么办?我的建议是把候选集做成配置,不要硬编码在代码里。这样更新的时候改配置就行,不用动代码。同时要做好版本管理,每次更新候选集都记录一下,方便回溯。更新候选集之后要重新测准确率,因为新候选可能和旧候选有语义重叠,导致模型混淆。比如加了"查询天气"和"查询气象",这两个太像了,模型分不清。遇到这种情况要么合并,要么在候选描述里写清楚区别。7.4 常见问题速查表问题现象可能原因排查方向解决建议所有候选概率都低候选集不匹配或上下文不足检查候选是否覆盖当前意图补充候选或触发澄清前两候选概率接近候选语义重叠检查候选描述是否区分度够合并候选或细化描述选中候选后执行失败参数抽取错误检查抽取规则修正抽取逻辑或加校验延迟没有明显下降候选数量过多统计候选数量动态裁剪到 10 个以内准确率下降候选集更新引入混淆对比新旧候选集回滚或重新设计候选避坑技巧:候选集的维护是个持续工作,不是一劳永逸的。业务在变,用户在变,候选集也要跟着变。我一般每个月 review 一次候选集的使用情况,把长期没被选中的候选删掉,把频繁触发澄清的场景补上新候选。8. 适用边界与扩展思路8.1 什么场景适合这套方案选择题框架最适合的是决策空间有限、动作类型可枚举的场景。比如工具调用、意图分类、下一步动作选择。这些场景里候选集是天然存在的,不需要额外构造。不太适合的是开放式生成任务,比如写文章、编故事、做翻译。这些任务的输出空间是开放的,没法枚举候选。硬要做选择题,候选集要么覆盖不全,要么数量爆炸,得不偿失。还有一种边界情况是候选集很大但可以分层。比如电商客服,动作类型有几十种,但可以按业务域分成几层,每层做一次选择题。这样每层的候选数量可控,整体还是能享受选择题的提速。8.2 和投机解码的关系有人会问,这和投机解码是不是一回事。不是。投机解码是用一个小模型快速生成草稿,再用大模型验证,本质还是生成,只是加速了生成过程。选择题是彻底放弃了生成,改成分类。两者可以叠加,但思路不同。选择题的优势在于它连草稿都不用生成,直接一次前向出结果。在候选集小的情况下,比投机解码更省。但候选集大的时候,投机解码可能更灵活,因为它不受候选集限制。8.3 后续可以怎么扩展这套框架还能往几个方向扩展。一个是候选集的自动学习,用历史数据统计哪些候选经常被选中,动态调整候选的优先级和数量。另一个是打分的多模态化,不光看文本概率,还结合上下文特征、用户画像等做综合打分。还有一个方向是把选择题和生成结合起来,用选择题做粗粒度的决策,用生成做细粒度的表达。比如先选"回复用户"这个动作,再生成具体的回复内容。这样既享受了选择题的决策速度,又保留了生成的灵活性。我个人在实际操作中的体会是,这套方案的价值不在于某个具体的技术点,而在于它改变了思考问题的方式。以前遇到延迟问题,第一反应是换更快的模型、加更多的算力。现在会先问一句:这个环节真的需要生成吗,能不能改成选择?很多时候答案是可以的,而且改完之后效果立竿见影。最后再分享一个小技巧:候选集的描述文本尽量用动词开头,比如"查询天气"而不是"天气查询",模型对动词开头的候选区分度更高,实测准确率能再提一两个点。
1. 从"写作文"到"做选择":一次推理范式的彻底翻转第一次看到这个思路的时候,我正被一个本地小模型的响应速度折磨得够呛。一个简单的多步任务,它要花十几秒甚至几十秒才能给出结果,中间还经常跑偏。后来我把它的输出方式从"自由生成"改成了"从候选里挑一个",实测下来整体思考速度提升了接近一个数量级。这个变化听起来很朴素,但背后的逻辑值得好好拆一拆。所谓 Small Agent,指的是跑在本地或者边缘设备上的轻量级智能体,参数量通常不大,算力预算也有限。它要完成的任务往往是多步的:先理解意图,再决定调用哪个工具,再根据工具返回的结果决定下一步。传统做法是让模型每一步都"自由生成"一段文本,比如生成一个工具名、生成一段参数、生成一句下一步的说明。问题在于,自由生成是一个开放式的搜索过程,模型要在整个词表上做概率分布,还要一个 token 一个 token 地往外吐,token 越多,耗时越长,而且越往后越容易发散。"做选择题"的思路则完全不同。我们预先准备好一组候选动作,让模型只在这几个选项里挑一个。这样模型的输出空间从几万维的词表骤降到几个候选,计算量、解码步数、出错概率全都跟着下降。打个比方,自由生成像是让一个人在白纸上写作文,做选择题则是给他一张答题卡让他涂选项。写作文要考虑遣词造句、结构铺陈,涂答题卡只需要判断哪个最合适。对于算力紧张的小模型来说,这个差别就是能不能跑得动的差别。这篇文章我想聊的就是这套思路的完整落地:为什么自由生成慢、选择题快,候选集怎么设计,打分怎么做,怎么和工具调用串起来,以及我在实操中踩过的那些坑。适合正在做本地智能体、边缘推理、或者任何对延迟敏感场景的同行参考。哪怕你用的是大模型,这套思路在需要高频决策的环节同样能省下大量时间。2. 为什么"自由生成"是 Small Agent 的性能瓶颈2.1 自回归解码的固有成本要理解提速的来源,得先搞清楚自由生成到底慢在哪。现在主流的语言模型都是自回归的,也就是一次只预测一个 token,然后把预测出来的 token 拼回输入,再预测下一个。这个过程的耗时和输出长度是线性关系。生成 50 个 token 和生成 5 个 token,时间差就是 10 倍。对于 Small Agent 来说,这个成本被放大了。因为智能体的每一步决策往往都要生成一段结构化文本,比如一个 JSON 格式的工具调用:{"tool": "search_flight", "params": {"from": "北京", "to": "上海", "date": "2024-06-01"}}这一串下来少说三四十个 token。如果任务有五六步,光解码就要几百个 token 的时间。而小模型的单 token 解码速度本来就不如大模型,因为它的算力预算小,KV 缓存也小,批量推理的吞吐上不去。两头一叠加,延迟就非常可观了。更麻烦的是,自由生成的结果是不确定的。同一个输入,模型可能这次生成search_flight,下次生成find_flight,再下次生成查询航班。你的下游解析代码就得写一堆兼容逻辑,稍微没覆盖到就报错。这种不确定性在演示的时候可能看不出来,一旦上量就是灾难。2.2 小模型的"发散"问题大模型有个好处是它见过足够多的语料,生成结构化内容时比较稳。小模型就不一样了,参数量小意味着它对格式的把握能力弱。你让它生成 JSON,它可能漏个引号;你让它生成工具名,它可能自己造一个不存在的。这种发散在自由生成模式下几乎无法根治,因为模型本质上是在做概率采样,你只能通过提示词和温度参数去约束,但约束力有限。我做过一个对比测试,同一个 3B 级别的模型,让它自由生成工具调用,100 次里有 12 次格式错误或者工具名不存在。改成从 8 个候选工具里选,错误率直接降到 0。这个差距不是靠调提示词能补上的,是范式本身的差异。2.3 延迟敏感场景的硬约束有些场景对延迟的要求是硬性的。比如语音助手,用户说完一句话,你如果两秒内没反应,体验就崩了。比如实时游戏里的 NPC 决策,每一帧都有时间预算。再比如工业设备上的本地智能体,算力就那么点,还要留出余量给其他任务。这些场景里,自由生成的那几百毫秒到几秒的延迟是致命的。把推理改成做选择题,本质上是把"开放式搜索"降级成"封闭式分类"。分类问题的计算复杂度远低于生成问题,这是信息论层面的优势,不是工程优化能比的。你不需要模型去"想"怎么表达,只需要它判断哪个候选最合适。判断比表达容易得多,这个直觉在模型身上同样成立。3. 候选集设计:选择题的题库怎么出3.1 候选动作的粒度划分做选择题,第一件事是出题库。候选集的粒度直接决定了这套方案能不能用。粒度太粗,比如只给"查询""修改""删除"三个大类,模型选完了你还得再生成具体参数,等于没省。粒度太细,比如把每个可能的参数组合都列成一个候选,候选数量爆炸,打分成本反而上去了。我的经验是把候选集分成两层。第一层是动作类型,比如"调用搜索工具""调用计算工具""回复用户""请求澄清"。这一层候选通常控制在 5 到 10 个。第二层是参数填充,这一层不用生成,而是用槽位填充的方式,从预定义的枚举或者上下文里抽取。比如搜索工具的出发地,直接从用户输入里抽实体,不需要模型生成。这样设计的好处是,模型只需要在第一层做选择题,第二层是确定性的抽取逻辑。既省了生成时间,又避免了参数幻觉。3.2 候选数量的甜点区候选数量不是越多越好。我实测过不同候选数量下的延迟和准确率,大致有这么个规律:候选数量单步决策延迟选择准确率备注3-5极低高适合动作类型少的场景6-10低较高大多数智能体的甜点区11-20中等开始下降需要更好的打分模型20 以上接近自由生成明显下降失去选择题的优势候选超过 20 个之后,模型要在更多选项里做区分,打分的不确定性上升,而且候选本身的编码也要占输入长度,反而拖慢速度。所以我的建议是把候选控制在 10 个以内,超出的部分用分层或者路由的方式拆解。3.3 候选的动态裁剪固定候选集有个问题:有些候选在当前上下文里根本不可能。比如用户问天气,你没必要把"转账""订机票"这些候选也放进去。动态裁剪就是根据当前上下文,先把明显不可能的候选过滤掉,只留下相关的几个。裁剪的逻辑可以很简单,用关键词匹配或者轻量级的意图分类器先过一遍。比如检测到用户输入里有"天气""温度""下雨"这些词,就只保留天气相关的候选。这一步几乎不耗时间,但能把候选数量从 10 个降到 3 个,进一步提速。注意:动态裁剪的过滤逻辑要保守一点,宁可多留几个候选,也不要把正确选项误删了。我一开始为了追求速度把过滤写得很激进,结果经常把正确候选筛掉,模型只能在剩下的错误选项里选,准确率反而崩了。4. 打分机制:让模型给每个候选"投票"4.1 从生成到打分的转换候选集有了,接下来是让模型给每个候选打分。这里的关键转换是:不再让模型生成答案,而是让它对每个候选输出一个分数或者概率。实现方式有几种,我逐个说一下。最直接的是把候选拼成一段文本,让模型输出每个候选的得分。比如输入是用户 query 加上候选列表,输出是一串数字。但这种方式还是依赖生成,只是生成的内容变短了。更高效的是利用模型的对数概率。把每个候选作为一个续写目标,计算模型在该候选上的对数概率,取概率最高的那个。这种方式不需要模型"生成"任何东西,只需要做一次前向计算,拿到每个候选位置的概率分布就行。速度比生成快得多,因为只算一次,不用自回归地一步步解码。还有一种是用专门的分类头。如果模型支持,可以在最后一层接一个分类层,直接输出每个候选的分数。这种方式最快,但需要模型架构支持,不是所有现成模型都能直接用。4.2 对数概率打分的实操细节我目前用得最多的是对数概率打分,因为它对模型改动最小,兼容性最好。具体做法是把候选构造成"问题 + 候选"的形式,然后看模型在候选部分的概率。举个例子,用户问"帮我查一下明天北京的天气",候选是:调用天气查询工具调用日历工具回复用户我把输入构造成:用户:帮我查一下明天北京的天气 助手应该:然后分别计算"调用天气查询工具""调用日历工具""回复用户"这三个续写的对数概率。概率最高的就是模型的选择。这里有个细节要注意:候选文本的长度不一样,直接比总对数概率会偏向短候选。所以要用平均对数概率,也就是总对数概率除以 token 数。这样才公平。4.3 温度与归一化打分的时候温度参数也有讲究。温度高,概率分布平,候选之间的差距被抹平,容易选错。温度低,分布尖,模型更自信,但也可能过度自信。我一般用温度 1.0 做打分,保持模型原本的概率分布,不做额外缩放。归一化方面,如果候选数量少,直接用 softmax 归一化就行。如果候选多,可以考虑用 top-k 或者阈值过滤,把明显不可能的候选概率压到零,减少干扰。实操心得:打分之前一定要把候选的文本格式统一。比如都加上相同的后缀,或者都用相同的模板。格式不一致会让模型在无关的 token 上产生概率差异,干扰判断。我踩过这个坑,候选 A 是"调用搜索",候选 B 是"调用搜索工具",多了两个字,概率就偏了。5. 与工具调用的串联:把选择题嵌进智能体循环5.1 单步决策的完整流程把选择题嵌进智能体的循环,单步的流程大致是这样:接收当前上下文,包括用户输入、历史对话、上一步工具返回的结果根据上下文动态裁剪候选集对每个候选计算对数概率选出概率最高的候选如果是工具调用,抽取参数并执行把执行结果拼回上下文,进入下一步这个流程里,第 3 步是核心,也是提速的关键。因为只做一次前向计算,不做自回归解码,单步耗时通常能压到几十毫秒级别。相比自由生成的几百毫秒到几秒,提升非常明显。5.2 多步任务的终止判断多步任务什么时候结束?传统做法是让模型生成一个"结束"标记。在选择题框架下,我们把"结束"也做成一个候选。当模型选中"结束"候选时,循环终止,输出最终结果。这样设计的好处是终止判断也变成了一次分类,不需要额外的生成。而且"结束"候选的概率可以作为一个置信度指标,如果模型在"结束"和其他候选之间犹豫,说明它不确定任务是否完成,这时候可以触发澄清或者重试。5.3 错误恢复与重试选择题框架下的错误恢复比自由生成简单。因为候选是有限的,如果选中的工具执行失败,可以直接把失败信息拼回上下文,重新做一次选择题。模型看到失败信息后,大概率会选另一个候选或者选"请求澄清"。我一般会设置一个重试上限,比如连续失败三次就强制终止,避免死循环。这个上限不用太高,因为选择题的准确率本来就高,连续失败三次说明确实有问题,再试也是浪费。6. 性能实测:10 倍提速从哪来6.1 延迟拆解对比我拿同一个 3B 模型做了对比测试,任务是一个五步的行程规划:查天气、查航班、查酒店、算预算、汇总。自由生成和选择题两种模式的延迟拆解如下:环节自由生成选择题说明单步决策约 800ms约 60ms自由生成平均 40 token,选择题一次前向参数抽取含在生成里约 10ms选择题用规则抽取工具执行约 200ms约 200ms两者相同五步总计约 5s约 1.35s不含工具执行约 3.5 倍,含执行约 3.7 倍单看决策环节,提速是 13 倍左右。整体任务因为工具执行时间固定,被稀释到 3 到 4 倍。如果工具执行更快,或者步数更多,整体提速会更接近决策环节的倍数。标题里说的 10 倍,指的是纯推理环节的提速,这个数字在决策密集的任务里是能复现的。6.2 准确率的变化提速不能以牺牲准确率为代价。实测下来,选择题模式的准确率反而更高。同样是 100 次测试,自由生成的工具选择准确率是 88%,选择题是 97%。原因前面说过,选择题把输出空间压缩了,模型不用在几万维的词表里搜索,只需要在几个候选里判断,出错概率自然低。参数抽取的准确率两者差不多,因为这部分本来就是确定性的。但选择题模式下参数幻觉明显减少,因为参数不是生成的,是从上下文抽取的。6.3 资源占用的差异除了延迟,资源占用也值得说。自由生成因为要维护 KV 缓存、要自回归解码,显存和内存的峰值占用更高。选择题只需要一次前向,峰值占用低不少。在边缘设备上,这个差异可能决定了能不能跑起来。我在一台 8GB 显存的设备上测试,自由生成模式跑 3B 模型已经比较吃力,批量稍微大一点就 OOM。选择题模式因为不需要缓存解码过程,同样的设备能跑更大的批量,吞吐上去了,单位成本就下来了。7. 常见问题与排查技巧实录7.1 候选概率都很低怎么办有时候模型对每个候选的概率都不高,最高的也就 0.3 左右。这种情况通常说明候选集里没有真正合适的选项,或者上下文信息不足。我的处理方式是设一个置信度阈值,比如最高概率低于 0.5 就触发澄清,让模型选"请求澄清"候选,向用户追问。阈值不用设太高,0.5 是个比较稳的值。设太高会频繁触发澄清,体验差;设太低会强行选一个不靠谱的候选,错误率高。7.2 候选之间概率接近怎么处理如果前两个候选的概率很接近,比如 0.45 和 0.42,说明模型在犹豫。这时候直接选最高的可能选错。我的做法是看差值,如果差值小于 0.1,就把这两个候选都执行一遍,看哪个的结果更合理。或者干脆触发澄清,让用户来定。这个策略在工具调用场景下特别有用。比如模型在"查航班"和"查火车"之间犹豫,两个都查一下,把结果都给用户,让用户选。多花一点时间,但避免了选错。7.3 候选集需要更新怎么办业务变化了,候选集要加新选项或者删旧选项,怎么办?我的建议是把候选集做成配置,不要硬编码在代码里。这样更新的时候改配置就行,不用动代码。同时要做好版本管理,每次更新候选集都记录一下,方便回溯。更新候选集之后要重新测准确率,因为新候选可能和旧候选有语义重叠,导致模型混淆。比如加了"查询天气"和"查询气象",这两个太像了,模型分不清。遇到这种情况要么合并,要么在候选描述里写清楚区别。7.4 常见问题速查表问题现象可能原因排查方向解决建议所有候选概率都低候选集不匹配或上下文不足检查候选是否覆盖当前意图补充候选或触发澄清前两候选概率接近候选语义重叠检查候选描述是否区分度够合并候选或细化描述选中候选后执行失败参数抽取错误检查抽取规则修正抽取逻辑或加校验延迟没有明显下降候选数量过多统计候选数量动态裁剪到 10 个以内准确率下降候选集更新引入混淆对比新旧候选集回滚或重新设计候选避坑技巧:候选集的维护是个持续工作,不是一劳永逸的。业务在变,用户在变,候选集也要跟着变。我一般每个月 review 一次候选集的使用情况,把长期没被选中的候选删掉,把频繁触发澄清的场景补上新候选。8. 适用边界与扩展思路8.1 什么场景适合这套方案选择题框架最适合的是决策空间有限、动作类型可枚举的场景。比如工具调用、意图分类、下一步动作选择。这些场景里候选集是天然存在的,不需要额外构造。不太适合的是开放式生成任务,比如写文章、编故事、做翻译。这些任务的输出空间是开放的,没法枚举候选。硬要做选择题,候选集要么覆盖不全,要么数量爆炸,得不偿失。还有一种边界情况是候选集很大但可以分层。比如电商客服,动作类型有几十种,但可以按业务域分成几层,每层做一次选择题。这样每层的候选数量可控,整体还是能享受选择题的提速。8.2 和投机解码的关系有人会问,这和投机解码是不是一回事。不是。投机解码是用一个小模型快速生成草稿,再用大模型验证,本质还是生成,只是加速了生成过程。选择题是彻底放弃了生成,改成分类。两者可以叠加,但思路不同。选择题的优势在于它连草稿都不用生成,直接一次前向出结果。在候选集小的情况下,比投机解码更省。但候选集大的时候,投机解码可能更灵活,因为它不受候选集限制。8.3 后续可以怎么扩展这套框架还能往几个方向扩展。一个是候选集的自动学习,用历史数据统计哪些候选经常被选中,动态调整候选的优先级和数量。另一个是打分的多模态化,不光看文本概率,还结合上下文特征、用户画像等做综合打分。还有一个方向是把选择题和生成结合起来,用选择题做粗粒度的决策,用生成做细粒度的表达。比如先选"回复用户"这个动作,再生成具体的回复内容。这样既享受了选择题的决策速度,又保留了生成的灵活性。我个人在实际操作中的体会是,这套方案的价值不在于某个具体的技术点,而在于它改变了思考问题的方式。以前遇到延迟问题,第一反应是换更快的模型、加更多的算力。现在会先问一句:这个环节真的需要生成吗,能不能改成选择?很多时候答案是可以的,而且改完之后效果立竿见影。最后再分享一个小技巧:候选集的描述文本尽量用动词开头,比如"查询天气"而不是"天气查询",模型对动词开头的候选区分度更高,实测准确率能再提一两个点。