端侧Agent工程化实战:Function Calling裁剪与MCP协议落地
1. 端侧 Agent 工程化的核心命题1.1 为什么端侧 Agent 不能照搬云端那一套端侧 Agent 和云端 Agent 最大的区别不在于模型大小而在于资源边界和交互模式。云端 Agent 可以随意调用几十个工具、维护超长上下文、跑多轮反思循环因为算力和带宽是弹性的。端侧不行——内存有限、电量有限、用户耐心更有限。我最初做端侧 Agent 时踩的第一个坑就是直接把云端的 ReAct 循环搬过来。结果一个简单的“帮我查下明天天气并设个提醒”Agent 在思考-行动-观察之间来回跳了七次每次都要重新走一遍本地推理耗时接近 20 秒。用户早就手动打开天气 App 了。所以端侧 Agent 工程化的第一原则是把复杂度从运行时转移到设计时。能在编译期确定的事情绝不留给推理时去决策。这直接引出了后面要讲的 Function Calling 裁剪、MCP 协议选型、以及状态机的显式建模。1.2 工程化的三个层次接口、编排、状态我把端侧 Agent 的工程化拆成三个层次后面所有内容都围绕这三层展开。接口层解决的是“Agent 怎么和外界说话”。Function Calling 是当前最主流的方案但端侧不能无脑全量注册工具需要做工具筛选和 schema 压缩。MCP 则是把接口标准化让同一个 Agent 能接入不同来源的能力不用为每个工具写适配代码。编排层解决的是“多个步骤怎么串起来”。端侧不适合动态规划更适合预定义的流程模板加有限分支。我通常会把高频场景固化成状态机只在必要节点让 LLM 做决策。状态层解决的是“上下文怎么管”。端侧内存紧张不能把所有历史都塞进 prompt。需要做分层记忆最近几轮保留原文较早的压缩成摘要更早的只保留结构化槽位。这三层不是孤立的。接口层的工具描述会影响编排层的决策复杂度编排层的状态设计又决定了状态层要存什么。工程化的难点就在于让三层协同而不是各自为政。1.3 本篇要解决的问题边界这篇是“Agent 工程化上”重点放在接口层和编排层也就是 Function Calling 的端侧适配、MCP 协议的落地、以及多步流程的编排策略。状态管理和记忆压缩放到下篇展开因为那部分涉及向量检索和摘要模型内容量单独成篇更合适。如果你正在做端侧 Agent或者准备把云端 Agent 往端侧迁移这篇里的工具裁剪方法、MCP 接入步骤、状态机编排模板都可以直接参考。我不讲空洞的架构图只讲我实际跑通过的方案和踩过的坑。2. Function Calling 在端侧的裁剪与适配2.1 全量注册工具为什么在端侧行不通Function Calling 的基本原理不复杂把可用工具的名称、描述、参数 schema 塞进 system prompt模型根据用户输入决定调哪个工具、传什么参数。云端模型上下文大注册二三十个工具毫无压力。端侧模型上下文通常只有 4K 到 8K token工具描述一多留给对话历史的空间就没了。我实测过一个数据一个中等复杂度的工具名称加描述加参数 schema平均消耗 120 到 200 token。注册 20 个工具光工具描述就吃掉 3000 token 左右。端侧模型本来推理能力就弱上下文再被工具描述挤占表现会断崖式下降。更麻烦的是工具越多模型选错的概率越高。这不是端侧独有的问题但在端侧更致命因为端侧没有云端那种“选错了重试一次”的余裕每次重试都是实打实的延迟和耗电。所以端侧 Function Calling 的第一件事不是“怎么调”而是“调哪些”。2.2 工具筛选按场景动态加载而不是全量常驻我的做法是把工具按场景分组运行时只加载当前场景相关的工具。具体分三步。第一步给每个工具打场景标签。比如“查天气”“设提醒”“查日程”都属于“日常助手”场景“发消息”“查联系人”属于“通讯”场景“调音量”“开蓝牙”属于“设备控制”场景。一个工具可以属于多个场景但标签要明确。第二步用一个轻量分类器判断当前用户输入属于哪个场景。这个分类器不需要 LLM用规则加小模型就够了。规则部分匹配关键词小模型部分处理模糊表达。我试过直接用 LLM 做场景分类准确率确实高一点但每次分类要多花 300 到 500 毫秒不划算。第三步只把当前场景的工具注册进 prompt。如果分类器置信度低就加载一个“通用工具集”里面只放最高频的五六个工具。这套方案实测下来工具描述占用的 token 从 3000 降到了 600 左右模型选工具的准确率反而提升了。因为干扰项少了端侧模型那点有限的注意力能集中在少数几个选项上。注意场景分类器要留一个“兜底”分支。当用户输入无法归类时不要强行塞进某个场景而是走通用工具集加澄清询问。我见过有人为了省事分类器输出什么就加载什么结果用户说“帮我弄一下那个”系统加载了“设备控制”场景模型一脸懵。2.3 Schema 压缩把工具描述从 200 token 压到 60 token工具筛选解决的是“注册几个”的问题Schema 压缩解决的是“每个工具占多少”的问题。端侧模型的工具描述不能像云端那样写小作文要精简到极致。我的压缩策略有四条。砍掉冗余描述。云端工具描述经常写“此工具用于查询指定城市的当前天气状况返回温度、湿度、风力等信息”。端侧只需要“查天气输入城市名返回温度湿度风力”。把“此工具用于”“返回...等信息”这类套话全删掉。参数名用短词。city_name改成cityreminder_content改成textscheduled_time改成time。模型对参数名的语义理解没那么依赖完整拼写短词完全够用。枚举值能省则省。如果一个参数只有两三个取值直接在描述里写“取值a/b/c”不要单独列 enum 结构。JSON schema 的 enum 字段本身也占 token。合并同类工具。“查当前天气”和“查未来天气”可以合并成一个“查天气”用参数区分。端侧工具数量少比功能细更重要。下面是我实际用的一个压缩前后对比项目压缩前压缩后工具名get_current_weatherweather描述查询指定城市的当前天气状况返回温度、湿度、风力等信息查天气输入城市返回温度湿度风力参数city_name: string, 要查询的城市名称city: stringtoken 估算约 180约 55一个工具省 125 token十个工具就是 1250 token。在 4K 上下文的端侧模型上这 1250 token 能多放三四轮对话历史体验差距非常明显。2.4 端侧 Function Calling 的解析容错端侧模型输出 JSON 的稳定性远不如云端。我遇到过各种奇葩输出多一个逗号、少一个引号、把参数值写成中文、在 JSON 前后加解释性文字。如果解析器不够健壮Agent 直接崩掉。我的解析流程是三层过滤。第一层正则提取。从模型输出里用正则抓出第一个完整的 JSON 对象忽略前后的自然语言。正则不要写太复杂匹配最外层花括号就行。第二层修复常见错误。尾逗号删掉单引号换成双引号中文引号换成英文引号缺右括号补上。这些都是规则修复不需要模型介入。第三层schema 校验加默认值填充。用工具定义的 schema 校验解析结果缺参数就填默认值类型不对就尝试转换。比如模型把time写成明天下午三点而 schema 要求 ISO 时间格式就调用一个本地时间解析函数转换。三层都失败才走兜底告诉用户“我没理解清楚你能再说一遍吗”。这个兜底话术要自然不要让用户觉得是系统出错了。实操心得解析失败的日志一定要存下来。我靠分析这些失败案例发现端侧模型最容易在参数值里加单位比如把25写成25度。后来我在 schema 描述里明确写“只填数字不要单位”失败率降了一半。这种经验只能从实际日志里来文档不会告诉你。3. MCP 协议在端侧 Agent 中的落地3.1 MCP 到底解决了什么问题MCP 全称 Model Context Protocol是一个让 Agent 和外部能力提供方之间标准化通信的协议。在 MCP 出现之前每接一个工具就要写一套适配代码这个工具用 HTTP那个用 WebSocket另一个用本地函数调用。工具一多适配代码比业务逻辑还多。MCP 的思路是把“能力提供”抽象成 ServerAgent 作为 Client 通过统一协议去发现和调用。Server 暴露自己有哪些工具、每个工具什么参数Client 不用提前知道运行时查询就行。对端侧 Agent 来说MCP 的价值有两个。一是解耦工具的实现和 Agent 的代码分开工具升级不用重新编译 Agent。二是动态发现Agent 启动时连上 MCP Server自动获取可用工具列表不用把工具描述硬编码在 prompt 里。但 MCP 也不是银弹。端侧资源有限MCP 的通信开销和 Server 管理成本必须考虑。下面讲具体怎么落地。3.2 端侧 MCP 的两种接入模式端侧 Agent 接入 MCP 有两种模式我都在项目里用过各有适用场景。本地进程模式。MCP Server 和 Agent 跑在同一台设备上通过本地 socket 或标准输入输出通信。这种模式延迟最低没有网络开销适合设备控制类工具比如调音量、读传感器、操作本地文件。缺点是 Server 要跟着 Agent 一起打包分发包体积会变大。远程连接模式。MCP Server 跑在远端Agent 通过网络连接。这种模式适合需要外部数据的工具比如查天气、搜网页、调在线服务。优点是 Agent 包体积小工具更新不用发版。缺点是依赖网络延迟不可控。我的选择策略是能本地就本地必须远程才远程。设备控制、本地文件、系统状态这类工具全部走本地进程模式因为它们的响应时间直接影响用户体验走网络绕一圈没必要。信息查询类工具走远程模式因为数据本来就在远端。两种模式可以共存。Agent 启动时先连本地 Server再按配置连远程 Server把所有工具合并成一个列表。对上层编排逻辑来说不用关心工具是本地还是远程统一按 MCP 接口调用就行。3.3 MCP 工具发现的端侧优化标准 MCP 流程是 Agent 连接 Server 后调用tools/list获取全部工具然后逐个注册。端侧不能这么干原因和前面 Function Calling 一样工具太多上下文扛不住。我的优化是在 MCP 层做一次预筛选。Agent 连接 Server 后先获取工具列表但不全部注册进 prompt而是存到本地工具索引里。然后根据当前场景从索引里挑出相关工具只把挑出来的注册进 prompt。这个索引结构很简单工具名、场景标签、token 估算、Server 地址。场景标签在 MCP Server 定义工具时就要带上作为工具元数据的一部分。如果 Server 没提供标签Agent 端可以维护一个映射表按工具名匹配场景。这样做的好处是MCP 的动态发现能力保留了但不会一次性把所有工具塞进上下文。工具索引本身不占 prompt 空间只在需要时查询。注意MCP Server 返回的工具描述可能很长端侧要在注册前做一次截断或压缩。我的做法是保留工具名和参数列表描述截断到 50 字以内。如果描述里有关键信息被截掉了就在本地索引里补一条备注但不进 prompt。3.4 MCP 通信的容错与降级端侧环境不稳定MCP 连接可能断Server 可能无响应。Agent 必须有降级策略不能因为一个工具连不上就整个卡住。我的容错设计分三级。连接级容错。MCP 连接建立时设超时比如 2 秒。超时就标记该 Server 不可用跳过它的工具继续加载其他 Server。不要让用户等一个连不上的 Server。调用级容错。工具调用设超时比如 3 秒。超时就返回“工具暂时不可用”让编排层决定是重试还是走备选方案。重试最多一次避免死循环。降级方案。每个远程工具在本地索引里可以配一个降级方案。比如“查天气”的远程 MCP 连不上就降级到本地缓存的天气数据或者直接告诉用户“网络不太好稍后再试”。降级方案不一定要完美但要让用户知道发生了什么而不是无声无息地失败。我踩过的一个坑是MCP Server 连不上时Agent 一直在后台重连耗电飞快。后来加了重连退避策略第一次等 1 秒第二次等 2 秒第三次等 4 秒最多重连三次就放弃。这个策略写进配置里不同工具可以调不同参数。4. 多步流程的编排策略4.1 端侧为什么不适合动态规划云端 Agent 常用动态规划让 LLM 自己决定下一步做什么走一步看一步。端侧不行原因有三个。推理成本高。每次让 LLM 做规划决策都要跑一次完整推理。端侧模型推理速度本来就慢多步规划意味着多次推理延迟叠加起来用户根本等不了。上下文消耗大。动态规划需要把每一步的观察结果都塞回 prompt上下文迅速膨胀。端侧那点上下文走三步就满了。稳定性差。端侧模型能力有限动态规划容易跑偏。一步错步步错最后出来的结果完全不可用。所以端侧编排的核心思路是把动态规划变成静态流程加有限分支。高频场景固化成状态机只在关键决策点让 LLM 介入。4.2 状态机编排把高频场景固化下来状态机编排的基本单位是“状态”和“转移”。每个状态代表一个步骤转移代表步骤之间的跳转条件。LLM 只在需要理解自然语言或做模糊判断的状态里出现其他状态都是确定性代码。以“设提醒”这个高频场景为例我把它拆成四个状态。状态一意图确认。判断用户是不是要设提醒。这个用规则加小模型就够了不需要 LLM。匹配到“提醒”“叫我”“别忘了”这类词进入下一状态。状态二信息抽取。从用户输入里抽时间和事项。这个需要 LLM因为时间表达千变万化“明天下午三点”“下周一早上”“吃完午饭之后”都要能处理。LLM 输出结构化的时间和事项解析后进入下一状态。状态三信息补全。检查时间和事项是否完整。缺时间就问时间缺事项就问事项。这个状态是确定性的根据缺失字段生成追问话术。状态四确认与执行。把抽取到的信息展示给用户确认确认后调用设提醒的工具。确认话术用模板生成不需要 LLM。整个流程里LLM 只在状态二出现一次。其他状态都是确定性代码延迟可控稳定性高。用户说“明天下午三点提醒我开会”从输入到执行完成实测 1.2 秒左右其中 LLM 推理占 800 毫秒其余是状态跳转和工具调用。4.3 分支设计什么时候让 LLM 做决策状态机不是完全排除 LLM 决策而是在关键分支点让 LLM 介入。我的原则是能用规则判断的用规则规则覆盖不了的用 LLM。还是设提醒的例子。状态二抽取完信息后有一个分支时间和事项都全了直接进状态四缺信息进状态三。这个分支用规则判断就行检查字段是否为空。但有些分支规则搞不定。比如用户说“提醒我明天那个事”这里的“那个事”指代不明需要结合上下文判断。这种分支就要让 LLM 介入把最近几轮对话和当前输入一起给 LLM让它判断“那个事”指的是什么。我的做法是在状态机里预留“LLM 决策节点”。这些节点的转移条件不是硬编码的而是调用 LLM 输出一个决策标签再根据标签跳转。LLM 决策节点的 prompt 要极简只给必要上下文和决策选项输出限定在几个标签里避免模型自由发挥。实操心得LLM 决策节点的输出一定要做校验。我遇到过模型输出一个不在选项里的标签状态机直接卡死。后来加了校验输出不在预期标签里就走默认分支同时记日志。默认分支通常是“追问澄清”最安全。4.4 编排层的工具调用顺序优化多步流程里工具调用的顺序会影响体验。有些工具可以并行调有些必须串行。端侧资源有限并行调用要谨慎但该并行的时候不并行延迟会很难看。我的判断标准是无依赖的工具并行有依赖的工具串行。比如“查天气并设提醒”这个流程查天气和设提醒之间没有数据依赖理论上可以并行。但设提醒需要用户确认而确认话术里可能要包含天气信息这就产生了依赖。所以实际编排是先查天气拿到结果后生成确认话术用户确认后再设提醒。再比如“查日程并查天气”这两个完全独立可以并行。端侧并行调用的实现方式是两个工具调用同时发起等两个都返回或超时后再继续。并行调用的超时要设短一点比如 2 秒避免一个慢工具拖累整个流程。编排层还要处理工具调用失败的情况。我的策略是关键路径上的工具失败就中断流程非关键路径上的工具失败就跳过。比如设提醒流程里设提醒工具是关键路径失败必须告诉用户查天气是非关键路径失败就跳过确认话术里不提天气就行。4.5 编排配置的存储与热更新状态机编排的配置不能硬编码在代码里否则改一个流程就要重新发版。我的做法是把编排配置存成 JSONAgent 启动时加载运行时可以热更新。配置结构大概是这样状态列表、每个状态的类型规则/LLM/工具调用、转移条件、LLM 节点的 prompt 模板、工具调用的参数映射。这套配置用 JSON 描述不涉及代码逻辑产品和运营也能看懂改起来不用等开发排期。热更新的触发方式有两种。一种是启动时拉取最新配置另一种是运行中收到推送后更新。端侧我倾向启动时拉取因为运行中更新状态机有风险万一新配置有问题用户正在用的流程会中断。启动时拉取最坏情况是这次启动用了旧配置下次启动就好了。配置版本要管理好。每个配置带版本号Agent 记录当前使用的版本。如果新配置加载失败回滚到上一个可用版本。这个回滚机制必须有我吃过亏一次配置更新把某个状态的转移条件写错了导致设提醒流程死循环用户投诉了好几个。5. 实操中的常见问题与排查5.1 工具调用失败排查速查表端侧 Agent 的工具调用失败原因很多我整理了一个速查表按现象查原因比从头翻日志快得多。现象可能原因排查方法解决模型不调工具直接回答工具描述没进 prompt打印实际 prompt检查工具段检查场景分类器是否加载了工具调了错误的工具工具描述太相似对比相似工具的 schema合并同类工具或加区分性描述参数解析失败模型输出格式错误看原始输出和解析日志加强解析容错schema 加示例工具调用超时远程 Server 无响应检查网络和 Server 状态加超时和降级重试一次多步流程卡住状态转移条件不满足打印状态跳转日志检查转移条件加默认分支上下文溢出工具描述或历史太长统计 prompt token 数压缩 schema裁剪历史这张表我贴在工位上排查问题时先对现象再按方法查大部分问题五分钟内能定位。5.2 端侧模型选工具不准的三种解法端侧模型选工具不准是高频问题我试过三种解法效果递增。解法一加 few-shot 示例。在 prompt 里放两三个“用户输入→工具调用”的示例。这对简单场景有效但示例本身占 token端侧上下文紧张时要慎用。解法二工具描述加区分性关键词。如果两个工具容易混在描述里加区分词。比如“查天气”和“查空气质量”前者描述里加“温度湿度”后者加“PM2.5 指数”。模型看到关键词就能区分。解法三两阶段选择。先让模型选工具类别再在类别里选具体工具。比如先选“信息查询”还是“设备控制”再选具体工具。两阶段比一阶段多一次推理但准确率提升明显。端侧如果延迟允许这个方案最稳。我现在的项目用的是解法二加解法三的组合工具描述加区分词同时把工具分成三四个大类先选类再选工具。实测选工具准确率从 70% 左右提升到 90% 以上。5.3 MCP 连接不稳定的处理经验MCP 连接不稳定在端侧很常见尤其是远程 Server。我的处理经验有三条。连接池要小。端侧不要维护太多 MCP 连接同时活跃的连接控制在三四个以内。连接多了每个连接的心跳和重连都会耗资源。心跳间隔要长。云端 MCP 可能 10 秒一次心跳端侧改成 30 秒甚至 60 秒。心跳太频繁端侧电量扛不住。但心跳间隔长了连接断了发现得晚所以调用前要检查连接状态断了就重连。重连要有退避。前面提过重连不能一直试。我的配置是首次重连等 1 秒第二次等 3 秒第三次等 9 秒三次失败就标记 Server 不可用等下次启动再试。注意MCP Server 的日志要能自定义。端侧调试时标准输出可能被系统吞掉要把 MCP 日志写到文件里方便排查。我一般让 Server 把连接事件、工具调用、错误信息都写到本地日志文件出问题时直接看文件。5.4 编排配置出错的回滚机制编排配置出错是生产环境最危险的问题因为影响面大。我的回滚机制分三步。配置校验。新配置加载前先校验状态转移是否有环、LLM 节点是否有默认分支、工具调用是否有超时设置。校验不过直接拒绝加载用旧配置。灰度加载。新配置先在一个低频场景试用观察一段时间没问题再全量。端侧做灰度比较麻烦我的简化做法是新配置加载后前三次流程走新配置同时记录日志如果三次都正常就继续用有异常就回滚。快速回滚。保留上一个可用配置的副本回滚时直接替换不用重新拉取。回滚要能在 1 秒内完成避免用户感知。这套机制我用了半年多成功拦截过两次配置错误。一次是转移条件写反了一次是 LLM 节点 prompt 模板少了个变量。如果没有校验和回滚这两个错误都会直接暴露给用户。5.5 端侧 Agent 工程化的经验总结做了几个端侧 Agent 项目后我最大的体会是端侧工程化的核心是“做减法”。云端 Agent 可以堆功能、堆工具、堆上下文端侧不行。端侧要把每一个 token、每一毫秒、每一毫安都算清楚。Function Calling 要裁剪不能全量注册。MCP 要筛选不能全量发现。编排要固化不能动态规划。这些“不能”不是限制而是端侧工程化的设计约束。在约束下找到最优解才是端侧 Agent 工程化的价值所在。还有一个体会是日志和监控比功能更重要。端侧环境复杂用户设备千差万别没有完善的日志出了问题根本不知道从哪查。我在项目里把工具调用、状态跳转、MCP 连接、配置加载都打了日志日志分级存储关键日志上报。这些日志帮我定位了大部分线上问题。最后分享一个小技巧端侧 Agent 的 prompt 要版本化。每次改 prompt 都记版本号日志里带上版本号。这样分析问题时能知道是哪个版本的 prompt 出的问题回滚也有依据。我见过有人改 prompt 不记版本出了问题不知道改了什么只能全部回滚损失很大。这个内容后续还可以这样扩展状态管理和记忆压缩的具体实现包括分层记忆的结构设计、摘要模型的选型和压缩策略、以及向量检索在端侧的轻量化方案。那部分内容量不小放到下篇单独讲更合适。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →