Appearance
可观测性
AgentScope 2.x 没有独立的"日志/监控模块"。可观测性由两件事承担:TracingMiddleware 把 Agent 全生命周期接到 OpenTelemetry(OTel)追踪,事件流(reply_stream)把执行过程 结构化暴露。前者解决"事后回溯与跨服务关联",后者解决"实时观测与自行计量"。两者共享同一套 执行链:TracingMiddleware 本身就是一个内置中间件,它观察的事件就是你从 reply_stream 消费的事件。
版本基线:本文以 Python
agentscope 2.0.4(PyPI,2026-07-07)为准,要求 Python>=3.11。 导入路径为from agentscope.middleware import TracingMiddleware(源:src/agentscope/middleware/_tracing/__init__.py)。2.x 文档站存在版本化陷阱:非版本化的/en/URL 会 404,本页所有官方链接均使用versions/2.0.4/en/形式。 ⚠️ 1.x 的evaluate评估模块在 2.0 已移除,截至 2.0.4 未回归,不可作为可观测能力使用 (详见文末"evaluate 未回归"一节)。
为什么需要可观测性
Agent 的一次 reply 不是单次函数调用,而是"多轮 ReAct 循环 + 多次模型调用 + 多次工具执行"的 复合过程。当结果异常、成本超标或延迟飙升时,你需要回答三个问题:
- 调试:哪一轮推理出了问题?哪个工具返回了错误?模型当时收到了什么上下文?
- 成本:这次 reply 一共调了几次模型?输入输出各多少 token?哪个工具最烧钱?
- 性能:reply 总耗时多少?模型调用占多少?工具执行占多少?哪一轮是瓶颈?
这三个问题都无法靠 print 解决——你需要在执行链的多个阶段同时记录结构化数据,并能把它们 关联成一条完整的调用链。2.x 的答案是:用 TracingMiddleware 自动产出 OpenTelemetry span, 把"reply -> 模型调用 -> 工具执行"组织成一棵层级树,发送到任意 OTLP 后端(Jaeger / Tempo / Datadog / Honeycomb / 自建 Collector)进行查询与可视化。
前端类比
如果你来自前端,OpenTelemetry 追踪类比为前端的分布式 tracing(Sentry Performance / Datadog APM / OpenTelemetry Browser):
- span 就是一个 trace 中的一个节点,类似前端 tracing 里的一次
fetch或一次组件渲染。 每个 span 有名字、起止时间、属性(attributes)和父 span。 - trace 是一次完整请求的所有 span 组成的树,类似前端一个页面加载从导航到 FCP 到 LCP 的 完整瀑布图。在 AgentScope 里,一次
reply就是一个 trace,根 span 是 reply 本身。 - OTLP endpoint 类比前端 SDK 配置的
tracesEndpoint——span 通过 HTTP/gRPC 推送到 Collector,再由 Collector 转发到后端存储与 UI。
AgentScope 原生语义:OTel 不是"打日志",而是结构化执行记录。span 的属性(attributes) 是键值对,不是自由文本;span 之间的父子关系由执行链自动建立,不需要手动传 trace context。 TracingMiddleware 把 Agent 已有的中间件 hook(on_reply / on_model_call / on_acting) 桥接到 OTel span,因此你不需要在业务代码里插桩——挂载中间件即生效。这一点和前端需要在每处 fetch 手动包 tracer.startActiveSpan() 不同:AgentScope 的执行链是框架托管的,中间件层是 唯一的、集中的插桩点。
TracingMiddleware:挂载方式
TracingMiddleware 是内置中间件,通过 Agent 构造函数的 middlewares 参数挂载:
python
import asyncio
import os
from agentscope.agent import Agent
from agentscope.credential import DashScopeCredential
from agentscope.message import UserMsg
from agentscope.middleware import TracingMiddleware
from agentscope.model import DashScopeChatModel
agent = Agent(
name="my_agent",
system_prompt="你是一个严谨的中文助手。",
model=DashScopeChatModel(
credential=DashScopeCredential(
api_key=os.environ["DASHSCOPE_API_KEY"],
),
model="qwen-plus",
),
middlewares=[
TracingMiddleware(), # 全生命周期 OTel 追踪
],
)
async def main() -> None:
result = await agent.reply(
UserMsg(name="user", content="用三句话介绍 AgentScope。"),
)
print(result.get_text_content())
asyncio.run(main())无 TracerProvider 时短路(零开销)
TracingMiddleware 在每个 hook 入口检查进程是否注册了 TracerProvider。当没有配置 TracerProvider 时,每个 hook 直接短路到 next_handler()——不创建 span、不计算属性、不分配 trace context,开销可忽略。
这意味着你可以在所有环境无条件挂载 TracingMiddleware(),只在需要采集的生产/预发环境配置 OTLP exporter。开发本地不配 exporter 时,中间件几乎是空操作,不会拖慢调试。
这和"挂了就一定有开销"的日志库不同:TracingMiddleware 的插桩点是框架 hook,是否真正产出 span 由 OTel 全局状态决定,而非中间件实例自身。所以不要为了"省开销"而在开发环境条件性地 移除它——直接常驻即可。
OpenTelemetry 配置
要真正产出 span,需在进程启动时注册 TracerProvider 并配置 OTLP exporter。下面是官方文档给出的 标准配置(源:2.0.4 文档 Middleware#tracing):
python
import os
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
# 1. 创建 TracerProvider
provider = TracerProvider()
# 2. 添加 BatchSpanProcessor + OTLPSpanExporter
# endpoint 指向你的 OTLP Collector(HTTP 协议)
# 本地起 Collector 时通常是 http://localhost:4318/v1/traces
provider.add_span_processor(
BatchSpanProcessor(
OTLPSpanExporter(
endpoint=os.environ.get(
"OTEL_EXPORTER_OTLP_ENDPOINT",
"http://localhost:4318/v1/traces",
),
),
),
)
# 3. 注册为全局 TracerProvider
# 必须在创建 Agent / 挂载 TracingMiddleware 之前完成
trace.set_tracer_provider(provider)关键点:
trace.set_tracer_provider(provider)必须且只能调用一次。重复调用会抛RuntimeError。 在 Web 服务里通常放在应用启动钩子(FastAPI 的lifespan/ ASGI startup)中执行。BatchSpanProcessor异步批量上报:span 不会立即发送,而是攒批后后台 flush。进程退出前 需调用provider.force_flush()或确保atexit钩子触发,否则末尾的 span 会丢失。- endpoint 协议:
otlp.proto.http走 HTTP(端口 4318,路径/v1/traces),otlp.proto.grpc走 gRPC(端口 4317)。上面的示例用的是 HTTP,无需额外 gRPC 依赖。 - 依赖安装:OTel 是可选依赖,需自行安装
pip install opentelemetry-sdk opentelemetry-exporter-otlp。AgentScope 本身不强制安装。
后端选择:本地开发可用 Jaeger(jaegertracing/all-in-one 镜像,开启 OTLP 接收)直接查看 trace;生产可对接 Tempo / Datadog / Honeycomb / Grafana Cloud / 自建 OpenTelemetry Collector。
span 层级
TracingMiddleware 只插桩三个 hook,产出一棵三层 span 树。下表给出每个 span 的来源、父 span 与 携带属性(源:2.0.4 官方文档 Middleware#tracing):
| span(来源 hook) | 父 span | 携带属性(attributes) |
|---|---|---|
Agent Reply(on_reply,根 span) | 无(trace 根) | Agent 名、session ID、reply ID、输入消息、最终输出消息、HITL 待处理工具调用、外部执行待处理工具调用 |
Model Call(on_model_call) | Agent Reply | 模型名、provider、输入 token 数、输出 token 数、请求消息、响应消息(流式响应在最终 chunk 上写入属性) |
Tool Execution(on_acting) | Agent Reply | 工具名、call ID、输入参数、工具执行结果 |
关键说明:
- 根 span 是 reply,不是 reasoning。
TracingMiddleware不插桩on_reasoning,因此单轮 ReAct 推理不会单独产生 span——它被包含在 reply 根 span内。模型调用和工具执行是 reply 的直接 子 span,彼此是兄弟关系(不是 model_call 套 acting)。 - 一次 reply 产生多个 Model Call / Tool Execution span:每轮 ReAct 会触发一次模型调用 (一个 Model Call span),随后每个工具调用触发一个 Tool Execution span。多轮循环下,这些 span 按时间顺序排列在 reply 根 span 之下,形成完整的执行瀑布。
- 流式响应的属性写在最终 chunk:当模型以流式返回时,
on_model_call包裹的是流式响应, span 属性(含 token 计数)会在流结束的最终 chunk 上一次性写入,而非每个 delta 都写。 - 外部执行补 span:当 Agent 收到
ExternalExecutionResultEvent(工具在 Agent 外部执行后 回灌结果)时,TracingMiddleware会为每个外部执行结果合成一个补偿 span,保证外部执行的工具 也能在 trace 里看到,不会出现观测盲区。
添加自定义 span
要在 Agent 生命周期内追踪自定义操作(例如一次内部检索、一次外部 HTTP 调用),直接用标准 OTel Python SDK,获取一个 scope 为 AgentScope 的 tracer,把目标代码包进 span:
python
from opentelemetry import trace
from agentscope import __version__
tracer = trace.get_tracer("agentscope", __version__)
with tracer.start_as_current_span(
name="my_custom_retrieval",
attributes={
"retrieval.query": "AgentScope 2.x",
"retrieval.top_k": 5,
},
end_on_exit=True,
) as span:
# 你的代码在这里执行,会作为当前 reply trace 的子 span 上报
results = do_retrieval(...)
span.set_attribute("retrieval.result_count", len(results))这些自定义 span 与 TracingMiddleware 产出的内置 span 共享同一个 TracerProvider,会被发送到 同一个 OTLP collector,并自动嵌套到当前 reply 的 span 树中(只要在 reply 调用栈内执行)。
事件流计量
TracingMiddleware 解决"事后回溯",事件流解决"实时观测与自行计量"。reply_stream 产出的每个 事件都携带结构化数据,你可以从中统计 token、耗时与工具调用情况。
相关事件类(源:src/agentscope/event/__init__.py):
| 事件 | 携带信息 | 计量用途 |
|---|---|---|
ReplyStartEvent | reply 起始(reply ID 等) | 标记 reply 计时起点 |
ModelCallStartEvent | 模型调用起始 | 标记单次模型调用计时起点 |
ModelCallEndEvent | 模型调用结束,携带 token 用量 | 累计 input/output token |
ToolCallStartEvent | 工具调用起始(工具名、call ID、参数) | 标记单次工具执行计时起点 |
ToolResultEndEvent | 工具执行结束(结果) | 累计工具执行耗时、统计成功率 |
ReplyEndEvent | reply 结束(结束原因) | 标记 reply 计时终点、汇总 |
所有事件继承自 EventBase,其 metadata 字段是通用数据载体,框架与中间件会把附加信息塞进 metadata。token 用量、模型名等计量字段通过 ModelCallEndEvent 暴露——这与 Model Call span 属性里的 token 计数是同一份数据,只是消费通道不同(一个是 OTLP span,一个是事件流)。
下面是从 reply_stream 自行统计 token 的示例:
python
import asyncio
import os
from agentscope.agent import Agent
from agentscope.credential import DashScopeCredential
from agentscope.event import ModelCallEndEvent, ReplyEndEvent, ReplyStartEvent
from agentscope.message import UserMsg
from agentscope.middleware import TracingMiddleware
from agentscope.model import DashScopeChatModel
agent = Agent(
name="my_agent",
system_prompt="你是一个严谨的中文助手。",
model=DashScopeChatModel(
credential=DashScopeCredential(
api_key=os.environ["DASHSCOPE_API_KEY"],
),
model="qwen-plus",
),
middlewares=[TracingMiddleware()],
)
async def main() -> None:
total_input_tokens = 0
total_output_tokens = 0
model_calls = 0
async for event in agent.reply_stream(
UserMsg(name="user", content="查一下今天杭州的天气并总结。"),
):
if isinstance(event, ModelCallEndEvent):
model_calls += 1
# token 用量随事件携带,字段名以实际 metadata 为准
# 这里从 metadata 读取(具体键名以你的模型返回为准)
usage = event.metadata.get("usage", {})
total_input_tokens += usage.get("input_tokens", 0)
total_output_tokens += usage.get("output_tokens", 0)
elif isinstance(event, ReplyEndEvent):
print(
f"reply 结束:模型调用 {model_calls} 次,"
f"输入 {total_input_tokens} token,输出 {total_output_tokens} token"
)
asyncio.run(main())⚠️ 上面
event.metadata.get("usage", ...)的具体键名取决于模型 provider 返回的 usage 结构。ModelCallEndEvent携带 token 用量这一事实来自源码事件定义,但各 provider 填入metadata的字段命名可能不同。接入时先打印一次event.metadata确认实际结构,再做累加。
事件流计量的价值在于实时:你可以在 reply 还没结束时就知道已经花了多少 token,从而做软预算 告警或前端进度展示。TracingMiddleware 的 span 则是事后完整记录,适合离线分析与长期趋势。 两者互补:实时用事件流,回溯用 OTel。
与中间件的关系
TracingMiddleware 本质上是一个内置中间件,实现了 on_reply / on_model_call / on_acting 三个洋葱 hook。它的全部能力都建立在中间件洋葱模型之上——没有中间件系统就没有 TracingMiddleware。
这意味着:
- 挂载与顺序遵循中间件规则:
middlewares=[TracingMiddleware(), ...]中,列表第一个是最外层。 如果你同时挂载TracingMiddleware和自定义中间件,把 Tracing 放最外层能保证它包裹整条链, span 覆盖最全。 - 可与其他中间件组合:Tracing 与
ReplyBudgetControlMiddleware(预算)、RAGMiddleware(检索)等可同时挂载,互不冲突。预算中间件触发的tool_choice="none"强制收尾也会被 Tracing 记录在 span 属性里。 - 中间件详情见专页:洋葱模型、6 个 hook、自定义中间件编写等内容,见 中间件。
evaluate 未回归
1.x 提供了一个 agentscope.evaluate 模块,用于对 Agent 做基准评估(benchmarking / evaluation 流水线)。该模块在 2.0.0 被移除,截至 2.0.4 未回归(源:2.0.4 release notes 与源码仓库—— src/agentscope/ 下已无 evaluate/ 目录)。
因此:
- 不要从
agentscope.evaluate导入任何东西,会ImportError/ModuleNotFoundError。 - 不要把"评估"当作 2.x 可观测能力的一部分。2.x 的可观测性只有 OTel 追踪 + 事件流两条通道, 没有内置的评估流水线。
- 评估需求暂需自行实现:可基于
reply_stream事件流 + TracingMiddleware span 自建评估 脚本,或等待官方后续版本回归评估能力。官方未给出evaluate回归时间表。 - 迁移影响与 1.x 到 2.x 的其他破坏性变更一并列在 1.x 到 2.x 迁移。
标注:
evaluate模块未回归(removed in 2.0.0, not returned as of 2.0.4)。本文所有 可观测能力均不依赖该模块。
常见错误
- 挂了
TracingMiddleware()却看不到 span:最常见原因是没有注册全局TracerProvider。无 provider 时中间件短路,不产出任何 span——这是设计行为(零开销),不是 bug。检查trace.set_tracer_provider(provider)是否在创建 Agent 前调用,且只调用了一次。 - OTLP endpoint 不通导致 span 丢失:
OTLPSpanExporter默认不会因发送失败而抛错中断业务, 失败的 span 会被丢弃或重试失败后静默丢弃。排查时先用curl确认 Collector 的http://<host>:4318/v1/traces可达,再检查 Collector 日志是否收到数据。生产环境务必监控 Collector 的span_drop指标。 - 进程退出时末尾 span 丢失:
BatchSpanProcessor是异步批量上报,进程直接sys.exit或被SIGKILL时,未 flush 的 span 会丢失。长任务结束或服务关停前调用provider.force_flush()(可配合atexit.register(provider.force_flush))。 - 误用
evaluate做评估:agentscope.evaluate在 2.x 已移除,导入会直接报错。如果你的 旧代码依赖它,需自行用事件流 + span 重建评估逻辑,不要期望它"还能用"。 - 在自定义 span 里用错 tracer scope:自定义 span 应使用
trace.get_tracer("agentscope", __version__)获取 tracer,这样它才会与TracingMiddleware的内置 span 归到同一 instrumentation scope,便于在后端按 scope 过滤。用trace.get_tracer(__name__)也能工作, 但会在 UI 里显示为不同 scope,难以与内置 span 一起筛选。 - 期望
on_reasoning有 span:TracingMiddleware不插桩on_reasoning,单轮推理没有独立 span。如果你需要单轮推理级别的追踪,自行在on_reasoning中间件里用tracer.start_as_current_span补一个自定义 span。 - 把 token 计数硬编码到业务代码:token 用量已由
ModelCallEndEvent和 Model Call span 属性自动携带,不需要你在业务代码里手动数。手动数既容易漏(流式 delta),也和框架重复。
先修
下一步
- 部署 Agent Service - 生产部署时如何统一配置 OTel collector 与多租户追踪