# 可观测性是产品功能，不是运维工具

> **📖 与 Part 3 的关系**：Part 3 第 8 章"遥测与可观测性完全解析"和第 17 章"遥测与分析系统完全解析"详细剖析了遥测系统的技术实现——五条数据出站通道的架构、8 个 OTel Counter、Span 层级设计、PII 脱敏机制等。本章不重复这些"是什么"和"怎么做"的问题，而是聚焦于**可观测性作为产品设计哲学的工程意义**：为什么 Claude Code 的工程团队选择把可观测性从"运维附加项"提升为"产品核心功能"？这种选择带来了什么收益，又付出了什么代价？

在大多数软件系统里，可观测性（日志、指标、追踪）是"运维的事"——开发者写功能，运维团队加监控。Claude Code 的代码库展示了一种不同的思路：**可观测性是产品设计的一部分，直接影响用户体验**。

> 💡 **通俗理解**：可观测性就像**汽车仪表盘**——不是给修车师傅看的维修手册，而是给驾驶员看的实时信息。你需要知道当前油量（token 用量）、车速（响应延迟）、发动机温度（缓存命中率）、导航状态（AI 正在做什么）。没有仪表盘，你只能"开盲车"。

> 🌍 **行业背景**："可观测性即产品功能"的理念并非 Claude Code 首创——**Google 的 Dapper**（2010 年）和由此衍生的 **OpenTelemetry** 标准已经将分布式追踪从"运维附加项"提升为"系统设计的基础设施"。Google SRE 一书（2016）系统性论述了可观测性在系统设计中的核心地位，Charity Majors（Honeycomb 创始人）在 2018 年前后进一步将"Observability-First Design"理念推广到更广泛的工程社区。在产品可观测性方面，**Stripe** 是公认的标杆——其 Dashboard 将 API 调用链路、错误率、延迟分布直接呈现给开发者用户，把可观测性变成了产品的核心卖点。在 AI 编码助手中，**Cursor** 的界面会显示"正在思考..."和 token 消耗计数；**Aider** 提供 `--verbose` 模式输出 API 调用详情；**LangSmith**（LangChain 的观测平台）专门解决 LLM 应用的调试和追踪问题，提供完整的 trace 可视化、成本归因、prompt 版本管理和评估反馈。
>
> Claude Code 的实践需要在行业背景中客观定位：**双层遥测分离**（OTel 面向基础设施 + logEvent 面向产品分析）是 2020 年以后大多数 B2B SaaS 产品的标准架构——任何有一定规模的 SaaS 都会分离基础设施监控（Datadog/New Relic/Grafana）和产品分析（Amplitude/Mixpanel/Segment），这是行业基线，不是创新。**将观测数据反馈到运行时决策**（如根据缓存状态动态抑制投机执行）也是 feature flags + A/B testing 的变体——LaunchDarkly、Split.io 在 2018 年就把这套做法产品化了。Claude Code 真正值得关注的独特之处在于：(1) **分类器决策追踪的闭环改进**——在 AI 编码助手中对权限分类器做细粒度线上追踪并反馈到模型迭代，这在同类工具中确实少见；(2) **Hook 事件的 SDK 可订阅性**——把可观测性作为 API 暴露给第三方生态；(3) **"鲸鱼会话"级别的异常检测成熟度**——对极端用例形成内部分类术语和系统化追踪流程。

---

## 证据 1：每个 hook 执行都有三个事件

当一个 hook（钩子——用户预设的自动化脚本，会在特定时机自动运行，详见 Part 3 第 4 章）运行时，系统会产生三种事件，就像快递的三条通知：

```typescript
emitHookStarted(hookId, hookName, hookEvent)    // "快递已出发"
emitHookProgress(...)                            // "快递正在配送中"（每秒更新一次）
emitHookResponse(...)                            // "快递已签收"（含运行结果和错误信息）
```

