第2章 把多模态原件做成可追溯数据包

工作阶段: 定义工作。
公共核心项目: 完成“工作场所设备巡检记录数据包”,把授权原件、候选提取、契约校验、问题分流、来源级划分和数据卡连成完整工程。
核心技术: 多模态信息提取、样本与标签、数据类型与模式、数据契约、质量检查、数据划分和来源追踪。
输入输出链: 授权材料与原件 → 来源索引 → AI候选提取 → 数据契约校验 → 人工复核 → 清洗数据与数据卡。
应用中的AI: 从不同形式材料中提取候选字段的多模态或语言模型;参考工程使用明确标记的离线候选回放。
协助开发的AI: 帮助设计字段、生成校验程序、解释错误日志和限定修改的项目型AI助手。
人的责任: 确认授权与标签含义,保存原件,处理冲突和缺失,批准数据版本。
主要交付物: 原始材料索引、数据契约、候选与清洗数据、问题记录、数据字典、数据卡和版本说明。
明确边界: 不收集无关身份信息;模型提取结果只是候选;无法确认的内容保留缺失,不用猜测填充。
跨章关系: 承接第1章的任务合同,为第3章检索、第5—9章模型训练和第10—12章系统交付提供可靠输入。

学习目标

完成本章学习后,你将能够:

  1. 为原始材料建立授权、来源、版本和保存位置记录;
  2. 根据工作输出设计字段、类型、单位、标签和缺失表示;
  3. 运行AI生成的完整校验程序,发现缺失、重复、越界和来源错误;
  4. 解释样本、特征、标签以及训练、验证、测试划分在当前数据包中的含义;
  5. 区分真实模型输出、离线候选回放和人工录入基线,不虚构效率或准确率;
  6. 将原件、数据、程序和说明组成另一组可以复现的数据包。

项目导入

人工智能项目常从“手头有不少材料”开始。图片存在聊天记录中,巡检表使用不同版本,录音没有时间和地点,环境数值缺少单位。多模态模型能够阅读图片、听取录音并概括文档,但它不能自动知道材料是否获准使用、某个编号指向哪台设备、18表示摄氏度还是湿度,也不能替专业人员确认标签。

本章完成一个可运行的工作场所设备巡检记录数据包。为避免使用真实个人与企业资料,样例包含模拟值班文字、虚拟传感器CSV、铭牌图片的观察文字替身、录音转写,以及一个专门用于隐私和授权阻断的测试原件。真实项目可把文字替身换成获准的图片和音频,但数据契约与验收门不变。

candidate_adapter.py把候选提取与具体模型隔离。课堂默认读取is_model_output=false的离线回放,断网也能运行;接入真实模型时必须登记模型与日期,不得上传未授权原件。普通程序依据数据契约检查字段,学生再回到原件复核。项目目标不是“全部识别”,而是让每条可用数据能回到来源、每个未知项没有被偷偷猜出。

本章成果

AI协同开发路线

学生先阅读TASK_CONTRACT.md并核对索引哈希,再观察离线候选怎样进入契约校验。学生亲手只改变一个单位规则,预测clean与issues数量变化;随后让协助开发的AI只增加“原件哈希变化”测试,审查差异并运行全部回归。接入真实多模态模型是可选扩展:模型候选先导出为带来源声明的JSON包,再通过--candidates进入与离线回放完全相同的校验主链;它不能改变原件只读、候选不等于事实和未授权来源必须阻断这三条规则。最终由另一组按README复现。

第一节 保留原件、授权和来源,再提取候选信息

一、原件先于提取结果

模型处理的是原件的某种表示,提取结果不是原件本身。项目目录先建立raw/,只读保存收到的文件;再建立index.csv,为每个原件分配source_id。索引至少记录文件名、材料类型、提供者、获取日期、授权范围、是否含个人信息、文件摘要和备注。文件摘要是由普通程序根据文件内容计算的短标识,可帮助发现原件是否被替换,不代表授权或真实性。

未经授权的人脸、姓名、联系方式和对话不得为了“训练AI”进入共享数据。若材料含完成任务所必需的个人信息,应按单位制度缩小访问范围,课堂样例优先使用模拟或脱敏数据。删除原件也要保留删除原因和责任人,不能让清洗后的表格变成没有来历的孤立数据。

配套工程位于resources/ch02/project/,关键目录如下:

