阶段四 · 模型部署与 Go 集成

数据飞轮与持续迭代

一句话总结

模型上线不是终点,而是迭代的起点. 线上数据是最好的训练资源--用户反馈, 误判样本, 新型违规模式,这些都能驱动模型越来越准.
本篇讲如何构建数据飞轮,让系统自我进化.

前置回顾

第 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 // Uncertain / UserReport / UserAppeal / Disagreement
HumanLabel *string // 人工标注后回填
}

type SampleSource int

const (
SourceUncertain SampleSource = iota // 模型不确定
SourceUserReport // 用户举报
SourceUserAppeal // 用户申诉
SourceDisagreement // 小模型与 LLM 不一致
)

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
}

// 小模型与 LLM 判断不一致
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,
// 申诉意味着用户认为不应该被拦截,这是一个隐式的 "safe" 标签
}
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 {
// 用户举报 + LLM 也判 block → 高置信度违规
return s.LLMResult != nil && s.LLMResult.Action == "block"
},
Label: "violation",
},
{
Source: SourceUserAppeal,
Condition: func(s Sample) bool {
// 用户申诉 + LLM 判 pass → 高置信度误判
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% 准确的. 推荐做法:

  1. LLM 置信度 > 0.9 的样本直接使用
  2. 置信度 0.7-0.9 的随机抽 20% 人工复核
  3. 置信度 < 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 // "v2.3.1"
CreatedAt time.Time
TrainingData struct {
Size int // 样本数
DateRange [2]time.Time
Sources []string // ["user_reports", "auto_labeled", "human_labeled"]
}
Metrics struct {
Accuracy float64
FalsePositive float64
FalseNegative float64
Latency_P99_ms float64
}
Status ModelStatus
ChangeLog string // "新增谐音变体样本 5000 条,FPR 从 1.2% 降至 0.7%"
}

自动化训练触发

什么时候该训练新版本?

type RetrainTrigger struct {
minNewSamples int // 最少新样本数
maxInterval time.Duration // 最长间隔
accuracyDrop float64 // 线上准确率下降阈值
}

func (t *RetrainTrigger) ShouldRetrain(stats SystemStats) (bool, string) {
// 条件 1: 积累了足够多新标注样本
if stats.NewLabeledSamples > t.minNewSamples {
return true, fmt.Sprintf("new samples: %d", stats.NewLabeledSamples)
}

// 条件 2: 距上次训练超过最大间隔
if time.Since(stats.LastTrainTime) > t.maxInterval {
return true, "max interval exceeded"
}

// 条件 3: 线上指标恶化
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 // "5min" / "2h" / "3days"
}

type UpdateType int

const (
UpdateRules UpdateType = iota // 改规则,5 分钟生效
UpdateThreshold // 改阈值,即时生效
UpdateModel // 重训模型,3-7 天周期
UpdateBoth // 规则先顶住,同时启动重训
)

规则先行,模型跟进

发现新型违规时的 SOP:

  1. 立即更新规则引擎,先把已知变体拦住(5 分钟)
  2. 同时收集该类型样本,积累到足够数量
  3. 启动模型重训,覆盖泛化场景(3-7 天)
  4. 新模型上线后,移除临时规则(规则引擎不需要兜底了)

反馈闭环设计

完整的闭环路径:

用户举报                    人工审核结论
│ │
▼ ▼
┌─────────────────────────────────────┐
│ 样本数据库 (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 // "easy" / "medium" / "hard"
Category string // "toxic" / "spam" / "sarcasm" / "normal"
}

// 评估新模型
func EvaluateModel(model *Model, evalSet *EvalDataset) EvalReport {
var correct, total int
var fp, fn int // false positive, false negative

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% 流量 │
│ - 归档旧版本 │
└──────────────┘

流水线配置

# ml-pipeline.yaml
name: moderation-model-retrain
trigger:
schedule: "0 2 * * 1" # 每周一凌晨 2 点
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

何时模型已经够好

模型不需要无限迭代. 判断"够好"的标准:

  1. 准确率 > 97%误封率 < 0.5% -- 大多数场景可以接受
  2. 漏放的违规内容 < 2% 且都是轻微违规(不是严重暴力/诈骗)
  3. 人工复审队列稳定在低水位(日均 < 50 条)
  4. 用户申诉率稳定下降且无新增投诉类型

达到这个水平后,迭代频率可以从每周降为每月甚至更长. 把精力放在新业务需求上.

系列总结

从第 1 篇 NLP 全景图到这里,走过了一条完整路径:

  1. 理解 NLP 基础概念(分词, 词向量, 语言模型)
  2. 动手训练分类模型(数据准备, 微调, 评估)
  3. 模型导出与推理优化(ONNX, 量化)
  4. 生产级系统设计(推理服务, Go 集成, 四层架构, 数据飞轮)

作为 Go 后端开发者,不需要成为 ML 研究员. 但理解模型的能力边界, 掌握系统集成的最佳实践, 能够设计数据闭环让系统持续进化--这些能力让你能独立负责一个 AI 驱动的业务系统.

快速回顾

  • 数据飞轮: 线上数据 → 标注 → 训练 → 上线 → 收集更多数据,越转越快
  • 收集策略: 不确定样本, 用户举报, 用户申诉, 模型分歧,四类高价值数据
  • 标注分层: 自动(规则) → LLM 辅助 → 人工,按成本递增选择
  • 训练触发: 新样本数, 时间间隔, 线上指标下降,三个条件任一满足即触发
  • 规则 vs 重训: 已知模式用规则先顶住,泛化能力不够时再训模型
  • 评估集隔离: 评估集是黄金标准,绝不混入训练数据
  • MLOps 流水线: 数据准备 → 训练 → 评估 → Canary → 全量,全自动化

动手练习

  1. 样本收集器: 实现 SampleCollector,对模型不确定的样本按 10% 比例采样存储
  2. 用户反馈接口: 实现举报和申诉的 HTTP 接口,将样本写入数据库
  3. LLM 批量标注: 写一个脚本,读取未标注样本,调用 LLM API 批量打标签,按置信度分桶
  4. 评估脚本: 实现 EvaluateModel,对比新旧模型在评估集上的准确率, FPR, FNR
  5. 重训触发器: 实现 RetrainTrigger,接入 Prometheus 指标,满足条件时发送通知
  6. 全流程演练: 从收集样本 → 标注 → 训练 → 导出 ONNX → 部署到 Triton → A/B 测试,走完一圈完整的飞轮