截至2026年10月7日,Shifter拥有规模最大的美国实时IP池,超过Oxylabs和Bright Data,而成本只是其一小部分。Shifter 现在拥有数量最多的美国活跃 IP 资源池

查看基准测试

AI训练流程中的数据溯源:证明你的数据集来源

AI训练数据溯源在实践中的含义:每份文档需要保留的记录、欧盟AI法案摘要的要求,以及如何将其贯彻始终。

数据科学 · 更新于2026年10月24日 · 4分钟阅读
抓取提取去重训练sha256 · url · fetched_at已排除: 2
每份训练副本都可追溯至其抓取记录。琥珀色记录被排除在外,并保持记录在案。
知识
本页内容
  1. 溯源为何成为一项要求
  2. 每份文档应记录哪些内容
  3. 代码中的溯源记录
  4. 在整个流程中携带溯源信息
  5. 值得借鉴的标准
  6. 溯源通常在哪里出问题
  7. 结论
  8. 来源与参考文献

询问一个 AI 团队某份训练文档的来源,得到的答案往往是数据集名称,而不是来源本身。监管机构、客户和法院现在想要的是第二种答案:哪个页面、什么时候抓取的、在何种条款下、以及训练前对它做了什么处理。对于那些从网络数据构建模型的团队来说,溯源已经从一种良好实践变成了必须能够拿出证明的事情。

难点在于时机。溯源记录所需的大部分信息,只在采集那一刻才存在。本指南将介绍这个问题为何变得紧迫、每份文档值得保留哪些记录,以及如何在提取、过滤和去重过程中保持这条线索不断。

溯源为何成为一项要求

三项进展让溯源从一个研究课题变成了一项合规任务。

欧盟《人工智能法案》要求公开摘要。 第53(1)(d)条要求通用人工智能模型的提供者发布”关于用于训练的内容的足够详细的摘要”,并使用人工智能办公室提供的模板。这些义务自2025年8月2日起适用,而在该日期之前已上市的模型则有时间延至2027年8月2日。欧盟委员会于2025年7月提出了该模板,并说明使用该模板是强制性的。

该模板要求提供具体信息。 对于抓取的数据,它要求提供所使用的爬虫名称、采集时间段,以及最相关域名的列表:按抓取内容规模排名前10%的所有域名,或对中小型公司而言为前5%或1,000个域名,以较低者为准。它还要求提供者说明他们在”数据采集之前和期间”如何遵守文本与数据挖掘的退出声明。第53(1)(c)条另行要求提供一项版权政策,用以识别并遵守这些保留声明。

公共数据资源正在萎缩,而相关记录一直很差。 数据溯源倡议(Data Provenance Initiative)在2023年审查了超过1,800个文本数据集,发现许可证缺失率超过70%,在广泛使用的托管网站上错误率超过50%。一项针对14,000个网络域名的后续审查发现,在单单一年内,C4数据集中约5%的所有词元(token),以及其中维护最活跃来源的28%,已被robots.txt完全限制。某个在抓取时可以接受的数据集,在今天抓取可能就不再被接受,而你只有在当时记录了状态,才能证明这种差异。

每份文档应记录哪些内容

溯源记录由爬虫在抓取时一次性写入。下游的每一步都是在其基础上增加内容,而不是取代它。

字段为何重要
来源 URL域名列表以及任何下架请求的依据
抓取时间(UTC)证明采集时间段以及该日期的退出状态
响应状态区分真实内容与屏蔽页面、错误页面
原始正文的哈希值将之后的每一份副本与收到的确切内容绑定
观测点同一个 URL 在不同国家和网络下可能返回不同内容
robots.txt 判定结果表明爬虫在抓取时遵守了当时生效的规则
观察到的 TDM 保留声明欧盟版权规则和该模板所关心的退出声明
声明的许可证或条款页面上关于再利用的声明内容(如有)
爬虫名称和版本模板要求提供爬虫名称;版本信息能解释行为变化
父记录将清理或去重后的副本链接回其来源

有两个字段经常被遗漏。第一个是观测点:从一个国家或网络观察到的页面,可能与在其他地方看到的同一 URL 不同,这正是我们那篇关于记录网络数据观测位置的文章所论证的。第二个是原始正文本身。哈希值能证明你收到的是哪些字节,但保留响应本身,例如通过将页面归档为 WARC 文件,才能让你展示它。

