跳到主要内容
版本:0.6

Tracing 快速开始

安装 extra

CubePi 将 OpenTelemetry 作为可选依赖:

pip install "cubepi[tracing]"

这会拉取 opentelemetry-sdk 及相关包。若未安装该 extra,导入 cubepi.tracing 时会抛出清晰的错误提示,让你在导入阶段就能发现问题,而不是运行到一半才报错。

挂载 Tracer

最简端到端配置——本地 JSONL 导出,符合习惯的 RAII 模式:

import asyncio
from cubepi import Agent, Model
from cubepi.providers.anthropic import AnthropicProvider
from cubepi.tracing import Tracer
from cubepi.tracing.exporters import JsonlSpanExporter


async def main() -> None:
agent = Agent(
provider=AnthropicProvider(api_key="…"),
model=Model(id="claude-sonnet-4-5-20250929", provider="anthropic"),
system_prompt="Be helpful.",
)

async with (
Tracer(
service_name="my-bot",
agent_name="assistant",
exporters=[JsonlSpanExporter(directory="./cubepi-traces")],
) as tracer,
tracer.attached(agent),
):
await agent.prompt("Say hello.")
await agent.wait_for_idle()
# 退出时:自动 detach(关闭所有已取消运行的 span,等待 flush)
# + tracer shutdown(flush + 关闭 exporter)。无需 try/finally。


asyncio.run(main())

如果无法改写成 async with(例如将 agent 传递给长生命周期 web handler), 也可以使用显式模式,效果完全等价:

detach = tracer.attach(agent)
try:
await agent.prompt("…")
finally:
# 以下两者单独调用均可:
# await detach() # 等待已调度的 flush
# await tracer.shutdown() # flush + 关闭 exporter
detach()
await tracer.shutdown()

即使完全忘记清理,Tracer 默认会注册一个 atexit hook,在进程退出时 同步 flush 缓冲 span——传入 atexit_flush=False 可关闭此行为,或在开发阶段 把它当作安全网使用。(SIGKILLos._exit 时不会触发;如需保证必达, 请使用 OTel 的同步 SimpleSpanProcessor。)

每次运行产生一个 JSONL 文件,按 trace_id 分片:

./cubepi-traces/
2026-05-19/
8e1c9a3f4b2d…d976a.jsonl ← 一条 trace,一个文件,每行一个 span

一条 trace 代表整次运行,包括所有嵌套 subagent 运行(它们继承父级的 trace_id,因此写入同一文件)。每个 span 仍携带 cubepi.run_id 属性, 可按单次运行过滤。

用任何支持 OTLP/JSON 的工具,或直接用 jq 打开:

