很多团队在排查 LLM Function Calling / Tool Call 问题时,第一反应是加重试、加校验、加执行保护。但在生产环境里,一部分错误并不是执行阶段才出现的,而是在 schema 定义那一刻就埋下了。工具名含糊、description 太短、参数类型过于开放、required 字段随意设计,都会让模型在调用前就走偏。
好的 Tool Schema 不只是给程序看的接口定义,更是给模型看的操作说明书。它要减少歧义,压缩自由度,把业务约束提前写进模型能理解的结构里。下面这 5 条原则,聚焦设计阶段的 schema 写法,而不是运行时防御。
原则一:命名用"动词_对象_范围",避免近义工具互相抢调用
工具名是模型选择 tool 的第一信号。很多失败调用不是参数错,而是工具一开始就选错了。命名最好同时表达动作、对象和范围,例如 search_messages_recent、create_calendar_event、get_user_profile_by_id。
不要使用 handle、process、manage 这类泛动词,也不要让多个工具只有细微差别,比如 get_user、fetch_user、query_user 同时存在。对模型来说,它们的语义边界太近,容易互相竞争。
❌ 坏例子:命名含糊,范围不清。
json
{
"name": "process_user",
"description": "Process user information.",
"parameters": {
"type": "object",
"properties": {
"user": { "type": "string" }
},
"required": ["user"]
}
}
这个工具名没有说明是查询、更新、删除还是同步,也没有说明 user 应该是 ID、邮箱还是用户名。模型只能猜。
✅ 好例子:动作、对象、范围都明确。
json
{
"name": "get_user_profile_by_open_id",
"description": "Get one user's basic profile by Feishu open_id. Use this only when the input is an open_id like 'ou_xxx'. Do not use it for email, phone number, or fuzzy name search.",
"parameters": {
"type": "object",
"properties": {
"open_id": {
"type": "string",
"description": "Feishu user open_id, format starts with 'ou_'. Example: 'ou_abc123'."
}
},
"required": ["open_id"]
}
}
这里的名字已经告诉模型:这是 get,不是 search;对象是 user profile;输入范围是 open_id。description 再补充不要用于邮箱、手机号和模糊搜索。
一个实用约定是:
text
动词_对象_范围
search_messages_recent
create_calendar_event
update_task_status
list_chat_members
get_invoice_by_number
如果两个工具名字读起来像同义词,通常说明应该合并、改名,或用更强的范围词区分。
原则二:让 description 做重活,写清格式、约束和计算规则
很多 schema 的 description 只有一句"Create an event"或"Search data"。这对人类工程师也许够,对模型不够。模型需要知道何时使用、何时不要使用、字段怎么计算、格式是什么、默认假设是什么。
好的 description 应该承担三类信息:
- 使用边界:什么场景用,什么场景不用;
- 参数规则:格式、单位、取值含义;
- 推导规则:用户没说完整时如何补齐。
❌ 坏例子:description 几乎没有信息。
json
{
"name": "create_calendar_event",
"description": "Create a calendar event.",
"parameters": {
"type": "object",
"properties": {
"title": { "type": "string" },
"start": { "type": "string" },
"end": { "type": "string" }
},
"required": ["title", "start"]
}
}
模型不知道时间格式、时区、默认时长、是否可以创建全天事件,也不知道 end 缺失时怎么办。
✅ 好例子:把模型容易猜错的规则写进去。
json
{
"name": "create_calendar_event",
"description": "Create one calendar event for the current user. Use only after the user has provided or confirmed the event title and start time. Time values must be RFC3339 strings with timezone, for example '2026-06-10T14:00:00+08:00'. If the user gives a start time but no duration, set end_time to 1 hour after start_time. Do not create events in the past unless the user explicitly asks to record a past event.",
"parameters": {
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "Event title shown on the calendar. Keep the user's original wording when possible."
},
"start_time": {
"type": "string",
"format": "date-time",
"description": "Event start time in RFC3339 format with timezone, e.g. '2026-06-10T14:00:00+08:00'."
},
"end_time": {
"type": "string",
"format": "date-time",
"description": "Event end time in RFC3339 format with timezone. If user did not specify duration, use start_time plus 1 hour."
}
},
"required": ["title", "start_time", "end_time"]
}
}
这类 description 不是废话,而是在减少模型自由发挥的空间。尤其是时间、金额、分页、排序、权限范围等字段,必须把计算规则写清楚。
原则三:用 enum、format 和结构化对象约束自由度,避免开放 string
开放 string 是 tool call 出错的高发地。只要字段写成 type: string,模型就可能填入中文、英文、别名、自然语言描述、甚至一整句话。能枚举就枚举,能用数组就别用逗号字符串,能用结构化对象就别用自由文本。
❌ 坏例子:所有参数都是 string。
json
{
"name": "search_orders",
"description": "Search orders.",
"parameters": {
"type": "object",
"properties": {
"status": { "type": "string" },
"date_range": { "type": "string" },
"sort": { "type": "string" }
}
}
}
模型可能传:
json
{
"status": "已完成",
"date_range": "最近一个月",
"sort": "按创建时间倒序"
}
这些值对人类可读,但后端很难稳定解析。
✅ 好例子:状态枚举、时间拆字段、排序结构化。
json
{
"name": "search_orders",
"description": "Search orders by structured filters. Convert relative user expressions like 'last month' into explicit start_date and end_date before calling.",
"parameters": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": ["pending", "paid", "shipped", "completed", "cancelled"],
"description": "Order status. Use 'completed' for orders that have been fully finished."
},
"start_date": {
"type": "string",
"format": "date",
"description": "Inclusive start date, format YYYY-MM-DD."
},
"end_date": {
"type": "string",
"format": "date",
"description": "Inclusive end date, format YYYY-MM-DD."
},
"sort": {
"type": "object",
"properties": {
"field": {
"type": "string",
"enum": ["created_at", "paid_at", "amount"]
},
"direction": {
"type": "string",
"enum": ["asc", "desc"]
}
},
"required": ["field", "direction"]
}
}
}
}
另一个常见问题是把列表塞进字符串。
❌ 坏例子:
json
{
"tag_ids": {
"type": "string",
"description": "Comma separated tag ids."
}
}
✅ 好例子:
json
{
"tag_ids": {
"type": "array",
"items": { "type": "string" },
"description": "List of tag ids. Do not join them into a comma-separated string."
}
}
Schema 的目标不是让模型"更会表达",而是让它"更少有表达空间"。
原则四:required 字段只放调用必须信息,optional 字段配合 default 要谨慎
required 字段设计太少,模型会在信息不足时过早调用工具;required 字段太多,模型会为了凑参数而编造值。正确做法是:把执行动作不可缺少、且不能由系统安全推导的字段设为 required;可以由服务端确定的字段,不要让模型填。
❌ 坏例子:required 太少,导致缺关键信息也能调用。
json
{
"name": "send_notification",
"description": "Send a notification to a user.",
"parameters": {
"type": "object",
"properties": {
"user_id": { "type": "string" },
"message": { "type": "string" },
"channel": { "type": "string" }
},
"required": ["message"]
}
}
这个 schema 允许模型只带 message 调用。接下来系统要么报错,要么走某个危险默认收件人。
✅ 好例子:收件人和内容必填,渠道受限并有明确默认策略。
json
{
"name": "send_notification_to_user",
"description": "Send one notification to a specific user. Use only after the recipient and message content are known. If channel is omitted, the server will use the user's preferred channel; do not guess the channel from the conversation.",
"parameters": {
"type": "object",
"properties": {
"user_id": {
"type": "string",
"description": "Stable internal user id. Must not be a display name."
},
"message": {
"type": "string",
"description": "Notification content to send. Do not include hidden reasoning or internal notes."
},
"channel": {
"type": "string",
"enum": ["email", "sms", "in_app"],
"description": "Optional. If omitted, server uses the user's preferred channel."
}
},
"required": ["user_id", "message"]
}
}
default 值也要小心。Schema 里的 default 对模型来说常常是"可以不想就填这个"。如果默认值带来真实副作用,例如是否通知用户、是否覆盖已有数据、是否公开可见,最好在 description 中写清楚由服务端决定,或者要求用户确认。
❌ 坏例子:危险默认值暴露给模型。
json
{
"overwrite": {
"type": "boolean",
"default": true,
"description": "Whether to overwrite existing file."
}
}
✅ 好例子:让破坏性选项显式化。
json
{
"overwrite": {
"type": "boolean",
"description": "Whether to overwrite an existing file. Set to true only when the user explicitly confirmed replacement. Otherwise omit or set false."
}
}
判断 required 的一个简单标准是:如果这个字段缺失时,服务端无法安全、确定、无副作用地推导,就应该 required;如果字段应该由认证态、租户配置、服务端策略决定,就不要交给模型。
原则五:工具分组与动态加载,控制单次上下文工具数量 ≤ 12
Schema 写得再好,如果一次性塞给模型 40 个工具,可靠性也会下降。工具越多,名字越相近,模型选择错误的概率越高。工程上应该按任务阶段、业务域、权限范围动态加载工具,尽量把单次上下文里的候选工具控制在 12 个以内。
这里的重点仍然是设计阶段:不要把所有 API 都平铺成一个巨大工具箱,而是给工具设计分组元数据和加载策略。
❌ 坏例子:所有工具常驻。
json
{
"toolset": "all_tools",
"description": "All available tools for the assistant.",
"tools": [
"search_user",
"get_user",
"update_user",
"delete_user",
"create_event",
"update_event",
"delete_event",
"search_message",
"send_message",
"create_invoice",
"refund_payment",
"query_order",
"update_order",
"export_report",
"upload_file",
"delete_file"
]
}
这会让模型在每轮都面对过多选择,尤其是 search_user / get_user / update_user 这类相邻工具,很容易误选。
✅ 好例子:按意图加载小工具集。
json
{
"tool_groups": [
{
"name": "calendar_scheduling",
"description": "Use for creating, updating, listing, and checking calendar events.",
"load_when": "The user asks about meetings, schedules, availability, reminders, or calendar events.",
"max_tools": 8,
"tools": [
"list_calendar_events",
"create_calendar_event",
"update_calendar_event",
"delete_calendar_event",
"check_user_freebusy"
]
},
{
"name": "message_search",
"description": "Use for searching and reading chat messages. Does not include sending messages.",
"load_when": "The user asks to find previous conversations, files, images, or mentions.",
"max_tools": 6,
"tools": [
"search_messages",
"list_chat_messages",
"get_thread_messages",
"fetch_message_file"
]
}
]
}
再进一步,可以把"读"和"写"分成不同组。用户只是查询历史消息时,不加载 send_message;用户只是看日程时,不加载 delete_calendar_event。这样做的好处不是隐藏能力,而是降低模型在当前任务里选错工具的概率。
工具数量的经验线可以这样定:
text
1-6 个:模型选择通常很稳定
7-12 个:可接受,但要注意命名区分
13-20 个:开始明显依赖 description 质量
20+ 个:建议拆分工具组或动态加载
如果某个任务必须暴露超过 12 个工具,优先检查是否存在命名相近、职责重叠、读写混放的问题。
Schema 自评 checklist
下面这张表可以直接贴到工具设计 PR 里,让 schema review 有一个固定入口。
markdown
| 检查项 | 是/否 | 备注 |
| --- | --- | --- |
| 工具名是否符合"动词_对象_范围"结构? | | |
| 是否避免了 handle/process/manage/do 等泛动词? | | |
| 是否存在多个名称相近、职责重叠的工具? | | |
| description 是否写清"什么时候用 / 什么时候不用"? | | |
| description 是否包含关键字段格式、单位、计算规则? | | |
| 时间字段是否使用 format: date 或 format: date-time,并说明时区? | | |
| 状态、类型、排序方向等字段是否使用 enum? | | |
| 列表参数是否使用 array,而不是逗号分隔 string? | | |
| 是否避免了需要后端解析自然语言的开放 string? | | |
| required 字段是否覆盖执行动作必需信息? | | |
| required 字段是否避免迫使模型编造未知值? | | |
| default 值是否不会引入副作用或权限风险? | | |
| 用户身份、租户、权限范围是否由服务端决定,而不是让模型填写? | | |
| 当前任务加载的工具数量是否 ≤ 12? | | |
| 读工具和写工具是否按场景分组加载? | | |
总结
Function Calling 的可靠性,不只取决于模型能力,也取决于我们给模型的接口是否清晰。好的 Tool Schema 会把歧义挡在调用之前:名字让模型选对工具,description 让模型理解边界,类型约束让模型少填错值,required 设计让模型不乱猜,动态加载让模型少面对无关选择。与其在执行阶段不断补救,不如在 schema 设计阶段先把自由度收窄。工程上,这通常是成本最低、收益最稳定的一层优化。