端侧 Agent 工程化,和云侧 Agent 工程化完全是两种打法。模型能不能在设备上跑起来,这只是第一关;真正让 Agent 能被用户日常使用,是第二关——运行时的调度、记忆、工具边界、权限、可观测性,缺哪一个都会让你从 Demo 回退成“玩具”。这篇是《深入理解端侧 Agent》系列的第四篇,也就是工程化的下半场,重点讲端侧 Agent 从“能推理”到“能干活”要跨过的那些工程坎。适合正在做端侧 AI 硬件部署、Agent 产品化、或者纠结 Agent 框架怎么选的工程师,也适合准备入行 Agent 开发的人当一份全局地图看。
1. 先看清全景:Agent 工程化(下)到底在补哪些模块
1.1 从上篇到这篇:交付路径还差哪几块
上篇花了不少篇幅讲模型层的事:选型、量化、NPU 适配、单次推理的性能评估。那部分解决的是“模型能不能在设备上跑”,这篇要解决的是“Agent 能不能在设备上长期、稳定、安全地干活”。
我在实际项目里看到最典型的翻车场景是这样:模型量化好了,首次推理延迟测出来 50 毫秒,团队很高兴,觉得已经成了。结果一接入真实对话流程,Agent 本地要加载系统提示词、注入用户画像、调一个工具去读文件、再把工具结果塞回上下文重新推理一次,整套链路跑下来用户等了 40 秒。中间还出现过一次 Agent 在工具执行完以后没保存状态,设备重启,对话断线全部丢失的故障。
所以工程化下半场要补的不是某一个点,而是一整条“可信运行链”。按我的拆解,主要七块:进程与生命周期管理、并发与请求调度、记忆与状态持久化、工具与 Skills 插件机制、安全沙箱与权限模型、可观测性与日志、失败恢复与降级。云侧这些东西很多都能靠 Kubernetes、Redis、Prometheus 一套组合拳解决,但端侧设备没有这些基础设施。端侧更像一台没有运维人员的微型服务器,你得把那些能力以轻代码的方式,全部内嵌到 Agent 运行时里。
1.2 Agent 框架与 Harness 的区别,以及为什么端侧更依赖后者
最近在社区里看到一个高频问题:Harness 和 Agent 框架到底什么关系。很多人把 LangChain、Dify、CrewAI 这类框架当成 Agent 的全部,这其实是理解偏差。
框架解决的是“Agent 循环怎么描述”:怎么让模型做计划、调用工具、观察结果、再决定下一步。它给你的是编排层,本质是一个相对通用的控制流模板。而 Harness 解决的是“这个循环怎么被安全地装进一个生产环境”。Harness 要管循环启动条件、单步超时、总预算上限、工具执行隔离、状态持久化、失败重试、事件日志。你甚至可以理解为:框架是发动机,Harness 是整车电子电气架构。
在端侧,你更需要 Harness,因为设备环境的资源边界和不可控性太强。你在 GitHub 上能看到一些叫 agent-harness、Hermes Agent 的开源项目,它们的核心思路基本一致:把 Agent 循环封装在一个可观测、可限流、可重启的执行环境里。用 Rust 写的这类运行时尤其多,因为 Rust 的内存控制和无 GC 行为,在端侧太合适了。后面讲并发和安全时,很多设计也都是从 Harness 的视角出发的。
1.3 端侧工程化的四个约束条件
做端侧 Agent 工程化之前,先把这几个约束刻在脑子里。第一,算力不是无限的,推理原则上应该串行,并行多路意味着成倍的内存。第二,设备可以被随时打断,来电、息屏、重启、断电,你不做状态持久化,用户就会骂娘。第三,端侧的最大价值是隐私,数据留在本地,那你就要把这个承诺在架构上立住,不能被一个权限漏洞打穿。第四,设备往往处于弱网环境,一旦工具调用依赖云端服务,就要有明确的超时和降级策略。
这四个约束是后面所有设计决策的出发点。理解了它们,你再看网上那些“端侧 Agent 扛并发”“端侧多 Agent 编排”的方案,就能分辨出哪些是工程现实,哪些只是 PPT。
2. 并发怎么处理:端侧 Agent 的资源调度策略
2.1 别把云侧的“扛并发”直接搬到端侧
先说明一个容易误入的概念:“Agent 怎么扛并发”在端侧和云侧是两件事。云侧扛并发是解决多个独立用户请求同时打进来,办法是水平扩容。端侧通常是单用户设备,真正的问题是:一个用户同时触发了多个 Agent 任务,或者一个 Agent 任务内部同时并发调用多个工具,这时候模型资源严重不够。
举一个实际数字:一个 1B 到 7B 参数量级的端侧模型,量化以后权重占用通常在 1GB 到 4GB,推理过程中还有 KV Cache 占用。设备总内存如果只有 8GB,你想同时保持两个 Agent 会话活跃,很快就会发现系统开始频繁换页,推理速度掉到不可接受的水平。我见过一个团队试过在同一个手机上同时跑两个 Agent 实例,结果模型加载内存接近 6GB,后台直接被系统杀掉。
所以端侧并发的设计目标不是“同时跑”,而是“排队跑得好”。你要保证优先级高的交互请求快速响应,同时不让后台批量任务饿死,也不让它们把内存挤爆。Singul 我们在路由侧做到的最优策略,本质上就是一个带优先级的任务队列,让推理引擎始终单例运行。
2.2 单引擎加队列:把多路请求排成一条可控的流
你可以在 Agent 运行时里维护一个任务队列,任务有优先级,推理引擎只关心从队列头拿任务。举个例子:
class AgentExecutor: def __init__(self): self.priority_queue = PriorityQueue() self.engine = EngineLoader.get_shared() async def submit(self, request, priority): task = AgentTask(request, priority) self.priority_queue.put(task) async def run(self): while True: task = await self.priority_queue.get() async with self.engine.acquire_context(): await self.engine.invoke(task.stream_context) self.priority_queue.task_done()代码是示意,重点在结构。请求按两到三级优先级划分:用户正在看的交互式请求放最高,后台预加载和摘要任务放中等级,批量数据整理放最低级。如果最高优先级任务到达,而当前正在跑一个低优先级任务,你可以选择立即取消低优先级任务,把推理资源让出来,或者让它最多跑到一个安全断点再让位。这里我建议优先采用“可中断点”而不是强杀,因为推理中途硬切可能让 KV Cache 和上下文状态不一致。
2.3 共享引擎与会话状态隔离
既然推理引擎必须单例,那多会话的关键就是“共享但不串味”。模型权重可以共享,因为推理引擎加载一次就够了;每个会话只保留独立的上下文状态、工具调用历史和记忆索引。工程上要把这两者分清楚:全局引擎是只读的,会话上下文是可变的。
最容易踩的坑是,把“会话独占一个 Agent 实例”写成“为每个会话加载一份模型”。我在一个项目里见过这样的代码,理由是并行度高,结果模型加载两回,直接把设备内存打穿。正确做法是:会话只持有自己那份 SessionState,里面包含当前上下文缓存的引用、变量表、任务进度。当请求进入时,运行时从队列把它调度到共享引擎上执行,执行完把增量状态写回 SessionState。
会话状态最好加事务性,避免工具调用写了一半,设备突然被杀,下次恢复出一份坏数据。状态更新的最小单位是事件,而不是直接覆盖快照。
2.4 端侧并发和响应速度的取舍
再来一个实操层面的计算逻辑。用户感受到的 Agent 响应延迟大致是:排队等待时间 + 模型推理时间 + 工具执行时间。模型推理时间取决于当前上下文长度和设备算力,工具执行时间取决于外部服务或本地文件 IO,排队等待时间就是你调度策略的结果。
假设设备推理速度是每秒 30 token,当前上下文 4000 token,一次完整生成 500 token,那模型层就要 150 秒左右,这显然太慢,你根本不可能让用户干等。如果上下文短一点 1500 token,生成 200 token,那就是大约 57 秒,还是偏慢。这意味着三件事:一是 Agent 要让对话上下文尽量精简,二是长任务必须主动降级为“后台执行 + 完成通知”,三是流式输出必须从一开始就把首 token 快点返回给用户,不要让用户面对空白窗口。
并发调度在端侧不是追求同时做的任务数,而是保证主任务永远不被阻塞。永远记住这句:端侧 Agent 的并发设计,本质是选择什么任务可以被牺牲。
3. 记忆与状态:端侧 Agent 的上下文不动产管理
3.1 三层记忆模型的端侧落地
社区里讨论 Agent 记忆时经常吵“记忆是不是伪需求”,我的看法是,工程上先把记忆拆层再判断。
端侧 Agent 的记忆至少分三层。短期记忆就是当前对话的最近片断,一般放在内存里;工作记忆是当前任务进行到哪一步了,正在执行的子任务、已经完成的清单,这种状态必须持久化,否则任务中断就断片了;长期记忆是用户的偏好、历史事实、领域知识,这些要落到本地数据库里,按需检索。
做端侧记忆,存储选型不需要大上向量数据库。SQLite 加一个本地向量索引完全够用。你甚至可以用更轻的思维方式:把长期记忆拆成结构化条目,按用户 ID、时间、类型存表,需要时用关键词和向量混合召回。端侧数据量级一般不会大到需要专用向量数据库,重点是建立可增删改查的记忆索引,并且一定要给用户一条“删除全部记忆”的路径,这是合规底线。
3.2 上下文预算:把 Token 当作真正的内存容量
很多刚接触的开发者会问“AI Agent Token 是什么意思”。往浅了说,Token 就是模型处理文本的基本单位;往深了说,Token 窗口就是模型这个“打工人”一次能摆在桌面上的便签总数。端侧模型上下文往往只有 4K 到 32K,比云上动辄 200K 小得多。所以 token 在端侧不是计费概念,它是内存容量,是硬预算。
上下文预算就是问一个问题:一份 8K 的上下文,系统提示词占 1500,工具定义占 1500,当前用户请求占 500,那留给历史记忆的只剩 4500。记忆放不下怎么办?压缩。老的对话要摘要化,而不是原封不动全塞进去。下面是一个很粗糙但实用的预算函数:
def build_context(system, tool_schemas, recent_history, retrieved_memory, query, max_tokens=8000): used = count_tokens(system) + count_tokens(tool_schemas) + count_tokens(query) available = max_tokens - used if count_tokens(recent_history) + count_tokens(retrieved_memory) <= available: return system + tool_schemas + recent_history + retrieved_memory + query # 优先保最近的对话,再用摘要压缩更老的部分 recent = recent_history[-8:] memory_summary = summarize(retrieved_memory, budget=available - count_tokens(recent)) return system + tool_schemas + recent + memory_summary + query这个函数的核心思想是:长期记忆永远只取 top-k,老对话永远做摘要压缩,系统提示词和工具说明尽量精简。你还可以把摘要结果缓存下来,避免每次请求都重新压缩。
3.3 状态持久化与崩溃恢复:Event Sourcing 的思路
Agent 执行到一半被系统杀掉,是端侧必然遇到的问题。单纯把 Agent 当前上下文存储成一个 JSON 快照是不够的,因为可能刚写到一半设备断电,留下一个半新半旧的文件。
我在端侧 Agent 里比较推荐 Event Sourcing 的思路:每次交互都落一条结构化事件记录,包括用户消息、Agent 的中间思考、工具调用、工具返回值、生成的回复、记忆更新操作。恢复流程很简单,从事件日志里重放,把 SessionState 重建出来,如果最后一条事件是不完整的工具调用,就标记为失败并允许重试。
比如一个 Agent 正在执行网页转 Markdown 的工具,突然系统被强杀。重启后,你翻事件记录发现 ToolCall 已经发出但 ToolResult 没回来,那你可以直接恢复会话,提示用户“刚才的任务被打断了,是否重试”。这比把错误原样抛给用户要好得多。
3.4 记忆治理:过期、遗忘与隐私删除
长期记忆如果不治理,最后会成为垃圾场。用户的旧偏好可能已经变化,老任务的历史此前可能还有用,但半年后就是噪声。工程上要给每条记忆加时间戳、来源、置信度,定期做汇总和过期清理。
隐私删除更关键。用户说“忘记关于我的所有数据”,你要能做到数据真正删除,而不是把向量库文件翻出来还能找到残留。这就回到前面说的,设备本地的记忆存储建议用加密数据库,删除时直接销毁密钥,让历史数据彻底不可恢复。
4. 把能力做成 Skills:工具、多 Agent 与编排模式
4.1 为什么 Skills 比 Tools 更适合端侧 Agent
Tools 是单个函数调用,Skills 是一整套可独立加载的能力包。它的差别在于,一个 Skill 包含触发描述、参数结构、实现代码、依赖清单、测试用例和示例调用。你可以把 Skill 想象成系统里的一个 APP,Tools 只是 APP 里的一个按钮。
端侧尤其需要 Skill 机制,原因有三。一是设备上的工具不能无限加载,每个工具定义都会占系统提示词 token,所以你必须按场景动态挂载;二是端侧 Agent 不一定预先知道自己要面对什么任务,动态发现 Skill 列表、按描述匹配能力,是很好的扩展方式;三是 Skill 单独开发和测试,不像 Tools 那样全都绞在 Agent 主代码里。
工程上,Skill 描述写得好不好直接影响 Agent 能不能正确调用它。描述里不能只写“抓取网页”,要写明适用场景、输入输出限制、使用注意、可能的失败模式。
4.2 一个可落地的 Skill 案例:把网页保存成 Markdown
有一个在端侧很典型的 Agent 技能:用户给一个链接,Agent 把网页正文转成 Markdown,存到本地笔记。这个任务看似简单,落地时有不少细节。
Skill 描述可以这样定义:
{ "name": "web_to_markdown", "description": "Fetch a URL and convert main content into Markdown.", "parameters": { "type": "object", "properties": { "url": { "type": "string", "format": "uri" } } }, "allowed_domains": ["*.example.com", "*.wikipedia.org"], "timeout_sec": 12, "max_bytes": 2097152 }执行流程是:网页请求、HTML 编码检测、正文抽取、HTML 转 Markdown、大小截断。这里必须加 允许域名白名单,否则 Agent 拿到一个恶意链接就去请求内网地址,会激起 SSRF 风险。超时和安全限制也一样要写进参数。工具执行完以后,返回给模型的不应该是整篇 Markdown 原文,而是摘要和存储路径,否则又白白烧掉一大截上下文 token。
测试这个 Skill 时,要准备干净的 HTML、乱码编码页面、超时页面、极大网页四类用例。工具测试层面很容易跑通,真正的问题往往在真实网络环境里,所以端侧 Skill 还要加一个“离线缓存优先”的选项,避免在弱网下卡死。
4.3 多 Agent 编排在端侧的正确姿势
社区里多 Agent 概念炒得很热,但端侧设备上别秀肌肉。你不能在设备内存里同时跑“主管 Agent”“研究员 Agent”“写作者 Agent”三份模型实例,那不叫编排,那叫内存爆炸。
端侧多 Agent 的正确姿势是:共享同一个推理引擎,用不同 System Prompt 和不同 Skills 组合模拟多个角色。它们之间通过内存中的消息总线通信,消息就是普通的 JSON 事件,而不是各自独立的推理循环。严格说,这不是多个 Agent,而是同一个大脑在不同角色间的切换调度。
如果你确实需要并行处理多个任务,建议用计划者-执行者模式:一个调度 Agent 把大任务拆成多个子任务,执行时按依赖关系排队,逐个调用共享引擎。如果你的任务真的需要同时运行,那就把不需要语言模型的部分拆分到普通函数进程里,模型只负责决策,不负责计算。
4.4 框架选型参考:LangChain、Dify、CrewAI 与自研 Harness
很多准备做端侧 Agent 的人会纠结选哪个框架。以我的经验,别急着上大框架,先想清楚你的交付环境。
LangChain 和 LangGraph 的优势是编排控制流非常灵活,装饰器和状态图能做很复杂的流程,适合快速验证 Agent 逻辑。但它的体积和抽象层也很重,直接丢到端侧设备上有点笨重,更合适在有完整 Python 运行时、资源宽裕的环境里使用。
Dify 是可视化工作流平台,适合做业务场景编排,但它更像一个独立系统,适合部署在服务端做运营和管理,对端侧嵌入式场景不友好。
CrewAI 偏向多角色协作,模型调用策略很丰富,适合写作、报告、会议这类自动流程,但如果设备端只需要一个轻量助手,它的多角色抽象反而浪费。
我的结论是:端侧选型优先自研一个轻量 Harness。如果你偏好现成框架,参考 LangGraph 的状态机思路,但把运行时换成轻量实现。语言层面,Rust 是一个非常值得考虑的选择,没有 GC 意味着内存可预测,Agent 在长时间运行时不易被未知的垃圾回收卡顿拖累。市面上不少端侧 Agent 运行时就是这么设计的。
5. 端侧 Agent 的安全边界不能等上线后再补
5.1 端侧安全风险最大来源:提示注入
Agent 安全排在第一位的问题是提示注入。端侧 Agent 会读网页、读邮件、读用户笔记,这些外部内容可能携带恶意指令。如果 Agent 把这些内容直接当成需要忠实执行的指令,它就可能被操纵去删除文件、发敏感消息或者读取隐私数据。
思路是先承认 Agent 会“相信”内容,然后在工程上设闸。凡是外部内容进入 Agent 上下文时,要加上明确的边界标记,例如“以下内容来自外部网页,不代表真实指令”。同时,对工具调用的输入做格式校验和权限校验,不允许 Agent 因为一篇文章里的文字就去调用删除类接口。还要限制工具的默认权限为最小可用集合。
5.2 工具执行的沙箱化:最小权限和调用边界
工具调用是 Agent 和安全之间的摩擦面。端侧设备上,工具可能访问本地文件、联系人、短信、相机,权限一旦放大,出问题就是系统级的。工程上要按危险等级给工具分类,普通读类工具可以在规则引擎内执行,高危写类和系统类工具必须放进沙箱进程,必要时再走用户确认。
沙箱的技术选型,在 Linux 类设备上可以用 seccomp 和 namespaces 限制系统调用和文件路径,在嵌入式平台上更简单的隔离可以用独立线程加文件访问 ACL。跨平台做得省心的方案,是把工具编译成 Wasm 运行在受限虚拟机里,工具本身拿不到系统资源,只能通过宿主暴露的窄接口通信。
不管用什么方案,以下几条底线必须守住:断网工具默认拒绝访问非白名单域名、文件工具默认只能访问 Agent 工作目录、任何工具不能读取系统密钥和登录态。
5.3 密钥与敏感数据的设备端规范
端侧 Agent 一旦接云端模型或第三方服务,就需要处理 API Key、用户令牌这类机密。很多开发为了方便,直接把密钥写死在配置里,甚至塞进 Prompt 让模型“记住”,这是绝对不可行的。
正确做法是设备端有一个 Credential Manager,密钥只经过它下发到工具层,模型本身永远看不到明文。Android 上用 Keystore,iOS 上用 Keychain,Linux 设备上用 keyring,实在不支持的芯片平台也要用带硬件加密的存储区域。密钥调用要加审计日志,谁在什么时间用哪个 Key 访问了什么服务,全程留痕。云端侧的密钥轮换策略端侧也一样需要。
5.4 用户确认机制:把高危操作权交回给用户
最后一道防线是用户确认。Agent 要读取屏幕内容、分享文件、发送消息、开启摄像头或麦克风,都应该弹出确认。确认弹窗不能做成“每次必弹但用户在 0.5 秒内盲点同意”的鸡肋,要做成按操作等级动态触发的机制。
比如 Agent 想在本地读一个文本文件给总结,这是低风险,不需要打断用户;但它想把一段文本发送到某个网络服务,就需要用户确认。用户确认在端侧还有一个额外作用:它逐渐为用户建立对 Agent 的信任模型,让他清楚地知道哪些操作被自动执行了,哪些操作 Agent 会争取授权。
6. 测试、可观测性与故障恢复:把端侧 Agent 当生产系统养
6.1 测试金字塔:从函数单元到设备真机
端侧 Agent 的测试,不能只测“模型输出像不像样”,要测的是 Agent 的决策轨迹是否符合预期。我建议分三层。
第一层是单元测试,覆盖工具函数和 Skill 本身,比如网页转 Markdown 技能在各种输入下的表现。第二层是轨迹回归测试,拿一批固定的历史对话记录,回放 Agent 循环,断言每次工具调用顺序、参数、错误处理是否符合预期。第三层是设备端到端测试,在真实手机上跑完整流程,看内存峰值、耗电、首次响应时间和崩溃率。
轨迹回归测试是 Agent 特有的环节,因为你不能断言模型回复的文本一模一样,但你可以断言它是否选择了正确的工具、是否合理处理了输入、有没有在超时后给出降级响应。这样做最值得花时间。
6.2 可观测性:事件日志比指标更重要
云服务习惯看吞吐量、延迟、错误率,端侧 Agent 更需要看事件轨迹。你要能精确知道某一个会话里,模型被调用了多少次,每次用了多少 token、耗时多少;工具被调用了哪些,哪个超时;记忆检索命中了哪几条;最后是谁终结了循环。
实现上不复杂,用结构化 JSON Lines 日志即可,每个事件带 session_id、trace_id、时间戳。每次模型调用、工具调用、记忆更新、错误捕获都是一条事件。日志写到本地文件,偶然上传或由用户主动导出。有了事件轨迹,线上问题就不再是黑盒。
6.3 失败恢复:把“Agent execution terminated due to error”翻译成人话
端侧 Agent 上线后最常见的报错就是一句冷冰冰的英文错误。用户不懂模型为何终止,开发也难排查,两边都很痛苦。工程上要把错误分类处理。
瞬时错误如模型服务超时、NPU 资源暂时不可用,可以做有限次指数退避重试。永久错误如非法输入、疑似注入、用户取消,直接终止并返回可读原因。还有一类降级错误,比如端侧模型内存不足,这时可以回退到预先缓存的多轮应对模板,至少不让用户面对空白。错误码要暴露在日志里,但展示给用户的必须是行动提示,例如“网络超时,请重试”或“该操作未被允许,已停止”。
6.4 给开发者的四步学习路径
如果你刚开始接触 Agent 工程化,不要一上来就研究多 Agent 编排和复杂记忆机制。我的建议是走一条递进路径。第一步,用一个现成框架把单 Agent 跑通,搞懂“模型调用-工具调用-观察结果”这个循环。第二步,研究一个 Harness 实现,自己写一个极简版,只做超时、重试、事件日志三件事。第三步,给 Agent 加一个 Skills 机制和本地记忆,做一个能跨重启恢复会话的小项目。第四步,给系统加安全边界和测试,模拟各种坏输入,把错误恢复补齐。走完这四步,你对端侧 Agent 工程化的理解会远远超过只读框架文档的人。
最后说一个我自己在端侧 Agent 项目里比较实际的体会:不要一上来就贪多,多 Agent、超长记忆、复杂工具链这些都先放一放。把一个最小闭环做到极致——一个模型、三个按需加载的 Skills、一套可靠的状态持久化、一份干净的事件日志——端侧产品的稳定性就能超过大多数热闹的 Demo。先把“不崩、可恢复、知道为什么失败”这三件事做扎实,再谈更多能力扩展,这是我踩过很多次坑之后最想分享的一条经验。