免费模型输出的JSON格式稳定但标签含义漂移,导致crash分类错误被错误路由。解决方案是用C++ sidecar做确定性契约校验,漂移标签进入隔离区。
凌晨 02:40,分流队列停止消费。worker 进程状态健康,Redis 长度持续增长,HTTP 客户端返回 200 OK,JSON 解码器正常输出对象。但 bucket 分配结果是错的。一份堆内存损坏报告被错误地路由到了网络超时 bucket。值班工程师打开载荷,label 看起来合理,JSON 也合法——这就是陷阱所在。
该服务使用一个 free model 端点,将原始崩溃栈转译为一组小的 triage label。模型每次都生成干净的 JSON,schema 保持稳定,但 value 不稳定。某天 heap_use_after_free 变成了 network_timeout,另一天 stack_overflow 变成了 plugin_crash。没有解析错误,没有超时,重试反而让情况更糟——每一次重试都返回另一个合法的、同样自信的错误答案。
本文介绍一个 C++ sidecar,它将模型 label 视为一个提案(proposal)而非最终结果(result)。该 sidecar 先应用一个确定性契约(deterministic contract),将分歧记录到漂移账本(drift ledger),将不稳定的 label 路由到隔离区(quarantine)。MonkeyCode 的免费模型访问和免费服务器选项分别作为提案来源和影子投票者(shadow voter)出现在流水线中。
披露:本文作为 MonkeyCode 产品推广的一部分撰写。
从失败入手,而非从 prompt 入手
第一个错误是把模型当作一个词汇表稳定的分类器。free model 端点适合从脏输入中提出 label 提案,但它不是一个确定性语法(deterministic grammar)。JSON 合法不等于 label 正确。一个返回 "label": "network_timeout" 和 "confidence": 0.96 的响应,通过了所有序列化检查,但 bug 在传输层完全不可见。
因此 sidecar 维护了一个小型契约表(contract table)。它不是模型的替代品,只覆盖团队能手动验证的已知崩溃特征。它刻意保持简单——只有当规范化后的栈包含精确的特征字符串(needle)时才触发规则。目标不是覆盖率,而是建立一个稳定的参考点来检测漂移。
#include <algorithm>
#include <cctype>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
std::string Normalize(std::string_view trace) {
std::string out;
out.reserve(trace.size());
bool last_space = false;
for (char c : trace) {
unsigned char uc = static_cast<unsigned char>(c);
if (std::isspace(uc) || !std::isprint(uc)) {
if (!last_space) out.push_back(' ');
last_space = true;
continue;
}
out.push_back(static_cast<char>(std::tolower(uc)));
last_space = false;
}
return out;
}
struct CrashSignature {
const char* bucket;
const char* needle;
};
const std::vector<CrashSignature> CONTRACT = {
{"heap_use_after_free", "heap-use-after-free"},
{"stack_overflow", "stack-overflow"},
{"network_timeout", "etimedout"},
{"null_deref", "null pointer dereference"},
};
std::optional<std::string> ContractVote(std::string_view trace) {
const std::string normalized = Normalize(trace);
for (const auto& rule : CONTRACT) {
if (normalized.find(rule.needle) == std::string::npos) continue;
return std::string(rule.bucket);
}
return std::nullopt;
}
这段代码刻意保持精简。它只拥有团队能手动验证的 label,不需要理解每一种崩溃,只需要捕捉模型开始用错误名字称呼已知特征的那一刻。
记录漂移,但不惩罚新 label
契约门控不是分类器。它只是一个能手动验证少量 label 的 oracle。当契约返回一个 label 而模型返回另一个时,sidecar 不重试,而是增加契约 bucket 对应账本单元的计数。当模型与契约一致时,该单元记录为一致。账本使用滑动窗口(sliding window),使旧事件不会永久污染当前比率。
#include <chrono>
#include <deque>
#include <map>
#include <string>
struct DriftEntry {
std::chrono::steady_clock::time_point at;
bool agreed;
};
class DriftLedger {
public:
explicit DriftLedger(std::chrono::seconds window) : window_(window) {}
void Record(const std::string& bucket, bool agreed) {
auto now = std::chrono::steady_clock::now();
auto& q = cells_[bucket];
q.push_back({now, agreed});
while (!q.empty() && now - q.front().at > window_) {
q.pop_front();
}
}
double DriftRate(const std::string& bucket) const {
auto it = cells_.find(bucket);
if (it == cells_.end() || it->second.empty()) return 0.0;
int drift = 0;
for (const auto& entry : it->second) {
if (!entry.agreed) ++drift;
}
return static_cast<double>(drift) / it->second.size();
}
private:
std::chrono::seconds window_;
std::map<std::string, std::deque<DriftEntry>> cells_;
};
账本是响应式的(reactive)。它不会在一次坏答案后阻塞模型,而是让比率变得可见。下面的路由层来决定发现分歧后如何处理。
以契约为权威进行路由
下面的辅助函数接收:规范化后的栈、模型提案、影子提案和账本。当契约有意见时,契约永远胜出。如果没有契约投票,sidecar 则回退到两个 free-model 投票者之间的一致性判断。
struct ModelProposal {
std::string label;
double confidence = 0.0;
};
std::string Route(const std::string& trace,
const ModelProposal& model,
const ModelProposal& shadow,
DriftLedger& ledger) {
const auto contract_vote = ContractVote(trace);
if (contract_vote) {
const bool model_agreed = model.label == *contract_vote;
ledger.Record(*contract_vote, model_agreed);
if (!model_agreed) return "quarantine";
return *contract_vote;
}
const double min_confidence = 0.85;
const bool model_confident = model.confidence >= min_confidence;
const bool shadow_confident = shadow.confidence >= min_confidence;
const bool labels_match = model.label == shadow.label;
if (labels_match && model_confident && shadow_confident) {
ledger.Record(model.label, true);
return model.label;
}
return "manual_review";
}
当契约弃权时,free server 选项作为影子投票者很有用。它将相同的规范化栈送入第二个服务实例。sidecar 将影子视为第二意见,而非 ground truth。如果契约和模型不一致,影子不能打破平局——契约胜出。如果契约没有意见,且两个 free 端点一致且置信度足够高,label 可以路由。如果它们不一致,案例进入人工审核。
一个可复现的路由决策测试
测试计划应从四个简单案例开始。下表显示了在任何网络调用之前预期的路由结果。
第一行是引发该事件的 bug。契约无需重试即可捕获它。第二行确认了快乐路径(happy path)。第三行和第四行展示当确定性 oracle 没有意见时的行为。
free server 解决不了什么
影子投票者不是独立的 ground truth。模型和影子可能共享相同的训练偏差,也可能共享相同的上游故障。两个错误的自信答案不会因为一致就变成一个正确答案。在本设计中,唯一不可妥协的检查是契约表。
MonkeyCode 的免费模型访问降低了运行影子投票者的成本,但没有移除对确定性门控的需求。sidecar 仍然需要隔离路径。账本仍然需要人工监控已知 bucket 的漂移率。
契约表应该保持窄小。只有在人工审核了真实崩溃特征后才能添加规则。太匆忙添加规则会产生误报,并侵蚀信任。置信度阈值是本地策略,不是产品承诺。在将其视为稳定数字之前,用标注数据对其调优。漂移账本在内存中,进程重启后窗口丢失。如果路由决策依赖于跨重启的历史,需要将其持久化。本文没有出现基准数字,因为端点限制未针对本文进行测量。
谁不应该使用这种方法
有严格延迟 SLO 的团队不应随意添加第二次网络投票。错误 label 会立即产生不可逆操作的系统,需要在路由前加入人工审批步骤。不能审核隔离队列的团队只会把故障转移到一个更安静的地方。安全关键、健康、金融和合规工作负载需要的控制比契约门控和漂移账本强得多。
干净的 JSON 响应从来不是安全护栏,契约门控才是。free model 可以廉价地提出 label 提案,free server 可以添加第二意见。但 sidecar 只有在确定性规则确认了已知案例,且漂移账本显示模型长期保持一致时,才能赢得路由信任。在扩大模型规模之前,先从一个小型契约表开始。