DLAI-Pydantic-笔记-全-
DLAI Pydantic 笔记(全)
001:欢迎来到LLM工作流的Pydantic课程 🎉
在本课程中,我们将学习如何使用Python的Pydantic包,来让大型语言模型(LLM)生成结构化的输出。这意味着,我们可以精确地控制LLM输出数据的格式。
概述
通常,LLM生成的是自由格式的文本。这在某些场景下非常有用,例如总结一篇文章或构思一个新菜谱。然而,当我们将LLM集成到一个更大的软件系统中时,情况就不同了。这类系统通常包含多个组件,我们需要以一种可预测的方式,将LLM生成的数据传递给下一个组件。这时,Pydantic和结构化输出就能发挥巨大作用。
结构化输出的重要性
为了理解结构化输出的价值,让我们设想一个客户支持应用的场景。
用户可能会提交一个请求,例如“我忘记了密码”,或者一个投诉,例如“我对购买的产品不满意”。我们可以将这个客户查询传递给一个LLM,并让它生成一个结构化的响应。

这个响应可能包含以下字段:
- 用户信息(如姓名、邮箱)
- 请求的文本内容
- 优先级
- 类别
- 其他信息(例如,这是否是一个投诉,以及相关的标签或关键词)

然后,你的系统可以根据这个结构化的响应来决定下一步做什么。
- 如果是一个紧急问题,你可能会创建一个支持工单,并将其转给人工客服进行跟进。
- 对于“我忘记了密码”这类简单请求,你可能会将这个结构化响应传递给另一个LLM智能体。该智能体可以调用一个工具(例如一个函数),去查找能帮助用户的常见问题解答(FAQ)。



无论是让LLM创建一个具有特定字段和内容的结构化响应以生成支持工单,还是让LLM提供调用工具和查找FAQ所需的参数,你都会对每个LLM响应的外观和应包含的数据类型有非常精确的期望。



本课程目标
在本课程中,你将学习使用Pydantic的不同方法,以确保LLM能精确地提供你所需的数据。
换句话说,你将学会验证从LLM获得的响应数据。在此过程中,你还将掌握数据验证技能,这些技能可以帮助你处理任何软件系统中的各种数据,包括:
- 系统的人工输入
- 外部API和数据源
- 系统中需要在组件间传递的任何数据
事实上,Pydantic在LLM兴起之前就已存在,并且是目前最流行的数据验证框架之一。Pydantic每月下载量超过3亿次,这不仅使其成为最受欢迎的数据验证框架,也是整个Python生态中最受欢迎的包之一。这是因为数据验证是任何软件应用的核心。
总结
在接下来的课程中,你将学习如何使用Pydantic从LLM获取结构化输出。同时,你也将构建起一套数据验证技能,这套技能可以应用于任何需要在组件间传递数据的软件应用中。
现在,让我们进入下一个视频,开始学习吧。
002:课程介绍 🎯
在本课程中,我们将学习如何使用 Pydantic 从大型语言模型获取结构化输出。我们将构建一个客户支持系统,并探索如何确保 LLM 的响应符合我们预期的格式和数据要求。
获取结构化输出的简单方法
上一节我们介绍了课程目标,本节中我们来看看获取结构化输出的最直接方法。
最简单的方式是在提示词中明确要求 LLM 以特定格式返回响应。一种非常常见的方法是要求响应采用 JavaScript 对象表示法格式。
如果你还不熟悉 JSON 格式,无需担心。本课程将涵盖处理 LLM 的 JSON 响应所需的所有知识。
在本课程中,我们将构建一个客户支持系统。为了理解其工作原理,可以想象一个系统:用户填写一个包含姓名、邮箱和请求的表单。
例如,用户 Joe 订购了一个遥控飞机套件,但发现到货时缺少零件,现在希望退货。
你可以构建一个如下所示的提示词,要求 LLM 分析用户查询。在花括号中,你将用户输入传递到提示词中,然后要求 LLM 以 JSON 格式响应,并遵循示例结构。
示例结构包含你希望填充的所有字段,例如来自用户输入的姓名、邮箱和查询,以及优先级类别、是否为投诉和一些标签。这只是一个你可能要求返回的假设性字段集合。你可以要求任何你想要的 JSON 结构。
然后,你将这个提示词传递给 LLM。在这种情况下,你希望得到的响应如下所示:姓名、邮箱和查询字段包含用户输入,然后优先级类别被标记为投诉,标签也根据用户输入填充完毕。
JSON 结构类似于 Python 字典,使用花括号开头和结尾,内部采用键值对格式。
在这个提示词中,你只是说:“这是用户的输入,我希望你以完全相同的格式提供响应,包括姓名、邮箱、查询等字段。” 然后,利用这个结构化输出,你可以在系统中自动创建支持工单,或决定是否调用工具以及下一步做什么。
事实证明,当你要求 LLM 返回这样的结构化响应时,在许多情况下,它可以做得相当接近,但并不总是完美的。
简单方法的局限性
上一节我们介绍了直接提示的方法,本节中我们来看看这种方法可能遇到的问题。
例如,LLM 可能在响应中添加额外的文本,比如“这是您请求的 JSON 输出”。或者,它可能添加其他格式,例如三个反引号的 Markdown 格式,这在要求 JSON 的 LLM 响应中非常常见。
除此之外,它可能不会提供你期望的所有字段,或者可能以不可用的方式格式化字段,例如邮箱格式错误。
正是这种响应格式的不可预测性,使得很难直接依赖 LLM 来提供结构化输出。而这就是 Pydantic 发挥作用的地方。

Pydantic 的解决方案
使用 Pydantic,你可以定义数据模型,以指定你期望的数据结构和类型。在 Python 中,为我们刚才查看的请求定义一个 Pydantic 数据模型如下所示:
from pydantic import BaseModel, EmailStr
from typing import List, Literal
class CustomerQuery(BaseModel):
name: str
email: EmailStr
query: str
priority: str
category: Literal["refund request", "information request", "other"]
is_complaint: bool
tags: List[str]
使用这样的 Pydantic 模型,你既定义了字段名称,也定义了模型中每个字段的数据类型。
在这里,name、query 和 priority 被定义为字符串。email 字段被定义为特殊的 Pydantic 数据类型 EmailStr,它要求特定的邮箱格式。category 字段被定义为字面量类型,只能取几个特定值之一,在本例中是“退款请求”、“信息请求”或“其他”。is_complaint 是一个布尔值,只能是真或假。tags 被定义为一个字符串列表。
然后,你可以使用这个 Pydantic 数据模型来验证从 LLM 收到的响应,以确保它符合你的期望。
使用 Pydantic 的两种方法
在本课程中,你将学习使用 Pydantic 数据模型从 LLM 获取结构化输出的两种方法。
第一种,也许是最简单的方法,就是提示 LLM 提供结构化输出,在提示词中给出你希望的结构示例,然后获取 LLM 的响应(你希望它是 JSON 格式),并尝试用它来创建你的数据模型实例。
以下是使用 LLM 响应作为输入的关键代码行:
validated_data = CustomerQuery.model_validate_json(llm_response)
如果 LLM 响应包含额外的意外文本或格式,或者 JSON 本身格式不正确,那么这一步将因验证错误而失败,让你知道 JSON 输入存在问题。
另一方面,如果 JSON 格式有效,但 JSON 中包含的数据与你的模型不匹配,那么这一步将因验证错误而失败,让你知道你的模型期望与输入的 JSON 之间存在不匹配。
如果 model_validate_json 步骤成功,这背后实际上发生了两个步骤:首先解析 JSON,然后使用它来创建你的 Pydantic 数据模型实例。如果所有这些都运行无误,那么你就在使用经过验证的数据,并可以准备将这些数据传递给你系统中的下一个组件。
但是,如果 LLM 响应的验证失败,你可以简单地捕获该验证错误,并在后续请求中将其传回给 LLM,要求它纠正导致错误的问题。这通常效果很好。如果一开始没有得到好的结果,你甚至可以运行多个错误捕获和纠正循环。
然而,事实证明,现在有一种更可靠的方法,可以与许多当前的 LLM API 和智能体框架一起使用。那就是将你的 Pydantic 数据模型作为初始请求的一部分传递给 LLM。这样,你就在 API 调用中精确表达了你的需求,并且可以更可靠地以你期望的格式获取所需的数据。
在某些情况下,当你在 API 调用中传递 Pydantic 模型时,幕后发生的是:这种提示、重试和验证逻辑正在自动为你处理。在其他情况下,LLM 提供商使用一种称为“约束生成”的方法来确保你每次都能获得有效的 JSON。在课程中,你将有机会使用采用这两种方法的框架。

