
1. 从能跑到能扛端侧 Agent 工程化到底在解决什么很多人第一次把 Agent 跑起来的时候兴奋点都在它居然能自己调工具了。写个 Function Calling 的 demo模型返回一个 JSON解析出来执行本地函数把结果塞回去再让模型总结——流程走通那一刻确实爽。但只要你把这个东西往真实场景里推一步问题就全冒出来了模型偶尔返回带 markdown 代码块的 JSON、参数类型对不上、工具调用失败后模型开始胡编、多轮对话上下文越滚越大、并发一上来整个链路直接卡死。这些问题的共同点是它们都不是模型能力问题而是工程问题。端侧 Agent 和云端 Agent 最大的区别在于端侧没有那么多资源可以挥霍没有稳定的网络兜底没有一整套服务端基础设施帮你擦屁股。你必须在客户端这一侧把可靠性、可观测性、资源控制这些事情自己扛起来。所谓Agent 工程化说白了就是把一个演示级的 Agent 变成一个生产级的 Agent。演示级关心的是能不能动生产级关心的是动了之后会不会崩、崩了能不能查、查到了能不能修。这一篇上主要聊的是工程化里最基础也最容易被忽视的部分协议层的设计也就是 Function Calling 的 schema 怎么定、MCP 这类协议怎么用、工具描述怎么写才能让模型少犯错。下一篇下会聊编排、并发、状态管理和可观测性。先给一个判断标准你可以拿它对照自己手上的项目如果你的 Agent 满足下面任意一条那它大概率还停留在演示级——工具调用的参数校验靠try/except一把梭报错了就重试或者直接放弃工具描述是随手写的一句话模型经常选错工具或者传错参数没有任何调用日志出问题只能靠print大法上下文无限增长从来没考虑过裁剪和压缩并发场景下多个 Agent 实例共享状态偶发数据错乱。这篇内容适合两类人一类是已经把 Agent demo 跑通、准备往产品里塞的开发者另一类是正在设计 Agent 框架、需要把协议层抽象做对的架构同学。如果你连 Function Calling 是什么都还没搞明白建议先去补一下基础这篇会偏工程细节。2. Function Calling 的 Schema 设计模型犯错的一大半原因在这里2.1 为什么 JSON Schema 不是随便写写Function Calling 的本质是让模型输出一段结构化数据这段数据的格式由你提供的 JSON Schema 约束。很多人以为 Schema 只是给解析器用的其实它首先是给模型看的。模型在生成参数时会参考 Schema 里的字段名、类型、描述、枚举值。Schema 写得含糊模型就只能猜猜错是必然的。我见过一个典型的反面案例一个查询天气的工具参数定义成{city: {type: string}}描述写的是城市。结果模型有时候传北京有时候传北京市有时候传Beijing甚至传中国北京朝阳区。下游的天气 API 只认标准城市名于是大量调用失败。问题不在模型在于 Schema 没有给出约束。正确的做法是把约束尽量前置到 Schema 里。城市名可以用 enum 限定或者至少给出明确的格式说明和示例。下面是一个对比写法Schema 片段模型表现含糊{city: {type: string}}传参五花八门下游频繁失败明确{city: {type: string, description: 城市名称使用标准中文简称如北京、上海不要带市或省份前缀, examples: [北京, 上海]}}传参稳定失败率大幅下降这个差异看起来很小但在实际项目里光是把工具描述和参数描述写清楚就能把工具调用的成功率从 70% 拉到 90% 以上。这是投入产出比最高的一件事没有之一。2.2 参数类型的选择能不用 object 就不用 object端侧 Agent 有一个很现实的约束模型越小对复杂嵌套结构的处理能力越弱。如果你定义一个深层嵌套的 object 参数小模型很容易生成结构不完整或者层级错乱的 JSON。我的经验是参数结构尽量扁平化。如果一个工具需要多个相关参数优先用平铺的字段而不是嵌套对象。如果确实需要传递复杂结构考虑用字符串承载 JSON然后在工具内部解析——这样模型只需要保证字符串是合法 JSON而不需要理解整个嵌套结构。举个例子一个创建日程的工具下面两种设计// 方案 A嵌套 object { type: object, properties: { event: { type: object, properties: { title: {type: string}, time: { type: object, properties: { start: {type: string}, end: {type: string} } } } } } } // 方案 B扁平化 { type: object, properties: { title: {type: string, description: 日程标题}, start_time: {type: string, description: 开始时间ISO 8601 格式如 2025-03-15T14:00:00}, end_time: {type: string, description: 结束时间ISO 8601 格式} }, required: [title, start_time] }方案 B 在小模型上的成功率明显更高。原因很简单模型生成扁平结构时每一步的决策空间更小出错概率更低。嵌套结构要求模型同时维护多个层级的括号和字段对注意力机制是个负担。2.3 required 字段的取舍别把可选参数写成必填一个常见的错误是把所有参数都标成 required。这会导致两个问题一是模型被迫为每个字段编造值二是当用户没提供某个信息时模型会去脑补而不是反问。正确的做法是严格区分必填和可选。必填字段是没有它这个工具就没法执行的可选字段是有更好没有也能跑的。对于可选字段在描述里说明默认行为比如不传则默认为当前时间。还有一个技巧对于枚举型参数如果某个值是最常用的可以在描述里标注默认值引导模型在不确定时选择它。这比让模型随机猜一个值要稳得多。2.4 工具描述的写法给模型写使用说明书工具描述description是模型选择工具的主要依据。很多人把它当成注释随便写结果模型在多个相似工具之间反复横跳。好的工具描述应该包含三部分这个工具做什么、什么时候用它、什么时候不要用它。第三点尤其重要因为模型最容易犯的错就是在不该用的时候用了某个工具。比如你有两个工具一个是搜索本地文档一个是搜索网络。如果描述只写搜索文档和搜索网络模型经常搞混。但如果写成搜索本地文档在用户本地的知识库中检索信息。当问题涉及用户私有资料、内部文档时使用。不要用于查询公开的实时信息。搜索网络检索互联网上的公开信息。当问题涉及新闻、实时数据、公开知识时使用。不要用于查询用户私有资料。这样模型的选择准确率会高很多。核心思路是用否定句划清边界。模型对不要做什么的敏感度往往比要做什么更高。3. MCP 协议端侧 Agent 工具生态的USB 接口3.1 MCP 解决的到底是什么问题在没有 MCP 之前每接一个工具你都要为它写一套适配代码定义 Schema、写调用逻辑、处理返回值、做错误映射。工具一多代码里全是重复的胶水层。更麻烦的是这些工具没法复用——A 项目写的文件读取工具B 项目要重新写一遍。MCPModel Context Protocol的思路是把工具抽象成一个标准化的服务。工具提供方按照 MCP 协议暴露自己的能力Agent 作为客户端去连接这些服务动态发现有哪些工具可用、每个工具的 Schema 是什么。这就像 USB 接口不管你是鼠标、键盘还是 U 盘插上去系统都能识别不需要为每个设备单独写驱动。对端侧 Agent 来说MCP 的价值在于解耦。Agent 核心逻辑不需要知道具体工具怎么实现只需要知道有一个 MCP 服务提供了这些能力。工具的增删改都在服务侧完成Agent 侧几乎不用动。3.2 MCP 的三种能力Tools、Resources、PromptsMCP 协议里定义了三种核心能力很多人只知道 Tools其实另外两种在端侧场景里也很有用。Tools是最常见的就是可调用的函数。Agent 发现工具列表模型决定调哪个客户端执行后把结果返回。这部分和 Function Calling 是一一对应的。Resources是只读的数据源。比如一个 MCP 服务可以把本地文件系统暴露成 ResourcesAgent 可以读取文件内容但不会修改。Resources 和 Tools 的区别在于Resources 是取数据Tools 是做动作。把只读操作和写操作分开有助于做权限控制——你可以放心地让 Agent 访问 Resources但对 Tools 的调用做更严格的审核。Prompts是预定义的提示模板。这个能力经常被忽视但在端侧很有用。比如你可以把总结这篇文档、翻译这段文字这类常用任务的提示词固化在 MCP 服务里Agent 直接调用模板不需要每次重新构造提示词。这既保证了提示词质量的一致性也减少了 Agent 侧的复杂度。3.3 端侧接入 MCP 的实操要点在端侧接入 MCP有几个坑我踩过这里直接说结论。第一连接方式的选择。MCP 支持 stdio 和 HTTP 两种传输方式。端侧应用如果和 MCP 服务在同一台机器上优先用 stdio因为不涉及网络延迟低、稳定性好。如果 MCP 服务在远端才用 HTTP。但要注意端侧环境网络可能不稳定HTTP 方式必须做好超时和重连。第二工具发现的时机。不要在每次对话时都去拉一遍工具列表那样开销太大。正确的做法是在 Agent 启动时拉一次缓存起来同时监听工具变更通知。MCP 协议支持服务端主动推送工具列表变化用好这个机制。第三Schema 的兼容性。不同 MCP 服务返回的 Schema 质量参差不齐有的描述写得很敷衍。Agent 侧最好做一层 Schema 校验和补全对于描述缺失的工具可以基于工具名做兜底描述。这一步能显著降低模型选错工具的概率。第四错误处理。MCP 服务可能因为各种原因失败服务没启动、调用超时、返回格式错误。Agent 侧要把这些错误统一映射成模型能理解的反馈而不是直接抛异常。比如服务不可用时返回给模型的信息应该是该工具当前不可用请尝试其他方式而不是一段堆栈。3.4 MCP 与 Function Calling 的关系经常有人问有了 MCP 还需要 Function Calling 吗答案是需要的两者是不同层次的东西。Function Calling 是模型侧的能力是模型输出结构化调用的机制。MCP 是工具侧的协议是工具如何被描述和调用的标准。Agent 的工作流是通过 MCP 发现工具把工具 Schema 转换成模型能理解的 Function Calling 格式模型生成调用请求Agent 再通过 MCP 去执行。所以 MCP 不是替代 Function Calling而是让 Function Calling 的工具体系变得可插拔、可复用。理解这一点你在设计 Agent 架构时就不会把两者混为一谈。4. 工具调用的可靠性工程从能调到调得稳4.1 参数校验永远不要相信模型输出的 JSON这是端侧 Agent 工程化的第一条铁律模型输出的 JSON 永远要当作不可信输入来处理。哪怕你用了最严格的 Schema模型仍然可能返回格式错误、类型不符、字段缺失的数据。校验要分三层做。第一层是语法校验确认输出是合法 JSON。模型经常在 JSON 外面包一层 markdown 代码块或者加一些解释性文字这些都要在解析前清理掉。第二层是结构校验对照 Schema 检查字段是否存在、类型是否正确。第三层是语义校验检查值的范围是否合理比如时间格式是否合法、枚举值是否在允许范围内。任何一层校验失败都不要直接抛错给用户而是把错误信息反馈给模型让它重新生成。这里有个技巧反馈错误时要具体告诉模型字段 start_time 的格式不对应该是 ISO 8601 格式而不是笼统地说参数错误。具体的反馈能让模型在下一轮修正笼统的反馈只会让它继续犯错。4.2 重试策略不是所有失败都值得重试重试是提高可靠性的常用手段但滥用重试会让问题更糟。要区分几类失败失败类型例子是否重试策略格式错误JSON 解析失败是把错误反馈给模型让它重新生成最多 2 次参数错误类型不符、枚举越界是同上附带具体错误说明工具执行失败网络超时、服务不可用视情况瞬时错误可重试持续错误直接反馈业务逻辑错误权限不足、资源不存在否直接反馈给模型让它换方案关键点是重试要有上限且每次重试要带上上次失败的原因。无脑重试只会浪费 token 和时间还可能陷入死循环。4.3 工具调用的幂等性设计端侧 Agent 经常需要重试这就要求工具调用是幂等的。如果一个创建订单的工具被调用了两次就会产生两个订单这是灾难性的。设计工具时对于写操作尽量引入幂等键。比如创建订单时让模型生成一个唯一的 request_id服务端根据这个 id 去重。这样即使重试也不会产生重复数据。对于无法做成幂等的操作要在工具描述里明确标注此操作不可重复执行并在 Agent 侧做特殊处理——比如调用前先确认状态调用后记录结果避免重复触发。4.4 超时与降级端侧环境资源有限工具调用必须有超时控制。一个卡住的工具调用会拖垮整个 Agent 流程。我的做法是给每个工具设置独立的超时时间超时后立即中断把超时作为结果反馈给模型。降级策略也很重要。当某个工具持续不可用时Agent 应该能切换到备选方案。比如主搜索工具挂了自动切到备用搜索工具。这需要在工具注册时标注能力标签让 Agent 知道哪些工具是可以互相替代的。5. 上下文管理端侧 Agent 最容易被忽视的资源瓶颈5.1 上下文为什么会失控Agent 的每一轮工具调用都会往上下文里塞东西模型的思考、工具调用的参数、工具的返回结果。一个稍微复杂点的任务几轮下来上下文就能涨到几千甚至上万 token。端侧模型的上下文窗口通常比云端小很快就会撑爆。更麻烦的是上下文里塞了大量无关信息后模型的注意力会被稀释表现反而下降。这就是所谓的上下文污染——信息越多模型越糊涂。5.2 上下文裁剪的几种策略裁剪上下文有几种常见策略各有适用场景。滑动窗口是最简单的只保留最近 N 轮对话。优点是实现简单缺点是可能丢掉早期的关键信息。适合任务步骤之间相对独立的场景。摘要压缩是把早期的对话用模型总结成一段简短摘要替换掉原始内容。这样既保留了关键信息又大幅压缩了体积。缺点是摘要本身要消耗一次模型调用且可能丢失细节。适合长任务场景。关键信息提取是从上下文中抽取结构化信息比如已确认的参数、已完成的任务只保留这些丢弃过程性内容。这种方式压缩率最高但实现复杂度也最高需要针对具体任务设计提取逻辑。实际项目里我通常把这几种组合使用近期对话用滑动窗口保留原文中期对话做摘要压缩远期只保留关键信息提取的结果。5.3 工具结果的精简工具返回的结果往往是上下文膨胀的重灾区。一个搜索工具可能返回十几条结果每条都带一堆元数据全塞进上下文纯属浪费。正确的做法是在工具侧就做精简只返回模型真正需要的信息。比如搜索工具只返回标题、摘要和链接不返回完整的 HTML 和一堆无关字段。如果模型需要更详细的内容再单独调用一个获取详情的工具。这个思路叫渐进式披露先给模型一个概览它需要细节时再按需获取。这样既控制了上下文体积又保证了信息的完整性。5.4 上下文的结构化组织上下文里的信息最好有清晰的结构而不是一堆散乱的文本。比如把系统提示、工具定义、对话历史、工具结果分区存放用明确的分隔符隔开。这样模型更容易定位信息也更不容易混淆。一个实用的技巧是给工具结果加上明确的标记比如用 XML 标签包裹tool_result namesearch statussuccess ...结果内容... /tool_result这样模型能清楚地知道这段内容是工具返回的而不是用户说的或者自己想的。在长上下文里这种标记能显著降低模型的混淆概率。6. 几个真实项目里踩过的坑6.1 工具名冲突导致的诡异行为有一次项目里同时接了两个 MCP 服务各自都有一个叫search的工具。Agent 在发现工具时没有做去重结果模型调用search时实际执行的是后注册的那个导致行为完全不符合预期。排查了半天才发现是工具名冲突。教训是工具名必须全局唯一。接入多个 MCP 服务时要么在注册时加命名空间前缀比如service_a.search要么在发现阶段就检测冲突并报错。千万别指望模型能区分两个同名工具。6.2 枚举值大小写不一致一个工具的参数用了枚举定义的是[pending, completed, failed]。结果模型有时候返回Pending有时候返回PENDING。校验直接失败任务中断。这个问题在大小写敏感的语言里特别常见。解决办法是在校验时做归一化把值统一转成小写再比对。或者在 Schema 描述里明确写使用小写但实测下来归一化比依赖模型遵守约定更可靠。6.3 工具返回超大结果撑爆上下文一个读取文件的工具用户让它读一个几 MB 的日志文件工具老老实实把整个文件内容返回了上下文瞬间爆掉后续所有调用都失败。这个坑的解法是在工具侧做大小限制。超过一定长度的结果要么截断并提示内容过长已截断要么返回一个引用比如文件路径和行号范围让模型按需分段读取。永远不要假设工具返回的结果是可控的。6.4 模型在工具失败后开始编造工具调用失败后如果反馈信息不清晰模型经常会开始编造结果。比如搜索工具超时了模型会假装搜索成功然后基于训练数据编一个答案出来。这在端侧场景里特别危险因为用户可能真的相信了。解决办法是在工具失败时给模型一个非常明确的信号比如工具执行失败你没有获取到任何真实数据请如实告知用户。同时在系统提示里强调不要编造工具结果。双管齐下能大幅降低编造行为。7. 工程化清单上线前你应该检查这些把上面这些内容整理成一份可执行的检查清单你在 Agent 上线前可以逐条对照。Schema 层所有工具的参数描述是否清晰、是否包含示例、是否用否定句划清了边界参数结构是否尽量扁平required 字段是否合理枚举值是否明确。协议层MCP 连接方式是否选对工具发现是否做了缓存和变更监听多服务接入时工具名是否做了命名空间隔离Schema 兼容性是否有兜底处理。可靠性层参数校验是否分了三层重试是否有上限且带失败原因写操作是否做了幂等工具调用是否有超时控制是否有降级方案。上下文层是否有裁剪策略工具结果是否做了精简上下文是否有清晰的结构标记是否有防止上下文无限增长的机制。可观测层工具调用是否有日志失败是否有分类统计是否能追踪一次完整任务的调用链路。这份清单不是一次性的而是应该随着项目迭代不断补充。每踩一个新坑就往清单里加一条。久而久之你的 Agent 工程化能力就沉淀下来了。端侧 Agent 的工程化本质上是在资源受限的环境里做取舍。你不能像云端那样堆资源所以必须在协议设计、可靠性、上下文管理这些基础层面做扎实。这些工作不像调模型参数那样有即时反馈但它们决定了你的 Agent 能不能真正扛住生产环境的考验。下一篇会聊编排和并发那是另一个维度的挑战。