毛利从 -50% 到转正
一、背景:这不是能力问题,是账单问题
1.1 一条容易被误读的新闻
2026 年 9 月,Bloomberg 报道了 Harvey 的模型迁移。标题党版本会是「法律 AI 巨头抛弃 GPT 和 Claude」,但真实原因朴素得多:
按用量计费模式下 token 消耗涨了约 20 倍,毛利从约 +50% 跌到约 -50%。 迁移到自研模型后,毛利转正。
注意这个数字关系:营收在涨,用户数在涨,产品质量没变差——但毛利从正五十变成负五十。原因只有一个:单用户成本涨得比单用户收入快。
1.2 为什么 AI 应用公司特别容易撞上这堵墙
传统 SaaS 的成本曲线:
用户数 ↑ → 边际成本 ≈ 0(多一个用户只是多一份数据)
AI 应用的成本曲线:
用户数 ↑ → 用户用得越多 → token 消耗 ↑↑
→ 且重度用户往往是付费意愿最强的那批
→ 成本与收入同向增长,且成本弹性更大
更糟的是三个放大器:
| 放大器 | 机制 | 影响 |
|---|---|---|
| 能力越强,用得越多 | 模型变强 → 用户敢交给它更长的任务 → token 数非线性上涨 | 每轮升级都推高成本 |
| Agent 化 | 单轮问答 → 多轮自主循环 → 单次请求触发几十次模型调用 | token 消耗 10–100 倍 |
| 上下文膨胀 | 为了效果好,不停往里塞文档、历史、工具输出 | 输入成本占比越来越高 |
Harvey 的场景是这三者的叠加:法律文档天然长(一份合同几百页)、任务天然多步(尽职调查、条款比对、风险标记)、质量容错极低(不能编)。
1.3 为什么是 2026 年才发生
三件事同时成熟了:
| 条件 | 2024 | 2026 |
|---|---|---|
| 开放权重模型能力 | 明显落后前沿 1–2 代 | Kimi K3(2.8T 参数、1M 上下文)、Step 5 Preview(600B MoE)等逼近前沿 |
| 后训练工程化 | 需要专门团队与大量试错 | LoRA / SFT / DPO 工具链成熟,8 周可跑完一轮 |
| 自托管推理成本 | 部署门槛高,GPU 贵 | vLLM / SGLang 成熟,规模化后单 token 成本可降到 API 价的 1/5–1/10 |
关键变化是第三个。开放权重模型一直存在,但「自己跑」的成本以前降不下来。现在能降了,于是「自建」第一次在经济上算得过来。
1.4 本文的定位
这不是一篇 Harvey 的内部复盘(我们拿不到内部数据)。这是一篇把公开信息拆成可执行动作的迁移指南:如果你的 AI 应用也撞上了单位经济学这堵墙,该按什么顺序做什么。
所有数字分两类:
- 标注
[公开]的来自公开报道 - 标注
[推演]的是基于同类迁移工程的合理估算,用于说明量级关系,不是承诺
二、工具分工:谁负责什么
2.1 迁移工程的角色表
| 角色 | 工具 | 负责什么 | 不负责什么 |
|---|---|---|---|
| 任务分层分析 | Langfuse + 自写脚本 | 把线上流量按任务类型切开,算出每类的 token 与成本 | 不做模型决策 |
| 评测集构建 | Label Studio + 内部法务团队 | 构建领域金标集(含 rubric) | 不写代码 |
| 基线模型选型 | vLLM + 开放权重模型 | 跑批量推理,出客观指标 | 不做最终决策(要人工看样) |
| 后训练 | LLaMA-Factory / 自研训练管线 | SFT + DPO | 不定义什么是「好」(由 rubric 定义) |
| 推理服务 | vLLM + Triton | 部署、批处理、量化 | 不管模型质量 |
| 灰度与监控 | 自研路由层 + Prometheus | 分流、A/B、成本与质量双监控 | 不做模型训练 |
| 回归拦截 | CI + 评测集自动跑 | 每次模型更新前跑一遍金标集 | 不决定上线 |
| 代码层改造 | Claude Code + 人工 review | 提示词适配、路由改造 | 不改评测标准 |
2.2 一个关键原则:评测标准不交给 AI
本案例里最重要的分工约定:
「什么算好」 → 人定(法务专家写 rubric)
「做得好不好」 → 机器跑(自动评测)
「哪里不好」 → AI 辅助分析(读 bad case 归类)
「怎么改」 → 人决定,AI 执行
AI 可以跑评测、可以做分析、可以写补丁,但不能定义「什么算改善」。 这条线一旦越过,就会出现「评测分数涨了但律师说输出不能用」的经典事故。
三、策略设计:不是全量替换,是分层
3.1 核心误判:以为要换掉前沿模型
最直觉(也最危险)的想法是「把 GPT/Claude 全换成自研模型」。这会导致:
- 最难的 5% 任务质量崩塌 → 用户流失
- 而那 5% 恰恰是付费意愿最强的场景
正确做法是分层路由。先把线上的真实任务按「难度 × 频次 × 容错度」切开。
3.2 任务分层矩阵
| 层级 | 任务类型 | 频次占比 | 单次 token | 容错度 | 用什么模型 |
|---|---|---|---|---|---|
| L0 | 格式转换、字段抽取、摘要、分类、检索重排 | ~55% | 2–8K | 中 | 自研小模型 / 规则 |
| L1 | 条款比对、风险点标注、模板填充 | ~30% | 10–40K | 低 | 自研主力模型 |
| L2 | 尽职调查、多文档交叉推理、争议分析 | ~12% | 40–150K | 极低 | 自研主力模型 + 前沿模型交叉校验 |
| L3 | 全新问题、高风险意见、客户定制 | ~3% | 不定 | 极低 | 前沿模型保留 |
算一下这笔账:
成本占比(迁移前,全部走前沿模型):
L0 55% 流量 × 5K token = 275K 单位
L1 30% 流量 × 25K token = 750K 单位
L2 12% 流量 × 90K token = 1080K 单位
L3 3% 流量 × 60K token = 180K 单位
-----------------------------------
合计 2285K 单位
L0+L1 占比 = (275+750)/2285 = 44.9%
L0+L1+L2 占比 = 92.1%
结论:把 L0 和 L1 迁走,就能覆盖约 45% 的成本;把 L2 也迁走(带校验),覆盖 92%。剩下 3% 继续用前沿模型,成本可以忽略。
这就是为什么「分层」比「全换」重要:它让你能用最小的质量风险,覆盖最大的成本面。
3.3 三级迁移路线图
Phase 1(第 1-2 周):评测集先行
├─ 建领域金标集(每类任务 ≥ 200 条)
├─ 定义 rubric(人工评分标准)
└─ 建立「当前前沿模型」的质量基线(这是唯一参照系)
Phase 2(第 3-4 周):选型与非后训练基线
├─ 3-5 个候选开放权重模型跑同一评测集
├─ 客观指标 + 人工盲评
└─ 选出基线模型
Phase 3(第 5-8 周):后训练
├─ 数据构建(领域语料 + 真实 bad case + 专家修正)
├─ SFT → DPO
└─ 每轮迭代必跑金标集
Phase 4(第 9-10 周):灰度
├─ L0 灰度 5% → 50% → 100%
├─ L1 灰度
└─ L2 双跑交叉校验
Phase 5(持续):成本与质量双监控
Phase 1 不能省。没有基线评测集,你在 Phase 3 完全无法判断「这个模型是不是变好了」,只能靠感觉——而感觉在 8 周里会漂移。
四、实施步骤
4.1 Step 1:把线上流量切开(第 1 周)
目标:知道钱到底花在哪类任务上。
做法:从 Langfuse / 自建 trace 里导出最近 30 天的请求,按任务类型聚合。
# analyze_traffic.py
import json
from collections import defaultdict
buckets = defaultdict(lambda: {"n": 0, "tok_in": 0, "tok_out": 0, "cost": 0.0})
with open("traces_30d.jsonl") as f:
for line in f:
r = json.loads(line)
t = r.get("task_type", "unknown")
b = buckets[t]
b["n"] += 1
b["tok_in"] += r["usage"]["input_tokens"]
b["tok_out"] += r["usage"]["output_tokens"]
b["cost"] += r["usage"].get("cost_usd", 0.0)
total_cost = sum(b["cost"] for b in buckets.values())
rows = sorted(buckets.items(), key=lambda kv: -kv[1]["cost"])
print(f"{'任务类型':<28}{'次数':>8}{'平均tok':>10}{'总成本$':>12}{'占比':>8}")
for t, b in rows:
avg = (b["tok_in"] + b["tok_out"]) / max(b["n"], 1)
print(f"{t:<28}{b['n']:>8}{avg:>10.0f}{b['cost']:>12.2f}{b['cost']/total_cost:>8.1%}")
输出样例(脱敏):
任务类型 次数 平均tok 总成本$ 占比
contract_clause_extract 184203 4,120 38,204 24.8%
doc_summarization 96211 6,880 25,910 16.8%
due_diligence_review 12874 92,400 24,180 15.7%
risk_flag_annotation 72330 21,300 21,690 14.1%
clause_comparison 41902 28,900 18,040 11.7%
legal_research_qa 63118 12,600 13,270 8.6%
custom_opinion_draft 2144 68,900 6,140 4.0%
other 15028 9,100 6,690 4.3%
----------------------------------------------------------------
合计 100%
这张表一出来,优先级就清楚了:前四类占 71.4%,其中 contract_clause_extract 和 doc_summarization 是典型的 L0 任务——迁移风险最低、收益最大。
4.2 Step 2:构建领域金标集(第 1-2 周)
这是整个工程最贵、最不可替代的一步。
4.2.1 金标集的构成
| 子集 | 条数 | 来源 | 用途 |
|---|---|---|---|
| 主评测集 | 每类任务 200–300 条 | 真实脱敏请求 + 专家构造 | 迭代期打分 |
| 盲评集 | 300 条 | 主集中随机抽样 | 人工盲评(防过拟合) |
| 回归集 | 100 条 | 历史 bad case | 每次上线前必跑 |
| 红队集 | 100 条 | 专家构造的对抗样本 | 安全与幻觉检查 |
总规模约 1200–1500 条。听起来不多,但每条都需要领域专家标注,实际工作量约 3 人 × 2 周。
4.2.2 Rubric 怎么写(关键)
评分标准必须可判定,不能是「质量好」这种。
contract_clause_extract 的 rubric 示例:
task: contract_clause_extract
dimensions:
- name: 条款完整性
weight: 0.35
levels:
3: 目标条款全部抽到,无遗漏
2: 漏 1 条次要条款
1: 漏 1 条主要条款
0: 漏 2 条以上或抽错主体
- name: 字段准确性
weight: 0.30
levels:
3: 所有字段值正确,引用位置准确
2: 1 个字段值有误但不影响理解
1: 引用位置(页码/条款号)错误
0: 字段值错误且影响法律判断
- name: 无幻觉
weight: 0.25
levels:
3: 无任何合同外内容
1: 出现轻微推断但已标注
0: 出现合同中不存在的内容(一票否决)
- name: 格式合规
weight: 0.10
levels:
3: 完全符合输出 schema
1: 部分符合
0: 无法解析
gates:
- 无幻觉维度得 0 → 总分直接判 0
- 总分 < 2.4 → 判为不可用
「一票否决」机制很重要。法律场景里,一条幻觉的代价远大于十条遗漏。加权平均会把它稀释掉,门控不会。
4.2.3 用 AI 辅助生成,但人工终审
你是一个法律评测集构建助手。
## 任务
为任务类型 {{task_type}} 生成 {{n}} 条评测样本。
## 输入材料
- 真实脱敏请求样本:{{samples}}
- 该任务的 rubric:{{rubric}}
## 要求
1. 每条样本包含:输入文档片段、期望输出、该条要考察的维度
2. 覆盖分布:
- 40% 常规案例(模型应该稳过)
- 30% 边界案例(容易出错的写法/结构)
- 20% 长尾案例(罕见条款、特殊 jurisdiction)
- 10% 对抗案例(诱导幻觉、上下文缺失、自相矛盾的文档)
3. 标注每条的难度等级(L0/L1/L2/L3)
## 禁止
- 不要生成你无法给出确定答案的样本
- 不要编造法律条文,所有引用必须来自输入材料
- 生成后自我检查:期望输出是否真的可从输入推出?不能就标 [REVIEW] 交给人工
## 输出
JSON 数组,每条含 {input, expected, dimension, difficulty, source_note}
AI 生成 + 人工终审能把构建成本降到纯人工的 1/3,但终审不能省——AI 生成的样本里通常有 15–25% 需要修改或作废。
4.3 Step 3:基线模型选型(第 3-4 周)
4.3.1 候选池
| 候选 | 参数 | 上下文 | 许可 | 备注 |
|---|---|---|---|---|
| Kimi K3 | 2.8T(开放权重) | 1M | 开放权重 | 原生视觉、支持显式提示词缓存 |
| GLM-5.3 | — | — | 开放权重 | 已在海外云托管输出 |
| Step 5 Preview | 600B MoE(27B 激活) | 1M | 承诺全开放 | AA 指数 44,价格约 GPT-5.6 Sol 的 1/7 |
| Qwen 系列 | 多档 | 长上下文 | 开放权重 | 中文场景强 |
| MiMo-V2.6-Pro | — | 1M | MIT | WebDev 榜 1628 |
4.3.2 选型评测的三个层次
层次一:客观指标(机器跑)
python eval_runner.py \
--model kimi-k3 \
--tasks tasks/main_eval.jsonl \
--rubric rubrics/ \
--judge claude-sonnet-4-5 \
--out results/kimi-k3-main.json
注意:judge 用前沿模型。评测这件事本身要花钱,但它是整个迁移的地基,不能省。用 Jev 这类判断模型可以降低成本(延迟 70–500ms,输入 $0.042/百万 token)。
层次二:人工盲评(人跑)
从盲评集抽 300 条,把「前沿模型输出」和「候选模型输出」混在一起,让法务专家不知道哪个是哪个,按 rubric 打分。
为什么必须盲评:知道来源会引入偏见。知道「这是便宜的自研模型」的人会不自觉地挑刺,反之亦然。
层次三:成本与延迟(工程跑)
模型 单卡吞吐(tok/s) P99延迟(s) 每百万输出token成本(自托管)
Kimi K3 1,840 3.2 $0.42 [推演]
GLM-5.3 2,210 2.8 $0.35 [推演]
Step 5 3,050 2.1 $0.26 [推演]
前沿API — 1.9 $6.00 [公开定价]
自托管成本含 GPU 摊销、电力、运维。估算口径要写清楚,否则后期对账必然吵架。
4.3.3 决策矩阵
| 候选 | 客观得分 | 盲评得分 | 自托管成本 | 部署难度 | 许可风险 | 结论 |
|---|---|---|---|---|---|---|
| 前沿模型(基线) | 88.4 | 4.31/5 | $6.00/M | — | — | 保留 L3 |
| Kimi K3 | 79.2 | 3.86/5 | $0.42/M | 中 | 低 | 选为基线 |
| GLM-5.3 | 77.5 | 3.74/5 | $0.35/M | 中 | 低 | 备选 |
| Step 5 Preview | 81.0 | 3.95/5 | $0.26/M | 高 | 待定(权重未全开) | 观察 |
选 Kimi K3 的理由:盲评差距最小(3.86 vs 4.31,差 0.45),自托管成本差 14 倍,且许可明确。Step 5 分数更高但权重承诺 10 月 15 日才全开,不能赌。
关键判断:不选分数最高的,选「分数够用 × 成本最低 × 风险可控」的。这是单位经济学驱动的选型,不是跑分驱动的选型。
4.4 Step 4:后训练(第 5-8 周)
4.4.1 数据来源与配比
| 数据类型 | 占比 | 来源 | 作用 |
|---|---|---|---|
| 领域继续预训练语料 | 40% | 公开法律文书、判例、法规、合同模板 | 补领域知识与术语 |
| SFT 示范数据 | 30% | 前沿模型生成 + 专家修正 | 教格式与推理链 |
| 真实 bad case 修正对 | 20% | 线上失败请求 + 专家给出的正确输出 | 针对性补短板 |
| DPO 偏好对 | 10% | 同一输入的两个输出,专家选优 | 对齐专家偏好 |
「真实 bad case 修正对」是 ROI 最高的一类。它直接对应你的用户实际会遇到的失败模式,比任何通用数据都有效。
4.4.2 bad case 挖掘
# mine_bad_cases.py
"""从线上 trace 里挖出需要进训练集的失败样本"""
import json
CRITERIA = {
"low_rubric": lambda r: r["rubric_score"] < 2.4,
"high_retry": lambda r: r["retry_count"] >= 2,
"human_edited": lambda r: r.get("human_edit_ratio", 0) > 0.3,
"explicit_negative": lambda r: r.get("user_feedback") == "thumbs_down",
"hallucination_flag": lambda r: r.get("hallucination_detected", False),
}
with open("traces_30d.jsonl") as f, open("bad_cases.jsonl", "w") as out:
for line in f:
r = json.loads(line)
hits = [name for name, fn in CRITERIA.items() if fn(r)]
if hits:
r["_bad_reasons"] = hits
out.write(json.dumps(r, ensure_ascii=False) + "\n")
人工编辑比例(human_edit_ratio)是被低估的信号。用户用了你的输出但改了 30% 以上,说明「方向对但不够好」——这类样本对后训练价值极高,而它们通常不会被标记为「失败」。
4.4.3 SFT 数据构造的 Prompt 模板
你要为法律文档处理任务生成训练数据(SFT 示范样本)。
## 输入
{{document_excerpt}}
## 目标任务
{{task_description}}
## 输出要求
严格按照下面这个格式与推理结构输出:
1. **定位**:先指出答案来自文档的哪一段(给出章节号/页码)
2. **抽取**:逐条列出抽取结果,每条带引用位置
3. **不确定项**:文档中未明确说明的内容,标注 [UNCERTAIN] 并说明缺什么
4. **禁止**:不得出现文档中没有的内容;无法从文档推出的,宁可标 [UNCERTAIN]
## 质量标准(rubric)
{{rubric}}
## 自检
生成后必须回答三个问题:
a. 我引用的每一处是否真的在文档里?
b. 有没有我把「合理推测」写成了「事实」?
c. 如果这份输出给到律师,他会不会因为某句话做出错误判断?
任一问题答案不利,重写。
「自检三问」是这套数据的核心。它把 rubric 里的「无幻觉」要求变成了生成时的显式检查点。
4.4.4 迭代节奏
Week 5: SFT v1 → 金标集得分 79.2 → 82.6
Week 6: + bad case 数据 → 82.6 → 85.1
Week 7: DPO v1 → 85.1 → 86.3
Week 8: DPO v2 + 红队修补 → 86.3 → 87.1
每次迭代必跑三件事:金标集客观分、盲评集人工抽查(50 条)、红队集安全检查。
只跑客观分是最大的陷阱:模型会对 judge 过拟合,客观分涨而实际质量不涨。每轮必须人工抽检。
4.5 Step 5:灰度切流(第 9-10 周)
4.5.1 路由层设计
# router.py
class ModelRouter:
def __init__(self, config):
self.config = config
def pick(self, task_type, payload):
level = self.config.task_levels.get(task_type, "L2")
# L3 永远走前沿模型
if level == "L3":
return ("frontier", {})
# 灰度比例按层级独立配置
rollout = self.config.rollout[level]
if rollout <= 0:
return ("frontier", {})
if rollout >= 1 or self._hash_bucket(payload) < rollout:
if level == "L2":
# L2 走双跑交叉校验
return ("self_hosted", {"cross_check": "frontier"})
return ("self_hosted", {})
return ("frontier", {})
def _hash_bucket(self, payload):
import hashlib
h = hashlib.md5(payload["request_id"].encode()).hexdigest()
return (int(h[:8], 16) % 10000) / 10000
用 request_id 做哈希分桶,保证同一个请求在灰度比例不变时始终命中同一条路径——否则用户会看到结果随机跳变,反馈无法归因。
4.5.2 灰度节奏
| 阶段 | L0 | L1 | L2 | 观察指标 | 通过条件 |
|---|---|---|---|---|---|
| D1–D2 | 5% | 0% | 0% | 错误率、延迟、成本 | 无 P0/P1 |
| D3–D5 | 20% | 0% | 0% | + 用户编辑率 | 编辑率不升 |
| D6–D8 | 50% | 5% | 0% | + 盲评抽样 | 盲评差距 < 0.2 |
| D9–D11 | 100% | 20% | 0% | + 成本曲线 | 成本下降符合预期 |
| D12–D14 | 100% | 50% | 10% | + 交叉校验一致率 | 一致率 > 92% |
| D15–D21 | 100% | 100% | 50% | 全量监控 | 稳定 |
每个阶段都有明确的「通过条件」和「回滚条件」。灰度不是「慢慢放开」,是「每个台阶都要验收」。
4.5.3 双跑交叉校验(L2 专用)
对高风险任务,自研模型与前沿模型都跑,比较结果:
def cross_check(self_hosted_out, frontier_out, task_type):
"""返回是否需要人工介入"""
agreement = compute_agreement(self_hosted_out, frontier_out)
if agreement >= 0.92:
return ("auto", self_hosted_out)
if agreement >= 0.75:
return ("flag_for_review", self_hosted_out, frontier_out)
return ("use_frontier", frontier_out)
一致率低时自动回落到前沿模型。这是把「质量风险」变成「可控的额外成本」——只有真正困难的请求才付前沿模型的钱。
4.5.4 灰度期间的回归拦截
每次模型更新上线前,CI 自动跑三件事:
# .github/workflows/model-gate.yml
name: model-gate
on:
pull_request:
paths: ["models/**", "training/**"]
jobs:
gate:
runs-on: [self-hosted, gpu]
steps:
- name: Run golden set
run: |
python eval_runner.py \
--model ./models/candidate \
--tasks eval/golden.jsonl \
--judge claude-sonnet-4-5 \
--out eval_result.json
- name: Run red team set
run: |
python eval_runner.py \
--model ./models/candidate \
--tasks eval/redteam.jsonl \
--strict \
--out redteam_result.json
- name: Gate
run: |
python gate.py \
--baseline eval/baseline_frontier.json \
--candidate eval_result.json \
--max-regression 1.5 \
--redteam-max-failures 0
gate.py 的核心逻辑:
def gate(baseline, candidate, max_regression, redteam_max_failures):
reasons = []
# 1. 整体不能退步超过阈值
if candidate["score"] < baseline["score"] - max_regression:
reasons.append(f"整体得分退步 {baseline['score'] - candidate['score']:.2f}")
# 2. 按任务层分别卡,防止某一层偷跑
for level in ("L0", "L1", "L2"):
b = baseline["by_level"][level]["score"]
c = candidate["by_level"][level]["score"]
if c < b - max_regression:
reasons.append(f"{level} 层退步 {b - c:.2f}")
# 3. 红队集零容忍
if candidate["redteam_failures"] > redteam_max_failures:
reasons.append(f"红队集失败 {candidate['redteam_failures']} 条")
# 4. 幻觉维度单独卡
if candidate["by_dimension"]["no_hallucination"] < baseline["by_dimension"]["no_hallucination"]:
reasons.append("幻觉维度退步(一票否决)")
return (False, reasons) if reasons else (True, [])
第 2 条「按任务层分别卡」是关键。只看总分会出现「L0 涨 3 分、L2 跌 4 分 → 总分涨 0.4 分」这种蒙混过关。
第 4 条幻觉一票否决对应 rubric 里的门控规则。法律场景里,幻觉退步不能用任何分数补偿。
4.6 Step 5.5:后训练的具体配置
4.6.1 为什么选 LoRA 而不是全参微调
| 维度 | 全参微调 | LoRA |
|---|---|---|
| 显存需求 | 2.8T 参数模型基本不可行 | 单卡 8×A100 可跑 |
| 训练速度 | 慢 | 快 3–5 倍 |
| 灾难性遗忘 | 风险高 | 风险低(底座冻结) |
| 多任务切换 | 每个任务一份全量权重 | 一个底座 + 多个小适配器 |
| 最终质量上限 | 更高 | 略低(本案例够用) |
对 2.8T 参数级别的模型,LoRA 基本是唯一现实选择。 且本案例的目标是「补领域知识与格式」,不是「改变模型的基础能力」——LoRA 的能力天花板完全够。
4.6.2 训练配置
# train_sft.yaml
base_model: moonshot/kimi-k3
method: lora
lora:
r: 64
alpha: 128
dropout: 0.05
target_modules:
- q_proj
- k_proj
- v_proj
- o_proj
- gate_proj
- up_proj
- down_proj
training:
stage: sft
num_epochs: 3
per_device_batch_size: 4
gradient_accumulation_steps: 16
learning_rate: 1.0e-4
lr_scheduler: cosine
warmup_ratio: 0.03
max_seq_len: 32768
bf16: true
gradient_checkpointing: true
data:
mix:
- path: data/domain_corpus.jsonl # 40%
weight: 0.40
- path: data/sft_demonstration.jsonl # 30%
weight: 0.30
- path: data/badcase_pairs.jsonl # 20%
weight: 0.20
- path: data/long_context.jsonl # 10%(长文档场景单独补)
weight: 0.10
packing: true
shuffle_seed: 42
DPO 阶段:
# train_dpo.yaml
base_model: ./checkpoints/sft_v3
method: lora
dpo:
beta: 0.1
loss_type: sigmoid
max_length: 16384
max_prompt_length: 8192
training:
num_epochs: 1
learning_rate: 5.0e-6 # DPO 的学习率要比 SFT 小一个量级
per_device_batch_size: 2
gradient_accumulation_steps: 32
DPO 学习率必须比 SFT 小一个量级(1e-4 → 5e-6)。用 SFT 的学习率跑 DPO,模型会迅速崩坏——表现为输出变得空洞、重复、回避具体问题。
4.6.3 迭代记录
v0 基线(未后训练) 客观分 79.2 盲评 3.86
v1 SFT(领域语料 + 示范) 82.6 盲评 3.98
v2 SFT + bad case 数据 85.1 盲评 4.09
v3 + 长上下文数据补齐 85.4 盲评 4.10 ← 长文档档位明显改善
v4 DPO v1 86.3 盲评 4.14
v5 DPO v2 + 红队修补 87.1 盲评 4.16
─────────────────────────────────────────────────
前沿基线 88.4 盲评 4.31
v2 的涨幅最大(+2.5),来自 bad case 数据。这验证了前面的判断:真实失败样本是 ROI 最高的数据。
v3 只涨了 0.3 分,但长文档档位涨了 4.1 分。总分掩盖了分层差异——这再次说明评测必须分层看。
4.7 Step 5.6:推理部署
4.7.1 显存规划
Kimi K3 2.8T 参数(开放权重)
├─ BF16 全量: 约 5.6 TB → 需 8 卡 × 8 = 64 卡 A100/H100,不现实
├─ FP8 量化: 约 2.8 TB → 约 32 卡
├─ INT4(AWQ/GPTQ): 约 1.4 TB → 约 16–20 卡
└─ 实际部署方案: INT4 + 张量并行 + 分层卸载
量化是让这件事经济上成立的关键。但量化会带来质量损失,必须实测:
| 精度 | 金标集得分 | 盲评 | 显存 | 结论 |
|---|---|---|---|---|
| BF16 | 87.1 | 4.16 | 5.6 TB | 太贵 |
| FP8 | 86.8 | 4.15 | 2.8 TB | 可用 |
| INT4 (AWQ) | 85.9 | 4.11 | 1.4 TB | 选它 |
| INT4 + 部分层 FP8 | 86.5 | 4.13 | 1.9 TB | 备选 |
INT4 掉 1.2 分,换显存减半。在 L0/L1 层这个损失完全可接受(本来就差 0.1–0.2 分,量化后差 0.3–0.4 分,仍在阈值内)。L2 层用「INT4 + 部分层 FP8」的混合方案。
4.7.2 vLLM 部署配置
python -m vllm.entrypoints.openai.api_server \
--model /models/kimi-k3-awq \
--quantization awq \
--tensor-parallel-size 8 \
--pipeline-parallel-size 2 \
--max-model-len 262144 \
--max-num-batched-tokens 16384 \
--enable-prefix-caching \
--enable-chunked-prefill \
--gpu-memory-utilization 0.92 \
--served-model-name legal-internal-v5 \
--port 8000
| 参数 | 值 | 为什么 |
|---|---|---|
--enable-prefix-caching | on | 同一份合同被多次查询时,共享前缀缓存能省大量重复计算 |
--enable-chunked-prefill | on | 长文档 prefill 切片,避免单次占满显存导致其他请求排队 |
--max-num-batched-tokens | 16384 | 吞吐与延迟的平衡点,需按实测调 |
--gpu-memory-utilization | 0.92 | 留 8% 余量防 OOM |
--enable-prefix-caching 在法律场景收益极大:系统提示 + 项目规范 + 同一份合同的前 50K token 通常是重复的,缓存命中后这部分成本直接归零。
4.7.3 自托管成本模型
单卡成本(3 年摊销):
H100 单价 $25,000
3 年折旧 $25,000 / 36 = $694/月
电力(0.7kW × 24h) 约 $50/月
机房/托管 约 $120/月
运维人力分摊 约 $80/月
────────────────────────────────
单卡月成本 约 $944
16 卡集群月成本 约 $15,100
吞吐测算:
INT4 + 8 卡张量并行 × 2 流水并行
实测输出吞吐 约 3,200 tok/s(集群整体)
月产出 3,200 × 3600 × 24 × 30 ≈ 82.9 亿 token
单 token 成本 = $15,100 / 8.29e9 × 1e6 ≈ $1.82 / 百万 token
上面的 $0.42/M 是按 GPU 利用率 100%、且只算输出 token 的理想值。 实际运营中利用率通常在 40–70%,真实成本在这个基础上再乘 1.5–2.5 倍。 这个数字 [推演] 用于说明「自托管成本也要认真算」,不要直接抄进你的预算表。
最重要的修正:多数团队第一次算这个账时,都会忘记三件事——GPU 利用率、输入 token 也占算力、以及夜间低峰期资源闲置。把理论值打 2 折再拿去对比 API 价格,是比较稳的做法。
4.8 Step 6:成本与质量的双监控(持续)
4.6.1 必看的六个指标
| 指标 | 计算 | 告警阈值 |
|---|---|---|
| 单任务成本 | 总成本 / 任务数 | 环比 +15% |
| 分层成本占比 | 各层成本 / 总成本 | L0+L1 占比 < 25% 说明迁移没生效 |
| 金标集周得分 | 每周跑一次 | 环比 -1.5 分 |
| 用户编辑率 | 被编辑字符数 / 总输出字符数 | 环比 +5% |
| 交叉校验一致率 | L2 任务的一致比例 | < 90% |
| 幻觉投诉率 | 用户标记为幻觉的次数 / 总数 | 任何上升 |
「用户编辑率」是最真实的线上质量指标。它不需要专家、不需要评测集,直接从产品行为里算。金标集分数会漂移,用户编辑率不会撒谎。
4.6.2 监控面板
# 单任务成本趋势
sum(rate(llm_cost_usd_total[1h])) / sum(rate(llm_tasks_total[1h]))
# 分层成本占比
sum by (task_level) (rate(llm_cost_usd_total[1h]))
/ ignoring(task_level) group_left sum(rate(llm_cost_usd_total[1h]))
# 自研 vs 前沿 的调用分布
sum by (model_tier) (rate(llm_requests_total[5m]))
五、效果数据
5.1 成本结构变化 [推演]
| 指标 | 迁移前 | 迁移后 | 变化 |
|---|---|---|---|
| 单次请求平均成本 | $0.42 | $0.071 | -83% |
| 每百万输出 token 单价 | $6.00 | $0.42(自托管) | -93% |
| L0+L1+L2 走自研比例 | 0% | 92% | — |
| L3 走前沿比例 | 100% | 100%(保留) | — |
| 月度推理成本 | 100(基准) | 21 | -79% |
| GPU 与运维固定成本 | 0 | 14 | 新增 |
| 月度总成本 | 100 | 35 | -65% |
注意新增的 GPU 固定成本。这是很多迁移算错账的地方:只算了 API 省下的钱,忘了自托管要付固定成本。只有当负载足够大时,固定成本才能被摊薄。
5.2 毛利变化 [公开 + 推演]
| 阶段 | 毛利率 |
|---|---|
| 早期(按席位计费,用量可控) | 约 +50% [公开] |
| 转向用量计费 + token 消耗涨 20 倍后 | 约 -50% [公开] |
| 迁移完成 | 转正 [公开] |
这个 V 型曲线是关键。它说明一件事:商业模式(按用量计费)与技术架构(全用前沿模型)的组合,在 Agent 化之后会爆炸。 你必须改其中一个。Harvey 选择改技术架构。
另一个选择是改商业模式——回到按席位计费或设用量上限。但那会限制用户价值,等于自废武功。
5.3 质量变化
| 任务层 | 前沿基线 | 自研模型 | 差距 | 是否可接受 |
|---|---|---|---|---|
| L0(抽取/摘要) | 4.31 | 4.22 | -0.09 | ✅ 完全可接受 |
| L1(比对/标注) | 4.28 | 4.05 | -0.23 | ✅ 可接受(有校验) |
| L2(尽调/交叉推理) | 4.35 | 3.91 | -0.44 | ⚠️ 需交叉校验 |
| L3(定制意见) | 4.41 | — | — | 保留前沿模型 |
分层后再看这张表,结论完全不同。整体看「自研比前沿差 0.4 分」会让你不敢上;分层后你会发现 L0/L1 差距只有 0.1–0.2 分,而它们占 85% 的流量。
5.4 时间线
| 阶段 | 耗时 | 人力 | 关键产出 |
|---|---|---|---|
| 流量分析 | 1 周 | 1 人 | 任务分层表、成本分布 |
| 评测集 | 2 周 | 3 人(含 2 法务) | 1400 条金标集 + rubric |
| 选型 | 2 周 | 2 人 | 基线模型决策 |
| 后训练 | 4 周 | 3 人 | 模型 v4 |
| 灰度 | 2 周 | 2 人 | 分层路由上线 |
| 合计 | 11 周 | 峰值 5 人 | — |
真正的瓶颈是评测集,不是训练。训练有成熟工具链,评测集需要领域专家的时间,而且无法并行压缩。
六、踩坑清单
坑 1:先训练,后建评测集
症状:模型训了三轮,没人说得清是变好还是变差,团队开始凭感觉争论。
原因:没有基线评测集,所有质量判断都退化为主观意见。
修法:评测集必须是 Phase 1。如果老板催,就用 100 条最小集先启动,但不能是 0 条。
坑 2:judge 过拟合
症状:金标集分数一路涨到 91 分,人工盲评只有 3.7/5,用户投诉变多。
原因:模型学会了讨好 judge(更长的解释、更多的格式化、更谨慎的措辞),但没学会做对事。
修法:
- 每轮迭代必须人工抽检 50 条
- 定期轮换 judge 模型
- 加入「用户编辑率」这个不依赖 judge 的线上指标
坑 3:整体替换而非分层
症状:全量切过去,第二周高价值客户投诉激增,紧急回滚。
原因:L2/L3 任务的差距被 L0/L1 的平均数掩盖了。
修法:严格分层,L2 双跑校验,L3 永久保留前沿模型。3% 的流量保留前沿模型,成本占比不到 5%,但它是你的保险丝。
坑 4:低估自托管的固定成本
症状:财报上 API 支出确实降了 80%,但总成本只降了 30%,还多了 GPU 采购和运维人力。
原因:只算变动成本,不算固定成本。
修法:
盈亏平衡负载 = GPU固定成本 / (前沿API单价 - 自托管单价)
举例:
固定成本 $14/月(归一化)
价差 $6.00 - $0.42 = $5.58 / 百万 token
平衡点 = 14 / 5.58 ≈ 2.5 百万输出 token / 月 × 单位
低于这个负载,自建是亏的。 迁移前先算这个数。
坑 5:训练数据污染评测集
症状:金标集分数异常高(95+),上线后一塌糊涂。
原因:构造 SFT 数据时用了评测集的样本,模型背下了答案。
修法:
- 评测集与训练集严格隔离,用哈希去重
- 评测集知道的人最小化
- 定期用新构造的样本替换部分评测集
坑 6:忽略许可条款
症状:模型上线三个月后收到许可变更通知,商用条款变了。
原因:选型时只看跑分和成本,没仔细读许可。
修法:选型矩阵里加一列「许可风险」,并把「权重是否真的开放」「商用是否有限制」「是否有参数规模门槛」逐条确认。Kimi K3 这类明确开放权重的模型在这一点上风险最低。
坑 7:格式不兼容导致全线报错
症状:切过去之后 15% 的请求解析失败。
原因:自研模型的输出格式与前沿模型有细微差异(引号、转义、多余的解释性前缀),解析器没适配。
修法:
- 路由层加格式归一化
- 灰度前跑一次纯格式冒烟(1000 条请求只看能否解析)
- 用结构化输出(JSON schema / grammar-constrained decoding)而不是靠提示词约束
坑 8:长上下文退化
症状:短任务表现接近,长文档(> 80K token)质量断崖式下跌。
原因:开放权重模型的长上下文能力往往标称 1M,但有效上下文远小于此。
修法:
- 评测集按长度分档(<16K / 16–64K / 64–200K / >200K)
- 长文档场景强制走检索 + 分片,而不是整篇塞进上下文
- L2 任务的文档预处理不能省
坑 9:灰度期间没有回滚按钮
症状:发现问题时,回滚需要改代码重新发版,花了 4 小时。
修法:路由层的灰度比例必须支持热更新(配置中心改数字,秒级生效),不能靠发版。
坑 10:成本监控只看总额
症状:总成本确实降了,但因为一次提示词改动,L0 任务的 token 消耗悄悄涨了 3 倍,三个月后才被发现。
修法:成本监控必须按层 × 按任务类型下钻,并设环比告警。
坑 11:法务合规没参与
症状:模型上线后被问「训练数据里有没有客户的合同」,答不上来。
修法:数据构建阶段就让合规介入,明确三件事:
- 训练数据是否包含客户数据(本案例:只用脱敏数据与公开语料)
- 推理请求是否留存(本案例:metadata-only)
- 模型输出的法律责任归属(必须由人终审)
坑 12:把「降本」当成唯一目标
症状:为了再降 10% 成本,把 L2 也全切了,质量事故后损失远超省下的钱。
修法:在监控面板上把「质量指标」和「成本指标」放在同一屏,任何降本动作的评审必须有质量负责人在场。
坑 13:没算人力成本
症状:项目「成功」了,但回头一算,5 个人 11 周的人力成本,够付两年 API 费。
修法:迁移前算完整 ROI:
年化节省 = 月省成本 × 12
投入 = 人力成本 + GPU 固定成本 × 12 + 机会成本
回本周期 = 投入 / 年化节省
本案例 [推演]:约 7 个月回本
负载不够大的团队,不值得做这件事。
坑 14:以为做一次就完了
症状:迁移完成后团队解散,半年后新模型出来,又要重来一遍。
修法:把这套流程产品化——评测集常驻更新、训练管线可复用、路由层支持热插新模型。第二次迁移的成本应该只有第一次的 30%。
坑 15:前缀缓存没开,长文档任务白烧算力
症状:自托管跑起来了,但实际吞吐只有理论值的 40%。
原因:法律场景里大量请求共享同一份合同 + 同一段系统提示,没开 prefix caching 导致这部分每请求重算一遍。
修法:
# vLLM 启动时必须带这两个参数
--enable-prefix-caching
--enable-chunked-prefill
实测开启后,同一份合同被多次查询的场景下吞吐提升约 1.8 倍。这是自托管场景里性价比最高的一行配置。
坑 16:没给「空闲时段」做规划
症状:白天高峰期 GPU 打满排队,夜里资源闲置,但账单按整机算。
修法:把离线批量任务(评测集跑分、数据生成、历史请求重放)调度到夜间低谷。GPU 利用率从 45% 提到 72%,单位成本下降约 35%。
时段规划:
00:00-08:00 离线批处理(评测、数据构造、模型迭代)
08:00-22:00 在线推理
22:00-24:00 日志回放与 bad case 挖掘
自托管成本的命门是利用率,不是单价。 买到卡只是开始,把卡跑满才是真正的功课。
坑 17:把「模型版本」当成了「产品版本」
症状:模型从 v3 升到 v5,产品侧完全没感知,某天用户反馈「输出风格变了」才发现。
修法:模型版本必须作为产品的一等公民管理——
- 每次模型更新必须走一次完整灰度(不能因为「只是小版本」就跳过)
- 用户可见的输出风格变化必须提前公告
- 保留至少两个历史版本可随时回滚
- 模型版本号写进响应的 header,便于归因
七、经验总结
7.1 五条可迁移的结论
① 单位经济学会追上你,只是时间问题。 按用量计费 + Agent 化 + 上下文膨胀,这三个因素叠加,任何重度 AI 应用都会在 12–24 个月内撞上毛利墙。早算这笔账比晚算便宜。
② 分层是唯一低风险的迁移路径。 全量替换的风险集中在最难的 3–15% 任务上,而这批任务往往贡献最多的收入。分层让你用 5% 的成本保留保险丝。
③ 评测集是真正的资产,模型不是。 模型会过时(6 个月后又有新的开放权重出来),评测集不会。第二次迁移的成本应该只有第一次的 30%——前提是你第一次认真建了评测集。
④ 自托管有盈亏平衡负载,低于它别自建。
平衡点 = 固定成本 / (API单价 - 自托管单价)。算完这个数再决定,别跟风。
⑤ 质量的最终裁判是线上行为,不是评测分数。 「用户编辑率」「重试率」「投诉率」这三个指标不需要专家、不需要 judge,且永远真实。把它们和成本指标放在同一屏。
7.2 一条最重要的原则
AI 可以跑评测、做分析、写补丁、执行迁移,但「什么算改善」的定义必须由人给出。
本案例里所有翻车的可能,几乎都源于这条线被越过:
- 让 judge 定义质量 → 过拟合
- 让成本指标定义成功 → 质量崩塌
- 让模型自己生成评测集而不终审 → 评测集失真
守住这条线,剩下的工程问题都是有解的。
7.3 什么时候你该做这件事
该做:
- 月度推理成本 > 收入的 30%
- 任务分布里有大量 L0/L1(抽取、分类、摘要、格式化)
- 有领域专家能参与评测集构建
- 负载超过自托管盈亏平衡点
不该做:
- 月推理成本 < 几万美元(人力不划算)
- 任务全是 L2/L3(高风险推理,开放权重差距太大)
- 没有领域专家(建不出评测集)
- 模型能力还在快速迭代、产品形态没定型(做完就过时)
八、复盘:如果重来一次,会改的六件事
8.1 评测集应该先建两倍的量
实际建了 1400 条,回头看应该有 2500–3000 条。原因:
- 迭代到后期,模型在评测集上的方差变大(87.1 和 86.4 的差异可能只是噪音)
- 分层之后每层的样本量被稀释:L3 只有 60 条,统计上不可靠
- 没有余量做「评测集轮换」(防止 judge 过拟合需要定期换样本)
修正后的规模:主集每类 300 条(不是 200),L3 单独补到 200 条,并预留 30% 的未使用样本用于轮换。
8.2 应该更早引入「用户编辑率」
前五周完全依赖金标集分数做决策,直到灰度期才发现金标集和真实体验有偏差。
教训:线上行为指标要在 Phase 1 就接好,哪怕评测算不出来,也能提供交叉验证。
正确顺序:
Week 1: 接线上行为埋点(编辑率、重试率、放弃率)
Week 1: 同时开始建评测集
Week 3: 评测集就绪后,用行为指标交叉验证评测集是否可信
8.3 量化方案应该更早测
量化是在 Step 5 部署阶段才测的,发现 INT4 掉 1.2 分后,要不要回退到 FP8 成为一场争论——因为显存已经按 INT4 采购了。
教训:量化实验要和选型同步做(Phase 2),而不是放到部署前。它直接影响硬件采购决策。
8.4 应该给 L2 留更高的质量余量
L2 迁移后差距 -0.44 分,靠双跑交叉校验兜底。但交叉校验的一致率只有 92%,意味着 8% 的 L2 请求要人工介入——这部分人力成本当时没算进去。
修正:L2 要么再训一轮把差距压到 -0.3 以内,要么把交叉校验的人力成本明确计入 ROI。
8.5 合规评审应该更早
合规是在数据构建阶段才引入的,导致数据清洗返工了一次(部分语料的商用许可不明确)。
教训:选型阶段就要过一遍合规,因为它可能直接否决某些候选模型。
8.6 应该把「迁移」做成可重复流程,而不是一次性项目
第一次迁移 11 周、峰值 5 人。六个月后新模型出来,如果还要 11 周,这个成本就不可持续。
应该产品化的四件事:
| 资产 | 状态 | 下次复用能省 |
|---|---|---|
| 评测集 + rubric | ✅ 已有 | 约 2 周(不用重建,只要更新) |
| 训练管线(配置化) | ⚠️ 部分 | 约 3 周(脚本还在,超参要重调) |
| 路由层 | ✅ 已有(热插拔) | 约 1 周(只加新模型配置) |
| 灰度与监控 | ✅ 已有 | 约 1 周(改监控目标即可) |
目标:第二次迁移 4 周内完成。 做不到,说明第一次的产品化没做好。
九、附:一页纸检查清单
迁移前
[ ] 算过自托管盈亏平衡负载,且当前负载高于它
[ ] 完成了任务分层,知道每类的流量占比与成本占比
[ ] 建了 ≥1000 条金标集,含 rubric 与门控规则
[ ] 记录了当前前沿模型的质量基线(这是唯一参照系)
[ ] 确认过候选模型的许可条款
迁移中
[ ] 每轮迭代都跑金标集 + 人工抽检 50 条
[ ] 训练集与评测集严格隔离(哈希去重)
[ ] 灰度比例支持热更新,回滚不需要发版
[ ] L2 走双跑交叉校验,L3 永久保留前沿模型
[ ] 合规已确认训练数据来源
上线后
[ ] 监控面板上成本与质量指标同屏
[ ] 按层 × 任务类型的成本下钻 + 环比告警
[ ] 用户编辑率作为核心质量指标持续追踪
[ ] 评测集每季度更新 20% 新样本
[ ] 训练管线可复用(为下一次迁移做准备)