做了将近一整年的端侧 Agent 产品之后,我越来越确信一件事:跑通一个 Demo Agent 和交付一个能稳定运行的端侧 Agent,中间隔着的不是算法能力,而是工程化能力。
这个系列前两篇聊了端侧 Agent 的基础概念和模型选型,按理说接下来该进入实操搭建了。但我在实际项目里发现,大部分人在“Agent 能回答问题了”这个阶段就卡住了——不是模型不行,而是推理、内存、并发、工具调用这些工程问题全都堆在一起爆发。这篇我重点讲工程化的“上篇”:从 demo 到可交付,端侧 Agent 在架构层面必须跨过哪些坎。
1. 跑通 Demo 之后,端侧 Agent 工程化才真正开始
我见过太多团队,包括我自己早期,死在同一个环节:Agent 在开发机上语义理解、工具调用、多轮对话都表现完美,一旦搬到端侧硬件上,立刻原形毕露。这个问题很普遍,想绕绕不过去。
1.1 我踩过的第一个坑:原型阶段的“假工程化”
第一次做端侧 Agent 原型时,我把模型装进手机,写了个 Streamlit 界面,用 LangChain 把几个函数包成 tools,模型能回答问题、能查天气、能开关灯。我当时觉得这事儿快成了。
但真正要把它变成一个后台常驻服务时,第一轮压测就崩了。三个问题同时炸出来:
- 内存峰值到 1.6GB,系统直接杀进程;
- 模型推理把 CPU 占满,前后台切换卡成 PPT;
- 同时挂起两个任务,第二个任务的上下文把前一个冲掉了。
这三个问题没一个是模型能力导致的,全都属于工程范畴。我把它们记下来,才发现“端侧部署模型”和“端侧 Agent 产品化”压根是两码事——前者只要一个推理引擎,后者需要一整套精心设计的调度系统、内存管理机制和上下文体系统。
1.2 端侧 Agent 工程化 vs 云端 Agent 工程化的本质差异
做云端 Agent 的同事常跟我说:“并发、缓存、限流这套,我们踩过坑了,你照着抄就行。”但真端侧落地时,发现完全抄不了。云端和端侧在“资源”这个词下的定义完全不同。
| 维度 | 云端 Agent | 端侧 Agent |
|---|---|---|
| 算力 | 虚拟化后按需扩容 | 固定算力,不可升级 |
| 内存 | 动态分配、几十 GB 起步 | 固定物理内存,通常 4-8GB 共享 |
| 功耗 | 基本不考虑 | 决定设备温度、续航、寿命 |
| 网络 | 高带宽低延迟 | 弱网、离线是常态 |
| 上下文 | 上下文窗口几乎无上限 | 必须精打细算 |
这个差异直接决定了端侧 Agent 架构设计的第一原则:一切都可以被牺牲,唯独“可控”不能被牺牲。云端方案可以靠资源堆叠解决大部分问题,端侧只能在资源边界内做文章。这个思维转变不完成,后面的每一步都会走弯路。
2. 端侧硬约束如何倒逼 Agent 架构决策
端侧工程化绕不开四个硬约束:内存、算力、功耗、网络。多数人把它们当作“性能优化”问题,但实际上它们是架构层面的约束,决定了 Agent 内部的每一条数据通路。
2.1 内存预算:模型权重、KV Cache 与上下文争抢同一块 RAM
端侧 Agent 有个隐形内存杀手,比模型本身更凶——KV Cache。很多人知道模型权重占内存,但忽略了自回归生成时每生成一个 token,都要存一份历史 token 的 key-value 结果。这个缓存随着上下文长度线性增长,一旦 agent 工具调用链拉长,几轮下来 KV Cache 可能吃掉几百 MB。
我在项目里做过一次统计:一个 7B 量化模型在端侧跑 2048 token 上下文,权重占 3.5GB(int8),KV Cache 约 500MB,Agent 自身状态和工具运行时占 300MB。设备总内存 8GB 的话,系统只剩不到一半可用,后台稍微开几个应用,OOM 几乎是必然。
所以现在我的做法是:把内存当作独立预算来管理。给模型推理、Agent 状态、KV Cache、工具运行时分别划定上限,超限的部分宁可降低上下文长度、主动触发缓存清理,也不能让内存失控。这事在系统设计阶段就要定好,否则后面调优时处处掣肘。
2.2 算力与功耗:NPU 调度、发热与推理时延的三角平衡
端侧 Agent 的算力分配,本质是“有限算力怎么切给多个消费者”的问题。模型推理要算力,工具调用(比如图上有图、服务里有 OCR)也要算力,NPU/DSP 核心只有一个,谁来用是调度器说了算。
最容易犯的一个错,是把 Agent 推理和设备主线程绑在同一个线程。用户在主界面滑动时,Agent 推理一来,主线程直接被抢占,画面掉帧,用户感知到的就是“手机卡了”。我的处理方式是至少两个进程:主进程管 UI,Agent 进程管推理。两个进程之间用轻量级 IPC 通信,这样即使 Agent 满载,UI 也是流畅的。
但这又会引出一个新问题:Agent 进程持续推理时,芯片温度会迅速爬升,超过一定阈值触发降频。降频之后推理变慢、环路时间变长,又进一步恶化体验。我解决方案是推理功率控制:检测到温度快要过线时,主动把推理 batch size 降下来或切换成低功耗推理模式,宁可慢一点,不要卡死或过热。
2.3 网络与离线:弱网环境下 Agent 的自主降级策略
端侧 Agent 不能假设自己永远在线。我遇到过导航类 Agent 在车库没信号、翻译 Agent 在飞机上离线跑的场景。网络中断不应该是 Exception,而应该是正常路径。
我的降级策略分四档:
- 在线全量:网络正常时,可调用需要云服务的工具;
- 在线受限:弱网时,关闭大流量工具(比如图像高清上传),保留轻量工具;
- 本地兜底:断网时,切换到本地的知识库检索和离线模型推理;
- 任务挂起:任务依赖外部服务时,保存现场状态,等到网络恢复再继续执行。
这里最容易被忽略的点也是核心点:工具的注册表里必须标注“网络依赖级别”,这样调度器才能真正智能决策。我见过不少人直到断网才后知后觉地发现自己的 Agent 连切离线模式的接口都没预留。
3. 工具注册表:Agent 工程化中比模型更重要的基础层
聊到工具调用,必须先澄清一个概念混淆。我看到网上关于“harness 和 Agent 框架有什么区别”的问题特别多。这块不搞清楚,选型时就容易选错。
3.1 harness 与 Agent 框架:两个常被混淆的概念
框架层管的是“Agent 怎么组织”:它怎么定义 system prompt、怎么管理 multi-turn 上下文、怎么调节 model 的参数,这属于 Agent 自身的宏观结构。
harness 层管的是“Agent 怎么与环境互动”:它负责把 Agent 的内部状态连接到外部世界——怎么给 Agent 暴露函数、怎么解析返回结果、怎么处理调用异常。用个不恰当但形象的比喻:框架是 Agent 的性格,harness 是 Agent 的手脚。
这个区别在工程化中有非常直接的影响。我在一个项目里发生过这样的事:团队在框架层很认真地设计了多轮对话逻辑,但给 Agent 暴露工具时用了一个没封装的 Python 函数,参数校验、超时控制、结果规范化全都没有。结果 Agent 一旦工具调用频繁出错,就会陷入“重新尝试-再出错-再尝试”的死循环,整个 loop 转不出去。
我后来强制团队形成一条铁律:所有暴露给 Agent 的函数必须先经过 harness 层包装。包装干什么呢?统一做输入校验、统一设置超时(model 在等你回复,可不能让它无限等)、统一把结构化错误转成模型能读懂的文本描述。这层不做好,工程能力再强也白搭。
3.2 主流的端侧 Agent 框架选型对比
现在社区里比较活跃的几个方案,各有侧重点。我最近的实测感受是这样:如果你只看 demo,它们几乎没有差异;一旦做端侧工程化,差异立刻显现。
| 框架 | 优势 | 端侧落地时的坑 |
|---|---|---|
| AutoGen(微软) | 多 Agent 会话调度能力强 | 默认面向云端资源设计,端侧需大量裁剪 |
| LangChain / LangGraph | 生态大、工具函数多 | 依赖注入和回调链太重,启动开销大 |
| LlamaIndex | 知识/RAG 管理成熟 | Agent 本身的 loop 控制偏弱 |
| 自研迷你框架 | 可控性强、内存可预测 | 开发成本高,需要极强的架构能力 |
我个人对端侧项目的倾向是:不迷信大框架,优先考虑能否按需裁剪。自研一个只有模型推理 + 工具调度 + 上下文管理三层结构的迷你框架,往往比硬扛一个通用框架更顺手。这不是技术洁癖,而是端侧对资源敏感,框架的每一个“便利功能”最终都要翻译成 ROM 和 RAM 的消耗。
3.3 三大核心模块的拆分与分工
根据我的项目实践,端侧 Agent 内部的工程架构可以抽象成三个模块:
Reasoning core(推理核心)。负责模型加载、推理调度、流式输出。它只做一件事:拿到 Request 返回 Output,不掺和任何业务逻辑。这个模块的工程核心点是模型上下文窗口管理——多长上下文该进还是该出,都由这层决定。
Tool orchestration(工具编排)。负责把 Agent 的意图变成具体的工具调用。它需要管工具注册、参数提取、工具返回结果的结构化处理。这一层才是“智能”真正发挥作用的战场——给模型的工具描述写得清不清楚、返回结果表述得明不明确,直接决定 Agent 的执行质量,也就是智能程度的上限。
Execution guard(执行守护)。负责预检和兜底。预检是调用前评估工具是否可执行,避免 Agent 在无谓的方向上浪费时间;兜底是调用失败后的降级策略——比如超时、重试、切换本地替代方案。这个模块混在 harness 里容易被忽略,但它决定了 Agent 服务的稳定性上限。
这三个模块的分工如果做清楚了,复杂的 Agent 工程问题就能化简成纯模块问题:推理慢了优化推理核心,工具老出错看工具编排,服务不稳定就查执行守护。问题定位速度会快一个量级。
4. 端侧 Agent 的并发模型:四核处理器上的多 Agent 调度
“并发”这个词放到端侧 Agent 语境里很有意思。做后端的朋友理解的并发是“上百路请求进来,机器要顶住”。但端侧设备上不会有上百路用户请求同时进来,端侧真正的并发问题,恰恰出在另一个地方。
4.1 “扛并发”在端侧的真实含义
端侧 Agent 的并发不是“同时在线上百用户”,而是三种形态的混合:
- 多 Agent 实例并发:日历提醒 Agent 和语音交互 Agent 可能同时活跃;
- 任务级并发:一个 Agent 内部可能同时有多个任务在跑,比如下载文件的同时在分析另一个数据源;
- 异步事件并发:用户语音输入、定时触发器、外部通知,这些事件随时可能打断当前任务。
我做过一次压力测试,在 8 核处理器上同时跑 3 个 Agent 实例(每个约 20M 内存)、每个 Agent 内部挂 2 个异步任务、同时处理 4 类外部事件。结果系统在 3 秒内整体延迟飙升了 4 倍。不是任何单个 Agent 的计算量爆炸,而是线程切换、锁竞争、事件队列堆积全撞到一起了。
4.2 单设备多 Agent 进程的内存隔离与通信
多 Agent 不能挤在一个进程里共享全部内存。我的方案是进程级隔离:每个 Agent 跑在独立进程,持有自己的内存池和状态存储,进程间通过一个轻量的消息总线通信。这个做法有两个好处:
一是防雪崩。某个 Agent 进程出问题会崩溃,但不会把整个系统拖下水。隔离边界决定了故障半径,进程崩溃就只伤及自己。
二是防监听。额外的好处是,某个进程的内存被意外撑爆时,不会影响其他 Agent 的生存空间。每个进程都有独立的地址空间,不能让 agent 自己从共享池里无限制地吃资源。
这套模型实现起来也不复杂。消息总线我用的是 Socket,借助操作系统的进程通信机制。每个 Agent 上报自己的状态(busy/idle/error),调度器维护一张状态表。通信协议用 JSON,简单可靠,不强上复杂的 IPC 方案,端侧场景越简单越稳。
4.3 任务循环的优先级注入与抢占策略
现在我在端侧 Agent 里用的是“事件驱动 + 优先级抢占”这套组合。任务循环里有一个全局事件队列,事件的类型决定优先级:
优先级0(最高): 用户显式打断 优先级1: 定时任务、系统通知 优先级2: 后台任务、数据处理 优先级3: 模型推理(非用户直接请求)模型推理其实不应该永远排最优先。用户正在手动操作 UI 的时候,后台推理应该主动降级。我采用的做法是推理调用前先检查 UI 活跃状态,如果用户在拖动手势,本轮推理自动延后或取消,等用户停止交互后再恢复。这个策略虽小,但对真实体验的提升巨大。
“抢占”也要讲究策略。Agent 正在执行一个长任务时,用户突然换了一个新指令,旧任务不能立刻被杀死,否则清理不彻底会留下脏状态。我选择了“协作式抢占”:旧任务收到取消信号后,在下一个检查点自己决定是否中止、保存现场、释放资源。这样避免了强杀导致的资源泄漏。
5. 上下文与记忆:工程化最容易翻车的隐藏深水区
OpenAI 那个智能体协议的论文里特别提到,Agent 的状态需要显式地管理,包括当前任务目标、对话历史、工作记忆和外部记忆。端侧 Agent 工程化时,这一块如果不做规划,翻车概率极高。
5.1 上下文窗口不是“可用内存”,必须精确分配
我发现很多端侧 Agent 的开发者在设计上下文时,把模型的上下文窗口当成了一座无限制的仓库:所有对话历史、工具响应、中间推理过程,一股脑往里塞。结果上下文很快被塞满,系统进入“挤出最旧内容”的被动模式——最关键的早期目标指令可能被踢出上下文,Agent 就变成了没头苍蝇。
现在我把上下文划分为三个独立区域,每个区域使用互不抢占的独立预算:
| 区域 | 预算占比 | 存放内容 |
|---|---|---|
| 目标区 | 10% | 用户核心意图、当前 Task 目标 |
| 对话区 | 60% | 多轮对话历史、工具往返记录 |
| 执行区 | 30% | 中间推理过程、待处理的工具输出 |
目标区的内容是最重要且最不能丢的。对话区可以剪裁压缩,执行区可以随时清空重启,但目标区一旦被挤出,整个 Agent 就丧失了方向感。这个比例可以根据实际任务做调整,但目标区永远必须是独立预算,不能和其他区域混着用。
5.2 Working Memory 与长期记忆的分离设计
Working memory 和长期记忆是两套完全不同的存储系统,技术上必须分开。
Working memory 放的是当前任务的瞬时状态:这个 Task 执行到哪一步了、已经调用过哪些工具、下一步计划是什么。它的特点是生命周期短、读写频繁、对时延要求高。我的实现是纯内存结构,甚至用了一个简单的状态机来管理,每次工具调用结束后更新一次状态。这样即使 Agent 中途休眠,恢复后也能知道自己“进行到哪了”。
长期记忆放的是用户偏好、历史知识、跨任务的沉淀内容。它需要持久化存储,但读写频率低很多。端侧上用 SQLite 就足够了,连向量数据库有时都未必需要。真正要处理的是“什么时候把工作记忆里的内容固化成长时记忆”——这个边界我还在摸索,目前的做法是任务结束时做一次“内容归组整理”,把重要片段写入长期记忆。
5.3 端侧向量检索:能用但不是银弹
很多 Agent 想实现“懂你的偏好”就引入向量检索。在云端这是日常操作,在端侧却要另说新建。本地跑 embedding 模型要占内存和算力,建立索引也要时间和存储空间,这些都要算进整个系统的资源预算。
我的做法是“阈值触发”式检索:平时不主动建索引,只有当用户明确表达(比如说了“记住我说的话”)或者任务结束时才触发一次记忆写入和检索更新。这样既能在需要时提供个性化上下文,又不会让 Agent 每时每刻都在跑 embedding 造成资源浪费。
对于真的需要在端侧跑向量检索的场景,我踩出来的一个经验是:索引要按时间分片,而不是一锅乱炖。同一个用户的记忆,早期的应该被逐层压缩或丢弃,近期的才保持高精度。否则索引膨胀后,检索时延会明显上升,最终拖垮整个 task 循环。
5.4 端侧 Agent 的上下文压缩策略
上下文压缩是我最后想讲的,也可能是最有价值的。当对话轮次变多,总能把部分对话历史揉成一段摘要,把更早的原话挤出去。但这个摘要的质量直接决定 Agent 后续行为的准确度。
我采用的压缩策略是分层压缩:第一层把工具调用返回的大段结构化数据压缩成“关键值提取”,只保留模型后续真正需要的数据;第二层才轮到对话历史,压缩时优先保留“用户的明确指令”和“对 Agent 行为的纠正”,这两类信息丢一条都会带来体验的实质损伤。
压缩时机也很讲究。我的触发条件是“当前上下文占用超过 70% 时预压缩”,而不是等快满了再暴力清理。预压缩有一个额外的好处:Agent 可以在低压力状态下将历史信息重组为稳定的摘要,比紧急腾挪时硬处理质量高很多。
这个技能(skill)系统、评测、安全话题,都属于端侧 Agent 工程化的“下半场”,我打算在系列下一篇继续展开。但就“上篇”而言,上面这些系统底子没打牢,后面的 skill 扩展和评测都无从谈起。我自己经历的一个朴素结论是:端侧 Agent 工程化做得好不好,先不看它多会回答问题,先看它长时间连续运行、各种事件打断、资源极度紧张时,还能不能有稳定可预期的输出。磨好底座,应用才有说服力。