
1. 端侧 Agent 工程化到底在解决什么问题1.1 从“能跑”到“跑得住”的鸿沟端侧 Agent 和云端 Agent 最大的区别不在于模型大小而在于资源边界是硬约束。云端你可以随便堆 GPU、堆内存、堆并发端侧不行——手机就是 8GB 内存车机可能只有 4GB 可用IoT 设备更是只有几百 KB。所以端侧 Agent 工程化的核心命题只有一个在有限资源下让 Agent 稳定、可预期地完成多步任务。我见过太多团队在 Demo 阶段跑通了一个端侧 Agent觉得“成了”结果一上真实设备就崩。崩的原因五花八门内存碎片导致第三次推理就 OOM、工具调用返回超时后整个链路卡死、多轮对话到第五轮上下文爆了、模型输出格式偶尔跑偏导致解析器直接抛异常。这些问题在云端可以用“重启一个 Pod”解决在端侧不行——用户不会接受你的 App 每隔五分钟闪退一次。所以工程化要解决的不是“能不能做出来”而是“能不能在 2GB 可用内存、单核 CPU、没有网络兜底的条件下连续稳定运行 30 分钟以上”。这个目标决定了后面所有的架构选型和实现细节。1.2 端侧 Agent 的四个工程化支柱我把端侧 Agent 工程化拆成四根支柱缺一不可资源调度模型加载、推理、工具执行三者共享同一块内存池必须做统一预算和回收。你不能让模型占着 1.5GB 不放然后工具调用再申请 500MB。编排容错Agent 的本质是“LLM 决策 工具执行”的循环。端侧环境下LLM 可能输出非法 JSON工具可能超时循环可能死锁。每一步都要有超时、重试、降级。状态管理多轮对话的上下文、工具调用的中间结果、Agent 的 scratchpad这些状态在端侧不能无限增长。必须有截断、摘要、持久化策略。可观测性端侧没有云端那么方便的日志系统但你必须能在出问题时定位到是哪一步、哪个工具、哪次推理出了岔子。轻量级埋点是刚需。这四根支柱里编排容错是最容易被低估的。很多人觉得“我调个 LLM解析个 JSON执行个函数能有多难”——难就难在端侧 LLM 的输出稳定性远不如云端大模型。一个 3B 参数的端侧模型在量化到 4bit 之后输出格式遵循能力会明显下降。你让它输出{tool: weather, city: 北京}它可能给你输出{tool: weather, city: 北京——少一个右花括号。云端你可以重试端侧重试一次就是几百毫秒的延迟和额外的电量消耗。2. 编排框架的选型逻辑与端侧适配2.1 为什么不能直接搬云端编排框架LangChain、LlamaIndex 这些云端编排框架设计假设是“网络可靠、内存充足、CPU 随便用”。它们的抽象层次很高一个 Chain 可能嵌套五六层每层都有回调、有日志、有异常处理。在端侧跑光是框架本身的初始化就可能吃掉几百毫秒和几十 MB 内存。更致命的是这些框架大量依赖动态反射和运行时类型检查。在 Android 上反射调用比直接调用慢一个数量级在 iOS 上Swift 的运行时虽然快一些但频繁的协议 witness table 查找也会累积成可观的开销。端侧 Agent 的编排层必须薄——薄到你能一眼看清每一步在干什么薄到没有隐藏的内存分配。我的经验是端侧 Agent 的编排框架核心代码不应该超过 2000 行。超过这个数说明抽象过度了。你需要的是一个状态机 工具注册表 超时控制器而不是一个通用编排引擎。2.2 自研轻量编排器的核心设计我推荐的自研编排器结构是这样的AgentLoop: 1. 构建 prompt系统提示 历史 当前输入 2. 调用 LLM 推理带超时 3. 解析输出JSON 解析 格式校验 4. 如果输出是工具调用 a. 查找工具注册表 b. 执行工具带超时 重试 c. 将结果追加到 scratchpad d. 回到步骤 1 5. 如果输出是最终答案 a. 返回答案 b. 清理 scratchpad这个循环看起来简单但每个环节都有坑。比如步骤 2 的 LLM 推理在端侧你可能用的是 llama.cpp 或者 MLC-LLM它们的 API 是流式的。你不能等整个输出生成完再解析——那样延迟太高。你需要流式解析一边生成 token一边尝试解析 JSON。一旦解析成功立即中断生成执行工具。这能省下大量时间。再比如步骤 3 的 JSON 解析端侧模型经常输出带 markdown 代码块的 JSON比如json {tool: weather, city: 北京}你的解析器必须能处理这种情况。我的做法是先用正则提取 {...} 或 [...] 之间的内容再做标准 JSON 解析。如果解析失败尝试修复常见错误补全缺失的引号、补全缺失的括号、把单引号替换成双引号。修复失败才走重试或降级。 ### 2.3 工具注册表的设计要点 工具注册表是端侧 Agent 的“手脚”。每个工具就是一个函数有名字、有参数 schema、有超时时间、有重试策略。在端侧工具执行必须**同步且可中断**。你不能让一个工具调用阻塞整个 Agent 循环超过 5 秒——用户会以为 App 卡死了。 我的工具注册表长这样 kotlin data class Tool( val name: String, val description: String, val parameters: MapString, ParamSpec, val timeoutMs: Long 3000, val maxRetries: Int 1, val execute: (MapString, Any) - ToolResult )关键点是timeoutMs和maxRetries。端侧工具大多是本地操作读文件、查数据库、调系统 API正常情况下 100ms 内完成。如果超过 3 秒大概率是出了问题比如文件锁死、数据库损坏这时候重试一次就够了再不行就返回错误让 LLM 决定下一步。注意端侧工具执行千万不要放在主线程。Android 上用Dispatchers.IOiOS 上用DispatchQueue.global()。但也不能随便开新线程——线程切换本身有开销频繁的工具调用会导致线程池爆炸。我的做法是维护一个固定大小的工具执行线程池通常 2-4 个线程所有工具调用都提交到这个池里。3. 容错机制让 Agent 在端侧“摔不死”3.1 LLM 输出异常的三种典型场景端侧 LLM 的输出异常我归纳为三类第一类格式跑偏。你要求输出 JSON它输出了一段自然语言解释最后才跟一个 JSON。或者 JSON 里混入了注释。或者用了中文引号“”而不是英文引号。这类问题占所有异常的 60% 以上。第二类工具名幻觉。LLM 编造了一个不存在的工具名比如你注册了get_weather它输出了fetch_weather。或者大小写不一致GetWeathervsget_weather。这类问题占 20% 左右。第三类参数缺失或类型错误。工具需要city参数LLM 没给。或者需要整数LLM 给了字符串25。这类问题占 15% 左右。剩下的 5% 是更诡异的输出截断生成到一半达到 max_tokens、输出重复陷入循环、输出乱码tokenizer 问题。3.2 分层容错策略针对这三类异常我的容错策略是分层的第一层Prompt 工程。在系统提示里明确给出 JSON schema并给出一个 few-shot 示例。示例要简单、格式严格。比如你必须严格按照以下 JSON 格式输出不要添加任何其他文字 {tool: 工具名, parameters: {参数名: 参数值}} 示例 用户北京天气怎么样 助手{tool: get_weather, parameters: {city: 北京}}第二层解析器容错。前面提到的正则提取 JSON 修复。修复规则包括去除 markdown 代码块标记、替换中文引号、补全缺失的右括号、去除尾随逗号。这些规则能解决 80% 的格式问题。第三层工具名模糊匹配。如果解析出的工具名不在注册表里用编辑距离Levenshtein distance找最接近的工具名。距离小于等于 2 就认为是同一个工具。比如fetch_weather和get_weather的编辑距离是 3f→g, e→e, t→t, c→g, h→e... 实际算一下是 4那就超过阈值了。这时候更好的做法是维护一个别名表fetch_weather→get_weatherGetWeather→get_weather。别名表可以手动维护也可以从历史日志里自动挖掘。第四层重试与降级。如果前三层都失败了走重试。重试时在 prompt 里追加一条错误信息“你上次的输出格式不正确请严格按照 JSON 格式重新输出。” 重试一次通常能解决大部分问题。如果重试还失败降级到“直接回答”模式不调用工具让 LLM 基于已有知识回答。虽然可能不准确但至少不会卡死。3.3 超时与死循环防护端侧 Agent 最怕的是死循环。LLM 可能反复调用同一个工具或者反复输出相同的错误格式。我的防护措施是最大循环次数默认 10 次。超过 10 次强制退出返回“任务过于复杂请简化后重试”。重复检测如果连续两次工具调用完全相同工具名 参数中断循环。总超时整个 Agent 循环的总时间不超过 30 秒。超过就中断。Token 预算整个循环消耗的 token 数不超过模型上下文窗口的 80%。超过就触发上下文截断或摘要。这些阈值不是拍脑袋定的。10 次循环是基于端侧模型的能力——一个 3B 模型在 10 步之内解决不了的任务再多给 10 步也大概率解决不了。30 秒总超时是基于用户耐心——超过 30 秒的等待用户会认为 App 卡死。80% 上下文窗口是留出空间给系统提示和工具返回结果。实操心得这些阈值最好做成可配置的并且在开发阶段打开详细日志。我通常会在端侧维护一个环形缓冲区记录最近 100 次 Agent 循环的每一步耗时、token 数、工具调用结果。出问题时让用户导出这个缓冲区比让用户描述“它卡住了”有用得多。4. 状态管理与上下文压缩4.1 端侧上下文的硬约束端侧 LLM 的上下文窗口通常比云端小得多。一个 3B 模型可能只有 4K 或 8K token 的上下文窗口。而 Agent 的多轮对话 工具调用结果很容易就撑爆这个窗口。我算过一笔账系统提示 200 token用户输入 50 tokenLLM 输出 100 token工具返回结果 200 token。一轮下来就是 550 token。10 轮就是 5500 token。如果模型只有 4K 窗口第 8 轮就爆了。所以端侧 Agent 必须有上下文压缩机制。压缩策略有三种滑动窗口只保留最近 N 轮对话。简单粗暴但会丢失早期的重要信息。摘要压缩用 LLM 把早期对话总结成一段短文本。效果好但需要额外的 LLM 调用增加延迟和电量消耗。关键信息提取从早期对话中提取实体和意图存成结构化数据。比如用户说“我要订明天去北京的机票”提取出{destination: 北京, date: 明天, action: 订机票}。后续对话只带这个结构化数据不带原始文本。我的做法是滑动窗口 关键信息提取。滑动窗口保留最近 3 轮完整对话更早的对话提取成关键信息列表。关键信息列表用 JSON 存储每项不超过 50 token。这样 10 轮对话的上下文可以压缩到 2000 token 以内。4.2 Scratchpad 的持久化与清理Agent 的 scratchpad中间推理步骤是内存消耗大户。每一步的工具调用结果都可能很长比如一个数据库查询返回了 100 行数据。这些数据如果一直留在内存里很快就会 OOM。我的策略是scratchpad 只保留最近 3 步的完整结果更早的结果只保留摘要。摘要由工具自己生成——每个工具在执行完后除了返回完整结果还要返回一个不超过 100 字的摘要。比如数据库查询工具返回完整数据的同时返回“查询到 100 条记录前 3 条是...”。如果 Agent 循环结束成功或失败scratchpad 立即清空。如果用户中途退出 Appscratchpad 可以选择持久化到磁盘下次打开时恢复。但持久化要加密——scratchpad 里可能包含用户隐私数据。4.3 内存池与对象复用端侧开发内存分配是性能杀手。频繁的new对象会导致 GC 频繁触发在 Android 上表现为卡顿在 iOS 上表现为内存峰值。Agent 循环里涉及大量临时对象prompt 字符串、JSON 解析中间对象、工具参数 Map、工具返回结果。我的做法是维护一个对象池。比如MapString, Any对象池每次工具调用前从池里取一个用完清空放回。字符串拼接用StringBuilder复用。JSON 解析用流式解析器避免一次性构建完整对象树。这些优化在云端可能没必要但在端侧一个 3B 模型的推理本身就要几百毫秒如果 GC 再卡个 50ms用户体验就会明显下降。5. 端侧部署的实操细节5.1 模型格式选择与量化端侧 LLM 的格式主要有 GGUF、ONNX、TFLite、Core ML。选择哪个取决于你的目标平台AndroidGGUFllama.cpp或 TFLite。GGUF 生态更成熟量化选项多。TFLite 与 Android 集成更好但支持的模型架构有限。iOSCore ML 或 GGUF通过 llama.cpp 的 iOS 绑定。Core ML 能利用 Neural Engine功耗更低。GGUF 更灵活但只能跑 CPU/GPU。跨平台ONNX Runtime 或 MLC-LLM。MLC-LLM 支持 Vulkan 和 Metal性能不错但编译配置复杂。量化方面端侧常用 Q4_K_M 或 Q5_K_M。Q4_K_M 在 3B 模型上大约占 2GB 内存Q5_K_M 大约 2.5GB。如果设备只有 4GB 内存Q4_K_M 是更安全的选择。Q3_K_S 更小约 1.5GB但输出质量下降明显格式遵循能力会变差容错机制的压力会更大。注意量化后的模型输出格式稳定性会下降。Q4_K_M 比 Q8_0 的格式错误率大概高 2-3 倍。所以如果你用了 Q4 量化容错机制必须做得更厚。我的经验是Q4 量化下JSON 解析失败率大约 5-8%Q8 下只有 1-2%。5.2 推理引擎的参数调优以 llama.cpp 为例端侧推理的关键参数参数推荐值说明n_ctx2048-4096上下文窗口越大内存占用越高n_batch128-256批处理大小端侧不宜过大n_threads2-4线程数超过物理核心数反而变慢n_gpu_layers0 或全部有 GPU 就全放 GPU没有就 0temp0.1-0.3温度端侧 Agent 需要确定性输出top_p0.9核采样配合低温度使用repeat_penalty1.1重复惩罚防止 LLM 陷入循环温度是关键。端侧 Agent 不需要创造性需要的是稳定和可预期。温度设 0.1 能大幅降低格式跑偏的概率。但温度太低也会导致 LLM 在遇到不确定情况时反复输出相同内容。0.1-0.3 是平衡点。5.3 电量与热管理端侧 Agent 跑在手机上电量和发热是绕不开的问题。一次完整的 Agent 循环3-5 轮可能消耗 1-2% 的电量手机背面温度可能上升 3-5 摄氏度。如果用户连续使用 10 分钟电量掉 10% 以上体验就很差了。我的优化手段推理批量化如果用户连续输入多个请求合并成一次推理。但端侧 Agent 通常是交互式的批量机会不多。工具执行异步化工具执行不阻塞推理。但 Agent 循环本身是串行的这个优化空间有限。动态降频检测到设备温度超过阈值时主动降低推理线程数或切换到更小的模型。这个需要与系统 API 配合。缓存相同的用户输入如果之前处理过直接返回缓存结果。端侧 Agent 的场景通常比较固定比如车机上的导航、音乐、空调控制缓存命中率可以很高。6. 常见问题与排查技巧实录6.1 典型问题速查表现象可能原因排查方法解决方案Agent 卡死无响应工具执行阻塞主线程检查工具是否在主线程执行移到 IO 线程池加超时反复调用同一工具LLM 陷入循环查看 scratchpad 是否重复加重复检测中断循环JSON 解析频繁失败模型量化过度检查量化等级升级到 Q5 或 Q8加强解析容错内存持续增长scratchpad 未清理监控内存曲线限制 scratchpad 长度及时清理推理速度突然变慢设备过热降频监控 CPU 频率和温度降低线程数暂停推理工具返回结果被截断上下文窗口不足检查 token 数压缩上下文截断工具结果多轮对话后回答质量下降上下文污染检查历史对话滑动窗口 关键信息提取App 启动后首次推理极慢模型加载耗时测量加载时间预加载模型或延迟加载6.2 独家避坑技巧技巧一用“心跳”检测 Agent 循环是否存活。在 Agent 循环的每一步之间插入一个心跳检查。如果超过 5 秒没有心跳强制中断循环并返回错误。这能防止 LLM 推理卡死导致整个 App 无响应。技巧二工具返回结果做“瘦身”。工具返回的原始数据往往包含大量冗余字段。在返回给 LLM 之前先做一次字段裁剪。比如数据库查询返回 20 个字段但 LLM 只需要其中 3 个。裁剪后 token 数能减少 70% 以上。技巧三维护一个“坏输出”样本库。每次 JSON 解析失败把原始输出存下来。积累几十条后分析规律针对性地改进 prompt 或解析器。我靠这个方法把 JSON 解析失败率从 8% 降到了 2% 以下。技巧四端侧 Agent 的日志要“可回放”。记录每一步的输入、输出、耗时、token 数。出问题时用这些日志在开发机上回放整个循环能快速定位问题。日志格式用 JSON Lines每行一个步骤方便解析。技巧五不要迷信“大而全”的编排框架。端侧 Agent 的编排层越薄越好。我见过一个团队用 LangChain 的 AgentExecutor 跑端侧光是框架初始化就花了 800ms内存占用 50MB。后来换成 200 行的自研状态机初始化 20ms内存占用 2MB。6.3 性能基准测试方法端侧 Agent 的性能测试不能只看“跑通了没有”。要测四个指标首 token 延迟从用户输入到 LLM 输出第一个 token 的时间。端侧目标 500ms。每 token 生成时间端侧 3B 模型在手机 CPU 上大约 50-100ms/token。GPU 上 20-50ms/token。工具执行时间本地工具目标 100ms。超过 500ms 的工具要考虑优化或异步化。端到端完成时间一个 3 轮循环的 Agent 任务目标 5 秒。测试时要用真实设备不要用模拟器。模拟器的 CPU 性能和内存行为与真机差异很大。Android 上至少测中端机骁龙 7 系和高端机骁龙 8 系各一台。iOS 上测 A14 及以上芯片的设备。7. 端侧 Agent 工程化的未来演进7.1 模型与编排的协同设计现在的端侧 Agent 工程化模型和编排是分开设计的。模型团队负责压缩和量化工程团队负责编排和容错。未来这两个会融合模型在训练时就知道自己会被部署到端侧会针对端侧的资源约束做优化。比如训练时就加入格式遵循的强化学习让模型天生就更倾向于输出严格 JSON。或者训练时就加入工具调用的特殊 token让解析器能更高效地提取工具调用。7.2 端云协同的混合架构纯端侧 Agent 的能力上限受模型大小限制。未来的趋势是端云协同简单任务端侧处理复杂任务云端处理。端侧 Agent 负责意图识别和任务分解云端负责复杂推理和知识检索。端侧 Agent 的工程化要为此做好准备编排层要支持“远程工具”容错机制要处理网络不可靠的情况状态管理要支持端云状态同步。7.3 硬件加速的普及现在端侧 LLM 推理主要靠 CPU 和 GPU。未来 NPU 会普及专门为 Transformer 推理优化。端侧 Agent 的工程化要能利用 NPU 的算力同时处理 NPU 的内存限制NPU 通常只有几 MB 的片上内存需要精细的内存调度。这会是下一个工程化难点。我个人在实际操作中的体会是端侧 Agent 工程化没有银弹。每一个优化都是权衡量化等级换内存温度换稳定性重试换延迟缓存换准确性。关键是明确你的场景最不能妥协的是什么。如果是车载语音助手延迟和稳定性最重要量化可以激进一些容错要厚。如果是端侧文档分析准确性最重要量化要保守上下文要尽量完整。想清楚这个后面的技术选型就顺了。