这不是"调试日志"。这些事件可以被 SDK 订阅（`includeHookEvents` 选项），让集成方实时知道正在发生什么。当 hook 执行时间超过几秒，用户需要看到"还在跑"的反馈——不然他们不知道 Claude 是在工作还是卡死了。

可观测性 = 用户体验。

---

## 证据 2：每个权限决策都被追踪

auto 模式的分类器每次运行都记录：

```typescript
logEvent('tengu_yolo_classifier_result', {
  toolName,
  decision,      // allow/deny
  durationMs,
  reason,
})
```

这不只是为了监控系统健康——这是为了改进分类器本身。通过追踪哪些操作被拒绝、拒绝原因是什么，工程师可以发现"分类器过于保守"或"分类器放行了不该放行的操作"的模式，然后调整训练数据或分类规则。

**可观测性 = 数据驱动的产品改进**。

> 📚 **课程关联**：证据 2 中"追踪分类器决策 → 发现模式 → 调整规则"的闭环，对应**软件工程**课程中的 **PDCA（Plan-Do-Check-Act）** 持续改进循环，也是**机器学习系统**课程中 **MLOps 监控反馈回路**的典型案例——模型的线上表现数据被收集并用于改进下一版模型。

---

## 证据 3：性能指标决定了功能的存废

Prompt Suggestion（预测用户下一条消息）有这样的抑制逻辑：

```typescript
// promptSuggestion.ts
const MAX_PARENT_UNCACHED_TOKENS = 10_000

export function getParentCacheSuppressReason(
  lastAssistantMessage: ...
): string | null {
  const usage = lastAssistantMessage.message.usage
  const inputTokens = usage.input_tokens ?? 0
  const cacheWriteTokens = usage.cache_creation_input_tokens ?? 0
  const outputTokens = usage.output_tokens ?? 0

  // 如果输入+缓存写入+输出的总 token 超过 10,000，返回 'cache_cold'
  return inputTokens + cacheWriteTokens + outputTokens > MAX_PARENT_UNCACHED_TOKENS
    ? 'cache_cold'
    : null
}

// 调用处：
const cacheReason = getParentCacheSuppressReason(lastAssistantMessage)
if (cacheReason) {               // cacheReason === 'cache_cold'
  logSuggestionSuppressed(cacheReason, ...)
  return null                     // 不生成预测
}
```

这个 `MAX_PARENT_UNCACHED_TOKENS = 10_000` 的阈值不是拍脑袋决定的。它来自生产数据：通过追踪预测命中率 vs 父请求的缓存状态，工程师发现了"缓存冷启动时预测没有意义"的规律，然后把这个规律编码成运行时决策。值得注意的是，这个阈值是硬编码常量——当模型升级（上下文窗口变大、缓存策略变化）后可能需要重新校准。源码中没有记录这个数值的推导过程（比如是通过线性回归发现的拐点，还是散点图人工判断的），它本质上是一个带注释的 magic number（"魔术数字"——程序员对"凭空出现的、没有解释来源的硬编码数值"的戏称），背后的"数据驱动"过程对读者来说是不可见的。

**可观测性 = 运行时的产品决策**。但"数据驱动"的实际程度取决于阈值校准的过程是否有文档化——这也是可观测性文化的一部分。

---

## 证据 4：WebhookHook 的调试日志

```typescript
// hooks.ts emitHookResponse()
// Always log full hook output to debug log for verbose mode debugging
const outputToLog = data.stdout || data.stderr || data.output
if (outputToLog) {
  logForDebugging(
    `Hook ${data.hookName} (${data.hookEvent}) ${data.outcome}:\n${outputToLog}`,
  )
}
```

注意"Always"——不管 hook 是否成功，不管是什么类型的事件，完整的 stdout/stderr 都会进入调试日志（通过 `claude --debug` 可见）。这是面向用户的可观测性：当你的 hook 行为异常时，你有工具去排查。

---

## 证据 5：鲸鱼会话（Whale Session）—— 可观测性驱动产品改进的完整案例

这是全章最有力的证据，因为它展示了从"发现异常"到"定位根因"到"代码修复"的完整闭环。