project/
├─ data/raw/                    # 不被候选覆盖的模拟原件
├─ data/source_index.csv        # 授权、隐私、SHA与来源组
├─ data/contract.json           # 可执行数据契约
├─ data/candidates_replay.json  # 明确非模型实测的离线候选
├─ candidate_adapter.py         # 可替换候选提取接口
├─ workflow.py                  # clean、issues、划分与报告
├─ evaluate_fixed_cases.py      # 冻结契约测试
├─ DATA_CARD.md
└─ acceptance.py

二、多模态模型只产生候选

输入给模型的提取任务应包含字段定义、允许值、缺失表示和来源编号。例如:“只从S003图片中读取可见铭牌信息;看不清的字段填null;每个非空值附原图中的证据短句或区域说明;不要根据同类设备猜型号。”参考S003正确地把时间与数值保留为null,而不是根据文件名或同类设备猜测。模型输出后,学生随机抽查,也要优先复核影响大、置信低或与其他材料冲突的字段。

参考运行没有调用多模态模型。OfflineReplayExtractor读取教师编写的8条候选并验证来源声明;CallableExtractor只定义获准模型的接入边界。这样既能学习模型如何进入工程,也不会把没有发生的模型调用写成结果。

文字清晰、格式稳定的扫描表格,可以把专用文字识别作为速度基线;需要同时理解图片、表格和上下文时,多模态模型可能更方便;少量关键字段由人工录入通常更可靠。技术选择取决于材料和错误后果,而不是默认“越通用越好”。

三、建立人工基线

参考工程提供4条教师确认的人工录入作为基线,并与相同4个模拟来源的离线候选比较地点、类型、时间、数值、单位、标签和证据7个字段。两者均为28/28字段相符。人工耗时没有测量,离线候选又不是真实模型输出,因此本章不声称AI更快或更准。

真实课堂可重新记录两名学生的录入时间、分歧和复核次数,再与获准模型比较。如果模型速度快但增加了无法发现的错误,应限制它只处理低风险字段;如果材料只有十几条且格式稳定,人工录入可能就是更合适的首版。没有这些真实记录时,正确结论是“尚不能比较效率”。

四、来源证据必须跟着数据走

候选记录至少保留source_id与evidence。后续发现数值异常时,可以回到具体原件,而不是重新翻遍文件夹。一个字段由多个来源共同支持时,应保留全部来源;来源相互冲突时,不要选择模型更喜欢的一项,要生成冲突记录并交责任人。

图示规划: 在此设置“原件—来源索引—候选字段—规则校验—人工复核—发布数据”的分层图,突出原件和候选数据不能相互覆盖。

第二节 设计字段、类型、单位、标签和数据契约

一、字段来自工作,不来自现有表格

字段应由后续工作输出反推。本项目要支持设施观察与统计,因此需要记录编号、来源、地点、设施类别、观察时间、现象、数值、单位、状态标签和复核状态。若后续不需要人员姓名,就不应因为原表中有一列而继续收集。

数据类型说明机器可以对值做什么。日期应使用统一格式,数值应保存为数值而不是“18度左右”这样的字符串,类别应来自允许列表。缺失不等于零,也不等于“正常”。本项目用null表示没有可确认的值,用status_label="待复核"说明记录仍需人判断。

二、单位和标签是专业约定

“温度=26”只有与摄氏度、测量时间、位置和设备状态一起才可解释。同一字段混用摄氏度和华氏度,程序仍可能画出漂亮曲线,却没有可比意义。数据契约需规定单位,转换时同时保留原值和转换规则。

标签是人按任务规则给样本的目标名称。本项目的status_label只允许“正常、待复核、超出服务范围”,不把“未知”强行标为“正常”。标签定义要写出正例、反例和边界。两位标注者对同一记录意见不同,说明规则可能不清楚,应先修订说明再扩大数据量。

三、把约定写成数据契约

数据契约可采用JSON或表格表达。本章首版规定:

字段 类型与允许值 是否必需 说明
record_id 字符串,唯一 是 由项目规则生成,不由模型猜测
source_id 必须存在于索引 是 指向原件
location 字符串或null 是 未知时保留null
asset_type 照明、桌椅、业务设备、环境之一 是 其他材料进入待复核
observed_at ISO日期时间或null 是 不从文件修改时间猜测
value 数值或null 是 仅传感记录使用
unit ℃、%RH、lx或null 是 有数值时必须有单位
status_label 正常、待复核、超出范围 是 需符合标签说明
evidence 原件证据短句或区域说明 是 支持候选值

