资讯动态

把Agent决策从自由生成改成做选择题:延迟降低近10倍的改造实录

发布时间:2026/10/13 8:36:27 来源:尧图企业网站定制
标题里这句话是我一个多月前改完某轻量级 Agent 之后在群里说的原话。当时那个小 Agent 的主要任务是做意图路由和工具选择——用户进来一句话它得判断该调哪个 API、该走哪条业务流。一开始它表现并不差准确率能看就是慢慢得用户都去倒了杯水回来它还没回话。我一度以为是模型太小、算力不够后来把推理的输出方式从自由生成改成做选择题端到端延迟直接掉了近一个数量级而且响应稳定了很多。这篇就把整个改造思路、踩过的坑和实测数据摊开讲清楚。这个方案适合谁如果你也在做轻量级 Agent、垂直领域的对话分发、工具调用的路由层或者你手上有个模型推理延迟压不下去的小服务这篇应该能帮你省掉不少弯路。核心就一句话很多看似需要思考的决策本质是分类问题而分类问题根本不需要让模型写一大段话。1. 生成式推理的账本10倍差距到底浪费在哪1.1 自回归解码的一次次“许愿”先别急着聊优化得算清楚原来那套为什么慢。所有现代对话模型都是自回归解码它每生成一个 token都要拿已经生成的整段内容去跑一次前向计算然后从词表里挑一个概率最高的 token或者按温度采样一个。你可以把它理解成一个人在玩接龙——每说一个字都得回忆一遍前面所有字才能决定下一个字说什么。这带来的直接后果是输出越长计算量线性增长而且每一步都不能并行至少原生解码是这样的。一个模型每秒能吞吐多少 token基本上决定了你的延迟天花板。我当时用的那个轻量模型单机部署解码速度大概每秒三四十个 token听上去不算慢但架不住它每次都要输出一大段分析过程。我抓过它一次实际请求的返回光是思考部分就写了 200 多个 token先复述了一遍用户问题再说用户在询问订单状态因此我应该查询订单接口最后才给一个 JSON 格式的动作。整个链路下来端到端两三秒是常态高峰期能到四五秒。用户那头的体感就是这机器人是不是卡死了。1.2 真正浪费的不是 tokens而是“自由”有人可能会说那我把 system prompt 改成不要输出多余内容直接给动作不就行了吗我试过有效果但是很有限。原因有二。一是模型在自由生成模式下即便指令再明确它也会时不时把推理过程带出来尤其是面对没见过的表述时话痨属性根本压不住。二是就算它严格只输出一个动作词序列长度从 200 降到 20延迟依然要几百毫秒——因为每个 token 都得过一遍完整的前向。深层的问题在于自由本身。生成模式每一步都在全词表里挑 token词表通常是几万甚至十几万大小。模型需要在这么大的空间里自由发挥而我们的业务场景里它明明只需要从 8 个固定动作里选一个。这就像一个学生参加考试明明全是选择题你却让它每道题都写一篇小作文来论证答案。时间花在哪了全花在组织语言上了而不是在思考选哪个上。1.3 10倍是怎么算出来的延迟加减法我这边实测过一组数改造前和改造后的对比先把账算明白。环节改造前自由生成改造后选择题首 token 延迟200~400ms200~400ms不变输出长度150~300 tokens1 token或几个候选共 1 次打分解码耗时3~8 秒20~80ms端到端 P50约 2.7s约 0.31s端到端 P95约 5.2s约 0.42s首 token 延迟没有变化因为这部分取决于输入长度和模型前向计算跟输出方式无关。真正的差距全在解码长度上从一两百个 token 压到 1 个 token时间自然就省下来了。P95 改善尤其明显——自由生成时偶尔会话痨爆发写出 400 多个 tokenP95 直接被拖到 5 秒开外选择题模式下输出长度被锁死怎么飘都飘不到哪去。所以 10 倍不是玄学就是延迟加减法把生成从写作文变成打勾省掉的不是思考时间而是把思考结果翻译成人话的时间。2. 从写作文到打勾Small Agent 的决策机制改造逻辑2.1 决策的本质是分类不是作文想通一个问题整个改造方向就清晰了Agent 在路由、工具选择、意图识别这类场景里核心任务是把输入映射到一个离散动作集合。这个过程本质是分类跟写一段自然语言分析没有关系。我打个比方。你去银行柜台办业务柜员问你办什么你说我要查一下卡里余额。柜员脑子里的处理流程是这句话匹配到查询余额这个业务 → 引导你去自助机 → 结束。他不会先给你写一篇根据您的需求我判断您属于查询余额类别理由是……的作文再办事。模型也一样。让它把内心的分类结果直接暴露出来而不是先包装成一段话速度和稳定性都会上一个台阶。你可能会担心那模型的推理过程去哪了答案是在参数里。一个训练充分的模型面对常见意图时该选哪个这个判断在前向计算时就已经有了倾向根本不需要靠输出文字来思考。2.2 选择题的三种实现姿势硬限流、logprob 判分、编号输出把做选择题落到工程上有三种常见姿势我按推荐程度排个序。第一种是硬限流也叫受限解码。如果你用的模型服务支持 logit bias 或者自定义解码策略可以直接把词表限制在候选答案对应的 token 集合上让模型只能在选项里挑。这种方式最干净输出天然合法不会出现模型选了个不在选项里的东西的尴尬。缺点是不少服务不支持或者说配置起来比较麻烦。第二种是 logprob 判分也是我最终采用的方案。具体做法把每个候选选项转成一个固定的答案 token 序列然后让模型服务返回这些候选序列的打分logprob取分数最高的那个作为决策结果。这个过程不需要模型真正写出答案而是让它同时评估所有选项哪个更可能被接在后面。第三种是编号输出让模型输出选项的编号比如3然后程序里做映射。实现最简单兼容性最好但有一个隐患模型偶尔会输出3。或者选项3这种带格式的内容解析时得做清洗而且相比前两种多了解析和容错的成本。这个方案适合当作降级通道不建议当主通道。2.3 为什么“做选择题”比“少写几个字”快更多这里有个容易误解的地方我得说清楚选择题模式省时间的原理不只是输出短而是解码步骤变少。自由生成哪怕你让它只输出一个词它也是一步一步解码的先预测第一个 token再预测第二个如果有的话。而 logprob 判分方案是一次前向计算同时拿到所有候选序列的分数没有逐 token 拼接的过程。相当于阅卷老师直接看每个学生的最终答案而不是等学生把答案一个字一个字念出来。所以你会看到一个反直觉的现象选项有 8 个还是 16 个对延迟几乎没影响。因为打分是并行的候选数量增加带来的只是排序那点开销。真正影响延迟的还是输入长度和模型大小。这也是为什么这招在小模型上效果尤其显著——小模型解码速度本来就不快把解码步数压到 1等于直接砍掉了最大的延迟来源。3. 实操改造链路选项设计到降级策略3.1 出题比答题更重要选项互相排斥、可判定把决策变成一个选择题第一步不是写代码而是出题。出题出得烂模型再聪明也没用。我总结了一套选项设计的五条原则踩过不少坑之后沉淀下来的。互斥性任意两个选项不能有语义重叠。比如查询库存和查询商品信息就很危险模型可能觉得两个都对分数被摊薄。穷尽性所有常见输入都要有落点。你预判不到的输入怎么办那就得有以上都不是或者转人工这种兜底选项。可区分性选项描述要具体到模型能一眼看出差别最好每个选项绑定一个明确的执行动作。比如查库存后面跟的是库存 APIdescription 里写清楚触发条件和输出字段。数量克制选项数量建议控制在 16 个以内。超过这个数模型对相似选项的区分度会明显下降准确率跟着崩。短描述优先选项文本越短模型打分越稳定。长描述会导致模型抓不住重点而且 logprob 的长度归一化问题会更难处理。我自己处理过一个反面教材某个场景里改地址和改收货信息两个选项并存模型经常把改地址的请求判到改收货信息上准确率掉了 5 个点。后来把两个选项合并成修改订单信息准确率立刻回来了。原则就一条在你把选择题交给模型之前先问自己一句——这道题让一个刚入职的实习生来做会不会有歧义如果有改题目不要指望模型替你消歧。3.2 用 logprob 还是第一个 token评分要小心确定了选项之后接着要解决怎么打分的问题。这里坑特别多我一个个说。首先是序列 logprob 和首 token logprob 的区别。有些模型服务只返回每个位置 token 的概率不直接给整句序列的分数。你要是拿第一个 token 的 logprob 当选项分数遇到查余额和查订单这种首字相同的选项就废了——它们第一个 token 可能都是查分数几乎一样区分不出来。稳妥的做法是给每个选项定义一个短小的标准答案序列比如查余额然后用整个序列的 logprob 求和或平均来排序。其次是长度归一化。两个选项长度不一样比如查余额3 个 token和转人工3 个 token还好但遇到查询最新促销活动8 个 token和退差价3 个 token这种长序列的 logprob 天然更小因为每一步概率都小于 1连乘起来越来越小。所以要么统一选项长度要么对 logprob 做长度归一化。我偷懒的做法是尽量把所有选项描述写成差不多的长度。再一个是温度和采样参数对打分的影响。打分阶段我有意把温度调低接近 0让输出更确定。如果你的服务不支持按请求设置采样参数那就保持默认并接受一点随机性但要在下游加一层阈值保护下面讲。3.3 阈值、兜底和把“不会做”也当成选项选择题模式最大的隐患是模型会硬选。就算它根本没看懂用户说什么只要候选列表在那儿它也会给你挑一个分数最高的。这是所有分类式方案的共性问题解决办法是加阈值和兜底。我在工程上做了两层保护。第一层每个选项都是带条件的动作条件不满足时模型不该选它——但这依赖模型自觉不保险。第二层才是关键显式地加一个无法判断/转人工选项并且要求模型分数达到一定水平才允许走自动流程否则一律转人工。分数阈值怎么定我建议先跑一两周无阈值模式把所有请求的分值分布存下来然后取 top1 概率的分位数来定。比如你希望最多 5% 的请求落到拿不准区域就看看这 5% 分位的概率值是多少把它设成阈值。阈值本身的写法也有讲究。我见过有人用top1 绝对概率大于 0.5这个对二选一还行但对 8 个选项的场景太苛刻。更合理的做法是看 top1 和 top2 的差距如果第一名只比第二名高一点点说明模型也在犹豫这时候转人工比硬猜靠谱。我在生产里是两者结合top1 概率低于 0.35 或者 top1 与 top2 差值低于 0.1就触发兜底。3.4 改造代码骨架一条从上到下的管线最后给个代码骨架算是把前面几节串起来。我用的是 Python 风格的伪代码方便你迁移到自己的工程里。# 选项定义description 是给模型看的action 是给业务用的answer_tokens 是评分用的标准序列 OPTIONS [ {name: lookup_order, desc: 查询订单状态和物流信息, answer_tokens: [1132, 4587]}, {name: check_stock, desc: 查询商品库存情况, answer_tokens: [1204, 3789]}, {name: calc_shipping, desc: 计算商品运费, answer_tokens: [1567, 3345]}, {name: human_handoff, desc: 无法判断转人工处理, answer_tokens: [2089, 6012]}, ] def select_action(user_input: str) - str: # 1. 请求模型服务对每个候选 answer_tokens 打分 scores model.score_candidates( promptbuild_prompt(user_input, OPTIONS), candidates[opt[answer_tokens] for opt in OPTIONS], temperature0.1, ) # 2. 长度归一化后排序 normalized [] for opt, score in zip(OPTIONS, scores): norm_score score / len(opt[answer_tokens]) normalized.append((opt, norm_score)) normalized.sort(keylambda x: x[1], reverseTrue) top1, sc1 normalized[0] top2, sc2 normalized[1] # 3. 阈值保护犹豫了就转人工 if sc1 0.35 or (sc1 - sc2) 0.10: return human_handoff return top1[name]这段代码的核心并不在具体语法而在三个决定候选怎么定义、分数怎么归一化、阈值怎么卡。你把这三个决定做对了适配自己的模型服务和业务场景就只是改配置的问题。4. 实测数据从 2.7s 到 0.31s 的每一步都花在哪4.1 一个典型的场景让它自己选该调哪个接口为了不让数据显得太抽象我说一下具体的测试场景。这是一个订单客服场景的轻量 Agent输入是用户的一句话输出是一个动作。动作候选有五个查订单、改地址、算运费、退货申请、转人工。改造前模型的 system prompt 写的是请判断用户意图并输出 JSON格式为 {action: xxx}不要输出其他内容。看着挺严格吧实际跑起来模型还是会絮叨。有一回用户说我地址写错了帮我改一下它输出的 JSON 前面还垫了一句好的我来帮您处理地址修改问题。就这一句废话多了十来个 token每多一个 token 都是在烧用户的耐心。改造后我把同一个请求送进 logprob 打分接口五个候选选项同时计算分数。结果非常干净改地址这个选项的归一化分数明显领先第二名一大截动作直接落到update_address。整条链路从用户输入到拿到动作耗时 300ms 上下。注意这里还没算下游真正执行 API 的时间但已经把 Agent 自身的思考成本压缩到了极限。4.2 延迟细项对照表钱花在哪了一目了然我把一次典型请求的耗时拆开来看这样你就能明白 10 倍从哪来的。阶段改造前耗时改造后耗时备注网络传输与排队80ms80ms不变输入 prefill180ms180ms由输入长度决定解码首 token120ms120ms不变后续解码3300ms0ms这是大头解析与后处理20ms10msJSON 解析变成数字比较合计约 3.7s约 390ms其中含部分固定开销可以看到prefill 和首 token 的耗时一点没省——这部分由模型大小和输入长度决定跟输出方式无关。我们省的纯粹是后续解码那段。在没有模型服务日志之前我一度以为延迟都花在模型思考上了拆完才发现所谓的思考在计算上只占那么一点绝大多数时间都耗在把思考结果一个字一个字念出来。4.3 稳定性P95、失败率和 token 账单一起看延迟中位数降低是意料之中更让我惊喜的是 P95 的改善。自由生成模式下模型偶尔会抽风写出一大段解释直接把那次请求拖到五六秒。选择题模式把输出长度上限锁死P95 几乎贴着 P50用户体感非常稳定。这不光是体验问题还直接影响下游超时设置。以前超时阈值得留到 10 秒才能保证不被误杀现在设 1 秒都绰绰有余下游服务的压力也小了很多。token 账单的变化更是直接。改造前一次请求平均输出 180 个 token高峰期能到 300 多改造后每请求只消耗 1 个 token 的生成量打分本身可能算在输入侧但输出侧基本可以忽略。按一天十万次请求算输出 token 成本直接省掉了 99%。这对于自部署的模型来说就是算力释放对于按量付费的 API 来说就是账单上的真金白银。准确率方面我在一万条带标注的历史数据上做了回测改造前意图分类准确率 91.2%改造后 93.6%。提升了原因我猜是选择题的选项描述比自由生成的 prompt 更聚焦模型不用边猜格式边猜意图。当然这个提升幅度不一定能完全复制到你的场景但至少说明做选择题不是拿准确率换速度——很多时候是两头都赚。5. 这招的边界什么时候该回去用“生成”5.1 选项覆盖不足就像考卷出了考纲外的题选择题方案有一个天然前提你的业务场景可以枚举。如果用户的输入五花八门而且新的意图随时可能出现那么选项列表就永远覆盖不全。覆盖不全的后果不是模型选一个错误的选项这么简单而是模型会硬凑——把一段完全没见过的话塞到某个语义最接近的选项里然后下游拿着错误动作执行比我不知道危害大得多。我踩过一次这样的坑。某个边缘场景里用户说你们这个包裹是不是丢在驿站了我的选项里只有查物流和转人工。模型实在看不出这两个哪个合适最后以微弱优势选了查物流下游真的去调了物流接口返回的却是无此运单用户收到个乱码式的回复。如果当时有无法判断的选项且阈值设得够狠这次请求本来就该转人工。所以我的建议是选项列表不是一成不变的至少每月要复盘一次未命中/转人工的记录看有没有高频的新意图值得单独开一个选项。选择题方案做得好不好长期看取决于你的选项列表迭代快不快。5.2 长尾输入和选项爆炸当选择题比作文还难出另一个边界是选项数量。我前面说控制在 16 个以内这是有代价的——它逼着你把细粒度的意图合并成粗粒度的动作。如果你的业务确实需要 50 个不同的动作那选择题方案会迅速劣化选项之间相似度太高模型打分区分度不足准确率暴跌而且阈值调不准。这种情况下我建议换个思路先做一个粗粒度路由比如 10 个以内的粗分类命中后再做第二层细分又是不超过 10 个的细选项。两级选择题比一级 50 选 1 准确率高得多而且每一级的延迟都是可控的。代价是两次请求但总延迟依然远小于写一篇 200 token 的作文。还有一类输入不适合选择题开放式的创作型任务、需要引用具体数据的问答、以及那些答案本身就不在预定义集合里的需求。遇到这种老老实实回到自由生成别硬套。5.3 复杂推理还是会想给你看选择题不适合什么说到底做选择题适合的是决策型任务——输入映射到一个离散动作。但如果是推理型任务比如数学应用题、多步逻辑判断、代码生成强制让它做选择题等于把推理过程压进一个窄小的输出空间模型反而容易出错。这里有个隐约的趋势很多模型的推理能力是通过链式思考体现的你把输出管死它可能不会默默思考再给答案而是直接把思考过程丢掉导致答案质量下降。这是我实测中确实碰到过的问题——在一个多步判断场景里选择题方案的准确率比自由生成低了 7 个点。所以我的判断标准很简单如果这道题选哪个本身就能代表答案用选择题如果答案必须包含过程、推导或新内容别用。那有没有中间态有。可以保留自由生成但用结构化输出约束它只输出最终结论或者让模型分两步走先自由生成简短的中间结论再把它映射到选项。这种混合模式复杂一些适合那些介于分类和开放性之间的场景。我的经验是先按最极端的纯选择题试跑一版如果准确率不行再往外松绑比反过来做要省时间。6. 落地这个方案后我愿意提醒你的几件事最后说几个我在实际落地过程中觉得特别容易被忽略的点都是拿试错换来的。第一个是选项的标准答案序列要跟模型的 tokenizer 对齐。我一开始图省事直接用中文文本当 answer_tokens结果发现不同 tokenizer 对同一句话的分词结果不一样归一化之后分数被搞乱了。建议先用模型服务的编码接口把每个选项描述转成 token id存起来不要运行时现转。第二个是打分接口的并发。选择题方案省了生成时间但如果你把请求都积压在打分接口上延迟优势会被排队吃掉。务必确认你的模型服务支持批量打分并且做到多个候选在同一次请求里返回。如果服务不支持批量候选打分这方案的价值至少打对折你可能得考虑换服务或自己包一层并行请求。第三个是日志里一定要留下top1 概率和top1 与 top2 的差值这两个字段。一开始我只记了最终动作后来调阈值时根本没有历史数据可参考只能重新跑回归。加上这两个字段之后每次阈值调整都能基于一周的真实分布来定省了太多事。第四个是选项描述跟用户真实用词之间的翻译成本。模型很擅长从口语映射到书面选项但前提是选项描述本身覆盖了足够的同义表达。我建议把高频用户原话直接揉进选项描述里——比如改地址的选项描述写成修改收货地址、更改配送信息效果比只写地址修改好不少。这套选择题化的改造坦白说不是什么高深技术它只是把该给模型出的题出对了。但很多 Agent 项目恰恰就死在这一点上让模型用最昂贵的方式做最廉价的事。如果你现在手上也有一个慢吞吞的轻量 Agent不妨先别急着换大模型、加算力试着把它的决策层改成选择题试试说不定你也能在群里甩出一句快了 10 倍。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