Event Hook¶
TFRobot 在运行时会经过一系列组件:Robot → Brain → Chain → LLM/Tool/Memory。每个组件在执行前后,都会通过 OpenTelemetry 的 Span 机制发出事件(Event)。Event Hook 允许你监听这些事件,在不侵入框架代码的前提下,注入自己的逻辑——比如打印日志、上报指标、记录审计信息。
核心概念与数据流¶
理解 Event Hook 需要先理清四个概念之间的关系:
Scene(场景)──── 标识事件来源(Robot / Chain / LLM / Tool ...)
│
Event(事件)──── 标识事件类型(before_chain_run / after_llm_generate ...)
│
EventContext ──── 携带事件的结构化上下文(模型名、Token 用量、工具返回值 ...)
│
Hook(钩子)──── 你编写的回调函数,接收 Event 名称和 EventContext
运行时数据流如下:
- TFRobot 组件(如 Chain)的关键方法被
span_decorator包装 - 方法执行前后,
span_decorator构造EventContext并调用TFSpan.add_event TFSpan.add_event将事件分发给HookManagerHookManager根据事件的scene字段,找到匹配的 Hook 并执行
关于 Scene 和 Event 的完整列表,参见 场景与事件。
定义 Hook 的两种方式¶
方式一:tf_event_hook 装饰器¶
最简洁的方式。装饰器会自动将函数注册到 HookManager:
from opentelemetry.util import types
from tfrobot.telemetry.hook import tf_event_hook
@tf_event_hook(scenes=["Chain"])
def on_chain_event(name: str, context: types.Attributes) -> None:
print(f"事件: {name}, 场景: {context.get('scene')}, 详情: {context.get('info')}")
scenes 参数指定监听哪些场景。传入 "all"(默认值)表示监听所有场景;传入列表如 ["Chain", "LLM"] 则只监听指定场景的事件。
方式二:继承 BaseHook 类¶
适用于需要携带状态或更复杂逻辑的场景。类方式需要显式注册到 HookManager:
from opentelemetry.util import types
from tfrobot.telemetry.hook import BaseHook, HookManager
class TokenCounter(BaseHook):
def __init__(self):
self.total_tokens = 0
def execute(self, name: str, context: types.Attributes) -> None:
token_usage = context.get("token_usage")
if isinstance(token_usage, int):
self.total_tokens += token_usage
print(f"累计 Token: {self.total_tokens}")
counter = TokenCounter()
HookManager().register_hook(counter, scene="LLM")
EventContext 参考¶
Hook 回调的 context 参数是一个字典,其字段由事件所属的 Scene 决定。所有 Scene 共享以下基础字段:
| 字段 | 类型 | 说明 |
|---|---|---|
scene |
str |
事件来源场景 |
entity_id |
str |
发送事件的实体 ID |
span_id |
str |
当前 Span 的 ID |
trace_id |
str |
当前 Trace 的 ID |
desc |
str \| None |
事件描述(面向开发者) |
info |
str |
事件详情(面向用户,可直接用于日志输出) |
baggage |
str |
调用方透传的上下文,JSON 字符串(详见 透传原始上下文) |
各 Scene 在基础字段之上扩展了专属字段:
Robot¶
| 字段 | 说明 |
|---|---|
tool_names |
机器人注册的所有工具名称 |
tool_info_str |
工具信息摘要 |
msg_content |
收到的用户消息内容 |
msg_from_user / msg_from_user_id / msg_id |
消息来源信息 |
Brain¶
| 字段 | 说明 |
|---|---|
msg_content |
收到的用户消息内容 |
msg_from_user / msg_from_user_id / msg_id |
消息来源信息 |
Chain¶
| 字段 | 说明 |
|---|---|
current_input |
当前链的输入内容 |
LLM¶
| 字段 | 说明 |
|---|---|
streaming |
是否为流式输出 |
token_usage |
Token 用量。整数表示总量;字典则分 prompt/completion/total |
user_input |
用户输入 |
llm_model_name |
模型名称 |
llm_result |
模型返回值(JSON 字符串,before 事件为 "null") |
Tool¶
| 字段 | 说明 |
|---|---|
tool_name |
工具名称 |
tool_args / tool_kwargs |
工具调用参数 |
tool_return |
工具返回值(JSON 字符串,before 事件为 "null") |
tool_version / tool_description |
工具版本和描述 |
关于 tool_return 和 llm_result
这两个字段经过 json.dumps 序列化。如果需要完整的结构化数据,对其反序列化即可。如果只需要可读文本(如打印日志),直接使用基础字段 info 更方便。
Memory / MemoryStore¶
| Scene | 字段 | 说明 |
|---|---|---|
| Memory | query / max_token / top_k |
记忆检索参数 |
| MemoryStore | key |
存储键 |
Drive¶
无额外字段。
HookManager 关键行为¶
单例模式¶
HookManager 是全局单例。所有 Hook 注册到同一个实例,TFRobot 内部的 TFSpan.add_event 也通过这个单例分发事件。
弱引用机制¶
register_hook 默认使用弱引用(weak=True)。这意味着如果你的 Hook 对象在外部被回收,HookManager 会自动清理对它的引用,避免内存泄漏。
弱引用的陷阱
当使用 @tf_event_hook(weak=True) 装饰类时,装饰器内部会临时创建一个实例并注册。由于没有外部引用持有这个实例,它会被立即回收,导致 Hook 实际上不生效。对类使用装饰器时,建议 weak=False。
跳过 Hook 执行¶
在 EventContext 中设置 __skip_hooks__=True 可以跳过所有 Hook 的执行。这在内部测试或特定场景下用于避免副作用。
断开连接¶
hm = HookManager()
hm.disconnect(hook) # 从所有场景断开
hm.disconnect(hook, scene="LLM") # 仅从 LLM 场景断开
hm.clear() # 清除所有 Hook