在客户端与模型端点间加透明缓存层,CI场景下相同错误不重复调用,1000次请求降至270次。
Free tier 的模型配额不是用来浪费的。最大的浪费不是糟糕的 prompt——而是重复请求。同一个编译器错误、同一个代码片段、同一个问题,被一次又一次地发往 endpoint。我写了一个小型的 C++ 缓存代理,夹在客户端和 MonkeyCode 免费模型 endpoint 之间。在一个模拟的 CI 工作负载下,它把模型调用从 1000 次减少到 270 次。降幅达 73%,代码只有约 200 行 C++。
声明:本文是 MonkeyCode 产品推广的一部分。该代理与 endpoint 无关,任何兼容 OpenAI 的聊天 endpoint 都可以使用。我在 MonkeyCode 的免费服务器选项上运行它,这恰好是 free tier 典型的工作负载形态:短生命周期、无状态、突发性。
背景:重复请求从何而来
在 CI 流水线中,同一个失败会出现在多个 job 中。一个失败的测试记录了同一个断言。一个构建步骤输出了同一个缺失头文件的错误。每个 job 独立调用模型请求解释。响应是相同的,但配额每次都被消耗。
目标很明确:添加一个透明的缓存层,拦截发往模型 endpoint 的 HTTP 请求,存储响应,并对重复请求从磁盘提供服务。无需客户端改动,无需模型改动,只是一个本地代理。
实现:五个步骤
步骤 1:设计缓存 key
缓存 key 必须在相同请求之间保持稳定。我通过对 JSON body 进行归一化处理,移除时间戳或 nonce 字段,然后用 SHA-256 对规范字符串进行哈希。
std::string cache_key(const std::string& body) {
// Remove non-deterministic fields like "timestamp" or "nonce"
auto canonical = strip_noise_fields(body);
return sha256(canonical);
}
步骤 2:用 SQLite 存储响应
SQLite 非常适合这个场景:单文件、无服务器、ACID。表结构很简单:
CREATE TABLE IF NOT EXISTS cache (
key TEXT PRIMARY KEY,
response TEXT NOT NULL,
created_at INTEGER NOT NULL
);
30 秒的 TTL 对于 CI 突发来说足够了。更长的 TTL 有可能提供过时的建议。
代理监听本地端口,读取请求,检查缓存。命中时立即返回存储的响应。未命中时,转发请求到上游 endpoint,缓冲完整响应,存储它,然后返回。
void handle_request(const httplib::Request& req, httplib::Response& res) {
auto key = cache_key(req.body);
if (auto cached = db.lookup(key)) {
res.set_content(*cached, "application/json");
return;
}
auto upstream = forward(req.body);
if (upstream) {
db.store(key, *upstream);
res.set_content(*upstream, "application/json");
} else {
res.status = 502;
}
}
步骤 4:处理流式响应
免费 endpoint 有时会流式传输 token。代理必须在缓存前缓冲完整流。我通过在 body 中请求 stream: false 来禁用可缓存请求的流式传输。这是一个权衡:未命中时延迟增加,但命中时立即返回。
步骤 5:添加简单的淘汰策略
内存中固定大小的 LRU,后端是 SQLite。当缓存超过 1000 条记录时,删除最旧的。这保持文件小、查询快。
我用 1000 个请求模拟了一个 CI 工作负载。请求池包含 100 个唯一 prompt,每个重复 10 次,模拟同一个错误出现在多个 job 中。代理运行在 MonkeyCode 的免费服务器选项上。
270 次未命中是 100 个唯一 prompt 加上 170 次在首次响应被缓存之前到达的重复请求。730 次命中在毫秒级返回。
计算很简单:同样的配额现在可以支撑 3.7 倍的工作量。对于 free tier 来说,这就是"够用"和"一直被拦截"之间的区别。
局限性:谁不应该使用这个
不要缓存创意任务(如代码生成)的响应。两个相同的 prompt 可能会合法地产生不同的有效答案。只缓存确定性解释、错误分析或文档查询。
不要缓存敏感数据。SQLite 文件是明文的。
代理增加了一个单点故障。如果它崩溃,客户端失去连接。将其作为 systemd 服务或容器运行。
30 秒的 TTL 是一个猜测。测量你自己的工作负载并调整。
重复请求在统计之前是不可见的。我以为 CI 工作负载大部分是唯一的。实际上并不是。
缓存代理比断路器更简单。它不需要检测故障,只需要记住答案。
Free tier 奖励自律。代理是一种自律形式:它让每一个配额单位都物尽其用。
最好的优化是不调用模型。
完整源码约 200 行 C++,使用 libcurl、SQLite 和一个微型 HTTP 服务器。我运行在 MonkeyCode 的免费模型访问和免费服务器选项上;任何兼容的 endpoint 行为都一样。如果你想尝试,从 30 秒 TTL 开始,测量你自己的命中率。