### 问题如何被发现

源码中的 BQ（BigQuery——Google 的大数据分析工具，可以对海量日志做即时查询）分析注释记录了完整的调查链路。下面这段英文注释是开发团队留下的"破案笔记"，记录了他们如何发现并定位一个严重的内存问题：

```typescript
// types.ts — InProcessTeammateTask
/**
 * BQ analysis (round 9, 2026-03-20) showed ~20MB RSS per agent at 500+ turn
 * sessions and ~125MB per concurrent agent in swarm bursts. Whale session
 * 9a990de8 launched 292 agents in 2 minutes and reached 36.8GB. The dominant
 * cost is this array holding a second full copy of every message.
 */
export const TEAMMATE_MESSAGES_UI_CAP = 50
```

"round 9"说明这不是一次性的查询，而是团队的第九轮系统性 BQ 分析——这意味着存在一个定期的、有编号的数据审查流程。

### 定位到具体 session

工程团队没有停留在"平均内存偏高"的统计结论上。通过 BigQuery 中按 session 分组的 RSS 内存分布，他们发现了极端异常值——session `9a990de8`。这个 session 的行为特征非常明确：**2 分钟内启动了 292 个 agent**，总 RSS（常驻内存——程序实际占用的内存大小）达到 36.8GB（一台普通笔记本电脑的总内存通常只有 8-16GB，这个单个会话就占满了两台电脑的内存）。团队给这类极端会话起了一个内部术语——**"鲸鱼会话"（whale session）**。命名本身就说明异常检测已经成熟到形成了内部分类体系：不是"某个用户报了个 bug"，而是"我们有一个标准类别叫 whale session，定义是……"。

### 根因分析

定位到 session 后，追踪到内存的主要消耗者：`task.messages` 数组。这个数组的本意是为 UI 层的"放大查看 transcript"对话框提供数据，但它保存了**每条消息的完整副本**。在正常会话中这不是问题，但当 swarm 模式批量启动数百个 agent 时，每个 agent 的 `task.messages` 都独立持有全量消息的拷贝，内存呈线性爆炸。

### 修复方案

修复策略也体现了可观测性数据指导的精确性——不是粗暴地"减少 agent 数量限制"，而是针对根因实施了两个精确干预：

1. **对 `task.messages` 加上 UI cap**（`TEAMMATE_MESSAGES_UI_CAP = 50`），只保留最近 50 条消息供 UI 显示，完整对话仍存在于本地磁盘的 transcript 文件中。
2. **在 agent 清理时主动释放内存**（`runAgent.ts`）——清除 clone 的 fork context、文件状态缓存、Perfetto 注册项、transcript 目录映射、以及孤立的 todo 条目：

```typescript
// runAgent.ts — agent 清理逻辑
agentToolUseContext.readFileState.clear()   // 释放文件状态缓存
initialMessages.length = 0                   // 释放 fork 上下文消息
unregisterPerfettoAgent(agentId)            // 释放 Perfetto 注册
clearAgentTranscriptSubdir(agentId)         // 释放 transcript 映射
// 清理孤立的 todos 条目——whale sessions 会 spawn 数百个 agent，
// 每个孤立 key 都是积少成多的小泄漏
```

### 为什么这个案例最有说服力

这个完整链路——**定期 BQ 分析 → 异常 session 定位 → 内存归因 → 精确修复**——是"可观测性驱动产品改进"最生动的实证。它说明了三件事：

1. **可观测性必须是持续的、有组织的**："round 9"意味着这不是偶发的调查，而是系统化的数据审查节奏
2. **粗粒度告警不够，需要 session 级别的追溯能力**：如果只有"平均 RSS"这个指标，36.8GB 的鲸鱼会话会被均值稀释而隐匿
3. **修复方案的精确度取决于观测数据的精确度**：因为能追踪到"是 `task.messages` 的全量副本"这个具体的内存消耗者，修复才能是精确的 cap 而不是粗暴的功能限制