jq -r '"\(.name) \(.attributes."gen_ai.operation.name" // "")"' \
cubepi-traces/2026-05-19/*.jsonl
# invoke_agent invoke_agent
# cubepi.turn
# chat claude-sonnet-4-5-20250929 chat

Span 层级

单次 prompt 经过一轮 LLM 往返,recorder 产生三个 span:

invoke_agent assistant [INTERNAL] gen_ai.operation.name=invoke_agent
└── cubepi.turn [INTERNAL] cubepi.turn.index=0
└── chat <model> [CLIENT] gen_ai.operation.name=chat

模型调用工具时,每个工具多一层:

invoke_agent assistant
└── cubepi.turn ← turn index 0
├── chat <model> ← 第一轮往返
└── execute_tool <tool_name> ← gen_ai.tool.name, gen_ai.tool.call.id
└── cubepi.turn ← turn index 1(工具结果返回后的响应)
└── chat <model>

MCP 工具的 execute_tool span 会有一个 CLIENT 子节点:

execute_tool <tool_name> [INTERNAL] cubepi 侧封装
└── tools/call <tool_name> [CLIENT] gen_ai.operation.name=execute_tool
mcp.method.name=tools/call
mcp.session.id=…
server.address / server.port

CLIENT span 会将 W3C traceparent 注入出站 HTTP 头,让下游已埋点的 MCP 服务器能够续接 trace。

取消、错误与中止

recorder 将取消视为控制信号,而非失败:

  • 流式传输中调用 agent.abort() → span 以 cubepi.aborted=trueerror.type=cubepi.aborted 关闭,状态为 UNSET(遵循 OTel 指导原则—— 取消不是错误)。
  • provider 抛出异常 → chat/turn/root 以 状态 ERROR 关闭,chat span 上有 exception 事件,error.type 由异常类派生(timeoutconnection_error、 完全限定类名……)。
  • MCP tools/call 返回 isError=true → CLIENT span 以 ERROR + error.type=mcp.is_error 关闭。

无论哪种情况,detach()tracer.shutdown() 始终会关闭运行遗留的所有 未关闭 span,已取消的运行依然会出现在后端,不会悄悄消失。

每个 span 的属性

默认(无需额外开启):

  • invoke_agent(根节点)—— gen_ai.operation.namegen_ai.provider.namegen_ai.agent.namecubepi.run_idcubepi.agent.system_prompt.sha256cubepi.agent.tools(名称列表)、cubepi.input_messages.countcubepi.output_messages.count
  • cubepi.turn —— cubepi.turn.indexcubepi.turn.stop_reasoncubepi.turn.tool_calls.countcubepi.turn.terminated_by_toolcubepi.run_id
  • chat <model> —— gen_ai.operation.namegen_ai.provider.namegen_ai.request.modelgen_ai.request.max_tokens / temperature / top_pgen_ai.request.streamgen_ai.usage.input_tokens / output_tokens / cache_read_input_tokens / cache_creation_input_tokens / reasoning_output_tokensgen_ai.response.model / finish_reasons / idgen_ai.response.time_to_first_chunk,以及 OpenAI 专属字段(openai.api.type、service tier、system fingerprint)
  • execute_tool <tool_name> —— gen_ai.operation.name=execute_toolgen_ai.tool.namegen_ai.tool.call.idgen_ai.tool.descriptiongen_ai.tool.typecubepi.tool.is_errorcubepi.tool.execution_mode
  • tools/call <tool_name>(仅 MCP)—— mcp.method.namemcp.session.idmcp.protocol.versionserver.addressserver.portgen_ai.tool.name

可选,通过 Tracer(record_content=True) 开启: gen_ai.input.messagesgen_ai.output.messagesgen_ai.system_instructionsgen_ai.tool.definitionsgen_ai.tool.call.argumentsgen_ai.tool.call.resultcubepi.llm.raw_requestcubepi.llm.raw_response。详见内容记录与脱敏

多 agent 单进程

TracerMeter 均可安全地在多个 agent 之间共享——多次调用 attach(agent) 即可。每次 attach 都有独立的 recorder / metric 状态, 并发 agent 不会共享或相互覆盖 span 或直方图状态;MCP CLIENT span 根据 哪个 agent 的 execute_tool span 是父节点来路由到正确的 Tracer。

使用 RAII 方式,叠加多个 agent 只需一个 async with

async with (
Tracer(...) as tracer,
tracer.attached(agent_a),
tracer.attached(agent_b),
):
await asyncio.gather(agent_a.prompt("…"), agent_b.prompt("…"))

为单次运行打标签

cubepi.tracing.tracing_context 将 per-run 标签和元数据作用于 invoke_agent span,非常适合记录 user_idsession_id、A/B 测试分组 等需要在后端过滤的字段:

from cubepi.tracing import tracing_context

async with tracer.attached(agent):
with tracing_context(tags=["beta-arm"], metadata={"user_id": "u-42"}):
await agent.prompt("Hello.")

span 上的属性:

  • cubepi.tags = ("beta-arm",)
  • cubepi.metadata.user_id = "u-42"

cubepi.metadata.* 前缀防止用户键与 recorder 自有 schema(如 cubepi.run_id) 冲突。标签和元数据的 contextvar 均为 per-asyncio-task 作用域,并发 agent 各自看到独立的值;嵌套的 tracing_context 块会合并(标签追加,元数据键取并集, 内层优先)。

下一步