当前位置: 首页 > news >正文

LLM结构化输出工程:让模型输出你真正需要的格式

LLM的输出天然是自由形式的文本但AI应用需要的往往是结构化数据。这中间的gap就是结构化输出工程要解决的问题。2026年这个问题有了更成熟的解法。本文系统梳理从基础到进阶的结构化输出技术栈以及常见坑和对应的解决策略。## 为什么结构化输出难直觉上很简单告诉模型用JSON格式输出就行了。实际上没那么简单模型不总是遵守。在复杂推理场景下模型倾向于先用自然语言思考然后顺手输出了文字而不是JSON。格式正确但语义错误。JSON格式对了但字段值是模型编的——比如要求输出置信度0-1之间的数字模型输出了0.99999看起来没问题其实是没有根据的。边界情况导致解析失败。字符串里包含未转义的特殊字符数组元素数量不对嵌套层级缺失这些都会让你的json.loads()抛异常。流式输出的挑战。流式响应下无法在输出完成前验证JSON完整性。## 方法一原生结构化输出首选OpenAI、Anthropic等主流模型现在都支持原生的结构化输出通过在API层面约束输出格式可靠性比提示词方法高得多。pythonfrom openai import AsyncOpenAIfrom pydantic import BaseModel, Fieldfrom typing import Literalclient AsyncOpenAI()# 定义输出结构class ArticleAnalysis(BaseModel): title: str Field(description文章标题) category: Literal[技术, 产品, 市场, 其他] Field(description文章分类) key_points: list[str] Field( description文章核心观点3-5条, min_length3, max_length5 ) sentiment: Literal[positive, neutral, negative] difficulty_level: int Field(ge1, le5, description阅读难度1-5) tags: list[str] Field(max_length5, description相关标签最多5个)async def analyze_article(article_text: str) - ArticleAnalysis: response await client.beta.chat.completions.parse( modelgpt-4o, messages[ { role: system, content: 你是一个内容分析专家请对文章进行结构化分析。 }, { role: user, content: f请分析以下文章\n\n{article_text} } ], response_formatArticleAnalysis, ) return response.choices[0].message.parsedresponse.choices[0].message.parsed直接返回一个Pydantic对象不需要手动解析JSONPydantic的验证在模型输出阶段就已经完成。### 处理refused情况模型有时会因为内容安全原因拒绝输出结构化结果pythonasync def safe_analyze(article_text: str) - ArticleAnalysis | None: response await client.beta.chat.completions.parse( model“gpt-4o”, messages[…], response_formatArticleAnalysis, ) message response.choices[0].message if message.refusal: logging.warning(fModel refused: {message.refusal}“) return None return message.parsed## 方法二Instructor库跨模型兼容如果你需要同时支持多个模型提供商Instructor是目前最好的抽象层pythonimport instructorfrom anthropic import AsyncAnthropicfrom openai import AsyncOpenAIfrom pydantic import BaseModel# 支持OpenAIopenai_client instructor.from_openai(AsyncOpenAI())# 支持Anthropicanthropic_client instructor.from_anthropic(AsyncAnthropic())# 支持本地模型通过Ollamaollama_client instructor.from_openai( AsyncOpenAI(base_url“http://localhost:11434/v1”, api_key“ollama”), modeinstructor.Mode.JSON # 本地模型用JSON mode)class UserProfile(BaseModel): name: str email: str role: Literal[“admin”, “user”, “viewer”] permissions: list[str]async def extract_user_profile(text: str, use_model: str “openai”) - UserProfile: client openai_client if use_model “openai” else anthropic_client return await client.chat.completions.create( model“gpt-4o” if use_model “openai” else “claude-3-5-sonnet-20241022”, response_modelUserProfile, messages[ {“role”: “user”, “content”: f从以下文本中提取用户信息\n{text}”} ], max_retries3 # Instructor自动处理格式错误重试 )Instructor的自动重试机制是个杀手锏——当模型输出不符合Pydantic Schema时它会自动把错误信息反馈给模型让模型修正最多重试N次。### 复杂嵌套结构pythonfrom pydantic import BaseModel, model_validatorclass Address(BaseModel): street: str city: str country: str “中国” postal_code: str | None Noneclass ContactInfo(BaseModel): phone: str | None None email: str | None None address: Address | None None model_validator(mode‘after’) def at_least_one_contact(self): if not self.phone and not self.email: raise ValueError(“至少需要提供手机号或邮箱中的一种”) return selfclass CustomerRecord(BaseModel): customer_id: str name: str contact: ContactInfo tags: list[str] [] created_at: str # ISO format model_validator(mode‘after’) def validate_customer_id(self): if not self.customer_id.startswith(“CUS-”): self.customer_id fCUS-{self.customer_id} return selfmodel_validator让你可以在Pydantic层面做跨字段的业务验证模型如果输出不符合的内容Instructor会自动触发重试。## 方法三输出解析器后处理方式有时候你无法控制模型的API参数比如用第三方代理只能对原始文本做后处理pythonimport jsonimport refrom typing import TypeVar, TypeT TypeVar(T, boundBaseModel)class RobustOutputParser: 鲁棒的输出解析器处理各种格式问题 def parse(self, text: str, model_class: Type[T]) - T | None: # 尝试方法1直接解析 try: return model_class.model_validate_json(text) except Exception: pass # 尝试方法2提取JSON块 json_block self._extract_json_block(text) if json_block: try: return model_class.model_validate_json(json_block) except Exception: pass # 尝试方法3修复常见的JSON错误 fixed_json self._fix_json(text) if fixed_json: try: return model_class.model_validate_json(fixed_json) except Exception: pass return None def _extract_json_block(self, text: str) - str | None: 提取markdown代码块中的JSON patterns [ rjson\s*(.?)\s, #json …r\s*([[{].?[]}])\s, #{ … }r([\[{].*[\]}]), # 直接找JSON结构贪婪 ] for pattern in patterns: match re.search(pattern, text, re.DOTALL) if match: return match.group(1) return None def _fix_json(self, text: str) - str | None: 修复常见JSON格式错误 # 提取JSON部分 json_start text.find({) json_end text.rfind(}) 1 if json_start -1 or json_end 0: json_start text.find([) json_end text.rfind(]) 1 if json_start -1: return None json_text text[json_start:json_end] # 常见修复尾部逗号 json_text re.sub(r,\s*}, }, json_text) json_text re.sub(r,\s*], ], json_text) # 单引号改双引号Python习惯 # 注意这个替换不完美只做简单处理 # json_text json_text.replace(, ) return json_text## 流式结构化输出流式场景下你需要在输出完整之前就开始处理数据pythonimport instructorfrom openai import AsyncOpenAIclient instructor.from_openai(AsyncOpenAI())async def stream_analysis(text: str): 流式获取结构化输出适合前端实时展示 async with client.chat.completions.stream( modelgpt-4o, response_modelArticleAnalysis, messages[ {role: user, content: f分析文章\n{text}} ] ) as stream: # 实时获取部分完成的对象 async for partial in stream.partial: # partial是一个部分填充的ArticleAnalysis对象 # 字段值可能是None还没生成到 if partial.title: yield {field: title, value: partial.title} if partial.key_points: yield {field: key_points, value: partial.key_points} # 获取最终完整对象 final await stream.get_final_completion() yield {field: complete, value: final.model_dump()}## 结构设计的最佳实践### 让Schema对LLM友好python# 不友好的Schemaclass BadSchema(BaseModel): d: str # 缩写字段名模型不知道是什么 v: int f: list # 太模糊# 友好的Schemaclass GoodSchema(BaseModel): description: str Field(description对象的详细描述100字以内) confidence_score: int Field( ge0, le100, description置信度分数0-100整数100表示完全确定 ) features: list[str] Field( description对象的特征列表每条特征用一句话描述, max_length10 )字段名要语义清晰description要明确说明格式要求这些信息会被注入到模型的上下文中直接影响输出质量。### 用枚举约束取值范围pythonfrom enum import Enumclass Priority(str, Enum): LOW low MEDIUM medium HIGH high CRITICAL criticalclass TaskItem(BaseModel): title: str priority: Priority # 模型只能输出这四个值之一 status: Literal[todo, in_progress, done, blocked]### 分阶段输出复杂结构对于非常复杂的输出可以分两步先输出骨架再填充细节pythonclass DocumentOutline(BaseModel): title: str sections: list[str] # 先只要章节标题class DocumentSection(BaseModel): title: str content: str # 再逐节生成内容 word_count: intasync def generate_long_document(topic: str) - list[DocumentSection]: # 第一步生成大纲 outline await client.chat.completions.create( modelgpt-4o, response_modelDocumentOutline, messages[{role: user, content: f为「{topic}」生成一个5节的文档大纲}] ) # 第二步逐节生成内容 sections [] for section_title in outline.sections: section await client.chat.completions.create( modelgpt-4o, response_modelDocumentSection, messages[{ role: user, content: f为文章「{outline.title}」写第「{section_title}」节约300字 }] ) sections.append(section) return sections## 生产环境监控结构化输出在生产环境需要监控两个关键指标解析成功率你的结构化输出有多少次真正返回了有效的对象。低于95%就需要排查。字段填充完整率哪些字段经常是None或空值这可能说明Schema设计有问题或者模型对某类信息的理解不够。pythonclass StructuredOutputMonitor: def record(self, schema_name: str, result, success: bool): metrics.increment(fstructured_output.{schema_name}.total) if success: metrics.increment(fstructured_output.{schema_name}.success) # 记录字段填充情况 if result: for field, value in result.model_dump().items(): if value is not None and value ! [] and value ! : metrics.increment( fstructured_output.{schema_name}.field.{field}.filled ) else: metrics.increment(fstructured_output.{schema_name}.failed)—本文关键词结构化输出、Pydantic、Instructor、JSON Schema、LLM工程化
http://www.gsyq.cn/news/1335730.html