Pydantic 在工具调用中的应用
Pydantic 在 LLM 工作流中的另一个重要用例是工具调用。
例如,对于之前我们看到的用户查询“我忘记了密码”,你可以将该输入传递给 LLM,并让它提供一个如下所示的结构化 JSON 响应。
然后,你可能希望将该 JSON 传递给另一个 LLM,并给予它根据用户查询调用工具的选项。
在这种情况下,你可能希望 LLM 调用一个 FAQ 查找工具。你可以将其视为你在代码中定义的另一个 Python 函数,它可以返回适当的响应,例如密码重置链接和一些用户说明。
Pydantic 在这种工具调用中发挥作用的方式是:你首先定义一个 Pydantic 数据模型,以指定该函数调用的参数。
以下是定义一个名为 FAQLookupArgs 的 Pydantic 模型的示例:
class FAQLookupArgs(BaseModel):
user_query: str
tags: List[str]
然后,你定义你的 lookup_faq_answer 函数,它接受这些 FAQLookupArgs 作为输入。这只是一个普通的 Python 函数,通过将查询中的标签和关键词与 FAQ 条目关键词进行匹配来查找 FAQ 答案。
然后,你可以在 LLM API 调用中定义一个可用的工具。至少对于 OpenAI 的 API 调用,它看起来是这样的:你指定一个函数类型,然后提供函数名称、功能描述以及它接受的输入参数。这正是你的 Pydantic 数据模型发挥作用的地方。
通过从你的 Pydantic 模型传入 model_json_schema,你就是在明确告诉 LLM 你的函数工具接受什么类型的输入参数。
有了这个,你就可以进行如下所示的 API 调用,在 tools 参数中传递你的工具定义。这告诉 LLM,如果提示中的消息表明这是好的下一步,它可以选择使用该工具。
如果 LLM 决定调用该工具,那么它将返回调用该函数所需的参数。然后,你可以使用你的 FAQLookupArgs Pydantic 模型来验证 LLM 提供的参数确实是函数所期望的。
然后,你可以使用这些参数调用该函数,并将结果传递给你系统中的下一步,或传回给 LLM 以完成响应的其余部分。
课程路径与起点
在本课程中,你将学习这些不同的方法,以 JSON 响应或调用函数的参数的形式从 LLM 获取结构化输出,并且你将使用 Pydantic 数据模型来完成这些。
但在深入验证 LLM 响应之前,为了开始学习,我们将首先了解 Pydantic 模型本身的基础知识。为此,我们将首先放大你将构建的这个客户支持系统的用户输入部分。
在下一课中,你将首先学习如何处理以 Python 字典或 JSON 字符串形式到达的用户输入。你将定义一个 Pydantic 数据模型来验证你的用户输入是否是你期望的格式,并且包含你期望的数据。
开始时,你将只定义 name 和 query 字段为字符串,并定义一个 email 字段为 EmailStr。
然后,你将使用包含用户输入的 Python 字典或 JSON 字符串来创建你的用户输入数据模型的实例。通过这个,你将了解 Pydantic 数据模型的工作原理,以及如何在本例中使用它们来验证用户输入数据。

正如我之前所说,本课程的最终目标是使用 Pydantic 从 LLM 获取经过验证的结构化输出。但在达到那个目标之前,我将在下一课中与你见面,探讨 Pydantic 数据模型的基础知识。
总结

本节课中我们一起学习了从 LLM 获取结构化输出的挑战,以及 Pydantic 如何通过定义严格的数据模型来解决这些问题。我们介绍了两种核心方法:先获取响应再验证,以及将模型定义直接集成到 API 调用中。我们还预览了 Pydantic 在工具调用中的应用。下一课,我们将从 Pydantic 模型的基础知识开始,为后续的 LLM 集成打下坚实的基础。
003:Pydantic 模型基础 🧱

在本节课中,我们将学习 Pydantic 数据模型的基础知识,即如何创建它们以及它们如何工作。我们将以验证用户输入为背景进行学习,沿用之前讨论的客户支持系统场景:用户首先填写包含姓名、邮箱和请求内容的表单,然后系统的第一步是验证用户输入是否符合预期,例如邮箱格式是否正确。让我们看看这是如何实现的。



导入与基础模型定义

