Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/cli/src/services/BuiltinCommandLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { authCommand } from '../ui/commands/authCommand.js';
import { bugCommand } from '../ui/commands/bugCommand.js';
import { clearCommand } from '../ui/commands/clearCommand.js';
import { compressCommand } from '../ui/commands/compressCommand.js';
import { contextCommand } from '../ui/commands/contextCommand.js';
import { copyCommand } from '../ui/commands/copyCommand.js';
import { docsCommand } from '../ui/commands/docsCommand.js';
import { directoryCommand } from '../ui/commands/directoryCommand.js';
Expand Down Expand Up @@ -64,6 +65,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
bugCommand,
clearCommand,
compressCommand,
contextCommand,
copyCommand,
docsCommand,
directoryCommand,
Expand Down
293 changes: 293 additions & 0 deletions packages/cli/src/ui/commands/CONTEXT_COMMAND.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
# `/context` 命令 — 上下文窗口用量分解

## 概述

`/context` 命令展示当前模型上下文窗口的 token 使用情况。它将整个上下文窗口拆分为多个分类,帮助用户理解 token 花在了哪里,以及还剩多少空间。

## 上下文窗口的组成

一次 API 请求发送给模型的完整 prompt 包含以下部分:

```
┌─────────────────────────────────────────────┐
│ Context Window (总容量) │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ System Prompt (系统提示词) │ │
│ │ └─ 核心指令 + 行为规则 │ │
│ ├─────────────────────────────────────┤ │
│ │ Tool Declarations (工具声明) │ │
│ │ ├─ Built-in tools (内置工具) │ │
│ │ ├─ MCP tools (MCP 工具) │ │
│ │ └─ SkillTool (技能工具) ◄──────────┼─── 包含所有 skill 的名称+描述
│ ├─────────────────────────────────────┤ │
│ │ Memory (用户记忆) │ │
│ │ └─ QWEN.md + extension configs │ │
│ ├─────────────────────────────────────┤ │
│ │ Messages (对话消息) │ │
│ │ ├─ 用户消息 │ │
│ │ ├─ 模型回复 │ │
│ │ └─ 工具调用 & 工具结果 ◄───────────┼─── skill body 在此加载
│ ├─────────────────────────────────────┤ │
│ │ Free Space (可用空间) │ │
│ ├─────────────────────────────────────┤ │
│ │ Autocompact Buffer (自动压缩缓冲) │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
```

**不变量**:所有分类之和 = Context Window 总容量。

## 各分类详解

### 1. System Prompt(系统提示词)

| 属性 | 说明 |
| ------------ | ------------------------------------------------------------------ |
| **数据来源** | `getCoreSystemPrompt(undefined, modelName)` |
| **包含内容** | 模型的核心行为指令、输出格式要求、安全规则等 |
| **不包含** | Memory 内容(单独计算) |
| **计算方式** | 对系统提示词文本调用 `estimateTokens()` |
| **变化频率** | 基本固定,除非修改了 `QWEN_SYSTEM_MD` 环境变量或 `.qwen/system.md` |

> **注意**:`getCoreSystemPrompt` 接受 `userMemory` 参数,这里传入 `undefined` 以排除 memory,因为 memory 作为独立分类统计。

### 2. Built-in Tools(内置工具)

| 属性 | 说明 |
| ------------ | ----------------------------------------------------------------------------------------------------- |
| **数据来源** | `toolRegistry.getAllTools()` 中非 MCP、非 SkillTool 的工具 |
| **包含内容** | `read_file`、`edit`、`run_shell_command`、`grep_search`、`glob`、`list_directory` 等核心工具的 schema |
| **计算方式** | `allToolsTokens - skillsTokens - mcpToolsTotalTokens` |
| **详情列表** | 逐项展示每个内置工具的名称和 token 占用,按 token 数降序排列 |

> **SkillTool** 虽然也是内置工具,但因其内容动态性(嵌入所有 skill 列表),独立作为 **Skills** 分类展示,不在 Built-in tools 中出现。

### 2b. MCP Tools(MCP 工具)

