十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

结构化输出:从概念到实战,释放AI工程化潜力的关键技术

结构化输出:从概念到实战,释放AI工程化潜力的关键技术 1. 从“一团乱麻”到“井井有条”为什么我们需要结构化输出在技术开发、数据分析乃至日常工作中我们常常会遇到这样的场景你调用了一个API它返回了一长串文本里面混杂着日期、人名、地址、金额你需要写一堆正则表达式去“抠”出这些信息或者你让一个大语言模型帮你总结一份会议纪要它洋洋洒洒写了几百字但你却很难用程序自动提取出“决议事项”和“负责人”。这种“自由文本”的输出方式就像给你一袋混在一起的乐高积木你需要花大量时间手动分拣才能开始搭建你想要的模型。“结构化输出”要解决的正是这个“分拣”的痛点。它的核心思想是让机器输出的结果从一开始就是结构化的、机器可读的、符合预定格式的数据而不是需要二次解析的自然语言文本。简单来说就是让API、模型或任何程序“说人话”的同时更要“说机器能懂的话”。这不仅仅是JSON和XML格式的区别而是一种设计范式的转变——从输出“文档”转向输出“数据”。对于开发者而言结构化输出意味着下游处理逻辑的极大简化。想象一下一个天气API返回“{‘city’: ‘北京’ ‘temp’: 25 ‘condition’: ‘晴’}”你的程序可以直接用data[‘temp’]获取温度值如果它返回“北京今天晴气温25度”你就得动用自然语言处理工具准确性还无法保证。对于正在兴起的AI应用开发这一点尤为重要。大语言模型LLM的“思考”过程是黑箱的、非结构化的但它的“答案”往往需要被精准地嵌入到工作流中作为下一个自动化步骤的输入。没有结构化输出AI的能力就很难被可靠地集成。因此当我们谈论“6.结构化输出”时我们讨论的是一项能将智能系统的潜力真正释放到生产环境中的关键技术。它连接了智能的“模糊性”与工程的“精确性”是构建可靠、可维护、可扩展的AI驱动应用的基础设施。接下来我们将深入其核心模式、实现策略以及那些只有踩过坑才知道的实战细节。2. 结构化输出的核心模式不止于JSON很多人一听到“结构化输出”第一反应就是JSON。没错JSON是一种极其流行和有效的结构化数据格式但它只是表现形式之一。结构化输出的本质在于约束与验证其模式可以归结为以下几种。2.1 模式一强类型架构Schema绑定这是最严格、最工程化的模式。它要求输出必须完全符合一个预定义的、带有类型声明的数据架构Schema。这个架构定义了所有可能的字段、它们的类型字符串、整数、布尔值、数组、嵌套对象、是否必填、取值范围、甚至更复杂的约束如字符串格式必须为邮箱、日期。为什么需要这个因为下游系统尤其是数据库、API网关或类型安全的编程语言如TypeScript Go依赖明确的数据契约。一个返回用户信息的接口如果“年龄”字段有时是数字25有时是字符串“二十五”下游系统就会崩溃。如何实现在传统编程中我们通过类Class或结构体Struct来定义。在现代AI编程中工具如PydanticPython或ZodTypeScript成为了事实标准。你可以定义一个Pydantic模型然后要求LLM的输出必须符合这个模型。from pydantic import BaseModel, Field from typing import List class MeetingSummary(BaseModel): topics: List[str] Field(description讨论的核心议题列表) decisions: List[str] Field(description达成的明确决议) action_items: List[dict] Field(description行动项包含任务和负责人) next_meeting_time: str Field(description下次会议时间格式为YYYY-MM-DD HH:MM) # 在你的LLM调用中你可以将这个模型“注入”给模型要求它按此格式生成。 # 例如使用LangChain的PydanticOutputParser或LlamaIndex的PydanticOutputParser。注意使用强类型架构时务必为每个字段提供清晰、无歧义的description。LLM是根据你的描述来理解该字段期望的内容的。模糊的描述会导致解析失败或数据错误。2.2 模式二函数调用Function Calling与工具使用Tool Use这是当前大模型API如OpenAI Anthropic最主流的原生结构化输出方式。其思想是你不直接问模型一个问题而是“告诉”模型你有哪些“工具”即函数可用每个工具需要什么参数。模型在理解你的问题后会选择调用哪个工具并生成一个严格符合该工具参数要求的JSON对象。为什么这种方式如此强大它将“意图识别”和“参数提取”这两个NLP中的难题打包交给了大模型并以一种极其可靠的方式JSON返回结果。下游系统只需要执行这个函数调用即可。这本质上是一种“引导式”的结构化输出。一个典型流程你定义工具get_weather(city: str, date: str) - str。你将用户查询“北京后天天气怎么样”和工具定义一起发送给大模型。大模型返回{“name”: “get_weather” “arguments”: {“city”: “北京” “date”: “2023-10-28”}}。你的程序解析这个JSON调用真实的天气API并将结果返回给用户或模型进行下一步。2.3 模式三引导式文本模板Guided Text Templates这是一种相对“软”的结构化适用于输出格式相对固定但内容灵活的场景。你为模型提供一个带有占位符和明确指示的模板。例如你可以要求模型请按照以下格式总结邮件 **发件人** [这里填写发件人] **核心诉求** [用一句话概括] **需跟进事项** 1. [事项一] 2. [事项二] **优先级** [高/中/低]模型会尽力用内容填充[]中的部分。虽然输出仍是文本但由于格式高度结构化你可以用非常简单的规则如按行分割、查找冒号后的内容来提取信息可靠性远高于处理完全自由的文本。2.4 模式对比与选型建议模式优点缺点适用场景强类型架构类型安全易于集成验证严格文档化好定义稍显繁琐对模型遵循能力要求高后端API、数据管道、需要入库的复杂数据函数调用与业务逻辑结合紧密动态性强是大模型API原生支持依赖于特定的模型API概念上需要理解“工具”智能助手、自动化工作流、需要执行具体操作的场景引导式模板简单直观无需复杂解析库人类可读性好结构化程度相对较低易受模型“创造性”输出破坏报告生成、内容摘要、格式固定的文档生成在实际项目中我通常会混合使用。对于核心数据流转采用强类型架构确保数据质量对于复杂的用户交互采用函数调用来驱动业务流程而对于只需要简单提取信息的场景一个设计良好的文本模板就足够了。3. 实战在LangChain中实现可靠的Pydantic结构化输出理论说再多不如一行代码。我们以最常见的AI应用开发框架LangChain为例看看如何将“一团乱麻”的LLM输出变成规整的Pydantic对象。这里我会分享从基础到进阶的完整流程以及我踩过的几个坑。3.1 基础搭建定义模型与解析器假设我们要开发一个智能新闻分类器输入一篇新闻标题和摘要输出结构化的信息。首先定义我们的数据结构。这里的字段设计很有讲究category使用Literal类型严格限定可选值防止模型胡编乱造。sentiment同样限定范围并给出描述。keywords要求是列表并限制最大数量避免模型输出过多无关词。summary要求是字符串并给出长度提示引导模型生成 concise 的总结。from langchain.output_parsers import PydanticOutputParser from langchain.prompts import PromptTemplate from langchain_openai import ChatOpenAI from pydantic import BaseModel, Field from typing import List, Literal # 1. 定义你的数据结构Pydantic Model class NewsAnalysis(BaseModel): category: Literal[政治, 经济, 科技, 体育, 娱乐, 社会] Field(description新闻所属的核心类别) sentiment: Literal[积极, 消极, 中性] Field(description新闻内容的情感倾向) keywords: List[str] Field(max_items5, description从新闻中提取的关键词最多5个) summary: str Field(description对新闻内容的简要总结不超过100字) # 2. 创建解析器 parser PydanticOutputParser(pydantic_objectNewsAnalysis) # 3. 构建提示词模板。关键一步将解析器的“指令”融入提示词。 prompt_template 你是一个专业的新闻分析师。请分析以下新闻内容并严格按照要求格式输出。 新闻标题{title} 新闻摘要{abstract} {format_instructions} prompt PromptTemplate( templateprompt_template, input_variables[title, abstract], partial_variables{format_instructions: parser.get_format_instructions()} # 注入格式指令 ) # 4. 组装链并运行 model ChatOpenAI(modelgpt-4-turbo-preview, temperature0) # temperature设为0输出更稳定 chain prompt | model | parser # 测试 news_title 人工智能新突破新型算法大幅提升图像识别效率 news_abstract 研究人员近日公布了一项革命性的人工智能算法该算法在标准图像识别数据集上的准确率提升了15%同时计算资源消耗降低了一半。业界认为这将加速自动驾驶、医疗影像分析等领域的落地。 try: result: NewsAnalysis chain.invoke({title: news_title, abstract: news_abstract}) print(f分类{result.category}) print(f情感{result.sentiment}) print(f关键词{result.keywords}) print(f总结{result.summary}) except Exception as e: print(f解析失败{e})运行上述代码你应该能得到一个NewsAnalysis对象它的每个属性都已经被正确填充。parser.get_format_instructions()这个方法至关重要它会自动生成一段详细的文本说明告诉LLM必须返回一个JSON对象并且每个字段必须是什么类型、代表什么。这是连接自由文本LLM和严格Pydantic模型的桥梁。3.2 避坑指南解析失败的常见原因与处理在实际使用中chain.invoke那一步很可能抛出异常。根据我的经验90%的失败原因可以归结为以下几点1. 模型“不听话”输出格式错误这是最常见的问题。模型可能忽略了你的格式指令在JSON外面加上了额外的解释性文字比如根据分析结果如下 { “category”: “科技” “sentiment”: “积极” ... }或者它返回了一个JSON数组而不是对象。解决方案强化提示词Prompt Engineering在提示词开头或结尾用非常强硬、清晰的语言强调。例如“你必须只输出一个JSON对象不要有任何额外的解释、标记、前缀或后缀。你的整个响应必须是一个可被直接解析的JSON字符串。”使用更强大的模型GPT-4系列在遵循复杂指令方面远强于GPT-3.5。如果对稳定性要求高投资GPT-4是值得的。后处理清洗在解析前对模型的原始输出进行简单的字符串处理例如用正则表达式提取第一个出现的{...}之间的内容。这是一个虽然“脏”但很有效的兜底策略。import json import re def extract_json_from_text(text: str): 尝试从可能包含额外文本的响应中提取JSON对象。 # 匹配第一个 { ... } 结构 match re.search(r\{.*\}, text, re.DOTALL) if match: try: return json.loads(match.group()) except json.JSONDecodeError: pass # 如果失败尝试匹配 json ... 代码块 match re.search(rjson\n(.*?)\n, text, re.DOTALL) if match: try: return json.loads(match.group(1)) except json.JSONDecodeError: pass raise ValueError(无法从响应中提取有效的JSON。) # 在chain中使用 raw_output (prompt | model).invoke({title: news_title, abstract: news_abstract}) json_data extract_json_from_text(raw_output.content) result NewsAnalysis(**json_data) # 直接用字典初始化Pydantic模型2. 字段值不符合约束模型可能将category输出为“人工智能”不在Literal列表中或者keywords输出了10个词超过了max_items5。解决方案提供更清晰的描述和示例在字段的description中不仅说明是什么还要举例说明。例如category: Literal[...] Field(description“新闻所属的核心类别例如一篇关于股票市场的新闻属于‘经济’关于世界杯的属于‘体育’。”)。使用try-except并重试捕获Pydantic的验证错误ValidationError然后可以将错误信息反馈给模型要求它修正。这构成了一个自我修正的循环虽然增加延迟但显著提升鲁棒性。from pydantic import ValidationError from langchain.schema import OutputParserException max_retries 2 for i in range(max_retries 1): try: result chain.invoke({title: news_title, abstract: news_abstract}) break # 成功则跳出循环 except (OutputParserException, ValidationError) as e: if i max_retries: raise e # 重试次数用尽抛出异常 print(f第{i1}次解析失败错误{e}。尝试修正...) # 构建一个修正提示将错误信息反馈给模型 correction_prompt f 上次分析失败因为输出不符合格式要求。错误信息是{str(e)}。 请重新分析之前的新闻内容并确保输出严格符合格式。 新闻标题{news_title} 新闻摘要{news_abstract} {parser.get_format_instructions()} # 这里简化处理实际中可能需要重新构造chain raw_output model.invoke(correction_prompt) # 再次尝试提取和解析...3. 处理可选字段与嵌套结构当你的Schema很复杂包含可选字段或深层嵌套对象时模型更容易出错。解决方案分而治之不要试图让模型一次性填充一个非常复杂的巨大JSON。采用多步推理Chain of Thought或序列化Sequential调用。例如先让模型判断类别再根据类别提取特定信息。提供默认值对于可选字段在Pydantic模型中设置合理的默认值如None避免因为模型未提供该字段而导致解析失败。4. 超越基础流式输出、部分解析与错误恢复在真实的生产环境中我们面对的需求远比简单的“一问一答”复杂。结构化输出也需要应对这些高级场景。4.1 流式输出Streaming中的结构化当LLM生成一个很长的结构化响应时比如一份包含多个条目的报告我们可能希望一边生成一边解析以提升用户体验前端可以逐步渲染或实现更早的错误检测。LangChain本身对PydanticOutputParser的流式支持可能不直接。一个实用的策略是在token流层级进行拼接和试探性解析。基本思路是我们监听模型返回的每一个token或chunk将它们拼接成一个不断增长的字符串。每当拼接后的字符串看起来“可能”构成了一个完整的JSON对象时例如括号匹配了我们就尝试用json.loads()去解析它。如果解析成功我们就得到了一个部分结果如果失败就继续等待更多token。import json from langchain_openai import ChatOpenAI model ChatOpenAI(model“gpt-4” streamingTrue) prompt “用JSON格式列出三个水果包含name和color属性。” accumulated_text “” for chunk in model.stream(prompt): if hasattr(chunk, ‘content’): delta chunk.content accumulated_text delta # 简单试探检查是否构成了一个可解析的JSON数组 if accumulated_text.strip().startswith(‘[’) and accumulated_text.strip().endswith(‘]’): try: partial_result json.loads(accumulated_text) print(f“流式接收到部分结果{partial_result}”) # 可以发送到前端 except json.JSONDecodeError: pass # 还不是完整的JSON继续积累注意这种方法不是百分百可靠因为模型可能在生成过程中输出暂时不合语法的JSON片段。它更适用于对实时性要求高、对中间结果有一定容错性的场景。对于必须保证最终结果绝对正确的场景还是建议等待完整响应后再进行解析。4.2 部分解析Partial Parsing与渐进式填充有些任务中模型可能无法一次性确定所有字段的值。例如在复杂推理中它可能需要先确定A才能推理出B。我们可以设计支持部分解析的流程。一种方法是使用Pydantic的validate_assignment和construct方法允许我们先创建一个部分填充的对象随后再补充剩余字段。另一种更清晰的方法是设计多轮对话或工作流每一轮只负责填充结构的一部分。例如分析一份公司财报第一轮提取company_namereport_period等基本信息。第二轮基于已提取的公司名和周期查询数据库获得历史数据再让模型分析revenue_growthprofit_margin等财务指标。第三轮结合所有信息生成investment_advice。每一轮都有自己的Pydantic模型和解析器将上一轮的结果作为下一轮的输入。这种“分阶段结构化”的策略降低了单次任务的复杂度提高了整体成功率。4.3 构建自修复Self-Correction的解析管道即使我们做了所有预防措施解析失败依然会发生。一个健壮的系统不能因此崩溃。我们需要一个包含错误恢复机制的管道。这个管道的核心思想是将解析失败视为一个可以自动处理的事件。当主解析器失败时触发一个“修复”子流程错误分析捕获异常提取模型原始输出和错误信息。修复请求将原始输出和错误信息如“字段‘category’的值‘AI’不在允许列表中”连同原始指令再次发送给模型或另一个专门的“修复模型”请求它修正输出。重解析尝试解析修正后的输出。降级策略如果修复后仍失败则记录日志并返回一个包含错误信息的默认结构或转入人工处理流程。class SelfHealingParser: def __init__(self, pydantic_model, llm, max_retries1): self.parser PydanticOutputParser(pydantic_objectpydantic_model) self.llm llm self.max_retries max_retries def parse_with_healing(self, llm_raw_output: str) - BaseModel: last_error None for attempt in range(self.max_retries 1): try: if attempt 0: # 第一次尝试直接解析 return self.parser.parse(llm_raw_output) else: # 后续尝试基于错误修复 print(f“尝试第{attempt1}次修复...”) repair_prompt f“”” 以下内容是一个语言模型的输出但它不符合指定的JSON格式要求导致解析失败。 失败原因{last_error} 原始输出{llm_raw_output} 请根据原始输出本意重新生成一个严格符合以下格式要求的JSON {self.parser.get_format_instructions()} 只输出JSON对象别无其他。 “”” repaired_output self.llm.invoke(repair_prompt).content return self.parser.parse(repaired_output) except Exception as e: last_error str(e) if attempt self.max_retries: raise OutputParserException(f“经过{self.max_retries}次修复尝试后仍解析失败。最后错误{last_error}”) from e continue这种自修复机制虽然增加了延迟和成本但对于关键任务应用它能显著提升系统的整体可用性。在实际部署中我们需要监控修复触发频率如果频率过高则说明提示词或Schema设计需要优化而不是依赖修复。
返回列表