Skip to content

透传原始上下文

TFRobot 的遥测系统基于 OpenTelemetry 构建。在实际业务中,工程师经常需要在 Hook 回调中获取调用方的业务上下文——比如用户 ID、会话 ID、请求来源等信息。这些信息不属于 TFRobot 内部状态,但 Hook 处理函数需要它们来完成日志关联、审计、计费等工作。

本文介绍 TFRobot 如何利用 OpenTelemetry Baggage 机制解决这个问题,以及最终的效果。

要解决的问题

考虑以下场景:你注册了一个 after_chain_run 的 Hook,希望在 Chain 执行结束后把结果写入某个业务系统。但 Hook 的回调签名是固定的——你只能拿到 event_nameattributes(即 BaseEventContext),拿不到调用方的业务上下文

如果不借助上下文透传,你只能依赖全局变量或闭包来传递这些信息,这在并发场景下会出错。

TFRobot 中的实现

span_decorator 自动采集 Baggage

TFRobot 的核心组件(Robot、Brain、Chain、LLM、Tool、Memory)都通过 span_decorator 包装了关键方法。在每次 Span 事件触发时,装饰器会自动读取当前线程的 Baggage 并写入事件上下文

# tfrobot/telemetry/context_schema.py — span_decorator 内部逻辑(简化)
with tracer.start_as_current_span(func.__name__, attributes=attr) as span:
    span_context = span.get_span_context()
    bg = baggage.get_all()           # 读取调用方附着的所有 Baggage
    bg_str = json.dumps(dict(bg))

    before_event_attr.update(
        span_id=format(span_context.span_id, "016x"),
        trace_id=format(span_context.trace_id, "032x"),
        baggage=bg_str,              # 写入事件上下文
    )
    span.add_event(before_event_name.value, attributes=before_event_attr)

BaseEventContext.baggage 字段

所有事件上下文都继承自 BaseEventContext,其中包含 baggage 字段:

class BaseEventContext(BaseModel):
    scene: EventScene
    entity_id: str
    span_id: Optional[str] = NOT_SET
    trace_id: Optional[str] = NOT_SET
    desc: Optional[str] = None
    info: str
    baggage: Optional[str] = NOT_SET   # 调用方透传的上下文,JSON 字符串

这意味着:任何注册了 Hook 的监听者,都可以从事件属性中直接读取 baggage 字段,无需额外传参。

实际案例:TFRobotServer 中的会话 ID 透传

TFRobotServer 是 TFRobot 的 HTTP 服务层,它的核心链路是:FastAPI 接口 → Celery 异步任务 → TFRobot.run()。在这条链路中,conversation_id(会话 ID)需要从 HTTP 请求一直传递到 TFRobot 内部的 Hook,用于将 Agent 运行轨迹(Trajectory)关联到正确的会话。

以下是真实的实现过程。

第一步:FastAPI 接口附着 Baggage

用户发送消息时,add_message 接口将 conversation_id 写入 Baggage,然后触发 Celery 任务:

# tfrobotserver/api/v1/chat/chats.py — add_message 方法(简化)
bg_with_conversation_id = baggage.set_baggage(
    "conversation_info", json.dumps({"conversation_id": conversation_id})
)
token = attach(bg_with_conversation_id)
try:
    task_id = self.run_service.run(robot_id, tf_msg)  # 发送 Celery 任务
finally:
    detach(token)

Celery 的 CeleryInstrumentor 配置了 propagate_headers=["traceparent", "tracestate", "baggage"],会自动将当前 OpenTelemetry Context(包括 Baggage)序列化到 Celery 消息头中。

第二步:Celery Worker 恢复 Baggage

Celery Worker 收到任务后,在 TFSRobotTask.__call__ 中从消息头提取并恢复上下文:

# tfrobotserver/robot_celery.py — TFSRobotTask(简化)
class TFSRobotTask(Task):
    def __call__(self, *args, **kwargs):
        headers = self.request.headers
        context = extract(headers) if headers else None

        tracer = get_tf_tracer_provider().get_tracer(self.name)
        ctx_token = None
        if context:
            # 必须显式 attach,仅在 start_as_current_span 中传 context 不会传播 Baggage
            ctx_token = attach(context)
        try:
            with tracer.start_as_current_span("TFRobotCelery::robot.run", context=context, ...):
                return super().__call__(*args, **kwargs)
        finally:
            if ctx_token:
                detach(ctx_token)

关键陷阱

仅在 tracer.start_as_current_span(context=context) 中传递 context,Baggage 不会被传播。必须显式调用 attach(context) 才能让后续的 baggage.get_all() 读取到值。这是 TFRobotServer 开发中踩过的坑。

第三步:TFRobot 内部自动采集

从这里开始就是 TFRobot 框架的工作了。robot.run() → Brain → Chain → LLM/Tool 的每个 span_decorator 都会自动调用 baggage.get_all() 并写入 EventContext 的 baggage 字段。调用方无需做任何额外操作。

第四步:Hook 中消费 Baggage

TFRobotServer 注册了一个 Hook 用于记录运行轨迹(Trajectory)。它从 EventContext 的 baggage 字段中解析出 conversation_id,将每个事件关联到对应的会话:

# tfrobotserver/telemetry/event_trajectory.py — record_trajectory_event(简化)
def record_trajectory_event(name: str, context: types.Attributes) -> Optional[Trajectory]:
    context = dict(context or {})
    if not context.get("conversation_id"):
        baggage_str = context.get("baggage", "{}")
        baggage_data = json.loads(baggage_str)
        conversation_info = json.loads(baggage_data.get("conversation_info", "{}"))
        conversation_id = conversation_info.get("conversation_id")
        if conversation_id is not None:
            context["conversation_id"] = conversation_id

    # 使用 conversation_id + trace_id + span_id 构建 Trajectory 记录并写入数据库
    ...

完整数据流

sequenceDiagram
    participant Client as 客户端
    participant API as FastAPI
    participant Celery as Celery Worker
    participant Robot as TFRobot
    participant Hook as Trajectory Hook

    Client->>API: POST /conversations/{id}/messages
    API->>API: baggage.set_baggage("conversation_info", ...)
    API->>API: attach(ctx)
    API->>Celery: send_task("robot.run")<br/>消息头携带 baggage

    Celery->>Celery: extract(headers) → context
    Celery->>Celery: attach(context)
    Celery->>Robot: robot.run(msg)

    Robot->>Robot: span_decorator 自动<br/>baggage.get_all() → EventContext
    Robot->>Hook: add_event 触发 HookManager
    Hook->>Hook: 从 context["baggage"]<br/>解析 conversation_id
    Hook->>Hook: 写入 Trajectory 数据库

效果与启示

维度 效果
解耦 FastAPI 层只管附着 Baggage,TFRobot 只管采集,Hook 只管消费——三方互不感知
跨进程透传 从 FastAPI 进程到 Celery Worker 进程,Baggage 通过消息头自动传播
并发安全 OpenTelemetry Context 是线程/协程隔离的,不同请求的 Baggage 互不干扰
零侵入 TFRobot 框架本身不需要知道 conversation_id 的存在,span_decorator 透明地传递

请求生命周期

Baggage 的生命周期与 attach/detach 的作用域一致。务必在 finally 块中调用 detach,避免上下文泄漏到后续请求。在异步场景中,确保 attach/detach 在同一个协程内配对使用。