详细指南:通过Agent指标、OTEL链路追踪、自定义trace属性三层实现AI Agent生产可观测性,并迁移至Amazon Bedrock AgentCore在CloudWatch中查看,附完整示例仓库。
你的 AI agent 已经上线生产。用户问了一个问题,它花了三十秒、调用了五个工具,给出了一个你无法解释的答案。它实际上做了什么?调用了哪些工具?在回答之前"思考"了多少次?如果你无法回答这些问题,你的 agent 就是在一抹黑。传统监控帮不了你:CPU、RAM、正常运行时间——这些盯的是机器,不是推理过程。
在这篇文章中,我们让一个旅行订票 agent 的正常行为变得可见。没有注入故障,没有混沌实验。一个真实的 agent 做它该做的事,通过四个越来越强大的视角来观察:
Agent 指标:这次运行花了多少成本,零额外配置
OpenTelemetry 链路追踪:agent 一步步走过的路径
自定义链路属性:你的业务上下文,放在同一条链路上
生产环境:通过 Amazon Bedrock AgentCore 在 Amazon CloudWatch 中获得同样的可见性
所有内容都来自一个可运行的示例仓库:observability-for-agents-sample-for-aws。每个演示都对应 Strands Agents 可观测性文档的特定章节。
先说明一下技术栈。演示使用了 Strands Agents,这是一个原生发送 OpenTelemetry 的开源 SDK。指标、分层链路追踪和 span 属性都是通用的 agent 可观测性概念。同样的模式可以迁移到其他 agent 框架,而且 Strands 与模型无关:支持任何 LLM 提供方(Amazon Bedrock、Anthropic、通过 Ollama 的本地模型,或其他)。
四个演示都针对同一个旅行 agent 进行插桩:它搜索真实的沙盒航班票价(Duffel API)、查询真实天气(Open-Meteo),并将航班预订写入本地 SQLite 账本。唯一变化的是,演示之间 agent 内部行为有多少变得可见,以及这些可见性数据存在哪里:

每一次 Strands agent 运行本身就自带指标:推理循环次数、token 使用量、每个工具的调用次数和耗时,通过 result.metrics.get_summary() 暴露。无需额外安装、无需导出器、无需设置。每一个 AI agent 运行都有一个轮廓,而这个轮廓在你配置任何东西之前就已经被捕获了。
用两个视角比较同一次运行。首先,传统日志:
DEBUG | strands.tools.executors._executor | tool_use=<...name': 'search_flights'...> | streaming
DEBUG | strands.tools.executors._executor | tool_use=<...name': 'get_weather'...> | streaming
DEBUG | strands.tools.executors._executor | tool_use=<...name': 'book_flight'...> | streaming
John Doe's flight from JFK to MIA has been successfully booked ... booking reference BK-JSFPJ5 ...
这对"这次运行了吗"有用。对"花了多少成本"没用。现在是内置指标,一行方法调用:
result = agent("Book a one-way flight from JFK to MIA...")
print(result.metrics.get_summary())
{
"total_cycles": 3,
"total_duration_s": 5.13,
"accumulated_usage": {"inputTokens": 2520, "outputTokens": 209, "totalTokens": 2729},
"tool_usage": {
"search_flights": {"call_count": 1, "success_count": 1, "average_time_s": 0.721},
"get_weather": {"call_count": 1, "success_count": 1, "average_time_s": 1.434},
"book_flight": {"call_count": 1, "success_count": 1, "average_time_s": 0.006}
}
}
这是 agent 运行的真实输出,每个字段都回答了日志行无法回答的问题:
total_cycles: 3。Agent 不是单次函数调用,而是一个循环:模型调用一个工具,用结果再思考,再调用下一个。这里走了三个循环。如果这个数字对一个问题永远超过十,那就出问题了,而现在你可以看到它。
accumulated_usage。整个订票过程用了 2,729 个 token。注意输入大约是输出的十倍;这在 agent 中很典型,因为每个工具结果都会被反馈给模型。这个数字告诉你每个请求实际上有多重。
tool_usage。三个工具,三种完全不同的性能画像:search_flights 0.7 秒(真实的 API 调用),get_weather 1.4 秒(另一个 API),book_flight 6 毫秒(一次本地写入)。没有这个分解,"agent 变慢了"是一个谜。有了它,就是诊断。
还有一个值得从第一天就培养的习惯:演示还会直接查询预订数据库,这样你可以交叉核对 agent 所说的("已预订!")和实际持久化的内容。在这次运行中,agent 的声明和 ground truth 是一致的。
坦诚的注意事项:在 Strands 1.47.0 中,accumulated_metrics.latencyMs 对某些 LLM 提供方读取为 0。这在 provider 流式处理代码中是一个 TODO(我通过阅读已安装的 SDK 源码验证了这一点)。Token 计数和每个工具的计时在所有地方都是准确的;将顶层 latencyMs 视为尚未实现。

