
做了大半年端侧 Agent 的工程化落地终于能抽出时间把这段经历完整梳理一遍。前两篇聊了 Agent 的基本认知和端侧推理环境这篇进入真正的硬骨头工程化。所谓工程化就是让 Agent 从“能跑”变成“能上线、能长期运行、能迭代维护”这件事比模型选型难多了也坑多了。读完这篇你会得到一套可以直接用的端侧 Agent 工程化框架从架构分层、框架选型、模型量化到并发控制、记忆管理、工具调用安全每一节都附上了我实际踩坑后修正的做法和可复算的参数。适合正在做端侧 AI 助手、智能硬件语音交互、离线知识库 Agent 的开发者也适合准备从云端 Agent 转向端侧的团队做参考。1. 从 Demo 到产品端侧 Agent 工程化到底在解决什么1.1 端侧 Agent 和云端 Agent 的本质差异先定义一下我理解的端侧 Agent。所谓端侧是指 Agent 的推理和大部分逻辑运行在用户设备上——手机、平板、PC、车机、边缘盒子而不是跑到云端再回传结果。这个定位带来的价值很明确一是隐私数据不出设备敏感信息不需要上传到任何远端二是交互延迟可控本地推理省去了网络往返体感更跟手三是断网也能用地铁、电梯、地下室这些弱网场景不会直接让助手变成废柴四是长期来看推理成本趋近于零不用按 token 付费。这些优势反过来塑造了工程化的约束。云端 Agent 遇到瓶颈可以加机器、加 GPU端侧 Agent 的内存、算力、功耗全是硬边界。同样一个 Agent 架构在云端可能只是性能问题到了端侧就是能不能跑起来的问题。我见过不少团队把云端那套 LangChain 全家桶搬到 Android 上结果光依赖树就装了五十多个包启动黑屏三秒内存吃满直接崩。这不是框架不好而是没有理解端侧约束下的架构选择逻辑。端侧工程化的本质是在资源受限的前提下把 Agent 的复杂度降下来同时把确定性提上去。云端可以对大模型抱有无限想象端侧必须做减法。每一步设计都要先问一句这个东西在目标设备上跑得动吗跑不动再好看都是白搭。1.2 “能上线”的四个硬指标我们内部对“能上线”有一套硬指标不是能跑通几个 Demo 就算完。第一个是任务成功率。用一个真实的 Agent 任务集比如 20 个固定场景每个场景跑 10 次成功率必须达到 85% 以上低于这个数直接回炉。任务集不是随便选的要覆盖日常使用频率最高的操作比如“帮我查天气并提醒我带伞”“把这段文字翻译成英文并保存到备忘录”这类多步闭环任务。第二个是性能水位。主要是首 token 延迟和平均生成速度不同设备差异很大但至少要保证用户感知不到明显卡顿。如果用户问一个问题要等五秒才听到第一个字这个产品基本没法用。第三个是资源水位。内存峰值必须控制在设备可用内存的一半以内避免和系统其他应用抢资源CPU/功耗在持续对话 10 分钟的情况下不能导致设备明显发热降频否则用户一边充电一边用着手机烫得能煎鸡蛋再好的体验也白搭。第四个是可观测性。线上出问题要能定位到是哪一轮对话、哪个工具调用、哪个 prompt 出了问题否则后面的迭代无从谈起。这四个指标里任务成功率是最难达标的因为它不单是模型能力问题还牵扯到规划、工具调用、记忆整个链条的工程质量。工程化的第一课就是把这四个指标变成每日自动跑的回归测试而不是上线前临时补一枪。2. 框架选型与架构分层别一上来就 LangChain2.1 Harness 与 Agent先分清运行环境与业务逻辑最近圈子里的讨论经常提到 Harness很多人把 Harness 和 Agent 混为一谈这里必须先理清。Agent 指的是那个“会思考、会规划、会调用工具”的智能体逻辑它本质上是一个循环Loop接收任务拆解计划调用工具观察结果修正计划直到完成任务。而 Harness 是这个循环得以运行的整套基础设施包括上下文管理、工具执行器、策略控制、日志追踪、错误恢复这些机制。打个比方Agent 是员工Harness 是公司的管理制度和办公环境。员工再能干没有流程、没有工具系统、没有安全规范也干不成事。在端侧场景里Harness 设计的好坏直接决定了 Agent 能不能稳定运行。我见过一个很典型的反面案例团队花大力气调 prompt、调模型Agent 在单测里表现得很好一旦接入真实工具链就频繁挂掉——缺超时控制、缺重试机制、缺上下文清理问题不在“智能”而在“环境”。所以端侧工程化第一步不是选什么大模型而是想清楚你的 Harness 要怎么搭循环怎么调度、工具怎么注册、上下文怎么管理、异常怎么恢复。这些定了Agent 逻辑的迭代才有基础。没有好的 HarnessAgent 的聪明才智根本无处发挥。2.2 主流框架横向对比以及我为什么最终自研谈到框架市面上的选择其实很多——LangChain、LangGraph、Dify、CrewAI 各有拥趸。我先把它们的真实使用情况放在一张表里对比再讲我的结论。框架设计定位端侧适配度依赖体积适合场景LangChain通用 Agent 开发套件低依赖多、抽象重很大云端快速原型、生态学习LangGraph有状态工作流编排中需要裁剪中复杂流程控制、云端生产Dify可视化 Agent 应用平台低服务端设计很大非技术团队快速搭建应用CrewAI多 Agent 协作框架低中多角色分工的研究原型自研 Harness端侧轻量运行环境最高很小端侧生产环境、长期维护这张表的核心结论是端侧场景下主流云端框架的性价比都不高。LangChain 最大的问题是抽象层级太多链式调用、回调、各种 Memory 模块叠在一起在端侧那点内存和 CPU 预算下根本跑不转而且依赖里掺杂了大量网络请求相关的组件天然和离线优先的设计冲突。Dify 就更不用说了它本质是一个服务端应用平台端侧根本没有它的位置。CrewAI 偏研究原型框架自身的稳定性都不够撑起生产环境。我最终选了自研 Harness 加最少依赖的路线核心原因有三个。第一是可控性端侧环境比云端复杂得多不同设备的 CPU、NPU、内存、系统版本差异很大只有自己掌控调度逻辑才能逐个适配。第二是裁剪性自研 Harness 可以做到只有几个模块哪些要哪些不要自己定依赖体积从几百 MB 压到几十 KB 级别。第三是可测试性自研之后我可以直接注入 Mock 模型和 Mock 工具做单测框架层级越少测试路径越短。当然自研的前提是你真的理解 Agent 循环的原理否则容易把框架里的坑重新踩一遍。如果你还在学习阶段先拿 LangChain 跑通业务逻辑完全没问题但不要默认它可以直接进端侧生产。2.3 端侧 Agent 的参考架构与模块职责这里给出我们团队在端侧落地时用的分层架构已经过了三版迭代目前比较稳定。整体分四层接入层、会话层、调度层和模型层。接入层负责对接不同入口比如语音助手、消息通知、App 内对话把输入统一成标准消息格式会话层管理会话状态包括上下文窗口、用户配置、长期记忆的读写调度层是 Harness 最核心的部分负责 Agent 主循环、工具注册与执行、策略控制比如什么时候该摘要、什么时候该终止、任务优先级管理模型层则是对推理引擎的封装屏蔽底层是 llama.cpp、MNN 还是 ONNX Runtime向上提供统一的 generate 接口返回 token 流和统计信息。这个分层的好处是所有模块可以独立测试、独立替换。比如模型层从 CPU 切换到 NPU调度层完全不用动记忆模块从本地 JSON 换到 SQLite会话层接口也不变。我特别想提醒的是调度层里一定要预留“干预点”hook比如每一轮 Agent 循环之后留一个回调方便观测和注入策略。没有干预点的 Harness后面想加日志、加审计、加安全策略都只能改核心代码非常痛苦。另外如果团队里有 Rust 基础端侧 Harness 我个人强烈推荐用 Rust 写。原因很实在内存占用确定、没有 GC 停顿、交叉编译方便一套代码可以编出 Android、Linux、嵌入式三个平台的版本。我们后来把 Python 原型用 Rust 重写后常驻内存直接降了 40%这对端侧是决定性的。但要注意 Rust 开发效率前期会慢一些业务验证期用 Python 跑逻辑、稳定期再迁 Rust是比较务实的路径。3. 硬件约束下的模型部署量化、推理引擎与性能实测3.1 先算内存账模型选型的量化公式端侧模型选型的第一件事不是看榜单分数而是算内存账。模型权重在内存里的占用有一个很简单的公式权重体积GB约等于参数量B乘以量化位数bit再除以 8。举个例子2B 模型 FP16 是 2×16÷8 4GBINT8 是 2GBINT4 是 1GB。这还只是权重实际运行还要加上 KV Cache、临时激活值和推理引擎开销一般要在权重体积基础上再留 30% 到 50% 的余量。我通常会倒推着选如果设备可用内存是 2GB那留给模型的上限是 1.2GB 左右算下来只能选 1B 到 2B 的 INT4 量化模型如果设备可用内存是 4GB可以选 3B 到 4B 的 INT47B INT4 的权重就有 3.5GB加上运行时很容易超过 5GB绝大多数手机根本顶不住。这个账不先算清楚后面部署阶段必然返工。还有一个容易忽略的量是 KV Cache 的占用它和上下文长度直接相关。粗略估算每 1K token 的 KV Cache 在 INT8 下大约占 1MB 左右按模型层数和头数有浮动端侧通常把上下文限制在 4K 到 8K 就是出于这个考量。上下文越长KV Cache 越大内存压力指数级上升这也是后面治理上下文膨胀的根本原因。3.2 推理引擎对比llama.cpp、MNN、ONNX Runtime模型文件选定之后接下来面临的是推理引擎选型。这里没有银弹关键看你的目标设备形态。llama.cpp 是我在 Linux 设备和树莓派上的首选它对 CPU 推理的优化做得非常到位原生支持 GGUF 格式和各类量化社区活跃度极高XNNPACK 后端在 ARM 平台上有明显加速。它的另一大优势是把模型封装成一个单文件GGUF分发和版本管理都极度简单部署的时候拷一个文件过去就行。移动端Android/iOS场景我会优先看 MNN。它对移动端做过多轮针对性优化支持 CPU/GPU/NPU 的异构调用在部分高通平台上能吃到 NPU 加速的红利。但 MNN 的算子支持和动态 shape 处理不如 ONNX Runtime 成熟如果你要部署的模型里有比较新的算子可能得先做一层算子兼容性检查。ONNX Runtime 则胜在中间格式的通用性生态里几乎所有模型都能导出 ONNX转给其他引擎的路径也更灵活。我在实测中发现一个高频坑引擎支持动态 shape 与否直接决定端侧 Agent 的体验。Agent 生成时上下文长度不断变化如果引擎不支持动态 shape就得把输入固定到最大长度内存和延迟都会浪费很多。选引擎时一定先翻它的文档确认 dynamic shape 支持情况再决定要不要额外套一层 Padding 逻辑。这个步骤看起来不起眼但踩过坑的人都知道它能把一个看似 OK 的部署方案直接推翻。3.3 性能与功耗的平衡一个实测数据参考把实测数据放在这里给大家一个参考。我们在骁龙平台、8GB 内存设备上跑 Qwen2.5-3B 的 INT4 量化模型CPU 推理单线程的情况下平均生成速度大概在每秒 15 到 20 token首 token 延迟约 300 到 500ms。换到 1.8B INT4 模型生成速度能到 25 到 35 token/s首 token 延迟降到 200ms 内。作为对比人类阅读速度大约每秒 5 到 8 token所以 1.8B 级别的速度其实已经能满足问答和助手场景3B 更适合对 reasoning 要求更高的任务。功耗方面持续对话 10 分钟后CPU 推理会让设备功耗明显上升发热导致降频会让生成速度再掉 20% 到 30%。我们的处理方式是把推理任务绑到特定大小核限制最高频率虽然峰值速度略降但长期运行更稳定。另外一个实用策略是空闲时卸载模型把内存让给系统用两秒左右的热加载时间换整机体验在很多场景下都值得。功耗的测定方法其实不难Android 上可以通过电池统计接口拿整机电流Linux 设备直接读电源管理节点。关键是压测脚本要固定场景——同样的对话流程、同样的负载时长数据才具有可比性。我们内部就吃过一次亏两次压测间隔了半个月系统版本和应用状态都变了数据对比失真后面所有功耗测试都固定在同一台设备、同一个系统版本上跑。4. 并发、延迟与稳定性端侧 Agent 怎么扛真实流量4.1 端侧的并发形态和云端完全不同很多后端背景的同事接手端侧 Agent 时会问同一个问题这个 Agent 能扛多少并发我对端侧并发的理解是单设备场景下几乎没有“高并发”这回事真实形态是三类叠加多轮对话中用户的连续追问、Agent 自动触发的后台任务、以及系统里多个 Agent 实例比如语音助手和消息摘要助手同时存在。真正的难点不是并发数量而是这三类请求在共享同一个模型实例时怎么保证延迟和数据一致性。端侧模型实例通常是不能并发推理的。一个 3B INT4 模型推理时激活值和 KV Cache 的内存占用会随着 batch 增大成倍上升端侧设备那点内存根本撑不住多 batch。而且同一时间跑两个推理CPU 资源也会被抢占最终两个请求都变慢体验更差。所以我们默认的做法是单个模型实例同一时间只服务一个请求其他请求进队列等待。这个决策会让吞吐看起来“很低”但实际上端侧 Agent 的请求频率本身不高串行化换来的是每个请求都有充足资源用户体感反而更好。4.2 请求队列、背压与降级策略把请求串行化之后队列设计就成了稳定性的核心。我们的实现比较简单一个有界队列加一个工作线程。队列长度上限按设备能力设置比如 8 或者 16满了之后新请求立即拒绝并返回“Agent 正忙请稍后再试”而不是无脑堆积到内存爆炸。这是典型的背压backpressure策略端侧虽然需求峰值不高但也要防住异常场景下的请求洪峰。简化版的实现思路长这样# 简化版请求队列伪代码 queue asyncio.Queue(maxsize8) async def enqueue(request): if queue.full(): return AgentBusy() # 背压立即拒绝 await queue.put(request) async def worker(model): while True: request await queue.get() try: result await model.generate(request) await notify(request, result) # 通知会话层回传结果 finally: queue.task_done()降级策略同样重要。我们内部把请求分成两级交互级和后台级。用户正在对话的请求属于交互级永远优先出队摘要生成、自动整理这类属于后台级可以等甚至可以在内存压力大时直接丢弃再安排下次重跑。这个策略用一句话概括宁可牺牲后台任务的完整度也要保住用户面对的那条对话链路。还要提一下重试与幂等。工具调用可能失败整条链路的某个环节可能超时所以 Harness 里必须有重试机制。但重试的前提是操作要幂等——同一个工具调用执行两次不能产生双倍副作用。比如发送通知这个 Skill我们要求它内部生成一个 requestID目标系统按这个 ID 去重没有幂等设计的重试在真实场景里很容易变成发三次短信。这个问题云端也常见但端侧离用户操作更近一旦发生用户感知更直接。4.3 上下文膨胀治理token 预算与摘要压缩端侧 Agent 最容易出现的问题之一是长对话后上下文越滚越大最终把内存和配额吃掉。我们给每个会话设定了一个 token 预算比如 4K一旦超出就触发上下文治理。治理手段按顺序递进先做滑动窗口只保留最近 N 轮对话如果仍然超限就触发摘要生成把更早的对话压成一段摘要再配合关键事实抽取把“用户住在北京、偏好环保产品”这类长期信息单独落到记忆存储里而不是留在上下文里反复占空间。摘要在工程上的实现需要特别小心摘要本身也要调用模型一轮摘要可能消耗几百 token 和几秒时间不能让用户明显感知卡顿。我们的做法是在用户对话的间隙比如上一轮回复结束后的空闲窗口异步触发摘要并且给摘要设置单独的、更小的模型上下文避免挤占主对话的推理资源。实际跑下来摘要策略的效果很明显40 轮以内的对话上下文占用能被控制在预算的 80% 以内长对话不再成为系统负担。token 预算的具体数值要按设备调整我们的经验值是预算不能超过模型上下文窗口的 70%留出 30% 给当前这一轮的输入输出。假设模型支持 8K 上下文预算就设 5.6K 左右触发摘要的阈值设在预算的 70% 也就是 3.9K。这样设置的好处是摘要本身也需要消耗 token不至于在摘要过程中又把上下文撑爆。5. 记忆与工具调用两个最容易被低估的工程点5.1 长期记忆的持久化与摘要时机端侧 Agent 的记忆分为短期记忆和长期记忆两层。短期记忆就是当前会话上下文通常由 Harness 直接管理不需要额外设计长期记忆才是工程化的分水岭——它要跨会话保存用户偏好、历史事实、任务状态并且在合适的时机写回上下文让 Agent 在下一轮对话中能用上。没有长期记忆的 Agent 像个失忆症患者每次对话都当第一次见面体验很难有质的提升。我们在端侧采用的长期记忆实现很朴素本地 SQLite 加向量检索如果有能力也可以纯结构化存储。每轮对话结束后由调度层决定哪些信息值得写入写入前会先做一次去重和票选。其中去重是必须做的否则同一件事在十轮对话里写十次记忆库就废了。记忆的写入时机我强烈建议放在异步任务里不要在用户交互路径上同步阻塞否则用户每说一句话都要等记忆落盘卡顿感极强。摘要时机的选择我踩过比较深的坑是“每轮都摘要”。早期实现是对话每增加一轮就生成一次摘要结果模型频繁被摘要任务打断对话质量反而下降。后来改成两个触发条件上下文达到 token 预算的 70% 时触发一次且两次摘要之间至少间隔 5 轮对话。这个策略既保证了摘要的时效性又不至于频繁抢占推理资源。摘要一定要保留关键事实的原文不能过度抽象否则 Agent 后续回忆时只能得到“用户喜欢 XX”的模糊结论而丢失了“用户在上个月买过 X 型号产品”这种具体信息。一个额外的提醒长期记忆的存储格式要预留版本字段。Agent 的记忆结构在迭代中一定会变没有版本控制升级后旧记忆全部无法解析用户的历史数据直接丢失这是线上事故级别的错误。5.2 Skill 调用的校验、超时与幂等工具调用在 Agent 生态里的名字很多Tool、Function、Skill本质上都是让模型具备调用外部能力的手段。端侧工程化对 Skill 的要求比云端更强调安全和可控因为端侧 Agent 距离用户和系统权限太近了一个错误调用可能直接影响系统功能。我们要求每个 Skill 在注册时必须提供 JSON Schema字段包括参数名、类型、必填性、取值范围比如这样{ name: send_message, description: 发送一条系统通知, parameters: { type: object, properties: { receiver: { type: string, minLength: 1 }, content: { type: string, maxLength: 200 } }, required: [receiver, content] } }模型返回的工具调用请求在执行前必须经过三重检查。第一重是 schema 校验参数类型不对、必填缺失就直接拒绝不要尝试“智能补全”补错的风险远大于收益。第二重是权限校验Skill 根据可访问的系统能力分级读取本地文件是普通权限发送通知、拨打电话则需要高权限高权限操作必须由用户二次确认。第三重是内容校验参数里如果出现路径穿越、命令注入这类模式直接拦截。端侧 Agent 最容易在这三重校验上偷工减料但这一环恰恰是不能省的。超时熔断是我反复强调的一点。工具调用不能无限等待我们在 Harness 里给每个 Skill 设了明确的超时时间默认 5 秒超过就把该次调用标记为失败并让 Agent 走异常分支比如更换工具或直接向用户说明。幂等设计同样不能省前文提到的 requestID 机制就是基础款更严格的做法是让 Skill 在执行前先检查目标状态已经完成的操作直接返回成功避免重试造成重复副作用。真实场景里模型经常会出现连续两次调用同一个 Skill 的情况没有幂等保护轻则重复发送重则数据写乱。6. 高频问题排查速查表与几条真实经验6.1 端侧 Agent 工程化高频问题速查把我们在端侧 Agent 落地这一年里遇到的高频问题整理成一张速查表基本都是能直接复用的排查思路。问题现象可能原因排查顺序解决方案启动即崩溃内存不足 / 算子不兼容先看崩溃栈再看模型加载日志换更小模型或调整量化替换推理引擎对话越跑越慢上下文膨胀 / KV Cache 超限检查 token 计数与内存曲线触发摘要压缩、滑动窗口工具调用频繁失败schema 不匹配 / 工具超时看工具执行日志有没有拒绝记录校准 schema、加超时熔断偶发卡顿后台任务占用推理队列看任务队列排队时间降低后台任务优先级、扩大队列模型输出乱码量化过激 / 引擎 bug切换回 FP16 对比更换量化方式或校准数据集长时间后功能失效内存泄漏 / 状态脏数据检查常驻内存曲线与本地存储定期重建会话状态、清理记忆库这张表解决的是“出了问题怎么查”但更重要的经验是别等问题出现才去查。端侧 Agent 从第一天就应当把日志、指标、异常上报埋好否则线上问题全靠猜效率极低。我们内部有一个不成文的规定任何一次线上问题如果事后拿不出完整的问题现场日志都算开发团队的失职。这个规定听起来苛刻但执行下来之后排查周期从按天计缩短到了按小时计。6.2 几条说烂了但真的救过命的经验第一先跑通最小闭环再上框架。我们最开始用最原始的“while 循环 请求模型 解析 JSON”拼出一个能跑通的 Agent再把其中反复出现的逻辑抽象成 Harness 模块。这样的好处是每一步都在为真实问题设计而不是被框架的概念绑架。等到你理解了循环的每一个环节再去看框架源码很多当时觉得玄妙的设计就都一目了然了。第二端侧 Agent 追求的是确定性不是聪明。云端你可以让模型自由发挥端侧由于资源紧张我们反而希望模型少一些自由发挥多一些结构化输出。比如工具调用参数宁可让模型输出 JSON 再校验也不要它用自然语言描述意图再由 Harness 去猜。效果上的差距可以在后续用更好的模型补但工程稳定性是底线底层逻辑一旦崩塌上面全是空中楼阁。第三内存峰值一定要压测出来。不要看文档里写的建议内存直接跑一个 100 轮以上的压测任务把内存曲线打出来你会看到很多平时根本察觉不到的问题比如长上下文后的 KV Cache 尖峰、工具调用临时对象的堆积。我们第一次做百轮压测时内存曲线在 80 轮附近出现了明显的台阶式上涨定位结果是某个 Skill 在重复往上下文里塞历史记录。这种问题靠人工点几个场景是永远发现不了的。第四日志和可观测性从第一天就埋。后期的每一次智能优化没有观测数据支撑都等于拍脑袋。我们曾经花两周调 Agent 的规划 prompt效果时好时坏后来上了完整日志追踪才发现问题根本不在 prompt而在某个工具返回了空结果导致规划路径每次都不一样。先看数据再改方案这条规矩救过我太多次了。我个人在这一年多里最大的体会是端侧 Agent 的工程化和云端最大的不同在于所有决策都被资源边界逼着做减法。云端你可以无脑上大模型、堆框架、加并发端侧每一步都要算账——内存账、延迟账、功耗账、维护账。这个约束一开始觉得憋屈做久了反而成为产品竞争力的来源因为它逼着你把 Agent 的每个环节都理清楚而不是让框架替你兜底。先写到这。端侧 Agent 工程化的另一半——多 Agent 协作、评估体系、持续集成与灰度发布——内容量同样不小我放到下一篇下里继续讲。如果你正在做端侧 Agent 或者准备从云端迁过来欢迎带着具体问题来交流多数坑我都替你踩过了。