端侧Agent工程化实战:状态机、工具调用与资源约束下的系统设计 在三个月前的小型语音助手上跑步还是拿到一台只有 8GB 内存的国产开发板上跑通端侧 Agent这两件事给我带来的心理冲击完全不同。跑通 Demo 的那一刻确实很爽但半个月之后当我把这个能回答问题的玩具拿去接真实业务时才发现真正的战争才刚刚开始。之前的文章已经聊过端侧 Agent 的概念和架构这次想认真讲讲工程化。先说结论端侧 Agent 工程化核心不是把模型塞进设备而是围绕模型在设备上做决策这件事把任务编排、状态存续、工具调用、上下文管理、能力降级、部署升级这些原本在服务端被忽视的问题重新按端侧的资源约束做一遍。这一篇是上先讲骨架和基础推理加速、并发控制、评测迭代这些偏术的内容放在下篇。1. 能跑通的 Demo 和能交付的 Agent 之间隔着一条什么河1.1 云端那一套在端侧直接水土不服很多从云端转过来的同学第一个反应是把 LangChain 或者 Dify 那套思路搬过来在设备上起一个 Python 服务把模型 API 换成端侧推理引擎就觉得万事大吉。我一开始也是这么干的结果被现实教育得很彻底。举一个具体差异云端的 Agent 默认模型是一个远程黑盒上下文再大也不过是 token 计费的问题端侧模型跑在本机上下文窗口直接吃内存带宽和显存。我的测试机上跑 7B 模型上下文从 4K 涨到 8K推理延迟翻了一倍多显存占用也肉眼可见地涨。这意味着端侧 Agent 的思考过程必须剪枝——能不看长文档就不看能压缩历史就压缩而不是像云端那样全部塞进 prompt 让模型自己挑。再比如工具调用。云端 Agent 调一个 API网络延迟几十毫秒失败重试也很从容端侧 Agent 要调的是本机应用、传感器、文件系统、蓝牙设备、甚至另一个端侧模型的输出。这些工具接口千奇百怪有的是 Unix socket有的是 Intent 广播有的是共享内存有的根本没有稳定接口只能靠 shell 命令拼凑。工程质量完全取决于这一层工具抽象做得好不好。1.2 四个被低估的维度资源、不确定性、状态、边界我梳理了一下端侧 Agent 工程化必须面对的四个核心维度每个都是河的一部分资源维度。芯片算力、内存带宽、电池、发热都是硬约束。模型推理要省非模型逻辑也要省。一个 Agent 框架本身如果吃掉 80MB 内存在手机上还能忍在智能音箱上就是灾难。任务不确定性。Agent 的输入不是一个固定 schema 的请求而是自然语言描述的目标。用户说帮我把今晚的菜谱根据冰箱里的东西定一下Agent 要先感知冰箱库存可能需要视觉识别或手动录入再规划菜谱再生成购物清单每一步都可能出现歧义。这种不确定性要求在架构上留出人机确认和中途改主意的弹性。状态长存。与一次性的 RPC 请求不同Agent 是一个长时间运行的会话体。它要记住用户偏好记得上次任务执行到哪一步要在设备重启后恢复上下文。这些状态放在哪里、怎么序列化、怎么保证一致性是端侧特有的难题。边界问题。端侧 Agent 拥有设备的控制权——能发消息、能调应用、能读写文件。这就把安全问题从网络安全变成了本机权限治理Agent 执行一个工具调用前是否需要用户授权授权粒度怎么设计如何防止提示词注入导致的越权操作这些不解决工程化就是空中楼阁。2. 先分清两个词Agent 是决策主体Harness 是承载它的运行环境2.1 为什么harness这个词在端侧尤其重要热搜里有个词反复出现harness 和 agent 的区别。我发现在端侧工程化语境下这两个概念必须切开否则后面所有设计都会拧巴。按我的理解Agent是决策主体它由三样东西组成模型推理能力、上下文当前任务相关的所有信息、工具集合它能调用的能力清单。Agent 负责想它接收目标产出计划决定下一步调什么工具。Harness是承载 Agent 运行的执行环境它负责做管理 Agent 的生命周期启动、暂停、恢复、终止、维护与用户的对话循环、把 Agent 的决策翻译成真实的工具调用、处理工具返回结果并回填给 Agent、记录轨迹日志、实施安全策略。用生活化类比Agent 是司机负责看路、判断、打方向盘Harness 是车——底盘、发动机、刹车、仪表盘还有交警安全策略。没有好的车再好的司机也跑不了长途。2.2 端侧 harness 的最低配置清单在端侧harness 不应该做得太重。我给出的最低配置清单是这样一个运行循环run loop接收用户输入 - 组装上下文 - 调用模型 - 解析决策 - 执行工具 - 更新状态 - 循环或结束一个任务状态机至少包含 idle、running、waiting_user、waiting_tool、error、done 这几种状态一个工具注册表统一管理工具的描述、参数 schema、执行函数、超时策略一个会话管理器负责上下文的持久化与恢复一个审计日志记录每次模型输入输出和工具调用用于调试与安全追溯这套东西如果自己写核心代码大约几百行就能搞定但关键在细节。我见过太多团队在跑 Demo 阶段用最简单的事件循环糊一个后面接真实工具时发现没有状态机、没有超时控制、没有日志只能推倒重来。2.3 框架选型自己写 harness还是套开源框架这是端侧 Agent 开发最先遇到的问题。我的个人结论是能自己写核心 harness 就自己写可以借鉴框架思想但别把开源框架直接搬进端侧。理由有三个一是依赖体积和平台适配。LangChain 这类框架是为服务端设计的依赖链很长在 Android/iOS/嵌入式 Linux 上跑起来非常痛苦。Dify 更是重度依赖云端部署和端侧场景八竿子打不着。CrewAI 的多 Agent 编排思想可以参考但它本身不是为单机资源受限环境设计的。二是抽象层次不匹配。端侧 Agent 需要的不是千行配置的 Chain而是足够底层的状态机 工具协议。开源框架的抽象级别越高你越难针对硬件做优化。三是调试路径。端侧出问题时要看的是模型推理输出了什么工具调用为什么超时状态是怎么丢失的这些在自研 harness 里一目了然在大框架里可能被封装成callback 没触发pipeline 状态不可见这类玄学问题。我并不是说所有框架都不能用。如果你的目标是快速验证业务逻辑在电脑上跑通一套 LangChain 完全没问题但落到端侧设备我更建议把它当作设计参考书而不是运行依赖。3. 任务编排把多步决策变成可暂停、可恢复、可中断的状态机3.1 为什么 ReAct 循环在端侧不够用上篇文章讲过 ReAct 是 Agent 的基础决策范式——推理Reasoning- 行动Action- 观察Observation循环。但工程化之后你会发现纯 ReAct 循环在端侧有很实际的痛点第一它是一次性的。用户问一句答一句Agent 没有任务进行到一半的概念。但真实场景是用户让 Agent 帮忙规划周末行程Agent 查到一半用户说等等周五晚上先不加这个——这时候如果 Agent 只能从头再来体验非常差。第二模型推理和工具执行是串行的。模型每输出一个动作就要等工具执行完才能继续。在云端这个延迟勉强可接受在端侧一次模型调用可能就要 1~3 秒串行循环一旦超过三四轮用户就以为设备死了。第三没有一个清晰的任务边界。什么时候算完成卡住了算失败还是算等用户这些问题在纯循环里没有答案。所以我在工程化时把决策循环升级成了任务状态机。3.2 一个最小可用的任务状态机设计我把 Agent 的每次任务抽象成这样一个状态机IDLE空闲等待用户新任务PLANNING模型正在生成计划可暂停EXECUTING正在执行当前步骤WAITING_TOOL已发起工具调用等待结果可中断WAITING_USER遇到歧义或需要授权等待用户输入COMPLETED任务完成FAILED任务失败走降级处理CANCELLED用户取消关键点在于状态必须可持久化。当 Agent 进入 WAITING_TOOL 时我们要把当前上下文、计划列表、已完成的步骤、当前步骤的中间输出一次性序列化存下来。这样即使设备因为省电策略杀掉了进程下次拉起时也能恢复现场。这个设计带来的直接收益是断点续聊。我的朋友在做的一个端侧助手用户说帮我把这份文档翻译成英文然后总结要点发给我。传统实现要一口气跑完翻译和总结状态机实现可以让 Agent 先翻译耗时很长走后台任务翻译完成进入 WAITING_USER 问用户翻译好了现在总结吗——用户说先不用了任务在总结步骤挂起下次用户说继续吧直接从总结步骤恢复不用重新加载全文。3.3 并行展开一个 Agent 如何同时执行多个独立子任务端侧 Agent 的多任务常常被误解成多 Agent 并发但在资源受限设备上跑多个模型实例是不现实的。更务实的做法是单个 Agent 的决策循环中允许展开多个并行的工具调用分支。比如用户说查一下明天的天气同时帮我把购物清单里的牛奶加到购物车。这里有两个完全独立的工具调用天气查询和购物车操作。如果串行执行至少两次往返如果并行展开一次模型推理输出两个 actionharness 同时发起两个工具调用总耗时接近最慢的那个。实现上我在 harness 的工具执行层维护一个 task group模型输出一个并行计划块plan block包含多个 actionharness 将它们分发给独立的 worker 并发执行每个 worker 有各自的超时时间全部返回后合并 observation 再送回模型。这里有一个坑并行执行时如果某个工具改了共享状态可能会导致数据竞争。所以工具调用要尽量设计成无副作用或可回滚对写操作特别是涉及用户数据的一律串行。3.4 人机协同把 WAITING_USER 当成一等公民端侧 Agent 最容易被忽略、但工程上必须设计好的一环是什么时候该问用户。我的原则是涉及不可逆操作、涉及花钱、涉及隐私数据的操作必须 WAITING_USER。比如删除文件、发送消息、下单购买、读取通讯录都需要用户确认。这条策略现在很多端侧设备厂商也都这么做了典型的就是手机上的 AI 助手调用系统级能力前弹授权框。但 WAITING_USER 不能只是弹个窗等输入那么简单。你要设计好悬置任务的恢复协议Agent 在等待用户时要保存一个临时上下文快照用户可能隔很久才回复这期间设备可能重启、应用可能被清理恢复时要能精准回到当初那个等待节点并且把用户新输入与旧任务上下文合并成延续决策。我在实现里给每次 WAITING_USER 分配了一个悬浮通知通知里带上任务 ID用户点通知就能恢复会话。恢复时把快照反序列化重新进入决策循环但这次输入的是用户对问题的回复而不是全新任务。4. 端侧记忆与状态从对话历史到可恢复的会话状态4.1 记忆不该是一段字符串而是一组结构化记录做 Agent 开发的人都知道 context 的重要性但端侧工程化对记忆的要求和云端完全不一样。云端可以无限堆 token端侧必须把记忆当作一种存储系统来设计。我从三个层面管理端侧 Agent 的记忆工作记忆当前任务相关的短期信息比如用户刚刚说的目标、当前正在看的文件内容。这部分直接存在于模型上下文里通常只有几百到几千 token。工程化要做的是及时清理——每完成一个子任务就把不再需要的中间输出从上下文摘除防止上下文膨胀吃掉推理速度。会话记忆一次完整交互中值得长期保留的内容比如用户偏好我喜欢简洁的回复不要用专业术语、重要事实用户家里有两个孩子、历史任务的结论。这部分建议落地为本地结构化存储SQLite 或轻量 KV在每次 Agent 开始决策前按需注入相关条目到上下文。程序记忆Agent 学会的技能或沉淀的经验对应现在大热的 skill 概念。比如一个网页转 Markdown技能它包含提示词片段、调用哪个工具、参数怎么填、常见错误怎么处理。这部分不是 prompt 字符串而应该是一段可执行、可编排的技能脚本。4.2 会话状态的序列化协议设计一个非常实际的工程问题是当设备进入省电模式或进程被杀Agent 的会话状态怎么保存我的方案是引入一个统一的 SessionRecord 结构JSON 序列化。核心字段包括session_id会话唯一 IDuser_profile_refs关联的用户画像条目 ID 列表task_stack当前任务状态机包含计划列表、完成/未完成标记context_handle指向一段压缩后的对话摘要而不是原始全文tool_memory本次会话中工具调用产生的重要中间结果resume_point用于恢复的节点指针比如等待用户确认删除每次状态变化进入 WAITING_TOOL、WAITING_USER、完成一个步骤都会触发一次持久化。写入频率不能高不然 IO 会拖慢整体所以我采用关键节点同步写 中间过程异步写的策略只有进入 WAITING_USER 和任务完成/失败时同步 flush其他中间状态只更新内存等进入关键节点再一起落盘。4.3 上下文压缩端侧 Agent 的保命技能端侧模型的上下文窗口有限对话一长要么爆窗要么延迟飙高。工程化必须设计一套上下文压缩机制。我用的招数是三段式摘要对话开始阶段原始消息完整保留对话中期超过一定轮数后每轮对话实时压缩成一句话语义摘要同时保留最近 N 轮原始文本长对话后期只保留全局摘要 最近一轮完整内容 关键事实列表压缩动作本身也要花钱调用模型做摘要所以不能每轮都做。我的经验值是原始累计超过上下文窗口的 60% 时触发一次压缩。压缩后的摘要可能丢失细节所以我在压缩时把用户明确表达的需求、约束、偏好单独抽取成 facts 清单这部分是结构化存储不参与摘要压缩。有一次用户在一个会话里连续问了十几个问题从菜谱到旅游攻略再到孩子教育每个问题跨度很大。如果没有系统整个上下文会乱成一锅粥。用三段式摘要后模型始终能看到最近一轮的完整诉求和全局事实清单虽然中间过程的细节丢了但用户体验反而是上升的——因为模型不再被无关的旧话题干扰。5. 工具调用的落地形态让 Agent 真正上手干活5.1 工具抽象别让 Agent 直接面对杂物在端侧Agent 要调用的不是云端的 REST API而是设备上的各种能力。如果直接让模型对着 shell 命令或者 system API 做决策安全性和稳定性都会失控。我的做法是定义一个中间层——Tool Adapter。每个工具对外暴露一个统一的接口描述name工具名给模型看的description用途说明要写清楚什么时候用、什么时候不要用input_schema参数定义的 JSON Schema模型严格按这个生成参数execute真正执行的那个函数timeout_ms超时上限requires_auth是否需要用户授权execution_mode串行默认还是允许并行举个例子一个打开应用工具。对模型暴露的接口很简单输入是应用包名或应用名。但适配层内部要做的事包括查应用列表、确认应用是否已安装、通过 Intent/AM 启动、处理启动失败、加上 5 秒超时。模型不需要知道这些细节它只管说打开微信。5.2 模型输出到工具调用的翻译JSON 解析的脆弱性治理这是端侧 Agent 工程化最脏最累的活之一。模型可能输出格式不完整的 JSON可能在 JSON 外包了 markdown 代码块也可能把参数名拼错。云端可以把模型换成语义更强的版本或做多次重试端侧推理成本高重试一次的代价太大。我的处理链路是分层兜底优先让模型输出结构化 JSON在 system prompt 里给死格式拿到原始输出后先做一次提取器用正则找到最外层花括号剥离所有非 JSON 前缀后缀如果标准 JSON 解析失败用宽松解析器容错允许单引号、去掉尾逗号、修正缺失括号这类常见问题再不行进入增量校验流程把模型输出按 token 流重新解码在解码过程中使用工具参数的 JSON Schema 做约束解码只保留符合 schema 的 token第四步听着复杂其实端侧推理引擎一般都能支持只是要花点功夫和引擎团队对接。它的效果是让模型根本不可能生成非法 JSON副作用是解码稍慢。我的建议对高频工具在关键参数上做约束解码对低频工具容忍前端解析失败并重试一次。别在重试上死磕多花时间提高 prompt 的约束力比事后修补有效得多。5.3 MCP 在端侧要不要用我的裁剪方案现在聊 Agent 工具协议绕不开 MCPModel Context Protocol。这个是现在最主流的一种开放工具协议社区生态也起来了。但端侧直接用标准 MCP 服务端是有问题的标准 MCP 大量依赖 JSON-RPC over stdio 或 HTTP还有初始化握手、能力协商、资源订阅这些机制对于一个嵌入式设备的资源预算来说太重了。我的裁剪思路是在端侧实现一个 MCP 的最小子集只保留 tools/list 和 tools/call 两个方法走本地 JSON-RPC over Unix socket 或共享内存。把 capability negotiation 固化成一份静态配置不做动态订阅不做 resource 玩法。这套裁剪给后续接外部工具留了很好的扩展性。社区里丰富的 MCP server比如各类 skill 仓库很多都兼容这个子集稍作适配就能接入。可以说端侧 Agent 的工具生态未来大概率会长在轻量级 MCP 兼容层之上而不是孤岛式的私有协议。5.4 工具执行的错误处理与降级工具调用失败的场景在端侧比云端多得多——蓝牙断开、文件被占用、用户取消了授权、权限不足、设备电池过低……我通常的做法是给工具执行结果加一个状态码 可读错误 建议下一步的三元组状态码SUCCESS / FAILED / TIMEOUT / NEED_USER_INPUT / PERMISSION_DENIED可读错误一句话说明给模型看建议下一步比如权限不足时建议引导用户去设置页打开权限蓝牙未连接时建议先执行连接流程模型在下一次决策中看到这个三元组就能直接修正方向而不是把同一个失败动作再执行一遍。这个设计特别重要不然模型会在同一个坑里绊倒两次甚至反复绊倒。6. 端侧部署与升级让 Agent 跑进真实设备的最后一公里6.1 程序与模型的解耦热更新的前提端侧 Agent 的更新问题常常被忽略。很多团队把 prompt 和工具逻辑一起编译进 App导致改一句提示词都要发版。工程化的目标是策略与代码分离。我的结构是三板斧模型文件独立存储按版本管理支持增量下载提示词模板和技能脚本以配置文件形式下发不走代码发版工具适配层保持稳定接口具体实现可以通过插件机制热替换举个例子模型要从 7B 换到 3B为了省电或者从英文模型换成中文能力更强的模型代码一行都不用改只要更新模型文件和相关配置文件。拖了半个月才想明白这个道理前面吃了不少亏。6.2 包体积与内存预算怎么算端侧 Agent 不是能跑就行资源预算是硬指标。整理一份可以拿去对产品对齐的预算清单具体参数因设备而异但这个思路可供参考模型文件7B INT4 量化后约 4GB3B INT4 约 2GB1.5B 约 1GB。小模型不能只看大小还要看效果够不够用运行时内存推理引擎 模型权重 KV cache 框架逻辑建议控制在设备可用内存的 30% 以内。8GB 的设备上Agent 全栈吃超过 2.5GB 就危险包体积增量Agent 框架本体不含模型压缩后控制在 10MB 以内超过这个数就得反思是不是引入了太多服务端时代的依赖网络流量纯端侧场景为 0但涉及云侧增强比如复杂的摘要生成可以发到云端要按次计费并做用户提示6.3 多级能力降级设备再弱也要有响应端侧设备千差万别同一个 Agent 可能跑在旗舰手机也可能跑在几十块钱的 MCU 上。工程化必须设计能力降级路径。我的降级阶梯长这样L0 完美模式完整大模型 全量工具 实时在线L1 均衡模式中等模型 核心工具集 对话压缩L2 节能模式小模型 只保留高频工具 每次决策强制精简上下文L3 兜底模式规则引擎 固定话术不调用模型保证设备还有反应这个降级不是手动切的而是 harness 根据设备状态电量、温度、内存水位、模型推理延迟自动触发。比如电量低于 15% 时从 L1 切到 L2用户能明显感觉回复变简单了但核心功能问天气、设闹钟还能用。产品层面要清楚降级不是 bug是续航和体验的取舍要让用户理解而不是感受到坏了。6.4 数据与隐私边界端侧 Agent 之所以有存在价值很大程度是因为数据不出设备。工程化要把这条优势变成可证明的承诺而不是口号。我的做法是默认所有决策链路完全本地执行模型推理、工具调用、状态存储都在设备内涉及云端的操作比如搜索增强、云同步必须明示并取得用户同意审计日志只记录必要信息且记录在本地不上传用户可以在设置里一键清空 Agent 的所有记忆与状态这一点在面向消费者的产品里尤其重要。之前做智能音箱时用户对设备是否在录音极度敏感。有了清晰的本地边界和用户控制权信任问题会好解决很多。这个经验放在端侧 Agent 上同样适用。7. 最后留几句实在话这轮做端侧 Agent 工程化踩了无数坑最大的感受是端侧 Agent 不是把 LLM 变小然后塞进设备而是从零设计一个在限制条件下做决策的系统。框架是骨架状态是血肉工具是手脚安全是免疫系统缺一样都会出问题。如果你正准备上手端侧 Agent我的建议是先把上述六个方面画成一个总架构图每块自己评估一下复杂度然后从最弱的地方开始补齐。不用一上来追求完美——能跑通一个最小闭环状态机 一个工具 持久化会话再去扩展是性价比最高的路径。下一篇下我会重点写推理加速、模型量化选型、端侧 Agent 的抗并发策略多实例管理、排队调度这部分和云端思路差异挺大以及怎么给端侧 Agent 做评测和迭代。这里面后续动作比较多我们需要一步步把工程化这顶帽子戴实。记住一句话Agent 的能力上限由模型决定但产品体验的下限由 harness 决定。把 harness 打磨好是端侧 Agent 从玩具到生产力的关键一步。