
1. 从“乱炖”到“拼图”为什么我们需要结构化输出如果你用过早期的RAG检索增强生成系统或者尝试过让大语言模型LLM帮你处理一些稍微复杂点的任务比如从一份财报里提取关键财务指标或者把一篇产品评测整理成表格你大概率经历过这种抓狂时刻你问得清清楚楚模型答得洋洋洒洒但答案就像一碗“信息乱炖”——数据点散落在段落各处格式随心所欲有些信息重复有些关键信息干脆缺失。你想把它喂给下游的自动化流程对不起得先请个“人工解析器”也就是你自己来手动摘抄、整理、格式化。这就是“非结构化输出”的典型困境。LLM很擅长生成流畅、连贯的自然语言但它的“思考”过程对我们来说是个黑盒输出的结果也是自由文本。对于人类阅读这或许可以接受但对于程序化处理、数据入库、自动化决策这种自由文本就是一场灾难。我们需要的是“结构化输出”——让模型按照我们预先定义好的、机器可读的格式比如JSON、Pydantic模型、列表等来返回答案。这就像从让模型“自由发挥写散文”变成了让它“严格按照图纸拼乐高”。而LlamaIndex作为构建高级RAG和AI应用的重要框架其核心价值之一就是提供了强大、灵活的工具链来“驾驭”LLM让它乖乖听话输出我们想要的格式。llamaindex的热度以及llamaindex rag实战、llamaindex langgraph这些相关热词的兴起都指向了一个共同的需求开发者不再满足于简单的问答而是希望构建能真正集成到生产流水线中的、可靠的智能体Agent或复杂应用。结构化输出正是实现这一步的关键基石。简单来说玩转LlamaIndex的结构化输出意味着你能构建可靠的数据提取管道从海量文档中自动、准确地抽取实体、关系、事件形成干净的数据集。实现复杂的多步推理与规划结合langgraph这类工作流引擎让AI智能体每一步的思考结果都能以结构化形式传递实现可控的、可调试的复杂任务链。打造真正的API式AI服务你的AI应用接口返回的不再是一段话而是一个标准的JSON对象前端、移动端、其他微服务可以直接消费无缝集成。提升评估与监控效率由于输出格式固定你可以轻松编写自动化脚本来评估答案的准确性、完整性监控AI服务的性能漂移。接下来我们就深入LlamaIndex的武器库看看它提供了哪些“模具”来塑造LLM输出的形状。2. LlamaIndex的结构化输出核心“模具”Pydantic与函数调用在LlamaIndex中实现结构化输出主要依赖于两大核心机制它们本质上都是利用了现代LLM如GPT-4、Claude 3、DeepSeek等对“函数调用”Function Calling或“JSON模式”JSON Mode的原生支持。LlamaIndex在此基础上做了优雅的封装让我们能用更符合Python开发者习惯的方式工作。2.1 Pydantic Output Parser定义你的数据“蓝图”Pydantic是Python中用于数据验证和设置管理的明星库它用Python类型注解来定义数据的结构。LlamaIndex的PydanticOutputParser允许你直接用一个Pydantic模型Model来定义你希望LLM输出的格式。为什么选择Pydantic因为它不仅仅是定义一个JSON结构。Pydantic模型自带类型验证确保price是float而不是string、默认值、字段描述等元信息。这些元信息在构造给LLM的提示词Prompt时极其有用能清晰地告诉模型每个字段期望的是什么。实操步骤与核心代码假设我们要从一段产品描述中提取结构化信息。首先定义你的Pydantic模型from pydantic import BaseModel, Field from typing import List, Optional class ProductInfo(BaseModel): 从文本中提取的产品信息 name: str Field(description产品的完整名称) brand: Optional[str] Field(defaultNone, description产品品牌如未提及则为None) key_features: List[str] Field(description产品的关键特性列表至少3项) price_range: Optional[str] Field(defaultNone, description价格区间例如100-200元或约1500美元) target_audience: Optional[str] Field(defaultNone, description产品目标用户群体描述)注意Field中的description至关重要LLM会读取这些描述来理解每个字段的含义。Optional和default让模型知道哪些信息可能缺失避免它胡编乱造。接着在LlamaIndex中组装你的查询引擎并绑定解析器from llama_index.core import VectorStoreIndex, SimpleDirectoryReader from llama_index.core.query_engine import CustomQueryEngine from llama_index.core.output_parsers import PydanticOutputParser from llama_index.core.prompts import PromptTemplate from llama_index.llms.openai import OpenAI # 1. 加载文档这里用虚拟文档示例 documents SimpleDirectoryReader(input_dir./product_docs).load_data() index VectorStoreIndex.from_documents(documents) # 2. 创建Pydantic解析器实例 parser PydanticOutputParser(output_clsProductInfo) # 3. 构建一个强化的提示词模板 # format_instructions 是解析器自动生成的告诉LLM如何格式化输出 format_instructions parser.get_format_instructions() query_str 提取以下上下文中关于产品的结构化信息。 prompt_template PromptTemplate( {query_str} 上下文信息如下 --------------------- {context_str} --------------------- 请严格根据以下要求输出 {format_instructions} 最终输出必须是纯JSON格式不要有任何额外的解释、前缀或后缀。 ) # 4. 创建查询引擎 llm OpenAI(modelgpt-4-turbo-preview) # 确保使用支持JSON模式的模型 class StructuredQueryEngine(CustomQueryEngine): llm: OpenAI index: VectorStoreIndex output_parser: PydanticOutputParser prompt_template: PromptTemplate def custom_query(self, query_str: str): # 检索相关上下文 retriever self.index.as_retriever(similarity_top_k3) nodes retriever.retrieve(query_str) context_str \n\n.join([n.node.get_content() for n in nodes]) # 格式化完整提示词 full_prompt self.prompt_template.format( query_strquery_str, context_strcontext_str, format_instructionsself.output_parser.get_format_instructions() ) # 调用LLM response self.llm.complete(full_prompt) # 解析输出这是关键一步 try: parsed_output self.output_parser.parse(response.text) return parsed_output except Exception as e: # 处理解析失败例如LLM没有返回合法JSON print(f解析失败: {e}, 原始响应: {response.text}) # 可以在这里加入重试逻辑或降级处理 return None query_engine StructuredQueryEngine( llmllm, indexindex, output_parserparser, prompt_templateprompt_template ) # 5. 执行查询 result query_engine.custom_query(找出旗舰手机X的主要信息) if result: print(f产品名称: {result.name}) print(f品牌: {result.brand}) print(f核心特性: {result.key_features}) # result 就是一个ProductInfo对象可以像普通Python对象一样使用也可以直接json()序列化 print(result.json(indent2))避坑经验描述要精准Field(description...)是给LLM看的“需求文档”务必清晰、无歧义。例如“价格”字段说明“请提取数字价格货币单位统一为人民币元”比单纯写“价格”要好得多。处理解析失败LLM并不总是乖乖遵守格式。一定要用try...except包裹parser.parse()调用。失败时记录原始响应这既是调试线索也可以作为反馈数据用于提示词优化。模型选择并非所有模型都同等擅长结构化输出。OpenAI的gpt-4-turbo系列、gpt-3.5-turbo-1106及以后版本对JSON模式支持很好。Anthropic的Claude 3系列也表现出色。如果使用开源模型需确认其是否经过函数调用/JSON输出微调。提供示例对于特别复杂的结构在提示词中提供一两个format_instructions之外的例子能极大提高输出质量。2.2 函数调用Tool Calling让输出成为动作的指令Pydantic输出主要解决“提取信息”的问题。而函数调用在LlamaIndex中通常通过Tool或QueryEngineTool暴露则更进一步它让LLM的输出直接成为一个可执行的“动作指令”这个指令本身也是结构化的。这在构建智能体Agent时是核心能力。智能体根据你的问题决定调用哪个工具函数并生成调用这个工具所需的、结构化的参数。LlamaIndex中的实现逻辑在LlamaIndex中你首先需要将你的功能如查询数据库、调用API、计算器包装成Tool对象。这些Tool对象会附带一个Pydantic模型用来定义其输入参数。from llama_index.core.tools import FunctionTool from pydantic import BaseModel, Field # 定义工具输入参数的模型 class WeatherQueryInput(BaseModel): location: str Field(description城市名称例如北京San Francisco) date: str Field(description查询日期格式YYYY-MM-DD例如2024-05-20) # 实现一个假的天气查询函数 def get_weather(location: str, date: str) - str: # 这里应该是调用真实天气API return f{location}在{date}的天气是晴朗25摄氏度。 # 包装成LlamaIndex的Tool weather_tool FunctionTool.from_defaults( fnget_weather, nameget_weather, description根据地点和日期查询天气, fn_schemaWeatherQueryInput # 关键绑定参数结构 ) # 假设我们有一个智能体 from llama_index.core.agent import ReActAgent agent ReActAgent.from_tools([weather_tool], llmllm, verboseTrue) # 当用户提问“北京明天天气怎么样”时 # LLM内部会进行推理最终生成一个结构化的调用请求类似于 # { # tool_name: get_weather, # tool_input: {location: 北京, date: 2024-05-21} # } # 这个结构化输出会被LlamaIndex框架捕获然后去执行对应的get_weather函数。 response agent.chat(北京明天天气怎么样)为什么这很重要因为输出被严格约束在了工具定义的参数范围内。LLM不会输出“北京明天可能有点热记得防晒”这种模糊文本而是必须产出{“location”: “北京” “date”: “2024-05-21”}这样的结构化字典。这保证了后续程序能可靠地解析并使用这个结果去执行真实操作。实操心得工具描述是关键FunctionTool中的description和fn_schema里每个字段的description共同指导LLM何时以及如何调用该工具。描述要简洁、准确。智能体与查询引擎的抉择如果你只需要从给定上下文中提取信息用PydanticOutputParser绑定查询引擎更直接。如果你的应用需要LLM自主决定使用多种能力、进行多步推理那么用Tool构建智能体是更强大的范式。llamaindex langgraph的热度正是源于对复杂、有状态的多智能体工作流的支持而这些工作流的基础正是结构化的工具调用。3. 进阶实战在RAG管道中嵌入结构化输出基础的RAG流程是检索Retrieve相关文档块然后让LLM基于这些上下文生成答案。如果我们想要这个答案就是结构化的就需要将上述技术与RAG管道深度融合。3.1 设计支持结构化输出的RAG查询引擎LlamaIndex的RetrieverQueryEngine是一个可扩展的基类。我们可以创建一个子类专门用于返回Pydantic对象。from llama_index.core import QueryBundle from llama_index.core.retrievers import BaseRetriever from llama_index.core.response_synthesizers import BaseSynthesizer from llama_index.core.output_parsers import PydanticOutputParser class StructuredRAGQueryEngine: 一个返回Pydantic对象的RAG查询引擎简化示例 def __init__( self, retriever: BaseRetriever, output_parser: PydanticOutputParser, llm: OpenAI, prompt_template_str: str, ): self.retriever retriever self.output_parser output_parser self.llm llm # 使用LlamaIndex的PromptTemplate self.prompt_template PromptTemplate(prompt_template_str) def query(self, query_str: str) - BaseModel: # 1. 检索 query_bundle QueryBundle(query_str) retrieved_nodes self.retriever.retrieve(query_bundle) if not retrieved_nodes: raise ValueError(未检索到相关上下文。) context_str \n\n.join([n.node.get_content() for n in retrieved_nodes]) # 2. 构建提示词 format_instructions self.output_parser.get_format_instructions() full_prompt self.prompt_template.format( context_strcontext_str, query_strquery_str, format_instructionsformat_instructions, ) # 3. 生成与解析 response self.llm.complete(full_prompt) parsed_result self.output_parser.parse(response.text) return parsed_result # 使用示例 from llama_index.core import VectorStoreIndex index VectorStoreIndex.from_documents(documents) retriever index.as_retriever(similarity_top_k4) # 假设我们想提取会议纪要 class MeetingMinutes(BaseModel): topic: str date: str attendees: List[str] decisions: List[str] action_items: List[dict] # 例如 [{person: 张三, task: 完成报告, deadline: 2024-05-30}] parser PydanticOutputParser(output_clsMeetingMinutes) prompt_str 你是一个专业的会议秘书请根据以下会议记录上下文提取出结构化的会议纪要信息。 上下文 {context_str} 用户问题{query_str} 输出要求 {format_instructions} 请确保所有信息均来源于上下文不要编造。 engine StructuredRAGQueryEngine(retriever, parser, llm, prompt_str) minutes engine.query(总结一下上周项目评审会的核心内容和待办事项)3.2 处理多文档与信息聚合一个更复杂的场景是答案所需的信息分散在多个文档中。例如你要整理一份“竞争对手产品对比表”信息来自A公司、B公司、C公司的官网PDF。策略分而治之后聚合并行提取针对每个文档或每个检索到的相关节点分别运行一次“结构化提取”查询目标是从该片段中提取出部分结构化信息可能是不完整的。结果聚合将所有部分结果收集起来。这里可能会遇到冲突同一字段不同文档给出不同值或缺失。智能聚合/裁决设计第二个LLM调用其输入是所有部分提取结果的列表任务是进行冲突解决、信息去重、补全缺失并生成最终的统一结构化输出。这个步骤的提示词需要精心设计指导LLM如何扮演“数据整合专家”的角色。# 伪代码示意 all_partial_results [] for doc in relevant_docs: partial_result structured_engine_for_single_doc.query(doc) all_partial_results.append(partial_result.dict()) # 转为字典 # 构建聚合提示词 aggregation_prompt f 你是一个数据分析师。以下是从多个来源提取的关于产品“{product_name}”的信息片段可能存在冲突或重复。 信息片段列表JSON格式 {json.dumps(all_partial_results, indent2, ensure_asciiFalse)} 请根据以下规则整合出一份唯一、准确、完整的结构化产品信息 1. 对于明确冲突的数据如两个不同价格优先采用来源更权威、时间更近的信息。 2. 合并所有来源提到的特性去重。 3. 如果某些字段在所有片段中都缺失请标记为null不要猜测。 请以以下JSON格式输出最终整合结果 {MeetingMinutes.schema_json(indent2)} final_llm_response llm.complete(aggregation_prompt) final_result parser.parse(final_llm_response.text)这个过程的挑战与技巧成本与延迟并行提取N个文档加上一次聚合调用总共需要N1次LLM调用。需要权衡精度与成本。可以通过更精准的检索similarity_top_k来减少N。聚合提示词设计这是成功的关键。必须明确给出冲突解决策略如“以财报数据为准而非新闻稿”并严格要求LLM不得在聚合阶段引入新知识不要猜测。结构化输出的嵌套你的Pydantic模型可以嵌套其他Pydantic模型或者使用List[SomeModel]来表示数组对象。这非常适合表示对比表格List[CompetitorProduct]或层级信息。4. 避坑指南结构化输出实战中的常见“雷区”即使理解了原理在实际操作中依然会踩坑。下面是我在多个项目中总结出的高频问题与解决方案。4.1 雷区一LLM不遵守格式解析失败现象output_parser.parse()抛出JSONDecodeError或其他验证错误。LLM返回的内容可能包含了额外的解释性文字如“好的根据您的要求我提取的信息如下”或者JSON格式不完整。根因排查与解决方案提示词不够强硬在提示词末尾使用强指令。不要只说“请输出JSON”而要明确说最终输出必须是纯JSON格式不要有任何额外的解释、前缀或后缀。直接以{开始以}结束。缺少示例对于复杂结构在format_instructions后手动添加一个example。例如格式必须严格遵循以下示例{ name: 示例产品, features: [特性1, 特性2], price: {currency: CNY, amount: 199.99} }模型能力不足如果使用较小的或未经专门微调的开源模型其遵循复杂JSON模式的能力可能很弱。解决方案降级结构简化输出模式减少嵌套尽量使用扁平结构。后处理清洗在解析前用简单的正则表达式如r\{.*\}配合re.DOTALL从响应文本中提取出最像JSON的那部分再进行解析。升级模型换用对JSON模式支持更好的模型如GPT-4、Claude 3 Opus等。这通常是性价比最高的方案。使用ResponseMode部分LLM提供商如OpenAI的API有专门的response_format参数。确保在初始化LlamaIndex的LLM对象时传递了相应参数。例如对于OpenAIfrom llama_index.llms.openai import OpenAI llm OpenAI(modelgpt-4-turbo-preview, response_format{ type: json_object }) # 强制JSON对象输出注意当使用response_format{ type: json_object }时OpenAI官方建议系统提示词或你的主要提示词中必须包含“JSON”这个词否则API可能报错。这是一个容易被忽略的细节。4.2 雷区二字段缺失或内容“幻觉”现象LLM返回了合法的JSON但某些应为null的字段被填入了虚构的内容或者必填字段缺失。解决方案在Pydantic模型中明确定义Optional和默认值如前所述将可能为空的字段标记为Optional[str] None。对于列表可以考虑设置default_factorylist。在提示词中强调“基于上下文”和“不要编造”请仅基于提供的上下文信息回答问题。如果上下文中没有明确提及某个字段的信息请将该字段的值设置为null对于JSON或空列表[]。绝对不要编造任何上下文之外的信息。使用检索评分进行过滤如果某个检索到的节点相关性分数similarity_score非常低那么基于它提取的信息可信度也低。可以在聚合阶段根据来源节点的分数对信息进行加权或过滤。后验检查编写简单的规则检查脚本。例如如果提取的“价格”字段包含“免费”但上下文中提到的是高端产品则可以触发人工审核或二次查询。4.3 雷区三性能与成本优化现象处理大量文档时每次查询都调用LLM进行全文档解析速度慢且成本高。优化策略分层索引与摘要在文档入库索引阶段就做一次“轻量级结构化”。例如使用一个快速的、便宜的模型如gpt-3.5-turbo或专门的提取模型为每个文档生成一个包含核心元数据如标题、作者、日期、关键词列表的摘要并将其作为元数据存储在索引中。在查询时先基于这些结构化元数据进行快速过滤减少需要送入大模型进行深度解析的文档数量。缓存对于相同的查询和上下文结构化输出结果是确定的。可以使用LlamaIndex内置的缓存机制或外部缓存如Redis来存储(query, context_hash) - structured_output的映射避免重复计算。批处理如果你需要从成千上万个文档中提取同一套结构化信息如批量处理合同不要用循环单条处理。将多个文档或文档块组合成一个批次构造一个批处理提示词让LLM一次性输出一个包含多个结果的JSON数组。这能大幅减少API调用次数和总耗时。但要注意上下文长度限制和模型对批处理的支持度。选择性价比模型对于提取任务gpt-4-turbo在精度和成本上往往比gpt-4更有优势。Claude 3 Haiku在速度和成本上极具竞争力且输出格式遵守度很好。多做A/B测试找到适合你任务的最优模型。4.4 雷区四复杂类型与验证现象Pydantic模型中有EmailStr、HttpUrl等复杂类型或者有自定义验证器。LLM输出的字符串可能无法通过Pydantic的严格验证。解决方案放宽前端收紧后端让LLM输出普通字符串如str然后在你的应用逻辑中再用Pydantic进行严格验证和转换。如果验证失败可以记录日志、使用默认值或触发错误处理流程。这样提示词更简单LLM更容易满足。在提示词中提供格式示例对于date字段明确要求“格式YYYY-MM-DD”。对于email可以写“必须是有效的邮箱地址格式包含符号”。使用try_parse或自定义解析器LlamaIndex的PydanticOutputParser在解析失败时会抛出异常。你可以继承它重写parse方法加入更宽松的解析逻辑或更详细的错误信息收集。结构化输出不是魔法它是一套结合了精确的规范定义Pydantic、清晰的指令沟通Prompt Engineering和可靠的执行框架LlamaIndex的工程实践。它把LLM从一位天马行空的诗人训练成一位严谨的数据录入员。这个过程需要调试和迭代但一旦跑通你的AI应用将从玩具升级为真正生产力的引擎。