1. 端侧 Agent 工程化到底在解决什么问题1.1 从“能跑”到“能交付”的鸿沟很多人第一次接触端侧 Agent都是被一个很酷的 Demo 吸引的本地跑一个量化后的大模型接上几个工具问它一句“帮我看看今天有什么安排”它就能调用日历、读文件、发通知看起来无所不能。但真要把这套东西做成一个能给别人用、能稳定运行、能持续迭代的产品你会发现 Demo 和产品之间隔着一整条工程化的鸿沟。我自己踩过最典型的一个坑早期写了个本地 Agent工具调用全靠字符串匹配模型输出里只要出现“调用天气工具”这几个字我就去执行天气查询。测试的时候一切正常上线第二天用户输入了一句“我不需要调用天气工具”结果 Agent 真的去查了天气。这就是典型的“能跑”但“不能交付”——它没有工程约束全靠运气。端侧 Agent 工程化本质上就是把“模型 工具 记忆 循环控制”这套东西从随手拼凑的脚本变成有明确边界、有错误处理、有可观测性、有版本管理的系统。它要解决的核心问题包括模型输出的不确定性怎么收敛、工具调用的协议怎么统一、上下文窗口怎么管理、失败之后怎么恢复、多轮对话的状态怎么保持。这些问题在云端 Agent 里同样存在但端侧因为算力、内存、功耗的限制会变得更加尖锐。1.2 端侧场景的特殊约束端侧 Agent 和云端 Agent 最大的区别不是模型大小而是资源边界。云端你可以随便开几百 GB 内存、挂一堆微服务端侧不行。一台中端手机可用内存可能就几个 GB还要分给系统和其他应用一个嵌入式设备内存可能只有几百 MB。这就导致端侧 Agent 的工程化必须回答几个云端不太关心的问题。第一是模型加载策略。你不能像云端那样常驻一个完整模型端侧往往需要按需加载、用完释放或者用多模型分级——简单意图用小模型复杂推理才唤起大模型。第二是工具执行的沙箱化。端侧工具往往直接操作本地文件、传感器、系统 API一旦模型调用出错后果比云端严重得多所以必须有权限校验和参数白名单。第三是功耗和延迟。端侧 Agent 如果每次思考都跑满 CPU手机发烫、电量狂掉用户直接卸载。所以工程化里必须考虑推理频率控制、批处理、缓存命中率这些指标。这些约束决定了端侧 Agent 的工程化不能照搬云端那套微服务架构而要走一条更轻、更紧凑、更强调确定性的路线。后面几节我会围绕 Function Calling、MCP、上下文管理、错误恢复这几个核心点把这条路线拆开讲。2. Function Calling 的工程化落地细节2.1 为什么 Function Calling 是端侧 Agent 的基石Function Calling 这个词听起来很技术其实用生活化的方式理解特别简单它就是给模型一本“工具说明书”告诉它有哪些工具、每个工具需要什么参数然后模型在需要的时候按格式填一张“调用申请单”出来。端侧 Agent 之所以离不开它是因为端侧模型本身能力有限不可能靠纯文本推理完成所有事必须借助外部工具来补足。但 Function Calling 在端侧落地时最大的挑战不是模型会不会调用而是调用得准不准、稳不稳。云端模型参数大对 schema 的理解能力强端侧量化模型经常出现参数缺失、类型错误、工具名拼错的情况。我实测过一个 3B 级别的量化模型在工具数量超过 8 个之后选错工具的概率明显上升尤其是功能相近的工具比如“查天气”和“查空气质量”它经常混。所以工程化的第一步是把 Function Calling 的 schema 设计当成 API 设计来做而不是随便写个 JSON 丢给模型。工具名要短、要唯一、要有语义区分度参数描述要具体不能写“查询内容”这种模糊词要写“城市名称例如北京、上海”枚举值要穷举不要让模型自由发挥。这些细节看起来琐碎但直接决定调用成功率。2.2 工具 schema 设计的五个硬性规则我把端侧 Function Calling 的 schema 设计总结成五条规则都是踩坑踩出来的。第一条工具数量控制在 5 到 10 个之间。太少了不够用太多了模型选择困难。如果业务确实需要很多工具就做分层先让模型选“类别”再在类别内选具体工具。第二条参数尽量扁平避免嵌套对象。端侧模型对嵌套结构的解析能力弱一个两层嵌套的 JSON错误率比扁平结构高不少。如果确实需要复杂参数拆成多个简单工具。第三条每个参数都要有类型和示例。不要只写{type: string}要写{type: string, description: 城市名如北京}。示例能显著提升模型填参的准确率。第四条必填参数和可选参数要明确区分。端侧模型经常把可选参数也填上导致调用失败。用required字段严格约束同时在描述里说明“不填则使用默认值”。第五条工具返回值也要有 schema。很多开发者只定义输入不管输出结果模型拿到返回值后不知道怎么解析。返回值结构要稳定字段名要语义化最好带上status和error_message。下面是一个我实际在用的天气工具 schema可以当模板参考{ name: get_weather, description: 查询指定城市的当前天气, parameters: { type: object, properties: { city: { type: string, description: 城市名称例如北京、上海 }, unit: { type: string, enum: [celsius, fahrenheit], description: 温度单位默认摄氏度 } }, required: [city] } }这个 schema 看起来简单但每一条都是有用的。description里给了示例unit用了枚举required只留了city。实测下来这种写法比随便写的 schema 调用成功率高出一大截。2.3 调用解析与容错处理模型输出 Function Calling 结果时格式不一定完全规范。有时候会多一个逗号有时候会把参数包在 markdown 代码块里有时候干脆输出一段自然语言说“我要调用天气工具”。端侧工程化必须对这些情况做容错。我的做法是三层解析。第一层尝试标准 JSON 解析成功就直接用。第二层如果失败用正则提取 JSON 片段再解析。第三层如果还失败走一个轻量级的规则匹配从文本里抽取关键参数。三层都失败就返回一个标准错误让 Agent 决定是重试还是换工具。这里有个细节重试次数一定要限制。我见过有人写了个无限重试的逻辑模型一直输出错误格式结果死循环把设备跑死。端侧资源有限重试最多两次两次不行就降级处理比如直接告诉用户“我暂时无法完成这个操作”。另外参数校验不能省。模型填的参数类型可能不对比如该填数字填了字符串该填枚举填了自由文本。在执行工具之前一定要做一次类型检查和范围检查。这一步在云端可能觉得多余但在端侧是保命的因为端侧工具往往直接操作硬件参数错了可能引发实际问题。3. MCP 协议在端侧 Agent 中的角色3.1 MCP 到底解决了什么工程问题MCP 最近热度很高很多人第一次听到会懵不知道它和 Function Calling 有什么区别。用一句话说Function Calling 解决的是“模型怎么调用一个工具”MCP 解决的是“工具怎么被标准化地描述、发现和连接”。前者是调用协议后者是连接协议。在端侧 Agent 里这个问题特别突出。假设你的 Agent 要接日历、文件系统、传感器、蓝牙设备每个工具的接口格式都不一样有的用 JSON有的用 gRPC有的直接是本地函数。如果没有统一协议每接一个新工具就要改一次 Agent 核心代码维护成本极高。MCP 的价值就是把这些工具抽象成统一的“服务端”Agent 作为“客户端”通过标准协议去发现和调用。端侧场景下MCP 还有一个额外好处它天然支持工具的动态发现。设备上装了什么能力Agent 启动时通过 MCP 查询一下就知道不需要硬编码。这对端侧很重要因为端侧设备型号多、能力差异大硬编码根本维护不过来。3.2 端侧 MCP 的轻量化改造标准 MCP 协议在云端跑没问题但直接搬到端侧会偏重。端侧 MCP 需要做几处改造。第一传输层简化。云端 MCP 常用 HTTP 或 WebSocket端侧如果工具就在本机完全可以用进程内调用或者本地 socket省掉网络栈开销。我实测过本地 socket 比 HTTP 在端侧快不少延迟能降一个数量级。第二工具描述精简。MCP 的工具描述如果太长会占用宝贵的上下文窗口。端侧要把描述压缩到最小必要信息去掉冗余的示例和说明只保留参数和用途。第三能力协商要轻。MCP 启动时的握手流程在端侧可以简化不需要完整的版本协商和能力列表交换直接用一个轻量级的 manifest 文件声明支持的工具即可。第四超时和重试要短。端侧工具执行通常很快如果超过几百毫秒还没返回大概率是出问题了没必要等太久。超时设短一点快速失败把控制权交回 Agent。这些改造的核心思路是端侧 MCP 不需要完整实现协议的所有特性只需要保留“标准化描述 动态发现 统一调用”这三个核心能力就够了。3.3 MCP 与 Function Calling 的协作方式在实际工程里MCP 和 Function Calling 不是二选一而是配合使用。MCP 负责工具的注册和发现Function Calling 负责具体的调用格式。Agent 启动时通过 MCP 拿到工具列表把这些工具转换成 Function Calling 的 schema 喂给模型模型决定调用哪个之后Agent 再通过 MCP 去执行。这个分工的好处是解耦。工具开发者只需要按 MCP 标准写一次工具就能被任何支持 MCP 的 Agent 使用Agent 开发者只需要处理 Function Calling 的解析不用关心工具底层怎么实现。端侧设备上这种解耦能大幅降低集成成本。不过要注意一点MCP 工具转 Function Calling schema 时要做一次裁剪。MCP 的工具描述可能包含很多端侧用不到的信息直接转过去会浪费上下文。我一般只保留工具名、简短描述、参数列表这三部分其他全部丢掉。4. 上下文窗口与记忆管理的工程实践4.1 端侧上下文窗口的残酷现实端侧模型最要命的一个限制就是上下文窗口小。云端模型动辄 128K、200K端侧量化模型可能只有 2K 到 8K。这意味着你没法把完整对话历史、所有工具描述、系统提示词一股脑塞进去必须精打细算。我刚开始做端侧 Agent 时习惯性地把系统提示词写得很长结果发现模型根本没空间处理用户输入。后来才明白端侧上下文管理的第一原则是每一段占用都要有明确回报。系统提示词只保留最核心的角色定义和输出格式要求工具描述只保留当前任务可能用到的对话历史只保留最近几轮。这里有个实用的技巧把上下文分成“固定区”和“动态区”。固定区放系统提示词和核心工具描述这部分尽量压缩到最小动态区放对话历史和临时工具结果这部分按需加载、用完即弃。固定区越小动态区空间越大Agent 的灵活性越高。4.2 对话历史的压缩策略对话历史不能无限增长必须有压缩策略。我试过几种方案各有适用场景。第一种是滑动窗口只保留最近 N 轮对话。简单粗暴但会丢失早期重要信息。适合任务导向的短对话场景。第二种是摘要压缩把早期对话用模型总结成一段简短摘要。这个方案效果好但需要额外推理端侧算力有限时要谨慎使用。我的做法是只在对话轮数超过阈值时才触发摘要而且用最小的模型来做。第三种是结构化记忆把对话中的关键信息抽取成键值对存起来比如用户偏好、任务状态、已确认的事实。这个方案最适合端侧因为存储开销小、检索快。我现在的 Agent 默认用这种方案对话历史只保留最近两轮其他信息全部结构化存储。结构化记忆的实现不复杂就是维护一个字典模型在对话中识别到需要记住的信息时调用一个save_memory工具写进去需要时再调用recall_memory读出来。关键是记忆的键要设计好不能太细也不能太粗。太细了检索不准太粗了信息丢失。4.3 工具结果的裁剪与缓存工具返回的结果往往很长直接塞进上下文会挤爆窗口。端侧必须对工具结果做裁剪。裁剪的原则是只保留模型决策需要的信息。比如查天气模型只需要知道温度和天气状况不需要知道湿度、风速、气压、紫外线指数这些细节。所以工具返回时就应该做一次过滤只返回核心字段。如果工具结果确实需要完整信息那就先存到本地上下文里只放一个引用 ID模型需要时再通过工具去取。这个模式在端侧特别有用因为端侧本地存储便宜上下文窗口贵。缓存也很重要。同一个工具在短时间内被多次调用结果大概率一样没必要重复执行。我一般给工具结果加一个短时效缓存比如 30 秒内相同参数的调用直接返回缓存。这能显著降低端侧功耗和延迟。5. 错误恢复与可观测性建设5.1 端侧 Agent 的失败模式分类端侧 Agent 的失败不是单一原因我把它分成四类每类处理方式不同。第一类是模型输出错误比如格式不对、参数缺失、工具名拼错。这类错误靠解析容错和重试解决。第二类是工具执行错误比如权限不足、文件不存在、设备忙。这类错误要把具体原因返回给模型让模型决定是换工具还是告诉用户。第三类是资源错误比如内存不足、推理超时。这类错误要触发降级策略比如换小模型、减少上下文、暂停非关键任务。第四类是逻辑错误比如模型陷入循环、反复调用同一个工具。这类错误要靠循环检测和步数限制来兜底。分类的意义在于不同错误要用不同策略不能一刀切。我见过有人把所有错误都当成“重试”结果资源错误越重试越糟逻辑错误直接死循环。5.2 循环检测与步数控制端侧 Agent 最容易出的问题就是循环。模型调用工具 A拿到结果后觉得不对又调用工具 A如此反复。端侧算力有限这种循环几分钟就能把电量耗光。我的做法是双保险。第一硬性步数限制一个任务最多执行 N 步超过就强制终止并返回当前结果。N 一般设 5 到 8具体看任务复杂度。第二循环检测记录最近几次工具调用的名称和参数如果发现重复模式就中断并提示模型换策略。循环检测的粒度要把握好。太粗了会误判正常的重复调用太细了检测不出来。我一般看“工具名 关键参数”的组合连续两次完全相同就触发警告连续三次就强制中断。5.3 日志与可观测性端侧 Agent 出问题时排查比云端难得多因为用户设备你拿不到日志也不一定传得回来。所以可观测性建设要提前做。我建议至少记录这几类信息每次模型推理的输入输出摘要、每次工具调用的名称参数和结果状态、每次错误的具体类型和上下文、整个任务的步数和耗时。这些信息本地存一份同时按需上报一份脱敏后的统计。日志要注意隐私。端侧 Agent 处理的往往是用户私人数据日志里不能直接存原始内容要做脱敏或者只存哈希。这一点在工程化早期就要定好规范后期补很麻烦。另外端侧要有一个“诊断模式”用户遇到问题时可以手动开启记录更详细的日志方便开发者定位。这个模式默认关闭避免日常运行开销过大。6. 端侧 Agent 工程化的实操建议6.1 从最小可用闭环开始我见过太多人一上来就想做全能 Agent结果卡在工程细节里出不来。正确的做法是先做一个最小可用闭环一个模型、一个工具、一条完整链路从用户输入到工具执行到结果返回全部跑通。这个闭环可能只有几十行代码但它验证了核心链路。闭环跑通之后再逐步加工具、加记忆、加错误处理。每加一个能力都要回归测试确保没破坏原有链路。端侧工程化最怕的就是“加着加着就崩了”因为没有云端那种完善的监控和回滚机制。6.2 性能与功耗的平衡端侧 Agent 的性能优化不是越快越好而是要在响应速度和功耗之间找平衡。我的经验是简单意图用规则匹配或者小模型复杂意图才唤起大模型工具调用能并行就并行但并行数不要超过设备核心数推理频率要限制不要每次用户输入都跑满模型。实测下来一个设计良好的端侧 Agent日常使用功耗应该和普通应用差不多不能明显影响续航。如果发现功耗异常优先检查是不是有循环调用或者频繁唤醒模型。6.3 版本管理与灰度发布端侧 Agent 的模型、提示词、工具 schema 都是会变的必须做版本管理。我的做法是把这三样东西打包成一个“Agent 配置”每次更新整体替换避免版本错配。灰度发布在端侧同样重要。新版本先在小比例设备上跑观察错误率和性能指标没问题再全量。端侧没有云端那种实时回滚能力所以灰度是唯一的保险。最后分享一个我踩过的坑端侧 Agent 的提示词不要写得太“聪明”要写得“笨”一点。云端模型能理解复杂的指令端侧模型不行。提示词要短、要直接、要具体最好用祈使句避免歧义。我现在的提示词基本都是“你是助手。用户问什么你调用对应工具。工具返回什么你就说什么。”这种大白话效果反而比精心设计的复杂提示词好。端侧 Agent 工程化这条路没有银弹全靠一个个细节抠出来。但每解决一个问题Agent 就稳一分这种踏实感是云端开发很难体会到的。