首先,我们需要导入必要的模块。从 Pydantic 导入 BaseModel,它是创建任何 Pydantic 数据模型的起点。BaseModel 内置了各种数据验证功能,我们可以在此基础上自定义数据模型。同时,我们导入 ValidationError 来捕获错误,导入 EmailStr 作为模型中的一种数据类型,并导入 json 用于后续的 JSON 解析。
from pydantic import BaseModel, ValidationError, EmailStr
import json
接下来,定义一个 Pydantic 数据模型。这里我们创建一个名为 UserInput 的类,它继承自 BaseModel。该类包含三个字段:name、email 和 query。其中,name 和 query 被设置为字符串类型,而 email 则使用了 Pydantic 的 EmailStr 类型。
class UserInput(BaseModel):
name: str
email: EmailStr
query: str
定义好模型后,我们可以创建一个实例。例如,设置 name 为 “Joe User”,email 为 “joeuser@example.com”,query 为 “I forgot my password”。然后可以将其打印出来。
user_input = UserInput(name="Joe User", email="joeuser@example.com", query="I forgot my password")
print(user_input)
虽然打印结果看起来并不复杂,但背后发生了一件很酷的事情:当你像这样创建 Pydantic 数据模型的实例时,Pydantic 已经验证了你输入的数据是否符合模型的预期。在本例中,它验证了 name 是字符串,email 符合 EmailStr 格式,query 也是字符串。因此,这里我们处理的是有效数据。
处理无效数据与验证错误
现在,尝试创建另一个模型实例,其中 email 被设置为一个无效的邮箱地址(例如,不含“@”符号)。运行代码会发生什么?你会得到一个 ValidationError。错误信息会提示:“value is not a valid email address: An email address must have an at-sign”。
这表明 Pydantic 的 EmailStr 格式要求邮箱地址中必须包含“@”符号。尝试添加“@”符号后,可能还会收到其他错误,例如“The part after the at-sign is not valid. It should have a period”,提示“@”符号后需要有一个点号。继续修正,在点号后添加内容,最终会通过验证。
这个小实验让你体会到 Pydantic 在幕后如何检查 email 字段是否符合 EmailStr 格式的预期。它要求字符串中包含“@”符号,“@”符号后某处要有一个点号,并且点号后要有内容。这并不意味着你得到了一个真正可用的有效邮箱地址,只是说明它符合邮箱字符串格式的基本预期。
EmailStr 是 Pydantic 内置的数据类型,但你也可以为模型中的任何特定字段定义自己的字符串模式或其他特定数据类型。
创建验证函数
与其像我们刚才测试 EmailStr 那样直接遇到验证错误,更好的做法是创建一个函数来优雅地处理验证过程。
以下是一个名为 validate_user_input 的函数,它接收一个 Python 字典作为输入数据,并尝试将其解析到 UserInput 模型中。如果成功,则打印“Valid user input created”并输出模型的 JSON 表示;如果出现问题,则捕获错误并打印错误内容。这更接近于你在软件应用程序中的实际做法:尝试创建模型实例,如果出现问题,则捕获错误并决定下一步操作。
def validate_user_input(input_data):
try:
user_input = UserInput(**input_data)
print("Valid user input created")
print(user_input.model_dump_json())
except ValidationError as e:
print("Validation error occurred")
print(e)
现在,我们可以使用这个函数来验证输入数据。首先,定义一个包含有效数据的 Python 字典,然后调用 validate_user_input 函数。
input_data = {"name": "Joe User", "email": "joeuser@example.com", "query": "I forgot my password"}
validate_user_input(input_data)
运行后,会显示“Valid user input created”并打印模型内容。接着,可以尝试其他版本的输入数据。例如,一个只包含 name 和 email 但没有 query 字段的字典。运行后会得到一个验证错误:“query field required”。这表明,当像上面那样定义 Pydantic 数据模型时,每个字段默认都是必需的。
定义可选字段与字段约束
当然,你也可以包含可选字段。接下来,我们将进一步定制模型。
首先,进行更多导入:从 Pydantic 导入 Field,从 typing 导入 Optional,从 datetime 导入 date。
from pydantic import Field
from typing import Optional
from datetime import date
现在,定义一个新版本的 UserInput 模型。它包含之前相同的三个字段,但新增了两个可选字段:order_id 和 purchase_date。
order_id被定义为可选的整数类型。使用Field对象可以为其添加更多自定义和定义。这里,我们设置默认值为None,添加描述“5-digit order number, cannot start with 0”,并设置约束规则:必须是大于 10000 且小于等于 99999 的整数。这强制执行了“5位数订单号且不能以0开头”的规则。purchase_date是另一个可选字段,类型为datetime.date对象,默认值也为None。
class UserInput(BaseModel):
name: str
email: EmailStr
query: str
order_id: Optional[int] = Field(default=None, description="5-digit order number, cannot start with 0", gt=10000, le=99999)
purchase_date: Optional[date] = None
定义好新模型后,可以用一些新的输入数据来测试。使用与最初完全相同的输入数据(不包含可选字段),通过新的 UserInput 模型运行 validate_user_input 函数。运行后没有问题,有效用户输入被创建,order_id 和 purchase_date 显示为 null(在 JSON 表示中)。需要注意的是,null 是模型内容的 JSON 表示形式。如果直接打印模型实例本身,你会看到 order_id 和 purchase_date 实际上是 Python 的 None。
探索模型特性:额外字段与类型强制转换
现在,可以进一步探索这个新模型。
首先,尝试包含所有五个字段的输入数据,其中 order_id 和 purchase_date 是有效值。运行后,会得到包含所有五个字段的有效用户输入。
接着,看看当输入数据包含一些预期之外的额外字段时会发生什么。例如,在数据中添加 system_message 和 iteration 字段。运行后,Pydantic 会直接忽略这些额外字段,仍然创建有效用户输入。这是 Pydantic 的一个特性,也是人们使用 Pydantic 的常见方式:你的系统中可能以 Python 字典或 JSON 数据的形式接收包含大量字段的数据,其中一些你关心,一些你不关心。你可以定义一个 Pydantic 数据模型来获取并验证你关心的字段,而忽略你不关心的字段。
另一个需要注意的点是,在 JSON 输出中,purchase_date 被打印为日期的字符串表示形式,这与你输入的 datetime 对象看起来不同。这是因为 JSON 表示与 Python 表示之间的差异。如果你打印模型实例本身,会发现 purchase_date 仍然是一个 datetime 对象。
更有趣的是,如果你在输入数据中直接使用日期的字符串表示形式(例如 ”2023-10-26″),Pydantic 也能正确处理,它会自动将该字符串转换为 date 对象。这被称为数据类型强制转换。Pydantic 会自动对某些数据类型的特定格式进行这种转换。例如,你也可以将 order_id 以字符串形式输入(如 ”12345″),Pydantic 会自动将其转换为整数。
数据类型强制转换是 Pydantic 的一个特性,它允许你对输入格式更加灵活。当然,如果你希望对特定字段接受的数据格式非常严格,也可以选择关闭类型强制转换。
需要注意的是,强制转换并非双向都行。例如,整数可以从字符串强制转换而来,但字符串不会从整数强制转换而来。如果你在定义为字符串的 name 字段中输入整数 99999,将会得到一个验证错误:“Input should be a valid string”。
处理 JSON 字符串输入
接下来,我们将向验证 LLM 输出的方向迈进一步,从 JSON 数据开始。
首先,定义一个包含模型字段的 JSON 字符串,然后将其解析为 Python 字典。
json_data = ‘{“name”: “Joe User”, “email”: “joeuser@example.com”, “query”: “I forgot my password”}’
input_data = json.loads(json_data)
print(input_data)
解析后,你可以对得到的 Python 字典运行 validate_user_input 函数,并看到有效用户输入被创建。
当我们后续处理 LLM 响应时,将从 LLM 获取字符串形式的数据,并用它来填充 Pydantic 数据模型。你可以尝试不同的 JSON 输入数据。例如,包含以 0 开头的 order_id 的数据。回想一下,order_id 的规则是不能以 0 开头。你可以先将 JSON 解析为 Python 字典(这一步本身没问题,因为它是有效的 JSON),但当你尝试用这些数据填充 UserInput 模型实例时,会遇到验证错误:“order_id should be greater than or equal to 10000”。
因此,当你从 JSON 字符串开始时,验证过程分为两步:首先解析 JSON,然后用 JSON 内容填充数据模型。在实际操作中,更便捷的方法是使用 Pydantic 内置的 model_validate_json 方法。
json_data = ‘{“name”: “Joe User”, “email”: “joeuser@example.com”, “query”: “I forgot my password”, “order_id”: 01234}’ # 注意:以0开头
try:
user_input = UserInput.model_validate_json(json_data)
except ValidationError as e:
print(e)
运行上述代码,你会因为 order_id 字段的问题而得到一个验证错误。但背后发生的过程正是那两步:首先解析 JSON,然后用 JSON 内容填充数据模型。
你还可以测试 JSON 本身无效的情况(例如缺少括号)。这时,运行 model_validate_json 会得到另一种类型的验证错误,提示 JSON 解析失败。因此,根据问题是出在 JSON 输入本身,还是出在 JSON 包含的数据上,你会得到不同类型的验证错误。而这正是我们下一步的方向:使用 Pydantic 的 model_validate_json 方法来验证从 LLM 返回的响应。
总结

