Agent 可观测概述3:采集与还原的现实选择


这是 Agent 可观测性系列的第三篇文章。前两篇我们聊了”为什么难”和”观测什么”,这一篇来聊最落地的问题:数据怎么采集?

理想很丰满,现实很骨感

如果不考虑任何约束,理想的 Agent 可观测性应该是这样的:

  • 完整记录每一次 LLM 调用的 Prompt 和 Response
  • 完整记录每一次工具调用的输入、输出、耗时
  • 完整记录 Agent 的思维链(Chain of Thought)
  • 实时关联所有数据,形成完整的执行图景

但现实中,我们面对的是:

  • Agent 可能是第三方框架,代码改不动
  • LLM 流量是 HTTPS 加密的,抓包看不到
  • 工具调用分散在各种协议里(HTTP、gRPC、SQL、管道)
  • 不同数据源的时间戳和 ID 体系不统一

这篇文章,就来聊聊在这些约束下,我们能做出什么样的现实选择。


一、三种采集范式的权衡

从技术实现角度,Agent 数据采集大致有三种范式:

范式 1:代码埋点 (SDK Instrumentation)

原理:在 Agent 代码中引入 SDK,SDK 在关键位置(LLM 调用、工具调用)自动上报数据。

典型场景

from agent_observability import trace

@trace
def call_llm(prompt):
    response = openai.chat.completions.create(...)
    return response

@trace
def call_tool(tool_name, params):
    result = tools[tool_name].execute(params)
    return result

优点

  • 能获取最丰富的上下文(变量值、调用栈、自定义标签)
  • 对应用层语义理解最深
  • 社区生态成熟(LangSmith、Langfuse 等)

缺点

  • 侵入性强:需要修改业务代码
  • 覆盖有限:只能埋你改得动的代码
  • 信任问题:Agent 可以选择性上报,绕过监控

适用场景:自研 Agent 的开发调试阶段

范式 2:网络代理 (Network Proxy)

原理:在 Agent 与外部服务之间部署代理,拦截并解析流量。

典型场景

  • 在 Agent Pod 前面加一层 Envoy Sidecar
  • 部署一个透明代理解密 HTTPS 流量
  • 使用 API Gateway 拦截 LLM 调用

优点

  • 无需改代码:对业务透明
  • 集中管控:一个代理点可以覆盖所有 Agent
  • 策略可控:可以在代理层做限流、审计、拦截

缺点

  • 性能开销:额外的网络跳转 + TLS 终结
  • 单点风险:代理挂了,Agent 就断了
  • 协议局限:只能处理网络流量,进程内调用看不到

适用场景:需要集中管控 LLM 出口流量的场景

范式 3:运行时观测 (Runtime Observation)

原理:在操作系统层面(内核或运行时)采集数据,无需修改任何应用代码。

典型技术

  • eBPF:在内核层面挂载探针,捕获系统调用
  • uprobes:在用户态函数入口/出口采集数据
  • ptrace:跟踪进程行为

优点

  • 零侵入:完全不需要改业务代码
  • 全覆盖:所有行为都能观测到,包括”不想被观测的”
  • 绕不过去:从内核层面采集,应用无法规避

缺点

  • 实现复杂:需要深入了解内核机制
  • 性能敏感:不当使用可能影响系统稳定性
  • 语义丢失:内核只能看到字节流,应用层语义需要重建

适用场景:生产环境的安全审计、异常检测

我的选择:分层组合

实际工作中,我发现没有银弹,最佳实践是分层组合:

层次采集方式采集内容
应用层SDK 埋点(可选)Prompt/Response 语义、自定义标签
网络层流量解析A2L、A2T 的 HTTP/gRPC/SQL 流量
系统层eBPF进程创建、文件读写、网络连接

三层数据通过 Trace ID 或时间窗口关联,形成完整视图。


二、审计日志与运行时行为的错位

在 Kubernetes 环境中,有一个特别棘手的问题:审计日志和运行时行为是割裂的

典型场景:kubectl exec

假设一个 Agent 通过 kubectl exec 进入 Pod 执行命令:

kubectl exec -it my-pod -- bash
# 进入容器后
rm -rf /data/important/*

从 K8s Audit Log 的视角,你能看到:

{
  "verb": "create",
  "resource": "pods/exec",
  "user": "system:serviceaccount:default:agent-sa",
  "objectRef": {
    "name": "my-pod",
    "namespace": "production"
  },
  "requestReceivedTimestamp": "2024-01-15T10:30:00Z"
}

审计日志告诉你:什么时候哪个 Pod 发起了 exec 请求。

但审计日志不会告诉你容器内执行了什么命令——因为 exec 建立的是一个交互式会话,后续的命令通过 SPDY/WebSocket 流式传输,不在审计日志的记录范围内。

另一边,如果你在节点上用 eBPF 监控 execve 系统调用,你能看到:

{
  "syscall": "execve",
  "pid": 12345,
  "ppid": 12340,
  "comm": "rm",
  "args": ["-rf", "/data/important/*"],
  "container_id": "abc123",
  "timestamp": "2024-01-15T10:30:05Z"
}

运行时监控告诉你:哪个进程什么时候执行了什么命令

但运行时监控不知道这个命令是谁发起的——Linux 内核不感知 Kubernetes 的身份体系。

这个错位的代价

两边的数据都是对的,但关联不起来就等于废了一半

  • 安全团队看到 Audit Log 说”有人 exec 进了 Pod”,但不知道具体干了什么,无法评估风险
  • 运维团队看到 eBPF 说”有人执行了 rm -rf”,但不知道是谁,无法追责

如何修复这个错位?

这是我花了很多时间研究的问题。核心思路是建立关联桥梁

方案 A:时间窗口 + 命名空间匹配

如果:
  - Audit Log 记录了 Pod P 在时间 T1 收到 exec 请求
  - eBPF 记录了容器 C(属于 Pod P)在时间 T2 有新进程创建
  - T2 - T1 < 阈值(如 2 秒)
  - 新进程是 shim 进程的直接子进程(exec 特征)
那么:
  - 关联成功,给 eBPF 事件打上 User 标签

这是一种启发式关联,在大多数场景下有效。

方案 B:Admission Webhook 注入 Trace ID

通过 K8s Admission Webhook 拦截 exec 请求,自动修改命令:

# 原命令
kubectl exec -it my-pod -- bash

# 被 Webhook 修改后
kubectl exec -it my-pod -- /bin/sh -c "export TRACE_ID=xxx; bash"

这样,后续在容器内启动的所有进程都能继承 TRACE_ID 环境变量,eBPF 采集时可以一并捕获。

这种方案更精确,但有Shell 依赖——如果容器里没有 /bin/sh,就会失败。


三、为什么”可还原性”比”全量采集”更重要

在设计采集方案时,我走过一个弯路:一开始追求”全量采集”——尽可能多地采集数据,越全越好。

后来发现这是个坑。

全量采集的问题

问题 1:数据量爆炸

一个活跃的 Agent 每分钟可能产生:

  • 10+ 次 LLM 调用,每次 Prompt + Response 可能有几千 Token
  • 50+ 次工具调用,每次有输入输出
  • 数百次系统调用

全部存储的话,一个 Agent 每天可能产生 GB 级别的数据。几十个 Agent 就是几十 GB。

问题 2:噪音太多

不是所有数据都有价值。大量的”正常”数据淹没了”异常”数据。当你真正需要排查问题时,反而找不到关键信息。

问题 3:隐私和合规

Prompt 和 Response 里可能包含敏感信息(用户数据、API Key、内部系统信息)。全量存储会带来隐私合规风险。

“可还原性”是更好的目标

我现在的设计原则是:不追求存储所有数据,而是确保在需要时能还原关键场景

具体来说:

原则 1:分层采样

  • 元数据全量采集:每次调用的时间、类型、耗时、状态码——这些数据量小,可以全量
  • 内容智能采样:Prompt/Response 内容只采集摘要或首尾部分
  • 异常时全量:当检测到异常(错误、超时、敏感操作)时,触发全量采集

原则 2:关联 > 完整

与其存储每次调用的完整内容,不如确保:

  • 每次调用都有唯一 ID
  • ID 能跨系统(K8s Audit、eBPF、网络流量)关联
  • 关联关系持久化

这样,当需要深入分析时,可以通过 ID 追溯到原始系统获取详情(比如重放请求、查询原始日志)。

原则 3:能重建 > 能记录

很多时候,与其记录”Agent 做了什么”,不如确保能重建 Agent 的决策上下文

  • 当时的 System Prompt 是什么?
  • 当时的历史对话是什么?
  • 当时可用的工具列表是什么?

有了这些,即使没有记录每一步的详细过程,也能通过重放来理解 Agent 的行为。


小结

采集是可观测性的基础,但采集方案的选择要基于现实约束:

  1. 三种范式各有适用场景:SDK 埋点适合开发调试,网络代理适合集中管控,运行时观测适合安全审计
  2. 审计与运行时的错位是真实痛点:需要设计关联机制来弥合
  3. 可还原性 > 全量采集:目标不是存储所有数据,而是确保关键场景能被还原

下一篇是这个系列的最后一篇,我们来聊聊:有了数据之后,如何从数据走向判断?成本归因、异常检测、行为基线,这些”可观测性”如何走向”可运营”。


上一篇:Agent 可观测概述2: 我如何理解 Agent 的关键对象

下一篇:Agent 可观测概述4: 从数据到判断

评论