指标是一张平面快照,链路追踪是路径。链路追踪记录了一个请求的完整层级:哪个推理循环调用了哪个模型调用,哪个调用触发了哪个工具,按什么顺序,带时间戳。在 Strands 中,开启 OpenTelemetry 链路追踪只需两行:
from strands.telemetry import StrandsTelemetry
strands_telemetry = StrandsTelemetry()
strands_telemetry.setup_console_exporter() # print the span tree to stdout
# strands_telemetry.setup_otlp_exporter() # or send it to a collector (Jaeger, CloudWatch, ...)
StrandsTelemetry 连接 OpenTelemetry SDK 并将其注册为全局 tracer provider。此后的每一次 Agent(...) 调用都会自动被插桩;不需要手动为你的 agent 循环包装 span。运行同样的旅行查询,控制台会打印出文档化的 span 层级:
invoke_agent Strands Agents # the whole run (top-level span)
execute_event_loop_cycle # one reasoning cycle
chat # the model invocation for that cycle
execute_tool search_flights # one span per tool call
execute_tool get_weather
execute_tool book_flight
每个 span 都带有属性。invoke_agent span 持有汇总数据(gen_ai.usage.total_tokens: 2725、gen_ai.request.model),每个 execute_tool span 持有该次调用的 gen_ai.tool.name、gen_ai.tool.call.id、tool.status 以及格式化的工具结果。光是链路追踪就足以回答"book_flight 失败了吗,它返回了什么?"——无需重跑任何东西。
而且因为这是标准的 OpenTelemetry,控制台导出器可以与任何 OTEL 后端互换。想在本地获得可视化 UI?一条 Docker 命令启动 Jaeger,一个环境变量指向导出器,agent 代码不变。