本节课中,我们一起学习了 Pydantic 数据模型的基础知识。


你看到了如何通过定义一个继承自 BaseModel 的类来创建数据模型,以及如何为该类定义字段和数据类型。我们还探讨了如何使用 Python 字典或 JSON 数据作为输入来填充数据模型的实例,如何处理验证错误,如何定义可选字段和添加字段约束,以及 Pydantic 的一些重要特性,如忽略额外字段和数据类型强制转换。


在下一课中,你将运用这些技能来验证从 LLM 返回的响应输出。我们下一课见。
004:验证LLM响应 🧪

在本节课中,我们将学习如何提示大型语言模型(LLM)以JSON格式返回结构化响应,并使用Pydantic数据模型来验证该JSON是否完全符合我们的需求。我们将看到,通过要求LLM提供我们所需的结构,通常可以接近目标。然后,我们可以利用Pydantic捕获任何验证错误,并将其反馈给LLM,从而创建一个反馈循环。这样,即使LLM的初始响应存在问题,我们最终也能引导LLM纠正错误,并精确地获得所需内容。这是一种传统的、通过重试反馈循环来获取结构化输出的方法。
概述
我们将从构建一个简单的反馈机制开始。虽然之后会介绍更优雅的方法,但亲手构建一次有助于理解那些更高级方法在幕后自动为我们处理了哪些工作。
现在,让我们开始编写代码。
导入与初始化





首先,导入必要的库。大部分内容与上一课相同,但额外从typing模块导入了List和Literal,用于定义新的Pydantic数据模型。然后,设置OpenAI客户端。




from typing import List, Literal
from pydantic import BaseModel, ValidationError
import openai
# 初始化OpenAI客户端
client = openai.OpenAI()
定义数据模型
接下来,设置一些示例用户输入数据。这里有一个包含所有字段的JSON对象,注意order_id和purchase_date在JSON中定义为null,它们将在模型中读取为默认值None。
user_input_json = {
"customer_name": "John Doe",
"customer_email": "john@example.com",
"order_id": None,
"purchase_date": None,
"query_text": "I haven't received my refund yet."
}
然后,定义与上一课相同的用户输入模型。
class UserInput(BaseModel):
customer_name: str
customer_email: str
order_id: str | None = None
purchase_date: str | None = None
query_text: str
使用model_validate_json方法,基于JSON数据创建一个UserInput模型的实例。
user_input = UserInput.model_validate_json(user_input_json)
现在,定义一个新的Pydantic模型CustomerQuery。它继承自UserInput,因此拥有UserInput的所有字段,并额外添加了四个字段。
class CustomerQuery(UserInput):
priority: str
category: Literal["refund request", "information request", "other"]
is_complaint: bool
tags: List[str]
以下是新增字段的说明:
priority:一个字符串,表示优先级(低/中/高)。category:一个字面量类型,必须是"refund request"、"information request"或"other"三者之一。is_complaint:一个布尔值,表示是否为投诉。tags:一个字符串列表。
构建提示词与调用LLM
接下来,构建一个提示词来引导LLM生成结构化响应。首先,定义一个期望的响应结构示例。
example_structure = {
"priority": "high",
"category": "refund request",
"is_complaint": True,
"tags": ["refund", "delayed"]
}
然后,构造完整的提示词。提示词要求LLM分析用户查询,并以指定的JSON结构返回分析结果,且只返回有效的JSON,不包含任何其他解释或文本。
prompt = f"""
Please analyze this user query: {user_input.model_dump_json()}.
Return your analysis as a JSON object matching this exact structure and data types: {example_structure}.
Respond only with valid JSON. Do not include any other explanations or text formatting before or after the JSON object.
"""
print(prompt)
定义一个函数来调用LLM。
def call_llm(prompt_text: str, model: str = "gpt-4") -> str:
response = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt_text}]
)
return response.choices[0].message.content
尝试调用LLM并查看其响应。
llm_response = call_llm(prompt)
print(llm_response)
我们可能会得到一个接近预期的响应,但请注意,响应内容可能在JSON字符串外部包含了一些额外的格式(如Markdown标记),这会导致问题。
验证响应与处理错误
现在,尝试使用model_validate_json方法验证LLM的响应,看是否能成功创建CustomerQuery模型的实例。
try:
validated_data = CustomerQuery.model_validate_json(llm_response)
print("Validation successful:", validated_data)
except ValidationError as e:
print("Validation error occurred:", e)
很可能会遇到验证错误。错误信息会明确指出问题所在,例如JSON格式无效。
为了更优雅地处理错误,我们定义一个函数来捕获验证错误。该函数尝试验证响应,如果成功则返回验证后的数据和None,如果失败则返回None和错误信息。
def validate_response(model, response_text):
try:
validated_data = model.model_validate_json(response_text)
return validated_data, None
except ValidationError as e:
error_msg = str(e).split('\n')[-2] # 获取错误信息的核心部分
return None, error_msg
validated_data, error = validate_response(CustomerQuery, llm_response)
if error:
print("Validation error:", error)
创建重试反馈循环
捕获到错误后,我们可以构建一个新的提示词,将错误信息反馈给LLM,要求它修正错误。
定义一个函数来创建重试提示词。该函数接收原始提示词、LLM的原始响应和验证错误信息,然后构造一个请求LLM修复错误的提示词。
def create_retry_prompt(original_prompt, original_response, error_message):
retry_prompt = f"""
This is a request to fix an error in the structure of an LLM response.
Here's the original prompt: {original_prompt}
Here's the original LLM response: {original_response}
Here's the error message generated when trying to parse this into the model: {error_message}
Compare the error message and the LLM response to identify what needs to be fixed or removed in the LLM response to resolve this error.
Again, respond only with valid JSON. Do not include any explanations or other text or formatting before or after the JSON string.
"""
return retry_prompt
retry_prompt = create_retry_prompt(prompt, llm_response, error)
print(retry_prompt)
现在,使用这个新的重试提示词再次调用LLM。
retry_response = call_llm(retry_prompt)
print(retry_response)
再次尝试验证这个新的响应。
validated_data, error = validate_response(CustomerQuery, retry_response)
if error:
print("Validation error on retry:", error)
有时,我们可能会遇到新的验证错误,例如category字段的值不在允许的范围内("refund request"、"information request"、"other")。这是因为在最初的示例中,我们只提供了"refund request",LLM可能不知道其他选项。这表明我们的初始设置并非万无一失。
我们可以再次使用create_retry_prompt函数,基于新的错误创建第二轮重试提示词。
second_retry_prompt = create_retry_prompt(retry_prompt, retry_response, error)
second_retry_response = call_llm(second_retry_prompt)
print(second_retry_response)
这个过程可能会变得有些繁琐。本节课的目的之一就是让你亲身体验从LLM获取精确格式的结构化响应所面临的挑战,以及由此产生的一些常见问题。
构建完整的重试系统
接下来,我们将构建一个完整的重试系统。该系统会发送初始提示词,如果JSON或数据模型填充出现问题,则捕获验证错误,构建重试提示词,并重复此过程,直到获得有效响应或达到重试次数上限。
以下是validate_llm_response函数的实现。它接收提示词、数据模型、重试次数和LLM模型作为参数,并尝试在指定次数内获取有效响应。
def validate_llm_response(prompt_text, data_model, max_retries=5, llm_model="gpt-4"):
response = call_llm(prompt_text, llm_model)
for attempt in range(max_retries):
validated_data, error = validate_response(data_model, response)
if error is None:
print(f"Attempt {attempt}: Success!")
return validated_data
else:
print(f"Attempt {attempt} failed. Error: {error}")
retry_prompt = create_retry_prompt(prompt_text, response, error)
response = call_llm(retry_prompt, llm_model)
print(f"Failed after {max_retries} retries.")
return None
现在,使用最初的提示词和CustomerQuery模型来尝试这个函数。
final_result = validate_llm_response(prompt, CustomerQuery)
if final_result:
print("Final validated data:", final_result)
运行多次后,你会发现结果并不稳定。有时需要三次失败尝试,有时两次,有时甚至在五次尝试后仍无法获得有效数据。这正说明了这种系统的脆弱性。我们当前的设置方式确实容易遇到这些错误和重试请求。当然,有方法可以优化初始提示词以提高成功率。
但你现在所经历的,正是那些在后台为你处理所有重试逻辑的库内部发生的事情。在下一课中,我们将尝试使用这些库。
使用Pydantic模型模式优化提示词
在进入下一课之前,我想展示一个使用Pydantic模型的小技巧,它可以帮助你构建更好的提示词,特别是在设置类似我们刚才构建的重试反馈循环时。
这个技巧就是打印出Pydantic模型的JSON模式。
print(CustomerQuery.model_json_schema())
你会看到,即使是一个相对简单的模型,其JSON模式看起来也相当复杂。虽然对人来说解析起来有点困难,但这正是能让LLM更清楚地了解你在结构化响应中期望内容的东西。
因此,与其像我们在课程开头的提示词中那样提供一个示例,不如提供你的Pydantic数据模型的模式。这通常能让你更快地从LLM获得所需的结构化响应。
回顾一下,我们最初构建的提示词是这样的:
# 原始提示词示例
prompt_with_example = f"""
Please analyze this user query: {user_input.model_dump_json()}.
Return your analysis as a JSON object matching this exact structure and data types: {example_structure}.
Respond only with valid JSON...
"""
一个更好的起点是提供如下提示词,其中传入你定义的模型模式(即那个长长的JSON模式),它精确描述了你的模型期望什么。
model_schema = CustomerQuery.model_json_schema()
prompt_with_schema = f"""
Please analyze this user query: {user_input.model_dump_json()}.
Return your analysis as a JSON object that strictly adheres to the following JSON Schema: {model_schema}.
Respond only with valid JSON. Do not include any other explanations or text formatting before or after the JSON object.
"""
当你创建这样的提示词时,你向LLM提供了关于你期望响应的更好指令。
现在,在你的validate_llm_response函数中尝试这个新的提示词。