契约既给模型看,也给程序和使用者看。修改字段或允许值必须提升版本号,并说明旧数据如何迁移。直接在表格里增加一列而不更新说明,会让不同成员对同一字段产生不同理解。

四、区分样本、特征和标签

一条设施观察记录可以作为一个样本。地点、时间、数值和现象中的可计算表示可能成为特征;经过专业规则确认的状态是标签。哪些字段成为特征取决于具体模型,不是所有收集来的信息都应输入模型。来源编号用于追踪,不应因其与某批次偶然相关就成为预测依据。

后续训练模型时,训练集用于调整参数,验证集用于选择设置,测试集用于最后检查。划分要按设备、批次或时间隔离。把同一设备同一分钟产生的相邻记录随机分散到训练和测试,会让测试结果过于乐观,这称为数据泄漏。

第三节 处理错误、缺失、重复、偏差和数据泄漏

一、运行完整最小校验程序

下面是便于课堂阅读的校验主链缩略。权威的workflow.py还检查原件SHA-256、授权、隐私、数值范围和来源级划分,并把每个问题保存为稳定代码。它不调用模型,也不替人改正事实;候选提取与确定性校验被有意分开。

from pathlib import Path
from datetime import datetime
import csv
import json

ROOT = Path(__file__).resolve().parent
ALLOWED_ASSETS = {"照明", "桌椅", "业务设备", "环境"}
ALLOWED_LABELS = {"正常", "待复核", "超出范围"}
ALLOWED_UNITS = {"℃", "%RH", "lx"}


def read_source_ids(path):
    with path.open(encoding="utf-8-sig", newline="") as file:
        rows = list(csv.DictReader(file))
    return {row["source_id"] for row in rows if row.get("authorized") == "yes"}


def valid_time(value):
    if value is None:
        return True
    try:
        datetime.fromisoformat(value)
        return True
    except (TypeError, ValueError):
        return False


def check(row, source_ids):
    problems = []
    required_fields = {
        "record_id", "source_id", "location", "asset_type", "observed_at",
        "value", "unit", "status_label", "evidence"
    }
    for field in sorted(required_fields - set(row)):
        problems.append(f"{field}:字段不存在")
    required_text = ["record_id", "source_id", "asset_type",
                     "status_label", "evidence"]
    for field in required_text:
        if field in row and (
            not isinstance(row[field], str) or not row[field].strip()
        ):
            problems.append(f"{field}:必须是非空字符串")
    location = row.get("location")
    if location is not None and (
        not isinstance(location, str) or not location.strip()
    ):
        problems.append("location:必须是非空字符串或null")
    if row.get("source_id") not in source_ids:
        problems.append("source_id:不存在或未授权")
    if row.get("asset_type") not in ALLOWED_ASSETS:
        problems.append("asset_type:不在允许列表")
    if row.get("status_label") not in ALLOWED_LABELS:
        problems.append("status_label:不在允许列表")
    if not valid_time(row.get("observed_at")):
        problems.append("observed_at:格式错误")
    value, unit = row.get("value"), row.get("unit")
    if value is not None:
        if isinstance(value, bool) or not isinstance(value, (int, float)):
            problems.append("value:必须是数值")
        if unit not in ALLOWED_UNITS:
            problems.append("unit:有数值时必须使用规定单位")
    elif unit is not None:
        problems.append("unit:没有数值时应为null")
    return problems


def main():
    source_ids = read_source_ids(ROOT / "data" / "source_index.csv")
    payload = json.loads(
        (ROOT / "data" / "candidates_replay.json").read_text(encoding="utf-8")
    )
    rows = payload["records"]
    clean, issues = [], []
    seen_ids, seen_evidence = set(), set()
    for row in rows:
        key = (row.get("source_id"), row.get("evidence"))
        problems = check(row, source_ids)
        record_id = row.get("record_id")
        if record_id in seen_ids:
            problems.append("record_id:重复")
        if key in seen_evidence:
            problems.append("duplicate:来源与证据重复")
        seen_ids.add(record_id)
        seen_evidence.add(key)
        if problems:
            issues.append({"record_id": row.get("record_id"),
                           "problems": ";".join(problems)})
        else:
            clean.append(row)
    out = ROOT / "output"
    out.mkdir(exist_ok=True)
    (out / "clean.json").write_text(
        json.dumps(clean, ensure_ascii=False, indent=2), encoding="utf-8"
    )
    with (out / "issues.csv").open("w", encoding="utf-8-sig", newline="") as file:
        writer = csv.DictWriter(file, fieldnames=["record_id", "problems"])
        writer.writeheader()
        writer.writerows(issues)
    print(f"通过结构校验{len(clean)}条,待处理{len(issues)}条")


