资讯动态

MCP工具定义实战:JSON Schema在具身智能Agent中的应用

发布时间:2026/9/13 2:40:33 来源:尧图企业网站定制
做具身智能方向的工程落地这两年绕不开一个话题MCPModel Context Protocol的工具定义规范。尤其当你需要让大模型去调用机械臂、传感器、仿真环境这些真实世界的工具时工具的JSON定义写得好不好直接决定Agent能不能用起来以及用起来之后稳不稳。标题里写成“Jason格式”其实是个很经典的笔误JSON的全称是JavaScript Object Notation跟人名Jason没有任何关系但搜索引擎里这么搜的人特别多可见这个格式的命名确实容易让人记混。MCP的工具定义标准简单说就是你给大模型一份“工具说明书”说明书里写清楚这个工具叫什么、有什么用、参数怎么填。这份说明书用JSON格式描述而其中最核心的部分是JSON Schema——一种用来描述JSON数据结构的规范。这篇文章我不打算泛泛讲概念而是直接拆解一份真实可用的工具定义长什么样每个字段为什么存在、怎么填才能让大模型少犯傻以及我在具身智能项目里实际踩过的坑。内容适合三类人正在做智能体Agent应用开发的工程师、准备接MCP协议的工具开发者以及刚接触具身智能、想搞清楚大模型怎么控制真实硬件的新手。1. 为什么MCP的工具定义非得用JSON1.1 MCP里“工具”到底是干什么的MCP是Anthropic在2024年底提出并开源的模型上下文协议目的很直接让大语言模型能标准化地连接外部数据源和工具。你可以把MCP理解成大模型世界的“USB接口”——鼠标键盘显示器各有各的协议但插上USB就能统一工作。在MCP体系里工具Tool是模型可以主动调用的函数比如查询数据库、操作文件系统、控制机械臂、读传感器数据。关键点在于大模型本身不会“知道”你的工具怎么用。它只是读取你提供的工具定义文本然后根据这些文本决定“要不要调用这个工具”以及“参数应该传什么”。所以工具定义的质量直接决定了模型调用的准确率。写工具定义这件事本质上是在做大模型的“外部提示词工程”——你写在JSON里的每一个描述字段都会被模型当成决策依据。1.2 为什么选JSON而不是别的格式市面上的结构化描述格式不少XML、YAML、Protobuf但MCP最终选了JSON或者说更准确地选了JSON Schema原因是多方面的。JSON的跨语言支持几乎是零成本的。Python、TypeScript、Java、C、Rust几乎每种语言都有内置或成熟的JSON解析库不需要额外生成代码、不需要编译器介入。对于MCP这种要接成千上万种不同技术栈工具的场景来说这是最现实的兼容性方案。JSON Schema本身是一种“自描述”的规范。你写出来的工具参数约束既可以被人类阅读也可以被程序校验更重要的是可以被大模型理解。模型训练语料里JSON Schema的样本实在太多了模型对它的语义理解天然优于XML或自定义格式。我自己测试过同一个工具定义分别用JSON Schema和自然语言描述模型对前者的参数生成准确率明显更高。从工程角度看JSON还有几个隐性优势无状态、可序列化、容易持久化和传输。MCP的传输层基于JSON-RPC工具定义本身就是JSON-RPC消息体的一部分直接用JSON描述工具可以减少一次格式转换减少出错面。1.3 工具定义在MCP协议中的位置搞清楚工具定义在MCP协议里的位置对理解它的结构有帮助。MCP的通信模型是客户端Client与服务器Server之间的JSON-RPC消息交互。当客户端启动并初始化会话后会发送一个tools/list请求服务器返回一个工具列表这个列表里的每一项就是一个工具定义对象。当模型决定调用某个工具时客户端再发送tools/call请求服务器执行具体逻辑并把结果返回。所以工具定义是“模型的行动指南”。服务器的代码逻辑写得再漂亮如果工具定义没写好模型就不知道什么时候调用、怎么传参数再好的功能也白搭。这也是我强调“工具定义先于代码实现”的原因——在写任何业务逻辑之前先把工具定义打磨好后面几乎不会返工。2. JSON Schema核心字段逐个拆解2.1 工具定义的整体结构一个标准的MCP工具定义长这样{ name: move_arm_to_pose, description: 控制机械臂末端运动到指定笛卡尔坐标位置常用于抓取、放置、轨迹规划等操作。, inputSchema: { type: object, properties: { x: { type: number, description: 目标X坐标单位毫米相对于机械臂基座坐标系。 }, y: { type: number, description: 目标Y坐标单位毫米相对于机械臂基座坐标系。 }, z: { type: number, description: 目标Z坐标单位毫米相对于机械臂基座坐标系。 }, speed: { type: number, description: 运动速度比例范围0.1到1.01.0为最大速度。, default: 0.5, minimum: 0.1, maximum: 1.0 } }, required: [x, y, z] } }这个结构分三层最外层是工具本身的信息name和description第二层是inputSchema输入参数约束第三层是JSON Schema内部的具体字段定义。后续新版本MCP还扩展了outputSchema、annotations等字段但name、description、inputSchema这三个是绝对核心。2.2 name和description给模型看的“第一印象”name是工具的唯一标识必须全局唯一。命名规范建议全小写加下划线比如move_arm_to_pose、read_sensor_data。我见过有人用驼峰命名moveArmToPose大模型通常也能识别但小写下划线是工具调用领域最通用的惯例保持一致最稳妥。description的作用可能比你想象中重要得多。它不只是给人看的注释更是大模型决定“何时使用这个工具”的关键依据。模型在接到用户问题后会遍历所有工具的描述根据语义相关度决定调用哪个。描述写得越具体、越明确模型的决策越准确。如果只是写“移动机械臂”模型可能搞不清楚这个工具和控制单个关节的工具有什么区别但如果写好“用于将机械臂末端移动到指定笛卡尔坐标位置常用于抓取、放置、轨迹规划”模型就能根据任务上下文做出合理判断。2.3 inputSchema里的properties参数的定义方式inputSchema的type固定为object表示这个工具接收一个JSON对象作为参数。对象的每一个字段在properties中定义每个字段又包含自己的type、description以及可选的约束条件。常用的type类型有这几种string字符串适合名称、ID、路径等文本信息number数字整数和小数都可以integer整数适合计数、索引boolean布尔值适合开关类参数array数组适合批量数据object嵌套对象适合结构化的复合参数每个字段的description同样重要。大模型通过description来理解这个参数的含义、单位、取值范围。我在实际项目中遇到过很多次不给单位模型就把毫米当成米不给坐标系模型就按自己的理解传值。这些坑几乎都可以通过写清楚description来规避。2.4 required、default、enum与约束条件required是一个数组列出调用工具时必须提供的字段。不需要把所有参数都设为必填能给出合理默认值的就让模型少传一个参数降低出错概率。default字段为参数提供默认值。当模型没传这个参数时服务器端可以自动使用默认值。比如速度、超时时间这类参数给一个安全默认值非常实用。enum用来限制参数的取值集合。比如控制模式只有position、velocity、force三种写成enum后模型就只能从这三个值里选从根源上杜绝乱传值。mode: { type: string, description: 控制模式位置控制、速度控制、力控。, enum: [position, velocity, force] }minimum和maximum用于数值范围约束minLength和maxLength用于字符串长度约束pattern用于正则匹配。这些约束不光是给人看的更是可以直接在服务器端做输入校验的依据。2.5 嵌套对象与数组处理复合参数有些工具的参数天然是复合结构。比如要控制机械臂走一段轨迹你需要的不是单个坐标而是一组坐标点加时间戳。这时候就需要嵌套结构{ name: plan_trajectory, description: 规划并执行机械臂末端轨迹。, inputSchema: { type: object, properties: { waypoints: { type: array, description: 轨迹途经点列表至少包含起点和终点。, items: { type: object, properties: { x: { type: number, description: X坐标毫米 }, y: { type: number, description: Y坐标毫米 }, z: { type: number, description: Z坐标毫米 } }, required: [x, y, z] }, minItems: 2 } }, required: [waypoints] } }这里array类型的字段用items来定义数组元素的schemaminItems控制最少元素数量。嵌套层级理论上不限但建议最多三层——层级太深会显著增加模型生成合法参数的难度也增加JSON Schema校验的复杂度。2.6 条件约束oneOf、anyOf、allOf这三个是JSON Schema中处理“条件分支”的关键字但在MCP工具定义中使用频率相对较低。什么场景会用到呢比如一个工具既可以按坐标控制也可以按关节角控制两种模式参数结构差异很大inputSchema: { type: object, oneOf: [ { properties: { mode: { const: cartesian }, x: { type: number }, y: { type: number }, z: { type: number } }, required: [mode, x, y, z] }, { properties: { mode: { const: joint }, joints: { type: array, items: { type: number } } }, required: [mode, joints] } ] }oneOf表示只能命中其中一个分支anyOf表示至少命中一个allOf表示同时满足所有。老实说这类复杂约束在实际MCP工具定义里用得不多——不是不好而是很多开源模型对复杂JSON Schema的遵循能力有限出错的概率会变大。我的建议是能用简单结构解决的不要上条件约束确实有复杂场景优先拆成多个独立工具而不是塞进一个工具里。3. 完整工具定义示例从通用场景到具身智能3.1 一个通用示例模拟查询设备状态先看一个贴近日常开发的示例。假设我们要做一个智能运维Agent需要查询机房设备的温度、风扇转速等状态{ name: get_device_status, description: 查询指定设备的实时运行状态包括CPU温度、风扇转速、电源功率等。当用户询问设备是否过热、风扇是否异常、功耗情况时使用。, inputSchema: { type: object, properties: { device_id: { type: string, description: 设备唯一标识ID格式如rack-01-node-03。 }, metrics: { type: array, description: 要查询的指标列表可选值cpu_temp、fan_speed、power、memory_usage。不传则返回所有指标。, items: { type: string, enum: [cpu_temp, fan_speed, power, memory_usage] } } }, required: [device_id] } }这个定义有两个值得学习的点一是description里明确写了“当用户询问……时使用”这是在引导大模型的调用时机二是metrics参数利用enumarray的组合既给了模型灵活性又限制了取值边界。这种“自由但有界”的设计思路比单纯的必填约束效果更好。3.2 具身智能核心示例一机械臂力控抓取接下来是具身智能项目里最常见的一类工具控制机械臂执行力控抓取。这个场景既要控制位置又要限制力度参数设计比纯位置控制复杂很多。{ name: grasp_object_force_control, description: 控制机械臂以指定的夹持力抓取目标物体。适用于抓取易碎品、形状不规则物体或需要恒力夹持的场景。抓取前通常需要先调用move_arm_to_pose将机械臂移动到物体附近。, inputSchema: { type: object, properties: { target_position: { type: object, description: 目标抓取位置的笛卡尔坐标单位毫米。, properties: { x: { type: number, description: X坐标相对机械臂基座。 }, y: { type: number, description: Y坐标相对机械臂基座。 }, z: { type: number, description: Z坐标相对机械臂基座。 } }, required: [x, y, z] }, force: { type: number, description: 目标夹持力单位牛顿范围5到30。默认15。, minimum: 5, maximum: 30, default: 15 }, grasp_strategy: { type: string, description: 抓取策略平行夹爪、吸附、自适应抓取。, enum: [parallel, suction, adaptive], default: parallel }, timeout: { type: number, description: 抓取超时时间单位秒。, default: 10 } }, required: [target_position] } }这个示例里我特意用了嵌套对象target_position来聚合三个坐标值。相比把x、y、z平铺在顶层这种方式在语义上更内聚模型一眼就能看出这三个值是一组的。同时force和grasp_strategy都给了默认值模型只需关心最关键的位置参数调用成功率明显提升。3.3 具身智能核心示例二读取传感器数据具身智能系统普遍依赖传感器反馈例如六维力/力矩传感器、激光雷达、相机深度数据。MCP工具定义同样适用于这些数据获取场景{ name: read_ft_sensor, description: 读取机械臂末端六维力/力矩传感器的当前数值包括三轴力和三轴力矩。当需要判断机械臂是否接触物体、检测碰撞或评估夹持力时使用。, inputSchema: { type: object, properties: { filter: { type: string, description: 滤波方式raw原始数据、kalman卡尔曼滤波、moving_average滑动平均。, enum: [raw, kalman, moving_average], default: kalman }, axes: { type: array, description: 需要读取的通道列表可选Fx、Fy、Fz、Mx、My、Mz不传则返回全部。, items: { type: string, enum: [Fx, Fy, Fz, Mx, My, Mz] } } } } }这类工具定义的重点是告诉模型“在什么判断场景下使用”。很多具身智能Agent在任务执行中需要根据传感器反馈做决策如果模型不知道有这个工具它就只会按开环逻辑运行碰到意外碰撞就抓瞎。描述写得越场景化模型越知道何时该查传感器。4. 工具定义实操要点与策略4.1 接口描述的“提示词工程”属性我在多个项目里反复验证过一件事工具定义里的description字段本质上是给大模型看的提示词。模型通过description来理解工具语义、调用时机、参数含义。所以写description的时候不要只写技术参数还要写使用场景。一句话标准描述里应该包含“这个工具是干什么的、什么时候该用它、什么时候不该用它、参数的语义/单位/范围”。能做到这四点大模型的工具调用准确率会有立竿见影的提升。我见过不少团队花大量时间调系统提示词却没意识到工具定义里那几十行description才是真正决定工具调用质量的核心。4.2 参数数量与必填项的取舍策略工具参数的个数对模型调用准确率影响很大。经验法则参数尽量控制在5个以内超过5个时考虑拆分工具或合并参数为嵌套对象。原因很简单——模型生成参数时每多一个字段就多一次出错机会参数越多联合出错的概率越大。必填项同样要克制。有些开发者习惯把所有参数都设为必填理由是“调用者就应该把所有信息都给全”。但在大模型场景里这种做法反而增加失败率。模型拿不到某个参数值时可能会编造一个而不是主动向你确认。给关键参数设置安全的默认值把必填项压到最少这才是符合大模型行为模式的策略。我通常在项目里遵循一个原则只把“没有值就没法执行”的参数设为必填其余全部设置默认值或标记为可选。4.3 工具粒度的划分一个工具只做一件事工具粒度设计是另一个高频踩坑点。一种常见错误是把所有操作都塞进一个工具里用一个大参数对象控制表面上看简洁实际运行却麻烦不断。反例{ name: robot_control, description: 控制机械臂。, inputSchema: { type: object, properties: { action: { type: string, enum: [move, grasp, release, stop] }, x: { type: number, description: 目标X坐标 }, y: { type: number, description: 目标Y坐标 }, z: { type: number, description: 目标Z坐标 }, force: { type: number }, speed: { type: number } }, required: [action] } }这种设计的问题在于action不同时需要的参数完全不同但模型面对的是全部参数的组合空间。move动作不需要force可模型可能就会莫名其妙地传一个force进来造成混乱。正确做法是拆成多个工具move_arm_to_pose、grasp_object_force_control、release_object、stop_arm。每个工具的参数只覆盖自己的场景模型决策时目标更清晰参数空间更小准确率自然更高。一个工具只做一件事这是工具定义的黄金法则。4.4 参数命名、顺序与版本的隐性影响参数命名对模型的理解也有影响。虽然理论上JSON对象的键是无序的但在模型眼里命名本身携带着语义信息。x、y、z这种简写没问题因为坐标系在具身智能里是常识但业务含义较强的参数命名尽量用完整的单词组合比如grasp_strategy、target_position避免用gs、tp这类只有开发者才懂的缩写。关于参数顺序JSON格式本身不要求字段有序但实际测试中把最重要的参数放在properties定义的前面模型倾向于优先关注这些参数减少遗漏。这可能是训练数据分布带来的行为特征不一定每个模型都一致但没有坏处。版本管理方面工具定义一旦发布给外部客户端使用修改时要考虑兼容性。新增可选字段是安全的删除或重命名字段会破坏已有调用调整required要格外谨慎。我在项目里会为工具接口打版本号较大的不兼容变更直接定义一个V2版本而不是改原来的定义。5. 具身智能场景下的特殊考量与扩展5.1 物理世界约束必须写进定义具身智能与纯软件工具的最大区别在于它操作的是真实物理设备存在机械极限、安全风险、实时性要求。这些约束必须显式写在工具定义里否则模型是“看不见”这些限制的。例如机械臂的关节角限制、最大速度、最大力矩都要在description或约束字段里明确。不要指望模型“凭常识”知道某台机械臂的Z轴行程范围——不同型号的机械臂参数差异极大模型没有上帝视角。更重要的是安全参数。力控抓取时最大夹持力、运动时允许的最大速度这些涉及人身安全和设备安全的参数不仅要在JSON里约束服务器端还要做硬性校验。JSON Schema是做第一层防御的真正的安全底线必须在代码层兜住。5.2 工具之间的协作与编排具身智能任务通常需要多个工具协同。一个典型的手眼协调场景先用相机获取物体位置get_object_pose再规划路径plan_trajectory然后移动机械臂move_arm_to_pose最后执行抓取grasp_object_force_control。MCP协议本身不支持在一个工具定义里直接调用另一个工具工具间的协作由大模型在推理过程中自主编排。但工具定义的description可以暗示这种上下游关系比如在grasp_object_force_control的description里写上“抓取前通常需要先调用move_arm_to_pose将机械臂移动到物体附近”。这相当于给模型提供了任务规划的路线图能显著提升多工具协作的流畅度。5.3 MCP工具与Agent Skill的区别这个话题最近讨论很多。我的理解是MCP工具更接近“原子能力”的标准化接口——输入输出明确、可校验、可以被多种上层逻辑复用Agent Skill则更接近“高层策略或流程模板”类似一个封装好的动作序列或者决策逻辑。举具身智能的例子MCP工具是move_arm_to_pose、grasp_object_force_control、read_ft_sensorAgent Skill则是更上层的“拿桌上的杯子”这种完整行为模板内部会调用多个MCP工具并且包含条件判断和失败恢复逻辑。两者的关系不是替代而是互补Skill负责“怎么决策”MCP负责“怎么执行”。在实际工程里我倾向于把底层的硬件控制全部封装成MCP工具上层再通过Skill或Agent框架做编排。5.4 大模型对工具定义真的是按JSON解析的吗这个问题要区分“训练时”和“推理时”。推理时MCP客户端把工具定义以JSON文本的形式放入上下文窗口大模型通过注意力机制理解这段文本再通过类似函数调用的机制输出JSON格式的参数。所以模型的解析本质是语义理解不是严格的程序化解析。这就解释了两个现象一是为什么description写得越好调用越准——因为模型的“理解”主要来自语义二是为什么某些模型对复杂的JSON Schema比如oneOf、嵌套多层的if-then遵循能力较差——它不一定“理解”这些约束的精确逻辑含义。实操中我的建议是为保证兼容性工具定义尽量用JSON Schema的基础能力type、properties、required、enum、minimum/maximum、default高级约束只在服务器端代码里做校验不要过度依赖模型来遵守。6. 常见问题与排查技巧实录6.1 高频问题速查表下面整理了我实际项目中遇到的典型问题直接对照排查症状可能原因解决方案模型调用工具但参数校验失败参数类型不匹配比如数字传成了字符串在description里明确类型和格式服务器端做宽松类型转换模型传了不存在的参数工具定义缺少additionalProperties: false在inputSchema顶层加上additionalProperties: false模型不知道什么时候该调用工具description缺少使用场景提示重写description加入“当……时使用”的引导语句单位或坐标系错误description里没写单位/坐标系在参数description中补全单位、坐标系、取值范围模型选了错误的工具工具定义太相似或命名模糊补充差异化描述必要时调整工具名称嵌套参数模型总是生成不完整嵌套层级过深简化结构减少嵌套深度或将复杂结构拆成单独工具同一工具被反复调用导致资源浪费缺少状态检查工具增加状态查询工具并在主工具description里提示先查询状态模型输出超长或格式错误返回结果过大在工具内部做结果截断或摘要控制返回内容大小6.2 一个真实排查案例力控参数被模型传成字符串有一次我在跑抓取场景时发现模型调用了grasp_object_force_control但force参数被传成了force: 15N。JSON Schema里明明定义的是type: number照理说校验应该直接拦下来但实际是因为客户端框架的校验不够严格字符串值被直接透传到了服务器端。排查过程先在服务器日志里看到force是字符串类型Python代码里做力控计算时直接抛异常。定位到问题后我没有只改服务器代码而是做了三件事第一在参数description里明确加上“仅传数字不要带单位单位固定为牛顿”第二在服务器端入口加了严格的类型校验和转换逻辑第三在工具定义里加了examples: [15]作为参考值虽然有部分模型对examples字段的利用不稳定但对Claude和部分国产模型有效。改完之后同一场景连续测试50次没有再出现字符串力控值。6.3 排查工具定义问题的调试方法当你发现Agent的工具调用不符合预期时按这个顺序排查效率最高第一步用单向测。直接绕过Agent用MCP客户端或测试脚本调用工具传入预期参数确认工具本身的逻辑是否正确。很多“工具调用失败”其实是工具内部代码报错跟工具定义无关先把硬件和逻辑层问题排除。第二步审查JSON Schema。用JSON Schema官方校验工具比如Python的jsonschema库验证定义是否合法。我遇到过有人把items写在了array字段的同级而不是items里导致定义本身就是非法的。第三步看模型实际“看到”的文本。很多MCP框架会把工具定义做转换或截断模型实际看到的描述可能跟你写的不一样。把发给模型的完整消息打印出来逐字检查工具定义的描述是否完整、有没有被截断、格式是否正确。第四步小步修改单变量验证。不要一次改多个字段否则你无法确定是哪个改动生效了。每次只调整一个描述或约束跑一批测试样本对比准确率变化。我习惯准备一组固定的测试问题集用来做工具定义修改前后的回归对比。6.4 两个容易忽略的“隐藏炸弹”第一个是工具定义的总长度。MCP会把所有工具定义都塞进上下文如果你的工具列表很长、每个描述又写得特别啰嗦会占用大量上下文窗口既挤占了对话历史的空间又可能让模型在长上下文中忽略某些工具。建议每个工具的description控制在100到150个汉字以内参数description控制在30到50个汉字以内总工具数量尽量控制在20个以内超过的话考虑按业务域拆分多个MCP Server。第二个是数字精度问题。JSON的number类型在多数语言里对应双精度浮点数但具身智能里有些参数需要高精度比如毫米级坐标。如果参数值超过53位二进制精度可能会导致精度丢失。实际项目中这个问题很少见但如果你在传递用于闭环控制的精密参数时发现值被微妙地改变了可以留意一下这个方向。7. 结尾一点个人体会做了小半年具身智能Agent的工程落地我对MCP工具定义最大的感受是它看起来像一份简单的接口文档实际上是大模型与物理世界之间最薄也最关键的一层翻译层。写工具定义的过程本质上是在用一种“模型能理解的方式”重新描述真实世界的规则。你写得越清晰、越结构化模型的行为就越可靠。反过来说如果你只把它当成普通的JSON配置随便写写后面一定会在各种意想不到的地方被坑到。最后再分享一个小技巧每次写完一个新的工具定义我都习惯找一个大模型用“你会怎么调用这个工具”来问一遍看看它的理解是否与我的意图一致。这种“面试”式的测试成本极低却能提前发现绝大多数描述不清的问题。MCP生态还在快速演进工具定义的标准也在不断完善但底层的原则不会变——让机器理解你的意图先从写一份清楚直白的JSON说明书开始。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价