final_result_schema = validate_llm_response(prompt_with_schema, CustomerQuery)
if final_result_schema:
print("Final validated data using schema:", final_result_schema)
在这种情况下,可能仍然会遇到一次JSON无效的问题,但在此之后,一次重试就能让你获得有效数据。这再次展示了幕后发生的事情:正如你将在下一课看到的,当你可以在API调用中直接传递Pydantic数据模型时,通常发生的情况就是从你的模型中提取数据模式,并将其用作提示LLM的一部分。
总结

在本节课中,我们一起学习了如何通过构建反馈循环来验证和修正LLM的结构化响应。我们首先尝试直接验证LLM的JSON输出,并处理了常见的格式错误和字段值错误。接着,我们构建了一个完整的重试系统来自动化这个过程。最后,我们探索了使用Pydantic模型的JSON模式作为提示词的一部分,以更清晰地向LLM传达我们的需求,这通常能提高首次尝试的成功率。




事实证明,这种提取Pydantic模型的JSON模式、用它构建提示词、然后处理验证并在反馈循环中重试的机制,正是某些允许你在API调用中直接传递Pydantic数据模型的框架在幕后工作的原理。在下一课中,我们将直接研究这些框架,看看如何通过传递Pydantic模型来更优雅地获取所需结构。
005:在API调用中传递Pydantic模型 🚀

在本节课中,我们将学习如何将Pydantic数据模型直接用于调用大语言模型(LLM)提供商的API。你将看到,与上一课中需要构建错误处理和重试逻辑相比,这种方法能让你以更简洁、更可靠的方式获得结构化的响应。
我们将通过多个不同的框架和LLM提供商来实践这一方法。通过亲身体验,你会发现,无论使用哪个LLM提供商或哪个智能体框架,使用Pydantic数据模型来获取所需响应结构的方式本质上都是相同的。现在,让我们进入实践环节,看看具体如何操作。


准备工作与模型定义
首先,我们需要导入必要的库。这里我们使用 instructor 库,它可以作为多个不同LLM提供商API调用的包装器,帮助我们在API调用中使用Pydantic数据模型来获取结构化响应。
instructor 的工作原理与上一课的方法非常相似:你传入Pydantic数据模型,instructor 会提取JSON模型模式,并据此构建提示词。如果响应有任何问题,它会执行一系列重试,以获取你期望的响应。本节课我们将首先结合Anthropic的模型来使用它。
接下来,我们可以定义Pydantic数据模型。这些模型与上一课中使用的相同。



