单例模式(Singleton Pattern)的意图只有一句话:确保某个类只有一个实例,并提供全局访问点。它是 GoF 23 种设计模式中最被滥用、也最常被误解的一个。
本文梳理 Python 中几种实现方式的本质差异,以及在 AI 基础设施工程中真实触发这个模式的场景。
四种实现方式
通过自定义元类拦截 __call__:
1 2 3 4 5 6 7 8 9 10 11 12 13
| class SingletonMeta(type): _instances: dict = {}
def __call__(cls, *args, **kwargs): if cls not in cls._instances: cls._instances[cls] = super().__call__(*args, **kwargs) return cls._instances[cls]
class LLMClient(metaclass=SingletonMeta): def __init__(self, model: str = "claude-opus-4-7"): self.model = model self._client = Anthropic()
|
代价是继承链会变复杂,与其他 Metaclass 组合时容易产生冲突。
2. 基于 __new__
不引入元类,直接在类层面控制:
1 2 3 4 5 6 7 8
| class ModelRegistry: _instance = None
def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) cls._instance._registry = {} return cls._instance
|
注意 __init__ 在每次构造时仍会执行,需要用 hasattr 守卫初始化逻辑,否则会重复初始化内部状态。
3. 模块级单例(推荐)
Python 模块本身是天然单例——import 机制保证同一模块只被加载一次:
1 2 3 4 5 6 7 8 9 10 11
| from anthropic import Anthropic
_client: Anthropic | None = None
def get_client() -> Anthropic: global _client if _client is None: _client = Anthropic() return _client
|
这是生产代码里最常见的做法,无需任何模式包装,语义清晰,易于测试时替换。
4. 线程安全版本
多线程环境下存在竞态条件,需要加锁:
1 2 3 4 5 6 7 8 9 10 11 12 13
| import threading
class ThreadSafeSingleton: _instance = None _lock = threading.Lock()
def __new__(cls): if cls._instance is None: with cls._lock: if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance
|
外层检查避免每次都获取锁,内层检查防止并发进入临界区后的重复创建。在 asyncio 场景下,把 threading.Lock 换成 asyncio.Lock 并在 async 初始化方法中处理。
在 AI 基础设施中的真实场景
单例模式在 AI infra 层有几个明确的落点:
LLM Client 复用:Anthropic SDK、OpenAI SDK 内部维护 HTTP 连接池,重复创建会浪费资源并影响速率限制计数。Client 应当是进程级单例。
Model Registry:在多 Agent 系统中,各 Agent 需要查询可用模型、路由规则、能力描述。Registry 天然是单例——多个副本意味着状态不一致,路由结果不可预期。
Tool Registry:Harness 层的工具注册中心,Agent 运行时动态查询可用工具,需要进程内唯一:
1 2 3 4 5 6 7 8 9 10
| class ToolRegistry(metaclass=SingletonMeta): def __init__(self): if not hasattr(self, "_tools"): self._tools: dict[str, Callable] = {}
def register(self, name: str, fn: Callable) -> None: self._tools[name] = fn
def get(self, name: str) -> Callable | None: return self._tools.get(name)
|
什么时候不该用
单例的核心问题是隐式全局状态——让测试变得困难(需要 mock 或 reset 实例),让依赖关系不透明(调用方看不到它依赖了单例)。
如果在用 FastAPI 或类似 DI 框架,lifespan + dependency injection 是更好的选择;资源的生命周期由框架管理,依赖关系显式声明。
如果在写 Agent 框架,考虑把共享状态通过参数显式传递给每个 Agent,而不是让它们通过全局单例通信——这在 tracing 和调试时会省掉很多麻烦。
单例解决的是资源共享问题,不是架构问题。区分清楚这一点,就知道什么时候该用、什么时候该克制。