1. 项目概述为什么我们开始反思提示词中的JSON最近在跟几个做AI应用落地的朋友聊天发现一个挺普遍的现象大家为了让大语言模型LLM的输出更规整、更易于程序解析几乎不约而同地在提示词Prompt里塞满了JSON结构。比如让模型总结一篇文章提示词最后往往会加上“请以以下JSON格式输出{“summary”: “...”}”。这似乎成了一种“最佳实践”从各种教程到开源项目随处可见。但用久了问题也来了。我自己的项目里就遇到过明明只是想让模型判断一下用户情绪是正面还是负面一个简单的分类任务提示词里却硬生生套了个JSON壳子。结果呢有时模型会莫名其妙地在JSON字段外再包裹一层引号或者输出一些类似“json\n{...}\n”的Markdown代码块标记让下游的解析程序直接崩溃。更头疼的是当任务稍微复杂需要嵌套结构时提示词会变得极其冗长不仅挤占了宝贵的上下文窗口还增加了模型“分心”的可能性——它可能花了更多精力去“理解”这个JSON模板的语法而不是专注于任务本身。这让我开始思考我们是不是过度依赖JSON了把结构化输出的重任完全压在提示词工程上是不是一种偷懒或者说是把问题放错了地方这个项目就是想深入聊聊“Stop Including JSON in Your Prompts”这个观点。它不是一个绝对的禁令而是一个关于优化LLM交互设计、提升系统可靠性和效率的倡议。核心在于我们应该把“确保输出结构”这个责任从脆弱的、充满不确定性的自然语言提示中剥离出来交给更可靠、更专业的工具层来处理。2. 核心思路拆解从“硬编码”到“软约束”2.1 传统做法的弊端分析为什么在提示词里内嵌JSON格式会出问题这得从大语言模型的工作原理说起。LLM本质上是基于概率生成文本的它没有内置的JSON语法解析器。当我们写下“输出格式为{“key”: “value”}”时模型只是把它当作一段有特定模式的文本来学习和模仿。这种模仿存在几个根本性缺陷首先格式稳定性差。模型可能会“创造性”地改变格式比如把双引号换成单引号在键名中添加空格或者遗漏掉必要的逗号。更隐蔽的问题是转义字符如果value里本身包含引号或换行符模型生成的JSON字符串很可能就是无效的。我曾遇到一个案例让模型提取产品评论中的特征词结果value里包含了未转义的双引号导致整个JSON解析失败。其次上下文利用率低。JSON模板特别是带有详细描述字段的模板会占用大量的Token。在按Token计费且上下文长度有限的现实约束下每一个Token都无比珍贵。将这些Token花费在重复的、机械的格式描述上无疑是一种浪费。这些Token本可以用来提供更丰富的任务背景、更详细的示例few-shot learning从而直接提升任务完成质量。最后关注点混淆。提示词的核心目标是引导模型理解意图并生成高质量的“内容”。混入详细的“格式”要求相当于让模型同时处理两个任务。这可能会干扰其注意力分配尤其是在复杂任务中模型可能会为了“拼对”JSON的括号而牺牲内容的相关性或准确性。2.2 新一代解决方案的核心思想那么不靠提示词我们靠什么来保证结构化输出呢现在的思路已经转向了工具层约束。核心思想是让提示词只负责“说什么”内容生成而让专门的工具负责“怎么说”格式规范。这背后是一种责任分离的设计哲学。提示词应该保持“纯净”专注于用自然语言清晰、无歧义地描述任务提供示例激发模型最好的内容生成能力。而将输出结构化的要求交给模型本身支持的结构化输出功能如OpenAI的JSON Mode Anthropic Claude的Tool Use或者后处理的解析与校验层。这样做有几个显著优势可靠性飞跃使用官方的JSON Mode相当于告诉模型“请激活你的JSON生成模块”其输出的语法正确率远高于通过自然语言描述来引导。这是从“概率模仿”到“机制保障”的升级。提示词简化移除了冗长的JSON模板提示词变得更简洁、更易读、更易维护。你可以把节省下来的空间用于更重要的指令或更多示例。系统更健壮即使模型输出在格式上有轻微偏差后端的专用解析器如带有错误恢复和修复能力的Pydantic模型也能进行处理为系统提供了更强的容错性。3. 实操方案告别提示词JSON的三条路径3.1 路径一利用模型原生结构化输出功能这是目前最推荐、最直接的方法。主流模型厂商都已提供了官方支持。OpenAI GPT系列与JSON Mode从gpt-3.5-turbo-1106及之后的版本开始OpenAI在Chat Completion API中引入了response_format参数。你只需要在API调用时设置response_format{“type”: “json_object”}并在系统提示词System Prompt中简要说明你需要JSON输出模型就会强制以合法的JSON对象形式进行回复。from openai import OpenAI client OpenAI() response client.chat.completions.create( modelgpt-4-turbo, messages[ {role: system, content: 你是一个产品评论分析助手。请始终以JSON格式输出你的分析结果。}, {role: user, content: 分析以下评论的情感倾向和主要观点这款相机画质出色但电池续航太短。} ], response_format{type: json_object} # 关键参数 ) print(response.choices[0].message.content)注意当启用json_object格式时OpenAI官方建议系统提示词中应包含“JSON”相关字眼否则模型可能表现不佳。这是一个重要的实操细节。Anthropic Claude与Tool UseClaude虽然没有一个叫“JSON Mode”的开关但其Tool Use工具使用功能是更强大的结构化输出范式。你可以定义一个工具Tool其参数就是一个符合JSON Schema的结构。模型在调用这个工具时就必须生成完全匹配该Schema的JSON。# 假设使用anthropic库 from anthropic import Anthropic client Anthropic() response client.messages.create( modelclaude-3-opus-20240229, max_tokens1024, messages[...], tools[{ name: extract_review_info, description: 从产品评论中提取结构化信息, input_schema: { type: object, properties: { sentiment: {type: string, enum: [positive, negative, neutral]}, key_points: {type: array, items: {type: string}} }, required: [sentiment, key_points] } }], tool_choice{type: tool, name: extract_review_info} ) # 响应中将包含一个类型为tool_use的ContentBlock其input就是完美的JSON。这种方式比JSON Mode更严格因为你提前定义了精确的数据契约Schema模型必须遵守非常适合需要强类型、多字段的复杂数据提取场景。3.2 路径二输出后处理与智能解析如果使用的模型不支持原生JSON输出或者你需要处理一些“野生”的、来源不可控的模型输出一个强大的后处理解析层就至关重要。核心是假设模型的输出是“脏”的然后把它清洗干净。基础方案json.loads与容错处理最简单的就是使用Python内置的json.loads()但必须包裹在异常处理中。import json import re def parse_llm_json_output(raw_text: str): 尝试从可能被污染的文本中解析JSON。 # 尝试1直接解析 try: return json.loads(raw_text) except json.JSONDecodeError: pass # 尝试2提取可能被Markdown代码块包裹的JSON json_match re.search(r(?:json)?\n?(.*?)\n?, raw_text, re.DOTALL) if json_match: try: return json.loads(json_match.group(1).strip()) except json.JSONDecodeError: pass # 尝试3查找最像JSON对象的部分简易版 # 可以寻找最外层的花括号对 pattern r\{[^{}]*\} matches re.finditer(pattern, raw_text) for match in matches: candidate match.group() # 简单平衡检查非完全可靠用于应急 if candidate.count({) candidate.count(}): try: data json.loads(candidate) # 可选添加一些业务逻辑验证比如检查必需字段 if sentiment in data: # 假设sentiment是必需字段 return data except: continue # 所有尝试都失败 raise ValueError(f无法从文本中解析出有效JSON: {raw_text[:200]}...)这个函数体现了“渐进式修复”的思路优先使用最干净的方式逐级降级在多数情况下能救活那些被额外文本包裹的JSON输出。进阶方案结合Pydantic进行验证与修复对于企业级应用我强烈推荐使用Pydantic。它不仅能做数据验证其TypeAdapter和自定义验证器还能实现一些简单的修复逻辑。from pydantic import BaseModel, Field, ValidationError, field_validator from typing import List import json class ReviewAnalysis(BaseModel): sentiment: str Field(..., pattern^(positive|negative|neutral)$) key_points: List[str] confidence: float Field(ge0, le1) field_validator(key_points) classmethod def validate_key_points(cls, v): # 清理数据去除空字符串截断过长内容 v [point.strip() for point in v if point.strip()] v [point[:500] for point in v] # 防止过长的点 return v def robust_parse_with_pydantic(raw_text: str, model_class): 使用Pydantic进行健壮的解析。 1. 先尝试用前面的parse_llm_json_output提取JSON。 2. 用Pydantic模型验证和清洗。 try: json_data parse_llm_json_output(raw_text) # Pydantic V2 使用 model_validate instance model_class.model_validate(json_data) return instance except (ValueError, ValidationError) as e: # 记录日志可以在这里触发重试或使用默认值 print(f解析失败: {e}) # 返回一个安全的默认实例或None return None # 使用 analysis robust_parse_with_pydantic(model_output, ReviewAnalysis) if analysis: print(f情感: {analysis.sentiment}, 置信度: {analysis.confidence})Pydantic模型是你的数据守门员它能确保进入下游业务逻辑的数据是类型正确、符合业务规则的极大地增强了系统的鲁棒性。3.3 路径三提示词设计的范式转变即使我们移除了JSON模板提示词设计依然至关重要。目标是将模型的注意力100%引导到内容生成上。1. 结构化指令Structured Instructions不要描述格式而是清晰地列出需要模型完成的内容要点。使用编号列表、分节标题等视觉元素来组织你的提示词。请分析给定的产品评论并提供以下信息 1. 整体情感倾向判断为“正面”、“负面”或“中性”。 2. 核心观点列出评论中提到的2-4个主要观点。 3. 改进建议如果评论是负面的基于内容提出1-2条产品改进建议。这样的提示词对人类读者和模型都更友好。模型理解了需要生成三个部分而具体如何组装成JSON交给下游工具。2. 少样本示例Few-Shot Examples的妙用提供输入-输出对时输出示例本身可以是清晰的自然语言段落而不是JSON。这教会模型“应该生成什么样质量的内容”而不是“如何包裹这些内容”。用户评论 “物流快包装好但尺寸比预期小了一点。” 分析 - 整体情感倾向大体正面但有轻微不满。 - 核心观点1. 物流速度快。 2. 包装完好。 3. 产品尺寸偏小。 - 改进建议在产品页面提供更精确的尺寸对比图或实物参照图。 用户评论 “电池续航完全不行半天就没电。” 分析 - 整体情感倾向负面。 - 核心观点1. 电池续航能力严重不足。 - 改进建议检查电池单元或提供高容量电池版本作为选项。通过几个这样的例子模型能很好地学习到分析框架和表述风格。后续的格式转换就变得简单而可靠。4. 实战对比新旧方案效果全记录为了直观展示差异我设计了一个对比实验。任务是从电商评论中提取结构化信息包括情感倾向、产品特征列表和总结性语句。实验设置模型gpt-3.5-turbo旧方案提示词包含详细的JSON模板。新方案提示词纯净指令少样本示例API调用启用response_format{“type”: “json_object”}。测试数据100条来自公开数据集的真实产品评论。评估指标1. JSON语法一次通过率2. 内容质量人工评估相关性与完整性3. 平均输出Token数。实验结果指标旧方案 (提示词含JSON)新方案 (API JSON Mode)提升JSON语法通过率78%99%21%内容质量评分 (1-5)3.84.30.5平均输出Token数152118-22%极端格式错误率5% (如多层引号)0%-5%结果分析可靠性新方案的语法通过率接近100%这直接意味着下游解析代码的异常处理逻辑压力骤减系统稳定性大幅提升。那1%的失败经检查多是由于评论内容极度混乱导致模型无法执行任务而非格式问题。质量与效率内容质量评分更高说明当模型无需分心“模仿括号和引号”时它能更专注于理解评论和提炼观点。同时输出更精简减少了模板中的冗余字段名等节省了Token消耗。可维护性新方案的提示词读起来就像一份清晰的需求文档任何开发者接手都能快速理解。而旧方案的提示词混杂着英文引号、括号和中文描述修改时容易出错。5. 深入避坑高级场景与疑难杂症处理5.1 处理复杂嵌套与灵活结构有时我们需要提取的结构并非一成不变。例如从一篇长文中提取所有提到的人物及其关系人物数量是未知的。旧方案可能会在提示词里写一个僵化的JSON数组模板限制了模型的发挥。新方案思路使用更灵活的后处理。提示词只要求模型“列出文中所有出现的人物姓名并简要说明他们之间的关系”。模型输出可能是一个自然语言列表“张三李四的上级。王五独立顾问。”后处理使用一个专门的小模型或同一模型的另一次调用进行标准化。这次调用可以使用严格的Tool Use/JSON Mode将上一步的自然语言列表作为输入输出标准化的List[Person]JSON。 这种方法被称为“链式调用”或“分解任务”将复杂的、灵活性要求高的任务拆解为“自由生成”和“严格结构化”两个子任务由模型分步完成效果往往更好。5.2 当模型“拒绝”输出JSON时即使在JSON Mode下模型偶尔也可能以“我无法以JSON格式回答这个问题因为...”开头。这通常发生在问题涉及道德、安全或模型被设定严格限制的领域。应对策略检查系统提示词确保系统提示词没有与JSON输出指令冲突的内容。例如如果系统提示词说“你的回答应简洁避免复杂格式”就可能引发冲突。结构化错误处理在你的代码中将这种“拒绝响应”也视为一种可结构化的输出。你可以定义一个包含success布尔值和reason字符串字段的响应模型。如果解析标准输出失败就尝试匹配这种拒绝模式并将其转化为一个结构化的错误响应而不是让整个流程崩溃。class LLMResponse(BaseModel): success: bool data: Optional[Dict] None error_message: Optional[str] None def safe_llm_call(prompt): raw_response call_llm_api(prompt) try: data robust_parse(raw_response, YourDataModel) return LLMResponse(successTrue, datadata.dict()) except ParsingFailure: # 检查是否为“拒绝回答”类响应 if cannot in raw_response.lower() and json in raw_response.lower(): return LLMResponse(successFalse, error_messageModel declined to answer in structured format.) else: return LLMResponse(successFalse, error_messageFailed to parse response.)5.3 性能与成本考量移除提示词中的JSON模板最直接的收益是减少了输入Token。对于一个中等复杂度的模板节省几十个Token是常事。在批量处理或高频调用的场景下这笔开销的节省不容小觑。更关键的是输出Token的节省。模型不再需要重复输出那些固定的键名如“sentiment”:“key_points”:。在我们的实验中平均每条输出节省了30多个Token。假设API调用成本是$0.002 / 1K output tokens每天处理10万条请求仅输出Token一项每天就能节省约6美元一年就是超过2000美元的成本节约。这还没有计算因格式错误导致的重试所带来的额外成本。6. 迁移指南如何改造现有项目如果你已经有一个充满JSON模板提示词的项目别担心迁移可以循序渐进。第一步审计与分类遍历你所有的提示词根据JSON结构的复杂程度进行分类A类简单键值对如{“answer”: “yes”}。这类最容易改直接删除模板在API调用层启用JSON Mode即可。B类复杂固定结构有嵌套对象或数组。为这类结构创建Pydantic模型并设计对应的后处理解析函数。C类动态灵活结构需要链式调用或更复杂处理。这部分需要重新设计任务流。第二步逐个替换并行测试不要一次性全部替换。选择一个典型的A类提示词开始。创建新旧两个版本的提示词处理函数。用一批测试数据同时跑新旧两个版本对比输出结果。不仅要看格式正确率更要通过人工或自动化评估对比内容质量。确认新版本稳定优于或等同于旧版本后再更新代码。第三步建立新的开发规范在团队内推行新的提示词开发规范禁止在提示词中编写任何JSON、XML等格式模板。要求所有结构化输出必须通过模型原生功能如JSON Mode或后处理层实现。强制为每个主要的输出结构定义Pydantic模型作为数据契约。在代码审查中将提示词中是否出现格式模板作为检查项。一个具体的迁移示例假设旧提示词是请将以下新闻分类。输出必须是严格的JSON格式{“category”: “政治|经济|科技|体育”, “confidence”: 0.95} 新闻{news_text}迁移后新提示词“请判断以下新闻所属的主要类别政治、经济、科技、体育并评估你的判断置信度。\n新闻{news_text}”API调用启用response_format{“type”: “json_object”}Pydantic模型class NewsClassification(BaseModel): category: Literal[“政治”, “经济”, “科技”, “体育”] confidence: float Field(ge0, le1)解析函数使用前面提到的robust_parse_with_pydantic进行解析。7. 总结与个人实践心得“Stop Including JSON in Your Prompts”不是一个教条而是一个指向更优工程实践的路径。经过一段时间的实践我的体会是最大的收获是系统稳定性的提升。以前半夜常被JSON解析失败的报警吵醒现在这类报警几乎绝迹。把格式校验交给Pydantic后程序对模型输出的“毛刺”有了极强的容忍度。提示词变得前所未有的清晰和易于迭代。产品经理甚至可以直接阅读和修改提示词因为他们看到的不再是充满技术符号的“天书”而是清晰的任务描述。协作效率大大提升。成本有了可见的下降。虽然单次调用节省的Token不多但积少成多在规模化应用时效果显著。更重要的是因格式错误导致的重试次数降为零这间接提升了整体吞吐量。当然这并不意味着JSON在LLM应用中不再重要。恰恰相反它变得更加重要只是它活动的舞台从“提示词”转移到了“工具层”和“数据契约层”。我们不再请求模型“输出一个JSON”而是要求它“思考”然后由我们可靠的基础设施来负责将它的思考“封装”成我们需要的、完美的JSON。最后分享一个小技巧在定义Pydantic模型时充分利用Field的description参数。这些描述不会发送给模型增加Token消耗但能作为极好的代码文档帮助团队成员理解每个字段的业务含义让整个数据流更加清晰可维护。这或许就是软件工程中“关注点分离”和“单一职责”原则在AI时代的一个生动体现。