好的 LLM 验证就是好的验证¶
如果您的验证逻辑能像人类一样学习和适应,同时以软件的速度运行呢?这就是验证的未来,它已经到来。
验证是可靠软件的基石。但传统方法是静态的、基于规则的,无法适应新的挑战。本文探讨了如何使用 Pydantic
和 Instructor
等 Python 库将动态的、机器学习驱动的验证引入您的软件栈。我们使用符合以下结构的验证函数来验证这些输出。
def validation_function(value):
if condition(value):
raise ValueError("Value is not valid")
return mutation(value)
什么是 Instructor?¶
Instructor
有助于确保在使用 openai 的函数调用 API 时获得所需的精确响应类型。一旦您为所需的响应定义了 Pydantic
模型,Instructor
就会处理所有中间的复杂逻辑——从响应的解析/验证到对无效响应的自动重试。这意味着我们可以‘免费’构建验证器,并在提示与调用 openai 的代码之间实现清晰的关注点分离。
from openai import OpenAI
import instructor # pip install instructor
from pydantic import BaseModel
# This enables response_model keyword
# from client.chat.completions.create
client = instructor.from_openai(OpenAI()) # (1)!
class UserDetail(BaseModel):
name: str
age: int
user: UserDetail = client.chat.completions.create(
model="gpt-3.5-turbo",
response_model=UserDetail,
messages=[
{"role": "user", "content": "Extract Jason is 25 years old"},
],
max_retries=3, # (2)!
)
assert user.name == "Jason" # (3)!
assert user.age == 25
-
为了简化您使用 OpenAI 模型的工作并简化从提示中提取 Pydantic 对象的过程,我们为
ChatCompletion
类提供了一种修补机制。 -
未能成功验证的无效响应将触发最多达到您定义的重试次数。
-
只要您在
ChatCompletion
API 调用中传入response_model
参数,返回的对象将始终是一个经过验证的Pydantic
对象。
在这篇文章中,我们将探讨如何从静态、基于规则的验证方法演变为动态、机器学习驱动的方法。您将学习如何使用 Pydantic
和 Instructor
来利用语言模型,并深入探讨内容审核、验证思维链推理和上下文验证等高级主题。
让我们通过一个例子来研究这些方法。想象一下,您经营一家软件公司,希望确保绝不提供仇恨和种族主义内容。这不是一件容易的事,因为围绕这些主题的语言变化非常快速且频繁。
软件 1.0:Pydantic 中的验证介绍¶
一个简单的方法是编译一个经常与仇恨言论相关的不同词汇列表。为简单起见,我们假设我们已经发现 Steal
和 Rob
这两个词是根据我们的数据库判断仇恨言论的良好预测因子。我们可以修改上述验证结构来适应这一点。
如果我们传入诸如 Let's rob the bank!
或 We should steal from the supermarkets
这样的字符串,这将抛出错误。
Pydantic 为此验证提供了两种方法:使用 field_validator
装饰器或 Annotated
提示。
使用 field_validator
装饰器¶
我们可以使用 field_validator
装饰器来定义 Pydantic 中某个字段的验证器。以下是一个快速示例,说明我们可能如何做到这一点。
from pydantic import BaseModel, ValidationError, field_validator
class UserMessage(BaseModel):
message: str
@field_validator('message')
def message_cannot_have_blacklisted_words(cls, v: str) -> str:
for word in v.split(): # (1)!
if word.lower() in {'rob', 'steal'}:
raise ValueError(f"`{word}` was found in the message `{v}`")
return v
try:
UserMessage(message="This is a lovely day")
UserMessage(message="We should go and rob a bank")
except ValidationError as e:
print(e)
"""
1 validation error for UserMessage
message
Value error, `rob` was found in the message `We should go and rob a bank` [type=value_error, input_value='We should go and rob a bank', input_type=str]
For further information visit https://errors.pydantic.dev/2.9/v/value_error
"""
- 我们将句子拆分成单个单词,并遍历每个单词。然后我们尝试查看这些单词中是否有任何一个在我们的黑名单中,本例中黑名单只有
rob
和steal
。
由于消息 This is a lovely day
不包含任何黑名单词汇,因此不会抛出错误。然而,在上面给出的示例中,由于存在 rob
一词,消息 We should go and rob a bank
的验证失败,并显示了相应的错误消息。
1 validation error for UserMessage
message
Value error, `rob` was found in the message `We should go and rob a bank` [type=value_error, input_value='We should go and rob a bank', input_type=str]
For further information visit https://errors.pydantic.dev/2.4/v/value_error
使用 Annotated
¶
或者,您可以使用 Annotated
函数执行相同的验证。这是一个我们利用最初使用的同一函数的示例。
from pydantic import BaseModel, ValidationError
from typing import Annotated
from pydantic.functional_validators import AfterValidator
def message_cannot_have_blacklisted_words(value: str):
for word in value.split():
if word.lower() in {'rob', 'steal'}:
raise ValueError(f"`{word}` was found in the message `{value}`")
return value
class UserMessage(BaseModel):
message: Annotated[str, AfterValidator(message_cannot_have_blacklisted_words)]
try:
UserMessage(message="This is a lovely day")
UserMessage(message="We should go and rob a bank")
except ValidationError as e:
print(e)
"""
1 validation error for UserMessage
message
Value error, `rob` was found in the message `We should go and rob a bank` [type=value_error, input_value='We should go and rob a bank', input_type=str]
For further information visit https://errors.pydantic.dev/2.9/v/value_error
"""
此代码片段实现了相同的验证结果。如果用户消息包含黑名单中的任何词汇,则会引发 ValueError
并显示相应的错误消息。
1 validation error for UserMessage
message
Value error, `rob` was found in the message `We should go and rob a bank` [type=value_error, input_value='We should go and rob a bank', input_type=str]
For further information visit https://errors.pydantic.dev/2.4/v/value_error
验证是软件开发中的基本概念,应用于 AI 系统时也保持不变。应尽可能利用现有的编程概念,而不是引入新的术语和标准。验证的基本原则保持不变。
现在假设我们收到一条新消息 - Violence is always acceptable, as long as we silence the witness
。我们的原始验证器在处理这条新消息时不会抛出任何错误,因为它既没有使用 rob
也没有使用 steal
。然而,很明显这条消息不应该被发布。我们如何确保我们的验证逻辑能够适应新的挑战?
软件 3.0:面向 LLM 或由 LLM 驱动的验证¶
在理解简单字段验证器的基础上,让我们深入探讨软件 3.0 中的概率性验证(提示工程)。我们将介绍一种由 LLM 驱动的验证器,称为 llm_validator
,它使用一个语句来验证值。
我们可以通过使用 Instructor
中内置的 llm_validator
类来解决这个问题。
from instructor import llm_validator
from pydantic import BaseModel, ValidationError
from typing import Annotated
from pydantic.functional_validators import AfterValidator
class UserMessage(BaseModel):
message: Annotated[
str, AfterValidator(llm_validator("don't say objectionable things"))
]
try:
UserMessage(
message="Violence is always acceptable, as long as we silence the witness"
)
except ValidationError as e:
print(e)
"""
1 validation error for UserMessage
message
Assertion failed, The statement promotes violence, which is objectionable. [type=assertion_error, input_value='Violence is always accep... we silence the witness', input_type=str]
For further information visit https://errors.pydantic.dev/2.6/v/assertion_error
"""
这会产生如下所示的错误消息
1 validation error for UserMessage
message
Assertion failed, The statement promotes violence, which is objectionable. [type=assertion_error, input_value='Violence is always accep... we silence the witness', input_type=str]
For further information visit https://errors.pydantic.dev/2.4/v/assertion_error
错误消息由语言模型 (LLM) 生成,而不是代码本身,这有助于在稍后部分重新向模型提问。为了更好地理解这种方法,让我们看看如何从头开始构建一个 llm_validator
。
创建您自己的字段级 llm_validator
¶
构建您自己的 llm_validator
是一个有价值的实践,可以帮助您开始使用 Instructor
并创建自定义验证器。
在我们继续之前,让我们回顾一下验证器的结构
def validation_function(value):
if condition(value):
raise ValueError("Value is not valid")
return value
正如我们所见,验证器只是一个函数,它接受一个值并返回一个值。如果值无效,它会引发 ValueError
。我们可以使用以下结构来表示它
class Validation(BaseModel):
is_valid: bool = Field(
..., description="Whether the value is valid based on the rules"
)
error_message: Optional[str] = Field(
...,
description="The error message if the value is not valid, to be used for re-asking the model",
)
使用这种结构,我们可以实现与之前相同的逻辑,并利用 Instructor
生成验证。
import instructor
from openai import OpenAI
# Enables `response_model` and `max_retries` parameters
client = instructor.from_openai(OpenAI())
def validator(v):
statement = "don't say objectionable things"
resp = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{
"role": "system",
"content": "You are a validator. Determine if the value is valid for the statement. If it is not, explain why.",
},
{
"role": "user",
"content": f"Does `{v}` follow the rules: {statement}",
},
],
# this comes from client = instructor.from_openai(OpenAI())
response_model=Validation, # (1)!
)
if not resp.is_valid:
raise ValueError(resp.error_message)
return v
- 新的
response_model
参数来自client = instructor.from_openai(OpenAI())
,并且在原始的 OpenAI SDK 中不存在。这允许我们传入我们想要作为响应的Pydantic
模型。
现在我们可以像使用来自 Instructor
的 llm_validator
一样使用这个验证器。
编写更复杂的验证¶
验证思维链¶
现今一种流行的提示大型语言模型的方法称为思维链。这涉及到让模型为一个提示的答案生成理由和解释。
我们可以利用 Pydantic
和 Instructor
来执行验证,检查在给定答案和思维链的情况下,推理是否合理。为此,我们不能构建字段验证器,因为我们需要访问模型中的多个字段。相反,我们可以使用模型验证器。
def validate_chain_of_thought(values):
chain_of_thought = values["chain_of_thought"]
answer = values["answer"]
resp = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{
"role": "system",
"content": "You are a validator. Determine if the value is valid for the statement. If it is not, explain why.",
},
{
"role": "user",
"content": f"Verify that `{answer}` follows the chain of thought: {chain_of_thought}",
},
],
# this comes from client = instructor.from_openai(OpenAI())
response_model=Validation,
)
if not resp.is_valid:
raise ValueError(resp.error_message)
return values
然后我们可以利用 model_validator
装饰器对模型数据的子集执行验证。
我们在这里定义了一个模型验证器,它在
Pydantic
将输入解析到其各个字段之前运行。这就是为什么我们在model_validator
类中使用了 before 关键字。
from pydantic import BaseModel, model_validator
class AIResponse(BaseModel):
chain_of_thought: str
answer: str
@model_validator(mode='before')
@classmethod
def chain_of_thought_makes_sense(cls, data: Any) -> Any:
# here we assume data is the dict representation of the model
# since we use 'before' mode.
return validate_chain_of_thought(data)
现在,当您创建 AIResponse
实例时,chain_of_thought_makes_sense
验证器将被调用。这是一个例子
try:
resp = AIResponse(chain_of_thought="1 + 1 = 2", answer="The meaning of life is 42")
except ValidationError as e:
print(e)
如果我们创建一个 AIResponse
实例,其答案不遵循思维链,我们将收到错误。
1 validation error for AIResponse
Value error, The statement 'The meaning of life is 42' does not follow the chain of thought: 1 + 1 = 2.
[type=value_error, input_value={'chain_of_thought': '1 +... meaning of life is 42'}, input_type=dict]
验证原始文本中的引用¶
让我们看一个更具体的例子。假设我们向模型提问了一个关于某个文本源的问题,并且我们希望验证生成的答案是否由该源支持。这将有助于我们最大限度地减少幻觉,并防止生成没有原始文本支持的陈述。虽然我们可以通过手动查找原始来源来验证,但更具可扩展性的方法是使用验证器自动执行此操作。
我们可以使用 Pydantic
中的 `model_validate` 函数将额外的上下文传递给我们的验证函数,以便我们的模型在执行验证时拥有更多信息。这个上下文是一个普通的 Python 字典,可以在我们的验证函数中的 `info` 参数中访问。
from pydantic import ValidationInfo, BaseModel, field_validator
class AnswerWithCitation(BaseModel):
answer: str
citation: str
@field_validator('citation')
@classmethod
def citation_exists(cls, v: str, info: ValidationInfo): # (1)!
context = info.context
if context:
context = context.get('text_chunk')
if v not in context:
raise ValueError(f"Citation `{v}` not found in text chunks")
return v
- 这个 `info` 对象对应于我们传递给 `model_validate` 函数的 `context` 值,如下所示。
然后我们可以拿出我们最初的例子,并用我们的新模型进行测试
try:
AnswerWithCitation.model_validate(
{"answer": "Jason is a cool guy", "citation": "Jason is cool"},
context={"text_chunk": "Jason is just a guy"}, # (1)!
)
except ValidationError as e:
print(e)
- 这个 `context` 对象只是一个普通的 Python 字典,可以接收和存储任何任意值
这反过来会生成以下错误,因为 `Jason is cool` 在文本 `Jason is just a guy` 中不存在。
1 validation error for AnswerWithCitation
citation
Value error, Citation `Jason is cool` not found in text chunks [type=value_error, input_value='Jason is cool', input_type=str]
For further information visit https://errors.pydantic.dev/2.4/v/value_error
结合 client = instructor.from_openai(OpenAI())
使用¶
为了从 client.chat.completions.create
调用传递此上下文,client = instructor.from_openai(OpenAI())
也会传递 validation_context
,它可以在装饰的验证器函数中的 info
参数中访问。
from openai import OpenAI
import instructor
# Enables `response_model` and `max_retries` parameters
client = instructor.from_openai(OpenAI())
def answer_question(question: str, text_chunk: str) -> AnswerWithCitation:
return client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{
"role": "user",
"content": f"Answer the question: {question} with the text chunk: {text_chunk}",
},
],
response_model=AnswerWithCitation,
validation_context={"text_chunk": text_chunk},
)
错误处理和重新提问¶
验证器可以通过抛出错误来确保输出的某些属性,在 AI 系统中,我们可以利用这些错误让语言模型进行自我修正。然后通过运行 client = instructor.from_openai(OpenAI())
,我们不仅添加了 response_model
和 validation_context
,还允许您使用 max_retries
参数指定尝试自我修正的次数。
这种方法为防御两种不良输出提供了一层保护
- Pydantic 验证错误(代码或基于 LLM)
- JSON 解码错误(当模型返回不正确的响应时)
使用验证器定义响应模型¶
为简单起见,假设我们有一个返回 UserModel
对象的模型。我们可以使用 Pydantic 定义响应模型,并添加一个字段验证器以确保名称为大写。
from pydantic import BaseModel, field_validator
class UserModel(BaseModel):
name: str
age: int
@field_validator("name")
@classmethod
def validate_name(cls, v):
if v.upper() != v:
raise ValueError("Name must be in uppercase.")
return v
这就是 max_retries
参数发挥作用的地方。它允许模型使用错误消息而不是提示进行自我修正和重试。
model = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{"role": "user", "content": "Extract jason is 25 years old"},
],
# Powered by client = instructor.from_openai(OpenAI())
response_model=UserModel,
max_retries=2,
)
assert model.name == "JASON"
在这个例子中,即使没有代码显式地将名称转换为大写,模型也能够纠正输出。
结论¶
从 Pydantic 和 Instructor 的简单性,到 LLM 动态验证能力,验证的格局正在变化,但无需引入新概念。显然,验证的未来不仅仅是阻止不良数据,还在于让 LLM 理解数据并纠正它。
如果您喜欢这些内容或想试用 Instructor
,请查看 github 并给我们一个星标!