从 Python 到 Node.js:我把两个开源项目揉成一个,在 DeepSeek 上跑出 76% 的 Token 节省率(附完整架构和 35 次真实测试数据)

先说结果:DeepSeek 真实 API 测试,tokensaver v2.0 代理模式总 Token 节省 76.1%,输出节省 88.4%,缓存命中时响应 1-3ms。下面讲讲两周的开发过程,包括踩过的坑和关键的 trade-off 决策。


1. 这事是怎么开始的

你调过 DeepSeek 的 API 就知道,一次"解释 Python 装饰器"的问题,裸调返回大概 1024 个 output token。里面有多少是真正有用的信息?我估了一下,可能 30-40% 都在说"好的,让我来帮你详细解释一下"“首先我们需要理解”"综上所述"这种话。

这些就是低信息密度的纯 Token 消耗,一分钱没少花,但没产生价值。

最近 GitHub 上爆了个项目叫 Caveman,一个 19 岁的荷兰小哥写的,52K star。核心思路有多简单?往 System Prompt 里塞一段"像原始人一样说话"的指令——不改模型、不加推理开销,输出直接砍半。

但这个方案有两个明显的问题:一是只支持英文,国产模型(DeepSeek、GLM、Kimi)用英文提示效果打折;二是你得手动把那段 prompt 贴到代码里,换个模型就得改代码重启。

我当时就想:能不能做成一个透明代理层,开发者完全不感知,自动处理所有压缩?


2. 系统架构

2.1 整体数据流

Client (OpenAI SDK)
  │ POST /v1/chat/completions
  ▼
┌─────────────────────────────────────────┐
│            tokensaver Proxy             │
│                                         │
│  Layer 1: SemanticCache                 │
│    ├─ Tokenize → Jaccard similarity     │
│    ├─ Hit  → return cached (1-3ms)      │
│    └─ Miss → continue                   │
│                                         │
│  Layer 2: InputCompressor               │
│    ├─ ContentProtector.protect()        │
│    ├─ Dedup lines / filter fillers      │
│    ├─ Truncate (aggressive mode)        │
│    └─ ContentProtector.restore()        │
│                                         │
│  Layer 3: OutputCompressor              │
│    ├─ Inject Caveman system prompt      │
│    ├─ Forward to upstream LLM           │
│    └─ PostProcessor.process()           │
│                                         │
│  Response: + tokensaver stats field     │
└─────────────────────────────────────────┘
  │
  ▼
Upstream LLM (DeepSeek / GLM / Kimi / ...)

2.2 模块职责

模块 文件 行数 核心算法
代理服务器 server.js 370 HTTP pipeline + auto model routing
语义缓存 semantic-cache.js 230 Jaccard similarity + TTL eviction
输入压缩 input.js 155 Line dedup + filler filter + truncation
内容保护 content-protector.js 95 Placeholder substitution pattern
后处理器 post-processor.js 85 Regex-based greeting removal

3. 核心模块实现

3.1 语义缓存:为什么选 Jaccard 而不是 Embedding

tokcut 的 Python 版本使用了 sentence-transformers 的 all-MiniLM-L6-v2 模型(约 80MB)做语义 embedding,然后用余弦相似度匹配。

Node.js 版本的 trade-off 分析:

// 方案 A: sentence-transformers → 需要 @xenova/transformers (~500MB deps)
// 方案 B: Jaccard similarity on tokenized text → 0 额外依赖

_jaccardSimilarity(tokensA, tokensB) {
  const setA = new Set(tokensA), setB = new Set(tokensB);
  let intersection = 0;
  for (const item of setA) {
    if (setB.has(item)) intersection++;
  }
  return intersection / (setA.size + setB.size - intersection);
}

实测对比(1000 条测试数据):

方案 冷启动 内存 命中率(0.92阈值)
sentence-transformers ~3s (模型下载) ~200MB 91.2%
Jaccard (js-tiktoken) 0ms ~15MB 88.7%

Jaccard 命中率仅低 2.5 个百分点,但启动零延迟、内存占用和部署复杂度大幅降低。对于中小规模部署场景,Jaccard 是更优选择。

3.2 内容保护器的占位符机制

这是整个压缩链路中最容易出 Bug 的模块。核心矛盾:压缩算法不能损坏代码块、URL 和版本号。

// protect → compress → restore 三段式
class ContentProtector {
  protect(text) {
    // 1. 多行代码块: ```...```
    text = text.replace(/```[\s\S]*?```/g, m => this._addPlaceholder(m));
    // 2. 行内代码: `...`
    text = text.replace(/`[^`]+`/g, m => this._addPlaceholder(m));
    // 3. URL: https?://...
    text = text.replace(/https?:\/\/[^\s<>"{}|\\^`\[\]]+/g, m => this._addPlaceholder(m));
    // 4. 版本号: \d+\.\d+\.\d+
    text = text.replace(/\b\d+\.\d+\.\d+\b/g, m => this._addPlaceholder(m));
    return text;
  }
  
  restore(text) {
    // 反向遍历还原,防止占位符嵌套
    for (let i = this.placeholders.length - 1; i >= 0; i--) {
      text = text.replace(`__TOKENSAVER_PROTECTED_${i}__`, this.placeholders[i]);
    }
    return text;
  }
}

一个踩过的坑:aggressive 压缩模式会截断文本到 70%,如果截断位置恰好切在占位符中间,restore() 就会失败。解决方案是在 _aggressiveCompress() 中加判断:

if (!text.includes('__TOKENSAVER_PROTECTED_')) {
  const limit = Math.max(Math.floor(text.length * 0.7), 1);
  text = text.substring(0, limit);
}

3.3 输入压缩的双模式设计

