第 15 章:元类与 Hook——方法调用的拦截

难度:进阶
你想在 Agent 每次 reply 前后自动执行一些逻辑(比如日志记录、参数校验),但不想修改 Agent 的源码。Hook 系统就是为这个设计的——它是怎么实现的?
上一章:第 14 章 继承体系
知识补全:元类(Metaclass)
在 Python 中,类也是对象。元类是”创建类的类”。1
2
3
4
5class MyClass: # 普通类,创建实例
pass
obj = MyClass() # obj 是 MyClass 的实例
# MyClass 是 type 的实例!
当你写 class MyClass: 时,Python 实际上调用 type.__new__() 来创建这个类。元类允许你拦截类的创建过程,在类被创建之前修改它的属性和方法。1
2
3
4
5
6
7
8
9
10
11class MyMeta(type):
def __new__(mcs, name, bases, attrs):
# 在类被创建前,修改 attrs(方法字典)
attrs['extra_method'] = lambda self: "added by metaclass"
return super().__new__(mcs, name, bases, attrs)
class MyClass(metaclass=MyMeta):
pass
obj = MyClass()
obj.extra_method() # "added by metaclass"
AgentScope 用这个机制在类定义时就自动给 reply、observe、print 方法包上 Hook 逻辑。
_AgentMeta:元类的实现
打开 src/agentscope/agent/_agent_meta.py,找到第 159 行:1
2
3
4
5
6
7# _agent_meta.py:159
class _AgentMeta(type):
def __new__(mcs, name, bases, attrs):
for func_name in ["reply", "print", "observe"]:
if func_name in attrs:
attrs[func_name] = _wrap_with_hooks(attrs[func_name])
return super().__new__(mcs, name, bases, attrs)
只有 8 行代码。它做的事情:
- 遍历
["reply", "print", "observe"]三个方法名 - 如果类定义了这个方法(
func_name in attrs),用_wrap_with_hooks包装它 - 用包装后的函数替换原来的方法
关键点:这个包装发生在类被创建时,不是在实例方法被调用时。所有 AgentBase 的子类自动获得 Hook 能力。
_wrap_with_hooks:Hook 的执行逻辑
_wrap_with_hooks(第 55 行)是一个装饰器工厂。它返回的 async_wrapper 做这些事:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32async def async_wrapper(self, *args, **kwargs):
# 1. 防重入保护(避免继承链中重复执行 Hook)
if getattr(self, hook_guard_attr, False):
return await original_func(self, *args, **kwargs)
# 2. 归一化参数(把位置参数转成关键字参数)
normalized_kwargs = _normalize_to_kwargs(original_func, self, *args, **kwargs)
# 3. 执行 pre-hooks(先实例级,后类级别)
pre_hooks = list(self._instance_pre_reply_hooks.values()) + \
list(self.__class__._class_pre_reply_hooks.values())
for pre_hook in pre_hooks:
modified = await pre_hook(self, deepcopy(normalized_kwargs))
if modified is not None:
normalized_kwargs = modified
# 4. 执行原始函数
setattr(self, hook_guard_attr, True)
try:
output = await original_func(self, **normalized_kwargs)
finally:
setattr(self, hook_guard_attr, False)
# 5. 执行 post-hooks
post_hooks = list(self._instance_post_reply_hooks.values()) + \
list(self.__class__._class_post_reply_hooks.values())
for post_hook in post_hooks:
modified = await post_hook(self, deepcopy(normalized_kwargs), deepcopy(output))
if modified is not None:
output = modified
return output
防重入保护
hook_guard_attr(如 _hook_running_reply)是一个标志位。当继承链中多个类都定义了 reply 时,每个都被 _AgentMeta 包装了,但 Hook 只执行一次——最外层设置标志,内层检测到标志就跳过。
执行顺序
1 | pre_hook(实例级) → pre_hook(类级别) → 原始函数 → post_hook(实例级) → post_hook(类级别) |
_ReActAgentMeta:扩展 Hook 类型
ReActAgentBase 使用了 _ReActAgentMeta 元类(在 _react_agent_base.py 中),它扩展了 Hook 类型:1
2
3# _react_agent_base.py:28-31
"pre_reasoning", "post_reasoning",
"pre_acting", "post_acting",
所以 ReAct Agent 支持 6 个 Hook 点:reply、observe、print、reasoning、acting。1
2
3
4flowchart TD
A["AgentBase (metaclass=_AgentMeta)"] --> B["reply / observe / print"]
C["ReActAgentBase (metaclass=_ReActAgentMeta)"] --> D["reply / observe / print / reasoning / acting"]
E["ReActAgent"] --> F["继承 ReActAgentBase 的所有 Hook"]
Hook 的注册
Hook 分两种级别:
类级别:所有实例共享。在类上直接注册:1
2
3
4
def log_hook(self, kwargs, output):
print(f"Agent {self.name} replied")
return output
实例级别:只影响单个实例。在实例上注册:1
agent.register_instance_hook("pre_reply", "validate", my_validator)
AgentScope 官方文档的 Building Blocks > Hooking Functions 页面展示了 Hook 的注册方式(register_instance_hook / register_class_hook)和支持的 Hook 类型表格:reply/observe/print 各有 pre 和 post 两个时机,ReAct Agent 额外支持 reasoning/acting 的 pre/post hooks。本章解释了这些 Hook 是如何通过元类在类定义时自动注入的。
AgentScope 1.0 论文对 Hook 机制的设计说明是:
“we ground agent behaviors in the ReAct paradigm and offer advanced agent-level infrastructure based on a systematic asynchronous design”
— AgentScope 1.0: A Comprehensive Framework for Building Agentic Applications, arXiv:2508.16279, Section 2.2
Hook 系统是这个”高级 Agent 基础设施”的一部分——让开发者在不修改源码的情况下,在 Agent 的关键方法执行前后插入自定义逻辑。
_normalize_to_kwargs:参数归一化
在 Hook 的执行链中,有一个容易被忽略但很重要的步骤:_normalize_to_kwargs(第 35 行)。
它的作用是把 reply(self, msg, structured_model=None) 的位置参数转成关键字参数字典:1
2
3
4
5
6
7
8# 调用方式 1:位置参数
await agent.reply(msg)
# 调用方式 2:关键字参数
await agent.reply(msg=msg)
# _normalize_to_kwargs 把两者都转成:
{"msg": msg}
为什么要这样做?因为 Hook 函数需要修改参数——如果用位置参数,Hook 无法知道第几个参数是什么。统一转成 kwargs 字典后,Hook 可以自由地增删改参数。
Hook 类型的完整列表
| 元类 | Hook 类型 | 对应方法 |
|---|---|---|
_AgentMeta | pre_reply / post_reply | reply() |
_AgentMeta | pre_print / post_print | print() |
_AgentMeta | pre_observe / post_observe | observe() |
_ReActAgentMeta | 以上全部 + pre_reasoning / post_reasoning | _reasoning() |
_ReActAgentMeta | 以上全部 + pre_acting / post_acting | _acting() |
每个 Hook 类型有实例级和类级别两种注册方式。实例级只影响单个 Agent,类级别影响所有该类的实例。
两个元类的继承关系
1 | # _agent_meta.py:159 |
_ReActAgentMeta 继承 _AgentMeta,先让父类包装 reply/observe/print,然后自己再包装 _reasoning/_acting。这样 ReAct Agent 自动获得全部 6 个 Hook 点。
调试实践:观察 Hook 的执行顺序
目标:亲眼看到 pre-hook → 原始函数 → post-hook 的执行链。
步骤:
- 在
src/agentscope/agent/_agent_meta.py的async_wrapper函数中(约第 70 行),加 print:
1 | async def async_wrapper(self, *args, **kwargs): |
- 创建测试脚本:
1 | import asyncio |
- 观察 Hook 链的执行顺序
改完后恢复:1
git checkout src/agentscope/agent/
试一试:注册一个日志 Hook
目标:用 Hook 记录 Agent 每次调用 reply 的参数。
步骤:
- 打开任意使用 ReActAgent 的脚本,在创建 Agent 之后加:
1 | import time |
- 如果没有 API key,可以在
src/agentscope/agent/_agent_meta.py的_wrap_with_hooks中加 print 观察 Hook 执行顺序:
1 | # 在 pre-hooks 循环前加 |
改完后恢复:1
git checkout src/agentscope/agent/
检查点
你现在理解了:
- 元类
_AgentMeta在类定义时自动包装reply/observe/print方法 _wrap_with_hooks实现 pre-hook → 原始函数 → post-hook 的执行链- 防重入保护确保继承链中 Hook 只执行一次
- Hook 分实例级和类级别,执行顺序是实例级先于类级别
_ReActAgentMeta扩展了 reasoning 和 acting 的 Hook
自检练习:
- 如果你在
AgentBase的子类中定义了reply方法但没有使用_AgentMeta元类,Hook 还会生效吗? _normalize_to_kwargs的作用是什么?为什么 pre-hook 接收的是kwargs字典而不是原始参数?
下一章预告
Hook 是在特定方法前后插入逻辑。还有一种更通用的模式——策略模式:同一个接口,根据不同的情况选择不同的实现。下一章我们看 Formatter 如何使用策略模式适配不同的模型 API。
下一章:第 16 章 策略模式