代码中的溯源记录

下面的示例为每份抓取的文档写入一条记录,只保留在抓取时被允许且未被保留声明限制的文档,并通过哈希值将每份提取的副本链接回其来源。它还生成了摘要模板所要求的域名排名。该示例可在 Python 3.9 及以上版本中运行,只使用标准库。

from __future__ import annotations

import hashlib
import json
from collections import Counter
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from urllib.parse import urlsplit


def sha256(data: bytes) -> str:
    return hashlib.sha256(data).hexdigest()


@dataclass
class Provenance:
    """One record per fetched document, written at collection time."""
    url: str
    fetched_at: str            # ISO 8601, UTC
    status: int
    content_sha256: str        # hash of the raw response body
    vantage: str               # where it was observed from, e.g. "residential:de"
    robots_allowed: bool       # robots.txt decision for our crawler, at fetch time
    tdm_reserved: bool         # machine-readable TDM opt-out seen, at fetch time
    license: str | None        # licence or terms as stated on the page, if any
    collector: str             # crawler name and version
    parents: list[str] = field(default_factory=list)  # hashes this record derives from
    step: str = "fetch"        # fetch, extract, filter, dedup ...


def fetched(url, body, status, vantage, robots_allowed, tdm_reserved, license=None,
            collector="corpus-crawler/1.4"):
    return Provenance(
        url=url,
        fetched_at=datetime.now(timezone.utc).isoformat(timespec="seconds"),
        status=status,
        content_sha256=sha256(body),
        vantage=vantage,
        robots_allowed=robots_allowed,
        tdm_reserved=tdm_reserved,
        license=license,
        collector=collector,
    )


def derived(parent: Provenance, new_body: bytes, step: str) -> Provenance:
    """A transformed copy keeps the source fields and points back at its parent."""
    rec = Provenance(**{**asdict(parent), "parents": [parent.content_sha256], "step": step})
    rec.content_sha256 = sha256(new_body)
    return rec


def eligible(rec: Provenance) -> bool:
    """Only documents that were allowed and not reserved at fetch time go further."""
    return rec.status == 200 and rec.robots_allowed and not rec.tdm_reserved


def domain_summary(records, top_share=0.1):
    """Domains by document count, and the top share of them, for a training-data summary."""
    counts = Counter(urlsplit(r.url).hostname for r in records)
    ranked = [d for d, _ in counts.most_common()]
    keep = max(1, round(len(ranked) * top_share))
    return counts, ranked[:keep]


if __name__ == "__main__":
    raw = [
        fetched("https://news.example/a", b"<html>a</html>", 200, "residential:de", True, False),
        fetched("https://news.example/b", b"<html>b</html>", 200, "residential:de", True, False),
        fetched("https://shop.example/p/1", b"<html>p1</html>", 200, "residential:fr", True, True),
        fetched("https://blog.example/x", b"<html>x</html>", 403, "residential:us", True, False),
        fetched("https://forum.example/t/9", b"<html>t9</html>", 200, "residential:us", False, False),
    ]
    kept = [r for r in raw if eligible(r)]
    texts = [derived(r, r.url.encode() + b" text", "extract") for r in kept]

    with open("manifest.jsonl", "w") as f:
        for rec in raw + texts:
            f.write(json.dumps(asdict(rec)) + "\n")

    counts, top = domain_summary(raw)
    print(f"fetched {len(raw)}, eligible {len(kept)}, extracted {len(texts)}")
    print("domains:", dict(counts))
    print("top domains:", top)
    child = texts[0]
    print("lineage ok:", child.parents == [kept[0].content_sha256], child.step, child.url)

输出:

fetched 5, eligible 2, extracted 2
domains: {'news.example': 2, 'shop.example': 1, 'blog.example': 1, 'forum.example': 1}
top domains: ['news.example']
lineage ok: True extract https://news.example/a

被保留声明限制的页面、被屏蔽的页面以及被robots.txt禁止的页面,都仍然保留在清单中。这一点很重要:你选择不使用的内容的记录,也是证明的一部分。该模板按抓取内容的规模对域名进行排名,所以在实际生产中,应统计字节数或词元数,而不是文档数量。

在整个流程中携带溯源信息

