生成集成 —— 让AI输出“可用”的答案
经过前面的数据准备、索引构建和检索优化我们终于来到了RAG流程的最后一站生成Generation。检索系统已经找回了最相关的上下文现在轮到LLM来阅读这些资料并写出最终答案了。但现实往往比理想更复杂——应用中需要的不是一段“美文”而是一个可以直接被程序消费的数据结构JSON对象、API调用参数、数据库记录……本章将深入探讨如何让LLM的输出从“自然语言”进化为“结构化数据”让AI真正成为应用程序的有机组成部分。格式化生成从大语言模型LLM那里获得一段非结构化的文本在应用中常常不满足实际需求。为了实现更复杂的逻辑、与外部工具交互或以用户友好的方式展示数据我们需要模型能够输出具有特定结构的数据例如JSON或XML。本章将讨论实现格式化生成的几种主流方法包括LangChain、LlamaIndex等框架内置的解决方案不依赖框架的实现思路以及一种更强大的技术——Function Calling。关于提示词工程在生成阶段提示词工程是一个重要的部分。但因为在前面的章节中已经有了较多介绍本章不再赘述而是聚焦于“如何让输出变得结构化”这一核心主题。一、为什么需要格式化生成先来看几个具体的应用场景场景用户输入期望的结构化输出RAG驱动的电商客服“推荐几款适合程序员的键盘”[{name: ..., price: 99, features: [...], url: ...}]自然语言转API调用“帮我查一下明天从上海到北京的航班”{departure: 上海, destination: 北京, date: 2025-07-18}数据自动提取一篇新闻文章{event: ..., time: ..., location: ..., people: [...]}智能表单填写“我叫张三25岁想申请软件开发岗位”{name: 张三, age: 25, position: 软件开发}在这些场景中格式化生成是连接LLM的自然语言理解能力和下游应用程序的程序化逻辑之间的关键桥梁。二、格式化生成的实现方法2.1 LangChain的Output ParsersLangChain提供了一个强大的组件——OutputParsers输出解析器专门用于处理LLM的输出。其核心思想是在发送给LLM的提示中自动注入一段关于如何格式化输出的指令并在得到结果后将LLM返回的纯文本字符串解析成预期的结构化数据如Python对象。LangChain提供了多种开箱即用的解析器解析器用途特点StrOutputParser基础字符串输出最简单直接返回LLM输出JsonOutputParserJSON解析支持嵌套结构和列表的复杂JSONPydanticOutputParser强类型数据验证与Pydantic模型结合自动验证类型和结构CommaSeparatedListOutputParser逗号分隔列表将输出解析为字符串列表DatetimeOutputParser日期时间解析将输出解析为日期时间对象深度案例PydanticOutputParser的工作原理以下通过一个具体代码示例分析PydanticOutputParser的内部机制——它将用户定义的Pydantic数据模型转换为详细的格式指令并注入提示词再将模型返回的JSON字符串安全解析为Pydantic对象实例。# (此处省略导入和LLM初始化代码) # # 1. 定义期望的数据结构 # from pydantic import BaseModel, Field from typing import List class PersonInfo(BaseModel): 用于存储个人信息的数据结构。 name: str Field(description人物姓名) age: int Field(description人物年龄) skills: List[str] Field(description技能列表) # # 2. 基于Pydantic模型创建解析器 # parser PydanticOutputParser(pydantic_objectPersonInfo) # # 3. 创建提示模板注入格式指令 # prompt PromptTemplate( template请根据以下文本提取信息。\n{format_instructions}\n{text}\n, input_variables[text], partial_variables{ format_instructions: parser.get_format_instructions() }, ) # # 4. 创建处理链LCEL方式 # chain prompt | llm | parser # # 5. 执行调用 # text 张三今年30岁他擅长Python和Go语言。 result chain.invoke({text: text}) print(result) # 输出: name张三 age30 skills[Python, Go语言]深度拆解解析器内部做了什么步骤操作详细说明步骤1调用.model_json_schema()PydanticOutputParser内部调用Pydantic模型的.model_json_schema()方法提取完整的JSON Schema定义包括字段名、类型、描述、是否必填等元数据步骤2生成格式指令将JSON Schema嵌入预设的提示模板中生成的format_instructions会明确要求LLM输出符合该Schema的JSON对象例如“You must format your output as a JSON object that conforms to the following schema...”步骤3注入并发送prompt将用户输入和格式指令组合成完整提示发送给LLM步骤4解析JSON字符串LLM返回JSON字符串后解析器先使用json.loads()将其解析为Python字典步骤5Pydantic验证使用PersonInfo.model_validate()验证字典——检查所有必填字段是否存在、类型是否正确如age是否为int步骤6返回对象实例验证通过后返回PersonInfo实例验证失败则抛出OutputParserException异常最佳实践Field(description...)中的描述文本会直接出现在format_instructions中因此需要清晰准确地描述每个字段的含义和格式要求直接影响LLM生成的质量。2.2 LlamaIndex的输出解析LlamaIndex的输出解析与生成过程紧密结合主要体现在两大核心组件中响应合成Response Synthesis和结构化输出Structured Output。响应合成的四种模式在RAG流程中检索器召回相关文本块后响应合成器Response Synthesizer负责以不同策略将它们呈现给LLM模式工作方式适用场景compact尽可能多地将文本块压缩进单次LLM调用默认推荐效率高refine逐块处理迭代优化答案需要多轮推理的复杂问题tree_summarize构建摘要树递归总结需要整体把握的长文档simple_summarize简单拼接所有文本块文本块少且短的场景Pydantic Programs当需要LLM返回结构化数据而非纯文本时LlamaIndex使用Pydantic Programs思路与LangChain的PydanticOutputParser一致from llama_index.core.program import LLMTextCompletionProgram from pydantic import BaseModel # 1. 定义数据模型 class FlightInfo(BaseModel): departure: str destination: str date: str # 2. 创建结构化输出程序 program LLMTextCompletionProgram.from_defaults( output_clsFlightInfo, prompt_template_str从以下文本中提取航班信息{text}, llmllm, verboseTrue ) # 3. 执行提取 result program(text明天从上海飞往北京的航班有哪些) print(result.departure) # 上海 print(result.destination) # 北京关键差异如果底层LLM支持Function CallingLlamaIndex会优先使用该功能以获得更可靠的结构化输出如果不支持则会回退到将JSON Schema注入提示词的方法。2.3 不依赖框架的简单实现如果不想依赖特定框架纯粹通过提示工程也能实现格式化生成。以下是一些实用技巧技巧说明示例明确要求JSON格式直接、强硬地要求模型返回JSON“你必须返回一个JSON对象不要包含任何解释性文字”提供JSON Schema在提示中给出想要的JSON模式描述每个键的含义和数据类型Few-shot示例给出1-2个完整的“用户输入→期望JSON输出”示例让模型学习输出格式和风格强调字段完整性明确要求所有字段都要有值“如果信息不存在请使用null值”使用语法约束对于本地部署的开源模型如llama.cpp使用GBNF语法强制约束输出确保生成的每个token都严格符合预定义的JSON语法# 纯提示工程实现示例 prompt f 请从以下文本中提取人物信息并以JSON格式返回。 要求 1. 只返回JSON对象不要包含任何解释 2. JSON必须包含以下字段name字符串、age整数、skills字符串数组 3. 如果某个字段信息缺失使用null值 文本{text} JSON输出 response llm.invoke(prompt) result json.loads(response.content) # 解析为Python字典三、Function CallingFunction Calling或称Tool Calling是近年来LLM领域的一个重要进展它极大地提升了模型与外部世界交互和生成结构化数据的能力。3.1 概念与工作流程Function Calling的本质是一个多轮对话流程让模型、代码和外部工具如API协同工作。通俗理解Function Calling就是让LLM从“写作文”变成“填写工单”——它不直接给你答案而是告诉你“该调用什么工具、传什么参数”由你的代码去执行并拿到结果。完整工作流程text┌─────────────────────────────────────────────────────────────────┐ │ ① 定义工具代码中提前定义好可用的函数包括名称、描述、参数 │ └─────────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────┐ │ ② 用户提问 → LLM分析意图 → 匹配最合适的工具 │ │ 返回特殊响应{tool_calls: [{name: get_weather, │ │ arguments: {city: 杭州}}]} │ └─────────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────┐ │ ③ 代码执行解析tool_calls → 调用实际API → 获取真实数据 │ │ 例如调用天气API得到 24℃晴朗 │ └─────────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────┐ │ ④ 结果反馈将工具执行结果包装为tool消息再次发送给模型 │ └─────────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────┐ │ ⑤ 最终生成模型结合工具返回的真实数据生成自然语言答案 │ │ 杭州今天天气晴朗气温24℃。 │ └─────────────────────────────────────────────────────────────────┘3.2 Function Calling实践以下使用OpenAI风格API展示完整流程# # 1. 定义工具JSON Schema格式 # tools [ { type: function, function: { name: get_weather, description: 获取指定城市的天气信息, parameters: { type: object, properties: { city: { type: string, description: 城市名称如杭州、北京 }, date: { type: string, description: 日期格式为YYYY-MM-DD默认为今天 } }, required: [city] } } } ] # # 2. 用户提问 → 第一次调用User → Model # messages [{role: user, content: 杭州今天天气怎么样}] response client.chat.completions.create( modelgpt-4, messagesmessages, toolstools, tool_choiceauto # 让模型自动决定是否调用工具 ) message response.choices[0].message # # 3. 处理tool_calls → 代码执行 # if message.tool_calls: tool_call message.tool_calls[0] function_name tool_call.function.name arguments json.loads(tool_call.function.arguments) print(f模型决定调用: {function_name}) print(f参数: {arguments}) # 输出: 模型决定调用: get_weather # 参数: {city: 杭州} # 模拟API调用实际场景中为真实HTTP请求 def get_weather(city: str) - str: weather_data {杭州: 24℃晴朗, 北京: 32℃多云} return weather_data.get(city, 未知天气) tool_result get_weather(arguments[city]) print(f工具返回: {tool_result}) # 输出: 工具返回: 24℃晴朗 # # 4. 结果反馈 → 第二次调用Tool → Model # messages.append(message) # 添加模型的tool_calls响应 messages.append({ role: tool, tool_call_id: tool_call.id, content: tool_result }) # 第二次发送给模型让它基于工具结果生成最终答案 final_response client.chat.completions.create( modelgpt-4, messagesmessages, ) print(最终答案:, final_response.choices[0].message.content) # 输出: 杭州今天天气晴朗气温24℃。3.3 Function Calling的三大优势优势说明与纯提示工程的对比可靠性更高模型原生支持输出结构稳定、精确纯提示工程依赖模型“自觉遵守”格式不稳定的概率更高意图识别不仅格式化输出更包含“意图到函数的映射”模型能主动判断“这个问题需要调用工具”而非被动等待指令与外部世界交互构建能执行实际任务的AI代理Agent的基础LLM可以查询数据库、调用API、控制智能家居等3.4 Function Calling的底层原理理解更透Function Calling的本质是让LLM学会“选函数”而非“写文本”。具体来说训练阶段在模型预训练时使用了大量包含“函数调用”格式的数据让模型学会了识别工具调用的模式。推理阶段模型在输出时不是直接生成文本而是生成一个特殊的tool_calls结构。这个结构的格式是固定的如JSON保证了一致性。系统层面工具的描述名称、参数、描述被包含在系统提示中模型在生成时会“看到”这些信息根据用户问题匹配最合适的工具。从概率角度看纯提示工程模型生成文本 → 我们解析JSON → 如果模型输出格式不对解析失败Function Calling模型生成tool_calls→ 系统直接解析 → 几乎不会格式错误这也是为什么Function Calling被认为比纯提示工程更可靠的原因——它走的是模型内部的“专用通道”而非“通用文本生成通道”。四、扩展现代结构化生成的进阶技术4.1 Instructor —— 结构化输出的“瑞士军刀”Instructor是一个开源Python库专门为简化LLM结构化输出而设计。它在Pydantic的基础上提供了一层优雅的封装支持OpenAI、Anthropic、Cohere等多种模型。核心特点特点说明简化代码几行代码即可实现结构化输出无需手动编写解析逻辑自动重试验证失败时自动重试并包含错误信息提高成功率流式支持支持流式结构化输出逐步获取验证后的对象多模型兼容同一套代码可切换不同LLM提供商import instructor from openai import OpenAI from pydantic import BaseModel, Field # 1. 定义数据模型 class UserInfo(BaseModel): name: str Field(description用户姓名) age: int Field(description用户年龄) skills: list[str] Field(description技能列表) # 2. 创建带instructor的客户端 client instructor.from_openai(OpenAI()) # 3. 直接返回Pydantic对象 user_info: UserInfo client.chat.completions.create( modelgpt-4, messages[ {role: user, content: 张三今年30岁擅长Python和Go语言} ], response_modelUserInfo, # 只需指定响应模型 ) print(user_info.name) # 张三 print(user_info.age) # 30 print(user_info.skills) # [Python, Go语言]Instructor vs LangChain PydanticOutputParser两者核心目标一致但Instructor的API更简洁且支持更灵活的验证器和重试策略在现代LLM开发中被广泛采用。4.2 Outlines —— 本地模型的“格式锁”对于本地部署的开源模型Outlines提供了基于正则表达式和上下文无关语法的结构化生成能力通过“格式锁”logit bias技术在Token生成层面强制约束输出格式。工作原理Outlines定义了JSON Schema对应的正则表达式然后在每个Token生成时只允许生成那些“能使输出继续匹配该正则表达式”的Token从而实现100%的格式保证。import outlines # 定义数据模型 outlines.pydantic_model class PersonInfo: name: str age: int # 生成符合模型的结构化输出 model outlines.models.transformers(microsoft/Phi-3-mini-4k-instruct) generator outlines.generate.json(model, PersonInfo) result generator(Extract name and age: John is 25 years old.) print(result) # PersonInfo(nameJohn, age25)适用场景本地部署的LLM不依赖外部API对成本敏感的应用无需为每次调用付费需要严格格式保证的生产环境4.3 Guidance —— 控制生成过程的“高级提示”Guidance是Microsoft开源的库允许开发者通过约束解码Constraint Decoding来控制LLM的生成过程确保输出符合特定格式。与Outlines的对比维度OutlinesGuidance核心原理基于JSON Schema的正则表达式约束基于模板的交互式约束适用模型Transformers兼容的本地模型支持本地模型和部分API学习曲线相对平缓较高需要学习特定语法一句话总结对于在线API调用使用Instructor最方便对于本地模型部署使用Outlines或Guidance实现100%格式保证对于快速原型开发使用LangChain的PydanticOutputParser足够。4.4 流式结构化输出实时处理JSON在实时应用中LLM的推理时间可能成为瓶颈。流式结构化输出允许在模型生成完整JSON之前就开始逐块解析和展示数据。实现方案LangChain的stream()通过chain.stream()获取流式输出但需要配合特殊解析器。Instructor的流式支持create_partial()方法支持流式获取Pydantic对象的渐进式构建。手动增量解析使用ijson等库增量解析JSON流。# Instructor流式示例 import instructor from openai import OpenAI client instructor.from_openai(OpenAI()) # 流式获取结构化数据 stream client.chat.completions.create_partial( modelgpt-4, messages[ {role: user, content: 列出5个Python库及其用途} ], response_modellist[LibraryInfo], ) for partial_object in stream: print(f已接收 {len(partial_object)} 个项目) # 可以实时展示/处理部分结果五、结构化生成的常见挑战与最佳实践5.1 常见挑战及解决方案挑战表现解决方案格式不一致有时返回纯文本有时返回JSON使用Function Calling或结构化生成库Instructor、Outlines字段缺失LLM漏掉了某些字段设置默认值使用验证器确保字段存在在提示中强调“必须包含所有字段”类型错误数字字段返回了字符串使用Pydantic自动类型转换使用Field(..., coerceTrue)中文JSON解析问题JSON中包含中文导致解析失败确保使用json.loads(..., ensure_asciiFalse)使用Pydantic自动处理复杂嵌套结构LLM难以生成深层嵌套的JSON拆分为多个简单的子结构使用Few-shot示例引导API调用失败Function Calling的网络请求失败实现重试机制使用tenacity库处理重试逻辑5.2 最佳实践总结实践说明优先级使用Pydantic定义Schema统一的模型定义 → 自动生成JSON Schema → 自动验证⭐⭐⭐ 必做Field描述要清晰Field(description用户姓名例如张三)⭐⭐⭐ 必做提供Few-shot示例让LLM学习输出格式特别是复杂结构⭐⭐ 推荐设置temperature0降低随机性确保格式稳定性⭐⭐ 推荐实现验证和重试格式错误时自动重试附带错误信息⭐⭐ 推荐使用系统性提示在系统提示中统一说明格式要求而非每次重复⭐ 可选监控输出质量记录格式错误率持续优化⭐ 可选5.3 格式化生成的完整流程模板from pydantic import BaseModel, Field, ValidationError from typing import List import json # 1. 定义数据模型 class OutputSchema(BaseModel): field1: str Field(description...) field2: int Field(description...) # 2. 构建提示 def build_prompt(input_text: str, schema: dict) - str: return f 请从以下文本中提取信息按照JSON Schema返回。 Schema: {json.dumps(schema, ensure_asciiFalse, indent2)} 文本: {input_text} 只返回JSON不要包含其他内容。 # 3. 调用LLM 解析 验证含重试 def extract_with_validation(text: str, max_retries: int 3): schema OutputSchema.model_json_schema() for attempt in range(max_retries): response llm.invoke(build_prompt(text, schema)) try: data json.loads(response.content) return OutputSchema.model_validate(data) except (json.JSONDecodeError, ValidationError) as e: print(f尝试 {attempt1} 失败: {e}) continue raise Exception(所有尝试均失败无法提取有效信息)六、总结方法适用场景优点缺点LangChain Output Parsers快速原型、框架统一开箱即用、LCEL集成依赖框架LlamaIndex Pydantic ProgramsLlamaIndex生态与RAG流程紧密集成依赖框架纯提示工程轻量场景、无框架依赖简单直接、无依赖格式不稳定Function Calling工具调用、API交互最稳定、原生支持需要LLM支持Instructor现代Python LLM开发简洁优雅、自动重试需要额外库Outlines/Guidance本地模型部署100%格式保证仅支持本地模型如果本文对你有帮助欢迎点赞、收藏、转发有任何疑问欢迎在评论区交流讨论

相关新闻