1. 端侧 Agent 工程化到底难在哪:从“能跑”到“敢用”的鸿沟
很多人第一次把 LLM 塞进端侧设备时,都会经历一个极其兴奋的阶段:模型加载成功、对话跑通、Demo 效果惊艳,然后信心满满地准备上线。结果一上真实场景就傻眼了——内存峰值飙到系统 OOM、首 token 延迟三秒起步、多轮对话到第五轮就开始胡言乱语、用户连续快速点击直接让整个 Agent 卡死。这些问题不是模型能力不够,而是工程化没做到位。
端侧 Agent 工程化和云端 Agent 有着本质区别。云端你可以堆 GPU、加副本、做弹性伸缩,端侧不行——手机就是那么多内存,车机就是那么点算力,IoT 设备连散热都是问题。所以端侧 Agent 的工程化核心矛盾只有一个:在极度受限的资源预算下,让一个概率性系统表现出确定性的行为。这句话听起来简单,但拆开来看,涉及模型量化与内存管理、推理调度与并发控制、上下文管理与记忆压缩、工具调用的容错与降级、以及全链路的可观测性建设。
我见过太多团队在端侧 Agent 项目上翻车,翻车姿势高度相似:前期把精力全砸在 prompt 调优和模型选型上,工程侧只做了个“能跑通”的壳子,等到真正要交付时才发现,稳定性、延迟、内存这三个指标一个都过不了。更麻烦的是,端侧 Agent 的问题往往不是单一因素导致的,而是模型、框架、系统资源三者耦合出来的复合问题。比如你看到的是“Agent 响应超时”,但根因可能是量化后的模型在某个特定输入下触发了异常长的推理路径,导致内存碎片累积,最终拖垮了整个进程。
这篇文章是“深入理解端侧 Agent”系列的第四篇下半部分,上一篇我们聊了 Agent 工程化的上半场——主要是架构分层和模块拆解。这一篇聚焦下半场的硬骨头:推理调度、并发扛压、容错降级、记忆管理、可观测性这五块。每一块我都会给出具体的方案选型逻辑、参数计算过程、以及我在实际项目中踩过的坑。适合正在做端侧 Agent 落地、或者准备把 Agent 从原型推向生产的同学参考。如果你还在纠结“Agent 是什么”或者“选哪个框架”,这篇可能对你偏深了,建议先看系列前三篇。
2. 推理调度:端侧 Agent 的“心脏起搏器”怎么设计
2.1 为什么端侧推理调度不能照搬云端方案
云端推理调度器的核心目标是吞吐量最大化,它可以在请求队列里做批处理、做优先级抢占、做动态扩缩容。端侧完全不是这个逻辑。端侧设备通常只有一个推理单元(比如手机的 NPU 或 CPU 的某个核心簇),没有“扩容”这个选项,而且端侧 Agent 的请求往往来自同一个用户,具有强时序依赖——用户问了一句,Agent 正在回答,用户又追问了一句,这两轮对话在语义上是连续的,不能像云端那样把第二个请求丢到另一个副本上去。
所以端侧推理调度的第一原则是:串行化保证语义一致性,但要在串行化的前提下把延迟压到最低。这意味着调度器需要做三件事:请求排队与合并、推理资源的分时复用、以及推理过程中的中断与恢复。
我试过一个方案是给调度器加一个“语义窗口”的概念。当用户连续输入时,调度器不会立即为每个输入启动推理,而是等待一个极短的时间窗口(比如 80ms),把窗口内的输入合并成一个请求。这个窗口的取值很关键:太短了合并效果不明显,太长了用户会觉得响应迟钝。实测下来,80ms 到 120ms 是一个比较舒服的区间,用户感知不到延迟,但合并率能到 30% 左右,对端侧算力节省非常明显。
2.2 推理中断与恢复的工程实现
端侧 Agent 有一个云端没有的场景:用户可能在 Agent 正在生成回答时突然打断,输入新的问题。这时候如果调度器不支持中断,要么等当前推理跑完(用户等得想砸手机),要么直接杀掉进程重新来(上下文全丢)。正确的做法是实现推理中断与状态保存。
具体来说,推理引擎需要支持在 token 生成的过程中接收中断信号,保存当前的 KV Cache 和生成状态,然后切换到新请求。等新请求处理完后,再根据优先级决定是否恢复之前的推理。这里有个坑:KV Cache 的保存和恢复在端侧是有成本的,如果 Cache 很大(比如长上下文场景),保存和恢复的开销可能比重新推理还大。所以我的经验是,只对短上下文(比如 2K token 以内)做中断恢复,长上下文场景直接丢弃重来,但要把已经生成的部分文本保留下来作为“已说出口的话”,避免用户看到回答突然消失。
另一个关键点是推理优先级的动态调整。端侧 Agent 的请求不是平等的:用户主动发起的对话优先级最高,后台的定时任务(比如记忆整理、日志上报)优先级最低,系统级的健康检查居中。调度器需要根据当前资源水位动态调整优先级——当内存紧张时,直接暂停所有后台任务,把资源全部让给用户交互。
2.3 批处理在端侧的变通做法
云端推理的批处理是把多个用户的请求拼成一个 batch 一起推理,端侧没有多用户,但可以做多任务批处理。比如用户问了一个问题,Agent 需要同时做意图识别、实体抽取、知识检索三个子任务,这三个子任务如果串行跑就是三倍延迟,但如果它们的输入是同一段文本,完全可以拼成一个 batch 一次推理出来。
这里的关键是任务编排层要能识别可批处理的任务组。我的做法是在 Agent 的规划阶段就标记出哪些子任务共享输入、哪些子任务有依赖关系,然后把无依赖且共享输入的任务合并成一个推理请求。实测下来,这种批处理能把多任务场景的延迟降低 40% 到 60%。但要注意,批处理会增加单次推理的内存峰值,因为要同时维护多个任务的 KV Cache。所以批处理的大小要根据设备内存动态调整,内存小于 4GB 的设备建议 batch size 不超过 2。
3. 并发扛压:当用户疯狂点击时 Agent 不能崩
3.1 端侧并发的真实场景与压力模型
很多人觉得端侧 Agent 是单用户系统,不存在并发问题。这个认知是错的。端侧 Agent 的并发压力来自三个方向:用户交互的突发性、后台任务的周期性、以及系统事件的不可预测性。
用户交互的突发性最好理解——用户可能连续快速发送多条消息,或者在 Agent 回答过程中反复点击“重新生成”。后台任务的周期性指的是 Agent 自身的记忆整理、日志压缩、模型热更新等任务,这些任务如果和用户交互撞在一起,就会争抢资源。系统事件的不可预测性包括来电、通知、其他 App 抢占前台等,这些事件会导致 Agent 被挂起或资源被回收。
我做过一个压力测试:在 6GB 内存的安卓设备上,让用户以每秒 3 次的频率发送消息,同时后台跑记忆整理任务。结果在第 12 秒左右,Agent 进程被系统杀掉。排查后发现根因是记忆整理任务持有了大量内存没有及时释放,加上用户交互的推理请求不断创建新的 KV Cache,两者叠加导致内存峰值突破系统限制。
3.2 并发控制的三层防线
针对端侧 Agent 的并发问题,我总结了三层防线:入口限流、资源隔离、优雅降级。
入口限流是最外层。在 Agent 的请求入口处加一个令牌桶,限制单位时间内的请求数量。端侧设备的令牌桶容量建议设置为 2 到 3,填充速率根据设备性能调整,低端设备每秒 1 个令牌,中高端设备每秒 2 到 3 个。超过限流的请求直接返回“正在处理中,请稍候”,而不是排队等待。这里有个细节:限流返回的提示语要友好,不能让用户觉得 Agent 卡死了。
资源隔离是中间层。把用户交互任务和后台任务放到不同的执行队列里,用户交互队列的优先级永远高于后台队列。当用户交互队列非空时,后台队列暂停执行。同时,给后台任务设置内存上限,比如最多使用总内存的 20%,超过就自动暂停。这个隔离机制在 Android 上可以通过WorkManager的约束条件来实现,在 iOS 上可以用BGTaskScheduler配合内存压力通知。
优雅降级是最内层。当资源实在不够时,Agent 要能主动降低服务质量而不是直接崩溃。降级策略包括:缩短上下文窗口(从 4K 降到 2K)、减少检索文档数量(从 5 篇降到 2 篇)、跳过非关键的后处理步骤(比如格式化、引用标注)。降级决策由资源监控模块触发,当内存使用率超过 85% 或推理队列长度超过 3 时,自动进入降级模式。
3.3 内存管理的实操细节
端侧 Agent 的内存问题,80% 出在 KV Cache 上。KV Cache 的大小和上下文长度、模型层数、注意力头数直接相关。以一个 7B 模型、32 层、32 头、上下文 4K 为例,KV Cache 的大小大约是2 * 32 * 32 * 4096 * 128 * 2 bytes,算下来接近 2GB。这在手机上是不可能接受的。
所以端侧 Agent 必须做 KV Cache 的量化和管理。我常用的方案是:对 KV Cache 做 8bit 量化,直接把内存占用砍半;对长上下文做滑动窗口,只保留最近 N 轮对话的 KV,更早的对话压缩成摘要存到磁盘;对不再活跃的会话及时释放 KV Cache,比如用户切换到其他 App 超过 30 秒,就释放当前会话的 Cache,等用户回来时重新计算。
这里有个容易忽略的点:KV Cache 的释放不是简单的free就完事了,因为端侧推理引擎通常有自己的内存池,释放的 Cache 会回到池子里而不是还给系统。如果内存池只增不减,最终还是会 OOM。所以需要给内存池设置一个上限,超过上限时强制收缩。这个上限的取值建议是设备可用内存的 40% 到 50%,留足空间给系统和用户交互。
4. 容错与降级:让 Agent 在“半坏”状态下也能干活
4.1 端侧 Agent 的故障分类与影响面
端侧 Agent 的故障和云端不一样。云端故障通常是“服务不可用”,端侧故障更多是“服务降质”——模型还能跑,但输出质量下降;工具还能调,但返回结果不完整;记忆还能读,但部分数据丢失。这种“半坏”状态如果处理不好,比直接崩溃更危险,因为用户可能基于错误的输出做出决策。
我把端侧 Agent 的故障分成四类:推理故障(模型输出异常、推理超时、内存不足)、工具故障(API 调用失败、返回格式错误、超时)、记忆故障(读写失败、数据损坏、索引丢失)、系统故障(进程被杀、权限被回收、网络中断)。每一类故障的影响面和恢复策略都不一样。
推理故障的影响面最大,因为它是 Agent 的核心能力。工具故障相对局部,只影响依赖该工具的功能。记忆故障的影响是累积的,短期可能看不出来,但长期会导致 Agent “越来越笨”。系统故障最不可控,只能靠状态持久化和快速恢复来兜底。
4.2 推理故障的容错策略
推理故障里最常见的是输出格式异常。端侧模型经过量化后,输出稳定性会下降,有时候会生成不符合预期格式的内容。比如你要求模型输出 JSON,它给你输出了一段带 markdown 标记的文本。这时候如果直接解析就会抛异常,整个 Agent 流程中断。
我的做法是在推理层和解析层之间加一个格式修复层。这个修复层做三件事:先用正则提取可能的 JSON 片段,再用轻量级的语法修复(比如补全缺失的括号、引号),最后如果修复失败,就触发一次“格式重试”——把格式要求用更强的 prompt 再问一遍模型。实测下来,这个修复层能把格式异常导致的失败率从 15% 降到 3% 以下。
另一个常见故障是推理超时。端侧设备性能波动大,同一个请求在冷启动和热启动下的延迟可能差好几倍。所以超时阈值不能设死,要根据设备当前状态动态调整。我的方案是维护一个推理延迟的滑动窗口,取 P95 延迟作为基准,超时阈值设为基准的 2 倍。如果连续三次超时,就触发降级——换用更小的模型或者更短的上下文。
4.3 工具调用的降级与兜底
端侧 Agent 的工具调用有个特殊问题:很多工具依赖网络,而端侧设备的网络状态极不稳定。用户可能在电梯里、地铁上、或者信号弱的角落使用 Agent,这时候工具调用失败是常态而不是异常。
所以工具调用的设计原则是:每个工具都要有本地兜底方案。比如天气查询工具,网络可用时调 API,网络不可用时返回本地缓存的最近一次天气数据,并明确标注“数据可能不是最新的”。再比如知识检索工具,网络不可用时降级为本地向量库检索,虽然覆盖范围小,但至少能返回一些相关内容。
工具调用的超时也要分层设置。快速工具(比如计算器、单位转换)超时设 500ms,中等工具(比如本地检索)设 2s,慢速工具(比如网络 API)设 5s。超过超时时间就立即返回兜底结果,不要让用户干等。这里有个经验:兜底结果的呈现方式很重要,不能简单地说“工具调用失败”,而要告诉用户“当前网络不佳,以下结果基于本地缓存,仅供参考”。这样用户对结果的可信度有预期,不会因为一次失败就否定整个 Agent。
5. 记忆管理:端侧 Agent 的“长期记忆”怎么不变成负担
5.1 端侧记忆系统的存储分层
端侧 Agent 的记忆管理和云端有本质区别。云端可以用向量数据库、图数据库、关系数据库随便堆,端侧不行——存储空间有限,IO 速度有限,而且用户对隐私敏感,很多数据不能明文存。
我的方案是三层存储结构:热记忆放在内存里,用 LRU 管理,只保留最近 10 轮对话和当前会话的关键实体;温记忆放在本地文件系统,用轻量级的嵌入式数据库(比如 SQLite 或 ObjectBox),存储最近 30 天的对话摘要和用户偏好;冷记忆放在压缩归档里,存储更早的历史数据,只在需要时解压读取。
这个分层的关键是每一层的数据格式要针对访问模式优化。热记忆用结构化的对象,方便快速读写;温记忆用带索引的表结构,支持按时间、主题、实体等多维度查询;冷记忆用压缩的文本格式,牺牲随机访问性能换取存储空间。实测下来,这个分层能把端侧 Agent 的记忆读取延迟控制在 50ms 以内,同时存储占用比全量存储降低 70%。
5.2 记忆压缩与摘要生成的实际操作
端侧 Agent 的记忆不能无限增长,必须做压缩。压缩的核心是把原始对话转成结构化摘要。这个过程本身需要调用 LLM,但端侧算力有限,不能每轮对话都做摘要。我的策略是:每 5 轮对话触发一次摘要生成,把 5 轮对话压缩成一段 100 字以内的摘要,同时提取关键实体和意图标签。
摘要生成的 prompt 需要特别设计。端侧模型能力有限,prompt 要尽量简单直接。我常用的模板是:“把以下对话压缩成一句话,保留人物、时间、地点、事件和结果。对话内容:[对话文本]”。这个模板在 7B 量化模型上的成功率能到 90% 以上。如果摘要生成失败,就退化为关键词提取,至少保留一些可检索的信息。
这里有个坑:摘要生成本身会消耗推理资源,如果和用户交互撞在一起,会导致延迟飙升。所以摘要生成必须放在后台任务队列里,并且设置资源约束——只在设备空闲(比如充电、锁屏)时执行。Android 上可以用WorkManager的setRequiresDeviceIdle(true)和setRequiresCharging(true)来实现,iOS 上可以用BGProcessingTask配合相应的约束。
5.3 记忆检索的精度与速度平衡
端侧记忆检索的难点在于:向量检索精度高但速度慢,关键词检索速度快但精度低。端侧设备上跑向量检索,如果向量库大了(比如超过 1 万条),检索延迟会明显上升。所以需要做混合检索:先用关键词做粗筛,把候选集缩小到 100 条以内,再用向量做精排。
关键词粗筛可以用倒排索引,端侧用 SQLite 的 FTS5 就能实现,速度很快。向量精排用轻量级的向量检索库,比如 Faiss 的移动版或者自己实现的余弦相似度计算。这里的关键是向量维度的选择——端侧建议用 256 维或 384 维的向量,不要用 768 维或更高,因为高维向量的存储和计算成本在端侧不划算。实测下来,384 维向量在端侧检索 1000 条数据的延迟大约是 20ms,完全可以接受。
另一个经验是记忆的时效性加权。检索时不能只看语义相似度,还要考虑时间因素。最近一周的记忆权重高,一个月前的记忆权重低。这个加权可以在精排阶段做,把时间衰减因子乘到相似度分数上。衰减函数用指数衰减比较自然,半衰期设 7 天左右。
6. 可观测性:端侧 Agent 的“黑匣子”怎么建
6.1 端侧可观测性的特殊约束
云端 Agent 的可观测性很好做——日志随便打,指标随便上报,链路追踪随便埋点。端侧不行,端侧的可观测性面临三个约束:存储空间有限、上报带宽有限、用户隐私敏感。
存储空间有限意味着不能全量打日志。我的做法是分级日志:ERROR 级别全量保留,WARN 级别保留最近 7 天,INFO 级别只保留最近 24 小时,DEBUG 级别只在开发模式开启。同时日志要压缩,用 protobuf 或者 messagepack 序列化,比 JSON 省一半空间。
上报带宽有限意味着不能实时上报。端侧 Agent 的指标上报应该采用批量+延迟策略:本地攒够 100 条或者每隔 1 小时上报一次,优先在 WiFi 和充电状态下上报。上报内容也要做聚合,比如延迟指标上报 P50、P95、P99 而不是原始数据点。
用户隐私敏感意味着不能上报原始对话内容。所有上报的日志和指标都要做脱敏处理,对话内容只保留长度、语言、意图标签等元信息,不保留具体文本。如果确实需要上报文本用于调试,必须经过用户明确授权,并且做匿名化处理。
6.2 关键指标的定义与采集
端侧 Agent 的可观测性不需要面面俱到,抓住几个关键指标就够了。我通常关注五类指标:延迟指标、资源指标、质量指标、故障指标、用户行为指标。
延迟指标包括首 token 延迟、端到端延迟、工具调用延迟。首 token 延迟反映的是推理启动速度,端到端延迟反映的是整体响应速度,工具调用延迟反映的是外部依赖的健康度。这三个指标要分开采集,因为它们的优化手段完全不同。
资源指标包括内存使用率、CPU 占用率、存储占用、电量消耗。端侧特别要关注内存使用率的峰值和趋势,因为 OOM 是端侧 Agent 的头号杀手。我的做法是每 10 秒采样一次内存使用率,记录峰值和 P95 值,当 P95 超过 80% 时触发告警。
质量指标包括输出格式正确率、工具调用成功率、记忆检索命中率。这些指标反映的是 Agent 的“健康度”,比延迟和资源更能说明问题。比如输出格式正确率突然下降,可能是模型量化出了问题;工具调用成功率下降,可能是网络环境变化。
故障指标包括推理失败次数、工具失败次数、记忆读写失败次数、进程被杀次数。这些指标要按故障类型分类统计,方便定位根因。用户行为指标包括会话时长、轮次分布、打断率、重试率。这些指标反映的是用户体验,打断率高说明 Agent 响应太慢,重试率高说明输出质量有问题。
6.3 从指标到行动的闭环
采集指标不是目的,目的是根据指标做决策。端侧 Agent 的可观测性要形成闭环:采集 -> 分析 -> 决策 -> 执行 -> 验证。
举个例子:如果内存使用率的 P95 连续三天超过 85%,系统应该自动触发一次“内存优化行动”——清理冷记忆、压缩温记忆、释放不活跃会话的 KV Cache。行动执行后,继续观察内存指标,如果一周内没有改善,就升级为“架构调整行动”——比如降低模型量化精度、缩短默认上下文长度。
这个闭环的关键是自动化。端侧 Agent 不可能靠人工 24 小时盯着指标,必须让系统自己判断、自己行动。但自动化也要有边界,涉及模型更新、架构调整这类高风险操作,还是要人工确认。我的做法是设置一个“自动化等级”:低风险操作(比如清理缓存)全自动,中风险操作(比如调整上下文长度)自动执行但通知开发者,高风险操作(比如切换模型)只生成建议不自动执行。
7. 工程化落地的几个真实教训
7.1 不要在端侧追求“全能 Agent”
我见过不少团队在端侧 Agent 上堆功能,恨不得把云端 Agent 的所有能力都搬过来。结果就是每个功能都做得半吊子,整体体验极差。端侧 Agent 的工程化第一原则是做减法:只保留最核心的能力,其他全部砍掉或者降级。
什么是核心能力?对大多数端侧 Agent 来说,对话、检索、简单工具调用这三样就够了。复杂的规划、多步推理、大规模知识图谱,这些在端侧跑不动也不划算。与其做一个什么都懂一点的“全能 Agent”,不如做一个在特定场景下真正好用的“专用 Agent”。
7.2 量化不是免费的午餐
模型量化能大幅降低端侧的内存和算力需求,但代价是输出质量的下降。这个下降在简单任务上不明显,但在复杂推理任务上会暴露得很明显。我的经验是:4bit 量化适合对话和检索,8bit 量化适合推理和工具调用。如果端侧设备内存实在紧张,可以对模型的不同层做混合量化——注意力层用 8bit,FFN 层用 4bit,这样能在质量和资源之间取得比较好的平衡。
另一个坑是量化后的模型对 prompt 更敏感。同样的 prompt,在原始模型上效果很好,在量化模型上可能就崩了。所以量化后必须重新做 prompt 调优,不能直接沿用原始 prompt。这个调优过程通常需要 2 到 3 轮迭代,每轮收集 50 到 100 个失败案例做针对性优化。
7.3 测试端侧 Agent 要用“真实设备+真实场景”
模拟器上跑得再好,真机上也可能翻车。端侧 Agent 的测试必须用真实设备,而且要多设备覆盖——不同品牌、不同内存、不同系统版本。我通常至少覆盖 5 台设备:一台低端(4GB 内存)、两台中端(6-8GB)、两台高端(12GB+)。
测试场景也要真实。不能只测“用户问一句 Agent 答一句”的理想场景,要测“用户连续快速输入”“Agent 回答过程中用户打断”“网络切换”“来电打断”“后台任务和用户交互并发”这些真实场景。这些场景下的问题往往在模拟器上复现不出来,只有真机才能暴露。
7.4 版本更新要留“后悔药”
端侧 Agent 的版本更新比云端风险大得多。云端更新出问题可以快速回滚,端侧更新出问题,用户可能已经用了好几天才发现。所以端侧 Agent 的更新必须留“后悔药”:保留上一个版本的模型和配置,支持一键回滚。
具体做法是:更新时把新版本写到独立目录,旧版本保留不动。启动时先尝试加载新版本,如果加载失败或者启动后 5 分钟内崩溃超过 3 次,自动回滚到旧版本。同时上报回滚事件,让开发者知道新版本有问题。这个机制看起来简单,但能避免很多“更新即翻车”的事故。
8. 写在最后:端侧 Agent 工程化的取舍之道
端侧 Agent 工程化做到最后,本质上是在做一系列取舍:模型大小和输出质量的取舍、上下文长度和内存占用的取舍、功能丰富度和稳定性的取舍、开发速度和长期维护的取舍。没有完美的方案,只有适合当前场景的平衡点。
我的经验是,端侧 Agent 的工程化优先级应该是:稳定性 > 延迟 > 内存 > 功能 > 质量。先保证不崩,再保证响应快,然后控制内存,最后才考虑加功能和提质量。这个顺序不能乱,乱了就会陷入“功能越加越多、系统越来越不稳”的恶性循环。
另外,端侧 Agent 的工程化不是一次性的工作,而是持续迭代的过程。设备在变、模型在变、用户在变,工程方案也要跟着变。我通常每季度做一次“工程健康度评估”,检查内存、延迟、故障率这些指标的趋势,如果发现恶化就及时调整。这个习惯帮我避免了很多“温水煮青蛙”式的问题。
最后分享一个我常用的判断标准:如果一个端侧 Agent 方案需要超过 3 个工程师全职维护,那它大概率设计得太复杂了。好的端侧 Agent 工程化应该是“简单、健壮、可预测”的,而不是“精巧、复杂、需要时刻盯着”的。