class InputCompressor {
  compressText(text) {
    let protected_ = this.protector.protect(text);
    
    if (this.mode === 'safe') {
      protected_ = this._safeCompress(protected_);    // 去重行 + 去填充词
    } else {
      protected_ = this._aggressiveCompress(protected_); // safe + 截断70%
    }
    
    return this.protector.restore(protected_);
  }
  
  compressMessages(messages) {
    // 关键设计:仅压缩 role === 'user' 的消息
    return messages.map(msg => 
      msg.role === 'user' 
        ? { ...msg, content: this.compressText(msg.content) }
        : msg
    );
  }
}

为什么只压 user 消息?因为 system prompt 中的技术约束和 assistant 的历史回答可能包含关键上下文(如错误信息原文),压缩这些内容会导致模型理解偏差。


4. Benchmark 方法

4.1 测试设计

被测模型: deepseek-chat
测试场景: 5 类(代码解释/技术概念/代码审查/最佳实践/问题排查)
测试方案: 7 种(Baseline + tokcut Lite/Full/Ultra + tokensaver CN-Full/Ultra)
总调用次数: 5 × 7 = 35 次

每轮调用设置 max_tokens=2048temperature=0.7,请求间间隔 1s 避免限流。Token 计数以 API 返回的 usage 字段为准。

4.2 测试结果(35次调用汇总)

方案 Prompt Token Output Token Total Token 总节省率 输出节省率
Baseline 189 8,051 8,240 - -
tokcut Lite 369 3,208 3,577 56.6% 60.2%
tokcut Full 424 2,278 2,702 67.2% 71.7%
tokcut Ultra 394 1,419 1,813 78.0% 82.4%
tokensaver CN-Full 1,039 931 1,970 76.1% 88.4%
tokensaver CN-Ultra 619 560 1,179 85.7% 93.0%

4.3 结果分析

核心发现 1:中文 Prompt 优于英文 Prompt。 tokensaver CN-Full 的输出节省率(88.4%)显著高于 tokcut Full(71.7%),差异为 16.7 个百分点。即使 tokensaver 的 System Prompt 开销更大(1039 vs 424 token),总节省率仍领先 8.9 个百分点。

核心发现 2:盈亏平衡分析。 tokensaver CN-Full 的额外 Prompt 开销为 1039 - 424 = 615 token,但由此产生额外输出节省 2278 - 931 = 1347 token,净收益 732 token,ROI = 119%。

核心发现 3:Ultra 模式的总节省率高达 85.7%,但输出可读性显著下降。 生产环境推荐 CN-Full,在可读性和节省率之间取得平衡。


5. 工程化设计

5.1 代理服务器的动态控制

// 通过 HTTP Header 实现按请求覆盖配置,无需重启服务
const outputCompressEnabled = this._parseBoolHeader(
  req.headers['x-tokensaver-compress'], this.compressEnabled
);
const compressLevel = req.headers['x-tokensaver-level'] || this.compressLevel;
const cacheEnabled = this._parseBoolHeader(
  req.headers['x-tokensaver-cache'], this.cache.enabled
);

支持 7 个 Header 控制项,覆盖压缩开关、级别、缓存行为、输入压缩等。

5.2 响应中的 Token 统计注入

{
  "tokensaver": {
    "cache_hit": false,
    "input_tokens_before": 150,
    "input_tokens_after": 120,
    "input_tokens_saved": 30,
    "output_tokens": 200,
    "output_tokens_saved": 800,
    "output_compression_applied": true,
    "prompt_compression_applied": false,
    "compression_level": "cn-full",
    "elapsed_ms": 2546
  }
}

5.3 测试覆盖

$ node tests/run.js       # v1 原有测试
  ✅ 通过: 17

$ node tests/run-v2.js    # v2 新增模块测试
  ✅ Content Protector:  4/4
  ✅ Input Compressor:   4/4
  ✅ Semantic Cache:     4/4
  ✅ Post Processor:     4/4
  ✅ Output Compressor:  4/4
  ✅ 通过: 20

总计: 37/37 通过

6. 与现有工作的对比

维度 Caveman tokcut tokensaver v2
部署方式 Claude Code 插件 Python 代理 Node.js 代理 + CLI
压缩语言 英文 英文 中文 + 英文 + 文言文
语义缓存 sentence-transformers Jaccard
输入压缩
代理模式
CI/CD
Docker
文件压缩
Benchmark

7. 不足和后面想做的事

几个明显的局限:

  • 还没支持 SSE 流式(streaming),目前只能处理非流式的 /v1/chat/completions。流式的问题是每 chunk 都要后处理,得算增量,还没想清楚怎么做。
  • Jaccard 缓存对超长文本的区分度肯定不如 Embedding。如果有人问两篇都很长的文章但主旨不同,Jaccard 的 token 集重合度可能误判。不过短中文本够用了。
  • 只兼容 OpenAI 接口格式,Anthropic 那种不标准的暂时不行。

后面想加的:流式响应、Redis 缓存后端(生产环境需要)、一个简单的 Web 面板看实时统计。


8. 收尾

一周时间,从刷到 B站 上的 Caveman 开始,一路写到这里。最关键的决策有两个:一是用真实 API 跑数据再做技术选型,不凭直觉——要不是跑了 35 次调用,我不会发现中文 prompt 比英文好那么多;二是把工程化补齐,Docker、CI、测试、文档,让一个 demo 变成能真正交付的东西。

如果你也用 LLM API 做开发,试试:

git clone https://github.com/luckychenxiaowen/tokensaver.git
cd tokensaver && npm install && npm run serve

然后 base_url 改一行。完事。


项目:github.com/luckychenxiaowen/tokensaver

在这里插入图片描述

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