开箱即用,span 携带技术属性:工具名、token 计数、状态。这些都无法回答"这是一次高价值预订吗?"这个上下文需要你自己添加,Strands 链路追踪文档记录了两种机制。演示同时用了两者。
静态上下文。Agent 级 trace_attributes 将元数据(session ID、user ID、标签)附加到 agent 产生的每个 span:
agent = Agent(
tools=[search_flights, get_weather, book_flight],
trace_attributes={"session.id": "demo-03-custom-trace-attributes"},
)
动态上下文。一个 hook 在业务规则触发的精确时刻标记活跃 span。AfterToolCallEvent 回调在每个工具调用完成后立即运行;在那个时刻,当前打开的 span 就是该工具的 execute_tool span,所以 trace.get_current_span() 可以直接获取它:
from opentelemetry import trace
from strands.hooks import AfterToolCallEvent, HookProvider, HookRegistry
VIP_THRESHOLD = 50.0 # low on purpose, so sandbox fares cross it
class TagVipBookings(HookProvider):
def __init__(self, threshold: float):
self.threshold = threshold
def register_hooks(self, registry: HookRegistry, **kwargs) -> None:
registry.add_callback(AfterToolCallEvent, self._tag_if_vip)
def _tag_if_vip(self, event: AfterToolCallEvent) -> None:
if event.tool_use.get("name") != "book_flight":
return
amount = float(event.tool_use.get("input", {}).get("amount", 0))
span = trace.get_current_span()
span.set_attribute("business.booking_amount_usd", amount)
span.set_attribute("business.vip_booking", amount >= self.threshold)
运行 agent,找到 execute_tool book_flight span,自定义属性就与 SDK 本身的属性并列存在:
{
"name": "execute_tool book_flight",
"attributes": {
"gen_ai.tool.name": "book_flight",
"gen_ai.tool.status": "success",
"business.booking_amount_usd": 88.73,
"business.vip_booking": true
}
}
关键细节:这些数据存在于链路上,而不是会话中。模型永远看不到它。Trace 属性是 OpenTelemetry span 元数据,与消息列表完全分离,所以它们不会向 agent 的上下文添加任何 token。但六个月后,"给我看看本季度所有 VIP 预订"就是对链路的一次搜索。
之前一切都活在你的终端里。开发阶段这样挺好,但你的 agent 不会在你的终端里跑,你也不会守在那儿盯着控制台输出。基于开放标准构建的回报:我们生成的所有内容(指标、链路追踪、属性)都是 OpenTelemetry 数据,而 OTEL 数据是可移植的。换掉导出器,agent 代码不变。
演示 04 将同一个旅行 agent 部署到 Amazon Bedrock AgentCore Runtime。生产架构如下:
Agent 运行在 AgentCore Runtime 上(代码改动是一个装饰器:@app.entrypoint)。
三个工具成为通过 AgentCore Gateway 服务的 AWS Lambda 函数(一个带 IAM 认证的 Model Context Protocol 端点)。
book_flight 写入 Amazon DynamoDB 而非 SQLite。同样的工具、同样的预订,真正的存储。
添加一个依赖 aws-opentelemetry-distro(AWS Distro for OpenTelemetry),将 OTEL 数据发送到 CloudWatch。Runtime 在其自动插桩下运行你的 agent。
一次性账户设置:开启 CloudWatch Transaction Search。没有它,链路追踪不会出现在控制台(官方指南)。
调用已部署的 agent 后,打开 CloudWatch GenAI Observability,你会看到三个视图:
Agents View:账户中的每个 AgentCore agent,包含调用次数、延迟和错误率。
Sessions View:每次对话。记得第三层的 session.id 吗?这就是它发挥作用的地方:这就是从"出了问题"到"这是完整的对话"的路径。
Traces View:你在终端里学会阅读的同一个 span 树(invoke_agent → cycles → chat + execute_tool),现在渲染成可视时间线,每个属性都可搜索,包括 business.vip_booking。
仓库提供了两种部署方式:一个 AWS CDK 堆栈(cdk deploy,cdk destroy 清除所有内容,包括 DynamoDB 表)和一个分步 boto3 notebook(如果你想看到每个 API 调用)。
AI agent 的日志、指标和链路追踪有什么区别?
日志是事件的时间戳文本记录("工具 X 被调用了")。指标是对这些事件的测量(调用了多少次、多长时间、多少 token)。链路追踪是连接它们的层级时间线。日志告诉你某事发生了,指标告诉你花了多少成本,链路追踪展示了路径。
基础 agent 指标需要 OpenTelemetry 吗?
不需要。在 Strands 中,result.metrics.get_summary() 是基础 SDK 的一部分:无需 [otel] 额外包、无需导出器、无需收集器。当你想看链路追踪时(第二层起)才需要 OpenTelemetry。
需要收集器才能看链路追踪吗?
不需要。setup_console_exporter() 将完整的 span 树打印到终端。想用真实后端时使用 setup_otlp_exporter():本地用 Jaeger,生产用 CloudWatch。
自定义 trace 属性会额外消耗 token 吗?
不会。它们是 OpenTelemetry span 元数据,与模型看到的消息列表完全分离。模型永远不会读取它们。
这只能用于 Strands Agents 或 AWS 吗?
不是。Agent 循环、hook、指标和 OpenTelemetry 链路追踪都是通用的 agent 可观测性概念。演示使用 Strands 是因为这些原语是内置的,而且 Strands 与模型无关:支持任何 LLM 提供方,agent 代码无需任何改动。同样的模式可以迁移到其他 agent 框架。
Strands 内置可观测性与手动插桩相比如何?
Strands 原生发送 OpenTelemetry span,无需手动包装。在没有原生 OTEL 支持的框架中,你需要使用 OpenTelemetry SDK 直接为每个工具调用和推理循环进行插桩。数据结构是一样的——只是设置方式不同。
Agent 可观测性,按这里构建的,分三层:
指标告诉你 agent 做了什么、效率如何:循环次数、token、工具耗时。SDK 自带,免费。
链路追踪向你展示它走过的路径:每个决策,按顺序,完整上下文。两行代码开启。
Trace 属性将你的上下文添加到路径中,这样你可以按对你的业务重要的事项来搜索。只需一个字典和一个 hook。
你构建一次,三层都绑定在 OpenTelemetry 上,托管运行时用最少的配置将它们带到生产环境。
一个刻意的边界:这篇文章讲的是可观测性,即看到 agent 已经在做什么。它不是关于弹性或混沌测试(注入故障并从中恢复);那是另一个相关的话题。一旦你能看到 agent 在做什么,下一步自然就是验证它。评估正是建立在这套数据之上的。你无法验证你看不到的东西。
旅行 agent、全部四个演示(每个都是自包含的,包含脚本和 Jupyter notebook)以及两种生产部署路径都在示例仓库里:
→ observability-for-agents-sample-for-aws
你需要 Python 3.10+、uv、LLM 提供方的 API 密钥(演示支持多个提供方),以及一个免费的 Duffel 沙盒 token。演示 01 在一分钟内跑完:
git clone https://github.com/elizabethfuentes12/observability-for-agents-sample-for-aws.git
cd observability-for-agents-sample-for-aws/01-agent-metrics
uv venv && source .venv/bin/activate
uv pip install -r requirements.txt
cp .env.example .env # fill in your LLM provider API key and DUFFEL_API_KEY
uv run python test_agent_metrics.py
Clone 下来,跑起来,别再一抹黑地跑你的 agent 了。如果你能看到每个循环,你最惊讶的 agent 是哪一个?在评论区告诉我。
参考资料:Strands Agents 可观测性文档 · OpenTelemetry · AgentCore 可观测性 · CloudWatch GenAI 可观测性