最近在整理内部Agent系统的手记,写到第8章,正好把CPU-Agent与混合调度这套方案沉淀一下。事情的起因很直接:我接手的是一个售后文档问答Agent,业务逻辑不复杂,真正让人头疼的是账单和排队。工单量上来之后,云上大模型推理费用每个月都在涨,高峰期接口慢到前端一直转圈。后来我把链路拆开看,发现大量请求根本不需要“深度思考”,真正需要云上模型出手的可能只有三成。这就是CPU-Agent与混合调度这套写法的来由。这篇文章会从为什么值得做、本地模型能干什么、调度层怎么设计,到生产环境的选型参数和踩坑记录,一次性讲透。适合正在用云端模型API组合Agent、对成本和延迟敏感的工程师,也适合想把一部分Agent能力搬回本地的团队参考。
1. 为什么不是“全上大模型”:CPU-Agent出现的逻辑起点
在动手之前,我先花了几天把当时的推理账单和调用链拉出来做了次复盘。不看不知道,所有问题其实都指向同一个事实:把整个Agent链路一股脑交给云端大模型,钱和延迟都被放大了好几倍。
1.1 一条工单请求,被云端模型吃了五道“过路费”
拆开一条典型的售后工单,处理过程通常不只是一个“问一句答一句”,而是由好几个子任务组成:先把用户留言做意图识别,再从里面抽订单号、商品型号,然后去知识库检索相关政策,最后生成回答,有时候还要判断要不要转人工。我当时数了一下,最复杂的一条链路前后要调用五轮云端模型。
每一轮调用都在花钱。按当时的量粗算一下,单次问答请求输入约1200个token,输出约300个token,按输入每百万token 2元、输出每百万token 8元的市场价折算:
单请求成本 = 1200 * 2 / 1,000,000 + 300 * 8 / 1,000,000 = 0.0024 + 0.0024 = 0.0048 元单看这0.0048元好像不贵,但一个月10万次请求就是480元。如果一条工单内部要调用两次或三次模型,这个数字直接翻倍甚至翻三倍。再加上向量检索、对象存储、日志链路等配套费用,月度账单很难看。
这里的关键不是“云端模型太贵”,而是“使用方式太粗暴”。Agent链路里有一部分任务是纯结构化的,比如识别这句话属于“退货咨询”还是“物流异常”,这类任务不需要模型生成一段漂亮话,只需要它选对分类。拿云端大模型做这件事,相当于每次请一位高级顾问来帮你做填空题,钱花得很冤。
1.2 延迟和可用性:比账单更麻烦的体验问题
账单只是痛点之一,真正让业务方忍不住吐槽的是延迟。晚高峰时段,云端接口的P95延迟经常飙到3秒左右;遇到平台波动,还会随机出现超时和5xx错误。对用户来说,多等两秒已经很难受了,更别说页面一直显示“正在输入”然后突然断掉。
我当时的判断是:像“意图识别”“实体抽取”“路由判断”这些环节,根本不该承受云端网络的波动。这些任务本身是确定性的,用本地模型跑,延迟是可控的。实测下来,一个0.5B到3B量级的模型在普通CPU上做分类,单请求延迟在100到600毫秒之间,P95非常稳。也就是说,把一部分环节从云端迁到本地,用户体验能肉眼可见地变好。
1.3 CPU-Agent不是要替代大模型,而是给大模型让路
这也是我在这个项目里想明白的一件事:CPU-Agent并不是“用一个跑在CPU上的大模型去硬扛所有流量”,而是把本地CPU能承载的多个轻量模型编排成一些固定任务的Agent,让它们负责那些“熟能生巧”的环节;混合调度则是更上层的总控,负责判断一条请求应该走本地、走云端,还是走本地和云端协作的中间路径。
一句话概括:能确定性完成的事情,不要让大模型做;需要真正思考的事情,不要让小模型硬扛。大模型的强项是语言理解、生成、多步推理,它的成本高、延迟大,所以应该被用在刀刃上。CPU-Agent存在的意义,就是把大模型从那些低价值请求里解放出来,让它的每一次调用都“值得”。
2. CPU-Agent的真实能力边界:哪些活儿能本地干,哪些必须上云
CPU-Agent听起来很美好,但它不是万能的。我在最开始踩过一个大坑:以为本地小模型也能像云端大模型一样自由发挥,结果是答非所问、逻辑混乱,用户反馈直线下降。后来我把任务分成两类来看,边界就清晰多了。
2.1 判断型任务与生成型任务的分水岭
判断型任务的特点是输出空间有限、答案可枚举,比如“这句话属于哪个意图”“这段话里有没有订单号”“这个问题该走流程A还是流程B”。这类任务本质上是分类和抽取,模型不需要“想”出什么新内容,只需要在给定选项里选一个,或者在给定文本里把关键片段找出来。0.5B到3B量级的模型,配合少量标注数据微调,在这类任务上能达到相当高的准确率。
生成型任务则完全不同。它的输出是开放的,可能是一段解释、一个方案、一句安抚话术,甚至是一段需要结合多个知识点推理出来的长答案。这类任务对模型的参数量、知识储备和推理能力要求都高,CPU上跑的小模型确实力不从心。
用生活里的场景类比:判断型任务像保安验票,乘客拿什么票、走哪个通道,当场就能判定;生成型任务像导游讲解,需要根据现场情况临时组织语言、串联知识点,普通人干不了。CPU-Agent适合当那个“保安”,不适合硬充“导游”。
2.2 我们保留在CPU侧的三个子Agent
明确边界之后,我把能本地化的环节都做了一个小Agent。目前线上保留在CPU侧的主要有三个:
| 子Agent | 模型量级 | 任务 | 实测效果 | P95延迟 |
|---|---|---|---|---|
| 意图分类Agent | 0.5B级 | 识别用户留言属于哪类售后意图(40+类别) | 准确率96.8% | 95ms |
| 结构化抽取Agent | 0.5B级 | 从留言中抽订单号、商品型号、问题类型 | 字段级F1约0.94 | 120ms |
| 检索路由Agent | 3B级 | 判断问题走静态知识库还是人工介入 | 路由正确率98.2% | 380ms |
这三个Agent组合起来,已经能完成“读懂工单、抽关键信息、决定走哪条路”这个前半段。真正需要云端模型动的,只剩下后半段的“生成最终回答”和“复杂多轮对话”。
我特意没让本地模型碰“解释性回答”这个环节。因为一旦让0.5B模型自由生成,它会把知识库里的内容揉得乱七八糟,语气还很自信。与其让它生成一段不可控的答案,不如让它只负责选路径、抽字段,把生成工作牢牢锁在云端。
2.3 边界不是静态的:上下文长度与知识时效的影响
能力边界还有一个动态因素:上下文长度和知识时效。
当用户对话超过五轮,或者问题里包含长尾商品名、错别字、方言表达时,小模型的判断准确率会明显下降。我们拿线下测试集跑过,普通问题准确率能到96%以上,但换成长对话场景直接掉到91%左右,大概有5%的请求会被分错意图。
更麻烦的是知识时效问题。本地小模型的知识来自训练数据,你训练时它不知道的事情,它无论如何也“编”不出来。比如某个新政策上线第三天,用户问“现在关税怎么算”,模型如果靠参数记忆回答,给出的就是训练时的旧政策。这个问题无解,所以后来我定了一条铁律:凡是事实性、时效性问题,一律不依赖小模型的参数记忆,必须走检索加云端生成这条路。
3. 混合调度总控:一条请求进来后的完整决策链
CPU-Agent和云端模型都准备好了之后,关键就在于把它们串起来。这也是“混合调度”这个名字的真正含义:不搞静态分流,而是由一个总控层根据实时情况动态决定每条请求走哪条路。
3.1 三档路径设计:L1本地直答、L2半程上云、L3全量上云
我把请求拆成三档路径,每一档对应不同的成本和体验:
- L1本地直答:请求命中高频FAQ或标准化售后问题,本地Agent直接从固定知识条目中匹配答案,不调云端。这类请求占我们总流量的35%左右,P95延迟150毫秒,成本几乎为零。
- L2半程上云:本地Agent先做意图识别、结构化抽取和检索召回,把压缩后的上下文(关键槽位加检索段落)发给云端模型,云端只做最终生成。这是性价比最高的一条路,也是大部分用户问题的归宿,占50%左右。
- L3全量上云:遇到复杂纠纷、超长对话、需要跨多篇文档理解的问题,本地只加一个路由标记,直接交给云端模型全权处理。这类请求只占15%,但消耗了大部分云端token支出。
L2是我认为最有设计价值的一条路径。它把“读懂问题”和“组织答案”拆给了不同的执行者:本地模型读问题、找资料,云端模型只负责“看着资料说话”。这样云端模型的输入被压缩了,token消耗大幅下降,同时回答质量基本不受影响。
3.2 路由判定不是if-else:置信度、熵与规则三合一
混合调度最核心的部分是路由判定。我在最开始写过一版用关键词做规则的if-else路由,结果被现实教育了:用户说话方式千奇百怪,关键词规则根本覆盖不了。
后来改成三层判定:先跑规则层,比如命中黑名单关键词、来自特定渠道、用户被标记为高风险,直接走相应路径;规则层没结论的,进入模型层,用意图分类Agent和检索路由Agent给出置信度;最后再加一层启发式兜底,解决模型“犹豫不决”的情况。
伪代码大概是这样的:
def route_request(request, top_intents): # 第一层:规则 rule_result = check_rules(request) if rule_result: return rule_result # 第二层:模型置信度 top1 = top_intents[0] top2 = top_intents[1] # 最高置信度足够高,且和第二名差距明显 if top1.score >= 0.9 and (top1.score - top2.score) >= 0.15: return "L1" # 中等置信度,交给L2:本地检索,云端生成 if top1.score >= 0.7: return "L2" # 模型自己都不确定,直接全量上云 return "L3"阈值不是拍脑袋定的。0.9以上配合0.15的差距,表示模型对判断“非常有把握”,此时走L1相对安全;低于0.7说明模型已经不太确定了,这时候如果执意走本地,事实性错误的比例会明显上升。调参过程中我发现,宁可在L3上多花点钱,也不能让小模型硬撑。
3.3 反馈回流:让路由越用越准的关键机制
混合调度和静态网关最大的区别,在于它有一个持续学习的回流机制。
线上每天会产生大量反馈信号:用户点踩、客服介入、用户二次追问、问题没有得到解决等。我把这些信号统一收集起来,路由层会记录每条请求的路径、模型输出、反馈结果,按天聚合生成一张“路由学习表”。每个星期,我会从表里抽出两三百条“被识别错误”的困难case,补充标注后灌回本地模型的训练集,重新微调一轮。
这个机制跑起来之后,效果是肉眼可见的。最开始L1直答的准确率只有94%左右,三个星期之后涨到了96.5%,路由抖动也少了。反馈回路让CPU-Agent和云端模型之间不是“一刀切”的关系,而是一个可以自我迭代的正循环。
4. 从原型到生产:模型选型、推理引擎与关键参数
原型跑通之后,真正要花力气的是把方案变成生产级服务。这里涉及三个问题:模型选哪个、推理引擎怎么配、踩线超时怎么兜底。
4.1 模型怎么选:三个候选的实测对比
我最初在三个量级的开源模型之间做了一次横向评测,统一跑在24核的CPU虚拟机里,测试任务包括意图分类、实体抽取和短文本生成。
| 模型配置 | 内存占用 | P95延迟 | 意图准确率 | 结论 |
|---|---|---|---|---|
| 0.5B级 | 约150MB | 95ms | 96.8% | 适合做意图分类与路由 |
| 3B级 | 约2.1GB | 420ms | 98.6% | 适合做实体抽取与短句判断 |
| 7B级 int8量化 | 约4.8GB | 1.8s | 99.1% | 效果接近云端,但CPU性价比太低 |
最终线上选型是“一大一小”组合:意图分类用0.5B级模型,追求速度;结构化抽取和检索路由用3B级模型,追求准确率。7B级模型被我放弃了,不是因为效果不好,而是在CPU上延迟实在压不下去,1.8秒的P95会把整条请求链路拖垮,不如直接升级到L3。
4.2 推理引擎与量化:CPU延迟从秒级压到百毫秒级
模型选完后,推理引擎的配置同样关键。同一份模型,用不同的引擎和量化方式跑,差距可能是三倍到五倍。
我用的是转换为ONNX格式后配合ONNX Runtime做推理,模型精度用int8动态量化。量化之后内存占用降到原来的四分之一左右,延迟也进一步压缩。推理进程部署成独立的多进程服务,每个进程绑定固定的CPU核,避免多线程争抢导致延迟抖动。
一个典型的会话初始化片段:
import onnxruntime as ort sess_options = ort.SessionOptions() sess_options.intra_op_num_threads = 4 # 单实例内部线程数 sess_options.inter_op_num_threads = 1 # 算子间不并行,减少锁竞争 session = ort.InferenceSession( "intent_model.onnx", sess_options, providers=["CPUExecutionProvider"] )线程数不是越多越好。我试过把intra_op_num_threads调到8,结果在并发请求下反而因为线程切换导致P95变差;最后稳定在4,配合进程数8、每进程绑定4个核,整机24核刚好跑满。
4.3 提示词模板、超时与降级策略
生产环境还有一个容易被忽视的问题:小模型的输出格式不稳定。本地0.5B级模型在自由文本输出时经常出现多余字段或JSON格式错乱,所以我在所有子Agent的提示词尾部都加了一条硬约束,要求只输出JSON,并且给定了固定的字段结构。输出的解析层再设置schema校验,解析失败就直接当作“本轮无效”,按超时处理。
超时策略我也定了明确矩阵:本地推理超过300毫秒,直接按“低置信度”处理,升级到L3;云端大模型超过4秒无响应,则回退到L1,返回一条固定的“已收到您的问题,客服稍后跟进”文案。这样能保证用户无论什么情况下都能得到响应,不至于卡死在某个环节。
5. 上线后踩过的坑:三个容易被忽略的真实问题
方案上线后的前两个月,我接连遇到三个奇怪问题。每一个单独看都不难解决,但如果不提前知道,排查起来会非常耗时间。
5.1 高度自信的错误答案:分类置信度不能当事实置信度用
第一个问题发生在一次政策更新之后。新政策上线第二天,用户问“现在这批货的关税谁来出”,本地意图分类Agent给出了一个超过0.95的高置信度,路由层判断走L1,直接返回了标准知识库里的旧政策答案。
排查之后我意识到问题出在自己的认知上:分类置信度高,只代表模型“很有把握地把这个句子归入某一类”,并不代表“答案在事实上是对的”。小模型对旧政策的记忆来自训练数据,它会把旧答案当成唯一标准答案。
这次之后我调整了所有L1的覆盖范围:L1只能承载那些知识变化极慢的标准化FAQ,比如“退款到账要几天”“怎么查看物流单号”,并且答案必须来自固定模板表,不允许小模型自由生成。凡是和政策、价格、时效相关的动态内容,强制走L2,必须先检索最新知识库再生成答案。
5.2 路由抖动:同一问题换着路径走
第二个问题很诡异:同一个用户、同一句话,前后隔十分钟发过来,一次走了L1,一次走了L3。用户反馈“你们系统抽风了”,因为两次收到的答案完全不是一个风格。
我查了日志,发现症结在向量检索的相似度分数不稳定。知识库会定期更新,有些历史段落的嵌入向量在新的向量空间里,分数被整体拉低,导致原本能命中L1的请求掉到了L3。
解决办法有两个:一是不再对相似度分数做硬性阈值判断,改成先排序取Top-K,然后用最高分和第二名的差距来判断是否可靠;二是给同一用户短时间内的重复请求加了一个15分钟的滑动窗口缓存,命中缓存直接复用上次的路由结果,避免频繁抖动。
5.3 成本账算不清:混合调度后的日志结构
第三个问题不是系统故障,而是财务上的:混合调度跑起来之后,月度账单反而讲不清了。原因是本地和云端混在一起,有的请求走了L1没花token,有的走了L2花了半份钱,有的走L3花了全份钱,但日志里只记录了“成功/失败”,没有记录路径。
我重新设计了请求日志结构,每个字段都对应一条完整链路:
{ "request_id": "xxx", "route": "L2", "intent": "return_request", "local_agent_latency_ms": 210, "cloud_input_tokens": 620, "cloud_output_tokens": 180, "cloud_billable_cost": 0.0008, "feedback": "negative", "updated_at": "2024-xx-xxT10:00:00Z" }月度复盘的时候,只要按route字段分组,就能看出每条路径的请求量、token消耗和反馈质量。这也让我后来能很有底气地说:L2路径的每千次请求成本比L3低60%,而且反馈分差不明显。没有日志结构,这句话根本讲不出来。
6. 这套架构还能怎么走:几次方向修正与延伸思考
方案稳定之后,我陆续做了几次方向修正,每一次都对整体效果有不小的提升。
6.1 从“先判断后检索”改为“先检索后判断”
第一版流程是:先用意图分类Agent判断用户想干嘛,再做检索,最后路由。上线后发现一个问题:当用户留言里出现新词汇、新品牌名或者错别字时,小模型很容易分错意图,后面的检索再准也没用。
后来我把流程倒了过来:先用BM25和向量检索做一次粗召回,把候选段落拼到提示词里,再让本地模型基于这些候选上下文做意图判断和路由。模型不再凭空猜意图,而是在“看了一点材料”之后做判断,准确率提升非常明显,错误路由率直接降了一半。这个改动也印证了一个经验:不要只盯着模型能力,顺序和上下文设计往往比换大参数模型更划算。
6.2 把“未知”当成正式意图来训练
另一个很重要的改动是让模型学会说“我不知道”。
最开始微调时,本地模型在40多个意图里硬挑,遇到完全没法归类的留言,它也会硬着头皮选一个最接近的。这种错误比拒绝回答更可怕,因为后续链路会被带偏。我在训练数据里加了一个“unknown”类别,并且要求模型在置信度低时优先输出unknown。
配合调度层,输出unknown的请求直接升级到L3或人工介入,不再尝试用本地模型硬答。上线之后,错误路由率又降了一个百分点,而且日志里的unknown案例成了每周优化训练数据的重要来源。
6.3 下一步想试试的端侧方向
目前这套混合调度的思路完全可以继续延伸到端侧设备。比如把0.5B级模型塞进移动端做语音助手的意图识别,云端大模型只在端侧无法处理时才被唤醒;又比如离线场景下,端侧小模型先缓存用户的问题,等网络恢复后再批量上传交给云端生成。方向是现成的,关键是做好端侧和云端的通信协议与状态同步,以及在“断网也能基本可用”和“联网体验更好”之间找到一个平衡点。
我现在回过头看,这套方案最值钱的不是省了多少钱,而是把整条Agent链路重新梳理了一遍。最后分享一个小技巧:云端返回的高质量答案,很多团队直接当历史日志存起来就完了,其实可以离线清洗成“问题、答案、路由标签”这样的训练数据集,每周灌回去微调CPU侧的小模型。我们坚持做了大半年,L1直答率从31%提到了37%,而且本地模型的回答风格越来越接近云端模型。这个习惯,比任何调度策略都重要。