如何确保大型语言模型(LLMs)始终以正确的格式生成输出——尤其是在执行任务或调用 API 时?能采取哪些措施来防止 LLM 生成不可预测或不完整的响应,从而破坏应用程序?
LLM 通常会产生大量非结构化数据,这些数据都需要经过处理之后,才能被有效使用。也就是输出具有不可预测性,这会导致错误、浪费时间并增加成本。
为了解决这一问题,OpenAI 和谷歌引入了结构化输出。结构化输出确保模型响应遵循严格的格式,减少错误,并使 LLM 更容易集成到需要一致且机器可读数据的应用程序中。
本文将解释结构化输出的工作原理,并以示例说明其实现方法和好处,以及在实际问题中可能遇到的挑战。
什么是结构化输出?
结构化输出确保模型生成的内容遵循预定义的格式,例如 JSON、XML 或 Markdown。以前,大型语言模型(LLMs)会生成没有特定结构的自由形式文本。而结构化输出提供了一种替代方案,使输出具有机器可读性、一致性,并能够轻松集成到其他系统中。
自由形式文本通常存在歧义且难以解析,而结构化输出则允许直接与软件系统集成,而无需复杂的数据转换。
将输出限制为特定格式意味着结构化输出有助于减少变异性与错误。因此,对于一致性与准确性至关重要的任务(如 API 交互或数据库更新),这些输出更加可靠。
OpenAI 推出结构化输出可以作为 JSON 模式的扩展或改进。JSON 模式于 2023 年发布,旨在确保模型在收到指令后能够输出 JSON。然而,JSON 模式的问题在于它无法始终输出正确的结构。结构化输出强制执行一种结构,使与 API 和其他工具的集成更加可靠,并降低了模型生成错误或无关内容的可能性。
根据 OpenAI 的数据,在结构化输出推出之前,通过提示工程让 LLM 以特定格式响应的可靠性约为 35.9%。而现在,如果将“strict”设置为 true,其可靠性达到了 100%。
结构化输出是如何工作的?
通常,大型语言模型(LLMs)基于概率预测逐个生成文本标记。然而,如果需要生成的文本必须符合特定格式,这种方法就不适用了。结构化输出通过预定义的规则或模式来引导生成过程,从而确保每个标记都符合所需的结构。为了监控和控制标记生成的顺序,通常会使用诸如**有限状态机(Finite State Machine, FSM)**之类的技术。
为了利用像 OpenAI 和 Gemini 这样的模型提供商实现结构化输出,需要完成以下步骤:
-
定义 JSON Schema:JSON 提供了一种标准化的格式,用于定义 JSON 文档中预期的结构和数据类型(例如字符串、数字、数组等)。
-
在 API 请求中引入 Schema:在 API 请求过程中,JSON Schema 会被包含在请求配置中。通过这种方式,模型被指示生成符合指定Schema的输出。
-
生成结构化数据:一旦请求被处理,大型语言模型(LLM)将生成符合定义 Schema 约束的输出。因此,每个响应都是一致的,并遵循预期的格式。
以下详细说明了如何在 OpenAI、Gemini 和 Humanloop 中设置这一功能。
如何在 OpenAI 中使用结构化输出
随着 OpenAI 结构化输出的引入,可以为动态生成用户界面等用例生成结构化数据。
不过需要注意的是,结构化输出仅适用于 OpenAI 最新的大型语言模型(LLM),目前包括:
- gpt-4o-mini-2024-07-18及之后版本
- gpt-4o-2024-08-06及之后版本
话虽如此,以下是通过response_format
和 SDK 辅助工具开始使用 gpt-4o 结构化输出和 gpt-4o mini 结构化输出功能的方法:
1. 定义对象
首先,定义一个表示 JSON Schema 的对象或数据结构,以确保模型遵循特定的结构,例如步骤列表。
以下是一个使用 Pydantic 实现此操作的示例:
from pydantic import BaseModel
class Step(BaseModel):
explanation: str
output: str
class MathResponse(BaseModel):
steps: list[Step]
final_answer: str
假设针对一个数学问题,Step
类定义每个步骤的两个字段:
explanation
:一个字符串,用于解释该步骤。output
:该步骤的结果。
MathResponse
定义了整体响应,其中包括(steps
)和 final_answer
(最终答案)。
2. 在 API 调用中引入对象
定义好对象后,可以使用response_format
参数将其包含在 API 请求中。SDK 会自动将模型的输出解析为所定义的对象。
completion = client.beta.chat.completions.parse(
model = "gpt-4o-2024-08-06",
messages = [
{"role": "system", "content": "You are a helpful math tutor. Guide the user through the solution step by step."},
{"role": "user", "content": "how can I solve 8x + 7 = -23"}
],
response_format = MathResponse
)
在此,调用 OpenAI 的 GPT-4o 解决一个数学问题。response_format
确保模型的输出符合MathResponse
模式。此外,SDK 会负责将响应解析为与所定义模式匹配的对象。
3. 处理边缘情况
有时,模型可能无法生成符合所提供 JSON Schema 的有效响应。
这可能发生在以下情况中:
- 模型因安全原因拒绝回答。
- 由于达到 token 限制,响应不完整。
以下是一个如何处理边缘情况的示例:
try:
completion = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[
{"role": "system", "content": "You are a helpful math tutor. Guide the user through the solution step by step."},
{"role": "user", "content": "how can I solve 8x + 7 = -23"}
],
response_format=MathResponse,
max_tokens=50
)
math_response = completion.choices[0].message
if math_response.parsed:
print(math_response.parsed)
elif math_response.refusal:
# handle refusal
print(math_response.refusal)
except Exception as e:
# Handle edge cases
if type(e) == openai.LengthFinishReasonError:
# Retry with a higher max tokens
print("Too many tokens: ", e)
pass
else:
# Handle other exceptions
print(e)
pass
程序会检查不同的边缘情况,例如由于 token 限制或安全原因导致的不完整响应。如果发生拒绝回答的情况,它会打印出解释;否则,它会处理并打印结构化输出。
4. 以类型安全的方式使用结构化数据
在使用结构化输出时,可以将解析后的 JSON 响应作为在response_format
中定义的类型的对象进行访问。这确保了类型安全,并允许直接处理结构化数据。
示例:
math_response = completion.choices[0].message.parsed
print(math_response.steps)
print(math_response.final_answer)
如何在 Gemini 中使用结构化输出
Gemini 生成的文本默认是无结构的,因此需要研究如何使用 Gemini 的结构化输出功能。
以下是使用 Gemini API 的方法:
1. 设置项目和 API 密钥
在开始之前,请确保项目已设置好,并且 API 密钥已配置。这是向 Gemini API 进行身份验证所必需的。要定义一个 JSON Schema,以指定输出的结构和数据类型。
2. 定义 JSON Schema 并将 Schema 提供给模型
需要定义一个JSON Schema,用于指定输出的结构和数据类型。
之后,将 Schema 提供给模型。有两种方法可以实现这一点:
(1)作为提示词中的文本 :可以直接在提示词中包含所需 JSON 格式的描述。
model = genai.GenerativeModel("gemini-1.5-pro-latest")
prompt = """List a few popular cookie recipes in JSON format.
Use this JSON schema:
Recipe = {'recipe_name': str, 'ingredients': list[str]}
Return: list[Recipe]"""
result = model.generate_content(prompt)
print(result)
(2)通过模型配置:这是一种更正式的方法,通过使用response_schema
为模型配置特定的 Schema。
import typing_extensions as typing
class Recipe(typing.TypedDict):
recipe_name: str
ingredients: list[str]
model = genai.GenerativeModel("gemini-1.5-pro-latest")
result = model.generate_content(
"List a few popular cookie recipes.",
generation_config=genai.GenerationConfig(
response_mime_type="application/json", response_schema=list[Recipe]
),
)
print(result)
3. 使用枚举(Enums)来限制输出
有可能需要将输出限制为特定选项,这在分类任务等应用场景中非常有用。在这种情况下,需要在 Schema 中使用枚举(Enums)。
import google.generativeai as genai
import enum
class Choice(enum.Enum):
PERCUSSION = "Percussion"
STRING = "String"
WOODWIND = "Woodwind"
BRASS = "Brass"
KEYBOARD = "Keyboard"
model = genai.GenerativeModel("gemini-1.5-pro-latest")
result = model.generate_content(
["What kind of instrument is this?", "organ.jpg"],
generation_config=genai.GenerationConfig(
response_mime_type="text/x.enum", response_schema=Choice
),
)
print(result) # Output will be one of the enum options
如何在 Humanloop 中使用结构化输出
Humanloop 的提示管理简化了提示开发过程,使版本控制、评估和协作更加便捷。
在 Humanloop 中,可以通过以下步骤使用结构化输出:
- 创建或选择一个提示词(Prompt)
- 打开“编辑器”(Editor)选项卡
- 选择“响应格式”(Response Format)下拉菜单
- 添加 JSON Schema(手动添加或使用 AI 驱动的 JSON Schema 生成器)
完成后,可以将 Humanloop API 部署到应用程序或智能体中,以利用结构化输出功能。
结构化输出的优点
使用结构化输出的一些优点包括:
- 减少幻觉:通过强制遵守 Schema,结构化输出起到了关键作用。遵守 Schema 意味着不太可能出现意外数据,因此输出中仅包含相关信息。这使得评估 LLM 应用程序变得更加容易,因为结构化输出提供了可预测且可验证的数据格式。
- 无缝集成:确保模型输出始终符合预定义的 Schema,可以简化与系统的集成过程。这对于需要结构化数据格式(如数据库或 API)的应用特别有用,一致性有助于平滑运行。
- 减少变异性:结构化输出限制了模型偏离指定格式的能力,从而减少了变异性。这也使得验证更加容易,因为输出保证符合Schema。因此,可能不需要使用复杂的后处理逻辑,因为可以依赖 Schema 来确保所有必需字段的存在并正确格式化。
结构化输出的挑战
尽管有多种好处,在使用结构化输出时也需要克服一些挑战:
- Schema设计复杂性:有可能需要处理结构复杂的 Schema。设计一个有效的 JSON Schema 可能是一个耗时且复杂的过程。例如,如果要从法律文件或多步骤流程中提取结构化信息,可能需要定义深层次嵌套的 Schema 以捕获所有必要细节而不引入错误。
- 输出上限:存在 16384 个 token 的限制,这意味着任何更大的输出将导致无效的 JSON。因此,在解析或在下游格式中使用数据时会导致问题。例如,如果生成像交易详情这样的对象列表,并且列表过长,则在达到 token 限制之前只会返回部分列表。
- 降低推理能力:虽然结构化输出有助于减少幻觉,但研究表明,相比使用自由形式响应,LLM 的推理能力可能会有所下降。
参考:https://humanloop.com/blog/structured-outputs