
1. 从一次线上事故说起为什么我把 CoT 换成了选择题去年冬天我负责的一个 Small Agent 项目在线上跑得磕磕绊绊。这个 Agent 做的事情本身不复杂接收用户的一段自然语言指令判断意图然后从十几个预定义的工具里挑一个执行。听起来像是教科书级别的入门任务但实际跑起来单次推理延迟稳定在 2.3 秒左右高峰期直接飙到 4 秒以上。用户等得不耐烦产品经理天天在群里艾特我日志里全是超时告警。我一开始的直觉是模型太小、算力不够于是换了个更大的模型延迟反而更高。后来我把每次推理的完整输出打出来看发现问题根本不在模型大小上——Agent 每次都在生成一大段 CoTChain of Thought思维链洋洋洒洒几百个 token从“用户可能想要什么”分析到“我应该考虑哪些因素”最后才给出一个工具名。这段 CoT 里 90% 的内容是废话但它实实在在地消耗了推理时间。转折点来自一次和同行的闲聊。他说他们团队早就把 Agent 的推理模式从“生成式”改成了“判别式”通俗讲就是把推理从“写作文”改成“做选择题”。具体做法是不再让模型自由生成一段思考过程再给答案而是把所有候选工具作为选项让模型直接输出最匹配的那个选项的编号或标签。改完之后他们的 Small Agent 单次推理延迟从 2 秒多降到了 200 毫秒以内思考速度快了 10 倍不止。我回去照着这个思路重构了一版实测下来延迟从 2.3 秒降到了 180 毫秒左右而且准确率几乎没有下降。这篇文章就把整个改造过程拆开讲清楚为什么生成式推理慢、选择题式推理快、具体怎么改、改的过程中踩了哪些坑、以及这套思路还能用在哪些场景里。如果你也在做 Small Agent 或者任何对延迟敏感的推理任务这篇应该能帮你省下不少试错时间。2. 生成式推理为什么慢从 Token 消耗到解码机制2.1 一个被忽视的事实CoT 的 token 成本是隐形的很多人做 Agent 的时候习惯性地让模型先输出一段思考过程再输出最终答案。这个习惯来自早期 CoT 论文的启发——让模型“一步一步想”确实能提升复杂推理任务的准确率。但问题在于CoT 的收益和成本是非线性的。对于复杂数学题、多跳逻辑推理CoT 确实有用因为模型需要中间步骤来维持上下文。但对于 Small Agent 的意图识别和工具选择这类任务决策空间其实很小——无非就是从 N 个工具里选一个。这种情况下CoT 带来的准确率提升微乎其微但 token 消耗却是实打实的。我做过一个统计改造前Agent 平均每次推理生成 280 个 token其中 CoT 部分占 240 个最终答案只占 40 个。也就是说85% 的推理时间花在了“思考过程”上而不是“思考结果”上。这就像你问一个人“今天星期几”他先给你背一遍日历算法再告诉你答案——答案是对的但完全没必要。2.2 自回归解码的串行瓶颈要理解为什么生成 280 个 token 比生成 40 个 token 慢那么多得从大模型的自回归解码机制说起。当前主流的生成式模型输出是逐个 token 串行解码的。每生成一个 token都要跑一次完整的前向计算然后把新 token 拼接到输入里再跑下一次。虽然 KV Cache 能缓存历史 token 的键值对减少重复计算但串行解码的本质没有变——你没法并行生成第 2 个 token 和第 3 个 token因为第 3 个 token 依赖于第 2 个 token 的结果。这就导致一个直接后果推理延迟和输出 token 数几乎成正比。输出 280 个 token 的时间差不多是输出 40 个 token 的 7 倍。在 Small Agent 这种对延迟极度敏感的场景里这个差距是致命的。更麻烦的是CoT 的输出长度是不稳定的。同一个意图模型有时候生成 150 个 token 就收尾了有时候能絮絮叨叨生成 400 个 token。这种不确定性让延迟优化变得非常困难——你没法通过简单的缓存或批处理来抹平波动。2.3 Small Agent 的特殊性决策空间小但调用频繁Small Agent 和大型 Agent 的一个重要区别在于它的决策空间通常很小但调用频率很高。一个典型的 Small Agent 可能只负责 10 到 30 个工具的调度每个工具的语义边界相对清晰。这种场景下生成式推理的“灵活性”优势根本发挥不出来反而成了负担。我见过不少团队在 Small Agent 上套用大型 Agent 的架构结果就是杀鸡用牛刀——模型每次都要“深思熟虑”一番但实际需要它做的只是“快速判断”。这种架构错配是延迟问题的根源。注意这里说的“决策空间小”是指候选动作的数量有限不是说任务本身简单。很多 Small Agent 处理的任务语义复杂度并不低只是它的输出空间被约束在了一个有限的集合里。这正是选择题式推理能发挥作用的前提。3. 选择题式推理的核心思路把生成问题转化为判别问题3.1 从“生成答案”到“选择答案”的本质转变选择题式推理的核心思想一句话就能说清楚不让模型自由生成答案而是让模型从预设的候选集中选择最匹配的一个。这个转变看似简单但它改变的是整个推理的计算模式。在生成式模式下模型的任务是“给定输入生成一段文本”。在选择题模式下模型的任务变成了“给定输入和一组候选输出最匹配候选的标识”。后者的输出空间被严格约束模型只需要输出一个短标签比如选项编号、工具名而不需要生成完整的自然语言。这个转变带来的第一个好处是输出 token 数大幅减少。原来需要生成 280 个 token现在只需要生成 1 到 5 个 token取决于选项的编码方式。输出 token 数减少串行解码的步数就减少延迟自然就降下来了。3.2 为什么判别比生成更适合 Small Agent从信息论的角度看生成式推理是在一个巨大的输出空间里搜索而选择题式推理是在一个有限的候选集里做判别。对于 Small Agent 来说候选集是已知的、有限的、封闭的——工具列表在运行时就已经确定了。这种情况下让模型去“生成”一个工具名本质上是在一个已知集合里做生成完全是多此一举。打个比方生成式推理像是让你在一张白纸上写出一个城市的名字选择题式推理像是给你一张地图让你指出城市的位置。前者需要你掌握拼写、语法、格式后者只需要你认得那个位置。对于“从 20 个工具里选一个”这个任务模型显然更擅长后者。另外判别式推理还有一个隐藏优势它天然支持置信度校准。生成式模型输出一段 CoT 后给出的答案你很难判断它有多确定。但选择题模式下模型对每个选项的输出概率可以直接作为置信度使用。当最高概率低于某个阈值时Agent 可以触发兜底逻辑或者请求人工介入这在生产环境里非常实用。3.3 SSR 与流式推理管线的配合这里要提一个和热词相关的概念SSRSpeculative Sampling and Refinement推测采样与精炼。在选择题式推理的框架下SSR 的思路可以这样用先用一个极小的模型或者规则引擎快速给出一个候选排序然后用主模型对这个排序做精炼和确认。因为候选集很小精炼阶段的输出 token 数极少整体延迟可以压得很低。流式推理管线在这里也有用武之地。虽然选择题式推理的输出很短但在多轮 Agent 交互中每一轮的推理结果都需要尽快传递给下游模块。把选择题式推理嵌入流式管线可以让 Agent 在几十毫秒内完成一轮“感知-决策-执行”的循环整体响应速度会有质的提升。4. 具体改造方案从 Prompt 设计到输出解析4.1 候选集的构建与编码改造的第一步是构建候选集。对于工具选择类 Agent候选集就是所有可用工具的列表。但直接把工具名扔给模型效果不一定好因为工具名可能很长、语义不够清晰。我的做法是给每个工具分配一个短编号加简短描述的组合。比如原来工具定义是这样的{ name: search_customer_order_history, description: 根据客户ID查询该客户过去12个月的所有订单记录 }改造后变成{ id: T03, label: 查订单历史, hint: 根据客户ID查询过去12个月订单 }模型只需要输出T03这个短标识而不是完整的工具名。这样做有两个好处一是输出 token 数从原来的十几个降到两三个二是短标识的生成难度更低模型不容易出错。候选集的规模也需要控制。我实测下来候选数在 5 到 30 之间时选择题式推理的准确率最稳定。超过 30 个候选模型的选择准确率会开始下降这时候需要引入分层选择或者检索预筛选。低于 5 个候选选择题的优势不明显直接用规则匹配可能更快。4.2 Prompt 模板的设计要点选择题式推理的 Prompt 设计和生成式有本质区别。生成式 Prompt 通常鼓励模型“详细思考”而选择题式 Prompt 要抑制模型的生成欲望引导它快速做判别。我的 Prompt 模板大致是这样的你是一个工具调度器。根据用户指令从下列工具中选择最合适的一个。 工具列表 T01: 查天气 - 查询指定城市的实时天气 T02: 查汇率 - 查询两种货币之间的实时汇率 T03: 查订单历史 - 根据客户ID查询过去12个月订单 ... 用户指令{user_input} 要求 1. 只输出工具编号不要输出任何其他内容 2. 如果有多个工具都合适选择最具体的那一个 3. 如果没有合适的工具输出 NONE 输出这个模板有几个关键设计点。第一明确要求只输出编号不给模型任何生成多余内容的空间。第二给出选择优先级规则“选最具体的”减少模型在边界情况下的犹豫。第三设置 NONE 选项让模型在无匹配时有明确的出口避免强行选择一个不相关的工具。实操心得在 Prompt 末尾加上“输出”这个前缀能显著降低模型输出多余内容比如“答案是T03”或者“我选择T03”的概率。这个技巧在多个模型上都验证过效果很稳定。4.3 输出解析与容错处理选择题式推理的输出解析比生成式简单得多但也不能掉以轻心。模型有时候会输出T03有时候会输出t03有时候会输出T03。或者T03\n。解析逻辑需要做标准化处理去空格、去标点、转大写然后再匹配候选集。如果解析失败或者输出不在候选集里需要有兜底策略。我的做法是第一次解析失败时用正则从输出里提取所有可能的编号取第一个匹配的如果还是失败直接返回 NONE触发上层兜底逻辑。这套策略在实际运行中解析失败率低于 0.1%。另外置信度阈值也很重要。选择题式推理可以拿到模型对每个选项的输出概率当最高概率低于 0.6 时我会让 Agent 走“澄清”流程——反问用户或者请求更明确的指令而不是强行执行一个可能错误的工具。5. 性能实测从 2.3 秒到 180 毫秒的完整记录5.1 测试环境与基准设置为了验证改造效果我搭了一套对比测试。硬件环境是一台配备 16GB 显存的单卡服务器模型用的是同一个 7B 参数量的指令微调模型。测试集包含 500 条真实用户指令覆盖 20 个工具的调度场景。基准组是改造前的生成式 CoT 推理对照组是改造后的选择题式推理。两组使用相同的模型权重、相同的温度参数0.1、相同的最大输出长度限制生成式 512 token选择题式 16 token。每组跑 3 轮取平均延迟和准确率。5.2 延迟对比10 倍差距的来源拆解实测结果如下表所示指标生成式 CoT选择题式变化平均输出 token 数2783.2-98.8%平均推理延迟2310ms182ms-92.1%P99 延迟4180ms310ms-92.6%工具选择准确率94.2%93.6%-0.6%解析失败率0.3%0.08%-73%延迟从 2310 毫秒降到 182 毫秒降幅 92.1%接近 13 倍。这个提升主要来自输出 token 数的减少——从 278 个 token 降到 3.2 个 token串行解码步数减少了 98.8%。剩下的延迟主要是输入编码和 KV Cache 构建的开销这部分在两种模式下是一样的。准确率只下降了 0.6 个百分点从 94.2% 降到 93.6%。这个差距在统计上不显著而且通过调整 Prompt 和候选集编码方式还可以进一步缩小。解析失败率反而降低了因为选择题的输出格式更规范解析逻辑更简单。5.3 准确率有没有下降边界案例分析准确率下降的 0.6 个百分点主要集中在两类边界案例上。第一类是语义模糊的指令比如“帮我看看那个东西”生成式模型通过 CoT 可能会推理出用户大概率指的是某个工具而选择题式模型因为没有“思考空间”更容易直接选 NONE。第二类是多工具都合理的场景生成式模型可能会在 CoT 里权衡后选一个选择题式模型有时候会选另一个。针对这两类问题我做了两个优化。一是在候选集里增加同义描述让语义模糊的指令也能匹配到某个工具。二是在 Prompt 里增加少量示例Few-shot展示边界情况下应该怎么选。优化后准确率回升到 94.0%和生成式基本持平。注意选择题式推理的准确率高度依赖候选集的质量。如果候选集的描述本身就不清晰模型再怎么选也选不对。所以在改造之前先把工具描述打磨一遍收益会非常明显。6. 踩过的坑与排查技巧实录6.1 模型不听话输出多余内容的三种情况改造初期最常遇到的问题就是模型不按格式输出。明明要求“只输出编号”模型偏偏要输出“根据用户指令最合适的工具是T03”。这种情况在指令微调不充分的模型上尤其常见。我的解决办法是三层防御。第一层是 Prompt 层面把格式要求放在最显眼的位置并且用“只输出”“不要输出任何其他内容”这种强约束措辞。第二层是停止符Stop Token层面把换行符和句号设为停止符模型输出到这些符号就截断。第三层是解析层面用正则提取编号忽略多余内容。三层防御下来格式违规率从最初的 8% 降到了 0.1% 以下。其中停止符的作用最大因为它是在解码阶段就截断了不依赖模型“听话”。6.2 候选集顺序的玄学影响这是一个我在文档里没看到过、但实测确实存在的现象候选集的排列顺序会影响模型的选则偏好。把某个工具放在列表第一位模型选择它的概率会略高于放在最后一位。这个偏差在候选数少的时候不明显但候选数超过 15 个之后首尾位置的偏差能达到 3 到 5 个百分点。解决办法有两个。一是随机打乱候选集顺序每次推理时重新排列让位置偏差在统计上被平均掉。二是把最常用的工具放在中间位置减少位置偏差对高频工具的影响。我采用的是第一种方案实现简单效果稳定。6.3 常见问题速查表问题现象可能原因排查方法解决方案输出不在候选集里模型幻觉或格式违规打印原始输出检查加强格式约束增加解析容错延迟没有明显下降输入过长或 KV Cache 未命中检查输入 token 数和缓存命中率精简 Prompt启用前缀缓存准确率骤降候选集描述不清或顺序偏差对比改造前后错误案例优化工具描述随机打乱顺序模型总是选 NONE阈值过高或 Prompt 过于保守检查置信度分布降低阈值增加 Few-shot 示例多轮对话中表现不稳定上下文污染检查历史消息是否混入候选集每轮重置候选集隔离上下文6.4 一个容易被忽视的细节温度参数生成式推理通常用较高的温度0.7 到 1.0来增加多样性但选择题式推理应该用极低的温度0.0 到 0.1。因为选择题需要的是确定性输出不是创造性输出。温度调高之后模型在边界案例上的选择会变得随机准确率明显下降。我实测过温度从 0.0 到 1.0 的影响温度 0.0 时准确率 93.6%温度 0.5 时降到 91.2%温度 1.0 时只有 87.4%。所以选择题式推理的温度参数直接拉到最低就对了。7. 这套思路还能用在哪里选择题式推理不只适用于工具调度。任何输出空间有限且可枚举的 Agent 任务都可以考虑这种模式。比如意图分类、情感判断、路由决策、参数填充这些任务的共同特点是候选集已知不需要模型自由生成。我后来把这套思路用到了一个客服 Agent 上把“判断用户情绪”从生成式改成了选择题式选项是“满意/中性/不满/愤怒”延迟从 1.8 秒降到了 150 毫秒准确率还略有提升。另一个场景是 RAG 里的文档相关性判断把“生成相关性评分”改成“从高/中/低三个选项里选”效果同样很好。甚至在一些看起来需要生成的任务上选择题式推理也能用。比如让 Agent 生成一段回复可以先让模型从几个预设的回复模板里选一个再让另一个模型或者规则引擎做填充。这样虽然牺牲了一些灵活性但换来了数量级的延迟下降在很多场景下是划算的。实操心得判断一个任务适不适合改成选择题式问自己一个问题——这个任务的输出能不能枚举完如果能就值得试如果不能就老老实实用生成式。别为了追求速度把该生成的任务硬改成选择题那样准确率会崩。最后分享一个小技巧在改造过程中保留一份生成式推理的“影子模式”让它在后台继续跑和选择题式的结果做对比。这样既能监控准确率变化又能在选择题式出错时快速定位是候选集问题还是模型问题。这个影子模式在我改造期间帮我发现了至少三个候选集描述不清的 bug强烈建议你也试试。