第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)

本章小结

五个核心认知:

  1. Schema描述是人和LLM之间的合同:描述越清晰,LLM越容易做出正确的工具选择和参数填写;含糊的Schema是工具调用错误率高的根本原因

  2. 工具名称要包含动词+名词+范围search_company_newsget_data 好100倍——LLM在选工具时,名称是最快速的信号

  3. 描述里要包含"适用/不适用场景":有多个相似工具时,明确标注"不适用XX,请用YY工具"能大幅减少工具混淆

  4. 参数描述要给格式、范围、示例"日期字符串" 不够,"ISO 8601格式:2024-01-15" 才是好描述——LLM需要知道确切格式

  5. 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能正确调用这个工具

→ 第03章:基础工具实战:文件、HTTP、系统命令