没有可观测性，这个问题可能只是"某些用户反映 Claude Code 占用大量内存"，然后被归因为"AI 就是吃内存"而搁置。有了可观测性，它变成了"session 9a990de8 在 2 分钟内启动 292 个 agent，`task.messages` 数组是主要内存消耗者"——一个可以精确修复的工程问题。

---

## 两层 telemetry 的分离（行业标准实践）

> **定位说明**：双层遥测分离是 2020 年以后 B2B SaaS 产品的行业基线，不是 Claude Code 的独创。任何有一定规模的 SaaS 都会分离基础设施监控（Datadog/New Relic/Grafana）和产品分析（Amplitude/Mixpanel/Segment）。本节记录 Claude Code 对这一标准实践的具体落地方式，重点不在"做了什么"，而在"做的过程中有哪些值得注意的工程细节"。关于五条数据通道的完整技术架构，参见 Part 3 第 8 章。

Claude Code 有两种 telemetry，服务不同的目的：

**OpenTelemetry（OTel）— 面向基础设施**

```typescript
// telemetry/sessionTracing.ts
startHookSpan(), endHookSpan()
startInteractionSpan(), endInteractionSpan()
```

Spans 和 Traces，用于追踪请求链路、性能瓶颈、错误率。这是经典的 APM（应用性能监控）用途。

**logEvent — 面向产品分析**

```typescript
logEvent('tengu_conversation_forked', {
  message_count: serializedMessages.length,
  has_custom_title: !!title,
})
```

产品事件，用于追踪功能使用情况、A/B 测试、功能开关的效果。设计上避免记录原始用户内容，只包含结构化的统计信息——但需要注意，结构化字段可能间接包含上下文线索（例如 `toolName: 'bash'` + `reason: 'contains rm -rf'` 可以推断用户意图），"不包含 PII"的说法需要加限定条件。

两层分开，各自服务不同的 consumer——基础设施团队看 OTel，产品团队看 logEvent。

**值得关注的工程细节**：这两套系统各自独立记录，这引出一个关联性问题——如果某个请求在 OTel trace 中显示成功但 logEvent 记录了异常，如何关联？两套系统之间是否共享 correlation ID？这种跨层关联的工程复杂度是双层分离的隐性代价（详见下文"代价与反思"一节）。

---

## 设计原则：把可观测性视为产品的一部分

这种做法在软件工程中被称为 **"Observability-First Design"**（可观测性优先设计）。其理念根基可追溯到 Google SRE 一书（2016）对可观测性在系统设计中核心地位的系统论述，Charity Majors（Honeycomb 创始人）在 2018 年前后将这一理念进一步推广到更广泛的工程社区。核心主张是：可观测性不是功能完成后的附加项，而应该在架构设计阶段就规划好。Claude Code 的实践与这一理念一致——但需要区分哪些是标准落地、哪些是独到做法：

1. **功能设计时就包含 emit 点**（行业标准实践）：hook 系统、权限系统、speculation 都在核心逻辑里有 emit，不是事后加的。这是 Observability-First Design 的基本要求
2. **数据驱动产品决策**（行业标准实践）：预测抑制阈值、缓存命中率监控——这些运行时行为来自对数据的分析。本质上是 feature flags + 数据反馈的标准模式
3. **面向用户的可观测性 + SDK 可订阅**（在 AI 编码助手中少见）：`--debug` 模式、hook 执行事件流（通过 `includeHookEvents` 选项可被第三方 SDK 订阅）——用户和集成方都可以看到系统在做什么。这是从"产品功能"到"平台能力"的跃迁
4. **分类器决策追踪闭环**（在 AI 编码助手中少见）：权限分类器的线上行为追踪 → 模式发现 → 训练数据/规则调整，是一个嵌入产品核心流程的 MLOps 反馈回路。Cursor 的权限模型是静态规则，Aider 没有自动权限分类——这个闭环是 Claude Code 的真正差异化能力
5. **区分用途**（行业基线）：OTel 给基础设施，logEvent 给产品分析。参见上文"两层 telemetry 的分离"一节的定位说明

