ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

端侧Agent工程化实战:Function Calling契约设计与上下文管理

2026/10/7 19:15:13 拓冰建站 浏览量
端侧Agent工程化实战:Function Calling契约设计与上下文管理 端侧 Agent 的系列文章写到第三篇话题终于从模型能不能跑在设备上转向了跑起来之后怎么把它变成一个能交付的工程。这个转折点其实挺关键的。我见过太多团队在 Demo 阶段兴奋得不行一个 Function Calling 跑通就以为大功告成结果一上真实场景工具数量从 3 个涨到 30 个参数从两个字符串变成嵌套三层的对象模型开始胡编参数、乱调工具、把不存在的字段塞进 JSON整个链路直接崩掉。问题不在于模型不够聪明而在于我们从来没把 Agent 当成一个正经的软件系统来设计。这篇主要聊 Agent 工程化里最容易被低估、也最容易翻车的两块工具调用的契约设计和上下文与协议的组织方式。前者决定了模型能不能稳定地说对话后者决定了这套东西能不能从单机 Demo 长成一个可维护的系统。Function Calling、JSON Schema、MCP 这些词最近热度很高但热词背后真正要解决的问题其实是工程约束而不是概念本身。不管你是刚接触端侧 Agent 的新手还是已经踩过几轮坑的老手这篇里的思路和参数细节应该都能直接用上。1. 为什么端侧 Agent 的工程化比云端更棘手先把一个前提说清楚端侧 Agent 和云端 Agent 在工程上的难点完全不是一回事。云端你可以假设网络稳定、算力管够、模型随便换大的工程重心放在编排和可观测性上。端侧反过来算力、内存、电量、模型体积全是硬约束你没法靠换个更强的模型来兜底只能靠工程手段把有限的能力榨干。1.1 端侧的三重约束算力、内存、上下文窗口端侧设备上跑的模型参数量通常被压到 1B 到 7B 这个区间量化之后更小。这意味着两件事第一模型的指令遵循能力天然弱于云端大模型你给它一个复杂的 JSON Schema它未必能一次填对第二上下文窗口往往只有 2K 到 8K token塞不下几十个工具的完整描述。我实测过一个 3B 量化模型给它 15 个工具的完整 JSON Schema光工具描述就吃掉了将近 3000 token留给对话历史和用户输入的空间所剩无几。更糟的是工具一多模型的选择准确率断崖式下跌——从 5 个工具时的 90% 多掉到 15 个工具时的 60% 出头。这不是模型的问题是你把选择题出得太难了。所以端侧工程化的第一条原则就是别把云端那套工具越多越强大的思路直接搬过来。端侧要做的是减法是分层是让模型在每一步只面对它真正需要做的决策。1.2 从能跑通到能交付的鸿沟在哪Demo 和产品的差距在 Agent 场景里被放得特别大。Demo 阶段你只测那几条 happy path用户问什么、模型调什么工具都是你设计好的。一旦上线用户会问出你想象不到的问题模型会在你没预料的地方调用工具参数会以各种奇怪的形式出现。我总结下来这条鸿沟主要体现在三个地方参数稳定性Demo 里模型偶尔填错参数你能手动改产品里没人帮你兜底必须靠 Schema 约束和校验层拦住。工具选择的确定性工具少的时候模型靠语义相似度就能选对工具多了必须引入路由、分组、检索等机制。失败的可恢复性Demo 里报错就重来产品里必须设计重试、降级、澄清追问等恢复路径。这三条恰好就是工程化要解决的核心问题。下面逐个拆。2. Function Calling 的契约本质JSON Schema 不是装饰品很多人对 Function Calling 的理解停留在模型输出一个函数名和参数这个层面觉得 JSON Schema 就是给模型看的说明书写得差不多就行。这个认知是端侧 Agent 翻车的头号原因。JSON Schema 在 Function Calling 里扮演的角色是模型和你的代码之间的一份正式契约它同时约束了模型该怎么输出、你的代码该怎么校验、出错时该怎么反馈。2.1 Schema 描述质量直接决定调用成功率同一个工具Schema 写得糙和写得细模型的调用成功率能差出一倍。我拿一个查询天气的工具做过对比测试粗糙版本长这样{ name: get_weather, description: 获取天气, parameters: { type: object, properties: { city: {type: string}, date: {type: string} } } }精细版本{ name: get_weather, description: 查询指定城市在指定日期的天气预报。当用户询问天气、气温、是否下雨、要不要带伞等问题时调用此工具。, parameters: { type: object, properties: { city: { type: string, description: 城市名称使用中文全称例如北京市、上海市不要使用拼音或英文 }, date: { type: string, description: 查询日期格式为 YYYY-MM-DD。若用户说今天则填入当天日期明天填入次日日期 } }, required: [city, date] } }在同一个 3B 模型上跑 200 条测试用例粗糙版本的参数完整率大概 70%精细版本能到 92% 以上。差距主要来自三处description 里明确了触发场景、字段描述里给了格式示例、required 字段做了显式声明。这里有个经验description 要写什么时候用字段 description 要写填什么格式。很多人的 Schema 只写了工具是干嘛的没写什么时候该触发模型就容易在该调用的时候不调用或者在不该调用的时候乱调用。2.2 参数类型设计的几个反直觉结论关于参数类型有几个结论是我踩坑之后才想明白的跟直觉不太一样。第一能用 string 就别用复杂嵌套。端侧小模型处理嵌套对象的能力很弱一个三层嵌套的参数结构填错率极高。如果业务上允许把嵌套结构拍平成几个平级字段或者干脆让模型输出一个 JSON 字符串你在代码里再解析。我做过对比同样是表达一个地址信息嵌套对象版本的字段错误率是扁平版本的 2.5 倍。第二枚举值要显式列出别指望模型自己收敛。如果某个参数只有几个合法取值一定要用enum约束死。比如查询类型只有实时和预报两种就写成enum: [realtime, forecast]。不写的话模型可能给你输出实时天气当前now各种变体你的后端根本接不住。第三数字类型要慎用。小模型对数字的边界感很差你让它填一个 1 到 100 的整数它可能给你填 0 或者 999。如果数字范围重要用minimum和maximum约束并且在 description 里再强调一遍。第四可选参数越少越好。每多一个可选参数模型就多一个决策点出错概率就上升。能通过上下文推断的参数就别让模型填在你的代码里补全。2.3 用校验层兜住模型的手滑无论 Schema 写得多好模型总会有填错的时候。工程化的关键不是追求 100% 正确而是在模型出错时能优雅地兜住。我的做法是在工具调用和实际执行之间加一层校验流程是这样的模型输出工具调用请求函数名 参数 JSON。校验层用 JSON Schema 做结构校验检查必填字段、类型、枚举、范围。校验失败时不直接报错而是把具体的错误信息拼成一条消息回传给模型让它重新生成。重试最多两次两次都失败就降级到澄清追问让用户补充信息。这里有个细节很关键回传给模型的错误信息要具体。不要只说参数错误要说字段 date 的格式应为 YYYY-MM-DD你填的是明天请转换为具体日期。模型看到具体错误修正的成功率会高很多。我实测下来带具体错误信息的重试第二次成功率能到 80% 以上而笼统报错的重试成功率不到 40%。import jsonschema def validate_and_retry(tool_call, schema, model, max_retry2): for attempt in range(max_retry 1): try: jsonschema.validate(tool_call[arguments], schema) return tool_call except jsonschema.ValidationError as e: if attempt max_retry: return {action: clarify, reason: str(e)} error_msg f参数校验失败{e.message}出错字段路径{list(e.path)} tool_call model.regenerate(tool_call, error_msg) return tool_call这段代码看着简单但它是端侧 Agent 稳定性的基石。没有这层你的 Agent 就是个玻璃人一碰就碎。3. 工具规模膨胀后的路由与分组策略工具数量是端侧 Agent 绕不过去的坎。业务稍微复杂一点工具数量轻松上到二三十个。前面说过工具一多模型的选择准确率就崩。解决办法不是换模型而是在模型之前做一层路由把每次决策的工具候选集缩小到 5 个以内。3.1 工具分组按业务域切分候选集最直接的做法是按业务域把工具分组。比如一个智能助手工具可以分成日程管理消息通知信息查询设备控制几组。用户说帮我看看明天有没有空先判断意图属于日程管理只把这一组的工具喂给模型。意图判断这一步可以用一个更小的模型或者规则来做成本很低。我一般用关键词加轻量分类模型的方式准确率能到 85% 以上剩下的靠模型在组内选择时兜底。分组之后每次模型面对的工具从 30 个降到 5 到 8 个选择准确率立刻回到 90% 以上。分组还有个额外好处每组工具的 Schema 可以独立优化不用担心改了一个工具的描述影响到其他组的调用。这在多人协作的项目里特别重要。3.2 基于语义检索的动态工具召回分组是静态的遇到跨域或者模糊意图就不够用了。这时候可以上动态召回把所有工具的 description 做向量化用户输入来了之后用语义相似度检索出最相关的 Top-K 个工具只把这 K 个喂给模型。这个方案在端侧要注意两点。第一向量化模型要选小的端侧跑不动大 embedding 模型一般用几十 MB 的轻量模型就够。第二工具描述的质量直接决定召回质量所以前面强调的 description 写法在这里又一次体现价值——描述写得越贴近用户可能的表达召回越准。我实测过30 个工具的场景下语义召回 Top-5 的命中率能到 88% 左右配合分组使用效果更好先分组缩小范围再在组内做语义召回。3.3 工具描述里的负向提示技巧这是个很少人提但特别有用的技巧在工具描述里明确写出什么时候不要用这个工具。小模型很容易被语义相似的工具搞混比如发送消息和发送邮件两个工具用户说发个消息模型可能选错。在发送消息的描述里加一句仅用于发送即时通讯消息不用于发送邮件在发送邮件的描述里加仅用于发送电子邮件不用于即时通讯两个工具的混淆率能下降一大半。这个技巧的本质是给模型提供区分性信息而不是只告诉它这个工具是干嘛的。4. MCP 在端侧 Agent 里的定位与落地方式MCP 这个词最近热度极高各种工具、插件都在往 MCP 上靠。但热度归热度落到端侧 Agent 的工程实践里得先想清楚 MCP 到底解决什么问题再决定要不要用、怎么用。4.1 MCP 解决的真正问题工具接入的标准化在没有 MCP 之前每接一个外部能力你都要为它单独写一套工具定义、参数转换、调用逻辑。接十个能力就是十套代码维护成本极高。MCP 的价值在于把工具提供方和工具使用方解耦用一套标准协议描述工具、传递调用、返回结果。对端侧 Agent 来说MCP 的意义在于你可以把工具的实现放在一个独立的进程或者服务里Agent 只负责按协议调用不用关心工具内部怎么实现。这在端侧尤其有价值因为端侧资源紧张工具实现和 Agent 逻辑分离之后可以各自优化互不干扰。但要清醒一点MCP 是接入标准不是能力增强。它不会让你的模型变聪明也不会自动解决工具选择的问题。工具多了照样要做路由参数照样要校验。把 MCP 当成银弹的团队最后都会失望。4.2 端侧引入 MCP 的取舍进程隔离与资源开销端侧引入 MCP 最大的顾虑是资源开销。MCP 通常意味着多一个进程或者多一层通信端侧的内存和电量本来就紧张多一层就多一份消耗。我的建议是分场景取舍场景是否引入 MCP理由工具数量少5、实现简单不引入直接内置调用省去通信开销工具数量多、需要动态扩展引入标准化收益大于开销工具实现需要独立更新引入解耦后可以单独升级工具极端资源受限设备谨慎引入优先保证主链路流畅关键判断标准是工具会不会频繁变化、需不需要独立演进。如果工具是固定的几个内置调用最省事如果工具生态要持续扩展MCP 的标准化价值就体现出来了。4.3 把 MCP 工具映射成 Function Calling 的实操端侧模型大多只认 Function Calling 格式所以实际落地时需要把 MCP 的工具描述转换成模型能理解的 Function Calling Schema。这个转换层是工程化的关键。转换时要注意几点MCP 的工具描述可能很详细但端侧上下文有限要做信息压缩只保留模型决策必需的部分MCP 的参数 schema 可能很复杂要做扁平化处理把嵌套结构拍平MCP 返回的结果可能很长要做截断和摘要避免撑爆上下文。我一般会维护一个映射配置把 MCP 工具的原始描述和端侧精简版描述对应起来这样既能享受 MCP 的标准化又能适配端侧的约束。5. 上下文管理端侧 Agent 最容易被忽视的战场聊完工具得聊聊上下文。端侧 Agent 的上下文窗口小但需要装的东西一点不少系统提示、工具描述、对话历史、工具返回结果、当前用户输入。怎么在这些内容之间分配有限的 token 预算是工程化里最考验功力的地方。5.1 上下文预算的分配原则我的经验分配大概是这样的系统提示和工具描述占 30% 到 40%对话历史占 30%工具返回结果占 20%留 10% 给用户输入和模型输出。这个比例不是死的要根据实际场景调。工具描述是刚性的不能随便砍但可以通过前面说的分组和召回来控制。对话历史是弹性的可以压缩、可以摘要、可以只保留最近几轮。工具返回结果是变量长结果必须做截断或者摘要。有个反直觉的点对话历史不是保留越多越好。端侧模型在长上下文里的注意力容易涣散保留太多历史反而干扰当前决策。我实测下来保留最近 3 到 5 轮对话配合一个历史摘要效果比保留全部历史更好。5.2 工具返回结果的压缩与摘要工具返回结果经常是上下文爆炸的元凶。一个查询接口返回一大段 JSON几千 token 就没了。处理方式有两种结构化截断和模型摘要。结构化截断是优先方案如果工具返回的是列表只保留前 N 条如果是长文本只保留关键字段。这需要你在工具定义阶段就约定好返回格式让返回结果本身就是精简的。模型摘要是兜底方案返回结果太长时用一个小模型做摘要只把摘要塞进上下文。但摘要会丢信息所以要谨慎用最好在工具层面就把结果控制好。提示工具返回结果的处理逻辑最好写在工具实现里而不是在 Agent 主循环里。这样每个工具对自己的返回负责主循环保持干净。5.3 多轮对话里的状态维护多轮对话里Agent 需要记住一些状态比如用户之前提到的城市、时间、偏好。这些状态如果全靠对话历史承载会占用大量 token。更好的做法是把关键状态抽出来单独维护一个结构化的状态对象每轮只把状态对象塞进上下文而不是塞整段历史。比如用户第一轮说帮我查北京的天气第二轮说那明天呢如果只靠历史模型要自己推断明天是接着北京说的。如果维护了状态对象{city: 北京}第二轮直接把状态带上模型就不用推断准确率更高token 也更省。这个状态对象怎么更新可以用规则也可以用模型抽取。规则适合字段固定的场景模型抽取适合灵活场景。我一般两者结合固定字段用规则自由信息用模型抽取。6. 错误处理与降级让 Agent 摔倒了能爬起来前面反复提到校验和重试这里系统讲讲端侧 Agent 的错误处理和降级设计。端侧环境的不确定性比云端大得多网络可能断、工具可能超时、模型可能输出垃圾没有一套完整的错误处理机制Agent 就是个定时炸弹。6.1 工具调用失败的分类与应对工具调用失败分几类应对方式完全不同参数错误模型填错了参数。应对是回传具体错误让模型重试。工具超时工具执行太久。应对是设置超时阈值超时后告知模型工具暂时不可用让它决定是换工具还是告知用户。工具返回错误工具执行了但返回错误码。应对是把错误信息翻译成模型能理解的自然语言让它决定下一步。工具不存在模型调用了不存在的工具。应对是回传可用工具列表让模型重新选择。每一类都要有明确的处理路径不能笼统地报错重试。我见过太多项目所有错误都走同一个重试逻辑结果参数错误重试三次还是错白白浪费算力和时间。6.2 模型输出异常的兜底策略模型输出异常包括输出不是合法 JSON、输出了不存在的工具名、输出了空参数、输出了超长内容。这些都要在解析层拦住。我的做法是解析层做严格校验任何异常都不直接抛给用户而是转成一条系统消息回传给模型让它重新生成。同时设置一个全局的重试上限比如整个对话轮次里模型最多重试 3 次超过就降级到抱歉我没能理解你的需求能再说一遍吗这种澄清话术。这里有个经验重试次数不是越多越好。端侧模型一旦陷入错误循环重试再多次也是错。设置上限及时降级比无限重试体验好得多。6.3 降级路径的设计从完整能力到最小可用降级设计要提前规划不能等出问题了才想。我的做法是设计三级降级完整能力所有工具可用模型正常调用。受限能力部分工具不可用时只暴露可用工具模型在受限范围内工作。最小可用模型或工具完全不可用时退化成纯对话或者固定话术应答。每一级降级都要保证用户能感知到当前状态而不是莫名其妙地功能消失。比如工具不可用时明确告诉用户天气查询暂时不可用而不是让模型硬编一个答案。7. 端侧 Agent 工程化的几条实战心得写到这里把一些零散但重要的心得集中说一下都是踩坑换来的。第一先做减法再做加法。端侧 Agent 的能力边界不是靠堆工具堆出来的而是靠精准的工具设计。宁可工具少而精不要工具多而杂。我见过一个项目工具从 8 个精简到 4 个之后整体成功率反而上升了 20 个百分点。第二Schema 是产品文档不是技术细节。写 Schema 的时候要想着模型看到这个会怎么理解而不是我的代码需要什么格式。这两者的差距就是调用成功率的差距。第三把模型当成一个能力有限但很听话的实习生。你给它的指令越明确、越具体、越有边界它表现越好。模糊的指令、开放式的任务端侧模型处理起来都很吃力。第四可观测性从第一天就要做。记录每一次工具调用的输入输出、每一次校验的结果、每一次重试的原因。这些日志在排查问题时价值极高等出问题了再补日志就晚了。第五别迷信协议和框架。MCP 也好各种 Agent 框架也好都是工具不是答案。真正决定 Agent 好不好用的是你对业务场景的理解和对工程细节的把控。关于端侧 Agent 的工程化工具契约和上下文管理这两块是地基地基打不牢上面盖什么都是空中楼阁。下一篇会接着聊工程化的下半部分包括多 Agent 协作、端侧的可观测性建设、以及怎么在资源受限的情况下做性能优化。这些内容我在实际项目里都趟过一遍到时候把具体的参数和踩坑细节都摊开讲。