跳到主要内容

2 篇博文 含有标签「数据管道」

查看所有标签

LLM 批量校验全量走 fallback?容量门控超限的全有全无陷阱

· 阅读需 5 分钟

生产环境首跑一个 LLM 批量校验任务,日志一片绿、状态成功——但检查输出发现 2449 个待校验对象全部标成了降级标记,实际 LLM 调用次数为零。「语义校验默认开启」的功能,等于一次都没开过。

在开发 AI运营 时遇到此问题——基于大语言模型的智能分析平台,自动洞察市场趋势、用户行为与销售数据;这个校验任务跑在其数据管道的标题优化环节。

TL;DR

容量门控按「预计量 ≤ 上限」做全有全无判断:试设的 cap 是 400,生产实际是 2449 个词×商品对,超限 → 整批降级、零 LLM 调用,且任务状态照样是成功。教训两条:容量上限必须用生产实测规模校准;超限降级应按单位粒度(分组/排队/截断)进行,并让「fallback 率 100%」这种异常可被观测。

问题现象

任务的 Layer2 是 LLM 语义校验,入口有一个容量门控:

def semantic_validate(pairs, cap=400):
if len(pairs) > cap:
# 超限:整批降级,一次 LLM 都不调
return [mark_overflow(p) for p in pairs]
return [llm_validate(p) for p in pairs]

生产首跑结果:

待校验词×商品对:2449/2449 全部 语义校验方式='overflow'
LLM 实际调用:0 次
任务状态:success(无任何报错)

如果只看「跑完没有」,一切正常;只有检查输出列的分布,才发现功能整体失效。

根因

两层问题叠加。第一层是数值:cap 试设 400,而生产规模是 60 个市场词×同类目商品 + 50 个本店词×商品、共 92 个商品,对数直接到 2449——预估和实测差了一个数量级。第二层是结构:门控是全有全无,超限即整批降级。「这是一个容量约束」的初衷,实际效果是「超限 = 功能整体关闭」,而且降级发生在数据列里、不抛错不打日志,完全静默。

这类「看起来成功、实际没干活」的静默失败和 DeepSeek thinking 吃满输出预算导致空回复静默兜底是同一个家族:错误被兜底逻辑消化,表面上永远 success。

解决方案

步骤 1:用生产实测规模校准 cap

上线前先统计真实待处理量,别用拍脑袋的预估值:

# dry-run:只统计规模,不产生 LLM 调用
python -c "from pipeline import build_pairs; print(len(build_pairs(shop='prod')))"

实测 2449 → cap 设 3000(约 1.2~2 倍余量),同时确认超大店铺超出时仍有降级路径,不会撞墙。

步骤 2:把调用粒度从「总量」改为「分组」

按商品分组调用,让调用次数随商品数线性增长,而不是随 词数×商品数 的乘积暴涨:

def semantic_validate(pairs, cap):
groups = group_by_product(pairs) # 92 商品 → ~92 次调用/轮
results = []
for g in groups:
if within_budget(g, cap): # 按组判断,不整批放弃
results.extend(llm_validate(g))
else:
log.warning("capacity gate: group degraded",
extra={"size": len(g), "cap": cap})
results.extend([mark_overflow(p) for p in g])
return results

本例校准后第三跑实测 2449/2449 全部走 LLM 校验;更大的店铺超出时按组降级,不再一损俱损。

步骤 3:让降级可观测

给降级路径埋点,并对异常比例告警(如 fallback 率 > 50%)。降级是安全网,不是掩体——它应该被看见,而不是替你掩盖超限。

注意事项

  • 容量类参数(cap、并发、批量大小)上线前必须用生产实测规模校准;测试环境的小样本永远撑不出生产数量级。
  • 全有全无门控只适合「成本硬上限」场景,且必须伴随显式告警;否则它就是一颗静默关闭功能的开关。
  • 降级动作要落在独立可查询的字段/指标上(本例是 语义校验方式 列),验收时先看分布、再看对错。
  • LLM 输出还有一类静默失败来自结构化校验,见 用 Zod 校验 LLM 输出却静默失败?别用 .strict()

常见问题

LLM 管道里的 fallback 机制应该怎么设计?

降级粒度尽量小——按条或按组降级,而不是整批放弃;降级动作必须留痕(标记列、日志、指标)并配置告警。全有全无式门控一旦触发等于整个功能关闭,只适合成本硬上限场景,且要显式报警。

LLM 批量任务的容量上限怎么定?

不能拍脑袋。先在生产规模或等比样本上跑一次 dry-run 统计实际待处理量,上限设为实测值的 1.5~2 倍,并随业务规模增长定期复核。预估与实测差一个数量级,是这类事故的标配。

怎么发现 LLM 任务被静默降级了?

任务状态往往仍是成功,必须检查输出:统计降级标记列的占比、核对实际 LLM 调用次数是否与预期一致。fallback 率异常(尤其 100%)应配置告警,把静默失败变成显式信号。

CCLEE

独立开发者,24年电商行业实战经验,专注将AI能力落地于真实商业场景。

合作咨询