from pydantic import BaseModel
from typing import List
class UserInput(BaseModel):
query: str
user_id: str
timestamp: str
class CustomerQuery(UserInput):
sentiment: str
categories: List[str]
priority: str
tags: List[str]
定义好模型后,我们可以创建一些示例用户输入数据。
example_input = {
"query": "我的订单还没到,已经延迟两天了。",
"user_id": "user_12345",
"timestamp": "2023-10-27T14:30:00Z"
}
我们可以使用模型的 model_validate_json 方法来验证这个输入。
validated_input = UserInput.model_validate_json(example_input)
然后,我们可以构建一个简单的提示词。
prompt = f"分析以下客户查询:{validated_input.query},并提供结构化响应。"
使用Instructor与Anthropic获取结构化响应
现在,我们可以设置LLM调用。这里我们结合使用 instructor 和 Anthropic 来创建一个客户端,在请求响应时传入我们的提示词以及期望的响应格式模型(即 CustomerQuery 模型)。
import instructor
from anthropic import Anthropic
# 创建客户端
client = instructor.from_anthropic(Anthropic())
# 发起调用
response = client.messages.create(
model="claude-3-opus-20240229",
messages=[{"role": "user", "content": prompt}],
response_model=CustomerQuery
)
查看返回的结果。
print(type(response))
print(response)
在这种情况下,你得到的是一个 CustomerQuery 数据模型的实例。instructor 包已经为你完成了LLM调用、必要的重试以及数据验证,并返回了一个填充好数据的有效数据模型实例。这意味着你不再需要额外的验证步骤。
直接使用OpenAI API
除了使用 instructor,你也可以直接通过API调用OpenAI来实现类似功能。
首先,创建OpenAI客户端并调用其测试版API,将响应格式指定为你的数据模型。
from openai import OpenAI
client = OpenAI()
completion = client.beta.chat.completions.parse(
model="gpt-4-turbo-preview",
messages=[{"role": "user", "content": prompt}],
response_format=CustomerQuery
)
response_content = completion.choices[0].message.content
print(type(response_content))
print(response_content)
运行后,你会发现返回的是一个字符串,就像普通的LLM响应一样。这个字符串的格式近似于你的数据模型的表示形式。
与使用 instructor 和 Anthropic 不同,这里你没有直接得到一个已验证的数据模型实例,而是得到了一个JSON字符串。OpenAI在幕后执行了一种称为“约束生成”的技术,因为你指定了期望JSON格式的响应,所以响应的生成方式保证了返回的是有效的JSON。
但这并不保证它一定是你的数据模型的有效实例。接下来,你可以尝试使用 model_validate_json 方法从这个响应内容创建 CustomerQuery 数据模型的实例。
try:
validated_response = CustomerQuery.model_validate_json(response_content)
print(validated_response)
except Exception as e:
print(f"验证失败: {e}")
在这个例子中,它成功了。你得到了有效数据,并且在填充数据模型时没有遇到任何验证错误。但这比直接获取有效模型实例多了一个步骤。
使用OpenAI的Responses API
OpenAI还提供了另一个版本的API,即Responses API。你可以传入一个名为 text_format 的参数,并将你的Pydantic数据模型放入其中。
from openai.types.chat import ChatCompletion
response = client.responses.create(
model="gpt-4-turbo-preview",
input=[{"role": "user", "content": prompt}],
text_format={"type": "object", "schema": CustomerQuery.model_json_schema()}
)
print(type(response))
查看返回的响应结构,你会发现它本身就是一个Pydantic模型。你的 CustomerQuery 模型实例被嵌套在OpenAI返回的另一个Pydantic模型内部。
为了更清楚地查看继承结构,我们可以定义一个简单的函数。
def print_mro(obj):
print([cls.__name__ for cls in obj.__class__.__mro__])
print_mro(response)
运行后,你会看到在顶层是一个嵌套结构,层层解包到底部,最终会找到一个Pydantic的BaseModel。这表明,从OpenAI返回的响应本身就是一个Pydantic模型。
在使用OpenAI的Responses API时,你感兴趣的内容位于 response.output_parsed 内部。
if hasattr(response, 'output_parsed'):
final_output = response.output_parsed
print(type(final_output))
print(final_output)
在这里,你可以看到 CustomerQuery 数据模型的一个实例。这意味着API返回了有效数据,你不再需要进行验证。OpenAI处理了初始JSON的构建(通过约束生成)并为你填充了数据模型,在将结果交还给你之前处理了所有验证。
使用PydanticAI智能体框架
最后,我们来看看PydanticAI。这是一个由Pydantic团队构建的智能体框架。你可以像这样创建一个智能体,并将你的数据模型作为输出类型传入。
import asyncio
from pydantic_ai import Agent
from pydantic_ai.models.gemini import GeminiModel
# 创建智能体
agent = Agent(
model=GeminiModel('gemini-1.5-pro'),
result_type=CustomerQuery
)
async def run_agent():
result = await agent.run(prompt)
return result
# 在Jupyter notebook环境中运行异步函数
response = asyncio.run(run_agent())
print(response.output)
同样,在 response.output 中,你得到了 CustomerQuery 数据模型的一个实例,有效数据直接包含在响应中。
你可以轻松地切换到另一个模型提供商,例如OpenAI。
from pydantic_ai.models.openai import OpenAIModel
agent_openai = Agent(
model=OpenAIModel('gpt-4-turbo-preview'),
result_type=CustomerQuery
)
async def run_agent_openai():
result = await agent_openai.run(prompt)
return result
response_openai = asyncio.run(run_agent_openai())
print(response_openai.output)

这同样成功了。你只需要更改模型字符串,就可以调用不同的LLM提供商。
总结
在本节课中,我们学习了如何将Pydantic数据模型直接用于调用不同LLM提供商和智能体框架的API。我们已经看到,这是从LLM获取结构化输出和已验证数据的有效方法,比上一课处理起来省事得多。
你接触了多种方法:
- 使用
instructor库作为包装器。 - 直接使用OpenAI API并手动验证JSON。
- 使用OpenAI的Responses API直接获取Pydantic模型实例。
- 使用PydanticAI智能体框架。
这些方法的核心思想是一致的:利用Pydantic模型来定义和约束LLM的输出结构,从而简化开发流程,提高代码的可靠性。正如我们所发现的,在LLM工作流中,Pydantic模型无处不在。



接下来,我们将探讨工具调用(Tool Calling),这是Pydantic模型在LLM工作流中的另一个重要用例。在下节课中,你将在工具的定义中使用Pydantic模型,并将这些工具传递给LLM。这将使LLM能够返回调用工具(如函数或API)所需的精确参数。我们下节课见。
006:在API调用中传递Pydantic模型 🛠️

概述

在本节课中,我们将学习如何将之前学到的“使用Pydantic数据模型从LLM获取结构化响应”的知识,与一项新能力相结合:在工具定义中使用Pydantic模型,并将其传递给LLM API调用。在LLM工作流中使用Pydantic主要有两大场景:结构化响应和工具调用。本节课的目标就是结合这两者,在你的应用程序中实现可靠的结构化工具调用。


整体流程概览
本节课代码较多,我们先从宏观上了解整个流程,这有助于后续理解。
起点与之前相同:获取包含姓名、邮箱和消息(例如“我的订单状态如何?”)的用户输入。
- 首先,使用你的用户输入数据模型来验证该输入。
- 接着,将验证后的用户输入传递给一个LLM调用,要求LLM返回一个有效的
CustomerQuery模型实例。 - 然后,将这个实例传递给另一个LLM API调用,并附带一些工具定义。
- 最后,整合所有信息,生成一个最终的
SupportTicket输出。
以下是实现此流程的代码步骤概览:
- 定义两个新的Pydantic数据模型:
FAQLookupArgs和CheckOrderStatusArgs。它们定义了工具调用所需的参数。 - 定义两个新函数:
lookup_faq_answer(接收FAQLookupArgs)和check_order_status(接收CheckOrderStatusArgs)。 - 创建工具定义,其中包含工具名称(即Python函数)、描述和参数。对于参数,你将传入上述Pydantic数据模型的JSON模式。
- 将这些工具定义传递给API调用,让LLM知道有哪些工具可用。LLM的任务是判断是否需要调用工具,并返回工具调用所需的参数。
- 使用为
FAQLookupArgs或CheckOrderStatusArgs定义的Pydantic数据模型,验证LLM返回的工具参数是否符合要求。 - 调用相应的工具并返回输出。
- 将所有信息(客户查询、工具输出)打包,进行最后一次LLM调用,生成最终的
SupportTicket输出。
接下来,我们将深入代码,逐一实现这些步骤。
代码实现详解
本节将包含大量设置和代码。我们会快速回顾已学过的部分,并在出现新内容时详细解释。
导入与基础设置

