第02章 工具Schema设计:让LLM准确理解你的工具
第02章 工具Schema设计:让LLM准确理解你的工具
“如果你的工具Schema写得不好,再聪明的LLM也会调用错。Schema是人和AI之间的合同。” —— AI系统工程师
工具Schema是工具调用系统中最被低估的部分。很多开发者花大量时间在工具的实现逻辑上,却用三句话草草描述Schema——然后疑惑为什么LLM总是调用错工具或传错参数。
本章专注Schema设计,这是提升工具调用准确率的最高ROI投入。
2.1 Schema的四个组成部分
# 完整的工具Schema解析
PERFECT_TOOL_SCHEMA = {
"type": "function",
"function": {
# ===== 组件1:工具名称 =====
"name": "search_company_news",
# 命名规则:
# ✅ 动词_名词_范围(清晰说明做什么、对什么、在哪里)
# ✅ 下划线分隔(不用驼峰,更清晰)
# ❌ get_data(太通用)
# ❌ tool1(无意义)
# ❌ searchCompanyNewsForGivenCompanyInRecentDays(太长)
# ===== 组件2:工具描述 =====
"description": """搜索特定公司的最新新闻和公告。
适用场景:
- 查询公司最近的新闻动态
- 了解公司公告、财报、重大事件
不适用场景:
- 通用话题搜索(请使用 search_web)
- 历史股价查询(请使用 get_stock_price)
返回:最近N条新闻,每条包含标题、来源、日期、摘要""",
# 描述要素:
# 1. 一句话核心功能
# 2. 适用场景(帮LLM知道什么时候选这个工具)
# 3. 不适用场景(帮LLM区分相似工具)
# 4. 返回内容描述
# ===== 组件3:参数定义 =====
"parameters": {
"type": "object",
"properties": {
"company_name": {
"type": "string",
"description": "公司名称,使用完整名称,例如:'阿里巴巴'、'腾讯'、'Apple Inc.'",
# 参数描述要告诉LLM:格式、示例、约束
},
"days": {
"type": "integer",
"description": "查询最近多少天的新闻。默认7天,最大90天",
"default": 7,
"minimum": 1,
"maximum": 90,
# 数值类型:提供范围约束和默认值
},
"language": {
"type": "string",
"enum": ["zh", "en"],
"description": "新闻语言:zh=中文,en=英文。默认zh",
"default": "zh",
# 枚举类型:列出所有合法值
},
"max_results": {
"type": "integer",
"description": "返回最多多少条新闻。默认5,最大20",
"default": 5,
"minimum": 1,
"maximum": 20,
},
},
# ===== 组件4:必填参数声明 =====
"required": ["company_name"],
# 原则:只有没有合理默认值的参数才必填
# 错误做法:把所有参数都required(LLM要推测所有参数,容易出错)
"additionalProperties": False,
# 强烈建议加这一行:禁止LLM传入未定义的参数
}
}
}
2.2 常见Schema设计错误
# 真实场景中常见的Schema设计问题
# ===== 错误1:描述太简单 =====
BAD_SCHEMA_1 = {
"name": "send_message",
"description": "发送消息", # 太简单!LLM不知道:发给谁?什么类型消息?
"parameters": {
"type": "object",
"properties": {
"to": {"type": "string"},
"content": {"type": "string"},
},
"required": ["to", "content"]
}
}
GOOD_SCHEMA_1 = {
"name": "send_email",
"description": """发送电子邮件给指定收件人。
适用于:通知、报告、警报发送
不适用于:即时消息(使用 send_slack_message)、短信(使用 send_sms)
注意:每次调用只能发送给一个收件人。批量发送请多次调用。""",
"parameters": {
"type": "object",
"properties": {
"to_email": {
"type": "string",
"description": "收件人邮箱地址,格式:user@domain.com",
"pattern": r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
},
"subject": {
"type": "string",
"description": "邮件主题,不超过100个字符",
"maxLength": 100,
},
"body": {
"type": "string",
"description": "邮件正文,支持纯文本。换行使用\\n",
},
"priority": {
"type": "string",
"enum": ["normal", "high", "urgent"],
"description": "邮件优先级:normal=普通,high=重要,urgent=紧急(会触发即时通知)",
"default": "normal",
}
},
"required": ["to_email", "subject", "body"],
"additionalProperties": False,
}
}
# ===== 错误2:参数名称模糊 =====
BAD_PARAM = {
"data": {"type": "string", "description": "数据"}, # data是什么数据?
"type": {"type": "string"}, # type是哪种type?
"id": {"type": "string"}, # 什么的ID?
"date": {"type": "string"}, # 什么格式?
}
GOOD_PARAM = {
"csv_data": {"type": "string", "description": "CSV格式的数据,首行为表头,逗号分隔"},
"export_format": {
"type": "string",
"enum": ["json", "csv", "xlsx"],
"description": "导出格式:json=JSON文件,csv=逗号分隔,xlsx=Excel文件"
},
"record_id": {"type": "string", "description": "数据库记录的UUID,格式:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"},
"start_date": {
"type": "string",
"description": "开始日期,ISO 8601格式:YYYY-MM-DD,例如:2024-01-15",
"pattern": r"^\d{4}-\d{2}-\d{2}$",
},
}
# ===== 错误3:Schema和实现不一致 =====
# Schema说参数是可选的,但实现里没有处理None的情况
def bad_tool(city: str, country: str = None) -> dict: # Schema里 country 是可选的
url = f"https://weather.api/{country}/{city}" # BUG: country 是 None 时崩溃
# ...
def good_tool(city: str, country: str = "CN") -> dict: # 有默认值
url = f"https://weather.api/{country}/{city}" # 安全
# ...
2.3 复杂参数类型的Schema
# 处理数组、嵌套对象、联合类型
# ===== 数组参数 =====
ARRAY_SCHEMA = {
"name": "bulk_lookup_users",
"description": "批量查询多个用户的信息",
"parameters": {
"type": "object",
"properties": {
"user_ids": {
"type": "array",
"items": {"type": "string"},
"description": "要查询的用户ID列表,最多一次查询20个",
"minItems": 1,
"maxItems": 20,
},
"fields": {
"type": "array",
"items": {
"type": "string",
"enum": ["name", "email", "phone", "created_at", "subscription_tier"]
},
"description": "要返回的字段列表。不填返回所有字段(会更慢)",
}
},
"required": ["user_ids"],
"additionalProperties": False,
}
}
# ===== 嵌套对象 =====
NESTED_SCHEMA = {
"name": "create_calendar_event",
"description": "在Google Calendar中创建日历事件",
"parameters": {
"type": "object",
"properties": {
"title": {"type": "string", "description": "事件标题"},
"time_range": {
"type": "object",
"description": "事件时间范围",
"properties": {
"start": {
"type": "string",
"description": "开始时间,ISO 8601格式含时区:2024-04-15T14:00:00+08:00"
},
"end": {
"type": "string",
"description": "结束时间,必须晚于开始时间"
}
},
"required": ["start", "end"],
},
"attendees": {
"type": "array",
"items": {
"type": "object",
"properties": {
"email": {"type": "string"},
"optional": {"type": "boolean", "description": "是否可选参与者"},
},
"required": ["email"],
},
"description": "参会者列表(可选)",
}
},
"required": ["title", "time_range"],
"additionalProperties": False,
}
}
# ===== 处理"或"关系的参数 =====
UNION_SCHEMA = {
"name": "lookup_user",
"description": "通过ID或邮箱查询用户。提供其中一种即可",
"parameters": {
"type": "object",
"properties": {
"user_id": {
"type": "string",
"description": "用户UUID(如果知道的话)"
},
"email": {
"type": "string",
"description": "用户邮箱地址(如果不知道user_id的话)"
}
},
# 注意:required 不填,但描述里明确说"至少提供其中一种"
"additionalProperties": False,
}
}
# 在工具实现里验证:
def lookup_user(user_id: str = None, email: str = None) -> dict:
if not user_id and not email:
return {"error": "必须提供 user_id 或 email 之一"}
# ...
2.4 Schema测试:验证设计质量
# 用这个测试框架验证你的Schema设计
class SchemaQualityChecker:
"""Schema质量检查器"""
def check(self, schema: dict) -> dict:
issues = []
score = 100
fn = schema.get("function", schema) # 支持两种格式
# 检查1:工具名称质量
name = fn.get("name", "")
if len(name) < 5:
issues.append(("ERROR", f"工具名称太短: '{name}',建议使用动词_名词格式"))
score -= 20
if name == name.upper() or "_" not in name:
issues.append(("WARNING", f"工具名称格式建议:动词_名词_范围(如 search_web_for_news)"))
score -= 5
# 检查2:描述质量
desc = fn.get("description", "")
if len(desc) < 30:
issues.append(("ERROR", "工具描述太短(< 30字),LLM无法准确判断何时使用"))
score -= 25
if "适用" not in desc and "用于" not in desc and "when" not in desc.lower():
issues.append(("WARNING", "建议在描述中添加适用场景说明"))
score -= 10
# 检查3:参数描述
params = fn.get("parameters", {}).get("properties", {})
for param_name, param_def in params.items():
if not param_def.get("description"):
issues.append(("ERROR", f"参数 '{param_name}' 缺少描述"))
score -= 10
elif len(param_def.get("description", "")) < 10:
issues.append(("WARNING", f"参数 '{param_name}' 描述太简短"))
score -= 5
# 检查4:additionalProperties
if not fn.get("parameters", {}).get("additionalProperties") is False:
issues.append(("WARNING", "建议添加 'additionalProperties': false 防止无效参数"))
score -= 5
return {
"score": max(0, score),
"grade": "A" if score >= 90 else "B" if score >= 70 else "C" if score >= 50 else "D",
"issues": issues,
"summary": f"Schema质量: {score}/100",
}
# 测试你的Schema
checker = SchemaQualityChecker()
# result = checker.check(MY_TOOL_SCHEMA)
# print(result)
本章小结
五个核心认知:
-
Schema描述是人和LLM之间的合同:描述越清晰,LLM越容易做出正确的工具选择和参数填写;含糊的Schema是工具调用错误率高的根本原因
-
工具名称要包含动词+名词+范围:
search_company_news比get_data好100倍——LLM在选工具时,名称是最快速的信号 -
描述里要包含"适用/不适用场景":有多个相似工具时,明确标注"不适用XX,请用YY工具"能大幅减少工具混淆
-
参数描述要给格式、范围、示例:
"日期字符串"不够,"ISO 8601格式:2024-01-15"才是好描述——LLM需要知道确切格式 -
additionalProperties: false是安全最佳实践:防止LLM"创造"你没定义的参数,减少意外行为
核心行动:
# 用 SchemaQualityChecker 检查你现有的工具Schema
# 目标:让每个工具的得分达到 80 分以上
# 特别关注:
# - 所有参数是否有清晰的描述和格式说明
# - 有没有容易混淆的工具对?如果有,在描述中明确区分
本章提示词模板
模板一:审查Schema质量
请帮我审查以下工具Schema的质量:
[粘贴你的Schema JSON]
请评估:
1. 工具名称是否清晰(能从名称猜到用途)?
2. 描述是否足够详细(包含适用场景)?
3. 每个参数的描述是否清晰(格式、范围、示例)?
4. required字段的设置是否合理?
5. 如果我同时有另一个类似工具[描述另一个工具],LLM是否容易混淆这两个?
6. 给出修改后的完整Schema(保持JSON格式)
模板二:为新工具设计Schema
我要给AI助手添加一个新工具,功能如下:
功能描述:[详细描述工具做什么]
输入:[描述需要什么输入参数]
输出:[描述工具返回什么]
可能失败的场景:[描述什么情况下工具会报错]
和已有工具的区别:[描述和其他类似工具的区别]
请帮我:
1. 设计符合OpenAI格式的完整工具Schema
2. 说明每个设计决策的理由(为什么这样命名、这样描述)
3. 指出这个Schema中最可能被LLM误解的地方
4. 给出3个测试用例(用户输入)来验证LLM能正确调用这个工具