Skip to content

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

运行时数据流如下:

  1. TFRobot 组件(如 Chain)的关键方法被 span_decorator 包装
  2. 方法执行前后,span_decorator 构造 EventContext 并调用 TFSpan.add_event
  3. TFSpan.add_event 将事件分发给 HookManager
  4. HookManager 根据事件的 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_returnllm_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