从 Python 到 Node.js:我把两个开源项目揉成一个,在 DeepSeek 上跑出 76% 的 Token 节省率(附完整架构和 35 次真实测试数据)
从 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.js370HTTP pipeline auto model routing语义缓存semantic-cache.js230Jaccard similarity TTL eviction输入压缩input.js155Line dedup filler filter truncation内容保护content-protector.js95Placeholder substitution pattern后处理器post-processor.js85Regex-based greeting removal3. 核心模块实现3.1 语义缓存为什么选 Jaccard 而不是 Embeddingtokcut 的 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){constsetAnewSet(tokensA),setBnewSet(tokensB);letintersection0;for(constitemofsetA){if(setB.has(item))intersection;}returnintersection/(setA.sizesetB.size-intersection);}实测对比1000 条测试数据方案冷启动内存命中率(0.92阈值)sentence-transformers~3s (模型下载)~200MB91.2%Jaccard (js-tiktoken)0ms~15MB88.7%Jaccard 命中率仅低 2.5 个百分点但启动零延迟、内存占用和部署复杂度大幅降低。对于中小规模部署场景Jaccard 是更优选择。3.2 内容保护器的占位符机制这是整个压缩链路中最容易出 Bug 的模块。核心矛盾压缩算法不能损坏代码块、URL 和版本号。// protect → compress → restore 三段式classContentProtector{protect(text){// 1. 多行代码块: ...texttext.replace(/[\s\S]*?/g,mthis._addPlaceholder(m));// 2. 行内代码: ...texttext.replace(/[^]/g,mthis._addPlaceholder(m));// 3. URL: https?://...texttext.replace(/https?:\/\/[^\s{}|\\^\[\]]/g,mthis._addPlaceholder(m));// 4. 版本号: \d\.\d\.\dtexttext.replace(/\b\d\.\d\.\d\b/g,mthis._addPlaceholder(m));returntext;}restore(text){// 反向遍历还原防止占位符嵌套for(letithis.placeholders.length-1;i0;i--){texttext.replace(__TOKENSAVER_PROTECTED_${i}__,this.placeholders[i]);}returntext;}}一个踩过的坑aggressive 压缩模式会截断文本到 70%如果截断位置恰好切在占位符中间restore()就会失败。解决方案是在_aggressiveCompress()中加判断if(!text.includes(__TOKENSAVER_PROTECTED_)){constlimitMath.max(Math.floor(text.length*0.7),1);texttext.substring(0,limit);}3.3 输入压缩的双模式设计classInputCompressor{compressText(text){letprotected_this.protector.protect(text);if(this.modesafe){protected_this._safeCompress(protected_);// 去重行 去填充词}else{protected_this._aggressiveCompress(protected_);// safe 截断70%}returnthis.protector.restore(protected_);}compressMessages(messages){// 关键设计仅压缩 role user 的消息returnmessages.map(msgmsg.roleuser?{...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_tokens2048temperature0.7请求间间隔 1s 避免限流。Token 计数以 API 返回的usage字段为准。4.2 测试结果35次调用汇总方案Prompt TokenOutput TokenTotal Token总节省率输出节省率Baseline1898,0518,240--tokcut Lite3693,2083,57756.6%60.2%tokcut Full4242,2782,70267.2%71.7%tokcut Ultra3941,4191,81378.0%82.4%tokensaver CN-Full1,0399311,97076.1%88.4%tokensaver CN-Ultra6195601,17985.7%93.0%4.3 结果分析核心发现 1中文 Prompt 优于英文 Prompt。tokensaver CN-Full 的输出节省率88.4%显著高于 tokcut Full71.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 tokenROI 119%。核心发现 3Ultra 模式的总节省率高达 85.7%但输出可读性显著下降。生产环境推荐 CN-Full在可读性和节省率之间取得平衡。5. 工程化设计5.1 代理服务器的动态控制// 通过 HTTP Header 实现按请求覆盖配置无需重启服务constoutputCompressEnabledthis._parseBoolHeader(req.headers[x-tokensaver-compress],this.compressEnabled);constcompressLevelreq.headers[x-tokensaver-level]||this.compressLevel;constcacheEnabledthis._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 测试覆盖$nodetests/run.js# v1 原有测试✅ 通过:17$nodetests/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. 与现有工作的对比维度Cavemantokcuttokensaver v2部署方式Claude Code 插件Python 代理Node.js 代理 CLI压缩语言英文英文中文 英文 文言文语义缓存❌sentence-transformersJaccard输入压缩❌✅✅代理模式❌✅✅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 做开发试试gitclone https://github.com/luckychenxiaowen/tokensaver.gitcdtokensavernpminstallnpmrun serve然后 base_url 改一行。完事。项目github.com/luckychenxiaowen/tokensaver