首先,导入所有必要的库。这基本上整合了之前课程中的所有内容。
import os
import re
from datetime import datetime
from typing import Optional, Literal
from pydantic import BaseModel, Field, field_validator
from pydantic_ai import Agent
import google.generativeai as genai
from openai import OpenAI
import instructor
from anthropic import Anthropic
定义用户输入与客户查询模型
接下来,定义你的用户输入数据模型。这次我们让order_id字段变得更复杂一些。
order_id原本是整数,现在将改为可选的字符串字段,默认值为None。其描述指明新格式为:三个大写字母,一个短横线,然后是五个数字。
这个例子很有趣,因为在实际应用中,你经常会遇到这种验证逻辑不简单的字段。这正是需要使用Pydantic的field_validator工具的地方。
通过field_validator,你可以在order_id字段上设置验证器,并定义一个函数。在这个函数中,使用正则表达式来检查输入的模式是否符合预期。如果不符合,可以抛出一个ValueError,告知用户问题所在以及期望的格式。
这只是如何在模型内部直接处理自定义验证的一个例子。这也是对输入数据实施安全性检查的一种方式。例如,如果有输入进来,你想确保它不是某种SQL注入或其他恶意输入,就可以使用类似这样的完全自定义的验证逻辑来验证进入数据模型的所有内容。
以下是定义该模型的代码:
class UserInput(BaseModel):
name: str
email: str
message: str
order_id: Optional[str] = Field(
default=None,
description="Order ID in the format: three capital letters, a dash, five numbers. Example: ABC-12345"
)
@field_validator('order_id')
def validate_order_id(cls, v):
if v is None:
return v
pattern = r'^[A-Z]{3}-\d{5}$'
if not re.match(pattern, v):
raise ValueError(f"Order ID '{v}' is invalid. Expected format: ABC-12345")
return v
然后,像之前一样定义你的客户查询数据模型,它继承自UserInput并添加了四个额外字段。
class CustomerQuery(UserInput):
query_category: Literal["order_status", "product_info", "faq", "complaint", "other"]
urgency: Literal["low", "medium", "high"]
sentiment: Literal["positive", "neutral", "negative"]
requires_follow_up: bool
验证输入与生成客户查询
接下来,定义validate_user_input函数,与之前做法相同。
def validate_user_input(user_input_json: str) -> UserInput:
try:
user_input = UserInput.model_validate_json(user_input_json)
return user_input
except Exception as e:
print(f"Validation error: {e}")
raise
然后,定义create_customer_query函数,它接收经过验证的用户数据(JSON字符串形式),并调用LLM来填充一个CustomerQuery实例。这只是对之前做法的一个小调整,本质相同:调用LLM,使用用户输入来填充你的客户查询模型实例。
这里我们使用Pydantic AI框架和Google的Gemini模型。调用agent.run时,指定output_type为CustomerQuery。返回的response.data就是经过验证的CustomerQuery数据模型实例。
def create_customer_query(validated_user_data: UserInput) -> CustomerQuery:
agent = Agent(
model='google-gemini-2.0-flash-exp',
system_prompt="You are a helpful assistant that categorizes customer queries.",
)
user_prompt = f"""
Please analyze the following user input and populate a CustomerQuery instance.
User Input:
{validated_user_data.model_dump_json()}
"""
response = agent.run(user_prompt, output_type=CustomerQuery)
return response.data
现在,可以测试一下目前的功能。
user_input_json = '''
{
"name": "John Doe",
"email": "john@example.com",
"message": "What's the status of my order ABC-12345?",
"order_id": "ABC-12345"
}
'''
validated_input = validate_user_input(user_input_json)
customer_query = create_customer_query(validated_input)
print(customer_query)
运行后,用户输入得到验证,客户查询成功生成,并且输出了一个有效的CustomerQuery数据模型实例。
定义工具参数模型与函数
接下来,定义一些新的Pydantic模型。
首先,定义一个名为FAQLookupArgs的Pydantic模型,它继承自BaseModel,并包含query和tags两个字段。这两个字段将是稍后定义的FAQ查询函数所期望的参数。
class FAQLookupArgs(BaseModel):
query: str
tags: list[str]
然后,定义另一个名为CheckOrderStatusArgs的模型,同样继承自BaseModel,包含order_id和email字段。order_id字段带有与之前相同的字段验证器。这些将是你传递给check_order_status函数的参数。
class CheckOrderStatusArgs(BaseModel):
order_id: str
email: str
@field_validator('order_id')
def validate_order_id(cls, v):
pattern = r'^[A-Z]{3}-\d{5}$'
if not re.match(pattern, v):
raise ValueError(f"Order ID '{v}' is invalid. Expected format: ABC-12345")
return v
接着,定义一些模拟的数据库数据。这里只是一些可供FAQ查询函数或订单状态查询函数实际查找的数据。
我们有一个包含问题、答案和关键词的小型模拟FAQ数据库,以及一个包含三个不同订单(包括状态、预计送达日期、购买日期和邮箱)的小型订单数据库。你无需担心这里的细节,这只是为了完成示例。
# 模拟FAQ数据库
fake_faq_db = [
{"question": "How do I reset my password?", "answer": "Go to 'Forgot Password' on the login page.", "keywords": ["password", "reset", "login"]},
{"question": "What is your return policy?", "answer": "You can return items within 30 days.", "keywords": ["return", "policy", "refund"]},
]
# 模拟订单数据库
fake_order_db = [
{"order_id": "ABC-12345", "status": "shipped", "estimated_delivery": "2024-01-15", "purchase_date": "2024-01-01", "email": "john@example.com"},
{"order_id": "XYZ-67890", "status": "processing", "estimated_delivery": "2024-01-20", "purchase_date": "2024-01-05", "email": "jane@example.com"},
]
然后,定义lookup_faq_answer函数。它接收上面创建的FAQLookupArgs模型实例作为输入。这个函数会遍历模拟的FAQ数据库,根据用户输入中的关键词进行搜索。如果找到答案,则返回答案;否则,返回提示信息。
def lookup_faq_answer(args: FAQLookupArgs) -> str:
for faq in fake_faq_db:
if any(keyword in args.query.lower() for keyword in faq["keywords"]):
return faq["answer"]
return "Sorry, I couldn't find an FAQ answer to your question."
接下来,定义check_order_status函数。它接收CheckOrderStatusArgs Pydantic模型的有效实例作为输入,然后尝试使用order_id和email从那个小型模拟订单数据库中查找匹配的订单。同样,这里的细节无需担心,所有这些都是为了构建客户支持系统中工具调用的示例。
def check_order_status(args: CheckOrderStatusArgs) -> dict:
for order in fake_order_db:
if order["order_id"] == args.order_id and order["email"] == args.email:
return {
"order_id": order["order_id"],
"status": order["status"],
"estimated_delivery": order["estimated_delivery"],
"note": "Order and email match."
}
return {"order_id": args.order_id, "status": "not found", "note": "No matching order found."}
定义工具与最终输出模型
最后,我们来到了有趣的部分:定义将在API调用中传递给LLM的工具。
在这里,我们定义了第一个工具,其名称为lookup_faq_answer(这是如果调用该工具时将执行的函数)。该函数期望的参数是FAQLookupArgs。你将把这些信息传递给LLM的API调用,而你的Pydantic数据模型就在这里用于定义该工具期望的参数。
接着,为check_order_status定义另一个工具,其参数是CheckOrderStatusArgs模型的模式。
tools = [
{
"type": "function",
"function": {
"name": "lookup_faq_answer",
"description": "Look up an answer from the FAQ database based on the user's query and tags.",
"parameters": FAQLookupArgs.model_json_schema(),
},
},
{
"type": "function",
"function": {
"name": "check_order_status",
"description": "Check the status of an order using the order ID and customer email for verification.",
"parameters": CheckOrderStatusArgs.model_json_schema(),
},
},
]
在进入LLM调用之前,作为最后一步,你还需要定义几个Pydantic模型。
这将是你的SupportTicket模型,即系统的最终输出。
首先,定义一个OrderDetails模型,它只包含状态、预计送达日期和备注等几个字段。
class OrderDetails(BaseModel):
status: str
estimated_delivery: str
note: str
然后,你将在SupportTicket中使用OrderDetails。现在,SupportTicket是一个继承自CustomerQuery的模型,因此它拥有CustomerQuery的所有属性,并且你添加了一些新内容:
recommended_next_action:这是一个字面量类型,包含一组可接受的值:escalate_to_agent、send_faq_response、send_order_status或no_action_needed。order_details字段:这里你将引入OrderDetails模型。这是在Pydantic数据模型中使用另一个Pydantic数据模型构建字段的示例。faq_response:用于存放查找FAQ答案后的响应。creation_date:创建日期。
以下是定义这些模型的代码:
class SupportTicket(CustomerQuery):
recommended_next_action: Literal["escalate_to_agent", "send_faq_response", "send_order_status", "no_action_needed"]
order_details: Optional[OrderDetails] = None
faq_response: Optional[str] = None
creation_date: datetime
决定下一步行动(工具调用)
现在,所有基础设施都已构建完成,可以定义你的LLM调用了。
你将定义这个decide_next_action_with_tools函数,它接收你的CustomerQuery数据模型实例作为输入。
首先,定义一个系统提示。为此,你需要先从SupportTicket模型中提取JSON模式,然后构建一个提示,说明你是一个有帮助的客户支持代理,你的工作是根据查询和下面期望的字段与SupportTicket模式来决定应为客户采取什么支持行动。
这里的思路是:用户的输入通过第一次LLM调用转换为CustomerQuery数据模型,然后将其传递给另一个LLM调用,以决定是调用工具还是构建不同形式的支持票证。提示说明,如果关于特定订单ID或FAQ响应的更多信息对响应用户查询有帮助,并且可以通过调用工具获得,那么就调用适当的工具来获取该信息。最后,你将SupportTicket模式作为期望的最终输出结构传入。
请注意,这并不是说“在响应中返回这个结构”,因为你将使用这个LLM调用来决定是否调用工具。但这有助于LLM判断工具调用是否是个好主意。
在这个例子中,你的系统提示就是上面所有这些内容。而你的用户提示则是传入的CustomerQuery模型的model_dump结果。这包含了用户输入中的所有信息,以及第一次LLM调用填充CustomerQuery模型时产生的额外信息。
然后,你将调用OpenAI的GPT-4,并在tools参数中传入那些工具定义,这让LLM知道有哪些工具可以作为下一步调用。
以下是该函数的代码:
def decide_next_action_with_tools(customer_query: CustomerQuery):
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
support_ticket_schema = SupportTicket.model_json_schema()
system_prompt = f"""
You are a helpful customer support agent. Your job is to determine what support action should be taken for the customer based on the query.
Expected fields and structure are defined in the SupportTicket schema below.
If more information on a particular order ID or FAQ response would be helpful in responding to the user query and can be obtained by calling a tool, then call the appropriate tool to get that information.
Support Ticket Schema:
{support_ticket_schema}
"""
user_prompt = f"Customer Query: {customer_query.model_dump_json()}"
response = client.chat.completions.create(
model="gpt-4",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
tools=tools,
tool_choice="auto",
)
message = response.choices[0].message
tool_calls = message.tool_calls
return message, tool_calls
现在,可以开始行动了。
调用decide_next_action_with_tools函数,传入customer_query。
你将得到LLM的消息响应、从该消息中提取的任何工具调用,以及输入的提示。接下来的打印语句只是打印出那些工具调用(如果LLM建议调用工具的话)。
运行后,在这种情况下,消息的content部分(即之前从LLM获取自由格式文本响应的位置)现在为null。但在下面的tool_calls中,你可以看到有一个调用check_order_status工具的指示。你可以在这里更清晰地打印出来:tool_calls传入了一个order_id和一个email,并希望调用check_order_status函数。所以,LLM在说:请使用这些参数调用check_order_status函数。
执行工具调用并获取输出
接下来,你需要设置一个函数,如果LLM确实想要调用工具,就执行工具调用。
定义一个名为get_tool_outputs的函数,传入来自LLM响应的tool_calls。然后,如果有工具调用,就根据LLM的指示调用lookup_faq_answer函数或check_order_status函数。
在工具调用的第一步,你将工具调用的函数参数传入相应的Pydantic数据模型(例如FAQLookupArgs),并使用model_validate_json方法来验证LLM传递的参数是否对该工具调用有效。因此,你的Pydantic数据模型在API调用本身中发挥了作用,作为工具定义的一部分,让LLM知道哪些参数是有效的。而当你从LLM获取参数后,首先要做的就是使用该模型来验证LLM返回的参数是否有效。

