news 2026/10/1 13:56:11

手写Tool Schema:从零打造高质量的LLM函数调用操作手册

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
手写Tool Schema:从零打造高质量的LLM函数调用操作手册

1. 为什么要手动创建tool schema

1.1 先从一个真实翻车场景说起

上个月我在做一个客服工单自动分类的Agent项目,工具函数很简单,就是一个create_ticket,接收部门、紧急程度、问题描述几个参数。最初我图省事,直接用TypeScript的函数签名自动生成了一份tool schema,结果联调的时候问题一堆。

最典型的场景是:用户在对话里说“我的网络老掉线,很急”,Agent居然把department填成了“很急”,把description填成了“网络老掉线”,然后把紧急程度留空。乍一看好像也在“工作”,但工单进系统之后全是错的。后来我手动重写了这份schema,给每个字段加了边界、枚举约束、示例值,还专门告诉模型“什么情况下不要调用这个工具”。同一套对话,识别准确率直接从惨不忍睹提升到了能用的水平。

说真的,tool schema在外行眼里就是一堆JSON字段说明,但在实际工程里它就是Agent的“操作手册”。模型不认识你的函数,它只认识你给它的schema。schema写得糙,Agent的行为就糙,这不是模型能力问题,是你没告诉它规则。

1.2 “自动生成”天然缺少三层信息

很多框架和语言都提供了“从函数签名自动生成tool schema”的能力,比如Python的Pydantic、TypeScript的Zod,都能从类型定义推导出JSON Schema。这种方案做原型很快,但投入真实业务的时候,你很快会发现自动生成的schema天然缺三层东西。

第一层是业务语义。函数签名里的name: string,到了真实场景可能是客户姓名、产品名称、订单号,这些语义只有你能写清楚。第二层是约束条件。函数参数有取值范围、格式要求、依赖关系,比如“紧急程度只能是0到5的整数”“手机号必须是11位数字”,类型系统表达不了这些,需要手动写进schema。第三层是使用策略,也就是什么场景下该用这个工具、什么场景下不该用。三个工具、几十个参数摆在模型面前,如果缺少这层说明,模型就会像没见过世面的实习生,拿到什么都想试一下。

自动生成适合开发阶段的快速验证,但到了打磨线上效果的时候,手动创建tool schema就是绕不开的一步。它不是“返工”,而是Agent工程质量的关键环节。

2. tool schema的核心设计拆解

2.1 schema的本质:一份写给LLM看的“操作契约”

很多人把tool schema理解成“接口文档”或者“参数校验规则”,其实都不完整。它的真实身份,是模型和你的系统之间的一份“操作契约”。

所谓契约,意味着双方都要遵守。你的系统承诺“只要你按格式给参数,我就执行对应的功能”,模型承诺“我按你的schema理解每个字段的含义并正确填充”。这份契约以JSON Schema的形式呈现,本身就是模型上下文的一部分。也就是说,schema不只是校验工具,它还会被模型逐字阅读。

这就引出一个关键推论:你在schema里写了什么、怎么描述、用什么措辞,都会直接影响模型的行为。举个例子,同一个参数,如果你写description: "紧急程度",模型可能传0到10之间任意值;如果你写description: "紧急程度,0为不紧急,5为非常紧急,仅在用户明确表示时间紧迫时填5",模型的填充准确率会显著提升。原因很简单,模型的判断依据就是你给的描述,描述越具体,模型的猜测空间越小。

所以我会把schema当成“写给AI的产品说明书”来写,而不是当成“给后端开发看的接口文档”来写。这是两种完全不同的写作方式。

2.2 手动创建的工具选型:JSON Schema原生的力量

在正式开始之前,先统一一下技术选型。目前各大Agent框架的tool schema基本都兼容OpenAI定义的JSON Schema格式。无论你用的是OpenAI Function Calling、Claude的tool use,还是开源的Agent框架,最终的tool参数基本都逃不出JSON Schema的范畴。

我建议手动创建时,直接以JSON Schema为基准。有两个原因:一是JSON Schema是一个标准规范,有大量现成校验器和工具链支持,比如ajv、python-jsonschema;二是JSON Schema的表达能力足够强,支持枚举、正则、嵌套对象、数组、条件约束,一个业务工具90%以上的参数约束都能表达清楚。

