# lspz 压缩格式规范
**版本**: v0.1.0
**状态**: 定稿
**最后更新**: 2026-05-09
## 概述
本文档定义 lspz 使用的紧凑格式(Compact Format),用于压缩 LSP 诊断消息,减少 Token 消耗。
### 目标
- **Token 节省**: 相比原始 JSON 格式节省 ≥ 40% Token
- **语义完整**: 保留所有关键信息(文件、行号、严重级别、消息)
- **可还原**: Agent 端可以还原为标准 LSP Diagnostic 格式
- **可扩展**: 支持版本控制和向后兼容
---
## 调研发现: 为什么去重是最核心的压缩手段
### 真实 LSP 服务器的字段使用情况
对 3 个真实 LSP 服务器(jinja-lsp、typescript-language-server、yaml-language-server)的实际诊断输出分析:
| `range` | ✅ | ✅ | ✅ | ❌ (核心信息) |
| `message` | ✅ | ✅ | ✅ | ❌ (核心信息) |
| `severity` | ✅ | ✅ | ✅ | ❌ (核心信息) |
| `source` | ✅ (固定"jinja-lsp") | ✅ | ✅ (固定"YAML") | ✅ (AI 不需要) |
| `code` | ❌ | ✅ | ✅ | ⚠️ 有时有用 |
| `relatedInformation` | ❌ | ✅ | ❌ | ✅ (AI 很少需要) |
| `tags` | ❌ | ✅ (Unnecessary/Deprecated) | ❌ | ⚠️ 可压缩 |
| `data` | ❌ | ❌ | ❌ | ✅ (极少使用) |
| `codeDescription` | ❌ | ❌ | ❌ | ✅ (极少使用) |
**结论**: 字段裁剪带来的收益有限——大多数服务器只使用 4-6 个字段。`dedup` 才是收益最大的策略。
### OpenCode 的现有压缩策略
作为参考的 AI Coding Agent OpenCode 的处理方式:
- 只保留 `severity === 1` (ERROR),丢弃 WARNING / INFO / HINT
- 每个文件最多 20 条,超出部分用 `"... and N more"` 替代
- 格式化为 `"ERROR [L:C] msg"` 单行字符串
- 工具输出截断在 2000 字符/50KB
这意味着:
1. OpenCode 丢弃了**所有警告信息**——这是 lspz 可以保留的信号
2. 每文件 20 条的上限在大型重构时会丢失信息——lspz 可以用紧凑格式保留更多
3. 没有跨文件错误分组——lspz 可以提供结构化分类
### lspz 的差异化价值
| WARNING | ❌ 丢弃 | ✅ 保留为 `W` 单字符标识 |
| 每文件上限 | 20 条 | 无上限(紧凑格式) |
| 跨文件聚合 | ❌ 无 | ✅ 按 (message, code) 分组 |
| 输出格式 | 非结构化文本 | 结构化 CompactJSON |
| 标准化 | OpenCode 独有 | 所有 Agent 通用 |
---
## 压缩策略(按优先级排列)
### Strategy 1: 去重合并 (Deduplication) — #1 收益
**为什么是 #1**: 一个 TypeScript 文件可能包含 30 个 "unused variable" warnings——它们的 message 和 severity 完全相同,只有 range 不同。合并后从 30 个 JSON 对象变成 1 个 entry + 30 个 compact range。
**规则**: 同一文件中相同 `message` + `severity` + `code` 的诊断合并为一个条目,`ranges` 平铺。
```json
// 原始: 3 条重复诊断 → 约 150 tokens
[
{"range": {"start": {"line": 10, "character": 0}, "end": {"line": 10, "character": 5}}, "severity": 2, "message": "unused variable `x`"},
{"range": {"start": {"line": 15, "character": 0}, "end": {"line": 15, "character": 5}}, "severity": 2, "message": "unused variable `x`"},
{"range": {"start": {"line": 20, "character": 0}, "end": {"line": 20, "character": 5}}, "severity": 2, "message": "unused variable `x`"}
]
// 压缩后: 1 条 + 3 个 range → 约 30 tokens (80% 节省!)
{
"m": "unused variable `x`",
"s": "W",
"c": "unused_variable",
"r": [[10, 0, 10, 5], [15, 0, 15, 5], [20, 0, 20, 5]]
}
```
#### 去重算法 (HashMap 分组)
```mermaid
flowchart LR
subgraph 输入["原始 Diagnostic[]"]
D1["{msg:'unused x', severity:2, code:'unused_var', range:{10,0,10,5}}"]
D2["{msg:'unused x', severity:2, code:'unused_var', range:{15,0,15,5}}"]
D3["{msg:'unused x', severity:2, code:'unused_var', range:{20,0,20,5}}"]
D4["{msg:'undeclared y', severity:1, code:'E0425', range:{25,0,25,8}}"]
end
subgraph 分组["HashMap 分组"]
K1["key = hash(message, severity, code)"]
K2["k1: (unused x, 2, unused_var)"]
K3["k2: (undeclared y, 1, E0425)"]
end
subgraph 合并["同一 key 内合并"]
M1["Group k1:{ranges: [10:0-10:5, 15:0-15:5, 20:0-20:5], count: 3}"]
M2["Group k2:{ranges: [25:0-25:8], count: 1}"]
end
subgraph 输出["去重结果"]
O1["1 条 compressed entry<br/>(取代原始 3 条)"]
O2["1 条 compressed entry<br/>(未去重)"]
end
D1 & D2 & D3 & D4 --> K1 --> K2 & K3
K2 --> M1 --> O1
K3 --> M2 --> O2
classDef input fill:#7ED321,stroke:#5BA01A,stroke-width:2px,color:#fff
classDef step fill:#4A90E2,stroke:#2E5C8A,stroke-width:2px,color:#fff
classDef output fill:#F5A623,stroke:#D4880F,stroke-width:2px,color:#fff
class D1,D2,D3,D4 input
class K1,K2,K3,M1,M2 step
class O1,O2 output
```
**关键实现细节**:
- HashMap key 使用 `(message, severity_number, code)` 三元组
- code 为 `Option` 时,`None` 视为同组
- 复杂度 O(n),n = 诊断条数
- 分组后每个 group 的 ranges 按位置排序(line→col),保持输出可读
### Strategy 2: 字段裁剪 (Field Pruning)
**默认保留字段**:
- `message`: 诊断消息
- `severity`: 严重级别
- `range`: 位置范围(压缩为 `[line, char, line, char]`)
- `code`: 诊断代码(可选,保留时有助于 AI 理解错误类型)
**默认移除字段**:
- `data`: 额外数据(极少使用)
- `codeDescription`: 代码描述(极少使用)
- `relatedInformation`: 相关信息(AI 很少需要)
- `tags`: 标签(压缩后编码到单字符)
- `source`: 来源(固定值,对 AI 无意义)
### Strategy 3: 枚举缩减 (Enum Compression)
**Severity 编码**:
| `1` (Error) | `'E'` | 错误 |
| `2` (Warning) | `'W'` | 警告 |
| `3` (Information) | `'I'` | 信息 |
| `4` (Hint) | `'H'` | 提示 |
**Tags 编码** (如有):
- `Unnecessary`: `'U'`
- `Deprecated`: `'D'`
### Strategy 4: Range Delta 编码
**原始格式** — 每个 range 需要 6 个字段名 + 4 个数字:
```json
{"start": {"line": 10, "character": 5}, "end": {"line": 10, "character": 15}}
```
**压缩格式** — 4 个数字数组,无字段名:
```
[10, 5, 10, 15]
```
**多个 Range**: 第一个 range 存绝对值,后续 range 存相对于前一个的差值(delta 编码)。这样当多个错误在同一行或连续几行时,增量值很小甚至为零。
#### Delta 编码算法
```mermaid
flowchart LR
subgraph 输入["输入: 3 个 LSP Range"]
R1["Range{start:{10,5}, end:{10,15}}"]
R2["Range{start:{11,5}, end:{11,15}}"]
R3["Range{start:{12,5}, end:{12,15}}"]
end
subgraph 绝对值["第一步: 转 [L,C,L,C]"]
A1["[10, 5, 10, 15]"]
A2["[11, 5, 11, 15]"]
A3["[12, 5, 12, 15]"]
end
subgraph 差分["第二步: 计算 Delta"]
D1["第一个: 保持绝对 → [10, 5, 10, 15]"]
D2["第二个: [11-10, 5-5, 11-10, 15-15] = [1, 0, 1, 0]"]
D3["第三个: [12-11, 5-5, 12-11, 15-15] = [1, 0, 1, 0]"]
end
subgraph 输出["输出: delta 编码数组"]
O1["[10, 5, 10, 15] ← 绝对值"]
O2["[1, 0, 1, 0] ← delta"]
O3["[1, 0, 1, 0] ← delta"]
end
R1 --> A1
R2 --> A2
R3 --> A3
A1 --> D1 --> O1
A2 --> D2 --> O2
A3 --> D3 --> O3
classDef input fill:#7ED321,stroke:#5BA01A,stroke-width:2px,color:#fff
classDef step fill:#4A90E2,stroke:#2E5C8A,stroke-width:2px,color:#fff
classDef output fill:#F5A623,stroke:#D4880F,stroke-width:2px,color:#fff
class R1,R2,R3 input
class A1,A2,A3,D1,D2,D3 step
class O1,O2,O3 output
```
**解码时还原**: 累加 delta 值恢复绝对值。
```json
// 存储:
{
"r": [
[10, 5, 10, 15], // 第一个: 绝对值
[1, 0, 1, 0], // 第二个: line+1, col 不变
[1, 0, 1, 0] // 第三个: line+1, col 不变
]
}
// 还原: 累加 delta
// range[1] = [10+1, 5+0, 10+1, 15+0] = [11, 5, 11, 15]
// range[2] = [11+1, 5+0, 11+1, 15+0] = [12, 5, 12, 15]
```
**何时不启用 delta**: 当只有 1 个 range 时,直接存绝对值 `[line, col, line, col]`,不做 delta。
---
## 紧凑格式 Schema
### 顶层结构
```typescript
interface CompactDiagnosticsParams {
version: 1; // 格式版本
uri: string; // 文档 URI
diagnostics: CompactDiagnostic[];
}
```
### 单个诊断
```typescript
interface CompactDiagnostic {
m: string; // message
// 可选字段
n?: number; // count (dedup 合并的诊断条数, 默认 1)
}
```
### Range 格式
```typescript
// 绝对 range: [line_start, char_start, line_end, char_end]
// delta range: [Δline_start, Δchar_start, Δline_end, Δchar_end] (相对于上一个)
type Range = [number, number, number, number];
type Ranges = Range[]; // 第一个是绝对,后续是 delta
```
---
## 完整示例
### 原始 LSP Diagnostics
```json
{
"uri": "file:///path/to/file.rs",
"diagnostics": [
{
"range": {
"start": {"line": 10, "character": 5},
"end": {"line": 10, "character": 15}
},
"severity": 2,
"message": "unused variable: `x`",
"code": "unused_variables",
"source": "rust-analyzer",
"tags": [1]
},
{
"range": {
"start": {"line": 11, "character": 5},
"end": {"line": 11, "character": 15}
},
"severity": 2,
"message": "unused variable: `x`",
"code": "unused_variables",
"source": "rust-analyzer",
"tags": [1]
},
{
"range": {
"start": {"line": 20, "character": 0},
"end": {"line": 20, "character": 10}
},
"severity": 1,
"message": "cannot find value `y` in this scope",
"code": "E0425",
"source": "rust-analyzer"
}
]
}
```
### 紧凑格式
```json
{
"version": 1,
"uri": "file:///path/to/file.rs",
"diagnostics": [
{
"m": "unused variable: `x`",
"s": "W",
"r": [[10, 5, 10, 15], [1, 0, 1, 0]],
"c": "unused_variables",
"t": "U",
"n": 2
},
{
"m": "cannot find value `y` in this scope",
"s": "E",
"r": [[20, 0, 20, 10]],
"c": "E0425"
}
]
}
```
### Token 对比
| 原始 JSON | ~350 | - |
| 紧凑格式 | ~180 | **48%** |
> 注: 上述示例只有 3 条诊断。在真实场景中(如一个 TypeScript 文件有 50+ 个 lint 错误),去重合并带来的收益更大。
---
## 压缩管道
```mermaid
flowchart LR
subgraph RAW["Raw Diagnostics[]"]
I1["{range, severity, message, code, source, tags}"]
I2["{range, severity, message, code, source, tags}"]
I3["{range, severity, message, code, source}"]
end
subgraph S1["Step 1: Group & Dedup"]
G1["Hash by (message, severity, code)"]
G2["Merge ranges"]
G3["Count n"]
end
subgraph S2["Step 2: Prune Fields"]
P1["Drop: source, data, codeDescription, relatedInformation"]
P2["Keep: message, severity, range, code"]
end
subgraph S3["Step 3: Encode"]
E1["severity: 1→E 2→W 3→I 4→H"]
E2["tags: [1]→'U'"]
E3["keys: m, s, r, c, n"]
end
subgraph S4["Step 4: Delta Ranges"]
D1["First: absolute"]
D2["Rest: delta"]
end
subgraph COMPACT["CompactDiagnostics{v, uri, diags[]}"]
O1["{m: '...', s: 'W', r: [[10,5,10,15],[1,0,1,0]], c: 'code', n: 2}"]
O2["{m: '...', s: 'E', r: [[20,0,20,10]], c: 'E0425'}"]
end
I1 & I2 & I3 --> G1 --> G2 --> G3
G3 --> P1 --> P2
P2 --> E1 --> E2 --> E3
E3 --> D1 --> D2
D2 --> O1
D2 --> O2
classDef raw fill:#7ED321,stroke:#5BA01A,stroke-width:2px,color:#fff
classDef step fill:#4A90E2,stroke:#2E5C8A,stroke-width:2px,color:#fff
classDef compact fill:#F5A623,stroke:#D4880F,stroke-width:2px,color:#fff
class I1,I2,I3 raw
class G1,G2,G3,P1,P2,E1,E2,E3,D1,D2 step
class O1,O2 compact
```
---
## 版本控制
### 格式版本字段
所有紧凑格式消息必须包含 `version` 字段:
```json
{
"version": 1,
"uri": "file:///path/to/file.rs",
"diagnostics": [...]
}
```
### 版本兼容性
| v1 | v1 | ✅ 完全兼容 |
| v2 | v1 | ⚠️ 可降级 |
| v1 | v2 | ❌ 需升级 |
### 升级策略
1. 增加版本号
2. Agent 端和 Proxy 端同时更新
3. 提供兼容层支持旧版本(临时)
---
## 输出格式进化
当前 Compact JSON 作为第一个输出格式,后续引入 [**TOON(Token-Oriented Object Notation)**](005-toon-format.md) 作为 LLM 优化的第二输出格式。
| Compact JSON | 当前 (v0.1–v0.5) | JSON 格式,字段缩短 + 去重,最小 token |
| TOON | v0.7+ | 表格格式+自解释字段名,LLM 直接消费 |
**选择指南**: 如果需要最小 token → 用 Compact JSON(缩写字段名)。
如果希望 LLM 直接理解 → 用 TOON(完整字段名,无需预解压)。
详见 [005-toon-format.md](005-toon-format.md)。
---
## 配置选项
```rust
/// 压缩策略的细粒度控制。
pub struct CompressionConfig {
/// 是否启用去重合并(#1 收益策略)
pub enable_dedup: bool,
/// 是否启用字段裁剪
pub enable_field_pruning: bool,
/// 是否启用枚举缩减
pub enable_enum_compression: bool,
/// 是否启用 Range delta 编码
pub enable_range_delta: bool,
/// 保留字段列表(当 enable_field_pruning=true 时)
pub keep_fields: Vec<String>,
}
impl Default for CompressionConfig {
fn default() -> Self {
Self {
enable_dedup: true,
enable_field_pruning: true,
enable_enum_compression: true,
enable_range_delta: true,
keep_fields: vec!["message".into(), "severity".into(), "range".into(), "code".into()],
}
}
}
```
---
## 参考文档
- [LSP Diagnostic 类型](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#diagnostic)
- [ROADMAP.md](https://github.com/straydragon/lspz/blob/main/ROADMAP.md) - 项目路线图
- [specs/001-tri-modal-architecture.md](001-tri-modal-architecture.md) - 三模态架构
- [specs/003-lsp-compatibility.md](003-lsp-compatibility.md) - LSP 兼容性
- [specs/005-toon-format.md](005-toon-format.md) - TOON 行协议格式