2423 字
约 8 分钟
13
S22-X15 · LLMOps 迭代闭环:成本账、灰度与用例门禁

LLMOps 常被理解成"给模型加监控",真正卡住团队的却是另外三件事:每次改动不知道会不会退化、每次退化不知道是哪一层改的、每次成本上涨不知道涨在哪条链路。这三个问题对应三样工程设施:用例门禁、分层版本、成本账。把它们串成闭环,才叫 LLMOps。


1. LLMOps 管的是四条线

线 对象 失控表现 抓手
版本线 prompt、模型、工具 schema 改了不知道哪个生效 三者独立版本号
质量线 用例集与判据 "感觉变好了" LLMTestCase 门禁
成本线 token、调用数、缓存命中 账单月增 40% 无人认领 四段式成本账
发布线 灰度比例、回滚 一次全量上线全站退化 按特征分桶放量

四条线必须有同一个关联键(trace_id / release_id),否则事后只能靠时间戳对齐,永远对不上。


2. 成本账:把 token 拆成四段

def cost_breakdown(calls):
    agg = dict(prompt=0, completion=0, cached=0, retry=0)
    for c in calls:
        agg["prompt"]     += c["prompt_tokens"]
        agg["completion"] += c["completion_tokens"]
        agg["cached"]     += c.get("cached_tokens", 0)
        agg["retry"]      += c["prompt_tokens"] if c["attempt"] > 1 else 0
    total = sum(agg.values())
    return {k: round(v / total, 3) for k, v in agg.items()} | {"total_tokens": total}
段 来源 常见占比(典型量级) 压缩手段
prompt 系统提示 + 检索片段 + 历史 50%–70% 精简提示、分块、去重
completion 生成内容 20%–40% 输出上限、结构化约束
cached 命中的前缀缓存 视场景 稳定前缀顺序、少改系统提示
retry 失败重试的重复输入 1%–10% 退避抖动、幂等、前置校验

retry 段最容易被忽略,因为它散落在每次失败里。把它单独计数,往往能直接定位一个"某供应商偶发 500 导致全量重试"的问题。

按段统计还能回答一个关键问题:是输入太长,还是输出太长。这两个结论对应的改法完全不同——前者改检索与提示,后者改约束与模型档位。


3. 分层版本:三个独立版本号

变更对象 版本载体 回滚粒度 必须记录的字段
prompt prompt@v12(内容哈希) 单点回滚 模板文本、变量列表
模型 model@qwen3-32b-2026-05 换模型 供应商、上下文窗口、单价
工具 schema tools@v7 单工具回滚 工具名、参数、是否幂等

合在一起构成 release_id = prompt@v12 + model@x + tools@v7。发布单上写这三段,而不是"后端 v2.3",否则线上退化时无法二分定位。

RELEASE = {"prompt": "v12", "model": "qwen3-32b-2026-05", "tools": "v7"}

def tag(rec):
    return {**rec, "release_id": "/".join(f"{k}@{v}" for k, v in RELEASE.items())}

4. LLMTestCase:用例即门禁

LLMTestCase 的核心不是"跑一遍看输出像不像",而是把预期拆成可判定的断言,并允许三类判据混用。

from dataclasses import dataclass

@dataclass
class LLMTestCase:
    name: str
    input: dict
    expected: dict
    graders: list          # ["exact", "schema", "llm_judge", "retrieval_hit"]

    def run(self, target):
        out = target(self.input)
        fails = [g for g in self.graders if not GRADERS[g](self.expected, out)]
        return {"name": self.name, "pass": not fails, "failed": fails, "out": out}
判据 可判定性 适合 误判来源
exact 强 枚举、编号、金额 格式差异
schema 强 结构化输出 语义对但字段错
contains 中 关键要素是否出现 同义表述
retrieval_hit 强 RAG 召回 标注集质量
llm_judge 弱 主观质量 评审模型自身偏差

铁律:门禁里 llm_judge 占比不要超过三成,且必须与至少一个强判据配对。全弱判据的门禁会稳定通过,然后线上翻车。

用例集规模也有讲究:50 条能挡住方向性错误,300 条以上才有统计意义。每次线上事故都应沉淀一条用例,这是唯一能让门禁随时间变强的机制。


5. 可复现任务作为回归底座

SWE-agent 这类"给定仓库 + 问题,产出补丁并通过测试"的基准,价值不在刷分,而在它把质量变成了可复现的判据:补丁跑不跑得过测试,是二值的。