如果你用的是TypeScript或Python,也可以先用Zod或Pydantic“描述”约束,然后通过工具转成JSON Schema。但这里有个陷阱:转换工具只负责语法层面的映射,不负责语义层面的丰富。比如Zod的z.string()转出来就是一个{ type: "string" },你对字段的业务说明、枚举约束、示例值,还是得手动补上。所以这篇文章的核心方式,就是先理清业务逻辑,然后逐个字段手动打磨,最后落地成JSON Schema。如果实在需要代码生成辅助,生成之后也必须人工review和补充。

3. 手动创建实操:从零到一份可用的tool schema

3.1 第一步:先把函数签名写清楚,再反推参数结构

手动创建schema的第一步不是写JSON,而是先定义工具函数本身。我习惯把工具函数当作“系统对外暴露的一个能力”,它的签名就是能力的边界。

拿我之前做的工单系统举例,工具函数长这样:

def create_ticket( department: str, urgency: int, title: str, description: str, customer_id: int, tags: list[str], ) -> str: """ 创建一条客服工单。 返回工单编号。 """ ...

有了函数签名之后,再反推schema结构。这一步的核心原则是:凡是要让模型判断的东西,都要显式列出来;凡是可以由系统确定的东西,不要暴露给模型。

举个例子,customer_id可以通过对话上下文或用户系统自动获取,就不要让模型来选。很多项目翻车就是因为把大量内部字段暴露给模型,导致模型瞎猜。我在实操中会把字段分成三类:模型必须提供的(如描述、紧急程度)、模型可以从上下文中推断的(如部门、客户ID)、用户在没有明确提及时可以留空的(如标签)。分类之后再决定哪些进schema、哪些由代码填充。

3.2 第二步:逐字段打磨类型、约束与边界

字段结构定好之后,最关键的部分来了:逐个字段写类型、约束、描述。这里我重点说几个高频易错的点。

第一,能用枚举绝不用自由字符串。比如部门字段,如果你写成{ type: "string" },模型可能给你填出“技术支持部”“技术支撑”“tech support”各种版本,处理起来非常头疼。手动定义枚举,模型的选择空间就锁死了。JSON Schema里这样写:

{ "type": "string", "enum": ["technical", "billing", "sales", "other"], "description": "工单所属部门。technical表示技术问题,billing表示账单问题,sales表示销售咨询,other表示其他。" }

第二,数值范围要写清楚。紧急程度如果定义成{ "type": "integer" },模型不知道范围有多大,可能填个99。加minimum和maximum之后,模型的行为立刻收敛。而且注意,描述里还要写“什么情况算紧急”,这是数值边界替代不了的信息。

第三,字符串长度与格式建议加上约束。比如title超过100字会让下游系统截断,就加maxLength;description要求最少10个字,就加minLength。这些约束既能让模型填出更合适的值,也能在模型出错时让后端校验快速暴露问题。

第四,数组和嵌套对象要有内部约束。比如tags是一个字符串数组,就要写明每个元素的类型和最大数量。很多人只写了顶层type: "array",结果模型塞了一个对象数组进来,JSON解析都过不了。加上items和maxItems,等于把嵌套规则也钉死了。

3.3 第三步:把描述当作写Prompt一样认真对待

我个人的体感是,schema里description字段对模型行为的影响力,比类型和约束加起来还大。自动生成工具最缺的就是这块,手动创建的精髓也几乎全在这里。

描述里要写什么?不是写“这是工单标题”,而是写清楚:这个字段的实际含义、取值范围背后的业务规则、什么场景下填什么值、拿不准的时候怎么处理。我甚至会在描述里直接给出示例:

{ "type": "string", "description": "工单主题,一句话概括用户遇到的问题。应包含核心关键词,例如'网络频繁掉线'、'账单金额异常'等。不要包含标点符号和情绪化表达。" }

这样写的好处是,模型在生成参数时会“看到”示例,从而模仿示例的风格来填充。我遇到过不少情况,同一个字段,描述从一句干巴巴的“问题描述”改成“包含核心关键词的一句话,长度控制在20字以内”之后,生成质量立刻上了一个台阶。

还有一个小技巧:在schema的顶层描述里写“这个工具在什么情况下使用”。很多框架允许在tool的description字段设置一句话说明,可别浪费了。比如“当用户报告系统故障并希望创建工单时使用本工具。若用户只是在询问流程而不要求创建,不要调用此工具。”这相当于给模型划了一条使用红线,能有效减少无效的工具调用。

