LLM 数据增强任务卡 4 天:upsert 少传 id 后发生了什么

这篇写一个训练语料数据处理流水线里的真实问题。

背景是某 A 股上市环保集团的训练推理平台。数据集上传后,要经过清洗、去重、LLM 数据增强,最后导出 JSONL 给训练链路。清洗任务通常几分钟,增强任务按数据量可能跑几十到上百小时。

一旦任务跑到这个时间跨度,核心问题就不只是「怎么调用模型」了,而是:

  • 任务怎么暂停
  • 怎么从断点继续
  • 同一批数据重跑时怎么不重复写
  • 写坏了怎么发现

总体链路

flowchart TD A[Dataset Upload] --> B[Relational Corpus Table] B --> C[Data Cleaning Task] B --> D[LLM Augmentation Task] C --> E[Upsert Writeback] D --> E E --> F[Export JSONL] F --> G[MinIO] S[task_execution_status] --> C S --> D R[Redis Pause Flag] --> C R --> D

这张图里最重要的不是 LLM,而是旁边两个状态源:DB 里的任务游标、Redis 里的暂停标志。

为什么语料要进 MySQL

如果只是脚本,处理 JSONL 文件最简单。但平台要支持查询、分析、预览、软删除和断点续跑,文件流不够用。

多轮对话样本被拆成关系表记录,简化后类似这样:

json 复制代码
{
  "id": 1024,
  "dataset_file_id": 7,
  "row_identifier": "row-00031",
  "turn_taking": 2,
  "data_system": "...",
  "data_prompt": "...",
  "data_reasoning_content": "...",
  "data_response": "...",
  "data_weight": 1,
  "is_deleted": "0"
}

这个 schema 在解决两个问题:平台能按行查询数据,任务也能按文件和 offset 恢复进度。

读取多轮对话时,按 row_identifier + id 排序重组。清洗或增强后,再把结果回写到同一套中间态里。

游标状态机

长任务不能靠 worker 记忆进度。我用状态表记录当前文件、offset、已处理行数和状态。

简化后的恢复逻辑是:

python 复制代码
resume_file_id, resume_offset = get_resume_position(task_id, stage)

for file_id in dataset_files:
    if should_skip(file_id, resume_file_id):
        continue

    offset = resume_offset if file_id == resume_file_id else 0
    resume_offset = 0

    while True:
        batch = load_batch(file_id=file_id, offset=offset, limit=batch_size)
        if not batch:
            break

        if is_task_paused(task_id):
            save_cursor(file_id=file_id, offset=offset, status="paused")
            raise TaskPausedException()

        process(batch)
        offset += batch_size
        save_cursor(file_id=file_id, offset=offset, status="running")

这里的 resume_offset = 0 很容易被忽略。缺了它,多文件恢复时,后续文件也会套用恢复文件的 offset,前 N 行会被跳过。

为什么清洗 batch=10000,增强 batch=10

清洗主要是 CPU 上的规则处理、格式整理和去重,单条很快。batch_size=10000 可以减少数据库往返。

增强任务每条都可能调 LLM。单条慢,外部服务也可能波动。如果 batch 太大,用户点暂停后要等很久才生效。增强侧 batch_size=10 牺牲了一点吞吐,换暂停响应。

这两个数字不是压测最优值。它们只是表达了不同任务的优先级:清洗要吞吐,增强要可控。

故障:upsert 漏 id,读写同表无限循环

Response 增强阶段踩过一个坑。

这个阶段从目标表分页读原始记录,调用 LLM 生成 response,再 upsert 回同一张表。原本的意图是:断点重跑同一批数据时,只更新原记录。

问题是有个回写 payload 少了主键 id

失败类型 脱敏样例 为什么错 处理策略
upsert 漏主键 response 增强结果未带 id MySQL 找不到 duplicate key,更新退化为插入 回写显式带 original_record.id
读写同表 offset 分页读目标表,同时向目标表追加新行 offset 增长追不上表尾增长,循环不退出 后续需要把无 id 更新做成硬失败
状态回写不完整 暂停后继续接口查不到 paused 异常分支只更新了部分状态源 补齐执行状态、本地业务状态和外部状态

修复代码本身只有一行:

python 复制代码
upsert_rows.append({
    "id": original_record.id,
    "data_response": generated_response,
    "dataset_file_id": original_record.dataset_file_id,
    "row_identifier": original_record.row_identifier
})

这段代码解决的是「让 upsert 命中原记录」。但根因不是一行字段,而是幂等依赖了隐式约定。

如果是现在收这块,我会在 DAO 层加保护:更新类 upsert 没有主键直接抛错,不允许退化成插入。

另一个边界:LLM 失败不能只记日志

批量 LLM 调用里,单条失败会被隔离,不中断整批。当前机制是失败项用空字符串占位,并统计 success/fail。

生产上没有确认出现过一批空样本混进训练集的问题。但从数据质量角度看,这个边界还是软。

更稳的做法是:

python 复制代码
if not generated_response.strip():
    mark_failed_sample(record_id, reason="empty_response")
    continue

if not validate_output(generated_response):
    mark_failed_sample(record_id, reason="invalid_format")
    continue

upsert(record_id, generated_response)

这段逻辑的目标不是让任务永远成功,而是避免失败样本静默进入导出链路。

Takeaways

这类训练语料流水线,别只盯着清洗规则和 prompt。

真正容易出问题的是更底层的东西:

  • 游标要持久化,不要只放 worker 内存。
  • 暂停要是业务状态,不是直接杀进程。
  • upsert 不能只靠调用方记得传主键。
  • 读写同表时,offset 分页要特别小心。
  • LLM 空结果要进失败队列,不要只记日志。

长任务系统最怕的不是失败。

最怕的是它看起来还在跑,其实已经开始制造脏数据。

如果有帮助,欢迎点赞收藏。

相关推荐
北冥you鱼2 小时前
Go 语言读取链上数据:从基础到实战
开发语言·后端·golang
程序员cxuan3 小时前
Claude Code 为了封禁中国用户,竟然在代码里下毒
人工智能·后端·程序员
郡杰3 小时前
Git基础与开发平台搭建
git·后端
ServBay3 小时前
MCP (Model Context Protocol) 深度指南,协议原理、Python 实现与生产级安全实践
后端·ai编程·mcp
道友可好3 小时前
Claude Code 泄露源码里的 89 个秘密
前端·人工智能·后端
用户8356290780514 小时前
使用 Python 保护和取消保护 Excel 工作表和工作簿
后端·python
Java面试题总结4 小时前
CubeSandbox 实战:从零部署到快照/克隆/回滚全体验
后端
Csvn4 小时前
Python 开发技巧:函数式编程与 lambda 进阶
后端
考虑考虑5 小时前
nginx配置ssl
运维·后端·nginx
uhakadotcom5 小时前
Python 中,`None` 不等于 `False`导致的问题
后端·面试·github