跳转至

如何用描述符实现 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 配置的参数校验怎么写才优雅”,我会直接亮出描述符方案:字段即规则,赋值即校验,代码胖在组件里,瘦在业务类中。