3.4 第四步:一个完整的精修版schema示例

下面给出我最终打磨出来的create_tickettool schema。这份schema在真实项目里测试过多轮,命中率和参数准确率都比较稳定:

{ "name": "create_ticket", "description": "创建一条客服工单并返回工单编号。当用户明确表示需要报修、投诉、咨询并需要后续跟进时使用。如果用户只是闲聊或询问流程,不要调用此工具。", "parameters": { "type": "object", "properties": { "department": { "type": "string", "enum": ["technical", "billing", "sales", "other"], "description": "工单所属部门。technical为技术故障,billing为账单或扣费问题,sales为购买或产品咨询,other为以上都不属于的情况。若无法判断,请选other。" }, "urgency": { "type": "integer", "minimum": 0, "maximum": 5, "description": "紧急程度,0为可等待,5为极度紧急。当用户使用'立刻''马上''宕机'等强烈词汇时为4或5;当用户表达不满但未说明时间要求时为2或3。" }, "title": { "type": "string", "minLength": 4, "maxLength": 50, "description": "工单主题,一句话概括问题,包含核心关键词,不要使用标点符号和情绪化表达。" }, "description": { "type": "string", "minLength": 10, "maxLength": 500, "description": "用户问题的详细描述。尽量还原用户原话中的关键信息,包括出现时间、频率、已尝试的操作、具体报错内容。不要遗漏可帮助定位问题的细节。" }, "tags": { "type": "array", "items": { "type": "string", "enum": ["network", "login", "payment", "crash", "device"] }, "maxItems": 3, "description": "问题标签,最多选择3个。network表示网络问题,login表示登录问题,payment表示支付问题,crash表示程序崩溃,device表示设备硬件。" } }, "required": ["department", "urgency", "title", "description"], "additionalProperties": false } }

你可以看到,这份schema的每一个字段都不是随便写的:枚举值 + 描述里的业务规则 + 边界约束 + 示例风格。这就是手动创建的意义,把“模型可能会猜错”的地方全部都提前堵上。

4. tool schema实战中的常见问题与排查

4.1 问题一:模型调用工具了,但参数填得乱七八糟

这是最常见的问题:工具被正确选中,但title是一整段对话原文,description里混着用户的情绪宣泄,urgency填了一个远超范围的值。遇到这种情况,我的排查顺序是固定的。

先看是不是类型约束太宽。比如title只写了type: "string"没有maxLength,模型有可能会把整句话都塞进去。加上了minLength和maxLength,模型反而会更倾向于生成精炼的标题。再看是不是描述里缺少“怎么写”的指导。描述只写“工单主题”,模型当然不知道怎么概括;描述里明确要求“包含核心关键词、控制在20字以内”,模型就会照做。最后看是不是缺枚举。自由填写的字符串字段,模型每次填法都不同,这是结构性问题,不是改一下描述能解决的,直接换枚举。

4.2 问题二:schema看起来没问题,但模型就是不调用工具

还有一类更隐蔽的问题:你提供了一个工具,但模型宁愿自己编一个答案,也不用你的工具。这种时候,问题往往不在parameters,而在tool级别的description上。

模型需要在很短的上下文里判断“现在该不该用这个工具”。如果tool的description写得太笼统,比如“创建一个工单”,模型会认为只有在用户字面说出“创建工单”时才该用,而用户的真实表达往往是“我的网连不上了快帮我处理”。这不是模型笨,而是你没让它理解“什么场景等于创建工单”。

我的方法是,把用户可能的表达方式直接写进tool描述里。比如这样:

当用户报告故障、投诉、服务咨询,并且事件需要后续跟进时使用本工具。触发场景示例:“网络掉线了”“账单扣错了”“登录不上”“我要投诉”。

description里放进触发场景示例之后,工具调用的召回率能明显提升。类似的效果,在OpenAI官方文档里也有提到:“description应描述工具的真实子任务,而不是重复工具名”。

4.3 问题三:复杂嵌套结构,模型解析直接失败

业务复杂的时候,工具参数免不了出现嵌套对象。最典型的比如订单查询工具:

{ "type": "object", "properties": { "filters": { "type": "array", "items": { "type": "object", "properties": { "field": { "type": "string" }, "operator": { "type": "string" }, "value": { "type": "string" } } } } } }