底座类型 判据 适合验证 维护成本
单元用例集 断言 单点行为 低
仓库级修复任务(SWE-agent 式) 测试通过率 多步工具使用、代码理解 中
端到端场景回放 最终状态 全链路 高
人工抽检 主观评分 风格、可用性 中

关键设计是冻结环境:镜像、依赖版本、测试命令全部固定。环境一漂,"通过率下降"就无法区分是模型退化还是依赖升级。


6. 灰度:按请求特征放量,不按百分比乱放

import hashlib

def bucket(user_id, release):
    h = hashlib.sha256(f"{user_id}:{release}".encode()).hexdigest()
    return int(h[:8], 16) % 100          # 0-99 稳定分桶

def pick(user_id, release, pct):
    return "new" if bucket(user_id, release) < pct else "old"
放量维度 优点 风险
按用户哈希 同一用户稳定同一版本 大客户可能全落在实验组
按流量百分比 简单 同一用户来回切,体验断裂
按请求特征(长度/语言) 精准限定风险面 需要可靠的特征提取
按内部账号先行 风险最低 样本不代表真实分布

灰度的三个必备开关:比例、白名单、一键回滚。缺了"一键回滚"的灰度只是分批上线。


7. Auto-review:自动评审能做什么、不能做什么

评审对象 自动化可行性 建议
格式/结构违规 高 完全自动,直接拦截
引用是否支撑结论 中 自动标记,人工抽检
事实正确性 低 只能做交叉检索提示
语气与品牌一致性 中 规则 + 少量标注
安全与合规表述 中高 规则优先,模型兜底
def auto_review(out, rules):
    hits = [r for r in rules if r.match(out)]
    if any(r.blocking for r in hits):
        return {"decision": "block", "hits": [r.id for r in hits]}
    if hits:
        return {"decision": "flag", "hits": [r.id for r in hits]}   # 进人工队列
    return {"decision": "pass"}

自动评审的正确定位是分流器,不是裁判。让它直接决定"通过/拒绝",等于把误判率直接写进业务。


8. E2ETest:端到端要断言什么

端到端测试在 LLM 系统里最容易写成"跑一遍不报错就算过"。有效的断言只有四类:

断言 检查点 为什么必要
结构 输出 schema、必填字段 下游解析的前提
引用 引用片段是否真实存在 幻觉引用的唯一硬证据
副作用 数据库/工单状态变化 动作真的发生了
预算 步数、token、时延上限 防长尾拖垮
def e2e_assert(trace, case):
    assert schema_ok(trace["output"], case["schema"])
    assert all(c["doc_id"] in trace["retrieved"] for c in trace["citations"])
    assert trace["db_delta"] == case["expect_delta"]
    assert trace["steps"] <= case["max_steps"]

把"引用必须来自检索结果"写进断言,是拦幻觉最划算的一步——它不判对错,只判"你是不是编的"。


9. 排错:六种闭环失效

现象 原因 处理
门禁全绿但线上退化 用例集与真实分布脱节 每月从线上采样补用例
无法定位是哪层改动导致退化 三个版本号混在一个发布号里 拆成 prompt@/model@/tools@
成本涨了但调用量没涨 输入变长或被重试放大 按四段拆账,先看 retry
灰度放量后指标剧烈震荡 按流量百分比切,用户来回切版本 改按用户哈希稳定分桶
回滚后仍异常 缓存里还是旧响应 版本号进缓存键
基准通过率突然下降 测试环境漂移 冻结镜像与依赖

10. 边界:这些事不该塞进 LLMOps

诉求 更该做的 理由
提升模型能力上限 换模型 / 微调 运营设施不改能力
修数据错误 数据治理 门禁只会稳定复现错误
降低单 token 单价 采购与路由 属商业与网关层
替代人工判断 明确判据 + 抽检 自动评审只能分流

11. 自测

  1. 成本账为什么要单独统计 retry 段?它能暴露哪一类问题?
  2. release_id 由哪三段组成?合成一个"后端版本号"会导致什么定位困难?
  3. 门禁里 llm_judge 占比过高会发生什么?如何配平?
  4. 用 SWE-agent 式仓库任务做底座时,为什么必须冻结运行环境?
  5. 灰度为什么优先按用户哈希而不是按流量百分比?
  6. 端到端断言里"引用必须来自检索结果"拦的是哪类错误?它判不了什么?
S22-X15 · LLMOps 迭代闭环:成本账、灰度与用例门禁
https://www.clxhxhhr.top/posts/6014/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。