LLM工具调用格式问题全解析:从JSON Schema设计到稳定输出实战 1. 从一次“格式炸裂”的惨痛经历说起那天下午我正在调试一个基于大语言模型的智能客服工单处理系统。核心流程很简单用户用自然语言描述问题LLM 理解后调用一个名为create_ticket的工具Tool将工单信息以结构化的 JSON 格式存入数据库。我信心满满因为我已经为这个工具精心设计了 JSON Schema明确规定了title字符串、priority枚举高、中、低、description字符串等字段。测试时LLM 对“我的网站无法访问请紧急处理”这样的描述总能完美地输出{title: 网站无法访问, priority: 高, description: 用户反馈网站无法访问请求紧急处理。}。一切看起来都很美好。直到线上一个真实用户输入了这样一句话“我觉得页面加载有点慢大概等了五六秒吧另外提交表单后那个成功提示的绿色小对勾图标好像颜色不太对是不是 #00FF00 这个绿色顺便问下如果升级到高级套餐响应速度能保证在 200ms 以内吗”灾难发生了。LLM 的回复看起来调用了工具但返回的 JSON 却是一团乱麻{“title”: “页面加载慢和图标颜色问题咨询”, “priority”: “中”, “description”: “用户反馈1. 页面加载慢约5-6秒。2. 表单提交后成功提示图标颜色异常疑似为#00FF00。3. 咨询升级高级套餐后响应速度能否保证在200ms内。”, “tags”: [“性能”, “UI”], “expected_response_time”: “200ms”}。问题在哪首先JSON 的引号是全角中文引号“”这直接导致后端 JSON 解析器报错。其次我定义的 Schema 里根本没有tags和expected_response_time这两个字段LLM “自作主张”地添加了它认为有用的信息。这次调用不仅失败了还因为传入了未定义的字段触发了后端 API 的验证错误整个流程中断。这就是典型的“格式炸了”。它不仅仅是 JSON 解析错误更深层的是 LLM 的 Tool Use工具调用行为超出了我们预设的边界。很多开发者包括当时的我都有一个误区以为给 LLM 一个工具的 Schema模式定义它就会像编译型语言一样严格地、一丝不苟地按照这个“合同”来执行。但现实是LLM 是生成模型它的核心能力是“模仿”和“补全”而非“遵守”。它看到了create_ticket需要title,priority,description它会尽力去生成符合这些字段的内容。但它也可能基于对对话上下文的理解“认为”添加tags和expected_response_time会让这张工单更完善于是它就加了。至于全角引号那可能是它在训练数据中见过类似格式或者在其内部 tokenization 过程中产生了混淆。所以Tool Use 到底能保证什么它能保证的远比你想象的要少而你不能指望它保证的恰恰是稳定性的关键。搞懂这条边界就是驯服 LLM、构建可靠 AI 应用的第一步。本文将彻底拆解 Tool Use 的能力边界并给出让 LLM 输出稳定、合规 JSON 的实战方案。2. 拆解 Tool Use承诺、幻觉与不可控的“自由发挥”当我们谈论 LLM 的 Tool Use 时通常指的是让模型根据对话和给定的工具描述名称、功能说明、参数 Schema生成一个结构化的调用请求比如一个 JSON 对象。这个过程看似是确定性的实则充满了概率性。我们必须清晰地认识到 LLM 在此环节中的真实能力与局限。2.1 LLM 对 Tool Schema 的理解本质模式识别与补全LLM 并不“理解”JSON Schema 作为一种契约或接口定义。它处理 Schema 的方式和处理一段描述性文本没有本质区别。当你提供如下 Schema 时{ name: create_ticket, description: 创建一条用户工单, parameters: { type: object, properties: { title: { type: string, description: 工单的简短标题 }, priority: { type: string, enum: [高, 中, 低], description: 工单优先级 } }, required: [title, priority] } }LLM 所做的是进行模式识别“哦这是一段关于create_ticket工具的文本。它需要一个title字符串和一个priority只能是‘高’、‘中’、‘低’。用户现在说‘网站挂了急’。那么我应该生成一个包含title和priority的 JSONtitle填‘网站故障’priority填‘高’。”关键在于这个“应该”是基于其训练数据中无数类似模式代码注释、API文档、问答对的概率推断而非逻辑执行。因此它的“遵守”是软性的、启发式的。2.2 Tool Use 的“三重不保证”边界基于上述本质我们可以划出三条清晰的、LLM 无法绝对保证的边界边界一格式的绝对正确性不保证LLM 生成的是文本序列。虽然它“知道”JSON 应该用双引号但字符编码问题可能输出全角引号、中文引号或其他 Unicode 引号变体。转义错误如果用户输入或它自己要生成的值里包含引号或换行符\n它可能忘记转义\\n导致 JSON 无效。尾随逗号在 JSON 数组或对象的最后一项后加上逗号这在 JavaScript 中允许但在严格 JSON 解析器中是错误。实操心得永远不要相信 LLM 输出的原始文本是语法完美的 JSON。必须经过一个健壮的 JSON 解析器或带容错机制的解析器的校验和清洗这是铁律。边界二对 Schema 的严格遵从性不保证这是最核心的边界。LLM 可能会添加未定义字段如开篇案例它认为tags有用就加上了。这在它看来是“让数据更完整”而非“违反契约”。忽略必填字段尤其当用户输入信息模糊时LLM 可能无法推断出某个必填字段的值于是选择不输出该字段或者输出null如果 Schema 未明确禁止null。误解字段类型或约束例如将数字123输出为字符串123或者当enum为[high, medium, low]时输出中文的“高”。输出完全无关的内容在极端情况或模型困惑时它可能输出一段解释性文字如“我将为您创建工单...”而非 JSON 对象。边界三逻辑一致性与业务规则不保证Schema 定义了结构但无法定义业务逻辑。LLM 无法保证值之间的逻辑关系例如一个start_date和一个end_dateLLM 可能输出start_date晚于end_date。符合业务常识priority字段填“高”但title是“咨询办公时间”这种内容与优先级的不匹配LLM 无法判断。对抗性输入的处理用户故意输入矛盾、模糊或诱导性信息LLM 生成的调用可能是不合理甚至有害的。理解这三条边界我们就从“期望 LLM 完美执行工具调用”的幻觉中走了出来转而思考如何在这个不完美的、概率性的系统之上构建确定性的、可靠的应用答案在于将 LLM 视为一个优秀的、但需要严格质检的“草案撰写员”而我们系统则是最终的“审核与执行官”。3. 构建防线从 Schema 设计到后处理的全链路加固要让 LLM 的 Tool Use 稳定可靠不能只靠“更好的提示词”必须建立一个从输入到输出的完整控制链路。下面这个表格概括了核心防线及其目标防线层级核心目标关键措施第一层Schema 设计防线最大化引导最小化歧义精细化 Schema 描述使用enum 添加strict模式提示第二层调用提示Prompt防线明确指令约束行为在 System Prompt 和 Few-shot 示例中强调格式与规则第三层输出解析与校验防线确保语法与结构合规使用带容错的 JSON 解析库验证 against Schema第四层业务逻辑校验防线确保数据符合业务规则编写独立的业务规则校验逻辑第五层异常处理与重试防线保障流程最终成功设计降级策略如让用户确认、安全重试机制3.1 第一层防线设计“防呆”的 SchemaSchema 是你的第一道也是最重要的指令。设计时要假设 LLM 是个粗心的新手。1. 描述description字段要极度精确不要只写“工单标题”。要写“用一句话简要概括问题的核心例如‘支付页面信用卡输入框无法点击’或‘产品详情页图片显示错位’。不要包含日期、人名或具体错误代码。”2. 善用enum枚举类型这是约束 LLM 离散值选择最强大的武器。将priority定义为enum: [high, medium, low]比type: string可靠得多。LLM 从有限集合中选一个比自由生成一个字符串犯错的概率低几个数量级。3. 利用strict模式提示如果框架支持一些 LLM 应用框架如 LangChain 的 Tool Calling 或 OpenAI 的function_call支持更严格的模式。虽然底层仍是提示词但可以在 Schema 中通过字段名或描述暗示。例如在参数描述开头加上[必须严格遵循禁止添加任何未定义的字段]。4. 结构化复杂参数对于复杂对象不要用一个庞大的字符串字段如description来装所有信息。将其拆分为多个结构化的字段。例如将用户反馈拆分为problem_phenomenon现象、reproduction_steps复现步骤、expected_behavior期望行为。这既方便 LLM 提取也便于后续处理。// 不佳的设计 { description: type: string } // 更好的设计 { problem_phenomenon: { type: string, description: 具体发生了什么问题例如‘点击提交按钮无反应’ }, reproduction_steps: { type: string, description: 请列出重现此问题的步骤用数字编号。 } }3.2 第二层防线编写强约束的调用提示System Prompt 和 Few-shot 示例是直接与 LLM 对话的渠道。在 System Prompt 中明确指令你是一个精确的工具调用助手。你的任务是根据用户请求和提供的工具定义生成严格符合工具参数要求的 JSON 对象。 关键规则 1. 输出必须是**纯粹的、有效的 JSON 对象**且仅包含工具定义中声明的字段。 2. **绝对禁止**添加任何注释、解释性文字或未在工具定义中出现的字段。 3. 确保字符串使用标准的双引号而非中文或全角引号。 4. 如果用户输入中缺少工具所需的必要信息请将对应字段的值设为 null而不是省略该字段或自行猜测。 请严格遵循以上规则。提供高质量的 Few-shot 示例示例是最直观的教学。提供 2-3 个正例和 1 个反例。用户网站首页图片加载不出来。 工具create_ticket 正确输出{title: 首页图片无法加载, priority: high, description: 用户报告网站首页的横幅图片无法显示。} 用户我觉得这个按钮颜色可以再亮一点。 工具create_ticket 正确输出{title: UI颜色优化建议, priority: low, description: 用户对按钮颜色提出了优化建议。} 用户错误报错代码500赶紧查一下服务器 工具create_ticket 错误输出不要这样{title: 服务器500错误, priority: 高, description: 用户遇到500错误。, urgency: immediate} // 错误添加了未定义的字段urgency且priority值未使用英文枚举。3.3 第三层防线实施严格的输出解析与校验这是将概率性输出转化为确定性数据的关键步骤。永远不要尝试用字符串处理或正则表达式来解析 LLM 的 JSON 输出。1. 使用健壮的 JSON 解析库在 Python 中优先使用json.loads()但必须将其包裹在try-except块中。对于包含尾随逗号等非严格 JSON可以考虑使用demjson3或json5这类更宽松的库进行首次解析然后再用标准库处理。import json import re def safe_json_parse(llm_output_text: str): 尝试解析 LLM 输出的文本为 JSON。 包含简单的预处理如替换全角引号。 # 预处理替换常见的中文/全角引号为半角双引号 text llm_output_text.strip() text re.sub(r[“”], , text) # 替换中文双引号 # 尝试解析 try: data json.loads(text) return {success: True, data: data} except json.JSONDecodeError as e: # 如果标准解析失败可以尝试更宽松的解析器或进行更复杂的清洗 # 例如查找文本中第一个 { 和最后一个 } 之间的内容 start text.find({) end text.rfind(}) 1 if start ! -1 and end ! 0: potential_json text[start:end] try: data json.loads(potential_json) return {success: True, data: data, note: extracted from text} except json.JSONDecodeError: pass return {success: False, error: str(e), raw_text: text}2. 进行 Schema 合规性验证解析出 JSON 对象后必须验证其是否符合你定义的 Schema。可以使用jsonschema库。from jsonschema import validate, ValidationError # 你的工具 Schema TICKET_SCHEMA { type: object, properties: { title: {type: string}, priority: {type: string, enum: [high, medium, low]}, description: {type: string} }, required: [title, priority], additionalProperties: False # 关键禁止额外字段 } def validate_against_schema(parsed_data: dict, schema: dict): 验证数据是否符合 JSON Schema。 try: validate(instanceparsed_data, schemaschema) return {valid: True} except ValidationError as e: return {valid: False, error_path: list(e.path), error_message: e.message}additionalProperties: False是杀手锏。它告诉校验器除了properties里定义的字段其他字段一律不允许。这样LLM 私自添加的tags字段会在这一步被果断拒绝。3.4 第四层防线执行业务逻辑校验即使 JSON 格式和 Schema 都通过了数据也可能在业务逻辑上不合理。这需要你编写独立的校验函数。def business_logic_validation(ticket_data: dict): errors [] # 示例1优先级与标题关键词的粗略校验 high_priority_keywords [宕机, 紧急, 无法访问, 严重错误, urgent, down] title ticket_data.get(title, ).lower() priority ticket_data.get(priority) if priority low and any(keyword in title for keyword in high_priority_keywords): errors.append(标题包含高优先级关键词但优先级被设置为‘低’请确认。) # 示例2描述长度过短 if len(ticket_data.get(description, ).strip()) 5: errors.append(问题描述过短可能无法有效处理。) return errors3.5 第五层防线设计优雅的降级与重试当任何一层防线失效时系统不能直接崩溃。1. 降级策略如果 LLM 多次无法生成有效调用可以降级为让用户确认或手动填写。例如将 LLM 生成的“草案”即使有错误和解析出的字段以表单形式展示给用户“我理解您想创建工单请确认以下信息标题[LLM生成的标题可编辑] 优先级[下拉框默认LLM选择]”。这既利用了 LLM 的预处理能力又把最终控制权交给了用户。2. 安全重试机制当解析或校验失败时不要简单地将原始错误扔回给 LLM。这可能导致它陷入死循环。更好的做法是构造一个清晰的错误提示引导它修正。def retry_with_feedback(llm_client, original_user_input, tool_schema, previous_error): 根据之前的错误构造新的提示词让 LLM 重试。 feedback_prompt f 之前尝试调用工具时出错了。 错误信息{previous_error} 请严格根据以下工具定义重新分析用户的请求并输出正确的 JSON。 用户请求{original_user_input} 工具定义{json.dumps(tool_schema, ensure_asciiFalse)} 记住输出必须是纯JSON且只包含定义中的字段。 # 再次调用 LLM new_response llm_client.chat(feedback_prompt) return new_response通常经过 1-2 次有针对性的重试LLM 都能输出正确的格式。设置一个重试上限如3次超过则转入降级流程。4. 实战案例打造一个“格式永不炸”的工单创建流程让我们将上述所有防线整合到一个具体的、可部署的流程中。假设我们使用 OpenAI GPT-4 和 FastAPI 后端。步骤 1: 定义坚如磐石的 Tool Schema# schemas.py CREATE_TICKET_SCHEMA { type: object, properties: { title: { type: string, description: 工单的简短核心标题例如‘支付失败’、‘页面布局错乱’。避免使用‘关于...的问题’这类宽泛表述。 }, priority: { type: string, enum: [high, medium, low], description: 工单紧急程度。high: 系统不可用核心功能故障。medium: 功能瑕疵影响体验。low: 优化建议、非关键问题。 }, description: { type: string, description: 问题的详细描述。包括现象、发生环境如浏览器、手机型号、复现步骤。 }, category: { type: string, enum: [bug, feature_request, consultation, other], description: 问题类型。 } }, required: [title, priority, category], additionalProperties: False # 锁死禁止任何额外字段 }步骤 2: 构造系统提示词与 Few-shot 示例# prompts.py SYSTEM_PROMPT 你是一个工单系统助手。你的唯一任务是将用户的自然语言描述转化为创建工单所需的精确结构化数据。 你必须遵守以下铁律 1. 输出必须是**一个且仅一个**有效的 JSON 对象。 2. 该 JSON 对象必须且只能包含以下字段title, priority, description, category。字段名称必须完全一致。 3. 字段值必须严格遵守定义priority 只能是 high/medium/lowcategory 只能是 bug/feature_request/consultation/other。 4. 如果用户输入中无法推断出某个必填字段title, priority, category的值将该字段设为 null。 5. 不要输出任何其他文字、解释、Markdown 代码块标记。只输出 JSON。 FEW_SHOT_EXAMPLES [ { user: 网站完全打不开了白屏, assistant: {title: 网站无法访问显示白屏, priority: high, description: 用户报告网站完全无法加载呈现白屏状态。, category: bug} }, { user: 我希望在报表里能加一个导出为 PDF 的功能。, assistant: {title: 报表增加PDF导出功能, priority: low, description: 用户建议为报表模块添加PDF格式导出功能。, category: feature_request} } ]步骤 3: 实现核心处理管道# pipeline.py import openai from jsonschema import validate, ValidationError import json import re from typing import Optional, Dict, Any from .schemas import CREATE_TICKET_SCHEMA from .prompts import SYSTEM_PROMPT, FEW_SHOT_EXAMPLES class TicketCreationPipeline: def __init__(self, llm_client): self.llm_client llm_client self.max_retries 2 def _call_llm(self, user_input: str) - str: 调用 LLM构造包含示例的提示词。 messages [ {role: system, content: SYSTEM_PROMPT}, *[{role: user, content: ex[user], role: assistant, content: ex[assistant]} for ex in FEW_SHOT_EXAMPLES], {role: user, content: user_input} ] response self.llm_client.chat.completions.create( modelgpt-4, messagesmessages, temperature0.1, # 低温度减少随机性 max_tokens500 ) return response.choices[0].message.content.strip() def _parse_and_validate(self, llm_raw_text: str) - Dict[str, Any]: 解析并验证 LLM 输出。 # 1. 预处理清理引号提取潜在 JSON text llm_raw_text.strip() text re.sub(r[“”], , text) text re.sub(rjson\n?|\n?, , text) # 移除可能的 Markdown 代码块标记 # 尝试直接解析 parsed_data None try: parsed_data json.loads(text) except json.JSONDecodeError: # 尝试提取花括号内的内容 match re.search(r\{.*\}, text, re.DOTALL) if match: try: parsed_data json.loads(match.group()) except json.JSONDecodeError: raise ValueError(f无法从文本中解析出有效 JSON。原始文本{text[:200]}...) else: raise ValueError(f输出中未找到疑似 JSON 的结构。原始文本{text[:200]}...) # 2. Schema 验证 try: validate(instanceparsed_data, schemaCREATE_TICKET_SCHEMA) except ValidationError as e: raise ValueError(f数据不符合 Schema 规范{e.message}路径{list(e.path)}) # 3. 基本业务逻辑校验 if parsed_data.get(priority) high and parsed_data.get(category) feature_request: # 高优先级通常是 bug而不是功能请求。记录警告不一定阻止。 # 在实际系统中可以记录日志或触发人工审核。 pass return parsed_data def process(self, user_input: str) - Dict[str, Any]: 主处理流程包含重试机制。 last_error None for attempt in range(self.max_retries 1): try: if attempt 0: llm_output self._call_llm(user_input) else: # 重试时将错误信息反馈给 LLM retry_prompt f上次调用出错了。错误信息{last_error} 请严格遵循指令重新处理用户的请求。 用户请求{user_input} 记住只输出 JSON且字段必须完全匹配。 llm_output self._call_llm(retry_prompt) validated_data self._parse_and_validate(llm_output) # 所有校验通过返回成功数据 return {status: success, data: validated_data} except (ValueError, json.JSONDecodeError, ValidationError) as e: last_error str(e) if attempt self.max_retries: # 重试次数用尽返回错误并建议降级 return { status: error, error: f经过{self.max_retries 1}次尝试仍无法生成有效工单数据。, suggestion: 建议转为人工表单填写。, last_llm_output: llm_output if llm_output in locals() else None, last_error: last_error } # 否则继续重试 continue步骤 4: 集成到 API 端点# main.py (FastAPI 示例) from fastapi import FastAPI, HTTPException from pydantic import BaseModel from .pipeline import TicketCreationPipeline import openai app FastAPI() llm_client openai.Client(api_keyyour-api-key) pipeline TicketCreationPipeline(llm_client) class UserRequest(BaseModel): query: str app.post(/api/create-ticket-draft) async def create_ticket_draft(request: UserRequest): result pipeline.process(request.query) if result[status] success: # 这里可以将 validated_data 存入数据库或进入下一步流程 return {message: 工单草稿生成成功, ticket_data: result[data]} else: # 对于错误可以返回结构化错误信息前端引导用户手动填写 raise HTTPException( status_code422, # Unprocessable Entity detail{ message: 无法自动创建工单, reason: result[error], suggestion: result[suggestion] } )这个流程将“格式炸了”的风险降到了最低。即使 LLM 第一次输出有问题重试机制和严格的校验也能极大提高最终成功率。对于最终仍无法处理的情况我们有明确的降级路径保证了用户体验和系统鲁棒性。5. 进阶思考超越格式Tool Use 的可靠性设计哲学当我们解决了格式问题更深层的问题是如何让 Tool Use 在复杂、多步的 Agent 工作流中保持可靠这涉及到系统设计哲学。1. 工具设计的原子性与幂等性每个工具的功能应该尽可能原子化、单一。create_ticket就只创建工单不要让它同时去发送邮件通知。这样 LLM 需要调用的意图更清晰Schema 也更简单。同时工具应尽可能设计成幂等的即多次调用同一参数产生相同效果这为安全重试提供了基础。2. 状态管理外置LLM 专注决策不要让 LLM 记忆复杂的中间状态。例如在一个多轮对话中收集用户信息来创建工单应该由外部系统如 Session来维护已收集的字段title,priority...每次 LLM 只需要根据当前用户输入判断是补充某个字段还是确认调用工具。这大大降低了 LLM 的认知负荷和出错概率。3. 采用“验证-执行”分离模式这是最稳健的模式。LLM 不直接调用具有副作用的工具如创建数据库记录、发送邮件。而是让它调用一个validate_and_propose_ticket工具该工具只返回一个经过校验的、结构化的工单提案。然后由后端系统或经过用户确认后再调用真正的create_ticket工具。这样LLM 的不可靠性被隔离在无副作用的验证阶段。4. 拥抱“人机协同”对于关键任务如创建高优先级工单、执行资金操作不要追求全自动化。设计流程让 LLM 生成草案然后必须由用户点击确认或修改后再执行。LLM 是强大的副驾驶但方向盘和刹车必须牢牢掌握在人类或确定性的系统逻辑手中。回到最初的问题Tool Use 到底能保证什么它保证的是在一个精心设计的、多层防御的系统中LLM 能够以较高的成功率将模糊的自然语言意图转化为一个可供后续确定性系统处理的、初步的结构化数据草案。它不能保证 100% 的格式正确不能保证 100% 的逻辑合规更不能替代系统的最终校验与执行。搞懂了这条边界我们就不会再把 LLM 当做一个可靠的程序员而是把它当做一个极具潜力但需要严格督导的实习生。我们的工作就是为这位“实习生”设计清晰无误的工作说明书Schema建立标准的操作流程Prompt配备自动化的校对工具解析与校验并设置关键的审核节点业务校验与人工确认。如此你的 LLM 应用才能从“动不动就炸”的玩具蜕变为真正可用的生产级工具。