if __name__ == "__main__":
    main()

程序中的允许列表和字段规则就是可执行的数据契约。实际运行8条候选后,4条进入clean,4条因未授权与个人信息、未知来源、错误单位或重复编号进入issues;同一记录可产生多个问题代码。学生不能把clean直接称为真实正确数据,它只表示通过当前契约,地点与标签仍需回看原件并由人确认。

二、错误、缺失和重复要分别处理

错误是已有值与证据不符,应更正并记录前值;缺失是没有足够信息,应保留null并说明是否需要补采;重复是同一事件被多次记录,应依据来源、时间和业务规则确认是否合并。不能用删除重复的方式抹掉真实重复发生的事件。

模型可能把设备铭牌上的额定电压当成环境传感值,也可能把图片拍摄日期当成观察日期。校验程序能发现部分格式问题,却不理解所有专业含义。因此问题表要包含“规则发现”和“人工发现”两类来源,避免把未被程序捕获的错误误认为不存在。

三、观察分布和偏差

统计不同地点、设备、时间段和标签的数量。如果“正常”记录全部来自白天,“待复核”全部来自夜间,模型以后可能利用时间代替真实状态。数据量大不能自动消除这种偏差。学生应查明差异来自真实工作分布、采集方便性还是标签规则。

控制变量实验。 保持原件、模型、提取说明和测试记录不变,只在B版数据契约中增加“有数值时单位必填”规则。先预测B版会发现多少此前看似通过的记录,再运行同一校验程序和同一候选数据。比较待处理数量及人工复核时间。若同时改变模型提示和候选文件,就无法说明变化来自契约规则。

四、设计失败测试和划分检查

fixed_validation_cases.json冻结6条不参与候选构造的样例:N01为正常记录,B01为有值无单位的边界,X01为陌生来源,P01为隐私文本,F01为错误日期,O01为越界湿度。evaluate_fixed_cases.py比较期望问题代码与实际代码;参考运行6/6通过。此外,单元测试还模拟索引哈希被改变、必需字段缺失和重复编号。

本书统一把这组在规则确定后才运行、不能为追逐结果而反复改写的封存样例称为固定测试。它与训练数据或候选回放彼此隔离,也不参与字段规则设计。固定测试通过只说明当前六类已声明行为可以重复,不能据此推断所有现实原件都能被正确整理。课堂新增一种来源、单位或敏感字段时,应先用非封存练习材料完善合同和程序,再为下一版本另建从未参与修改的新固定测试,并由另一位同学复核版本与来源记录。

清洗记录按来源组划分:S001与S002进入train,S003进入validation,S004进入test;同一来源不跨集合。这里只是治理演示,4条记录绝不足以训练模型。测试结论仍要分层:结构通过、来源可追、专业确认和可用于训练不是同一件事。

第四节 形成可复现的数据包并交给另一位使用者

一、固定目录、版本和生成步骤

可复现数据包不依赖作者记忆。配套README.md说明数据用途、目录、输入输出、真实结果和限制。运行入口只有一条:

cd resources/ch02/project
python acceptance.py

它检查26个必需文件和Python语法,重新生成数据包,运行固定测试与13项单元测试,再核对参考结果。清洗数据由脚本生成,不手工覆盖;人工修订进入新候选或修订记录,再重新生成。

数据卡回答:数据为解决什么任务而建;包含哪些对象和时间范围;怎样采集、授权、清洗和划分;有哪些缺失和偏差;适合与不适合什么用途;由谁维护;如何反馈错误。数据卡不是宣传页,应明确写出不足。

二、让另一组真正复现

交接组只获得项目包和说明,不接受原组口头提示。它应能核对文件摘要,运行校验,得到相同的通过与问题数量,并随机回查3条记录的来源。如果复现者不知道某列单位、找不到原件、运行后覆盖了人工记录,说明数据包仍有隐含假设。

参考复现得到:5个原件摘要全部一致,8条候选中clean=4、rejected=4,冻结测试6/6;离线候选和人工基线均为28/28字段相符。结论为usable_for_teaching_only,因为原件是模拟材料、候选不是模型实测、人工时间未记录。差异可能来自文件丢失、脚本版本不同、格式被修改或说明不完整,必须保留日志再做限定修改。

