很多人聊端侧 Agent,聊的是模型、是推理框架、是“把 7B 模型塞进手机”。但真把 Agent 落地的团队都清楚,模型只是第一公里,后面还有一整套工程问题:记忆放哪、上下文怎么省、工具怎么调度、死循环怎么兜底、多轮对话怎么不出错。我自己踩过不少坑,也看过不少团队在“模型跑通了,Agent 却跑不起来”的阶段卡住——问题基本都出在工程化,而不是模型。
这篇文章是“深入理解端侧 Agent”系列的第三篇,聚焦 Agent 工程化的上半场。我会围绕框架选型、记忆设计、上下文预算、编排与工具链这几个方向展开,再梳理几个我实测中遇到的高频问题。适合正在做端侧 Agent、或打算把 Agent 从 Demo 推向产品的工程同学参考。
1. 端侧 Agent 工程化到底在工程化什么
1.1 先给“端侧 Agent”画个边界
先说清楚端侧 Agent 是什么。模型只是大脑,Agent 是一个完整的系统:感知输入、规划任务、调用工具、操作记忆、给出反馈,然后根据反馈继续迭代。在端侧跑,意味着这些全部要在用户的手机、PC、车载设备或智能硬件上完成,至少大部分计算在本地完成。
所以“端侧 Agent 工程化”不是“把模型文件压缩一下然后部署到手机上”——那是 MLOps / 端侧推理的范畴。工程化要解决的是:Agent 作为一个常驻软件系统,如何在有限算力、有限内存、不稳定的网络环境和碎片化设备上,稳定、安全、可预期地完成多轮任务。
我见过最好的 Agent 改造方向,也见过最差的。最差的是把 Agent loop、工具调用、记忆全部揉在一个 Service 里,所有状态都放内存,最后用户多问几句就开始胡说八道。最好的则是把系统边界画得清清楚楚:每一步都知道状态在哪、花了多少 token、下一次该做什么。
1.2 端云差异带来的四大约束
端侧 Agent 和云端 Agent 相比,有几个工程上绕不开的差异,直接决定了方案取舍。
| 维度 | 云端 Agent | 端侧 Agent |
|---|---|---|
| 上下文窗口 | 通常可以给 8K、32K、128K | 4K~8K 是常态,撑死 16K,且 KV cache 占内存巨大 |
| 算力 | GPU 集群,可以暴力解码 | CPU/NPU/GPU 混合,内存带宽是硬瓶颈 |
| 网络 | 常驻,延迟低 | 可能离线,网络不可靠 |
| 升级 | 服务端热更新,秒级生效 | 要发版、要等用户同意、要处理旧版本残留 |
| 隐私 | 数据上云,合规成本高 | 本地数据,但也要防止端侧能力滥用 |
| 功耗与发热 | 基本不考虑 | 长时间推理会导致设备发热,用户体验直接崩 |
这六条里头,最容易被低估的是上下文窗口和功耗。上下文窗口决定了你必须在端侧做“上下文压缩”,而不是把整段历史丢给模型。功耗则决定了你不能让模型持续嗡嗡运行,必须在“响应延迟”和“token 预算”之间做用户可感知的取舍。
1.3 工程化的三个优先级:先跑通、再稳定、再聪明
很多团队一上来就上多 Agent、上 RAG、上复杂规划器,结果 Demo 都跑不通。我的经验是,端侧 Agent 工程化要严格按三个优先级推进:
- 先跑通最小闭环:感知用户意图 -> 调用少数工具 -> 返回可验证的结果。哪怕只有两个工具、不做记忆压缩,也要先把主链路打通。
- 再稳定:解决工具调用失败、token 超限、死循环、重复执行等稳定性问题。这个阶段的价值远超任何“聪明”特性。
- 最后再变聪明:加入记忆、检索、多 Agent 协作、主动学习等能力。
为什么这个顺序重要?因为端侧的调试成本远高于云端。云端出错你可以在服务器上打日志、看 trace,几分钟定位;端侧出错,用户只会感受到“卡了一下”或“没反应”,而你要从多个设备上报的日志和数据中还原现场,成本高一个数量级。基础不稳定,任何上层智能都是空中楼阁。
2. 从 Demo 到产品:Agent 运行时与框架选型
2.1 Harness 到底是什么玩意
“Harness”这个词最近特别火,很多人第一次听是在研究 agent 评测或 agent 框架时。它和“框架”并不是一回事。
框架是一个比较泛的概念,指你做 Agent 时用的依赖库,比如 LangGraph、Coze、Dify。而 Harness 在 Agent 语境下,指的是围住模型推理循环的那一层“管线”:输入怎么进、输出怎么解析、怎么调用工具、怎么处理错误、什么时候停止、如何记录 trace。你可以理解成,模型是一个发动机,Harness 是引擎盖下面把所有管路接好的那套系统。
很多时候我们说的“Agent 框架”,其实核心就是一套 Harness。把它单独拎出来讨论,是因为你在端侧做 Agent,不一定非要用完整的通用框架,但无论如何都需要自己的 Harness。哪怕手写 500 行循环,那也是 Harness。它决定了 Agent 的行为边界,也决定了你能不能高效开发、测试、监控。
2.2 自研、通用框架、端侧方案怎么选
我总结了三条路线,成熟度和匹配对象完全不同:
第一条:直接用通用 Agent 框架(LangGraph、Dify、Coze 等)
- 优点:生态好、工具多、圈子大,社区踩坑经验丰富。
- 缺点:绝大多数通用框架假设云端部署,依赖 Python 运行时、大量依赖库、服务常驻内存,直接搬到手机/PC 端会非常难受。强行套壳的后果是包体积暴涨、启动时间变长、权限模型和端侧不一致。
- 适合:服务端编排 + 端侧执行的混合形态,或早期快速验证。
第二条:基于端侧推理框架自研轻量 Harness(llama.cpp、onnxruntime、MediaPipe LLM Inference API 等)
- 优点:完全受控,行为可预测,资源占用极致可控,适合嵌入式平台。
- 缺点:什么都要自己写,包括解析、工具调用、记忆管理、并发保护,初期成本高。
- 适合:已经有稳定端侧团队、Agent 场景明确、不想被框架绑定。
第三条:混合——云上编排 + 端侧推理/端侧工具执行
- 优点:兼顾灵活性、性能和生态。复杂规划放云端,端侧负责低延迟响应和敏感操作。
- 缺点:架构复杂,网络不可用时能力降级明显。
- 适合:产品形态本身就是“主端协同”,或者希望快速迭代 Agent 逻辑但保留端侧体验优势的团队。
我的建议是,如果团队还处于第一个端侧 Agent 产品阶段,不要急着选通用框架;先用最简单的“循环 + 工具注册表 + 状态日志”手写一套 Harness,跑通之后再看哪些环节需要引入更重的框架。通用框架解决的问题是“开发效率”,但在端侧你首先面对的是“资源约束”,框架的抽象层往往会掩盖关键问题。
2.3 一个能上线的极简分层
无论选什么框架,最终系统一定长这样(从下往上):
- 执行层:负责真正跑模型推理,对流式输出、上下文缓存、停止条件负责。端侧这层要接推理引擎,比如 llama.cpp 或 MediaPipe。
- 工具层:所有可被 Agent 调用的能力,统一定义成 JSON Schema 描述的函数或本地接口,比如读相册、发通知、访问日历、执行快捷指令。
- 记忆层:保存工作记忆和长期记忆,提供读写和检索能力。端侧通常用 SQLite 或本地 KV 存储。
- 规划层:根据用户请求和工具描述生成执行计划。可以是 ReAct 式的逐步推理,也可以是 Plan-Execute 式的先规划再执行。
- 校验层:最后对 Agent 产生的结果做校验,包括权限校验、输出格式校验、安全性检查,避免模型幻觉直接落到用户界面。
每层之间只通过定义好的接口通信,状态尽量不放在全局。这样排查问题的时候,你能一眼看出是模型输出问题、工具返回问题,还是记忆读取问题。这个极简分层我建议所有团队都照着搭一遍,哪怕最后你用了某个现成框架。
3. 端侧 Agent 的记忆与上下文工程
3.1 别把记忆只当成“聊天记录”
“记忆”是 Agent 工程化里最容易被搞错的概念。很多人以为记忆就是保存用户说了什么,然后塞回 prompt。实际上,记忆是一个立体结构,至少要分三层:
工作记忆(Working Memory):当前任务进行中产生的临时信息,比如用户本次提出的需求、正在执行的工具输出、当前规划步骤。它存在于单次会话,甚至单次任务的上下文里,任务结束就可以丢弃。热词“agent 存储 working memory”指的就是给这层做持久化,目的是支持任务中断恢复。
长期记忆(Long-term Memory):用户长期偏好、历史诉求、重要事实,比如“用户习惯把日程安排在上午”“用户上次让我处理过这类文件”。这部分适合存向量 + 结构化摘要,端侧存放,不上云。
程序性记忆(Procedural Memory):Agent 会用的技能和工具,比如“如何格式化文档”“如何调用系统通知”。在端侧 Agent 里,这层通常表现为 Skill 注册表和工具描述库,是 Agent 后天的“肌肉记忆”。
在端侧,这三层都受存储容量和查询耗时的限制,所以记忆工程的核心其实是三个字:丢得掉。很多人做了记忆之后,反而把 Agent 变笨了,因为检索出来的旧记忆干扰了当前判断。所以端侧记忆体系的第一原则是:默认不加载,需要时再查。
3.2 上下文预算怎么算
端侧 Agent 的上下文窗口是单选题,不是主观题。以 4K 上下文为例,你需要把预算分给:
| 用途 | 预算 | 说明 |
|---|---|---|
| 系统指令 | 300~500 token | 角色定义 + 行为规则,尽量精炼 |
| 工具描述 | 600~1200 token | 每个工具的 name 和 JSON Schema 都会占 token |
| 历史摘要 | 500~1000 token | 压缩后的对话摘要,而不是原文 |
| 当前请求 | 300~800 token | 用户当前输入 |
| 模型输出 | 剩余 | 必须给生成留出余量,否则容易中途截断 |
一个 4K 的上下文,真正能拿来存“任务过程”的,其实只有很小的比例。所以工程上的关键动作是:把工具描述做成精简版,系统指令做成模板,历史摘要动态压缩。
我实测下来,端侧 Agent 的上下文分配公式大致是:
可用上下文 = 窗口大小 - 系统指令 - 工具描述 - 保留输出余量
比如窗口 4096,系统指令 400,工具描述 800,保留输出 800,那给历史和用户输入的预算只有 2096 token。再扣掉用户正在输入的内容,历史摘要实际上最多分到 1300 左右。如果你的 Agent 有五个工具、每个 Schema 两百 token,那工具描述就吃掉 1000,历史预算会进一步萎缩。所以控制工具数量、控制 Schema 篇幅,是端侧上下文优化的第一课。
3.3 摘要压缩与检索的实操做法
在端侧做记忆,我推荐一套组合拳:
短期对话用滑动窗口 + 摘要:每轮对话结束后,把旧的几条消息交给模型生成一段摘要,替换原文。摘要消息本身带着一个时间戳,下次请求时优先参考最近摘要。注意:摘要输入本身也占 token,所以摘要也要每 N 轮做一次二级压缩,避免摘要积累成新的“长尾巴”。
长期记忆用向量检索 + 结构化 KV:端侧向量库首选 SQLite + 简单的 embedding 表,不一定要上独立的向量数据库。索引维度不要太高,128~384 维足够。Chunk 大小建议 128~256 token,Overlap 建议 16~32 token。检索 topK 在端侧通常取 3~5 个,多了既耗时又容易引入噪声。
程序性记忆用 Skill 清单 + 按需加载:不要把 20 个 Skill 的描述全部塞进上下文。端侧做法是在系统指令里只写一个“你有以下能力”的目录行,具体描述按需查询:模型先输出“需要读相册能力”,Harness 再查出该能力完整描述注入下一轮。这个机制能省一大截 token。
说到检索,我特别想提醒:端侧 embedding 模型质量有限,检索到的结果不一定相关。所以任何检索注入内容的开头,都要带上显式标记,比如“【记忆检索】以下是用户历史偏好,可能与当前任务相关,但请以用户最新指示为准”。这个分隔手段能显著降低端侧模型把旧记忆当成事实的概率。
3.4 我的记忆工程踩坑记录
第一坑:记忆污染。我早期把用户历史摘要直接拼接进系统指令,结果用户在某次会话里说“我不喜欢某 App”,之后好几轮 Agent 都反复提这个偏好,因为它在摘要里被留了下来。后来我规定:摘要只记录用户明确表达的持久偏好,而且每次引用偏好时必须带上“用户曾说过”的引述语气,而不是作为事实陈述。
第二坑:上下文超限不报错。端侧推理引擎遇到超过窗口的内容,通常不是报错而是悄悄截断。结果就是 Agent 莫名失忆,用户以为你傻了。对策很直接:每次组装 prompt 前做 token 计数,超过预算就强制做摘要压缩,宁可丢细节也不要让上下文被截断。
第三坑:检索召回了完全无关的内容。有一次用户问“帮我设置闹钟”,检索层把长期记忆里的“用户喜欢在睡前听播客”给拉了出来,Agent 就开始聊播客。后来加了打分阈值,低于阈值直接不注入,模型没有历史可用时反而表现更稳定。
4. Agent 编排与工具调用的工程细节
4.1 编排方式:ReAct、Plan-Execute 与状态机
Agent 的编排方式决定了它处理复杂任务的上限。端侧最主流的两种:
ReAct:边想边做,每一步都基于模型输出决定下一步。优点是很灵活,缺点是每一步都要推理,token 开销大,而且模型在长任务里容易迷失方向。适合单一、直接的任务。
Plan-Execute:先让模型制定一个计划,按步骤执行,每执行完一步再确认下一步。优点是省 token、行为更可控,缺点是计划可能不合理,而且中途出现意外时不够灵活。适合操作链较长但步骤明确的场景。
端侧 Agent 我强烈建议默认 Plan-Execute 变体:先让模型输出任务列表,然后 Harness 按顺序执行,每步结束后把结果追加到上下文,再让模型判断是继续、修复还是终止。这本质上就是一个轻量状态机。你可以把每一步的状态定义成pending / running / success / failed / retry,用状态机约束模型的自由度。
为什么端侧不适合全自由 ReAct?因为 token 预算有限。ReAct 的“思考”会消耗大量上下文,在端侧尤其致命。Plan-Execute 把思考集中在开头,执行阶段只做“确认”,能省下超过一半的 token。
4.2 Function Calling 在端侧的坑
端侧模型的 Function Calling 能力远不如云端大模型,这是所有做端侧 Agent 的人迟早要面对的现实。具体表现:
- 经常不按 Schema 输出,或者输出残缺 JSON;
- 同一个工具描述,云端模型准确率高,端侧模型会漏参数;
- 工具返回错误后,模型会重复调用同一个工具十几次,形成死循环。
我在项目里踩得最深的一个坑就是:工具返回“未找到文件”,模型偏不信,反复用同一参数重试同一工具,直接烧掉了整个上下文。之后我给 Harness 加了三道保险:
- 解析容错:模型输出不完整 JSON 时,不直接报错,而是做一次修复尝试。比如补全引号、合并残缺字段、提取嵌套 JSON;实在解析不了,再返回“格式错误”。
- 最大重试次数:同一个工具调用失败后,重试次数默认控制在 2 次以内;第二次失败直接返回给用户“这个操作暂时没完成”,而不是继续循环。
- 工具输出截断与摘要:文件内容太长时,不给模型全文,只给前 200 字符 + “内容已被截断,如需更多请调用查看指定段落工具”。这样既保上下文,又避免模型读一半就开始编。
还有一点很实用:工具描述里用示例。端侧模型对“参数格式”的理解高度依赖示例,与其写严谨的 JSON Schema,不如在 description 里写“例如:查询用户名为 admin 的文件时,传入 {owner: "admin"}”。实测下来,示例比纯 Schema 对端侧模型友好得多。
4.3 Skill 是工具调用的标准化进阶
最近 Skill 这个词热度很高,其实它在端侧工程里的价值比在云端更明显:因为 Skill 是“工具调用 + 多步流程 + 局部记忆”的打包单元。
一个端侧 Skill 的标准形态应该是:一个 manifest 文件描述触发条件、输入输出参数、权限使用范围和执行步骤,一段可执行的本地代码或脚本,以及一段模型可见的“何时使用我”的描述。下面这个 manifest 是我在项目里常用的简化模板:
{ "name": "set_reminder", "description": "在本地日历中创建提醒事项。当用户要求设置提醒/闹钟时使用。", "trigger": { "keywords": ["提醒", "约会", "待办", "remind"] }, "inputs": { "title": {"type": "string", "required": true, "example": "下午三点开会"}, "time": {"type": "string", "required": true, "example": "15:00"}, "repeat": {"type": "string", "required": false, "enum": ["none", "daily", "weekly"]} }, "permissions": ["calendar:write"], "steps": ["parse_datetime", "create_event", "notify_user"] }把工具升级成 Skill,核心收益是幂等性。比如“设置提醒”这个 Skill,如果模型因为第一遍执行没返回成功信息,又调用了一次,你不能再创建一个重复提醒。所以每个端侧 Skill 都应该自带去重逻辑:要么在执行前检查时间+标题是否已存在,要么执行后返回一个 operation_id,重复调用时直接返回上次结果。
另外,Skill 也要有版本。端侧升级麻烦,Skill 一旦被多个流程引用,改参数格式会牵连很多旧上下文。我的习惯是 manifest 里加 version 字段,模型调用时如果发现版本不匹配,Harness 直接拒绝并返回“该技能已升级,请重新描述需求”。
4.4 多 Agent 协同别急着上
“多 Agent”这两年特别火,但在我看来,端侧场景 90% 的情况不该上多 Agent。原因很简单:端侧每多一个 Agent 循环,就多一份上下文开销、多一份状态管理复杂度、多一条出错的链路。端侧的多 Agent,应该专注做角色切分,而不是做“群聊式协作”。
我推荐一种保守但实用的模式:一个主控 Agent + 若干个工具型 Agent。主控 Agent 负责任务拆解,工具型 Agent 只专注执行自己的特定领域(比如信息检索、系统控制),把结果传回主控。它们之间不自由对话,只有主控发指令、子 Agent 返回结果。这样可以避免多 Agent 之间的“幻觉传播”和 token 爆炸。
有一点需要提醒:子 Agent 的返回结果也必须经过校验层,不能默认“子 Agent 比我聪明所以结果绝对可靠”。在我测试中,端侧子 Agent 在领域内确实表现更专注,但一旦超出它的领域,它也会一本正经地编造结果,其幻觉程度和主控 Agent 半斤八两。
5. 常见问题与排查技巧实录
5.1 一次真实死循环 Debug 过程
说一次我最典型的排查经历。用户要求“帮我整理桌面上的文件并归类”,Agent 的规划是先列出桌面文件,再逐个移动。但半天没有任何输出。查 trace 之后发现,Agent 第一轮输出了一个不完整的文件列表,Harness 解析失败,于是模型又重试了一遍 list_files 工具。list_files 返回了正常结果,但模型没有按计划继续,而是再次调用了 list_files,因为它的上下文里堆满了历史和失败信息。就这样连续三次 list_files,第四次才勉强开始移动文件,但此时上下文已经快被塞满,移动文件的调用参数又被截断了。
排查思路很简单:分阶段看 trace。第一步看模型输出是否合理,第二步看工具返回是否可解析,第三步看控制流是否进入死循环。结果发现三个环节都有问题:模型不该重试那么多次、Harness 不该允许同样参数重复调用、上下文管理本该在第一次失败时就做一次历史摘要压缩。
修复方案也对应三条:引入“相同工具+相同参数不允许连续调用超过一次”的规则;解析失败时先做摘要 压缩再重试;每次工具调用完成强制检查剩余上下文预算,低于阈值时立即触发压缩。
5.2 高频问题速查表
| 问题 | 根因 | 解法建议 |
|---|---|---|
| Agent 反复调用同一工具 | 模型卡在重试循环,缺终止条件 | 同一工具同参数连续调用不超过 2 次;加超时和循环保护 |
| 工具返回未格式化内容 | 端侧模型 JSON 输出不稳定 | 引入解析修复层;结果先截断再注入上下文 |
| 对话久了 Agent 变笨 | 上下文被历史撑爆,关键信息被截断 | 做滑动窗口 + 摘要压缩;实时监控 token 预算 |
| 记忆检索乱入无关内容 | 阈值过低或 chunk 切分不合理 | 加阈值;缩小 chunk;在注入内容上加“仅供参考”标记 |
| 任务执行一半闪断 | 设备资源不足或 App 被系统回收 | 工作记忆持久化到 SQLite;启动时恢复未完成任务 |
| 新 Skill 上线后旧流程异常 | 版本不匹配,模型还在按旧参数调用 | Skill 带版本号;Harness 拒绝旧版本调用并提示用户重新描述 |
| 模型输出安全内容被用户投诉 | 端侧模型安全对齐弱 | 在系统指令中强调边界;校验层加入输出过滤;敏感操作二次确认 |
| 设备发热严重 | 连续推理时间过长 | 分段推理;显示层提前输出“正在处理”;限制单次任务推理轮数 |
5.3 给新手几个避坑建议
围绕端侧 Agent 工程化,我最后给几条具体的建议,都是踩过坑之后沉淀下来的:
一,工具数量宁少勿多。端侧每多一个工具,模型的选择焦虑就高一分,token 占用也高一截。先保留最核心的 5~8 个工具,验证链路稳定后再逐步增加。
二,所有工具都要幂等。被重复调用是常态,不是意外。像“发送消息”“创建文件”“下单”这类有副作用的操作,必须设计 operation_id 去重机制。
三,日志里必须有 request_id。端侧排查问题时,没有 request_id 几乎等于没有现场。从入口到工具调用再到模型生成,全程贯穿同一个 ID,才能把分散的日志串成一条完整 trace。
四,模型版本冻结要谨慎。端侧模型升级不像云端改个参数那么轻量,一旦新版模型在真实场景表现不如旧版,回滚成本极高。上线前必须准备一个包含 30~50 个真实用户任务的回归集,自动跑一遍再决定。
五,状态保存比模型推理更重要。Agent 进程可能随时被杀,但只要状态在,用户就不会觉得“它失忆了”。尽量把每一步的状态变化都落到 SQLite 事务里,而不是只存在内存里。
六,不要迷信“全端侧”。很多操作如复杂知识检索、高难度写作,端侧模型的能力确实有限。务实的做法是设计好降级链路:能本地完成就本地,本地判断不了再走云端辅助,并明确告知用户当前处理方式。
做端侧 Agent 工程化这段时间,我最大的体感是:模型能力决定天花板,工程能力决定你能不能摸到那层天花板。很多团队抱怨端侧模型不够聪明,但实际排查下来,更多问题出在工具调用失控、记忆管理混乱、上下文被截断这些工程环节上。先把 Harness 做好,把状态管清楚,把每个失败路径都想明白,再回头看模型能力,你会发现事情比想象中简单。下一篇我会接着聊端侧 Agent 的并发处理、安全沙箱设计和端云协同的降级策略,这些都是“工程化”下半场的主战场。