生成集成 —— 让AI输出“可用”的答案
2026/7/11 2:13:20 网站建设 项目流程

经过前面的数据准备、索引构建和检索优化,我们终于来到了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 Parsers

LangChain提供了一个强大的组件——OutputParsers(输出解析器),专门用于处理LLM的输出。其核心思想是:在发送给LLM的提示中自动注入一段关于如何格式化输出的指令,并在得到结果后将LLM返回的纯文本字符串解析成预期的结构化数据(如Python对象)。

LangChain提供了多种开箱即用的解析器:

解析器用途特点
StrOutputParser基础字符串输出最简单,直接返回LLM输出
JsonOutputParserJSON解析支持嵌套结构和列表的复杂JSON
PydanticOutputParser强类型数据验证与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_object=PersonInfo) # ========================================== # 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='张三' age=30 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_cls=FlightInfo, prompt_template_str="从以下文本中提取航班信息:{text}", llm=llm, verbose=True ) # 3. 执行提取 result = program(text="明天从上海飞往北京的航班有哪些?") print(result.departure) # "上海" print(result.destination) # "北京"

📝关键差异:如果底层LLM支持Function Calling,LlamaIndex会优先使用该功能以获得更可靠的结构化输出;如果不支持,则会回退到将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 Calling

Function 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( model="gpt-4", messages=messages, tools=tools, tool_choice="auto" # 让模型自动决定是否调用工具 ) 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( model="gpt-4", messages=messages, ) print("最终答案:", final_response.choices[0].message.content) # 输出: 杭州今天天气晴朗,气温24℃。
3.3 Function Calling的三大优势
优势说明与纯提示工程的对比
可靠性更高模型原生支持,输出结构稳定、精确纯提示工程依赖模型“自觉遵守”,格式不稳定的概率更高
意图识别不仅格式化输出,更包含“意图到函数的映射”模型能主动判断“这个问题需要调用工具”,而非被动等待指令
与外部世界交互构建能执行实际任务的AI代理(Agent)的基础LLM可以查询数据库、调用API、控制智能家居等
3.4 Function Calling的底层原理(理解更透)

Function Calling的本质是让LLM学会“选函数”而非“写文本”。具体来说:

  1. 训练阶段:在模型预训练时,使用了大量包含“函数调用”格式的数据,让模型学会了识别工具调用的模式。

  2. 推理阶段:模型在输出时,不是直接生成文本,而是生成一个特殊的tool_calls结构。这个结构的格式是固定的(如JSON),保证了一致性。

  3. 系统层面:工具的描述(名称、参数、描述)被包含在系统提示中,模型在生成时会“看到”这些信息,根据用户问题匹配最合适的工具。

从概率角度看

  • 纯提示工程:模型生成文本 → 我们解析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( model="gpt-4", messages=[ {"role": "user", "content": "张三今年30岁,擅长Python和Go语言"} ], response_model=UserInfo, # 只需指定响应模型! ) 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(name="John", age=25)

适用场景

  • 本地部署的LLM(不依赖外部API)

  • 对成本敏感的应用(无需为每次调用付费)

  • 需要严格格式保证的生产环境

4.3 Guidance —— 控制生成过程的“高级提示”

Guidance是Microsoft开源的库,允许开发者通过约束解码(Constraint Decoding)来控制LLM的生成过程,确保输出符合特定格式。

与Outlines的对比

维度OutlinesGuidance
核心原理基于JSON Schema的正则表达式约束基于模板的交互式约束
适用模型Transformers兼容的本地模型支持本地模型和部分API
学习曲线相对平缓较高(需要学习特定语法)

📝一句话总结:对于在线API调用,使用Instructor最方便;对于本地模型部署,使用OutlinesGuidance实现100%格式保证;对于快速原型开发,使用LangChain的PydanticOutputParser足够。

4.4 流式结构化输出:实时处理JSON

在实时应用中,LLM的推理时间可能成为瓶颈。流式结构化输出允许在模型生成完整JSON之前,就开始逐块解析和展示数据。

实现方案

  1. LangChain的stream():通过chain.stream()获取流式输出,但需要配合特殊解析器。

  2. Instructor的流式支持create_partial()方法支持流式获取Pydantic对象的渐进式构建。

  3. 手动增量解析:使用ijson等库增量解析JSON流。

# Instructor流式示例 import instructor from openai import OpenAI client = instructor.from_openai(OpenAI()) # 流式获取结构化数据 stream = client.chat.completions.create_partial( model="gpt-4", messages=[ {"role": "user", "content": "列出5个Python库及其用途"} ], response_model=list[LibraryInfo], ) for partial_object in stream: print(f"已接收 {len(partial_object)} 个项目") # 可以实时展示/处理部分结果

五、结构化生成的常见挑战与最佳实践

5.1 常见挑战及解决方案
挑战表现解决方案
格式不一致有时返回纯文本,有时返回JSON使用Function Calling或结构化生成库(Instructor、Outlines)
字段缺失LLM漏掉了某些字段设置默认值;使用验证器确保字段存在;在提示中强调“必须包含所有字段”
类型错误数字字段返回了字符串使用Pydantic自动类型转换;使用Field(..., coerce=True)
中文JSON解析问题JSON中包含中文导致解析失败确保使用json.loads(..., ensure_ascii=False);使用Pydantic自动处理
复杂嵌套结构LLM难以生成深层嵌套的JSON拆分为多个简单的子结构;使用Few-shot示例引导
API调用失败Function Calling的网络请求失败实现重试机制;使用tenacity库处理重试逻辑
5.2 最佳实践总结
实践说明优先级
使用Pydantic定义Schema统一的模型定义 → 自动生成JSON Schema → 自动验证⭐⭐⭐ 必做
Field描述要清晰Field(description="用户姓名,例如:张三")⭐⭐⭐ 必做
提供Few-shot示例让LLM学习输出格式,特别是复杂结构⭐⭐ 推荐
设置temperature=0降低随机性,确保格式稳定性⭐⭐ 推荐
实现验证和重试格式错误时自动重试,附带错误信息⭐⭐ 推荐
使用系统性提示在系统提示中统一说明格式要求,而非每次重复⭐ 可选
监控输出质量记录格式错误率,持续优化⭐ 可选
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_ascii=False, indent=2)} 文本: {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"尝试 {attempt+1} 失败: {e}") continue raise Exception("所有尝试均失败,无法提取有效信息")

六、总结

方法适用场景优点缺点
LangChain Output Parsers快速原型、框架统一开箱即用、LCEL集成依赖框架
LlamaIndex Pydantic ProgramsLlamaIndex生态与RAG流程紧密集成依赖框架
纯提示工程轻量场景、无框架依赖简单直接、无依赖格式不稳定
Function Calling工具调用、API交互最稳定、原生支持需要LLM支持
Instructor现代Python LLM开发简洁优雅、自动重试需要额外库
Outlines/Guidance本地模型部署100%格式保证仅支持本地模型

如果本文对你有帮助,欢迎点赞、收藏、转发!有任何疑问,欢迎在评论区交流讨论~

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询