相关文章:

  • MobileNetV2肺癌病理图像分类|全网独家实战,MSA注意力改进篇 引入MSA多尺度注意力,强化病理特征提取、助力微小病灶识别、病理切片分类、临床辅助诊断有效涨点
  • CAPEv2 沙箱安装部署
  • 一多 OS 的技术闭环彻底打通
  • 鸿蒙动态信息流与健康档案模块:声明式列表与网格的深度融合
  • AI产品经理入门实战:如何理解数字人驱动?
  • 百万级 MySQL 大表导入前,别让这两个默认参数拖垮性能_2026-05-20
  • COMSOL电磁超声仿真避坑指南:从‘域不适用’报错到结果收敛的完整调试流程
  • 无人机算法之第四章 ArduPilot 主要配置参数及效果
  • GNSS模块教程:大夏龙雀 DX-GP21,从硬件接线到 NMEA 数据解析
  • [具身智能-824]:人的大脑,如何实现高实时、多模态联合、发现表象背后的各种规律和层层叠叠的不同层次的语义的?
  • 【C++】类和对象( 类的定义、实例化、 this指针、 C++和C语言实现Stack对比)
  • 电脑截图工具深度测评:PixPin、Snipaste、兔灵截图(Utools插件)
  • ⚡ 淘汰你的不是 AI,而是会用 AI 的同行
  • 8 张 RTX 5090 跑 Qwen3.6-27B:从装 vLLM 到压测调优的真实数据(含完整脚本)
  • 全面详解 bgfx
  • 别再乱改Rime配置了!先搞懂程序文件夹和用户文件夹的区别(Windows/Ubuntu路径详解)
  • Cursor试用限制终极解决方案:3分钟快速重置设备标识实战指南
  • 无磁钻具:市场发展现状与未来前景趋势
  • FPGA管脚不够用?手把手教你用74HC595级联驱动8位数码管(附Verilog代码与仿真)
  • 测试经理为保障项目按期交付,主动规划核心内容
  • YimMenu:GTA5终极防护与增强完整指南
  • 保姆级教程:在S32G274ARDB2上,用IPCF点亮RGB LED(附源码解析)
  • cp520靶场学习笔记
  • 手把手教你用ProgISP和USBASP,给老旧Arduino开发板‘续命’升级Bootloader
  • 数据库一体机简史:谁为数据仓库正名?
  • 企业级RAG系统数据可信生死线:Perplexity验证功能内测权限仅剩最后17个——附白名单申请通道
  • 射灯轨道灯怎么选?看完这篇不花冤枉钱! 这几家射灯轨道灯公司靠谱吗?老师傅偷偷告诉你! 装修小白必看:射灯轨道灯避坑指南,这家公司口碑最好!
  • 多模态协作:文本、图像、语音Agent配合
  • 答辩ppt模板资源合集
  • 乐山汽车低趴改装技术全解析:乐山汽车灯光改装/乐山汽车电器维修/乐山汽车维修保养/靠谱品牌筛选推荐 - 优质品牌商家