数据管道快照槽位错位?0 行段丢弃导致位置漂移

· 阅读需 6 分钟

核对一次生产任务的决策快照时,发现第 5 个阶段的特征数据落在了数组槽 3,而不是设计文档里写的槽 4——下游和抽屉组件按「段号−1」取值,取到的是上一阶段的输出。

在开发 AI运营 时遇到此问题——基于大语言模型的智能分析平台,自动洞察市场趋势、用户行为与销售数据;快照是管道留给前端展示和事后审计的决策依据。

TL;DR

管道把各阶段(段)输出按执行顺序 push 进快照数组,而 if rows.empty: skip 会把 0 行段整个丢弃,后续所有段前移一位——「段号−1 = 槽位」的静态映射随时被打破,且哪种段为空取决于运行态,槽位每次都可能不同。解法两条路:消费端按行内键名实时认槽(推荐),或写入端给空段保留占位、维持槽位恒定。

问题现象

装配逻辑长这样:

snapshot = {"features": [], "rule_output": []}
for seg in segments: # 段①…段⑤ 顺序执行
df = execute_sql(seg.sql)
if not df.empty: # 0 行段在这里被丢弃
snapshot["features"].append(df.to_dict("records"))

设计假设是「段⑤ → 槽 4」。但生产快照核验发现段⑤的特征落在槽 3:

全段有数据:      段①→0  段②→1  段③→2  段④→3  段⑤→4   ✓ 符合假设
段④ 空表被弃: 段①→0 段②→1 段③→2 段⑤→3 ✗ 前移
段③④ 都空: 段①→0 段②→1 段⑤→2 ✗ 再前移

同一个代码版本,不同店铺/不同权限下快照槽位完全不同——保护词白名单是空表时段④被弃,权限关闭时段③被弃,槽位跟着运行态漂移。

根因

位置寻址撞上了稀疏装配。 快照数组是运行时把「有输出的段」压缩拼接的产物,本质是个稀疏集合;而下游按「段号−1」硬编码取值,等价于假设「每个段必然产出至少一行」。这个假设在三种常见情形下都会碎:白名单空表、功能开关关闭、业务数据天然为空——0 行是常态而不是异常。

更深一层,if not df.empty 这个判空本身没写错,错的是契约的隐含前提:设计文档写了「槽位 = 段号−1」,却没人把它声明成显式契约。所有按位置消费的下游都在继承一个未被承认、也无人维护的假设。

解决方案

方案 A(推荐):消费端按行键名认槽

让每行数据自带段标识键,消费方在读取时实时解析位置,不做任何静态映射:

def locate_segment(features: list, seg_key: str) -> dict:
for row in features:
if seg_key in row: # 行内自带段标识,按内容寻址
return row
raise KeyError(f"segment '{seg_key}' missing in snapshot")

槽位漂移从此无关紧要——找的是「键名长这样的段」,不是「第 N 个元素」。唯一要求是所有消费方统一走这个解析入口(写进 processor docstring 和消费方契约,明确禁止硬编码槽位)。

方案 B:写入端保留空段占位

如果下游暂时改不动,可以让装配端维持「槽位 = 段号」恒定:

snapshot["features"].append(
df.to_dict("records") if not df.empty else {"__empty__": True}
)

代价是快照里出现占位对象,所有消费方都得处理它;作为过渡方案可用,长期仍建议收敛到方案 A。

步骤 3:用多种运行态做契约测试

把「全段有数据 / 单段空 / 多段空」三种运行态做成快照 fixture,断言消费方在三种形态下解析结果一致。只测全满场景,等于没测。

注意事项

  • 规格文档里任何「位置对应关系」都必须显式声明寻址方式(按键名/按 ID),并注明「禁止按下标硬编码」;隐含假设一定会被某个运行态打破。
  • 判空跳过(if empty: skip)是最常见的压缩来源——同类静默丢数据还有 Airflow PostgresHook 多语句 SQL 只返回第一段结果,同样是「不报错、悄悄少东西」。
  • 改造消费方时,先用三种运行态 fixture 回归,再上生产;只验证「全段有数据」的场景会漏掉全部错位路径。

常见问题

数据工程里怎么处理 schema drift?

把位置契约换成键名契约:快照、消息、接口按字段名或段标识寻址,而不是数组下标。上游发生未经约定的结构变化(空段被跳过、字段增删)时,按名寻址的下游最多报「找不到」,不会静默拿到错误数据。

schema drift 和 schema evolution 有什么区别?

Schema evolution 是显式管理的版本演进(加字段、发版本、迁移消费方);schema drift 是被动发生的漂移——上游一改、下游不知不觉错位。本例的槽位前移就是典型 drift:没人改契约,是数据形态变了。

怎么检测数据管道里这类槽位错位?

两层:契约测试覆盖多种运行态(全段有数据/单段空/多段空),断言消费方解析一致;生产侧定期抽检快照,核对槽位内容自带的段标识与预期段是否对应。发现「内容与位置对不上」即是 drift。

CCLEE

独立开发者,24年电商行业实战经验,专注将AI能力落地于真实商业场景。

合作咨询