一句话总结
模型上线不是终点,而是迭代的起点. 线上数据是最好的训练资源--用户反馈, 误判样本, 新型违规模式,这些都能驱动模型越来越准.
本篇讲如何构建数据飞轮,让系统自我进化.
前置回顾
第 18 篇完成了端到端系统设计:规则 → 小模型 → LLM → 人工复审的四层架构.
现在回答最后一个问题:系统上线后,怎么持续变好?
数据飞轮的概念
┌─────────────────────────────────────────────────────┐ │ 数据飞轮 │ │ │ │ 收集样本 ──────→ 标注数据 │ │ ↑ │ │ │ │ ▼ │ │ 线上推理 ←────── 训练新模型 │ │ │ ↑ │ │ │ │ │ │ └──→ 评估效果 ──────┘ │ │ │ │ 每转一圈: 数据更多 → 模型更准 → 收集更多好数据 │ └─────────────────────────────────────────────────────┘
|
飞轮的核心思想: 模型上线后产生的数据(预测结果 + 用户反馈)可以反过来改进模型本身. 循环转得越快,模型进化越快.
数据飞轮 = 搜索引擎的排名算法. 搜索结果越好 → 用户越多 → 点击行为数据越多 → 排名算法越准 → 搜索结果更好. 同理:审核模型越准 → 误判越少 → 用户申诉越少 → 人工标注的样本质量越高 → 模型更准.
在线样本收集
收集什么
不是所有线上数据都有价值. 重点收集这几类:
| 类型 |
来源 |
价值 |
| 模型不确定的样本 |
置信度在 0.4-0.7 之间的预测 |
模型最需要学习的决策边界 |
| 用户举报的消息 |
用户点击"举报"按钮 |
真实违规样本,自带标签 |
| 用户申诉的消息 |
被拦截后用户点击"申诉" |
误判样本,修正模型偏差 |
| LLM 与小模型不一致 |
小模型判 pass 但 LLM 判 block |
小模型的能力盲区 |
| 新型违规模式 |
人工复审发现的新变体 |
对抗性样本,提升鲁棒性 |
Go 侧样本采集
type SampleCollector struct { store SampleStore sampler *Sampler }
type Sample struct { ID string Text string GroupID string Timestamp time.Time ModelPrediction ModelResult LLMResult *LLMResult Source SampleSource HumanLabel *string }
type SampleSource int
const ( SourceUncertain SampleSource = iota SourceUserReport SourceUserAppeal SourceDisagreement )
func (c *SampleCollector) MaybeCollect(ctx context.Context, msg *Message, result ModelResult, llmResult *LLMResult) { source := c.determineSource(result, llmResult) if source == -1 { return }
if source == SourceUncertain && !c.sampler.ShouldSample() { return }
sample := Sample{ ID: msg.ID, Text: msg.Text, GroupID: msg.GroupID, Timestamp: time.Now(), ModelPrediction: result, LLMResult: llmResult, Source: source, }
go c.store.Save(context.Background(), sample) }
func (c *SampleCollector) determineSource(result ModelResult, llmResult *LLMResult) SampleSource { maxScore := float32(0) for _, s := range result.Scores { if s > maxScore { maxScore = s } } if maxScore > 0.4 && maxScore < 0.7 { return SourceUncertain }
if llmResult != nil && result.Action.String() != llmResult.Action { return SourceDisagreement }
return -1 }
|
用户反馈接口
func (m *Moderator) HandleReport(ctx context.Context, req ReportRequest) error { sample := Sample{ ID: req.MessageID, Text: req.MessageText, GroupID: req.GroupID, Timestamp: time.Now(), Source: SourceUserReport, } return m.collector.store.Save(ctx, sample) }
func (m *Moderator) HandleAppeal(ctx context.Context, req AppealRequest) error { sample := Sample{ ID: req.MessageID, Text: req.MessageText, GroupID: req.GroupID, Timestamp: time.Now(), Source: SourceUserAppeal, } if err := m.collector.store.Save(ctx, sample); err != nil { return err } return m.recallBlock(ctx, req.MessageID) }
|
标注管线
收集到的样本需要标注才能用于训练. 三种标注方式按成本递增排列:
1. 自动标注(成本最低)
部分样本可以自动打标签:
type AutoLabeler struct { rules []LabelRule }
type LabelRule struct { Source SampleSource Condition func(Sample) bool Label string }
var defaultRules = []LabelRule{ { Source: SourceUserReport, Condition: func(s Sample) bool { return s.LLMResult != nil && s.LLMResult.Action == "block" }, Label: "violation", }, { Source: SourceUserAppeal, Condition: func(s Sample) bool { return s.LLMResult != nil && s.LLMResult.Action == "pass" }, Label: "safe", }, }
|
2. LLM 辅助标注(中等成本)
对于无法自动标注的样本,用 LLM 批量标注:
import json from openai import OpenAI
client = OpenAI()
LABEL_PROMPT = """对以下群聊消息进行内容审核标注. 输出 JSON: {"label": "safe"|"toxic"|"spam"|"ad"|"scam", "confidence": 0.0-1.0, "reason": "..."}
消息: {text} """
def batch_label(samples: list[dict]) -> list[dict]: results = [] for sample in samples: resp = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": LABEL_PROMPT.format(text=sample["text"])}], response_format={"type": "json_object"}, ) label_result = json.loads(resp.choices[0].message.content) sample["auto_label"] = label_result["label"] sample["label_confidence"] = label_result["confidence"] results.append(sample) return results
|
LLM 标注的可靠性
LLM 标注不是 100% 准确的. 推荐做法:
- LLM 置信度 > 0.9 的样本直接使用
- 置信度 0.7-0.9 的随机抽 20% 人工复核
- 置信度 < 0.7 的全部走人工标注
3. 人工标注(最高质量)
低置信度样本和 LLM 无法判断的边缘案例由人工标注. 标注界面需要展示:
┌─────────────────────────────────────────────────────┐ │ 标注任务 #12345 │ │ │ │ 消息: "这个群主真是个人才,天天发这些东西" │ │ 群名: 二手交易群 │ │ 上下文: │ │ [群主]: 今日推荐:XX保健品,原价899现价99 │ │ [群主]: 有需要的私信我 │ │ [用户A]: 这个群主真是个人才,天天发这些东西 ← 当前 │ │ │ │ 模型预测: toxic(0.62) / safe(0.38) │ │ LLM 判断: safe (讽刺但不违规) │ │ │ │ 标注选项: [safe] [toxic] [spam] [需要更多上下文] │ └─────────────────────────────────────────────────────┘
|
模型版本管理 (MLOps)
版本生命周期
模型版本状态流转:
training → validating → staging → canary → production → deprecated → archived │ │ │ │ │ │ │ 验证集 Shadow 10%流量 全量 停用 │ 评估 测试(不影响 A/B测试 服务 下线 │ 线上决策) │ └── 训练失败 → failed
|
版本元数据
type ModelVersion struct { Version string CreatedAt time.Time TrainingData struct { Size int DateRange [2]time.Time Sources []string } Metrics struct { Accuracy float64 FalsePositive float64 FalseNegative float64 Latency_P99_ms float64 } Status ModelStatus ChangeLog string }
|
自动化训练触发
什么时候该训练新版本?
type RetrainTrigger struct { minNewSamples int maxInterval time.Duration accuracyDrop float64 }
func (t *RetrainTrigger) ShouldRetrain(stats SystemStats) (bool, string) { if stats.NewLabeledSamples > t.minNewSamples { return true, fmt.Sprintf("new samples: %d", stats.NewLabeledSamples) }
if time.Since(stats.LastTrainTime) > t.maxInterval { return true, "max interval exceeded" }
if stats.CurrentAccuracy < stats.BaselineAccuracy-t.accuracyDrop { return true, fmt.Sprintf("accuracy dropped: %.2f → %.2f", stats.BaselineAccuracy, stats.CurrentAccuracy) }
return false, "" }
|
训练还是更新规则?
不是所有问题都需要重新训练模型. 判断标准:
| 场景 |
解决方案 |
理由 |
| 新出现的违规关键词 |
更新规则引擎 |
关键词匹配比模型更快更确定 |
| 新的谐音/变体(大量) |
重训模型 |
变体太多规则写不完,需要模型泛化 |
| 某类误判(阈值问题) |
调整阈值 |
不需要重训,只需要调决策边界 |
| 全新违规类型(如 AI 生成诈骗) |
重训模型 + 新标签 |
现有模型没见过这类样本 |
| 特定群的特殊规则 |
群级配置 |
比如技术群允许贴代码链接 |
type UpdateDecision struct { Type UpdateType Reason string Effort string }
type UpdateType int
const ( UpdateRules UpdateType = iota UpdateThreshold UpdateModel UpdateBoth )
|
规则先行,模型跟进
发现新型违规时的 SOP:
- 立即更新规则引擎,先把已知变体拦住(5 分钟)
- 同时收集该类型样本,积累到足够数量
- 启动模型重训,覆盖泛化场景(3-7 天)
- 新模型上线后,移除临时规则(规则引擎不需要兜底了)
反馈闭环设计
完整的闭环路径:
用户举报 人工审核结论 │ │ ▼ ▼ ┌─────────────────────────────────────┐ │ 样本数据库 (Sample Store) │ │ │ │ raw_text | source | auto_label | │ │ human_label | model_version | │ │ created_at | used_in_training │ └──────────────────┬──────────────────┘ │ ┌─────────┴─────────┐ ▼ ▼ ┌──────────────┐ ┌──────────────┐ │ 训练数据构建 │ │ 评估数据集 │ │ (80%) │ │ (20%) │ └──────┬───────┘ └──────┬───────┘ │ │ ▼ ▼ ┌──────────────┐ ┌──────────────┐ │ 模型训练 │ │ 离线评估 │ └──────┬───────┘ └──────┬───────┘ │ │ ▼ ▼ ┌──────────────────────────────────┐ │ 新版本模型上线 A/B 测试 │ └──────────────────┬───────────────┘ │ ▼ 线上指标对比 (全量 or 回滚)
|
评估数据集管理
评估集是"黄金标准",用来衡量模型是否真的变好了:
type EvalDataset struct { Name string Samples []EvalSample UpdatedAt time.Time }
type EvalSample struct { Text string Label string Difficulty string Category string }
func EvaluateModel(model *Model, evalSet *EvalDataset) EvalReport { var correct, total int var fp, fn int
for _, sample := range evalSet.Samples { prediction := model.Predict(sample.Text) total++
if prediction == sample.Label { correct++ } else if prediction == "violation" && sample.Label == "safe" { fp++ } else if prediction == "safe" && sample.Label != "safe" { fn++ } }
return EvalReport{ Accuracy: float64(correct) / float64(total), FalsePositive: float64(fp) / float64(total), FalseNegative: float64(fn) / float64(total), } }
|
评估集泄露
评估集的样本绝对不能混入训练集. 否则模型会"背答案",评估结果虚高但实际效果没提升. 每次构建训练数据时用 ID 去重确认.
自动化流水线 (CI/CD for ML)
触发条件满足 │ ▼ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ 1. 数据准备 │────→│ 2. 模型训练 │────→│ 3. 离线评估 │ │ - 拉取新样本 │ │ - 微调/全量 │ │ - 跑评估集 │ │ - 清洗去重 │ │ - 保存 ckpt │ │ - 对比 baseline│ │ - 划分 train/ │ │ - 导出 ONNX │ │ - 生成报告 │ │ eval │ └──────────────┘ └───────┬──────┘ └──────────────┘ │ │ 指标达标? ┌────────┴────────┐ │ Yes │ No ▼ ▼ ┌──────────────┐ 通知负责人 │ 4. 部署 Canary │ 人工介入 │ - 上传模型 │ │ - 10% 流量 │ │ - 观察 3 天 │ └───────┬──────┘ │ 线上指标达标? ▼ ┌──────────────┐ │ 5. 全量上线 │ │ - 100% 流量 │ │ - 归档旧版本 │ └──────────────┘
|
流水线配置
name: moderation-model-retrain trigger: schedule: "0 2 * * 1" manual: true condition: new_samples_min: 5000 accuracy_drop_threshold: 0.02
stages: prepare_data: script: scripts/prepare_training_data.py params: sample_sources: ["user_reports", "appeals", "uncertain", "disagreement"] max_samples: 50000 eval_split: 0.2
train: script: scripts/train.py params: base_model: "distilbert-base-multilingual-cased" epochs: 5 batch_size: 32 learning_rate: 2e-5 resources: gpu: 1
evaluate: script: scripts/evaluate.py params: eval_dataset: "golden_eval_set_v3" baseline_model: "production_current" gates: accuracy_min: 0.95 fpr_max: 0.01 fnr_max: 0.05
deploy_canary: script: scripts/deploy.py params: target: "triton" traffic_percent: 10 observe_days: 3
promote: script: scripts/promote.py requires: manual_approval
|
何时模型已经够好
模型不需要无限迭代. 判断"够好"的标准:
- 准确率 > 97% 且 误封率 < 0.5% -- 大多数场景可以接受
- 漏放的违规内容 < 2% 且都是轻微违规(不是严重暴力/诈骗)
- 人工复审队列稳定在低水位(日均 < 50 条)
- 用户申诉率稳定下降且无新增投诉类型
达到这个水平后,迭代频率可以从每周降为每月甚至更长. 把精力放在新业务需求上.
系列总结
从第 1 篇 NLP 全景图到这里,走过了一条完整路径:
- 理解 NLP 基础概念(分词, 词向量, 语言模型)
- 动手训练分类模型(数据准备, 微调, 评估)
- 模型导出与推理优化(ONNX, 量化)
- 生产级系统设计(推理服务, Go 集成, 四层架构, 数据飞轮)
作为 Go 后端开发者,不需要成为 ML 研究员. 但理解模型的能力边界, 掌握系统集成的最佳实践, 能够设计数据闭环让系统持续进化--这些能力让你能独立负责一个 AI 驱动的业务系统.
快速回顾
- 数据飞轮: 线上数据 → 标注 → 训练 → 上线 → 收集更多数据,越转越快
- 收集策略: 不确定样本, 用户举报, 用户申诉, 模型分歧,四类高价值数据
- 标注分层: 自动(规则) → LLM 辅助 → 人工,按成本递增选择
- 训练触发: 新样本数, 时间间隔, 线上指标下降,三个条件任一满足即触发
- 规则 vs 重训: 已知模式用规则先顶住,泛化能力不够时再训模型
- 评估集隔离: 评估集是黄金标准,绝不混入训练数据
- MLOps 流水线: 数据准备 → 训练 → 评估 → Canary → 全量,全自动化
动手练习
- 样本收集器: 实现
SampleCollector,对模型不确定的样本按 10% 比例采样存储
- 用户反馈接口: 实现举报和申诉的 HTTP 接口,将样本写入数据库
- LLM 批量标注: 写一个脚本,读取未标注样本,调用 LLM API 批量打标签,按置信度分桶
- 评估脚本: 实现
EvaluateModel,对比新旧模型在评估集上的准确率, FPR, FNR
- 重训触发器: 实现
RetrainTrigger,接入 Prometheus 指标,满足条件时发送通知
- 全流程演练: 从收集样本 → 标注 → 训练 → 导出 ONNX → 部署到 Triton → A/B 测试,走完一圈完整的飞轮