> 📚 **课程关联**：双层遥测架构（OTel + logEvent）对应**分布式系统**课程中的**关注点分离（Separation of Concerns）**原则。OTel 的 Span/Trace 模型是分布式追踪的工业标准，理解它需要**计算机网络**课程中的请求链路追踪知识；而 logEvent 的结构化事件设计则属于**数据库系统**课程中事件日志（Event Sourcing）模式的应用。

---

## 对你的系统的启示

当你在构建 AI 应用时，问自己：

- 当 AI 行为异常时，我有工具来排查吗？
- 我怎么知道某个优化（比如 prompt cache）是否真的生效了？
- 我能追踪到"是哪个特定会话触发了这个 bug"吗？
- 用户能知道 AI 现在在做什么（长时间运行时）？

这些问题的答案取决于你在设计功能时，是否把可观测性当作功能的一部分——而不是留到"上线后再说"。

---

## 代码落点

- `src/services/analytics/index.ts`：`logEvent()` / `logEventAsync()` 函数——产品分析级别的结构化日志（详见 Part 3 第 17 章）
- `src/utils/telemetry/instrumentation.ts`：OTel 初始化与 Counter——基础设施级别的分布式追踪（详见 Part 3 第 8 章）
- `src/utils/telemetry/sessionTracing.ts`：Span 层级管理——`startHookSpan` / `startInteractionSpan` 等
- `src/services/PromptSuggestion/promptSuggestion.ts`：`getParentCacheSuppressReason()` + `MAX_PARENT_UNCACHED_TOKENS` 阈值
- `src/tasks/InProcessTeammateTask/types.ts`：鲸鱼会话 BQ 分析注释 + `TEAMMATE_MESSAGES_UI_CAP`
- `src/tools/AgentTool/runAgent.ts`：Agent 内存清理逻辑（whale session 修复）
- `src/utils/hooks/hookEvents.ts`：Hook 事件发射——`emitHookStarted`/`emitHookProgress`/`emitHookResponse` 三事件模式（注意：函数名是 `emitHookResponse` 而非 `emitHookCompleted`，语义是"返回了响应数据"而非"执行结束"，因为该事件携带了完整的 stdout/stderr/exitCode/outcome）

## 代价与反思

一篇讨论工程哲学的文章如果只谈收益不谈代价，就不是哲学而是广告。"可观测性内嵌"的模式在 Claude Code 中展现了显著价值（尤其是鲸鱼会话案例），但它的代价同样值得系统性审视。

### 1. 遥测代码的维护税

Part 3 第 8 章的分析显示，Claude Code 整个代码库有**超过 7,400 行遥测相关代码**，分布在 `src/utils/telemetry/`（4 个核心文件，3,363 行）和 `src/services/analytics/`（9 个文件，4,040 行）。这意味着遥测代码本身就是一个中等规模的子系统，需要独立的测试、维护和演进。

每个 emit 点都是一个契约——事件名称、字段结构、语义含义构成了 producer（业务代码）和 consumer（分析管线、dashboard、告警规则）之间的接口。如果事件格式变了（比如给 `tengu_yolo_classifier_result` 加一个新字段或改变 `reason` 的枚举值），所有下游消费者都需要更新。在五条数据出站通道的架构下，一个事件格式变更可能需要同步更新 BigQuery schema、Datadog dashboard、1P 日志解析器等多个下游系统——这是典型的"分布式 schema 演进"问题。

### 2. 隐私与可调试性的张力

本章证据 2 中 `logEvent` 记录的 `toolName` 和 `reason` 字段在设计上不包含原始用户内容，但结构化字段可能间接泄露用户意图。例如 `toolName: 'bash'` + `reason: 'contains rm -rf'` 的组合可以推断用户正在执行危险操作。更准确的说法是"设计上避免记录原始用户内容（PII），但结构化字段可能包含上下文线索"。