| 属性 | 说明 |
| ------------ | ----------------------------------------------------------------------- |
| **数据来源** | `toolRegistry.getAllTools()` 中 `DiscoveredMCPTool` 实例 |
| **包含内容** | 通过 MCP 协议连接的外部工具服务器提供的工具 schema |
| **计算方式** | 各 MCP 工具 `estimateTokens(JSON.stringify(tool.schema))` 之和 |
| **详情列表** | 逐项展示每个 MCP 工具的名称(`serverName__toolName` 格式)和 token 占用 |
| **条件显示** | 仅当存在 MCP 工具时才显示此分类行和详情 |

### 3. Skills(技能)⭐ 渐进式披露

Skills 采用**两阶段加载**设计:

| 阶段 | 加载内容 | Token 归属 | 何时加载 |
| ------------ | ---------------------------------------------- | ----------------- | ------------------------------- |
| **第一阶段** | 每个 skill 的 name + 短 description + 使用说明 | **Skills 分类** | 每次 API 请求都发送 |
| **第二阶段** | 完整的 SKILL.md body 内容(详细指令、模板等) | **Messages 分类** | 模型调用 `skill` 工具后按需注入 |

**`/context` 中 Skills 分类展示的是第一阶段的常驻开销。**

#### 第一阶段的实现细节

SkillTool 在初始化时将所有 skill 信息嵌入其 `description` 字段:

```
Execute a skill within the main conversation

<skills_instructions>
... 使用说明(~600 字符)...
</skills_instructions>

<available_skills>
<skill>
<name>pdf</name>
<description>Convert PDF files to text (project)</description>
<location>project</location>
</skill>
<skill>
<name>xlsx</name>
<description>Process Excel spreadsheets (user)</description>
<location>user</location>
</skill>
...更多 skills...
</available_skills>
```

这整块文本是 SkillTool 的 tool declaration 的一部分,每次 API 请求都会发送。

#### Token 计算方式

```
skillsTokens = estimateTokens(JSON.stringify(skillTool.schema))
```

直接从 ToolRegistry 中获取 SkillTool 的完整 schema 进行估算,确保包含:

- 使用说明文本(`<skills_instructions>`)
- 所有 skill 的 XML 列表(`<available_skills>`)
- schema 参数定义

#### 第二阶段(按需加载)

当模型调用 `skill` 工具时,`SkillToolInvocation.execute()` 会加载完整的 SKILL.md:

```typescript
const skill = await this.skillManager.loadSkillForRuntime(this.params.skill);
const llmContent = `Base directory: ${baseDir}\n\n${skill.body}\n`;
```

这个 body 内容作为工具调用结果注入到对话中,token 开销归入 **Messages** 分类。

#### Skills 详情列表

每个 skill 的详情行展示该 skill 在第一阶段中的大致占用,按 token 数降序排列。注意:

- 各 skill 详情的 token 之和 **< Skills 分类总数**,差值是 skills_instructions 指令文本的开销
- 详情仅展示名称和描述的 token,不包含 schema 参数定义部分

### 4. Memory Files(用户记忆)

| 属性 | 说明 |
| ------------ | -------------------------------------------------------------------------- |
| **数据来源** | `config.getUserMemory()` |
| **包含内容** | `QWEN.md`、extension 配置、`output-language` 等用户级配置文件 |
| **加载位置** | 拼接到 System Prompt 末尾(通过 `getCoreSystemPrompt(userMemory, model)`) |
| **计算方式** | 解析 memory 文本中的 `--- Context from: <path> ---` 标记,分文件估算 token |

**Memory 内容格式**:

```
--- Context from: ~/.qwen/QWEN.md ---
用户自定义规则和偏好...
--- End of Context from: ~/.qwen/QWEN.md ---
--- Context from: ~/.qwen/extensions/config.md ---
扩展配置内容...
--- End of Context from: ~/.qwen/extensions/config.md ---
```

> **为什么 System Prompt 不包含 Memory?** 计算 System Prompt token 时传入 `userMemory = undefined`,Memory 作为独立分类展示,避免两个分类重叠。实际 API 请求中 memory 是拼接在 system prompt 末尾的。

### 5. Messages(对话消息)

| 属性 | 说明 |
| ------------ | ---------------------------------------------------------------- |
| **数据来源** | 反推:`totalTokens - systemPrompt - allTools - memory` |
| **包含内容** | 所有用户消息、模型回复、工具调用参数、工具返回结果 |
| **特别包含** | skill body(第二阶段按需加载的内容)、文件读取结果、shell 输出等 |
| **计算方式** | `max(0, apiTotalTokens - estimatedOverhead)` |

