如何用描述符实现 Agent 配置类的参数校验?
直接上干货。描述符的本质,是把属性的存取逻辑从类里抽出来,封装成可复用的校验组件。对 Agent 配置类这种“参数多、校验规则杂”的场景,用描述符比在 init 里写一堆 if 干净得多。
🔧 描述符的工作流¶
描述符就是实现了 set(或 get)的对象。当你在类属性上赋值时,Python 会先看这个类属性有没有 set,有就交给它处理,否则走实例字典。
用一个最简图表示校验过程:
config.temperature = 2.5
│
▼
┌───────────────┐
│ 描述符 __set__ │
│ - 检查类型 │
│ - 检查范围 │
│ - 合规则写入 │
│ 实例字典 │
│ - 不合规抛错 │
└───────────────┘
🧱 实现两个常用校验描述符¶
为了让例子看着像真实代码,我写一个 FloatRange 和一个 StringChoice,都能带默认值。
class FloatRange:
def __init__(self, low, high, default=None):
self.low = low
self.high = high
self.default = default
self.name = None # 在 __set_name__ 里自动拿到字段名
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner):
if instance is None:
return self
# 从实例字典取,没有则返回默认值
return instance.__dict__.get(self.name, self.default)
def __set__(self, instance, value):
if not isinstance(value, (int, float)):
raise TypeError(f"{self.name} 必须是数字")
if not (self.low <= value <= self.high):
raise ValueError(f"{self.name} 必须在 [{self.low}, {self.high}] 之间")
instance.__dict__[self.name] = value
class StringChoice:
def __init__(self, choices, default=None):
self.choices = set(choices)
self.default = default
self.name = None
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__.get(self.name, self.default)
def __set__(self, instance, value):
if value not in self.choices:
raise ValueError(f"{self.name} 必须是 {self.choices} 之一")
instance.__dict__[self.name] = value
🎯 在 Agent 配置类里声明式使用¶
现在声明配置类只需要把描述符写在类体里,像声明字段一样:
class AgentConfig:
temperature = FloatRange(0.0, 2.0, default=1.0)
max_tokens = FloatRange(1, 4096, default=2048)
model = StringChoice(['gpt-4', 'gpt-3.5-turbo'], default='gpt-4')
def __init__(self, **kwargs):
for key, val in kwargs.items():
setattr(self, key, val) # 触发描述符校验
# 使用
cfg = AgentConfig(temperature=1.2, model='gpt-4') # 正常
cfg.temperature = 2.5 # 抛 ValueError
cfg.model = 'gemini' # 抛 ValueError
整个过程没有一行 if 判断,校验全收敛在描述符里。新增参数只需再加一行描述符,AgentConfig 本体零改动。
📊 对比:不用描述符是什么样子¶
如果不用描述符,校验代码会散在 init 里:
class AgentConfig:
def __init__(self, temperature=1.0, max_tokens=2048, model='gpt-4'):
if not 0 <= temperature <= 2:
raise ValueError(...)
if not 1 <= max_tokens <= 4096:
raise ValueError(...)
if model not in {'gpt-4', 'gpt-3.5-turbo'}:
raise ValueError(...)
self.temperature = temperature
...
不仅重复,每次运行时都要跑一遍校验(哪怕值没变),而且不支持属性赋值后的即时校验(cfg.temperature = 999 会直接绕过去,除非你写 @property)。用描述符则完美覆盖:定义时声明规则,赋值时校验,读取时自动取默认值。
🧠 为什么这适合 Agent 配置¶
Agent 的配置往往会被多层继承、Mixin 组合,或从 YAML 文件动态加载。用描述符能让配置类本身保持零逻辑,所有约束都显式写在字段上,可读性和可维护性直接上一个台阶。
并且,描述符是 Python 数据模型的基石,@property 本质上也是描述符,自己写并不会引入额外依赖,很轻量。
所以,当有人问“Agent 配置的参数校验怎么写才优雅”,我会直接亮出描述符方案:字段即规则,赋值即校验,代码胖在组件里,瘦在业务类中。