这种结构,模型不是填不出来,而是很容易填出不稳定结果,比如operator填了一个程序完全不认识的自定义符号,或者field填了不存在的列名。嵌套结构越深,模型生成的自由度就越大,越容易出错。

我的建议是,嵌套对象里的每个字段同样要加枚举和描述。operator就用枚举限制住,只允许eq、gt、lt、contains;field也用枚举限制为真实存在的列名。嵌套结构本身没问题,问题是嵌套结构里的自由度没被控制住。自由度越小,模型生成越稳定,这是tool schema设计中一条贯穿始终的原则。

4.4 常见问题速查表与我的排障顺序

整理一张速查表,方便大家对照排查:

现象可能原因优先处理方式
工具被调用但参数错误描述缺少业务规则逐字段补充示例与取值说明
模型不调用工具tool描述未写触发场景在description中写入用户典型表达
枚举字段填了可接受之外的值未使用enum约束一律改为enum并覆盖未知值用other兜底
数值字段填了离谱值未设置minimum/maximum显式写出范围并说明业务含义
数组字段解析失败未约束items与maxItems为items增加类型和枚举,限制长度
解释性字段内容过冗长未设置maxLength添加长度约束并在描述里给出句式模板
模型调用工具过于频繁description未写明“不使用”的场景增加反面说明,明确舍弃的情况
object内字段缺失required未包含必填字段重新检查必填项,补全required

排障的顺序也很重要。我通常先做“合法性检查”,也就是用ajv或python-jsonschema工具验证schema本身有没有语法问题,比如required数组里写了不存在的字段、类型写错等,这类低级错误浪费了我不少时间。然后做“调用实验”,用几个典型用户query去测模型,观察它选不选工具、填什么参数。最后才微调描述和约束。这样每一步都有明确的方向,不会白忙。

5. 手动创建schema的工程化落地

5.1 直接写JSON还是用代码生成器?

到了这个阶段,你应该已经理解了为什么需要手动创建。但工程落地还有一层选择:是直接在代码仓库里维护JSON文件,还是用Zod/Pydantic定义约束再转成JSON Schema?

我的答案是:维护方式看团队,但最终产物必须是一份可校验的JSON Schema。理由有三条。

第一,JSON Schema是跨语言的,Python后端、TypeScript前端都可以用统一的标准校验;第二,JSON Schema可以脱离代码单独review,产品、测试甚至业务同事都能读得懂;第三,大多数Agent框架接收的输入就是JSON Schema,转换步骤在线上反而是多余的一环。

如果你用TypeScript,Zod的z.string().describe("...").enum([...])写法更贴近开发习惯,配合zod-to-json-schema可以生成标准schema。但请记住,转换只解决“类型表达”问题,不解决“描述丰富度”问题。你依然要在Zod链上手动补describe,补完之后再人工读一遍生成的JSON,看有没有丢失内容。

5.2 为schema建立版本管理意识

tool schema上线之后不是一成不变的。业务部门改了流程,紧急程度从5级变成3级,或者新增了一个部门枚举值,这些变化都要同步到schema里。一旦多个Agent共享同一个工具,schema的变更就必须有版本意识。

我的习惯是这样:每个tool schema放在独立文件里,命名带版本号,例如create_ticket.v2.json。字段变更时更新版本,并在变更说明里写清楚“哪个字段动了、为什么动、影响的Agent有哪些”。这样出问题的时候,回溯成本会低很多。另一个值得做的是在CI流水线里加一个schema校验步骤,确保任何提交到Agent配置库的改动都是合法JSON Schema。这个自动化检查看起来简单,但能拦住很多手误。

5.3 测试你的schema:三个维度的实测方案

手动创建的schema到底好不好用,不能靠感觉,要做三类测试。

第一类是结构校验测试:用ajv去校验模型输出的参数JSON是否符合schema。这一步是底线,保证入参合法。

第二类是覆盖率测试:准备一组真实的用户对话样本,覆盖各种触发场景,记录“该调用时是否调用”“调用时参数是否完全正确”。这组测试直接反映schema描述的质量,是优化schema的主要依据。

第三类是边界压力测试:故意使用模糊、歧义、极长的用户输入,观察模型的工具调用行为。比如用户说“这个订单不对,你们到底行不行”,模型能不能识别出需要创建投诉工单,还是把空洞的情绪描述填进description跑了。边界测试能暴露描述里的盲区。