下一步是继续调用该函数,传入这些参数,并返回结果。对于check_order_status也是同样的操作:首先要验证LLM返回的参数是否符合模型的期望,然后调用该函数。无论哪种情况,你都将返回函数的输出。
运行这个单元格来定义get_tool_outputs函数,并将其用于上一次LLM响应中返回的tool_calls。


def get_tool_outputs(tool_calls):
tool_outputs = []
if tool_calls:
for tool_call in tool_calls:
func_name = tool_call.function.name
args = tool_call.function.arguments
if func_name == "lookup_faq_answer":
# 验证参数
validated_args = FAQLookupArgs.model_validate_json(args)
# 调用函数
result = lookup_faq_answer(validated_args)
tool_outputs.append({"tool": func_name, "result": result})
elif func_name == "check_order_status":
# 验证参数
validated_args = CheckOrderStatusArgs.model_validate_json(args)
# 调用
# 007:6.课程总结 🎉

在本课程中,我们学习了如何使用Pydantic为基于大语言模型(LLM)的应用程序带来结构、可靠性和数据验证能力。
## 课程回顾
上一节我们探讨了Pydantic在复杂工作流中的应用,本节我们将对整个课程内容进行总结。
恭喜你完成了本课程。你已学会如何使用Pydantic为你所有基于LLM的应用程序带来结构、可靠性和验证功能。
不仅如此,你还掌握了一些核心的数据验证技能。这些技能在你构建的任何需要将数据从一个组件传递到另一个组件的软件系统中都很有帮助。
通过本课程的学习,你已经打下了一个非常坚实的基础。
## 后续学习建议
话虽如此,Pydantic的功能远不止我们在本课程中涵盖的这些。
因此,希望你继续学习并提升你的技能。
我期待看到你构建出的作品。😊
## 总结
本节课中我们一起学习了Pydantic的核心概念及其在LLM工作流中的关键作用。我们了解了如何通过定义数据模型来确保数据的结构和类型安全,并掌握了数据验证的基本方法。这些知识为你构建更健壮、可靠的应用程序提供了重要支持。


浙公网安备 33010602011771号