Metadata-Version: 2.5
Name: cls-agent-observability
Version: 0.1.0
Summary: OpenAI Agents SDK 的腾讯云 CLS Agent 可观测接入 —— 一行初始化，无需手动埋点
Project-URL: Homepage, https://github.com/Tinker-LGD2026/cls-agent-observability
Project-URL: Documentation, https://github.com/Tinker-LGD2026/cls-agent-observability#readme
Project-URL: Source, https://github.com/Tinker-LGD2026/cls-agent-observability
Project-URL: Issues, https://github.com/Tinker-LGD2026/cls-agent-observability/issues
Author: tinkerli
Maintainer: tinkerli
License: MIT License
        
        Copyright (c) 2026 tinkerli
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: agent,cls,llm,observability,openai-agents,opentelemetry,tencent-cloud,tracing
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: System :: Monitoring
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: openai-agents<1.0,>=0.15
Requires-Dist: opentelemetry-api>=1.44.0
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.44.0
Requires-Dist: opentelemetry-sdk>=1.44.0
Description-Content-Type: text/markdown

# cls-agent-observability

给 [OpenAI Agents SDK](https://github.com/openai/openai-agents-python) 应用接入**腾讯云 CLS Agent 可观测**。

**一行代码初始化，业务代码零改动。**

```python
from cls_agent_observability import setup

setup()   # 读环境变量即可
```

之后正常调用 `Runner.run()` / `Runner.run_streamed()`，Agent 的调用链、Token 用量、
工具调用、错误原因就会自动出现在 CLS 控制台，**不需要在业务代码里写任何埋点**。

---

## 目录

- [它能给你什么](#它能给你什么)
- [快速开始（5 分钟）](#快速开始5-分钟)
- [工作原理](#工作原理)
- [完整配置项](#完整配置项)
- [自定义字段：三种注入方式](#自定义字段三种注入方式)
- [部署：Docker / 虚拟机 / 物理机](#部署docker--虚拟机--物理机)
- [对你的应用有多大影响](#对你的应用有多大影响)
- [兼容性](#兼容性)
- [排障](#排障)

---

## 它能给你什么

接入后在 CLS 的 Agent 可观测里可以看到：

| 能力 | 说明 |
|---|---|
| **完整调用链** | 一次请求 → Agent 调用 → ReAct 轮次 → 模型调用 / 工具调用的父子关系 |
| **多轮会话聚合** | 同一个用户会话的多次请求归到同一个 Session，轮次号 `t1 / t2 / t3` 递增 |
| **Token 与成本** | 每次模型调用的输入/输出/缓存命中 Token，以及每个 Agent 的汇总用量 |
| **失败可定位** | 限频（`rate_limit`）、超时（`timeout`）、鉴权失败（`authentication_error`）等**分类**错误，而不是一句笼统的报错 |
| **工具调用详情** | 工具名、入参、返回值、耗时，失败时带错误类型 |
| **多 Agent / Handoff** | Agent 之间的转交关系，各 Agent 的开销分别归因 |
| **正文可选** | 输入输出消息按需采集，默认关闭（保护隐私与存储成本） |

设计上的几个取舍：

- **不侵入业务代码**：完全基于 Agents SDK 自身的 `TracingProcessor` 生命周期回调，
  你不需要手写 span、不需要装饰器、不需要改函数签名。
- **不引入专有依赖**：上报走 CLS 官方支持的标准 OTLP/HTTP，只依赖 OpenTelemetry 社区包。
- **不给你的应用添新故障点**：转换层全程异常隔离，内部状态有内存上界，
  上报在后台线程完成（详见[对你的应用有多大影响](#对你的应用有多大影响)）。

---

## 快速开始（5 分钟）

### 第 1 步：装包

```bash
pip install cls-agent-observability
```

**中国大陆网络较慢时用国内镜像**（任选其一）：

```bash
# 清华 TUNA（推荐）
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple cls-agent-observability

# 阿里云
pip install -i https://mirrors.aliyun.com/pypi/simple/ cls-agent-observability

# 腾讯云（在腾讯云服务器上用这个最快）
pip install -i https://mirrors.cloud.tencent.com/pypi/simple/ cls-agent-observability
```

想长期生效，把镜像写进 pip 配置（只需做一次）：

```bash
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
pip config set global.trusted-host pypi.tuna.tsinghua.edu.cn
```

<details>
<summary>用 uv / poetry / conda 的写法</summary>

```bash
# uv
uv add cls-agent-observability --default-index https://pypi.tuna.tsinghua.edu.cn/simple

# poetry
poetry source add --priority=primary tuna https://pypi.tuna.tsinghua.edu.cn/simple
poetry add cls-agent-observability

# conda 环境里仍用 pip 装
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple cls-agent-observability
```

</details>

### 第 2 步：在 CLS 创建一个 Agent 可观测应用

日志主题**不用手工建** —— 它由"应用接入"流程自动创建并关联。手工建的普通
Trace 主题不属于任何应用，控制台的调用链页面是按应用组织的，会找不到入口。

1. 登录腾讯云 → 日志服务 CLS → 左侧 **Agent 可观测**
   （直达：<https://console.cloud.tencent.com/cls/agent/observe>）
2. 选一个**地域**（记住它，第 3 步的接入点要用同一个地域）
3. 点 **【应用接入】**，按页面提示创建应用
4. 在新建好的应用**右侧点【编辑】**，复制 **Trace 日志主题 ID**
   （形如 `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`）
5. 准备一对访问密钥。**建议用只有写入权限的子账号密钥**，不要用主账号密钥
   （API 密钥管理：<https://console.cloud.tencent.com/cam/capi>）

> **最容易踩的坑：`CLS_TOPIC_ID` 要填 Trace 日志主题 ID，不是应用 ID。**
> 两者形态相似（都是 uuid），但应用 ID 只用于在控制台里定位应用，**不作为配置项**。
> 填错的表现是数据写不进去，而且报错不直观 —— 这是官方 FAQ 的头号排查项。

### 第 3 步：配置 4 个环境变量

```bash
# 接入点：把 <region> 换成你的地域，如 ap-guangzhou / ap-shanghai / ap-beijing
# 在腾讯云内网（CVM/TKE）用 .cls.tencentyun.com，走内网更快且不产生公网流量费
export CLS_ENDPOINT="ap-guangzhou.cls.tencentyun.com"
# 公网环境用：ap-guangzhou.cls.tencentcs.com

export CLS_TOPIC_ID="第 2 步复制的 Trace 日志主题 ID"   # 不是应用 ID
export CLS_SECRET_ID="你的SecretId"
export CLS_SECRET_KEY="你的SecretKey"
```

> **接入点的地域必须和日志主题的地域一致**，否则会报 `topic not found`。

### 第 4 步：加一行代码

在你的应用**启动时、第一次调用 `Runner.run()` 之前**：

```python
from agents import Agent, Runner
from cls_agent_observability import setup

setup()          # ← 就这一行

agent = Agent(name="我的助手", instructions="你是一个有用的助手。")
result = await Runner.run(agent, "你好")
print(result.final_output)
```

完成。去控制台看数据：**Agent 可观测 → 点进第 2 步创建的应用 → 调用链 →
选包含测试请求的时间范围 → 打开最新一条 Trace**。数据有几十秒的索引延迟。

建议第一次验证时跑一个**带工具调用**的请求，这样能一次性看到
`entry / agent / step / chat / tool` 完整的调用树，而不只是一个 chat。

### 第 5 步（重要）：让多轮对话聚合成一个会话

上面这样每次请求都是一个独立会话。要把同一个用户的多轮对话串起来，
传一个稳定的会话 ID 进去：

```python
from agents import RunConfig, Runner

result = await Runner.run(
    agent,
    user_message,
    run_config=RunConfig(group_id=会话ID),   # ← 你自己的会话标识
)
```

`group_id` 用什么值由你决定 —— 常见做法是浏览器 session id、聊天窗口 id、
或数据库里的 conversation id。**只要同一个对话的多次请求传同一个值**，
CLS 上就会归到同一个 Session，轮次号自动 `t1 / t2 / t3` 递增。

> **请传短的稳定标识**（UUID、会话表主键等），不要把整段对话历史或序列化对象
> 当成 `group_id`。原因：session id 会写进**每一条 span** 的两个字段，超长值会
> 直接放大你的存储与流量成本；而且实测 CLS 的检索接口有约 12000 字符的查询长度
> 上限，超过后**数据虽然存进去了，却再也无法按 session 筛出来**。
>
> 本 SDK 已做兜底：超过 `max_session_id_length`（默认 512）时会截断，并拼上原值
> 的 sha256 指纹（形如 `<前缀>~9ba3fb227507`）。**指纹保证不同会话不会被合并成
> 同一个**，同一个原始值也始终映射到同一个结果（所以轮次号仍连续递增）。
> 首次发生截断时会打一条 `WARNING` 日志（只打一次，不刷屏）。

### 第 6 步（可选）：应用退出时把缓冲区刷出去

上报是批量异步的，进程直接退出可能丢掉最后几条：

```python
provider = setup()
try:
    ...  # 你的应用主体
finally:
    provider.force_flush()
    provider.shutdown()
```

FastAPI 里可以挂到 lifespan：

```python
from contextlib import asynccontextmanager
from fastapi import FastAPI

@asynccontextmanager
async def lifespan(app: FastAPI):
    provider = setup()
    yield
    provider.force_flush()
    provider.shutdown()

app = FastAPI(lifespan=lifespan)
```

---

## 工作原理

OpenAI Agents SDK 在运行过程中会产生自己的 Trace/Span（这是它内置的能力）。
本 SDK 注册一个 `TracingProcessor`，**监听**这些事件并翻译成符合 CLS Agent Trace
协议的 OpenTelemetry Span，再通过标准 OTLP/HTTP 发到你的 CLS 日志主题。

```
你的代码                Agents SDK 内部              本 SDK                     CLS
────────                ───────────────              ──────                     ───
Runner.run()  ──────►  trace 开始
                       ├─ agent span      ──────►  监听并翻译
                       ├─ turn span               （加上 session/turn/
                       ├─ generation span          token/错误分类等
                       └─ function span            协议字段）
                       trace 结束                      │
                                                       ▼
                                              OpenTelemetry Span
                                                       │
                                          BatchSpanProcessor（后台线程）
                                                       │
                                              OTLP/HTTP  ──────────────►  日志主题
```

映射关系：

| Agents SDK 的 span | CLS 协议的 span kind | span 名称 |
|---|---|---|
| trace 本身 | `entry` | `enter_application` |
| Agent 调用 | `agent` | `invoke_agent <Agent名>` |
| ReAct 轮次 | `step` | `react round_<N>` |
| 模型调用 | `chat` | `chat <模型名>` |
| 工具调用 | `tool` | `execute_tool <工具名>` |

**为什么不需要你埋点**：这些 span 是 Agents SDK 自己产生的，我们只是"翻译"，
所以你的业务代码不用做任何配合。

---

## 完整配置项

两种配置方式，**代码传参优先级高于环境变量**：

```python
from cls_agent_observability import CLSConfig, setup

setup(CLSConfig(
    endpoint="ap-guangzhou.cls.tencentyun.com",
    topic_id="...",
    secret_id="...",
    secret_key="...",
))
```

### 必填

| 配置项 | 环境变量 | 说明 |
|---|---|---|
| `endpoint` | `CLS_ENDPOINT` | CLS 接入点域名（不带 `https://`），地域需与主题一致 |
| `topic_id` | `CLS_TOPIC_ID` | 日志主题 ID |
| `secret_id` | `CLS_SECRET_ID` | 访问密钥 ID |
| `secret_key` | `CLS_SECRET_KEY` | 访问密钥 Key |

缺任何一项，`setup()` 会抛 `RuntimeError` 并明确指出缺哪个（不会静默失败）。

### 服务标识

| 配置项 | 环境变量 | 默认 | 说明 |
|---|---|---|---|
| `service_name` | `CLS_SERVICE_NAME` | `openai-agents-app` | 服务名。CLS 上是独立索引列，可直接 `service:"xxx"` 检索 |
| `host_name` | `CLS_HOST_NAME` | 本机 hostname | 主机名。K8s 上建议注入 Pod 名 |
| `deployment_environment` | `CLS_DEPLOYMENT_ENVIRONMENT` | 不上报 | 运行环境，如 `production` / `staging` |

### 正文采集（隐私与成本相关）

| 配置项 | 环境变量 | 默认 | 说明 |
|---|---|---|---|
| `content_mode` | `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` | `off` | `off` / `truncate` / `full` |
| `content_max_length` | `CLS_CONTENT_MAX_LENGTH` | `8192` | `truncate` 模式下单字段最大字符数 |
| `content_total_max_length` | `CLS_CONTENT_TOTAL_MAX_LENGTH` | `1048576` | 单条 span 全部正文合计上限 |
| `input_messages_mode` | `CLS_INPUT_MESSAGES_MODE` | `full` | `full` 自包含；`delta` 只报增量，存储从 O(n²) 降到 O(n) |

> **默认关闭正文采集**是有意的。开启后 `gen_ai.output.messages` 会包含模型的
> **思维链（reasoning）**，体积常是最终答案的数倍，且可能暴露内部推理过程。
> 关闭正文**不影响**调用链、耗时、状态和 Token 用量的上报。
>
> `truncate` 只限制长度，**不等于脱敏** —— 手机号、账号、密钥若出现在提示词或
> 工具返回值里仍会被完整上报。涉敏场景需要你自己在工具返回值和提示词层面处理。

### 用户身份

| 配置项 | 环境变量 | 说明 |
|---|---|---|
| `user_id` | `CLS_USER_ID` | 用户标识。**仅适用于 CLI / 单用户场景** |
| `user_name` | `CLS_USER_NAME` | 用户展示名，与 `user_id` 是两个独立字段 |

Web 服务里用户是每请求变化的，**不要用静态配置**，改用按请求注入（见下一节）。
两者都不设时不上报该字段 —— 本 SDK 不会用主机名之类的东西伪造用户身份。

### 行为与资源上界

| 配置项 | 环境变量 | 默认 | 说明 |
|---|---|---|---|
| `replace_existing_processors` | — | `True` | `True` 时 CLS 成为唯一上报目标；`False` 则与你已有的 processor 并存 |
| `mirror_identity_in_resource` | `CLS_MIRROR_IDENTITY_IN_RESOURCE` | `False` | 在 `resource` 内额外镜像一份服务/主机标识 |
| `max_tracked_sessions` | — | `10000` | 同时跟踪的会话数上界，超出按 LRU 淘汰 |
| `max_inflight_traces` | — | `2000` | 进行中 trace 数上界，防止异常路径下的内存泄漏 |
| `max_session_id_length` | — | `512` | `group_id` 长度上限，超出会截断并附指纹（见下）|
| `debug` | `CLS_DEBUG` | `False` | 打印内部调试日志（不含密钥与正文） |

关于 `replace_existing_processors`：默认 `True`，意味着 Agents SDK 自带的
"上报到 OpenAI 平台"会被替换掉 —— 通常这正是你想要的（数据不外流、也不会因为
没配 `OPENAI_API_KEY` 而反复打告警日志）。如果你已经接了别的可观测后端并希望
同时保留，改成 `False`：

```python
setup(CLSConfig(replace_existing_processors=False))
```

---

## 自定义字段：三种注入方式

按"这个值多久变一次"来选，选错会导致数据不对或性能浪费。

### 方式一：每请求变化的值 → `RunConfig.trace_metadata`

用户 ID、租户 ID、请求来源这类**每次请求都不同**的值：

```python
from agents import RunConfig, Runner

result = await Runner.run(
    agent,
    user_message,
    run_config=RunConfig(
        group_id=会话ID,
        trace_metadata={
            "gen_ai.user.id": "u-10086",      # 用户标识
            "gen_ai.user.name": "张三",        # 用户展示名
        },
    ),
)
```

目前支持通过 `trace_metadata` 注入的键（每个都接受多种别名，写哪个都行）：

| 协议字段 | 可用的键名 |
|---|---|
| `gen_ai.user.id` | `gen_ai.user.id` / `user_id` / `user.id` |
| `gen_ai.user.name` | `gen_ai.user.name` / `user_name` / `user.name` |

### 方式二：进程级不变的业务维度 → `extra_span_attributes`

租户、部署单元、业务线这类**整个进程生命周期都不变**的值。会加到**每一条 span** 上：

```python
setup(CLSConfig(
    extra_span_attributes={
        "agent.myapp.tenant_id": "acme-corp",
        "agent.myapp.region": "cn-south",
    },
))
```

**命名建议用 `agent.<你的产品名>.<字段>`** —— CLS Agent Trace 协议为厂商扩展保留了
`agent.*` 命名空间，这样不会和协议自身的字段冲突。

检索方式：`attribute.agent.myapp.tenant_id:"acme-corp"`

### 方式三：服务/环境级标识 → `extra_resource_attributes`

服务版本、命名空间、集群名这类描述"这个进程是谁"的值：

```python
setup(CLSConfig(
    service_name="order-agent",
    host_name="pod-order-agent-7f9c",
    deployment_environment="production",
    extra_resource_attributes={
        "service.version": "1.4.2",
        "service.namespace": "commerce",
    },
))
```

检索方式：`service:"order-agent"`、`resource.service.version:"1.4.2"`

> 一个实测结论：CLS 的 OTLP 接入层会把 `service.name` / `host.name` 提取成顶层
> 独立列（`service` / `host`），其余 resource 属性留在 `resource` JSON 内。
> 顶层列有独立索引、检索更快，所以**高频过滤的维度优先放 `service_name`**。
> 如果你的下游系统需要从 `resource` 里读到服务标识，开
> `mirror_identity_in_resource=True` 会额外镜像一份 `agent.service.name` / `agent.host.name`。

### 三种方式怎么选

| 值的变化频率 | 用哪个 | 例子 |
|---|---|---|
| 每次请求都不同 | `RunConfig.trace_metadata` | 用户 ID、请求 ID |
| 进程启动后不变，需按 span 过滤 | `extra_span_attributes` | 租户、业务线 |
| 描述服务/环境本身 | `extra_resource_attributes` | 版本、命名空间、集群 |

**不要**把每请求变化的值放进 `extra_*`（那是 `setup()` 时固定的，放进去所有请求都是同一个值）。

---

## 部署：Docker / 虚拟机 / 物理机

三种方式的差别只在**环境变量怎么传**，代码完全一样。

**通用原则：密钥必须运行时注入，不能写进代码、Dockerfile、镜像层或提交到 Git。**

### Docker

```dockerfile
FROM python:3.12-slim
WORKDIR /app

# 用国内镜像加速（构建在境内时）
RUN pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "-m", "your_app"]
```

运行时注入（**不要** `ENV CLS_SECRET_KEY=...`）：

```bash
docker run -d \
  -e CLS_ENDPOINT="ap-guangzhou.cls.tencentyun.com" \
  -e CLS_TOPIC_ID="..." \
  -e CLS_SECRET_ID="..." \
  -e CLS_SECRET_KEY="..." \
  -e CLS_SERVICE_NAME="order-agent" \
  -e CLS_DEPLOYMENT_ENVIRONMENT="production" \
  your-image:tag
```

docker-compose：

```yaml
services:
  app:
    image: your-image:tag
    env_file: .env          # .env 要加进 .gitignore
    environment:
      CLS_SERVICE_NAME: order-agent
      CLS_DEPLOYMENT_ENVIRONMENT: production
```

### Kubernetes / TKE

```yaml
env:
  - name: CLS_ENDPOINT
    value: "ap-guangzhou.cls.tencentyun.com"     # 内网接入点
  - name: CLS_TOPIC_ID
    value: "你的主题ID"
  - name: CLS_SERVICE_NAME
    value: "order-agent"
  - name: CLS_DEPLOYMENT_ENVIRONMENT
    value: "production"
  - name: CLS_HOST_NAME                          # 注入 Pod 名，便于定位副本
    valueFrom:
      fieldRef:
        fieldPath: metadata.name
  - name: CLS_SECRET_ID
    valueFrom:
      secretKeyRef: { name: cls-credentials, key: secret-id }
  - name: CLS_SECRET_KEY
    valueFrom:
      secretKeyRef: { name: cls-credentials, key: secret-key }
```

> **多副本注意**：轮次计数器保存在**进程内存**里。如果同一个会话的不同请求被
> 负载均衡到不同 Pod，Session 仍能正确聚合（因为用的是你传的 `group_id`），
> 但轮次号可能不连续。需要严格连续的轮次号时，请在网关层做会话亲和性
> （sticky session），或自行在应用侧管理轮次并通过 `group_id` 编码。

### 虚拟机 / 物理机（systemd）

```ini
# /etc/systemd/system/my-agent.service
[Unit]
Description=My Agent
After=network-online.target

[Service]
User=appuser
WorkingDirectory=/opt/my-agent
# 密钥放独立文件，权限设为 600，只有服务账号可读
EnvironmentFile=/etc/my-agent/cls.env
ExecStart=/opt/my-agent/.venv/bin/python -m your_app
Restart=always

[Install]
WantedBy=multi-user.target
```

```bash
# /etc/my-agent/cls.env  （chmod 600）
CLS_ENDPOINT=ap-guangzhou.cls.tencentyun.com
CLS_TOPIC_ID=...
CLS_SECRET_ID=...
CLS_SECRET_KEY=...
CLS_SERVICE_NAME=order-agent
CLS_DEPLOYMENT_ENVIRONMENT=production
```

```bash
sudo chmod 600 /etc/my-agent/cls.env
sudo systemctl daemon-reload && sudo systemctl enable --now my-agent
```

裸机上临时跑（开发调试）也可以用 `.env` 文件 + `python-dotenv`：

```python
from dotenv import load_dotenv
load_dotenv()            # 必须在 setup() 之前
from cls_agent_observability import setup
setup()
```

> **注意**：如果你的应用用 **pydantic-settings** 读 `.env`，它**不会**把值写进
> `os.environ`，本 SDK 的环境变量回落读不到。这种情况请把配置项声明进你自己的
> Settings，然后显式传给 `CLSConfig(...)`。

---

## 对你的应用有多大影响

可观测组件不该成为新的故障点。这里是实测数据和具体做法。

### 性能

实测（`scripts/benchmark_overhead.py`，每请求约 11 个 span，含 3 轮 ReAct 与工具调用）：

| 场景 | 每请求净开销 | 折算每 span |
|---|---|---|
| `content_mode=off`（生产默认） | ≈ 295 μs | ≈ 27 μs |
| `content_mode=truncate` | ≈ 372 μs | ≈ 34 μs |

真实 Agent 请求含模型调用，端到端通常在 1000 ms 量级，**转换层占比约 0.03%**。

### 不阻塞请求路径

上报由 OpenTelemetry 的 `BatchSpanProcessor` 在**后台线程**批量完成。
CLS 不可用、网络抖动、鉴权失败都不会阻塞你的业务请求 —— 最坏情况是这段观测数据丢失。

### 异常隔离

`TracingProcessor` 的四个生命周期入口全部包了异常隔离层：内部出错只记一条
`warning` 日志（含堆栈，不含正文与密钥），**不会把异常抛进你的 Agent 执行流**。

对上游 span 类型采用**鸭子类型分发**，运行期不 import Agents SDK 的内部类 ——
上游重命名或移除某个类型时，你的应用不会在启动时崩掉。

### 内存有界

内部状态全部有上界，不会随运行时长无限增长：

- 会话状态（轮次计数等）上界 `max_tracked_sessions`（默认 10000），超出按 LRU 淘汰
- 进行中的 trace 上界 `max_inflight_traces`（默认 2000），防止异常路径下泄漏

实测回放 1000 个请求后转换层常驻 **28 KiB**，进行中 trace 残留 **0** 条。

### 配置校验

`setup()` 时校验必填项，缺失直接抛 `RuntimeError` 并指出缺哪一项 ——
宁可启动时明确失败，也不要上线后才发现一直没数据。

---

## 兼容性

### Python

**3.10 / 3.11 / 3.12 / 3.13** 均实测通过（`scripts/check_python_compat.py`）。

### openai-agents

依赖声明 `>=0.15,<1.0`。实测通过的版本：**0.15.0 / 0.17.0 / 0.18.0 / 0.19.0 / 0.20.0 / 0.21.0 / 0.22.0**。

由于对上游 span 类型使用鸭子类型分发、不在运行期 import 其内部类，
小版本升级通常无需等本 SDK 跟进。

### 与其他可观测方案共存

已经接了别的 OpenTelemetry 后端时，两种做法：

```python
# 做法一：并存（保留你已有的 processor）
setup(CLSConfig(replace_existing_processors=False))

# 做法二：复用你已有的 TracerProvider（这样 Resource、采样器等配置统一）
setup(CLSConfig(...), provider=your_existing_tracer_provider)
```

---

## 排障

### 控制台看不到数据

按顺序排查：

1. **`setup()` 是否在第一次 `Runner.run()` 之前调用**？之前产生的 trace 不会补采。
2. **`CLS_TOPIC_ID` 是不是误填成了应用 ID**？要填的是应用【编辑】里的
   **Trace 日志主题 ID**。两者都是 uuid，最容易搞混，这是头号原因。
3. **地域是否匹配**？`CLS_ENDPOINT` 的地域必须与日志主题的地域一致，否则报 `topic not found`。
4. **内网/外网域名选对了吗**？腾讯云内网用 `.cls.tencentyun.com`，公网环境用
   `.cls.tencentcs.com`。选错会连不上。
5. **等够时间了吗**？CLS 索引有几十秒延迟。
6. **是否在应用内的调用链页面查看**？普通日志检索页面看的是原始日志，
   调用链视图在 **Agent 可观测 → 进入应用 → 调用链**。
7. **进程是否过早退出**？短脚本请在结束前 `provider.force_flush()`。
8. **开 debug 看日志**：`CLS_DEBUG=true`，或

   ```python
   import logging
   logging.getLogger("cls_agent_observability").setLevel(logging.DEBUG)
   ```

9. **子账号权限**：需要 `cls:PushLog`（写入）权限。401/403 通常是密钥或权限问题。

### 多轮对话没聚合成一个 Session

没传 `RunConfig(group_id=...)`。不传时每次请求都是独立会话（这是与 Agents SDK
一致的兜底行为）。见[第 5 步](#第-5-步重要让多轮对话聚合成一个会话)。

### 看不到输入输出消息

正文采集默认关闭。设 `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=truncate`。
请先读[正文采集](#正文采集隐私与成本相关)一节的隐私提示。

### Token 或模型名缺失

这些值来自模型服务的返回结果。部分 OpenAI 兼容服务不返回 `usage` 或响应模型名，
此时无法凭空推算。请确认你的模型服务是否返回了这些字段。

### 出现重复的调用链

检查是否同时启用了多个上报路径（例如既调了 `setup()` 又自己注册了
`TracingProcessor`）。对照验证之外，建议同一个 Agent 实例只保留一条上报路径。

### 日志里出现 "OPENAI_API_KEY is not set, skipping trace export"

这是 Agents SDK 自带的"上报到 OpenAI 平台"processor 在报警，与本 SDK 无关。
把 `replace_existing_processors` 保持默认 `True` 即可（CLS 成为唯一上报目标）。

---

## 许可证

MIT
