各位来宾,各位同仁,下午好!
今天,我们聚焦一个在现代软件工程,尤其是在蓬勃发展的智能体(Agentic)系统领域中,至关重要却常常被忽视的议题:如何确保智能体之间高效、可靠地传递高维数据结构,并在此过程中强制执行严格的类型与数据约束。 我们的主题是“Type-safe Agentic Interfaces:利用 Pydantic 2.0 在运行时强制约束节点间传递的高维向量结构”。
随着人工智能技术的飞速发展,特别是大型语言模型(LLMs)的崛起,我们正步入一个由多个智能体协作完成复杂任务的时代。这些智能体可能负责感知环境、生成嵌入、进行推理、规划行动,甚至直接与外部世界交互。在这样的分布式、模块化系统中,智能体之间的数据流变得异常复杂。我们不再仅仅传递简单的字符串或整数,而是频繁地交换高维向量,例如语义嵌入(semantic embeddings)、特征向量(feature vectors)、注意力权重(attention weights)、状态表示(state representations)等。这些数据不仅维度高,而且往往承载着丰富的语义信息,并对特定的结构、数值范围甚至数学属性(如L2范数)有严格要求。
智能体系统中的数据契约缺失之痛
想象一下,一个智能体系统由“感知智能体”、“推理智能体”和“行动智能体”组成。
- 感知智能体接收原始输入(如图像、文本),将其转换为高维嵌入向量。
- 推理智能体接收这些嵌入向量,结合上下文,生成一个高维的行动计划向量。
- 行动智能体解析行动计划向量,执行具体操作。
在这个链条中,如果感知智能体输出的嵌入向量维度不正确,或者推理智能体期望的向量是L2归一化的但实际不是,会发生什么?
- 运行时错误: 最直接的后果是程序崩溃,通常表现为
IndexError、ValueError或 NumPy 操作失败。 - 静默错误与数据污染: 更糟糕的是,错误可能不会立即显现,而是导致下游智能体产生错误或次优的决策,最终导致整个系统行为异常,且难以追溯。
- 调试噩梦: 在一个由多个服务、多个团队协作构建的复杂系统中,定位这类因数据契约不匹配导致的错误,无异于大海捞针,耗时耗力。
- 协作障碍: 当接口定义不清晰、不强制时,不同团队或开发者之间容易产生误解,导致集成成本高昂,开发效率低下。
传统的数据类型提示(Type Hints)在编译时(或静态分析时)提供了一定的帮助,但它们无法在运行时强制验证数据的内容和结构。例如,list[float] 告诉我们这是一个浮点数列表,但无法保证它的长度是512,也无法保证所有元素都在0到1之间。我们需要一个更强大的工具,一个能够在数据进入或离开智能体接口时,就其结构、内容和语义进行严格审查的守门员。
这就是 Pydantic 2.0 登场的时刻。
Pydantic 2.0:数据验证与模型构建的利器
Pydantic 是一个基于 Python 类型提示的、强大的数据验证和设置管理库。它允许我们使用标准 Python 类型提示来定义数据模式(schemas),并在运行时自动验证数据。Pydantic 2.0 在性能和功能上都取得了显著进步,其核心验证逻辑用 Rust 重写,带来了数倍乃至数十倍的性能提升,这对于处理高维数据和高并发场景至关重要。
Pydantic 的核心理念:
- 声明式数据模型: 使用 Python 类和类型提示来定义数据结构。
- 运行时验证: 在数据被实例化或赋值时自动进行验证。
- 丰富的功能: 支持默认值、可选字段、嵌套模型、自定义验证器等。
- 易于集成: 可与 FastAPI、SQLModel 等框架无缝集成。
Pydantic 2.0 的关键特性:
- Rust 核心: 大幅提升了验证速度和内存效率。
BaseModel: 这是所有 Pydantic 模型的基石。它将 Python 类转换为具有验证、序列化和反序列化功能的强大数据结构。Field: 允许为模型字段提供额外的元数据,如默认值、别名、描述、以及更细粒度的验证规则(例如min_length,max_length,ge,le等)。Annotated: Python 3.9+ 的特性,与typing.Annotated结合Field,可以为类型添加更丰富的元数据,而无需创建复杂的自定义类型。- 自定义验证器:
@field_validator和@model_validator装饰器允许我们编写自定义函数来验证单个字段或整个模型的数据。 TypeAdapter: Pydantic 2.0 引入的一个强大工具,它允许我们对任何类型(不仅仅是BaseModel)进行验证和解析,而无需先定义一个完整的模型。这对于验证函数参数或简单的数据结构非常有用。- 数据转换: Pydantic 不仅仅是验证,它还能将输入数据转换成正确的类型(例如,将字符串 "123" 转换为整数 123)。
利用 Pydantic 建模高维向量
现在,我们来看看如何用 Pydantic 来精确地描述和约束高维向量。
1. 基础建模:列表与数组
最简单的高维向量表示可能是 Python 的 list[float]。Pydantic 可以很好地处理它:
from pydantic import BaseModel, Field, ValidationError
from typing import List
class SimpleEmbedding(BaseModel):
vector: List[float] = Field(..., description="A simple list of floats representing an embedding.")
# 示例:有效数据
try:
embedding_data = {"vector": [0.1, 0.2, 0.3]}
embedding = SimpleEmbedding(**embedding_data)
print(f"Valid embedding: {embedding.vector}")
except ValidationError as e:
print(f"Validation error: {e}")
# 示例:无效数据(非浮点数)
try:
embedding_data_invalid = {"vector": [0.1, "0.2", 0.3]}
embedding = SimpleEmbedding(**embedding_data_invalid) # Pydantic 会尝试转换 "0.2" -> 0.2
print(f"Valid embedding (with conversion): {embedding.vector}")
except ValidationError as e:
print(f"Validation error: {e}")
# 示例:无效数据(非列表)
try:
embedding_data_wrong_type = {"vector": "not a list"}
embedding = SimpleEmbedding(**embedding_data_wrong_type)
except ValidationError as e:
print(f"Validation error: {e}")
输出:
Valid embedding: [0.1, 0.2, 0.3]
Valid embedding (with conversion): [0.1, 0.2, 0.3]
Validation error: 1 validation error for SimpleEmbedding
vector
Input should be a valid list [type=list_type, input_value='not a list', input_type=str]
Pydantic 甚至可以尝试将字符串数字转换为浮点数,这在处理来自 JSON 或表单的数据时非常方便。
然而,list[float] 缺乏对向量维度的严格约束,也无法直接与 NumPy 等科学计算库无缝集成。
2. 集成 NumPy ndarray
在实际的智能体系统中,高维向量通常以 NumPy ndarray 的形式存在。Pydantic 本身不直接支持 ndarray 作为其内部类型,但我们可以通过自定义类型或在验证器中处理它。
为了更好地集成 NumPy,我们可以定义一个自定义类型。一种常见的方法是让 Pydantic 将 ndarray 视为一个可序列化为列表的类型,并在反序列化时将其转换回 ndarray。
import numpy as np
from pydantic import BaseModel, Field, ValidationError, BeforeValidator
from typing import Annotated, List, Any
from typing_extensions import Self # For Pydantic v2 model_validator
# 定义一个将 ndarray 转换为 list[float] 的函数
def validate_ndarray_to_list(v: Any) -> List[float]:
if isinstance(v, np.ndarray):
if v.ndim != 1:
raise ValueError("ndarray must be 1-dimensional")
return v.tolist()
if isinstance(v, list):
# 确保列表中的所有元素都是浮点数或可转换为浮点数
return [float(x) for x in v]
raise TypeError("Input must be a numpy array or a list of numbers")
# 定义一个将列表转换为 ndarray 的函数(用于外部使用,Pydantic内部通常不需要反序列化回ndarray)
def parse_list_to_ndarray(v: List[float]) -> np.ndarray:
return np.array(v, dtype=np.float32)
# 使用 Annotated 和 BeforeValidator 来处理输入
NdArrayAsList = Annotated[List[float], BeforeValidator(validate_ndarray_to_list)]
class NpEmbedding(BaseModel):
vector: NdArrayAsList = Field(..., description="An embedding vector, internally stored as list, can accept ndarray.")
dimension: int = Field(..., description="The expected dimension of the vector.")
# Model validator to enforce dimension after initial field validation
@model_validator(mode='after')
def check_vector_dimension(self) -> Self:
if len(self.vector) != self.dimension:
raise ValueError(f"Vector dimension mismatch: expected {self.dimension}, got {len(self.vector)}")
return self
# 示例:有效数据 (NumPy array)
try:
np_vec = np.random.rand(128).astype(np.float32)
embedding_data = {"vector": np_vec, "dimension": 128}
embedding = NpEmbedding(**embedding_data)
print(f"Valid NpEmbedding (from ndarray): {embedding.vector[:5]}...")
# 转换为 NumPy array 供外部使用
np_output = parse_list_to_ndarray(embedding.vector)
print(f"Converted back to ndarray: {np_output.shape}")
except ValidationError as e:
print(f"Validation error: {e}")
# 示例:有效数据 (Python list)
try:
list_vec = [0.1] * 64
embedding_data = {"vector": list_vec, "dimension": 64}
embedding = NpEmbedding(**embedding_data)
print(f"Valid NpEmbedding (from list): {embedding.vector[:5]}...")
except ValidationError as e:
print(f"Validation error: {e}")
# 示例:维度不匹配
try:
np_vec_wrong_dim = np.random.rand(100).astype(np.float32)
embedding_data = {"vector": np_vec_wrong_dim, "dimension": 128}
embedding = NpEmbedding(**embedding_data)
except ValidationError as e:
print(f"Validation error (dimension mismatch): {e}")
# 示例:非一维数组
try:
np_vec_2d = np.random.rand(10, 10).astype(np.float32)
embedding_data = {"vector": np_vec_2d, "dimension": 100}
embedding = NpEmbedding(**embedding_data)
except ValidationError as e:
print(f"Validation error (2D array): {e}")
输出:
Valid NpEmbedding (from ndarray): [0.038140416, 0.6558005, 0.1706249, 0.45781076, 0.9997194]...
Converted back to ndarray: (128,)
Valid NpEmbedding (from list): [0.1, 0.1, 0.1, 0.1, 0.1]...
Validation error (dimension mismatch): 1 validation error for NpEmbedding
Value error, Vector dimension mismatch: expected 128, got 100 [type=value_error, input_value={'vector': [0.10309328...], 'dimension': 128}, input_type=dict]
Validation error (2D array): 1 validation error for NpEmbedding
vector
Value error, ndarray must be 1-dimensional [type=value_error, input_value=array([[0.06173775, 0.09633333...], [0.8407481 , 0.5367618 ]]), input_type=ndarray]
在这个例子中,我们做了几件事:
validate_ndarray_to_list: 一个预处理函数,它能够接受np.ndarray或list,并将其统一转换为list[float]。如果输入是np.ndarray,它还会检查是否为一维。NdArrayAsList: 使用Annotated和BeforeValidator将validate_ndarray_to_list绑定到List[float]类型上。这意味着任何赋给vector字段的值都会先经过这个验证器处理。@model_validator(mode='after'): 这是一个模型级别的验证器,在所有字段都被单独验证后执行。我们用它来检查vector列表的实际长度是否与dimension字段匹配。parse_list_to_ndarray: 一个辅助函数,用于将 Pydantic 模型内部的list[float]转换回np.ndarray,供下游智能体使用。
通过这种方式,我们不仅强制了输入数据的类型,还强制了其结构和维度。
3. 增强语义与约束:自定义验证器
除了维度,高维向量还可能需要满足其他复杂的业务逻辑或数学属性。例如,嵌入向量可能需要L2归一化,或者某些特征向量的元素必须在特定范围内。
import numpy as np
from pydantic import BaseModel, Field, ValidationError, BeforeValidator, AfterValidator
from typing import Annotated, List, Any
from typing_extensions import Self # For Pydantic v2 model_validator
# 辅助函数:将输入转换为 float 列表
def convert_to_float_list(v: Any) -> List[float]:
if isinstance(v, np.ndarray):
if v.ndim != 1:
raise ValueError("ndarray must be 1-dimensional")
return v.astype(float).tolist()
if isinstance(v, list):
return [float(x) for x in v]
raise TypeError("Input must be a numpy array or a list of numbers")
# 定义一个验证器来检查 L2 范数并归一化
def validate_l2_norm_and_normalize(v: List[float]) -> List[float]:
vec_np = np.array(v, dtype=np.float32)
norm = np.linalg.norm(vec_np)
if norm == 0:
raise ValueError("Vector cannot be a zero vector for L2 normalization.")
# 如果范数不在 [0.99, 1.01] 范围内,则进行归一化
if not (0.99 <= norm <= 1.01): # 允许浮点数误差
return (vec_np / norm).tolist()
return v # 已经归一化,直接返回
# 定义一个验证器来检查元素范围
def validate_elements_range(v: List[float], min_val: float, max_val: float) -> List[float]:
if not all(min_val <= x <= max_val for x in v):
raise ValueError(f"Vector elements must be between {min_val} and {max_val}")
return v
# 组合 Pydantic 类型
# 这是一个预处理类型,将输入转换为 float 列表
FloatListInput = Annotated[List[float], BeforeValidator(convert_to_float_list)]
class SemanticEmbedding(BaseModel):
vector: FloatListInput = Field(..., description="An L2-normalized semantic embedding vector.")
dimension: int = Field(..., gt=0, description="The expected dimension of the embedding.")
# 使用 AfterValidator 在转换成 list[float] 后进行 L2 归一化
normalized_vector: Annotated[
List[float],
AfterValidator(validate_l2_norm_and_normalize)
] = Field(..., alias='vector', description="The L2-normalized version of the vector.")
# Model validator to enforce dimension after initial field validation
@model_validator(mode='after')
def check_dimension_and_range(self) -> Self:
if len(self.vector) != self.dimension:
raise ValueError(f"Vector dimension mismatch: expected {self.dimension}, got {len(self.vector)}")
# 假设这里我们还需要检查向量元素在 -1.0 到 1.0 之间
validate_elements_range(self.vector, -1.0, 1.0)
return self
# 也可以直接在 field 上使用 AfterValidator 链式调用
# vector: Annotated[
# List[float],
# BeforeValidator(convert_to_float_list),
# AfterValidator(validate_l2_norm_and_normalize),
# AfterValidator(lambda v: validate_elements_range(v, -1.0, 1.0))
# ] = Field(..., description="An L2-normalized semantic embedding vector with range check.")
# 示例:有效但未归一化的向量(应被归一化)
try:
vec_unnorm = np.array([1.0, 1.0, 1.0], dtype=np.float32)
data = {"vector": vec_unnorm, "dimension": 3}
emb = SemanticEmbedding(**data)
print(f"Original vector: {vec_unnorm.tolist()}")
print(f"Normalized vector: {emb.vector}")
print(f"L2 norm of result: {np.linalg.norm(np.array(emb.vector)):.4f}")
except ValidationError as e:
print(f"Validation error: {e}")
# 示例:已归一化的向量
try:
vec_norm = np.array([0.57735, 0.57735, 0.57735], dtype=np.float32) # L2 norm ~ 1.0
data = {"vector": vec_norm, "dimension": 3}
emb = SemanticEmbedding(**data)
print(f"Original normalized vector: {vec_norm.tolist()}")
print(f"Resulting vector: {emb.vector}")
print(f"L2 norm of result: {np.linalg.norm(np.array(emb.vector)):.4f}")
except ValidationError as e:
print(f"Validation error: {e}")
# 示例:维度不匹配
try:
vec_wrong_dim = np.array([0.5, 0.5], dtype=np.float32)
data = {"vector": vec_wrong_dim, "dimension": 3}
emb = SemanticEmbedding(**data)
except ValidationError as e:
print(f"Validation error (dimension mismatch): {e}")
# 示例:元素超出范围
try:
vec_out_of_range = np.array([0.5, 1.5, 0.5], dtype=np.float32)
data = {"vector": vec_out_of_range, "dimension": 3}
emb = SemanticEmbedding(**data)
except ValidationError as e:
print(f"Validation error (out of range): {e}")
输出:
Original vector: [1.0, 1.0, 1.0]
Normalized vector: [0.5773502691896258, 0.5773502691896258, 0.5773502691896258]
L2 norm of result: 1.0000
Original normalized vector: [0.57735, 0.57735, 0.57735]
Resulting vector: [0.5773500299453735, 0.5773500299453735, 0.5773500299453735]
L2 norm of result: 1.0000
Validation error (dimension mismatch): 1 validation error for SemanticEmbedding
Value error, Vector dimension mismatch: expected 3, got 2 [type=value_error, input_value={'vector': [0.5, 0.5], 'dimension': 3}, input_type=dict]
Validation error (out of range): 1 validation error for SemanticEmbedding
Value error, Vector elements must be between -1.0 and 1.0 [type=value_error, input_value={'vector': [0.28867513459481287, 0.8660254037844386, 0.28867513459481287], 'dimension': 3}, input_type=dict]
这里我们展示了:
- 链式验证器:
AfterValidator允许在数据类型转换后进行进一步的验证和转换。我们可以将多个AfterValidator组合起来,形成一个验证链。 alias:normalized_vector字段使用alias='vector',这意味着在输入数据中它会查找名为vector的键,但模型内部它被命名为normalized_vector。这在处理输入/输出结构略有差异但逻辑上关联的字段时很有用。- 模型级验证器: 可以在验证所有单个字段后,检查整个模型的数据一致性,例如,这里我们用它再次检查了元素的范围,并确保了维度。
4. 复杂向量结构:多特征组合
智能体系统中的“状态”或“计划”往往不仅仅是一个单一的嵌入向量,而可能是多个不同语义向量的组合,甚至包含其他元数据。Pydantic 的嵌套模型特性可以优雅地处理这种情况。
import numpy as np
from pydantic import BaseModel, Field, ValidationError, BeforeValidator, AfterValidator
from typing import Annotated, List, Any, Literal
from typing_extensions import Self
# 复用之前的验证器
def convert_to_float_list(v: Any) -> List[float]:
if isinstance(v, np.ndarray):
if v.ndim != 1:
raise ValueError("ndarray must be 1-dimensional")
return v.astype(float).tolist()
if isinstance(v, list):
return [float(x) for x in v]
raise TypeError("Input must be a numpy array or a list of numbers")
def validate_l2_norm_and_normalize(v: List[float]) -> List[float]:
vec_np = np.array(v, dtype=np.float32)
norm = np.linalg.norm(vec_np)
if norm == 0:
raise ValueError("Vector cannot be a zero vector for L2 normalization.")
if not (0.99 <= norm <= 1.01):
return (vec_np / norm).tolist()
return v
# 定义一个统一的向量类型
NormalizedVector = Annotated[
List[float],
BeforeValidator(convert_to_float_list),
AfterValidator(validate_l2_norm_and_normalize)
]
class ImageEmbedding(BaseModel):
vector: NormalizedVector = Field(..., min_length=128, max_length=128, description="L2-normalized 128-dim image embedding.")
confidence: float = Field(..., ge=0.0, le=1.0, description="Confidence score of the embedding.")
class TextEmbedding(BaseModel):
vector: NormalizedVector = Field(..., min_length=768, max_length=768, description="L2-normalized 768-dim text embedding.")
language: str = Field(..., pattern=r"^[a-z]{2}$", description="Two-letter language code (e.g., 'en', 'zh').")
class AgentStateVector(BaseModel):
state_id: str = Field(..., min_length=1, description="Unique identifier for the agent state.")
timestamp: float = Field(..., gt=0.0, description="Unix timestamp of when the state was generated.")
# 嵌套的 ImageEmbedding
visual_context: ImageEmbedding = Field(..., description="Visual context embedding of the agent's current perception.")
# 嵌套的 TextEmbedding (可能是可选的)
textual_context: TextEmbedding | None = Field(default=None, description="Textual context embedding, if available.")
# 额外的控制向量,例如注意力权重
attention_weights: NormalizedVector = Field(..., min_length=5, max_length=5, description="Attention weights over 5 different aspects.")
# 动作偏好向量
action_preference: NormalizedVector = Field(..., min_length=3, max_length=3, description="Preferences for 3 possible actions.")
# 一个简单的状态标志
status: Literal["idle", "processing", "error"] = Field(..., description="Current status of the agent.")
# 示例:创建一个有效的 AgentStateVector
try:
image_vec = np.random.rand(128) * 10 # 未归一化,将自动处理
text_vec = np.random.rand(768) # 假设已归一化
attention_vec = np.array([0.1, 0.2, 0.3, 0.2, 0.2]) # 已归一化
action_pref_vec = np.array([0.5, 0.3, 0.2]) # 已归一化
state_data = {
"state_id": "agent-001-state-XYZ",
"timestamp": 1678886400.0,
"visual_context": {
"vector": image_vec.tolist(),
"confidence": 0.95
},
"textual_context": {
"vector": text_vec.tolist(),
"language": "en"
},
"attention_weights": attention_vec.tolist(),
"action_preference": action_pref_vec.tolist(),
"status": "processing"
}
agent_state = AgentStateVector(**state_data)
print("Successfully created AgentStateVector:")
print(f" Visual vector norm: {np.linalg.norm(np.array(agent_state.visual_context.vector)):.4f}")
print(f" Text vector norm: {np.linalg.norm(np.array(agent_state.textual_context.vector)):.4f}")
print(f" Attention weights: {agent_state.attention_weights}")
except ValidationError as e:
print(f"Validation error: {e}")
# 示例:无效数据 - 图像向量维度错误
try:
image_vec_wrong_dim = np.random.rand(64)
state_data_invalid = {
"state_id": "agent-002-state-ABC",
"timestamp": 1678886401.0,
"visual_context": {
"vector": image_vec_wrong_dim.tolist(),
"confidence": 0.8
},
"attention_weights": [0.2, 0.2, 0.2, 0.2, 0.2],
"action_preference": [0.3, 0.3, 0.4],
"status": "idle"
}
agent_state_invalid = AgentStateVector(**state_data_invalid)
except ValidationError as e:
print(f"Validation error (wrong image dim): {e}")
输出:
Successfully created AgentStateVector:
Visual vector norm: 1.0000
Text vector norm: 1.0000
Attention weights: [0.1, 0.2, 0.3, 0.2, 0.2]
Validation error (wrong image dim): 1 validation error for AgentStateVector
visual_context.vector
List should have at least 128 items after validation [type=too_short, input_value=[0.758872013840428, 0.37042845...], input_type=list]
通过这个例子,我们看到 Pydantic 如何允许我们:
- 组合模型:
AgentStateVector包含了ImageEmbedding和TextEmbedding作为其字段。 - 重用验证逻辑:
NormalizedVector类型被重用于多个字段,确保了 L2 归一化和 NumPy 兼容性。 - 精细控制: 使用
min_length和max_length严格控制向量维度,ge/le控制数值范围,pattern控制字符串格式。 - 枚举类型: 使用
Literal定义status字段只能是预设的几个值之一。
构建类型安全的智能体接口
现在我们有了强大的 Pydantic 模型来描述高维数据,下一步就是将它们用作智能体之间的接口契约。
1. 定义智能体节点与接口
在智能体系统中,每个智能体(或节点)都可以被视为一个具有特定输入和输出的函数或服务。我们可以将 Pydantic 模型作为这些函数的参数和返回值类型提示,并在运行时强制执行。
表格:智能体系统接口设计
| 智能体角色 | 输入数据契约 (Pydantic 模型) | 输出数据契约 (Pydantic 模型) | 关键数据类型 | 约束示例 |
|---|---|---|---|---|
| 感知智能体 | RawInput (e.g., ImageURL) |
PerceptionOutput (e.g., ImageEmbedding, ObjectDetections) |
ImageEmbedding |
维度=128, L2归一化, 置信度[0,1] |
| 推理智能体 | ReasoningInput (e.g., ImageEmbedding, TextEmbedding, GoalVector) |
ReasoningOutput (e.g., ActionPlanVector, ContextUpdate) |
ActionPlanVector |
维度=512, L1归一化(概率分布),元素[0,1] |
| 行动智能体 | ActionInput (e.g., ActionPlanVector, ExecutionParameters) |
ActionOutput (e.g., ActionStatus, FeedbackVector) |
FeedbackVector |
维度=64, 元素[-1,1] |
2. 示例:一个多阶段智能体工作流
我们来构建一个简化的智能体工作流,并展示 Pydantic 如何在每个阶段强制数据契约。
智能体接口定义:
import numpy as np
from pydantic import BaseModel, Field, ValidationError, BeforeValidator, AfterValidator
from typing import Annotated, List, Any, Literal, Optional
from typing_extensions import Self
# --- 核心数据类型定义 (复用之前的 NormalizedVector) ---
def convert_to_float_list(v: Any) -> List[float]:
if isinstance(v, np.ndarray):
if v.ndim != 1:
raise ValueError("ndarray must be 1-dimensional")
return v.astype(float).tolist()
if isinstance(v, list):
return [float(x) for x in v]
raise TypeError("Input must be a numpy array or a list of numbers")
def validate_l2_norm_and_normalize(v: List[float]) -> List[float]:
vec_np = np.array(v, dtype=np.float32)
norm = np.linalg.norm(vec_np)
if norm == 0:
# Pydantic allows returning `None` from a validator if the field is Optional
# or raising an error if it's required.
raise ValueError("Vector cannot be a zero vector for L2 normalization.")
if not (0.99 <= norm <= 1.01):
return (vec_np / norm).tolist()
return v
NormalizedVector = Annotated[
List[float],
BeforeValidator(convert_to_float_list),
AfterValidator(validate_l2_norm_and_normalize)
]
# --- 智能体输入/输出模型 ---
# 感知智能体的输出
class ImagePerception(BaseModel):
image_id: str = Field(..., description="Unique ID for the image.")
embedding: NormalizedVector = Field(..., min_length=128, max_length=128, description="L2-normalized 128-dim image embedding.")
confidence: float = Field(..., ge=0.0, le=1.0, description="Confidence score of the image perception.")
class TextPerception(BaseModel):
text_id: str = Field(..., description="Unique ID for the text.")
embedding: NormalizedVector = Field(..., min_length=768, max_length=768, description="L2-normalized 768-dim text embedding.")
language: str = Field(..., pattern=r"^[a-z]{2}$", description="Two-letter language code (e.g., 'en', 'zh').")
# 推理智能体的输入
class ReasoningInput(BaseModel):
visual_context: ImagePerception = Field(..., description="Perceived visual context.")
textual_context: Optional[TextPerception] = Field(default=None, description="Perceived textual context.")
goal_vector: NormalizedVector = Field(..., min_length=256, max_length=256, description="L2-normalized 256-dim goal representation.")
# 推理智能体的输出
class ActionPlan(BaseModel):
plan_id: str = Field(..., description="Unique ID for the action plan.")
action_sequence_vector: NormalizedVector = Field(..., min_length=512, max_length=512, description="L2-normalized 512-dim vector representing action sequence.")
urgency_score: float = Field(..., ge=0.0, le=1.0, description="How urgent is this plan's execution.")
@model_validator(mode='after')
def check_plan_elements_range(self) -> Self:
# 假设 action_sequence_vector 的元素也应该在 [0, 1] 之间
if not all(0.0 <= x <= 1.0 for x in self.action_sequence_vector):
raise ValueError("Action sequence vector elements must be between 0.0 and 1.0.")
return self
# 行动智能体的输入
class ActionExecutionInput(BaseModel):
plan: ActionPlan = Field(..., description="The action plan to execute.")
execution_environment: Literal["simulated", "real_world"] = Field(..., description="Environment for execution.")
# 行动智能体的输出
class ActionExecutionResult(BaseModel):
result_id: str = Field(..., description="Unique ID for the execution result.")
status: Literal["success", "failure", "partial_success"] = Field(..., description="Status of the action execution.")
feedback_vector: NormalizedVector = Field(..., min_length=64, max_length=64, description="L2-normalized 64-dim feedback vector.")
error_message: Optional[str] = Field(default=None, description="Error message if execution failed.")
智能体实现(伪代码,但展示如何使用 Pydantic 验证):
# --- 智能体功能模拟 ---
class PerceptionAgent:
def process_image(self, image_url: str) -> ImagePerception:
# 模拟图像处理和嵌入生成
print(f"[PerceptionAgent] Processing image: {image_url}")
if "bad_image" in image_url:
# 模拟生成不符合规范的嵌入
raw_embedding = np.random.rand(100) # 错误维度
confidence = 0.5
else:
raw_embedding = np.random.rand(128) * 5 # 未归一化
confidence = 0.9
# 使用 Pydantic 模型验证和封装输出
try:
# 注意:Pydantic 会自动处理归一化和维度检查
output = ImagePerception(
image_id=f"img_{hash(image_url)}",
embedding=raw_embedding.tolist(), # 输入可以是非归一化的列表
confidence=confidence
)
print(f"[PerceptionAgent] Image processed, embedding generated (norm={np.linalg.norm(np.array(output.embedding)):.4f}).")
return output
except ValidationError as e:
print(f"[PerceptionAgent] ERROR: Failed to produce valid ImagePerception: {e}")
raise # 重新抛出,以便上游捕获
def process_text(self, text_content: str) -> TextPerception:
print(f"[PerceptionAgent] Processing text: {text_content[:20]}...")
raw_embedding = np.random.rand(768) # 假设已归一化
language = "en"
return TextPerception(
text_id=f"txt_{hash(text_content)}",
embedding=raw_embedding.tolist(),
language=language
)
class ReasoningAgent:
def generate_plan(self, input_data: ReasoningInput) -> ActionPlan:
# 运行时强制约束:输入数据必须符合 ReasoningInput 模型
# Pydantic 在实例化时已经完成了验证,这里只需使用
print(f"[ReasoningAgent] Received input:")
print(f" Visual context ID: {input_data.visual_context.image_id}")
if input_data.textual_context:
print(f" Textual context ID: {input_data.textual_context.text_id}")
print(f" Goal vector norm: {np.linalg.norm(np.array(input_data.goal_vector)):.4f}")
# 模拟计划生成逻辑
raw_plan_vec = np.random.rand(512) # 假设需要归一化且元素在 [0,1]
urgency = 0.85
# 使用 Pydantic 模型验证和封装输出
try:
output = ActionPlan(
plan_id=f"plan_{hash(str(input_data))}",
action_sequence_vector=raw_plan_vec.tolist(),
urgency_score=urgency
)
print(f"[ReasoningAgent] Plan generated (norm={np.linalg.norm(np.array(output.action_sequence_vector)):.4f}).")
return output
except ValidationError as e:
print(f"[ReasoningAgent] ERROR: Failed to produce valid ActionPlan: {e}")
raise
class ActionAgent:
def execute_plan(self, input_data: ActionExecutionInput) -> ActionExecutionResult:
# 运行时强制约束:输入数据必须符合 ActionExecutionInput 模型
print(f"[ActionAgent] Executing plan ID: {input_data.plan.plan_id}")
print(f" Environment: {input_data.execution_environment}")
print(f" Plan vector norm: {np.linalg.norm(np.array(input_data.plan.action_sequence_vector)):.4f}")
# 模拟执行逻辑
if input_data.plan.urgency_score > 0.9:
status = "failure" # 紧急计划容易失败
error_msg = "Overly urgent plan failed."
feedback_vec = np.random.uniform(-1, 0, 64) # 负反馈
else:
status = "success"
error_msg = None
feedback_vec = np.random.uniform(0, 1, 64) # 正反馈
# 使用 Pydantic 模型验证和封装输出
try:
output = ActionExecutionResult(
result_id=f"result_{hash(input_data.plan.plan_id)}",
status=status,
feedback_vector=feedback_vec.tolist(),
error_message=error_msg
)
print(f"[ActionAgent] Plan executed with status: {output.status}.")
return output
except ValidationError as e:
print(f"[ActionAgent] ERROR: Failed to produce valid ActionExecutionResult: {e}")
raise
模拟工作流执行:
# --- 工作流模拟 ---
print("n--- Scenario 1: Successful Workflow ---")
perception_agent = PerceptionAgent()
reasoning_agent = ReasoningAgent()
action_agent = ActionAgent()
try:
# 1. 感知阶段
image_perception = perception_agent.process_image("good_image_url_1")
text_perception = perception_agent.process_text("Some relevant context text.")
goal_vec = np.random.rand(256) # 模拟目标向量
# 2. 推理阶段
reasoning_input_data = ReasoningInput(
visual_context=image_perception,
textual_context=text_perception,
goal_vector=goal_vec.tolist()
)
action_plan = reasoning_agent.generate_plan(reasoning_input_data)
# 3. 行动阶段
action_exec_input = ActionExecutionInput(
plan=action_plan,
execution_environment="simulated"
)
execution_result = action_agent.execute_plan(action_exec_input)
print(f"Workflow completed successfully. Final status: {execution_result.status}")
except ValidationError as e:
print(f"Workflow failed due to validation error: {e}")
except Exception as e:
print(f"Workflow failed due to unexpected error: {e}")
print("n--- Scenario 2: Failure due to invalid PerceptionAgent output ---")
try:
image_perception_invalid = perception_agent.process_image("bad_image_url_with_wrong_dim")
# 这里会因为 ImagePerception 的验证失败而抛出异常,阻止数据流向下游
# 如果 PerceptionAgent 内部没有验证,那么 ReasoningAgent 接收到错误数据后才会崩溃
except ValidationError as e:
print(f"Caught expected validation error from PerceptionAgent: {e.errors()}")
print("Workflow stopped early due to invalid perception output.")
print("n--- Scenario 3: Failure due to invalid ActionPlan (e.g., plan elements out of range) ---")
try:
image_perception = perception_agent.process_image("good_image_url_2")
text_perception = perception_agent.process_text("Another piece of text.")
goal_vec = np.random.rand(256)
reasoning_input_data = ReasoningInput(
visual_context=image_perception,
textual_context=text_perception,
goal_vector=goal_vec.tolist()
)
# 模拟推理智能体生成一个元素超出范围的计划
class BadActionPlan(ActionPlan):
# 覆盖验证器,使其接受错误数据,仅为演示目的
@model_validator(mode='after')
def check_plan_elements_range(self) -> Self:
if not all(-0.5 <= x <= 1.5 for x in self.action_sequence_vector): # 放松范围
raise ValueError("Simulated: Action sequence vector elements are outside [0,1].")
return self
# 直接构造一个不符合规范的 ActionPlan,绕过 ReasoningAgent 自身的验证
# 在实际应用中,这通常是 ReasoningAgent 内部的 Bug 导致的输出错误
bad_plan_data = {
"plan_id": "bad_plan_id",
"action_sequence_vector": [2.0] * 512, # 元素超出 [0,1]
"urgency_score": 0.5
}
# 这里我们模拟 ReasoningAgent 内部有 bug,生成了不合规的 ActionPlan
# 实际上,如果 ReasoningAgent 正确使用了 Pydantic,这里也会在生成时失败
# 但我们为了演示 ActionAgent 端的验证,直接构造一个错误计划
bad_action_plan_output = ActionPlan(**bad_plan_data) # 这里的 ActionPlan 是我们正常定义的,所以会失败
action_exec_input = ActionExecutionInput(
plan=bad_action_plan_output, # 传递一个不合规的计划
execution_environment="real_world"
)
execution_result = action_agent.execute_plan(action_exec_input)
except ValidationError as e:
print(f"Caught expected validation error when executing action: {e.errors()}")
print("Workflow stopped due to invalid action plan.")
except Exception as e:
print(f"Workflow failed due to unexpected error: {e}")
输出(截选关键部分):
--- Scenario 1: Successful Workflow ---
[PerceptionAgent] Processing image: good_image_url_1
[PerceptionAgent] Image processed, embedding generated (norm=1.0000).
[PerceptionAgent] Processing text: Some relevant cont...
[ReasoningAgent] Received input:
Visual context ID: img_1711776595562725516
Textual context ID: txt_365991871217646194
Goal vector norm: 1.0000
[ReasoningAgent] Plan generated (norm=1.0000).
[ActionAgent] Executing plan ID: plan_7781358913917173111
Environment: simulated
Plan vector norm: 1.0000
[ActionAgent] Plan executed with status: success.
Workflow completed successfully. Final status: success
--- Scenario 2: Failure due to invalid PerceptionAgent output ---
[PerceptionAgent] Processing image: bad_image_url_with_wrong_dim
[PerceptionAgent] ERROR: Failed to produce valid ImagePerception: 1 validation error for ImagePerception
embedding
List should have at least 128 items after validation [type=too_short, input_value=[0.11978255307527606, 0.44...], input_type=list]
Caught expected validation error from PerceptionAgent: [{'type': 'too_short', 'loc': ('embedding',), 'msg': 'List should have at least 128 items after validation', 'input': [0.11978255307527606, 0.4475510688099617, 0.9996614144360662, 0.4851174987040439, 0.9942690967008139, 0.8166946327334731, 0.3804245511042767, 0.5484501768822002, 0.5756088277258957, 0.8524317180470222, 0.9080649725860718, 0.05268480749021966, 0.3164923298687729, 0.7062402287714902, 0.6554522436423985, 0.8038753232187648, 0.6970425027552553, 0.7512702581673894, 0.9252033838573132, 0.741001407983637, 0.0984180447321584, 0.2871081389279585, 0.6406734107149257, 0.8267035653457814, 0.1656860368142998, 0.7601614051065476, 0.7099778152504627, 0.3840212716715456, 0.8037380961803525, 0.7303358045618491, 0.893202958479532, 0.00762417618063073, 0.1705663678082692, 0.7086884394998089, 0.5134703759976721, 0.4906935147875952, 0.6279183377771746, 0.07682979261313364, 0.9387431329606138, 0.6397984027731777, 0.2642878939632832, 0.4727402660232532, 0.7314757343609804, 0.1264353482708316, 0.2831206109312001, 0.5475654316377708, 0.7583689408018956, 0.9880190539158548, 0.34294060868612984, 0.07342600207860156, 0.1651523315904834, 0.29778544837372747, 0.6358320491873117, 0.04604900742511449, 0.519391054363293, 0.8239327823568779, 0.8524177428801129, 0.04835265492305888, 0.2319089279743415, 0.10642398418579075, 0.5192135084992562, 0.4552467332766073, 0.9080780287114227, 0.3533411442007077, 0.19897072559779354, 0.9250009696329402, 0.2762030252654157, 0.7397736657805166, 0.6923486337267156, 0.5510009581519781, 0.09849206114421115, 0.7419197022295674, 0.15570076722238495, 0.2476562094895669, 0.6385202652431792, 0.1872167733221989, 0.9669528292437651, 0.6212172782976214, 0.8315185906733435, 0.9930776949216053, 0.12282291917967269, 0.1993475765719912, 0.4632168341629851, 0.4674068367988365, 0.697554900139369, 0.1925350320468087, 0.7963390779774134, 0.3835697334758547, 0.01524355209378122, 0.6271960162590213, 0.30154941914949324, 0.5898869862215893, 0.11059483320695195, 0.4357421128362796, 0.8415758850608056, 0.14798381831828135, 0.42838725916051775, 0.8256561845474326, 0.0890126749453265, 0.2624449836173041, 0.7394870020613204, 0.811568478465492, 0.06322749547631336, 0.9859232306161136, 0.4276184852080373, 0.6322891969248476, 0.8152575231908928, 0.1843236054178521, 0.1748283488219018, 0.3855661605553186, 0.2520638069502931, 0.7684619933555234, 0.8521946399131666, 0.9410196723235659, 0.569508546524376, 0.5401217032768564, 0.9855325881478148, 0.3168341641323381, 0.06401676839352723, 0.08914614631245749, 0.7831855219195982, 0.7392686851216698, 0.6865611484433148, 0.5422616258950669, 0.42886026859570776, 0.9868733979450702, 0.5739832269931818, 0.9016147610013997, 0.07185365516709424, 0.8576403061596791, 0.4300300180709405, 0.4682025707921867, 0.7719630712790708, 0.8970428318712616, 0.17062490089069128, 0.0674251760416973, 0.10640994505315895, 0.7046039474768345, 0.5186259068069415, 0.9575936836472421, 0.1042533860438189, 0.43572886498516096, 0.5977934444535355, 0.9577909062369688], 'context': {'min_length': 128}}]
Workflow stopped early due to invalid perception output.
--- Scenario 3: Failure due to invalid ActionPlan (e.g., plan elements out of range) ---
[PerceptionAgent] Processing image: good_image_url_2
[PerceptionAgent] Image processed, embedding generated (norm=1.0000).
[PerceptionAgent] Processing text: Another piece of...
Validation error for ActionPlan
action_sequence_vector
Value error, Action sequence vector elements must be between 0.0 and 1.0. [type=value_error, input_value=[2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0,