ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

端侧Agent工程化实战:Function Calling、JSON Schema与MCP的稳定性设计

端侧Agent工程化实战:Function Calling、JSON Schema与MCP的稳定性设计 端侧 Agent 这两年从 demo 走向真正可用的产品中间隔着的其实不是模型能力而是工程化。我见过太多团队拿着一个效果惊艳的原型信心满满地要落地结果卡在工具调用的稳定性、上下文管理、错误恢复这些脏活累活上最后不了了之。这一篇我想聊的就是这些脏活累活——当你要把一个端侧 Agent 真正做成能交付的东西时绕不开的那几个工程问题。标题里说的工程化上我理解重点应该放在 Agent 与外部世界交互的这套机制上也就是 Function Calling、JSON Schema、MCP 这几块。为什么先讲这个因为端侧 Agent 和云端 Agent 最大的区别就在于它得在资源受限、网络不稳定、甚至完全离线的环境下可靠地调用本地能力。这套调用机制设计得好不好直接决定了你的 Agent 是能用还是好用。不管你是刚接触端侧 Agent 的新手还是已经踩过一些坑的老手这篇里的选型思路、参数细节和避坑经验应该都能对上你的某些实际场景。1. 端侧 Agent 工程化的整体设计思路1.1 为什么端侧 Agent 的工程化和云端完全是两回事先把一个认知摆正端侧 Agent 不是把云端 Agent 缩小了塞进手机。这个类比会害死人。云端 Agent 的工程假设是——算力近乎无限、网络永远在线、可以随时调用任意 API、失败了重试成本极低。端侧把这些假设全部推翻。算力上端侧能跑的模型参数量级通常在 1B 到 8B 之间量化后这意味着模型的推理能力、指令遵循能力、长上下文处理能力都明显弱于云端大模型。网络方面端侧场景经常是弱网甚至离线你不能假设每次工具调用都能打到远端服务。调用能力上端侧 Agent 能触及的是设备本地的能力——文件系统、传感器、本地应用、系统 API而不是云端那一堆 REST 接口。这些差异直接决定了工程化的重心不同。云端 Agent 工程化更多在拼 prompt 编排、多 Agent 协作、RAG 检索质量端侧 Agent 工程化则必须把大量精力放在调用协议的健壮性、上下文的极致压缩、失败路径的完备处理上。我个人的经验是端侧 Agent 项目里真正花在让模型更聪明上的时间可能只占两成剩下八成都在处理模型不够聪明时怎么兜底。所以这一篇讲 Function Calling、JSON Schema、MCP不是单纯讲 API 怎么用而是讲在端侧这个约束条件下怎么把这套交互机制设计得足够稳。1.2 工具调用机制的三层抽象从硬编码到协议化我习惯把端侧 Agent 的工具调用机制分成三层来看理解这三层后面所有的选型和取舍都会清晰很多。最底层是硬编码调用。就是你在代码里写死如果模型输出里包含打开相机就调用openCamera()。这种方式在 demo 阶段最快但完全不可扩展加一个功能就要改一次代码而且模型输出的解析极其脆弱。我早期做的一个端侧助手就是这么干的后来功能加到十几个代码里全是 if-else维护起来想死。中间层是基于 JSON Schema 的 Function Calling。这是目前主流方案。你用一种结构化的 schema 描述每个工具的名字、参数、类型模型根据用户意图输出一个符合 schema 的 JSON你的代码解析这个 JSON 去执行。相比硬编码它的好处是工具描述和调用逻辑解耦了加工具只需要加一份 schema 描述不用改调用框架。最上层是协议化的工具接入典型代表就是 MCP。MCP 把工具提供方和工具使用方彻底解耦工具不再是你 Agent 代码里的一部分而是一个独立运行的服务通过标准协议暴露能力。Agent 只需要知道有这么个服务它提供这些工具具体实现完全在黑盒里。这一层解决的是生态问题——让第三方能力可以即插即用。这三层不是替代关系而是递进关系。一个成熟的端侧 Agent 工程往往是底层用 Function Calling 处理核心高频能力上层用 MCP 接入可扩展的第三方能力。理解这个分层你在做架构决策时就不会纠结到底该用哪个。1.3 端侧约束下的核心取舍能力、体积、延迟的三角博弈端侧工程化最折磨人的地方在于你永远在三个东西之间做取舍能力覆盖度、包体积、响应延迟。这三个几乎不可能同时最优。举个例子。你想让 Agent 支持尽可能多的工具那工具描述schema就会变多这些描述要占上下文 token端侧模型上下文窗口本来就小工具一多留给对话历史的空间就没了模型还容易在众多工具里选错。你想让包体积小就得砍工具、砍模型量化精度能力又下来了。你想延迟低就得减少推理轮次但工具调用往往需要多轮先规划、再调用、再总结轮次一少复杂任务就做不了。我的取舍原则是这样的核心场景的工具走 Function Calling 硬集成保证延迟和稳定性长尾工具走 MCP 按需加载保证能力覆盖上下文用动态裁剪保证不爆窗口。这个原则不是拍脑袋来的是踩了很多坑之后总结的。后面几节会具体展开每一块怎么做。2. Function Calling 在端侧的落地细节2.1 Function Calling 的本质一次受约束的结构化生成很多人把 Function Calling 想得很神秘其实它的本质非常朴素它就是让模型生成一段符合特定结构的文本然后你用代码去解析这段文本。所谓调用是你在解析之后自己执行的模型本身并没有真的调用任何函数。理解这一点极其重要因为它意味着Function Calling 的可靠性一半取决于模型的结构化生成能力另一半取决于你的解析和容错逻辑。端侧模型的结构化生成能力普遍偏弱所以后一半的权重更大。一个典型的 Function Calling 流程是这样的你把工具定义schema和用户输入一起塞给模型模型输出一段 JSON比如{name: get_weather, arguments: {city: 北京}}你的代码解析出 name 和 arguments找到对应的函数执行把结果再塞回模型模型基于结果生成自然语言回复。端侧做这件事的难点在于小模型经常输出不合法的 JSON——少个引号、多个逗号、把字符串写成数字、甚至夹杂自然语言解释。所以你的解析器必须极其健壮不能假设模型一定输出完美 JSON。2.2 JSON Schema 设计让模型少犯错的第一道防线JSON Schema 不只是给解析器看的它更是给模型看的说明书。schema 设计得好模型犯错率能降一大截。我在端侧项目里总结了几条 schema 设计原则都是血泪教训。第一工具名要语义明确且唯一。别用getData、process这种含糊的名字模型在多个相似工具间会选错。用get_weather_by_city、convert_currency这种一看就懂的名字。端侧模型对语义的敏感度不如大模型名字起得越直白越好。第二参数描述要写清楚别偷懒。很多人 schema 里参数只写个类型就完事比如{city: {type: string}}。这样模型根本不知道 city 该填什么格式。正确的做法是加上 description{city: {type: string, description: 城市名称使用中文例如北京、上海}}。这个 description 会直接影响模型的填充质量。第三参数类型尽量用基础类型避免嵌套。端侧模型处理嵌套对象和数组的能力很弱。如果一个工具需要复杂参数宁可拆成多个简单工具也别搞一个深层嵌套的 schema。我试过一个需要嵌套三层对象的工具端侧模型几乎没一次填对过后来拆成三个扁平工具成功率立刻上来了。第四枚举值要显式列出。如果某个参数只能是几个固定值之一一定要用 enum 列出来别让模型自由发挥。比如{unit: {type: string, enum: [celsius, fahrenheit]}}。这样模型只能在两个值里选不会瞎编。下面是一个我实际用过的、比较规范的端侧工具 schema 示例{ name: set_alarm, description: 在设备上设置一个闹钟, parameters: { type: object, properties: { time: { type: string, description: 闹钟时间24小时制格式为 HH:MM例如 07:30 }, label: { type: string, description: 闹钟标签用于说明用途例如 起床、开会 }, repeat: { type: string, enum: [once, daily, weekday], description: 重复规则once 表示仅一次daily 表示每天weekday 表示工作日 } }, required: [time] } }注意这里required只放了time因为 label 和 repeat 可以缺省。端侧模型面对必填项太多的工具容易因为凑不齐参数而放弃调用。必填项越少调用成功率越高这是个很实用的经验。2.3 端侧模型的调用格式适配不同模型的方言问题这里有个很现实的坑不同端侧模型的 Function Calling 格式是不一样的。有的模型用特定的特殊 token 包裹工具调用比如某些模型用tool_call标签有的模型直接输出 JSON有的模型需要在 system prompt 里用特定模板注入工具定义。你不能假设一套 prompt 走天下。我踩过的坑是在 A 模型上调得好好的工具调用换到 B 模型上直接失效因为 B 模型根本不认那套格式。解决办法是给每个模型写一个适配层把统一的工具定义转换成该模型期望的格式再把该模型的输出解析回统一结构。这个适配层的设计思路是定义一套内部的工具描述结构跟具体模型无关然后针对每个模型实现两个函数——formatToolsForModel(tools)和parseModelOutput(output)。这样上层业务逻辑完全不用关心底层用的是哪个模型。对于端侧部署还有个细节很多端侧推理框架比如 llama.cpp 系列、MLC 等对 Function Calling 的支持程度不一。有的框架内置了工具调用模板你只要按它的格式传工具定义就行有的框架完全没有你得自己在 prompt 里拼。选框架的时候一定要先确认它对工具调用的支持情况否则后面会返工。2.4 多轮工具调用的状态管理别让上下文失控复杂任务往往需要多轮工具调用。比如用户说帮我查一下明天北京的天气如果下雨就提醒我带伞这需要先调用天气工具拿到结果判断是否下雨再决定是否设置提醒。这是两轮甚至三轮调用。端侧做多轮调用最大的敌人是上下文膨胀。每一轮的工具调用请求和结果都要塞回上下文几轮下来 token 就爆了。端侧模型上下文窗口可能只有 4K 到 8K非常紧张。我的做法是工具结果摘要化。工具返回的原始结果往往很长比如一个 API 返回一大坨 JSON但模型真正需要的可能只是其中一两个字段。所以在把工具结果塞回上下文之前先做一层提取和压缩只保留关键信息。比如天气 API 返回了温度、湿度、风速、气压、紫外线等十几个字段但用户只关心下不下雨那就只把降水概率和天气状况塞回去。另一个技巧是及时清理中间态。多轮调用完成后那些中间的请求和结果如果不再需要就从上下文里删掉只保留最终的对话。这需要你的上下文管理器支持按标记删除实现起来不难但效果显著。3. MCP 协议在端侧 Agent 中的角色3.1 MCP 到底解决了什么问题MCP 这个词最近热度很高但很多人对它的理解还停留在又一个协议的层面。我用一句话概括它的价值MCP 让工具从Agent 的一部分变成了Agent 可以连接的外部服务。在没有 MCP 之前你要给 Agent 加一个工具得改 Agent 的代码把工具实现和 schema 都塞进去重新打包发布。工具和 Agent 是强耦合的。MCP 之后工具可以是一个独立进程通过标准协议对外暴露Agent 只需要连接它、发现它有哪些工具、按需调用。工具可以独立更新、独立部署甚至由第三方提供。对端侧 Agent 来说这个解耦尤其有价值。因为端侧设备能力有限不可能把所有工具都内置进去。有了 MCP你可以让 Agent 按需连接本地或近端的 MCP 服务用到什么连什么不用就不占资源。3.2 MCP 的核心概念Server、Client、Tools、ResourcesMCP 的架构其实不复杂核心就几个概念我用大白话解释一遍。MCP Server是能力的提供方。它跑在一个进程里对外声明我提供这些工具、这些资源。比如一个文件管理的 MCP Server会声明它提供读文件写文件列目录这些工具。MCP Client是能力的消费方也就是你的 Agent。它连接到 Server查询有哪些工具可用然后按需调用。Tools是 Server 暴露的可执行操作对应 Function Calling 里的函数。Resources是 Server 暴露的数据比如文件内容、数据库记录Agent 可以读取但不能执行。通信上MCP 支持几种传输方式端侧场景常用的是本地进程间通信stdio或者本地网络HTTP/SSE。端侧 Agent 通常连接的是跑在同一设备或同一局域网内的 MCP Server。理解这几个概念后你会发现 MCP 本质上就是把 Function Calling 那套东西标准化、服务化了。工具定义还是 JSON Schema调用还是结构化请求只是多了一层协议封装和发现机制。3.3 端侧接入 MCP 的两种模式内置 Server 与外部连接端侧 Agent 接入 MCP我实践下来有两种模式各有适用场景。模式一内置 Server。把 MCP Server 作为 Agent 应用的一部分打包进去随应用启动。这种模式下Server 和 Agent 在同一进程或同一应用内通信走本地延迟极低。适合那些核心的、高频的、必须离线可用的能力。缺点是包体积会变大且工具更新要跟着应用一起发版。模式二外部连接。Agent 作为 MCP Client去连接外部独立的 MCP Server。这些 Server 可以是设备上其他应用提供的也可以是局域网内其他设备提供的。这种模式下Agent 本身很轻能力按需获取。适合长尾能力、需要独立更新的能力、或者由第三方提供的能力。缺点是依赖外部服务的可用性网络或服务挂了就调不了。我的实际做法是混合把最核心的十来个工具做成内置 Server保证基础体验把扩展能力做成外部 Server按需连接。这样既保证了核心场景的稳定又保留了扩展性。3.4 MCP 工具发现与动态加载的工程实现MCP 相比传统 Function Calling 最大的工程差异在于动态发现。传统方式下工具列表是编译期就确定的MCP 下工具列表是运行时从 Server 查询来的。这带来一个端侧特有的问题上下文预算的动态分配。你连的 Server 越多可用工具越多但你不能把所有工具定义都塞进上下文——端侧窗口装不下。所以需要一套工具筛选机制。我的做法是按场景分组 按需注入。把工具按功能域分组比如设备控制信息查询内容生成根据当前对话的意图只注入相关组的工具定义。判断意图可以用一个轻量分类器或者干脆让模型先做一次我需要哪类工具的判断再注入对应工具。这样上下文里始终只有当前需要的工具既省 token 又降低模型选错的概率。另一个细节是工具描述的缓存。MCP Server 的工具列表不会频繁变所以查询一次后可以缓存不用每次对话都去查。但要注意缓存失效策略——Server 更新了工具Client 得能感知到。通常用版本号或时间戳做校验。4. 工具调用的稳定性与容错工程4.1 解析失败的兜底当模型输出不是合法 JSON这是端侧 Agent 最高频的故障。模型输出的 JSON 各种奇葩少引号、多逗号、中文标点、夹杂解释文字、把整个 JSON 包在 markdown 代码块里……我统计过端侧小模型的首次 JSON 合法率大概只有六七成剩下三四成都要靠兜底。兜底策略我分三级。第一级是宽松解析不要求严格 JSON用容错解析器比如支持尾逗号、单引号、无引号 key 的解析器尽量把内容抠出来。第二级是正则提取如果宽松解析也失败用正则从输出里找{...}结构尝试提取。第三级是重试前两级都失败把错误信息反馈给模型让它重新生成一次通常加一句请只输出合法 JSON不要有其他内容能救回大部分。实测下来三级兜底能把工具调用的整体成功率从六七成拉到九成以上。剩下那一成就交给用户重试或者降级到纯对话模式。4.2 参数校验与修正模型填错参数怎么办模型输出合法 JSON 不代表参数填对了。常见错误有类型不对该填数字填了字符串、枚举值不在范围内、必填项缺失、格式不符合要求比如时间格式写错。我的做法是在执行工具前加一层参数校验与修正。校验用 JSON Schema 的校验器大部分语言都有成熟库校验失败时先尝试自动修正类型不对就尝试转换字符串 30 转数字 30枚举值接近就做模糊匹配摄氏度 匹配到 celsius格式不对就尝试规范化7点30 规范成 07:30。自动修正搞不定的再反馈给模型重填。这里有个技巧反馈时不要只说参数错误要具体说参数 time 格式应为 HH:MM你填的是 7点30请重新填写。具体的错误信息能显著提高模型修正的成功率。4.3 超时、重试与降级端侧不可靠环境的应对端侧环境不可靠是常态。工具执行可能超时本地服务卡住、可能失败权限不足、资源被占、可能返回异常。这些都得有应对。超时每个工具调用都要设超时端侧建议设短一点比如 3 到 5 秒。超时后不要傻等直接走失败路径。重试不是所有失败都值得重试。网络类、临时资源类失败可以重试参数类、权限类失败重试也没用。重试要有次数上限我一般设 2 次和退避策略间隔递增避免雪崩。降级工具彻底不可用时Agent 要能优雅降级。比如天气工具挂了Agent 应该告诉用户暂时查不了天气而不是卡死或者报一堆错误。降级路径要在设计阶段就想好别等出问题了才补。下面这张表是我整理的常见故障与应对策略可以直接对照排查故障类型典型表现应对策略JSON 解析失败输出非法 JSON三级兜底宽松解析、正则提取、反馈重试参数类型错误数字填成字符串自动类型转换失败则反馈重填枚举值越界填了不存在的选项模糊匹配到最近合法值必填项缺失少填了 required 字段反馈模型补填或使用默认值工具执行超时本地服务无响应设短超时走失败路径工具执行异常权限不足、资源占用分类处理可重试的退避重试工具不存在调用了未注册的工具校验工具名反馈模型重新选择上下文溢出token 超限动态裁剪历史摘要化工具结果4.4 调用链路的可观测性出问题了怎么定位端侧 Agent 出问题最难的是定位——用户说它没反应你根本不知道卡在哪一步。所以可观测性必须做。我的做法是在调用链路上埋点记录每个关键节点模型输入、模型原始输出、解析结果、校验结果、工具执行请求、工具执行结果、最终回复。这些日志本地存一份注意隐私敏感内容脱敏出问题时能回放整个链路。端侧日志要注意体积不能无限存。我一般用环形缓冲只保留最近 N 次调用。另外可以做一个调试模式开启后记录详细日志平时只记关键节点平衡可观测性和资源占用。5. 上下文与性能的工程优化5.1 工具定义的动态裁剪省 token 就是省一切前面提过工具定义占上下文。端侧窗口小工具一多就爆。所以工具定义的动态裁剪是刚需。裁剪策略我按优先级排第一按意图筛选只注入当前意图相关的工具组。第二按使用频率排序高频工具优先保留低频工具在窗口紧张时裁掉。第三工具描述精简description 写清楚但不啰嗦能省则省。实测下来一个原本需要 2000 token 的工具定义集经过动态裁剪后通常能压到 500 token 以内省下的空间留给对话历史和工具结果整体体验提升明显。5.2 工具结果的压缩与摘要别把原始数据全塞回去工具返回的原始数据往往远超模型需要。一个查询接口可能返回几十个字段模型只需要其中两三个。把原始数据全塞回上下文既浪费 token 又干扰模型判断。我的做法是给每个工具配一个结果处理器负责从原始结果里提取关键字段转成紧凑格式。比如天气工具返回一大坨 JSON处理器只提取天气状况、温度、降水概率三个字段拼成一句简短的话塞回去。这样既省 token模型理解起来也更直接。对于确实需要保留大量数据的场景比如文档内容用分块 按需加载先只塞摘要或前几段模型需要更多时再按需取。别一次性全塞。5.3 端侧推理的性能调优延迟从哪来怎么降端侧 Agent 的延迟主要来自三块模型推理、工具执行、上下文处理。工具执行通常很快本地调用上下文处理是纯计算也快大头在模型推理。降推理延迟的手段有几个。一是减少推理轮次能一轮搞定的别搞两轮这需要在 prompt 设计上下功夫让模型一次输出完整的调用计划。二是用更小的模型处理简单任务复杂任务才上大模型做模型分级路由。三是优化推理参数比如限制 max_tokens工具调用输出通常很短没必要给大预算、用更激进的量化。我实测过一个优化把工具调用的 max_tokens 从默认的 512 降到 128因为工具调用 JSON 通常就几十个 token结果延迟降了将近三成而且几乎不影响成功率。这种小优化积累起来体验差别很大。5.4 内存与功耗端侧绕不开的硬约束端侧还有个云端没有的约束内存和功耗。模型加载占内存上下文缓存占内存工具执行也可能占内存。内存一紧张系统就可能杀进程。我的经验是模型常驻上下文按需。模型加载慢尽量常驻内存别反复加载上下文缓存可以按需创建和释放对话结束就清掉。另外工具执行完要及时释放资源别让临时对象堆积。功耗方面推理是耗电大户。端侧 Agent 要避免频繁唤醒推理能批量处理的别拆成多次。比如多个工具调用如果能合并成一次推理输出就别分多次。这对续航的影响很直接。6. 实操中的常见问题与排查技巧6.1 工具调用成功率低的排查路径工具调用成功率低别急着换模型先按这个顺序排查。先看 schema。工具名是否清晰参数描述是否完整必填项是否太多枚举是否明确我遇到过好几次问题就出在 schema 写得含糊模型根本不知道该怎么填。再看 prompt。工具定义是怎么注入的格式对不对有没有给模型足够的调用示例端侧模型很吃示例给一两个 few-shot 示例成功率能明显提升。然后看解析。解析器是否足够健壮失败时有没有兜底很多成功率低其实是解析失败被算进去了实际模型输出是对的。最后才看模型。如果前面都没问题那可能是模型本身的结构化生成能力不足考虑换个对 Function Calling 支持更好的模型。6.2 MCP 连接失败的典型原因MCP 连接失败常见原因有这么几个。传输方式不匹配Client 用 stdioServer 却监听 HTTP自然连不上。Server 未启动外部 Server 依赖的进程没起来。权限问题端侧访问某些资源需要权限没授权就失败。版本不兼容MCP 协议本身在演进Client 和 Server 版本差太多可能握手失败。排查时先确认 Server 是否正常单独跑一下 Server 看能不能起来再确认传输配置是否一致最后看日志里的握手信息。MCP 的日志通常比较详细仔细看能定位到具体哪一步失败。6.3 上下文溢出的预防与处理上下文溢出是端侧高频问题。预防手段前面说了工具定义裁剪、工具结果压缩、历史清理。处理手段是溢出时的紧急裁剪检测到接近窗口上限立刻按优先级丢弃最不重要的内容通常是较早的对话历史保证当前轮次能完成。我建议在上下文管理器里设两个阈值软阈值比如窗口的 80%触发主动裁剪硬阈值比如 95%触发紧急裁剪。主动裁剪温和保留关键信息紧急裁剪激进只保当前轮次。两级配合基本不会真的溢出。6.4 一份可直接对照的排查速查表现象可能原因排查动作模型不调用工具工具描述不清、prompt 缺示例检查 schema 和 few-shot调用错工具工具名相似、描述重叠重命名工具明确区分描述参数填错描述不完整、类型复杂补全 description简化类型JSON 解析失败模型输出不规范加兜底解析反馈重试工具执行超时本地服务卡顿设短超时检查服务状态MCP 连不上传输不匹配、Server 未启动核对配置单独测 Server上下文溢出工具多、结果长裁剪工具定义压缩结果响应慢推理轮次多、max_tokens 大减轮次降 max_tokens这张表我贴在工位上很久了出问题先扫一遍八成能对上。7. 一些踩坑之后的个人体会做端侧 Agent 工程化这段时间最大的体会是别跟端侧的约束较劲要学会顺着约束设计。云端那套能力不够就加模型、加算力的思路在端侧行不通。端侧的正确姿势是承认模型不够强然后用工程手段把它的短板补上——schema 设计得让模型少犯错兜底逻辑做得让错误可恢复上下文管得让窗口够用。还有一个体会是工具调用的稳定性比工具的数量重要得多。我早期贪多给 Agent 塞了几十个工具结果模型天天选错用户体验极差。后来砍到十几个核心工具每个都打磨到调用成功率九成以上体验反而好了。端侧用户要的是我说的事它能办成而不是它好像什么都能干但什么都干不好。最后分享一个我觉得很值的小技巧给工具调用加一个确认环节。对于有副作用的工具比如删除文件、发送消息在真正执行前让 Agent 跟用户确认一下。这既避免了误操作也给模型多一次纠错机会。实现上就是在工具 schema 里加个requires_confirmation标记执行前检查这个标记需要确认的就先问用户。这个机制帮我挡掉了不少因为模型理解偏差导致的误操作。MCP 这块生态还在快速演进工具发现、权限管理、多 Server 协作这些方向都还有不少工程问题值得深挖。下一篇如果聊工程化下我打算讲讲 Agent 的状态持久化、多 Agent 协作在端侧的落地以及怎么给端侧 Agent 做一套靠谱的评测体系——这些也是实际项目里绕不开的硬骨头。
返回列表