
这系列写到现在前面两篇把端侧 Agent 的底层原理和典型场景聊得差不多了。之前我们讨论过为什么端侧推理更适合隐私敏感、延迟敏感的任务也梳理过模型如何落地到手机和嵌入式设备上。今天这篇进到一个更现实、也更让人头疼的层面——Agent 工程化。说白了就是怎么把一个能在云端跑通的 Agent Demo挪到端侧设备上变成一个稳定、可控、能扛住真实使用压力的产品级系统。我自己踩过不少坑从树莓派上跑一个简单的工具调用 Agent到在量产设备上做整套 Agent 服务中间的差距并不是“多加几个功能”那么简单。端侧 Agent 的工程化本质上是做一套资源约束下的取舍系统算力、内存、功耗、包体积、网络波动、设备碎片化每一样都在逼着你重新设计。这篇先聊“上”的部分——架构分层、harness 与 Agent 的边界、上下文与记忆管理后续再补并发、安全、部署这些更硬的骨头。这一篇适合两类人一类是在把云端 Agent 往端侧迁移发现动不动就超时、爆内存的开发者另一类是刚开始做端侧 AI 硬件部署想直接避开常见大坑的新手。我会把原因讲透也会给出能直接抄作业的方案。1. 端侧 Agent 的工程化边界先认清对手再动手1.1 端侧与云端 Agent 的本质差异云端 Agent 跑在数据中心网络稳定、算力富裕、内存按需扩容、GPU 随便调用。你给它一个任务它可以在几十个 API 之间来回调度中途做十几次 tool calling等多久都不算事。端侧完全是反过来的。以手机端为例现在的旗舰机 NPU 虽然理论算力不差但实际跑一个 7B 参数的量化模型推理速度也就是每秒十几到二十几个 token。如果是 3B 级别的模型在千元机上跑速度能到每秒三四十 token但内存占用也得 3~4GB 起步。量产 App 不可能把所有内存都让给模型后台的页面栈、网络框架、图片缓存哪个不是吃内存的大户更别提老设备的功耗约束——连续高负载推理五分钟机身温度就上来了系统立刻开始降频推理速度断崖式下跌。所以端侧 Agent 的工程化第一课就是接受一个事实端侧 Agent 不是在更差的环境里重跑云端逻辑而是按端侧的环境重新设计整个系统。模型要选对尺寸Agent 的执行逻辑要更简洁工具调用的粒度要设计得刚好合适甚至交互模型本身都得改——用户不能像一个云端使用者那样把一个大任务丢给 Agent 然后耐心等待两分钟。典型的端侧 Agent 应该把任务拆得更细每一小步都在用户可视范围内完成并随时准备把回答打断重来。这不是体验设计问题而是资源约束下的必然选择。1.2 部署形态决定工程复杂度端侧 Agent 的部署形态五花八门我把常见的几类列了一张表方便你对号入座部署形态典型设备算力参考内存上限工程化重点手机端 App 内置中高端智能手机NPU7B INT4 约 15~25 tok/s500MB~1GB分配给推理功耗、核心权限、版本兼容嵌入式开发板RK3588、树莓派 5、Jetson Orin NanoCPU/GPU3B INT4 约 20~40 tok/s1~4GB稳定性、断点续跑、散热桌面应用内置macOS / Windows 笔记本统一内存或独立 GPU7B 可跑到 30~60 tok/s2~8GB多任务并发、插件生态端网协同手机 本地局域网服务器模型在服务器端侧只做采集和展示端侧仅需几十 MB通信协议、断线处理不同形态的工程化重点差异很大。手机端除了性能和功耗最麻烦的是系统权限比如访问相册里的图片做多模态识别iOS 和 Android 的权限模型完全不同。嵌入式设备倒是权限上放得开但掉电、进程被杀是家常便饭所以“任务恢复到上次中断点”是刚需。桌面端看似资源最多但用户同时开着浏览器、编译器、视频会议你抢太多内存一样会被骂。工程化的第一个原则先明确你的部署形态再做技术选型。不要在手机端上硬套桌面端的 Agent 编排框架也不要在嵌入式设备上幻想跑 70B 模型外加十几路并发。所有后续的设计决策都从这张表出发。2. 工程化架构从 Demo 到可交付的分层设计2.1 端侧 Agent 的四层架构我在自己项目里逐渐把端侧 Agent 的服务端拆分成了四个明确层级从下往上依次是模型推理层、Agent 编排层、工具执行层、应用接入层。模型推理层处理的是“把 token 跑出来”这件事。这里的选择通常是 llama.cpp 或 ONNX Runtime再往上套一层针对自己业务的推理封装流式输出、会话分批、KV cache 管理、中断信号处理。这个层要做得足够薄尽量不要塞业务逻辑进去因为模型可能会被替换比如从 3B 升级到 7B或者换一家推理引擎封装稳定的话替换的代价就小很多。Agent 编排层是核心它负责控制 Agent 的思考循环拿到用户指令之后要不要调用工具、调用哪个工具、传什么参数、拿回结果后如何继续推理。在这层里你不会直接接触模型而是面向一个“能回复文本也能输出结构化工具调用请求”的统一接口。云端做这个层一般用 LangChain 或者自研的 workflow 引擎但端侧要轻量得多通常就是一个循环加几个工具函数配合 prompt 模板和调度规则。再往上工具执行层把 Agent 编排层发出的调用请求真正落地。比如访问本地文件、查询日历、调起相机、执行 Shell 命令等。这个层必须做权限和沙箱隔离因为 Agent 的推理结果天生不可靠它可能生成一个合法但完全不符合用户预期的命令你的工具执行层就是要拦住这种风险。最上面的应用接入层是窗体和 UI。对用户而言看到的是一个聊天界面或者一个悬浮球对上层业务而言它提供的是“把这个任务交给 Agent”的 API。这一层还负责把用户的多模态输入标准化比如拍照取词、录屏分析转换成 Agent 能理解的指令格式。2.2 harness 与 Agent 的职责边界别再混为一谈“harness”这个词最近很热很多人把它和 Agent 混着用导致团队里沟通成本极高。我个人的理解是Agent 是策略harness 是执行环境。Agent 负责“想”——分析输入、规划步骤、决定调用哪个工具harness 负责“做”——拉起模型、管理循环、解析模型输出、把工具结果送回去、处理超时和重试。类比一下Agent 是司机harness 是车。司机决定怎么开车负责执行。你不能让司机去检查机油和轮胎那是车的事车也不能替司机决定走哪条路。如果这两个概念不分开代码很快就变成一个没法维护的巨型函数既要写推理逻辑又要处理模型输出格式还要管超时重启最后谁都不敢改。实际工程中harness 通常是一套通用的运行时不感知具体业务。它只处理四件事维护 Agent 的循环结构思考 → 行动 → 观察 → 再思考与模型层通信获取推理结果并做结构化解析管理上下文窗口自动做裁剪和摘要处理异常——最大循环次数、单次推理超时、工具调用失败重试你的 Agent 业务逻辑尽量写成纯函数输入一个 Request 上下文输出一个 Response 上下文。harness 负责在每次循环中把当前上下文喂给你的 Agent 函数再把 Agent 的输出喂回上下文。这样工具一换、模型一换Agent 策略代码几乎不用动。我见过有人把工具调用的 JSON schema 定义、权限校验、上下文压缩这些活儿全部堆在 Agent 代码里刚开始跑得通后来 Agent 应用规模一上来就崩得稀里哗啦。职责分离不是代码洁癖是工程化最基本的保命手段。2.3 框架选型重型框架并不是好选择端侧 Agent 框架选型是个反复踩坑的领域。云端的成熟框架——LangChain、CrewAI、AutoGen——功能丰富但它们的依赖链和抽象层数量在端侧设备上是沉重负担。我一个朋友在 RK3588 上装了 LangChain光导入包就花了十几秒跑一次简单任务内存涨了 300MB最后全部推翻重写。端侧工程化更建议这种组合模型推理用 llama.cpp、ONNX Runtime 这类底层引擎不引入大框架Agent 编排自研一个轻量 harness核心就三五个文件工具执行按业务手写 tiny tools每个工具一个模块额外加一层权限校验会话与记忆本地方案比如 SQLite 加 JSON 文件避免引入重量级向量数据库如果是桌面端的 Agent 应用可以考虑 Hermes Agent 这类已经做好 harness 和 agent 分离的开源项目直接在上层做业务扩展。它的设计理念我很认同把任务运行的控制逻辑shell 执行、进程管理、工具调用和模型策略分开对工程化非常友好。但如果你做的是嵌入手持设备这种极端受限环境还是得砍到只剩核心循环。框架选型的核心标准只有三个启动耗时、内存新增占用、可裁剪度。其他花里胡哨的功能都往后排。端侧设备不是服务器装一个 200MB 依赖的 AI 框架用户手机上多一个大几百 MB 的应用根本没法接受。3. 上下文、记忆与状态管理端侧 Agent 的命门3.1 上下文膨胀是推理变慢的头号元凶我在实际项目里测过一个现象一个 7B INT4 模型输入 200 token 时首 token 延迟大概 300ms输入 2000 token 时直接涨到 1.2 秒到了 5000 token首 token 延迟接近 2.5 秒。这还只是 7B换个更大的模型衰减更夸张。端侧 Agent 跑不了几轮上下文就膨胀上去了因为每一轮工具调用的结果、用户的追加消息全都在往上下文里塞。所以上下文管理不是可选项是端侧 Agent 工程化里最高优的优化点。常用的手段有以下几种自动裁剪设一个硬性 token 上限超过就把最旧的对话轮次直接扔掉。注意别只丢用户消息工具结果和 Agent 的思考链也得一起丢否则上下文语义会断裂。动态摘要每跑完一个子任务把这段对话压缩成一个几十字的摘要放回上下文里作为长期记忆的一部分。端侧用小模型做摘要就够了因为摘要不需要多高质量能抓住关键信息就行。关键信息抽取不要啥都往上下文里塞。用户的偏好、当前任务的中间结果抽出来放到结构化的 working memory 里而不是堆在聊天历史里。经验上一个端侧 Agent 的单轮会话上下文控制在 4K token 以内体验最好8K 是极限超过 8K 就必须做压缩或摘要。这不是模型窗口不够——很多模型支持 32K——而是推理延迟和内存占用不允许你那么挥霍。3.2 工作记忆与长期记忆的分层设计端侧 Agent 的记忆设计我这里推荐一个简单的三层结构参考了很多项目的做法也踩过不少坑。第一层是即时工作记忆对应 Agent 当前任务的临时状态。比如用户正在写一段文案让 Agent 帮忙改了三版那么“第三版内容”和“用户的修改意见”就属于这一层。它们只存在于当前会话里会话结束就销毁。实现上用进程内对象即可不需要持久化。第二层是会话级记忆对应一次完整任务的上下文。包括用户的目标、已经完成的步骤、当前工具结果摘要。这个要持久化到本地存储因为端侧进程很可能中途被杀App 重启后至少能把任务恢复到上一个状态。我通常用一个 JSONL 文件记录这个 session 的全部事件恢复时重放就行。第三层是用户级长期记忆对应跨会话的稳定信息。用户的称呼、常用的工具偏好、经常访问的文件路径、可复用的技能等。这层优先存 SQLite结构化查询方便字段清晰。如果需要做语义检索可以在 SQLite 里挂一个轻量向量扩展但对端侧来说向量检索的性价比通常不高能不用就不用。为什么要分层因为不同层级的记忆有不同的生命周期和访问频率。如果你把所有东西都堆在一个内存对象里会话一多就爆如果全部落盘每次推理都去查数据库latency 又拉满。分层的本质是给不同数据匹配不同的存储策略。3.3 状态机不是过度设计是端侧保命技能端侧 Agent 和云端最大的一个区别在于云端进程基本稳定端侧进程随时可能被系统回收。iOS 的后台墓碑机制Android 的默认后台杀进程嵌入式设备的突然断电都是常态。所以在工程化时务必给 Agent 的执行过程定义一套状态机。我用的最小集是INIT初始化、PLANNING规划中、EXECUTING工具执行中、OBSERVING观测结果中、FINISHED完成、FAILED失败、INTERRUPTED中断。每次 Agent 循环到达一个稳定点比如等待用户输入、工具执行完毕就把当前状态写进本地事务日志。进程被杀了重启之后先读日志发现状态是 EXECUTING 且工具执行到一半就根据工具本身是否具备幂等性决定是重跑还是标记失败。这个过程有点像数据库的 WAL 日志思路是完全一样的。比状态机更务实的一点是工具调用的幂等性设计。比如“创建文件夹”这种操作天然幂等重复执行没问题“发送消息”这种操作不幂等重复执行会重复发送。你的工具执行层在实现时就要标记每个工具是否幂等Agent 恢复时才能决定是否安全重放。这个细节我见过太多项目忽略最后都是在线上收到了重复短信或重复扣费才知道后悔。4. 工程化核心实践从设计到落地的关键细节4.1 轻量 harness 的最小实现结构前面反复强调自研轻量 harness我直接把我验证过的最小结构写出来你照着搭建即可。agent_core/ ├── harness.py # 循环控制、超时与中断管理 ├── context.py # 上下文对象包含 messages、working_memory、状态 ├── parser.py # 模型输出解析提取 tool_call ├── tools/ │ ├── registry.py # 工具注册表名称到函数的映射 │ ├── permission.py # 工具调用前权限检查 │ └── builtin/ # 具体工具实现 ├── memory/ │ ├── session.py # 会话记忆 │ ├── longterm.py # 长期记忆 │ └── summarizer.py # 上下文摘要 └── storage/ ├── journal.py # 事件日志用于恢复 └── sqlite_store.py # 持久化存储封装harness.py 的核心循环可以简化成def run_agent(request): ctx build_context(request) for step in range(MAX_STEPS): savepoint(ctx) # 记录日志方便恢复 response model.generate(ctx.to_messages()) if not response.tool_call: return response.text tool registry.get(response.tool_call.name) if not permission.check(response.tool_call): return 权限不足已拒绝执行该操作 result tool.run(**response.tool_call.args) ctx.add_observation(result) return 已达到最大执行步数终止任务这段代码看起来简单但背后要处理的问题一点不少。比如model.generate可能是流式的需要支持中途取消tool.run可能长时间卡住需要带超时控制ctx.to_messages()里要做上下文裁剪和摘要。把这些细节都做扎实一个可用harness 差不多就需要几百行代码。4.2 上下文压缩与摘要的实操参数上下文压缩的时机和参数直接决定体验。我分享一下自己调出来的经验值。触发裁剪的硬阈值设为 6K token 比较合适模型窗口如果是 8K留出 2K 的余量避免某次工具结果特别长时超限。裁剪时保留前三轮对话约 1K token包含系统提示和用户首条指令中间部分统一压缩成摘要。摘要的 prompt 要专门写不要复用通用总结的 prompt。我的摘要 prompt 大概是这个逻辑“你是会话摘要器提取用户的核心需求、已完成步骤、关键结果、未完成事项压缩成 200 字以内保留可以继续执行任务的信息。”用 llama.cpp 跑一个 3B 模型做摘要大约耗时 300ms完全可接受。每轮对话结束后判断一次如果当前上下文超过 6K就触发摘要否则不动。只在超出阈值时做摘要不要每个循环都跑否则反而增加延迟。工具结果尤其要控制大小。我之前写过一个工具它会把一个目录下的所有文件内容读回来结果一个工具结果就几千 token直接把上下文打爆。后来统一改成工具返回内容上限 800 token超长时只返回前 800 token 加一个“内容已截断如需完整内容请调用另一个工具”的标记。让 Agent 自己去决定要不要继续读取比把海量内容硬塞给模型强得多。4.3 工具调用链路中的稳定性设计工具执行层的稳定性是端侧 Agent 工程化里最容易翻车的地方。大问题集中在三类模型输出格式不可靠、工具本身不稳定、工具调用的权限和安全漏洞。先说格式不可靠。端侧模型普遍比云端的大模型更容易在结构化输出上翻车JSON 写错、参数类型不对是很常见的。所以我建议在 harness 里做一个容错解析链先用严格 JSON 解析失败后用正则提取{...}片段重试再不行就提示 Agent 重新输出。链路设计完解析成功率能从 80% 拉到 95% 以上。重点不是让解析器多聪明而是让失败模式可控。然后是工具本身不稳定。端侧设备的 IO 比云端服务器慢得多尤其访问外部存储或网络时经常会有几秒甚至十几秒的等待。每个工具调用都要带超时机制——我用的是 10 秒默认值超时后返回一个明确的超时信号让 Agent 可以决定重试还是换方案。还要给工具调用加最大重试次数一般是 2 次超过就直接返回失败避免 Agent 陷入无限重试的死循环。权限和安全这里多说一句。工具执行层必须做白名单校验Agent 想调用的每个工具、每个工具的参数范围都要经过权限检查。比如一个写文件的工具要限制它能写的路径前缀不能让它去覆盖系统目录一个执行 Shell 的工具要限制可执行命令的集合不能让它随便rm -rf。这层校验可以在 harness 循环里做也可以在工具函数入口做关键是必须有。没做这层你的 Agent 就是一个随时可能做出违法操作的后门。4.4 版本升级与技能Skill机制端侧 Agent 上线之后最难的不是第一次把功能做完而是持续迭代。模型在更新Agent 的规划策略在调整工具在增加每一处改动都影响整体行为。要做好工程化版本管理必须上升到“可独立发布的模块”粒度。这里要说一下 skill 机制。“Claude Agent Skills” 是个很好的思路在端侧同样适用。一个 skill 本质上是一个文件包包含一段描述性 prompt什么时候用这个 skill、若干工具定义JSON schema、执行逻辑Python/JS 脚本、以及示例用法。skill 粒度刚好介于“单个工具”和“完整 Agent 行为”之间特别适合端侧灰度发布。我的做法是把所有 skill 放在一个独立的目录里每个 skill 自带版本号。Agent 编排层启动时扫描 skill 目录把可用 skill 的 prompt 拼进系统提示词工具注册表也由 skill 自己注册。这样加一个新技能、改一个现有技能都只要替换对应的 skill 目录不影响 Agent 核心代码。热更新可以做但不是必须。在嵌入式设备上我一般是在应用重启时检测 skill 目录的版本变更桌面端则可以做一个轻量的文件监听检测到变更后热加载。这种设计让工程迭代节奏快很多——模型不用换Agent 逻辑不需要动上传一个新 skill 包就上线了一个新能力。5. 实战中的高频问题与排查经验5.1 一组真实高频问题速查表我在端侧 Agent 调试过程中把最常见的问题整理成了一张速查表按“症状→原因→解法”给你列出来症状常见原因排查与解法跑几轮后推理越来越慢上下文膨胀有效 token 超过 4K打开日志观察每次请求的 token 数设置 6K 阈值自动压缩模型经常输出无法解析的 JSON端侧模型指令遵循能力弱加容错解析链用更简单的 schema或换成指令遵循更好的模型工具调用超时率高端侧 IO 慢工具设计太重缩短单次加载内容分页获取给每个工具调用设独立超时App 切换后台再回来Agent 状态丢了进程被回收状态没持久化按上文状态机设计每个 stable point 落日志恢复时重放多轮对话后 Agent 忘记最初目标上下文裁剪把关键信息丢了摘要时显式提取用户目标和未完成事项放回系统提示词首次调用模型特别慢模型文件没预热或内存不足触发 swap首次启动做模型预加载并把 KV cache 预热检查内存占用必要时换更小的量化模型这六类问题占了端侧 Agent 调试工作量的七成以上。其中绝大部分不是算法问题而是工程问题——上下文没管好、边界条件没处理、恢复机制缺失。5.2 端侧 Agent 的调试技巧与工具链端侧调试比云端痛苦很多没有随时可看的集中式日志没有随时可进的在线 Debugger有些嵌入式设备甚至没有屏幕。我摸索了一套相对高效的调试方法。第一所有 Agent 决策过程都要输出 trace。从用户请求到最终响应中间每个步骤模型原始输出、经过解析后的 tool_call、工具返回结果、上下文更新后的状态全部写成结构化的日志JSON Lines 格式落盘到一个 trace 目录。出问题时直接回放这个 trace 就能定位到具体是哪个环节出了问题。这比让用户描述“它表现不对”然后盲猜高效得多。第二做一批固定的回归用例。我维护了一个测试集里面包括“简单问答”“多轮工具调用”“需要拒绝的危险请求”“上下文超长压缩”这类典型场景每次改完代码跑一遍看输出和预期是否有大偏差。端侧模型有随机性同一份代码在不同设备上输出也可能不同所以回归的重点是检查“是否答非所问”和“是否调用错误工具”而不是要求逐字一致。第三在端侧设备本地留一个最小复现入口。我在手机上写了一个 Debug 页面可以直接输入测试请求、查看 Agent 的中间 trace、手动触发工具调用。没有这个入口你每次改完代码都得走一遍完整 UI 流程效率极低。桌面上我还会用命令行跑一个 headless 版 Agent直接用 curl 发请求调试速度能快一个量级。第四刚开始实验阶段端着“失败是常态”的心态。端侧 Agent 不像云端那样长时间稳定随机性、硬件差异、上下文干扰都会导致偶发问题。不要追求 100% 的确定性输出而是把错误处理做扎实让用户遇到问题时至少能得到一个合理的兜底回应比如“这个请求我没能完成你可以换个方式再试一次”。系统设计上留足容错比让模型每次都完美更实际。6. 关于工程化节奏的最后一件事做了这么多端侧 Agent 项目我最大的体会是工程化的节奏决定了项目的生死。不要一开始就追求大而全的功能框架也不要因为“端侧”就觉得要省到极致。先把最小可用链路跑通——一个模型、一个轻量 harness、三五把工具、二层记忆然后放到真实设备上一边跑一边补。尤其要养成“先跑 trace 再写代码”的习惯。任何一次异常表现先看 trace 里模型到底输出了什么、harness 是怎么解析的、工具返回了什么再决定改哪一层。别一上来就调模型、换 prompt八成问题不在那里。这篇先把架构分层、harness 边界、上下文与记忆管理这些工程化的地基聊透了下一篇我会展开讲并发模型怎么设计、端侧安全怎么落地、以及热更新和可观测性体系怎么搭。这些部分实操性更强我们下篇见。