复现时还应逐层解释“通过”的含义。5个摘要一致,只能证明本次读取的原件没有相对索引发生变化;4条进入clean_records.json,只说明它们通过当前字段、单位、范围、授权和隐私规则,不证明现场事实一定正确;6/6冻结测试说明校验器按预先约定处理了正常、边界、陌生、隐私、错误日期和越界数值,不说明规则覆盖了所有现实情况。最后仍要打开issues.csv,沿source_id回到原件,判断拒绝原因是否准确。另一组若只报告“验收成功”而说不清这三层差别,复现任务仍未完成。

工程把故障也作为交付的一部分:原件哈希变化时停止,必需字段缺失时报错,记录编号重复时两条都进入问题表,未授权或含个人信息的来源不得因候选内容看似无害而放行。学生可以请大语言模型解释某个问题代码,但不能让它直接改写原件、伪造授权或猜测缺失单位。每一次人工纠正都应保留旧值、依据、修改人和新版本,再由相同入口重新生成数据包。

可追溯比表面整洁更重要。

三、专业迁移卡

专业迁移卡A:设计与品牌素材库
输入改为品牌手册、标志文件、字体授权、历史海报和图片素材;规范包括颜色值、最小尺寸、版权和可修改范围;高代价错误是误用标志、混淆授权或泄露未发布作品。交付物为素材索引、权利字段、可用规格、缩略图和审核记录,由品牌负责人及权利审核人员批准。

专业迁移卡B:智能制造设备日志
输入改为设备台账、控制器日志、点检记录和经授权的铭牌照片;单位、采样频率、设备编号和停机时刻必须统一;高代价错误是跨设备合并、时间错位或把维护后数据泄漏进此前的测试。交付物为按设备与批次划分的数据包,由设备工程人员确认标签和使用边界。

专业迁移卡C:生物培养观察数据
输入改为培养批次、环境记录、显微或外观图像和实验备注;需要遵守样本编码、实验条件和伦理授权;高代价错误是混淆批次、单位或把探索性观察当成诊断结论。交付物为批次隔离数据、数据字典和实验复核表,由实验指导人员确认。

四、作出发布决定

数据包可以“发布供本项目使用”“限内部复核使用”或“退回补采”。本参考包只获准作为教材模拟数据使用,不得对外宣称真实工作现场数据或模型性能。决定依据包括授权完整性、来源可追性、字段通过率、标签一致性、偏差、泄漏风险和复现结果。缺少关键授权、原件摘要改变或测试集泄漏时,即使程序能运行,也必须停止发布。

本章小结

本章把零散材料变成了可追溯的数据底座。原件、授权和来源必须在AI提取之前建立;多模态模型产生候选值,数据契约规定字段、类型、单位、标签和缺失方式,普通程序执行确定性检查,专业人员确认事实和标签。结构通过不等于数据正确,可用于观察也不等于可用于训练。可靠数据包应记录错误、缺失、重复、偏差和划分方法,并由另一组按照说明复现。数据质量不是模型训练前的一次清洁工作,而是贯穿更新和交付的责任。

关键术语

目标测试

  1. 多模态模型从铭牌图片中读出的型号首先应被视为什么?
    A. 已确认事实 B. 候选值 C. 训练标签 D. 授权证明
  2. 数值字段为26但没有单位时,最恰当的处理是什么?
    A. 默认摄氏度 B. 删除记录 C. 标记待处理并回查来源 D. 改为0
  3. 下列哪种划分最能降低同一设备数据泄漏?
    A. 相邻记录随机拆分 B. 按设备或批次整体划分 C. 复制到三个集合 D. 只使用训练集
  4. 校验程序显示“通过结构校验”意味着什么?
    A. 专业事实一定正确 B. 已获得所有授权 C. 记录符合当前可执行字段规则 D. 一定适合训练
  5. 判断并改错:“缺失值和数值0都表示没有信息,可以相互替换。”
  6. 判断并改错:“数据越多,采集偏差和标签偏差就会自动消失。”
  7. 为一张经授权的设备铭牌图片设计四项来源索引字段,并说明各自用途。
  8. 给出一条有数值、单位缺失的候选记录,说明校验、人工复核和修订记录应怎样衔接。
  9. 设计一次只改变“单位必填规则”的控制变量实验,写出预测、保持不变条件和比较指标。
  10. 交接组可以运行脚本,但随机抽查时找不到5条记录的原件。请作出发布、限用或退回结论,并列出两项修复要求。