> **注意**:Messages 是通过 API 返回的 `totalTokens` 减去其他分类的估算值得出的,因此它吸收了估算误差。如果 overhead 被高估,Messages 会被相应低估。

### 6. Free Space(可用空间)

| 属性 | 说明 |
| ------------ | ----------------------------------------------------- |
| **计算方式** | `contextWindowSize - totalTokens - autocompactBuffer` |
| **含义** | 在触发自动压缩之前,还能容纳多少 token 的对话内容 |

### 7. Autocompact Buffer(自动压缩缓冲区)

| 属性 | 说明 |
| ------------ | ----------------------------------------------------------------- |
| **计算方式** | `(1 - compressionThreshold) × contextWindowSize` |
| **默认值** | `(1 - 0.7) × 131072 = 39322`(约 30% 的上下文窗口) |
| **含义** | 当 token 用量达到 70% 时触发自动压缩,这 30% 的空间作为缓冲区预留 |

## 两种展示模式

### 模式 A:无 API 数据(首次使用,尚未发送消息)

```
Context Usage

No API response yet. Send a message to see actual usage.

Estimated pre-conversation overhead
Model: glm-5 Context window: 131.1k tokens

█ System prompt 4.8k tokens (3.7%)
█ System tools 5.2k tokens (4.0%)
█ Memory files 845 tokens (0.6%)
█ Skills 5.1k tokens (3.9%)
░ Free space 75.8k tokens (57.8%)
░ Autocompact buffer 39.3k tokens (30.0%)
```

- **不显示进度条和 total 数字**:避免估算值与后续 API 实际值产生不合理的对比
- **不显示 Messages 行**:尚无对话
- 各分类基于本地启发式估算(`estimateTokens`),可能与实际 API tokenizer 有 ~10% 偏差

### 模式 B:有 API 数据(已进行对话)

```
Context Usage

██████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ glm-5
25.3k/131.1k tokens (19.3%)

Usage by category
█ System prompt 4.5k tokens (3.4%)
█ System tools 4.9k tokens (3.7%)
█ Memory files 790 tokens (0.6%)
█ Skills 4.8k tokens (3.7%)
█ Messages 10.3k tokens (7.9%)
░ Free space 66.5k tokens (50.7%)
░ Autocompact buffer 39.3k tokens (30.0%)
```

- **`totalTokens` 来自 API 响应**(`usageMetadata.promptTokenCount`),是最准确的值
- **当本地估算 > API total 时**:按比例缩放各 overhead 分类,确保分类之和 = totalTokens
- **Messages** = `totalTokens - scaledOverhead`,包含所有对话内容 + 按需加载的 skill body

## Token 估算方法

由于无法直接访问模型的 tokenizer,使用基于字符的启发式估算:

```
tokens ≈ ⌈asciiChars / 4 + nonAsciiChars × 1.5⌉
```

| 字符类型 | 比例 | 依据 |
| --------------------------------- | --------------- | -------------------------------- |
| ASCII(英文、JSON 结构字符等) | ~4 字符/token | BPE tokenizer 对英文的平均压缩率 |
| 非 ASCII(中文、日文等 CJK 字符) | ~1.5 token/字符 | CJK 字符通常映射为 1-2 个 token |

**已知局限**:

- 不同模型的 tokenizer 有差异,估算可能偏差 ±10-20%
- JSON 结构字符(`{`, `"`, `:` 等)的实际 token 化比率与自然语言不同
- 当估算偏高时,通过 `overheadScale` 按比例缩放校正

## 数据流图

```
┌──────────────────┐
│ API Response │
│ promptTokenCount │ ─── totalTokens (ground truth)
└──────────────────┘
┌──────────────────────────┼──────────────────────────┐
│ │ │
▼ ▼ ▼
estimateTokens() estimateTokens() estimateTokens()
│ │ │
▼ ▼ ▼
systemPromptTokens allToolsTokens memoryFilesTokens
┌─────┴──────┐
│ │
▼ ▼
systemToolsTokens skillsTokens
(allTools - skills) (from SkillTool schema)
│ │
└─────┬──────┘
rawOverhead = systemPrompt + allTools + memory
┌───────────┼───────────┐
│ overheadScale │ (= min(1, totalTokens/rawOverhead))
▼ ▼
scaled categories messages = totalTokens - scaledOverhead
│ │
└───────────┬───────────┘
breakdown output
```
Loading