在抓取时写入的记录,只有当之后的每一步都保留这条链接时,才有用。

  • 提取。 你用来训练的文本并不是你抓取的 HTML。给提取出的文本赋予自己的哈希值,并让它指向原始记录,就像上面 derived() 所做的那样。
  • 过滤。 既要记录某份文档被丢弃这件事,也要记录丢弃的原因。“低于质量阈值”和”TDM保留声明”对监管机构来说是不同的答案。
  • 去重。 当近似重复的内容被合并时,保留幸存副本所代表的每一个来源的列表。否则,针对某个 URL 的下架请求将无法找到它最终进入的那份文档。
  • 混合与采样。 记录每次训练运行使用的是哪个版本的清单,这样摘要描述的才是模型实际看到的数据。
  • 移除。 保留一个以 URL 和哈希值为键的移除清单。当某个网站之后声明保留其权利,或权利持有者要求移除时,相同的键可以在每一个衍生数据集中找到该文档。

退出状态尤其值得关注。robots.txt 文件或文本与数据挖掘保留声明在你抓取之后可能发生变化,变好变坏都有可能。记录下你在抓取时所做的判定,并在每次新的采集之前重新检查,而不是假设去年的答案仍然成立。

值得借鉴的标准

你不需要从零开始发明一套模式。

  • 《数据集数据表》(Datasheets for Datasets)(Gebru 等人,发表于《ACM通讯》,2021年)提出,每个数据集都应记录其动机、组成、采集过程和推荐用途。这正是数据集层面摘要应有的形式。
  • 数据与可信人工智能联盟(Data & Trusted AI Alliance)的数据溯源标准在三个组别(来源、溯源和用途)中定义了22个元数据字段,包括数据生成的日期范围、方法、使用许可以及同意文件的存放位置。1.0.0版本于2024年7月发布。
  • **C2PA 内容凭证(Content Credentials)**将一份经签名的溯源清单附加到单个媒体资产上。它们对图像、音频和视频最为有用,因为资产本身可以携带其记录。

溯源通常在哪里出问题

  • 事后才补充记录。 没有逐份文档记录的抓取,无论数据集说明文档写得多好,事后都无法被审计。
  • 许可证是从数据集说明文档中照搬的。 上述审查发现,这正是错误集中出现的地方。记录来源页面实际说了什么,并把汇总的标签当作需要核实的说法。
  • 屏蔽页面被当作内容计入。 200响应仍然可能是一个验证挑战页面。状态码和内容检查都应纳入记录。
  • 观测点不明。 从本地的住宅连接采集每个市场的数据,只有在记录中注明每次抓取使用的国家和网络时,才对溯源有帮助。

结论

溯源信息在采集时记录成本很低,但事后几乎不可能补救。每份文档在抓取时写入一条记录,让哈希值贯穿每一次转换,既保留你排除的内容,也保留你采纳的内容,这样《人工智能法案》所要求的摘要就会变成一次查询,而不是一场调查。

来源与参考文献

常见问题

什么是AI训练数据的数据溯源?

它是关于每条训练数据来源及其经历的记录:来源URL或数据集、采集时间与地点、采集那一刻的选择退出和许可状态,以及它在进入训练数据集过程中经历的每一步转换。

欧盟AI法案是否要求按每个URL提供溯源信息?

不要求。通用人工智能模型的提供者必须使用欧盟委员会的模板发布训练内容摘要,其中要求列出主要数据集、最相关的抓取域名、爬虫名称、采集周期以及选择退出的遵守情况。列出单个URL属于自愿行为,但如果没有按文档记录的信息,就无法可靠地生成所要求的摘要。

数据集构建完成后能否重新还原溯源信息?

只能部分还原。来源URL有时可以恢复,但选择退出状态、实际收到的响应以及抓取时的观测位置一旦过去就无法还原。这正是为什么溯源信息必须由爬虫在抓取时写入,而不能事后补充。

每份文档所需的最低溯源记录包括哪些内容?

来源URL、UTC格式的抓取时间、响应状态、原始正文的哈希值、观测所用的接入位置、抓取时robots.txt及文本与数据挖掘选择退出的决定、任何声明的许可、爬虫名称和版本,以及指向其衍生来源记录的链接。

准备好开始了吗?

试用 Shifter 住宅代理,205M+ 个 IP,195+ 个国家,低至 $0.10/GB。

立即开始