我见过一些团队用LLM as a Judge的方式,让另一个大模型去评估工具调用的质量,效果也不错。但成本有点高,日常优化还是“人工看对话样本 + 统计调用准确率”性价比最高。

5.4 从手动到半自动:沉淀一份自己的schema模板库

最后说一个长期价值很高的习惯:把常用工具的schema模板沉淀成库。比如工单类、搜索类、订单类、内容生成类,这些业务形态高度相似,schema结构也有很多共通之处。做过的项目多了之后,你会发现大量描述写法、约束模式、枚举设计都是可以复用的。

我自己收了一套模板库,里面分三块:一是各业务模块的典型参数结构,二是常用的枚举值写法(包括“无法判断时填other”这种兜底策略),三是描述撰写的模板句式。新项目接入一个新工具时,我直接从这个库里复制、改字段,而不是每次都从零开始想。这个习惯大大提升了我的交付效率,也保证了多个Agent项目之间工具风格的一致性。

如果你刚接触手动创建tool schema,我也建议你从第三个工具开始就留意沉淀,不要等到积累了几十个工具再回头补。到那会儿你已经记不清每个工具当时是怎么设计的了。

6. 最后聊一点我的实际体会

做Agent开发这段时间,我最大的感受是,工具调用准确率不高的时候,先别急着换模型、换框架,先把tool schema翻出来逐字段问自己几个问题:这个字段模型真的知道怎么填吗?不填会怎样?填错了会怎样?描述里有没有把业务规则说透?

很多看起来像是“模型不够聪明”的问题,根源其实是“schema没把话说清楚”。模型从来不知道你的函数内部逻辑,它只能在schema描述的范围内做选择。你把边界划得越清楚,它跑偏的概率就越小。

手动创建tool schema,说到底就是在做Agent的“行为塑造”,把一份薄薄的接口定义变成一份富含业务规则的操作手册。这件事没有太多炫技的空间,靠的是耐心和细致,但它对Agent效果的影响,远比大多数人想象的要大。希望这篇梳理能帮你在自己的Agent项目上少走一些弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 13:56:09

Cursor 接入 MCP 协议实战:配置、选型与避坑指南

1. 为什么大家都在给 Cursor 接 MCP如果你最近在折腾 Cursor,大概率会刷到“MCP”这个词。MCP 全称 Model Context Protocol,翻译过来叫“模型上下文协议”,说白了就是一套让 AI 助手能跟外部工具、数据源对话的通用接口标准。你可以把它理解…

作者头像 李华
网站建设 2026/10/1 13:56:00

STEP 7 MicroWIN SMART V2.7.0.0 安装全链路解析

1. 这不是普通软件安装:STEP 7 MicroWIN SMART V2.7.0.0 的工业现场真实处境 你搜“STEP 7 MicroWIN SMART V2.7.0.0安装”,大概率正坐在工控柜前,手边摆着一台刚拆封的S7-200 SMART PLC,或者正被产线停机逼得焦头烂额——老板在微…

作者头像 李华
网站建设 2026/10/1 13:55:34

超大型电商系统高可用架构设计与DDD落地实践

简介:本资源是京东商城官方出品的超大型电商系统架构设计方案PDF文档,面向中高级后端工程师、系统架构师及电商平台技术负责人,聚焦高并发、高可用、可扩展的分布式系统设计实践。方案完整覆盖架构目标(99.99%系统可用性、50分钟全…

作者头像 李华
网站建设 2026/10/1 13:53:41

麒麟V10海光平台NVIDIA驱动安装实战指南

1. 环境理解与整体思路 1.1 这套平台组合到底特殊在哪 接手这个任务的时候,第一反应不是“装个驱动能有多难”,而是要先想清楚:麒麟V10、海光、NVIDIA这三个词凑在一起,意味着什么。 先说海光。海光的CPU是x86架构,指…

作者头像 李华
网站建设 2026/10/1 13:52:58

违规驾驶行为识别系统:基于姿态估计与OpenCV的Python实现全解析

简介:这套供Python毕业设计项目参考的违规驾驶行为识别系统完整源码与数据库包,适合计算机相关专业学生用于课程设计或毕业设计。系统围绕驾驶行为检测任务,涵盖数据处理、模型训练、推理识别与结果展示等环节,能够帮助初学者理解…

作者头像 李华