目录
[10.1 元编程:编写能"编写代码"的代码](#10.1 元编程:编写能“编写代码”的代码)
[10.1.1 装饰器高级应用:为模型 API 自动添加重试功能](#10.1.1 装饰器高级应用:为模型 API 自动添加重试功能)
[10.1.2 类装饰器:实现 LLM 客户端的单例模式](#10.1.2 类装饰器:实现 LLM 客户端的单例模式)
[10.2 并发与并行编程:同时处理多个 AI 请求](#10.2 并发与并行编程:同时处理多个 AI 请求)
[10.2.1 协程与异步 I/O:高并发调用多个大模型 API](#10.2.1 协程与异步 I/O:高并发调用多个大模型 API)
[10.2.2 多进程:加速模型评估等 CPU 密集型任务](#10.2.2 多进程:加速模型评估等 CPU 密集型任务)
[10.3 元类:自动注册所有大模型插件](#10.3 元类:自动注册所有大模型插件)
[10.4 描述符:精确控制模型参数的取值](#10.4 描述符:精确控制模型参数的取值)
[10.5 上下文管理器高级用法:统计模型推理耗时](#10.5 上下文管理器高级用法:统计模型推理耗时)
[10.6 动态属性和动态模块导入:灵活加载模型插件](#10.6 动态属性和动态模块导入:灵活加载模型插件)
[10.6.1 动态属性:让配置对象支持任意字段](#10.6.1 动态属性:让配置对象支持任意字段)
[10.6.2 动态模块导入:根据配置加载不同的模型实现](#10.6.2 动态模块导入:根据配置加载不同的模型实现)
[10.7 抽象语法树(AST)操作:分析 AI 代码的依赖](#10.7 抽象语法树(AST)操作:分析 AI 代码的依赖)
[10.8 类型系统与运行时验证:让模型配置更安全](#10.8 类型系统与运行时验证:让模型配置更安全)
[10.8.1 类型注解与 dataclass](#10.8.1 类型注解与 dataclass)
[10.8.2 运行时类型验证(使用 Pydantic)](#10.8.2 运行时类型验证(使用 Pydantic))
[10.9 C扩展开发:调用高性能 C 库加速 token 计数](#10.9 C扩展开发:调用高性能 C 库加速 token 计数)
[10.10 高级设计模式:状态模式管理对话流程](#10.10 高级设计模式:状态模式管理对话流程)
[10.11 最佳实践指南](#10.11 最佳实践指南)
[10.11.1 何时使用高阶特性](#10.11.1 何时使用高阶特性)
[10.11.2 性能优化小技巧](#10.11.2 性能优化小技巧)
[10.12 本章小结](#10.12 本章小结)
当你已经掌握了 Python 的基础语法和面向对象编程后,可能会遇到更复杂的场景:你需要自动重试失败的大模型 API 调用、同时处理上百个对话请求、动态加载不同的模型插件、对模型参数进行类型验证......这些需求需要用到 Python 的高阶特性。
本章将带你学习元编程、并发、元类、描述符等高级概念,并全部用大模型 开发场景举例。虽然内容有一定深度,但我们会尽量用通俗的语言和真实案例帮助你理解,让你在 AI 应用开发中游刃有余。
10.1 元编程:编写能"编写代码"的代码
元编程指的是让代码能够操作、生成或修改其他代码。在 AI 开发中,元编程可以帮助我们自动添加重试机制、实现单例模式、简化插件注册等。
10.1.1 装饰器高级应用:为模型 API 自动添加重试功能
调用大模型 API 时,网络抖动或服务限流可能导致临时失败。我们可以写一个带参数的装饰器,为任何函数自动添加重试逻辑。
python
def retry(max_attempts=3, delay=1):
"""
操作失败自动重试装饰器工厂。
该函数返回一个装饰器,装饰器会将被装饰函数在失败时自动重试指定次数。
参数:
max_attempts (int): 最大尝试次数,默认为3(包括第一次尝试)。
delay (int/float): 每次重试之间的等待秒数,默认为1秒。
返回:
decorator (function): 真正的装饰器函数。
"""
def decorator(func):
"""
真正的装饰器,接收被装饰的函数。
参数:
func (callable): 需要被重试的函数。
返回:
wrapper (function): 包装后的函数,具备重试逻辑。
"""
import time
def wrapper(*args, **kwargs):
"""
包装函数,实现重试逻辑。
参数:
*args: 被装饰函数的任意位置参数。
**kwargs: 被装饰函数的任意关键字参数。
返回:
被装饰函数的返回值(若某次调用成功)。
异常:
RuntimeError: 当达到最大尝试次数仍然失败时抛出。
"""
# 从1开始计数,表示第几次尝试
for attempt in range(1, max_attempts + 1):
try:
# 尝试调用原函数,如果成功则直接返回结果
return func(*args, **kwargs)
except Exception as e:
# 打印当前尝试失败的详细信息
print(f"第 {attempt} 次调用失败: {e}")
# 如果还有重试次数,则等待指定延迟后再继续下一次循环
if attempt < max_attempts:
time.sleep(delay)
# 循环结束仍未成功,说明所有尝试均失败,抛出异常
raise RuntimeError(f"重试 {max_attempts} 次均失败")
return wrapper
return decorator
# 使用示例:模拟一个不稳定的 LLM API(带有70%概率失败)
@retry(max_attempts=3, delay=0.5)
def call_llm(prompt):
"""
模拟调用大模型API,有70%的概率引发网络超时异常。
参数:
prompt (str): 用户输入的提示词。
返回:
str: 模拟的模型回复文本。
"""
import random
# 70%的概率抛出连接异常,模拟不稳定的API
if random.random() < 0.7:
raise ConnectionError("API 网络超时")
# 30%的概率成功返回回复
return f"模型回复: {prompt}"
# 测试调用(由于成功率低,通常会触发重试)
print(call_llm("你好"))
AI 场景说明:你可以用这个装饰器包装任何调用外部大模型 API 的函数,自动处理临时故障,提升系统鲁棒性。
@retry(max_attempts=3, delay=0.5) 是一个装饰器,它将下面的 call_llm 函数"包装"起来,赋予其自动重试的能力:当 call_llm 执行过程中抛出异常(例如网络超时)时,装饰器会自动捕获异常、等待 0.5 秒,然后重新调用该函数,最多重试 3 次(含首次失败后的两次重试)。如果 3 次全部失败,最终会抛出一个 RuntimeError。这样就不需要在函数内部写繁琐的重试循环代码。
运行上述代码,输出如下内容:
python
tianpeng@DESKTOP-4L1UF5S:~/my-ai-service$ poetry run python src/my_ai_service/loop.py
第 1 次调用失败: API 网络超时
模型回复: 你好
tianpeng@DESKTOP-4L1UF5S:~/my-ai-service$ poetry run python src/my_ai_service/loop.py
模型回复: 你好
tianpeng@DESKTOP-4L1UF5S:~/my-ai-service$ poetry run python src/my_ai_service/loop.py
模型回复: 你好
tianpeng@DESKTOP-4L1UF5S:~/my-ai-service$ poetry run python src/my_ai_service/loop.py
第 1 次调用失败: API 网络超时
第 2 次调用失败: API 网络超时
第 3 次调用失败: API 网络超时
Traceback (most recent call last):
File "/home/tianpeng/my-ai-service/src/my_ai_service/loop.py", line 77, in <module>
print(call_llm("你好"))
^^^^^^^^^^^^^^^^
File "/home/tianpeng/my-ai-service/src/my_ai_service/loop.py", line 51, in wrapper
raise RuntimeError(f"重试 {max_attempts} 次均失败")
RuntimeError: 重试 3 次均失败
tianpeng@DESKTOP-4L1UF5S:~/my-ai-service$
上述内容是多次运行的结果。
10.1.2 类装饰器:实现 LLM 客户端的单例模式
在 AI 应用中,某些资源(比如模型配置管理器、全局缓存对象)应该全局唯一。类装饰器可以轻松实现单例模式。
python
class Singleton:
"""类装饰器:确保一个类只有一个实例。
该装饰器通过维护一个字典 _instances 来存储每个被装饰类对应的唯一实例。
当类被调用(实例化)时,如果实例尚未创建,则创建并存储;否则返回已存在的实例。
"""
# 类属性,存储所有被装饰类的实例,键为类对象,值为该类唯一的实例
_instances = {}
def __init__(self, cls):
"""初始化装饰器,接收被装饰的类。
参数:
cls (type): 被装饰的类(例如 GlobalLLMConfig)。
"""
self.cls = cls
def __call__(self, *args, **kwargs):
"""使得 Singleton 实例可以像函数一样被调用,在实例化类时自动触发。
当用户执行 GlobalLLMConfig() 时,实际调用的是 Singleton 对象的 __call__ 方法。
参数:
*args: 传递给被装饰类构造函数的任意位置参数。
**kwargs: 传递给被装饰类构造函数的任意关键字参数。
返回:
object: 被装饰类的唯一实例。
"""
# 如果该类还没有创建过实例,则创建并存储
if self.cls not in self._instances:
self._instances[self.cls] = self.cls(*args, **kwargs)
# 返回已存在的实例
return self._instances[self.cls]
@Singleton
class GlobalLLMConfig:
"""全局大模型配置管理器。
该类使用 @Singleton 装饰器装饰,因此整个程序中只会存在一个实例。
用于集中管理大模型的配置参数(如 model、temperature 等),避免多处代码各自维护配置造成不一致。
"""
def __init__(self):
"""初始化配置管理器,设置默认的配置项。
注意:由于单例特性,此 __init__ 方法在整个程序生命周期中只会执行一次。
"""
self.config = {"model": "gpt-4", "temperature": 0.7}
print("初始化全局配置") # 用于验证只被调用一次
def get(self, key):
"""获取指定配置项的值。
参数:
key (str): 配置项的键名。
返回:
配置项的值,如果键不存在则返回 None。
"""
return self.config.get(key)
def set(self, key, value):
"""设置或更新配置项的值。
参数:
key (str): 配置项的键名。
value: 配置项的值。
"""
self.config[key] = value
# 测试单例行为
conf1 = GlobalLLMConfig() # 第一次实例化,会打印 "初始化全局配置"
conf2 = GlobalLLMConfig() # 第二次实例化,不会重新执行 __init__,直接返回已有实例
print(conf1 is conf2) # 验证两个变量指向同一个对象,输出 True
# 通过 conf1 修改配置,conf2 也能看到变化,证明是同一个实例
conf1.set("temperature", 0.9)
print(conf2.get("temperature")) # 输出 0.9
10.2 并发与并行编程:同时处理多个 AI 请求
现代 AI 应用往往需要同时处理多个用户请求或批量调用模型。Python 提供了三种并发方式:asyncio(异步 I/O)、threading(多线程)、multiprocessing(多进程)。
10.2.1 协程与异步 I/O:高并发调用多个大模型 API
当任务是 I/O 密集型(如等待网络响应)时,asyncio 能以极低的资源开销实现高并发。
python
import asyncio # 导入异步 I/O 库,支持协程和事件循环
import random # 导入随机数库,用于模拟随机的网络延迟
async def call_llm_async(model, prompt):
"""模拟异步调用 LLM API。
该协程模拟调用大语言模型 API 的过程,通过 asyncio.sleep 模拟网络延迟,
并返回一个模拟的模型回复。
参数:
model (str): 模型名称,如 "gpt-4"。
prompt (str): 用户输入的提示词。
返回:
str: 格式化为 "模型名称 回复: 用户提示词" 的字符串。
"""
# 打印调用开始信息,提示词仅显示前20个字符避免过长
print(f"开始调用 {model}:{prompt[:20]}...")
# 模拟网络 I/O 延迟,随机等待 0.5 到 2 秒
# await 会暂时交出控制权,让事件循环执行其他协程
await asyncio.sleep(random.uniform(0.5, 2))
# 模拟 API 返回的回复文本
return f"{model} 回复: {prompt}"
async def main():
"""主协程,负责并发调用多个模型并收集结果。"""
# 创建三个协程任务,但尚未执行(它们只是协程对象)
tasks = [
call_llm_async("gpt-4", "写一首诗"), # 任务1
call_llm_async("claude-3", "解释量子计算"), # 任务2
call_llm_async("gemini", "推荐一部电影") # 任务3
]
# asyncio.gather 并发执行所有任务,等待全部完成后返回结果列表
# 结果顺序与传入任务顺序一致
results = await asyncio.gather(*tasks)
# 遍历结果并打印每个模型的回复
for res in results:
print(res)
# 程序入口:运行主协程,启动事件循环
# asyncio.run() 会创建新的事件循环,运行 main() 协程,结束后关闭循环
asyncio.run(main())
AI 场景:批量对多个提示词进行生成、同时请求多个模型做对比、流式输出处理等。
10.2.2 多进程:加速模型评估等 CPU 密集型任务
对大量数据做 token 计数、计算 perplexity 或执行模型推理(如果没有 GPU)是 CPU 密集型的,多进程可以充分利用多核 CPU。
python
from multiprocessing import Pool # 导入进程池模块,用于并行执行任务
import time # 导入时间模块,用于模拟耗时操作
def simulate_inference(prompt):
"""模拟一个耗时的模型推理任务。
参数:
prompt (str): 输入的提示词。
返回:
str: 处理完成后的结果字符串。
"""
# 假设这里是对 prompt 做复杂的向量计算(如 LLM 推理)
# 使用 time.sleep 模拟计算耗时,实际应为真正的计算或 I/O 操作
time.sleep(0.2) # 模拟 0.2 秒的计算延迟
# 返回模拟的处理结果
return f"处理完成: {prompt}"
# 以下代码仅在直接运行此脚本时执行(被导入时不会执行)
if __name__ == "__main__":
# 生成 20 个待处理的提示词,格式为 "问题0"、"问题1" ... "问题19"
prompts = [f"问题{i}" for i in range(20)]
# 使用 with 语句创建进程池,指定并发进程数为 4
# with 块结束时自动关闭进程池并回收资源
with Pool(4) as pool:
# pool.map 将 simulate_inference 函数应用到 prompts 列表的每个元素上
# 它会自动将任务分发到 4 个进程中并行执行,并保持输出顺序与输入顺序一致
results = pool.map(simulate_inference, prompts)
# 打印处理结果的数量,验证是否所有 20 个提示词都被处理完成
print(f"共处理 {len(results)} 个提示词")
AI 场景:批量对测试集进行模型推理、并行计算多个超参数下的评估指标等。
10.3 元类:自动注册所有大模型插件
元类是类的类,用于控制类的创建过程 。在 AI 框架开发中,可以用元类实现自动注册机制:你只需定义一个新模型插件类,它就会自动加入全局插件注册表。
python
class PluginMeta(type):
"""元类:自动注册所有继承自 BasePlugin 的子类。
元类是类的类,用于控制类的创建过程。这里通过重写 __new__ 方法,
在子类被定义时自动将其注册到 registry 字典中,从而实现插件的自动发现。
"""
# 类属性:存储所有已注册的插件类,键为类名,值为类对象
registry = {}
def __new__(cls, name, bases, attrs):
"""创建新类时自动调用。
参数:
name (str): 待创建的类的名称。
bases (tuple): 待创建的类的父类元组。
attrs (dict): 类的属性字典(方法、类变量等)。
返回:
type: 新创建的类对象。
"""
# 调用父类(type)的 __new__ 方法实际创建类
new_class = super().__new__(cls, name, bases, attrs)
# 如果类名不是 "BasePlugin",说明这是一个具体的插件子类,需要注册
# (基类本身不参与注册,避免占位)
if name != "BasePlugin":
# 将新类存入 registry,键为类名,值为类对象
cls.registry[name] = new_class
# 返回新创建的类
return new_class
class BasePlugin(metaclass=PluginMeta):
"""所有插件的基类。
指定 metaclass=PluginMeta,使得该类的所有子类在定义时会自动被 PluginMeta 元类处理,
从而实现自动注册。
"""
pass
class GPT4Plugin(BasePlugin):
"""具体插件:GPT-4 实现。"""
def generate(self, prompt):
"""根据提示词生成回复。
参数:
prompt (str): 用户输入的提示词。
返回:
str: 带有插件标识的模拟回复。
"""
return f"[GPT-4] {prompt}"
class ClaudePlugin(BasePlugin):
"""具体插件:Claude 实现。"""
def generate(self, prompt):
"""根据提示词生成回复。"""
return f"[Claude] {prompt}"
# 自动注册完成后,可以通过名称从 registry 中动态获取插件类并实例化
plugin_name = "GPT4Plugin" # 指定要使用的插件名称
plugin = PluginMeta.registry[plugin_name]() # 从注册表中获取类并创建实例
print(plugin.generate("你好")) # 调用插件的 generate 方法,输出: [GPT-4] 你好
# 查看所有已注册的插件类名
print("已注册插件:", list(PluginMeta.registry.keys()))
AI 场景 :开发一个支持多模型的可扩展框架,用户只需添加新类就能自动被系统发现和使用。
10.4 描述符:精确控制模型参数的取值
描述符允许你自定义对属性的访问行为(获取、设置、删除)。在大模型参数配置中,常常需要验证温度、top_p 等值是否在合法范围内,描述符就是绝佳的工具。
python
class ValidatedParameter:
"""带类型和范围验证的描述符。
描述符是实现了 __get__ 和 __set__ 方法的类,用于控制对类属性的访问。
这里用于对 LLM 配置参数进行类型检查和数值范围验证。
"""
def __init__(self, name, type_, min_val=None, max_val=None):
"""初始化验证器。
参数:
name (str): 属性名(用于错误提示和存储键名)。
type_ (type): 期望的属性类型(如 int, float, str)。
min_val (可选): 数值类型的最小值(仅对 int/float 有效)。
max_val (可选): 数值类型的最大值。
"""
self.name = name # 属性名称
self.type = type_ # 期望的数据类型
self.min = min_val # 最小值约束
self.max = max_val # 最大值约束
def __set__(self, instance, value):
"""当对属性赋值时自动调用。
参数:
instance: 被操作的类实例(如 LLMConfig 实例)。
value: 赋予该属性的值。
"""
# 类型检查:如果值的类型不符合要求,抛出 TypeError
if not isinstance(value, self.type):
raise TypeError(f"{self.name} 必须是 {self.type.__name__} 类型")
# 最小值检查
if self.min is not None and value < self.min:
raise ValueError(f"{self.name} 不能小于 {self.min}")
# 最大值检查
if self.max is not None and value > self.max:
raise ValueError(f"{self.name} 不能大于 {self.max}")
# 验证通过,将值存储到实例的 __dict__ 中(避免无限递归)
instance.__dict__[self.name] = value
def __get__(self, instance, owner):
"""当读取属性值时自动调用。
参数:
instance: 被操作的类实例。
owner: 类本身(用于类级别访问,此处未使用)。
返回:
存储的属性值,如果不存在则返回 None。
"""
# 从实例的 __dict__ 中获取值,未设置则返回 None
return instance.__dict__.get(self.name)
class LLMConfig:
"""大模型配置类,使用描述符对参数进行自动验证。"""
# 定义类属性为 ValidatedParameter 描述符实例
# 这将拦截对 temperature、max_tokens、model_name 的读写操作
temperature = ValidatedParameter("temperature", float, 0.0, 2.0)
max_tokens = ValidatedParameter("max_tokens", int, 1, 8192)
model_name = ValidatedParameter("model_name", str)
def __init__(self, model_name, temperature, max_tokens):
"""初始化配置,调用描述符的 __set__ 进行验证。
参数:
model_name (str): 模型名称。
temperature (float): 采样温度,应在 0.0~2.0 之间。
max_tokens (int): 最大生成 token 数,应在 1~8192 之间。
"""
# 下面的赋值会触发每个描述符的 __set__ 方法进行验证
self.model_name = model_name
self.temperature = temperature
self.max_tokens = max_tokens
# ========== 正确使用示例 ==========
config = LLMConfig("gpt-4", 0.8, 2048) # 所有参数合法,初始化成功
print(config.temperature) # 读取属性,输出 0.8
# ========== 错误使用会触发异常(取消注释可测试) ==========
# 以下操作会触发描述符的验证,抛出对应异常:
# config.temperature = 2.5 # ValueError: temperature 不能大于 2.0
# config.max_tokens = "3000" # TypeError: max_tokens 必须是 int 类型
AI 场景:在配置类中自动检查所有模型超参数的合法性,避免无效调用。
10.5 上下文管理器高级用法:统计模型推理耗时
上下文管理器(with 语句)不仅用于文件操作,还可以自定义资源管理。利用 contextlib.contextmanager 装饰器,可以轻松创建计时器、日志上下文等。
python
from contextlib import contextmanager # 导入上下文管理器装饰器,用于简化自定义上下文管理器的编写
import time # 导入时间模块,用于计算耗时
@contextmanager
def timing(label: str):
"""计时上下文管理器,用于测量代码块执行时间。
使用 yield 之前的代码在进入 with 块时执行,yield 之后的代码在退出 with 块时执行。
参数:
label (str): 用于标识计时任务的标签,输出时会显示。
用法:
with timing("推理"):
# 需要计时的代码块
"""
# 记录开始时间(使用 perf_counter 获取高精度计时器)
start = time.perf_counter()
try:
# yield 将控制权交还给 with 块内的代码
# 这里不产出任何值,所以 as 子句不需要变量
yield
finally:
# 无论 with 块内是否发生异常,都会执行此处
# 计算结束时间并打印耗时,保留 4 位小数
end = time.perf_counter()
print(f"{label} 耗时: {end - start:.4f} 秒")
def llm_inference(prompt):
"""模拟大语言模型的推理过程。
参数:
prompt (str): 用户输入的提示词。
返回:
str: 模拟的模型回复。
"""
# 使用 time.sleep 模拟模型推理的耗时(如网络延迟、GPU 计算等)
time.sleep(1.2) # 假设推理需要 1.2 秒
return f"回复: {prompt}"
# 使用自定义的计时上下文管理器来测量 llm_inference 的执行时间
with timing("gpt-4 推理"):
# 这个缩进块中的代码会被 timing 上下文管理器包裹
result = llm_inference("讲个笑话")
print(result)
AI 场景:评估不同模型或不同 batch size 的推理延迟、监控 API 调用耗时等。
10.6 动态属性和动态模块导入:灵活加载模型插件
10.6.1 动态属性:让配置对象支持任意字段
有时我们需要一个可以动态添加属性的配置对象,而不必事先定义所有字段。
python
class DynamicConfig:
"""允许动态设置任意属性的配置类。
该类不需要预先定义属性,可以在初始化时通过关键字参数任意设置,
也可以在创建后通过赋值动态添加新属性。访问不存在的属性时返回 None 而不是抛出 AttributeError。
"""
def __init__(self, **kwargs):
"""初始化方法,接收任意关键字参数。
参数:
**kwargs: 任意数量的关键字参数,例如 model="gpt-4", temperature=0.7。
这些键值对会被动态设置为实例的属性。
"""
# 遍历传入的所有关键字参数
for key, value in kwargs.items():
# setattr(self, key, value) 等价于 self.key = value
# 这里动态地将每个参数添加为实例属性
setattr(self, key, value)
def __getattr__(self, name):
"""当访问一个不存在的属性时,Python 会自动调用此方法。
参数:
name (str): 被访问的属性名。
返回:
总是返回 None,而不是抛出 AttributeError。这允许访问任意未定义的属性而不报错。
"""
return None
# ========== 使用示例 ==========
# 创建配置实例,传入模型名称和温度参数
config = DynamicConfig(model="gpt-4", temperature=0.7)
# 访问已存在的属性(通过 __init__ 添加的)
print(config.model) # 输出: gpt-4
# 访问不存在的属性(触发 __getattr__,返回 None)
print(config.max_tokens) # 输出: None(不会抛出异常)
# 动态添加新属性(直接赋值,不会触发 __getattr__)
config.max_tokens = 2048
# 再次访问,因为属性已存在,直接返回其值
print(config.max_tokens) # 输出: 2048
10.6.2 动态模块导入:根据配置加载不同的模型实现
你可以让程序在运行时根据字符串名称导入模块,从而实现插件式架构。
python
import importlib # 导入 importlib 模块,提供动态导入 Python 模块的能力
def load_llm(model_name: str):
"""动态加载模型模块。
该函数尝试导入名为 models.{model_name} 的模块,并从中获取名为 LLM 的类,
然后实例化并返回。如果模块不存在或模块中没有 LLM 类,则打印错误并返回 None。
参数:
model_name (str): 模型名称,对应 models 目录下的子模块名,例如 "gpt4"。
返回:
模块中 LLM 类的实例,或 None(加载失败时)。
"""
try:
# importlib.import_module 动态导入模块
# 假设 model_name 为 "gpt4",则导入 models.gpt4 模块
module = importlib.import_module(f"models.{model_name}")
# 使用 getattr 从导入的模块中获取名为 "LLM" 的属性(应该是一个类)
llm_class = getattr(module, "LLM")
# 实例化并返回
return llm_class()
except (ImportError, AttributeError) as e:
# ImportError: 模块不存在(如 models.gpt4 找不到)
# AttributeError: 模块存在但没有名为 LLM 的属性
print(f"无法加载模型 {model_name}: {e}")
return None
# 使用示例(需要预先创建 models/gpt4.py 文件,其中定义了 LLM 类):
# llm = load_llm("gpt4")
AI 场景 :根据用户输入的模型名称(字符串)动态加载对应的模型实现,无需硬编码大量的 if-else。
10.7 抽象语法树(AST)操作:分析 AI 代码的依赖
AST 将 Python 源代码解析成树状结构,你可以遍历这棵树来分析、修改代码。例如,自动提取一个脚本中调用了哪些大模型 API。
python
import ast # 导入抽象语法树模块,用于解析 Python 代码并操作其语法结构
class ModelAPIExtractor(ast.NodeVisitor):
"""自定义 AST 访问器,用于提取所有对 call_llm 函数的调用。
继承自 ast.NodeVisitor,通过重写 visit_Call 方法来捕获函数调用节点,
并从中提取第一个参数(提示词)的值。
"""
def __init__(self):
"""初始化提取器,创建一个空列表用于存储提取到的提示词。"""
self.prompts = []
def visit_Call(self, node):
"""重写 visit_Call 方法,在遍历到函数调用节点时自动调用。
参数:
node (ast.Call): 一个函数调用的语法树节点。
"""
# 检查调用的函数名是否为 "call_llm"
# node.func 是被调用的函数表达式;ast.Name 表示简单名称,其 id 是函数名字符串
if isinstance(node.func, ast.Name) and node.func.id == "call_llm":
# 如果调用语句中有位置参数(node.args 不为空)
if node.args:
# 获取第一个参数(索引 0)
arg = node.args[0]
# 检查该参数是否为常量(如字符串、数字等)
if isinstance(arg, ast.Constant):
# 将常量的值(即提示词字符串)添加到列表
self.prompts.append(arg.value)
# 继续遍历当前节点下的子节点(保证不遗漏嵌套调用)
self.generic_visit(node)
# 待分析的 Python 源代码字符串
code = """
def main():
call_llm("什么是人工智能")
call_llm("写一首诗")
print("done")
"""
# 将源代码解析为抽象语法树(AST)对象
tree = ast.parse(code)
# 创建提取器实例并访问语法树(触发遍历和回调)
extractor = ModelAPIExtractor()
extractor.visit(tree)
# 打印提取到的所有提示词
print("提取到的提示词:", extractor.prompts) # 输出: ['什么是人工智能', '写一首诗']
AI 场景:分析自动化测试脚本,收集所有发送给模型的提示词;或检查代码中是否使用了禁止的模型调用。
10.8 类型系统与运行时验证:让模型配置更安全
10.8.1 类型注解与 dataclass
类型注解(Type Hints)是 Python 3.5+ 引入的语法,通过在变量、函数参数和返回值后添加 : 类型 或 -> 类型 来标注预期数据类型(如 name: str),虽然运行时不会强制检查,但能提升代码可读性、支持 IDE 自动补全和静态类型检查工具(如 mypy)。而 @dataclass 装饰器(Python 3.7+)利用类型注解自动为类生成 __init__、__repr__、__eq__ 等方法,大大简化了数据容器类的编写(例如 @dataclass class Config: model: str; temperature: float = 0.7)。两者经常配合使用:类型注解描述字段类型, dataclass 据此自动生成初始化代码,让代码更简洁、更规范。
类型注解可以提高代码可读性,而 dataclass 能自动生成 __init__ 等常用方法。
python
from typing import List, Optional # 导入类型提示工具,List 用于列表类型,Optional 表示可能为 None
from dataclasses import dataclass # 导入 dataclass 装饰器,用于自动生成 __init__、__repr__ 等方法
@dataclass
class LLMRequest:
"""用于封装大模型 API 请求参数的数据类。
使用 @dataclass 装饰器后,Python 会自动为这个类生成:
- __init__ 方法(根据字段定义)
- __repr__ 方法(便于打印调试)
- __eq__ 方法(比较两个对象是否相等)
以及其他有用的方法。
字段:
model (str): 模型名称,例如 "gpt-4"。
prompt (str): 用户输入的提示词文本。
temperature (float): 采样温度,控制输出的随机性,默认 0.7。
max_tokens (Optional[int]): 最大输出 token 数,可为 None 表示不限制。
"""
model: str # 模型名称,必填
prompt: str # 提示词,必填
temperature: float = 0.7 # 温度,可选,默认 0.7
max_tokens: Optional[int] = None # 最大 token 数,可选,默认 None
# 创建 LLMRequest 实例,使用关键字参数指定 temperature
req = LLMRequest("gpt-4", "讲个笑话", temperature=0.8)
# 打印实例,由于 @dataclass 自动生成了 __repr__,输出内容包含所有字段及其值
print(req) # 输出: LLMRequest(model='gpt-4', prompt='讲个笑话', temperature=0.8, max_tokens=None)
10.8.2 运行时类型验证(使用 Pydantic)
pydantic 库可以在运行时自动验证数据,并给出清晰的错误信息。
python
from pydantic import BaseModel, Field, ValidationError
# 从 pydantic 库导入:
# - BaseModel: Pydantic 数据模型基类,提供自动验证、序列化等功能
# - Field: 用于定义字段的附加属性(如验证规则、默认值等)
# - ValidationError: 当数据验证失败时抛出的异常类
class LLMConfig(BaseModel):
"""大模型配置类,使用 Pydantic 实现自动类型验证和范围检查。
继承自 BaseModel 后,Pydantic 会:
- 根据类型注解自动进行类型转换和验证
- 在实例化时检查字段是否符合定义的约束
- 提供完善的错误信息
"""
# 字段定义,类型注解为 str,无额外验证规则
model: str
# Field 用于添加验证约束:
# ge=0.0 表示大于等于 0.0 (greater than or equal)
# le=2.0 表示小于等于 2.0 (less than or equal)
temperature: float = Field(ge=0.0, le=2.0)
# gt=0 表示大于 0 (greater than)
# le=8192 表示小于等于 8192
max_tokens: int = Field(gt=0, le=8192)
try:
# 尝试创建配置实例,传入参数
# 注意:temperature=2.5 超出了 0.0~2.0 的范围
# max_tokens=10000 超出了 1~8192 的范围(gt=0 隐含最小值 1)
config = LLMConfig(model="gpt-4", temperature=2.5, max_tokens=10000)
except ValidationError as e:
# 验证失败时捕获异常,并打印格式化的错误信息
# e.json(indent=2) 将错误信息以 JSON 格式输出,缩进 2 个空格,便于阅读
print("配置验证失败:", e.json(indent=2))
AI 场景:对从配置文件或用户输入读取的参数进行严格校验,避免无效请求。
10.9 C扩展开发:调用高性能 C 库加速 token 计数
当 Python 性能成为瓶颈时,可以编写 C 扩展(使用 ctypes 或 Cython)。下面用 ctypes 调用一个 C 函数来统计文本中的 token 数(模拟)。
C 代码 token_counter.c:
python
#include <string.h> // 包含字符串操作相关函数的头文件(虽然本函数未直接使用,但常见于字符串处理)
/**
* 统计文本中的 token 数量(按空格分词)。
* 这是一个简单的实现,将连续的空格分隔视为不同 token 的边界。
* 注意:该函数会假设文本非空,若传入空字符串会返回 1(因为初始 count=1)。
*
* @param text 以 null 结尾的 C 字符串,不能为 NULL。
* @return 文本中 token 的个数(按空格划分)。
*/
int count_tokens(const char* text) {
// 初始化 token 计数为 1,假设至少有一个 token
int count = 1;
// 遍历字符串中的每个字符,直到遇到 null 终止符 '\0'
for (int i = 0; text[i] != '\0'; i++) {
// 如果当前字符是空格,则 token 数量加 1
if (text[i] == ' ') {
count++;
}
}
// 返回统计到的 token 数量
return count;
}
编译(Linux/macOS):
python
gcc -shared -o libtoken.so -fPIC token_counter.c
Python 调用:
python
from ctypes import CDLL, c_char_p, c_int
# ctypes 是 Python 标准库,用于调用 C 语言编写的动态链接库(.so / .dll)。
# - CDLL: 用于加载共享库,并允许调用其中的函数。
# - c_char_p: 表示 C 语言中的 char* 类型(以 null 结尾的字符串)。
# - c_int: 表示 C 语言中的 int 类型。
# 加载共享库文件(假设当前目录下存在 libtoken.so 或 libtoken.dll)
# CDLL 的构造函数返回一个库对象,可以通过该对象访问库中导出的函数。
lib = CDLL("./libtoken.so")
# 设置库中 count_tokens 函数的参数类型。
# argtypes 是一个元组,按顺序指定每个参数的类型。
# 这里表示 count_tokens 函数接受一个 C 字符串指针作为参数。
lib.count_tokens.argtypes = [c_char_p]
# 设置库中 count_tokens 函数的返回值类型。
# restype 指定函数返回的类型,这里表示返回一个 int。
lib.count_tokens.restype = c_int
# 准备要处理的文本字符串。Python 字符串需要编码为 bytes 才能传递给 C 函数。
# 这里使用 UTF-8 编码,因为 C 函数通常期望 null 结尾的 UTF-8 字符串。
text = "This is a sample sentence".encode("utf-8")
# 调用 C 库中的 count_tokens 函数,传入编码后的文本。
# 该函数应返回文本中的 token 数量(按某种规则分割)。
tokens = lib.count_tokens(text)
# 打印结果
print(f"Token 数量: {tokens}") # 预期输出 5(因为例句有 5 个单词)
AI 场景 :将高频使用的 token 编解码、文本预处理等函数用 C 重写,显著提升数据处理速度。
10.10 高级设计模式:状态模式管理对话流程
在构建多轮对话 AI 时,对话可能处于不同状态(如等待用户输入、等待确认、处理中)。状态模式能让代码清晰且易于扩展。
python
class DialogueState:
"""对话状态基类。
定义所有具体状态类的统一接口。每个状态类负责处理在该状态下用户输入后的行为,
并可能将对话上下文切换到下一个状态。
"""
def handle(self, context, user_input):
"""处理用户输入的抽象方法,子类必须重写。
参数:
context (DialogueContext): 对话上下文对象,用于访问和修改当前状态。
user_input (str): 用户输入的文本。
"""
pass
class AwaitingInputState(DialogueState):
"""等待用户输入状态。
在此状态下,系统会接收用户输入,打印用户消息,然后切换到处理状态(ProcessingState)。
"""
def handle(self, context, user_input):
"""处理用户输入:打印用户消息,并切换到 ProcessingState。
参数:
context (DialogueContext): 对话上下文,用于修改当前状态。
user_input (str): 用户输入的消息。
"""
# 输出用户输入的内容
print(f"用户说: {user_input}")
print("AI 正在思考...")
# 将对话上下文的状态切换到处理状态,表示正在处理中
context.state = ProcessingState()
class ProcessingState(DialogueState):
"""处理状态(模拟 AI 回复)。
在此状态下,系统模拟 AI 生成回复,然后切换回等待输入状态。
"""
def handle(self, context, user_input):
"""处理用户输入:模拟 AI 回复,并切换回 AwaitingInputState。
参数:
context (DialogueContext): 对话上下文,用于修改当前状态。
user_input (str): 用户输入的消息(这里用于构建回复内容)。
"""
# 模拟 AI 回复,并基于用户输入生成回复文本
print(f"AI 回复: 已处理「{user_input}」")
# 处理完成后,将状态切换回等待用户输入的状态
context.state = AwaitingInputState()
class DialogueContext:
"""对话上下文,维护当前状态并驱动状态转换。
通过 send 方法接收用户输入,并委托给当前状态对象处理。
"""
def __init__(self):
"""初始化对话上下文,起始状态为 AwaitingInputState。"""
self.state = AwaitingInputState()
def send(self, user_input):
"""发送用户输入,由当前状态处理。
参数:
user_input (str): 用户输入的消息。
"""
# 调用当前状态的 handle 方法,传入自身上下文和用户输入
self.state.handle(self, user_input)
# ========== 模拟多轮对话 ==========
dialogue = DialogueContext()
dialogue.send("你好") # 输出:用户说: 你好 -> AI 正在思考... -> AI 回复: 已处理「你好」
dialogue.send("再讲个笑话") # 输出:用户说: 再讲个笑话 -> AI 正在思考... -> AI 回复: 已处理「再讲个笑话」
AI 场景:实现复杂的对话流程,如意图确认、多步骤表单填写、人工转接等。
10.11 最佳实践指南
10.11.1 何时使用高阶特性
| 特性 | 适用场景(AI 相关) | 风险提示 |
|---|---|---|
| 装饰器 | 重试、缓存、日志、权限校验 | 多层装饰降低可读性 |
| 元类 | 插件自动注册、ORM 框架 | 增加理解成本,非必要不用 |
| 描述符 | 参数验证、懒加载属性 | 可能过度设计简单属性 |
| asyncio | 高并发 API 调用、流式输出 | 学习曲线较陡,且并非所有库都支持 |
| 多进程 | 批量离线推理、数据预处理 | 进程间通信开销大 |
| 动态导入 | 插件化架构、多模型切换 | 可能引入运行时错误 |
| AST | 代码分析、自动化测试生成 | 依赖 Python 版本细节 |
10.11.2 性能优化小技巧
- 使用
__slots__:当你有大量小对象时(例如表示每个 token 的结构),__slots__可以大幅减少内存占用。
python
class Token:
"""表示一个词元(Token)的类,用于存储文本片段及其在原始序列中的位置。
通过定义 __slots__ 来固定实例属性,避免为每个实例创建 __dict__ 字典,
从而显著减少内存占用并提升属性访问速度。
"""
# __slots__ 是一个类变量,它列出了该类实例允许拥有的属性名称。
# 使用 __slots__ 后,实例将不再拥有 __dict__ 字典,只能拥有 slots 中列出的属性。
# 这可以节省内存(尤其当需要创建大量 Token 对象时,如在大语言模型的分词处理中)。
__slots__ = ('text', 'position')
def __init__(self, text, position):
"""初始化 Token 实例。
参数:
text (str): 词元的文本内容。
position (int): 该词元在原始文本或序列中的位置索引(通常从 0 开始)。
"""
self.text = text # 存储词元文本
self.position = position # 存储词元位置
- 缓存计算结果 :对于重复输入的 prompt 嵌入计算,使用
functools.lru_cache。
python
from functools import lru_cache # 导入 lru_cache 装饰器,用于为函数添加最少最近使用(LRU)缓存
@lru_cache(maxsize=128) # 应用缓存装饰器,最多缓存 128 个不同的调用结果
def get_embedding(text: str):
"""获取文本的嵌入向量表示(模拟版本)。
由于计算嵌入通常是耗时操作(例如调用神经网络模型),
使用 lru_cache 可以自动缓存相同输入的结果,
避免重复计算,提高效率。
参数:
text (str): 输入文本。
返回:
list: 模拟的嵌入向量,这里简单返回每个字符的 Unicode 码点列表。
"""
# 此处应为真实嵌入计算逻辑,这里用列表推导式模拟耗时操作
return [ord(c) for c in text]
- 生成器节约 内存:处理大规模数据集(如几百万条 prompt)时,用生成器逐条读取,而不是一次性加载到列表。
10.12 本章小结
| 高阶主题 | 核心作用 | AI 应用示例 |
|---|---|---|
| 元编程(装饰器、类装饰器) | 动态增强函数或类 | 自动重试 API、实现单例配置 |
| 并发(asyncio、多进程) | 提升 I/O 或 CPU 密集型任务吞吐量 | 高并发调用 LLM、批量模型评估 |
| 元类 | 控制类的创建过程 | 自动注册所有模型插件 |
| 描述符 | 精细控制属性访问 | 验证模型超参数取值范围 |
| 上下文管理器 | 自动管理资源(如计时) | 测量模型推理耗时 |
| 动态属性/导入 | 运行时灵活性 | 动态加载不同模型插件 |
| AST | 分析或修改源代码 | 提取代码中所有 API 调用 |
| 类型系统(Pydantic) | 运行时数据验证 | 校验用户输入或配置文件 |
| C 扩展 | 性能关键路径优化 | 加速 token 计数、预处理 |
| 状态模式 | 管理复杂状态流转 | 多轮对话流程控制 |
写在最后:高阶特性是强大的工具,但也是一把双刃剑。在 AI 开发中,优先考虑代码的可读性和可维护性,只有当简单方案无法满足需求时,再谨慎引入元类、AST 等复杂技术。愿你写出既优雅又高效的 AI 代码!