这个设计选择创造了一个两难：当分类器误判时，工程师只有 `toolName` 和 `reason` 而没有用户的原始 prompt，如何复现问题？如果为了可调试性而增加记录粒度，就会触碰隐私红线；如果严格限制记录内容，某些 bug 就只能靠用户主动提交复现步骤才能定位。Claude Code 选择了偏向隐私保护的一端——这是一个合理但有代价的取舍，尤其是在 AI Agent 分类器这种"决策正确性直接影响用户安全"的场景中。

### 3. 双层系统的一致性挑战

OTel 和 logEvent 各自独立记录事件。这种分离带来了关注点隔离的好处，但也引入了跨层关联的工程复杂度：如果某个请求在 OTel 的 trace 中显示成功（span 状态为 OK），但 logEvent 记录了该请求的分类器异常——运维团队和产品团队看到的是同一次请求的两种不同"真相"。

关键问题是：两套系统之间是否共享 correlation ID？从源码来看，OTel 使用标准的 trace ID / span ID 体系，logEvent 使用 session ID + 事件时间戳体系，两者之间的关联需要通过时间窗口和 session 粒度来推断，而非精确的 ID 匹配。这在日常场景下尚可工作，但在高并发的 swarm 场景（如鲸鱼会话中 2 分钟启动 292 个 agent）中，基于时间窗口的关联可能出现歧义。

### 4. "宁可多记"策略的长期可持续性

证据 4 中"Always log full hook output"的设计决策体现了"宁可多记也不少记"的原则——不管 hook 是否成功、不管什么类型的事件，完整的 stdout/stderr 都进入调试日志。这和数据膨胀风险之间形成了张力。

在单用户本地运行的场景下，调试日志的存储成本几乎可以忽略。但 logEvent 发送到服务端的产品事件就不同了——每个 emit 点在百万级 DAU 规模下乘以五条数据出站通道，存储和传输成本是非线性增长的。源码中 `datadog.ts` 的白名单过滤和 `sink.ts` 的采样机制说明团队已经在做数据量控制，但这本身就是"宁可多记"策略的维护成本——你需要持续维护采样规则和白名单，确保既不漏掉关键信号、又不淹没在噪声中。

### 5. `--debug` 模式的安全边界

面向用户的 `--debug` 模式是可观测性的重要用户界面，但它也暴露了内部实现细节——函数调用链、缓存状态、hook 执行流程等。对于善意用户来说这是强大的排查工具，但对于攻击者来说这也是反向工程的线索来源。

更关键的问题是：`--debug` 的输出有没有做信息分级？是否区分了"用户级调试信息"（如"hook X 执行超时"）和"内部实现细节"（如 OTel span 的内部结构、遥测管线的路由逻辑）？从源码来看，`logForDebugging` 函数是一个统一的输出口，没有明显的分级机制。这意味着 `--debug` 是一个"全有或全无"的开关，可能在帮助用户排查问题的同时暴露了不必要的内部信息。

### 权衡总结

| 维度 | 收益 | 代价 |
|------|------|------|
| 维护 | 早期发现问题（如鲸鱼会话） | 7,400+ 行遥测代码的持续维护、schema 演进协调 |
| 隐私 | 结构化统计不含原始用户内容 | 间接上下文泄露风险 + 分类器误判时缺乏复现数据 |
| 一致性 | 两层各司其职、关注点分离 | 跨层关联依赖时间窗口推断，高并发下可能歧义 |
| 存储 | 完整日志支持深度排查 | 五条通道 × 百万 DAU = 非线性存储增长 |
| 安全 | 用户有工具排查问题 | `--debug` 无分级，可能暴露过多内部实现 |

一个成熟的工程哲学需要展示对自身代价的清醒认知。Claude Code 的可观测性系统在"做了什么"这个维度上是优秀的——鲸鱼会话案例证明了投资回报。但在"代价管理"维度上仍有改进空间，特别是跨层关联、隐私-可调试性平衡、以及 debug 信息分级这三个方面。
