训练一个高质量的大语言模型(LLM),你需要什么?
算力?当然。模型架构?必须的。但最关键的是:数据。
海量、干净、多样化的文本数据。
问题是:如何获取这些数据?
传统方式是爬虫 + 清洗,耗时耗力。现在,有更好的方法。
LLM训练数据的要求
数量要求
规模:
- 小模型(1B参数):10-50GB文本
- 中模型(7B参数):100-500GB文本
- 大模型(70B+参数):数TB文本
来源:
- 网页内容
- 书籍和文章
- 代码仓库
- 对话数据
质量要求
不是所有文本都适合训练:
好的训练数据:
- 语法正确
- 逻辑清晰
- 信息丰富
- 格式统一
差的训练数据:
- 充满广告和垃圾信息
- 格式混乱(HTML标签、乱码)
- 重复内容
- 低质量机器生成文本
多样性要求
领域多样性:
- 新闻
- 科技
- 医疗
- 法律
- 文学
- 日常对话
风格多样性:
- 正式文体
- 口语化
- 专业术语
- 通俗表达
传统数据准备的痛点
痛点1:网页爬取复杂
挑战:
- 反爬虫机制
- 动态加载内容
- 验证码
- IP封禁
成本:
- 代理IP:数万元/月
- 开发和维护:数人月
- 法律风险
痛点2:内容提取困难
HTML充满噪音:
<div class="header">...</div>
<nav>...</nav>
<aside class="ads">广告...</aside>
<div class="social-share">...</div>
<article>
<h1>真正的内容</h1>
<p>正文...</p>
</article>
<footer>...</footer>
<script>大量JavaScript代码...</script>
如何准确提取<article>中的内容?
每个网站结构不同,需要定制化。
痛点3:格式统一耗时
原始HTML需要转换为训练友好的格式:
- 去除HTML标签
- 保留文本结构(标题、段落)
- 统一格式(如Markdown)
- 处理特殊字符
手动处理:不现实
自动化脚本:容易出错
痛点4:质量控制难
如何过滤低质量内容?
- 广告和垃圾信息
- 重复内容
- 机器生成文本
- 错误信息
需要复杂的启发式规则或ML模型。
痛点5:法律和伦理
爬虫面临:
- 版权问题
- 隐私问题
- 服务条款违反
- 法律风险
Reader API:简化数据准备
Reader API专为内容提取设计,完美契合LLM训练需求。
核心功能
1. 智能内容提取
自动识别主要内容,去除噪音:
const content = await readerAPI.extract('https://example.com/article');
// 返回干净的内容
{
"title": "文章标题",
"content": "# 文章标题\n\n正文内容...",
"author": "作者",
"publishedDate": "2024-01-01",
"url": "https://example.com/article"
}
无需:
- 分析HTML结构
- 写选择器
- 处理每个网站的特殊性
2. Markdown输出
直接输出训练友好的格式:
# 文章标题
## 第一部分
段落内容...
## 第二部分
更多内容...
优势:
- 保留结构(标题、列表)
- 纯文本,易处理
- 统一格式
3. 元数据提供
获取有用的元数据:
- 发布时间(过滤过时内容)
- 作者(评估可信度)
- 来源URL(溯源)
- 语言(多语言训练)
4. 批量处理
高效处理大量URL:
const urls = [...]; // 数万个URL
const contents = await Promise.all(
urls.map(url => readerAPI.extract(url))
);
5. 错误处理
自动处理:
- 无法访问的页面
- 格式异常的内容
- 超时
返回状态码,易于重试。
实战:构建训练数据集
步骤1:收集URL列表
从哪里获取URL?
方法1:使用SERP API
搜索特定主题的文章:
const topics = ['人工智能', '机器学习', '深度学习', ...];
const urls = [];
for (const topic of topics) {
const results = await serpAPI.search({
query: topic,
num: 100
});
urls.push(...results.map(r => r.url));
}
方法2:网站地图
大型网站的sitemap.xml:
const sitemap = await fetch('https://example.com/sitemap.xml');
const urls = parseSitemap(sitemap);
方法3:现有数据集
开源URL列表:
- Common Crawl
- C4数据集
- 学术论文链接
步骤2:批量提取内容
const BATCH_SIZE = 100;
const DELAY = 1000; // 1秒延迟,避免过载
async function extractBatch(urls) {
const results = [];
for (let i = 0; i < urls.length; i += BATCH_SIZE) {
const batch = urls.slice(i, i + BATCH_SIZE);
console.log(`Processing batch ${i/BATCH_SIZE + 1}...`);
const batchResults = await Promise.all(
batch.map(async url => {
try {
const content = await readerAPI.extract(url);
return { success: true, url, content };
} catch (error) {
return { success: false, url, error: error.message };
}
})
);
results.push(...batchResults);
// 延迟,避免过载
await new Promise(resolve => setTimeout(resolve, DELAY));
}
return results;
}
步骤3:质量过滤
function filterQuality(content) {
// 1. 长度检查
if (content.content.length < 500) {
return false; // 太短
}
if (content.content.length > 50000) {
return false; // 太长,可能是列表页
}
// 2. 语言检查
if (!isChineseText(content.content)) {
return false; // 非中文
}
// 3. 重复内容检查
if (isDuplicate(content.content)) {
return false;
}
// 4. 质量评分
const score = calculateQualityScore(content.content);
if (score < 0.6) {
return false;
}
return true;
}
function calculateQualityScore(text) {
let score = 1.0;
// 惩罚因素
if (text.includes('点击这里')) score -= 0.1;
if (text.includes('广告')) score -= 0.1;
if (text.match(/[\u4e00-\u9fa5]/g).length / text.length < 0.5) {
score -= 0.2; // 中文占比太低
}
// 奖励因素
if (hasClearStructure(text)) score += 0.1;
if (hasRichInformation(text)) score += 0.1;
return Math.max(0, Math.min(1, score));
}
步骤4:去重
import { createHash } from 'crypto';
const seenHashes = new Set();
function deduplication(contents) {
return contents.filter(content => {
const hash = createHash('sha256')
.update(content.content)
.digest('hex');
if (seenHashes.has(hash)) {
return false; // 重复
}
seenHashes.add(hash);
return true;
});
}
步骤5:格式转换
转换为训练格式(如JSONL):
function saveAsJSONL(contents, filename) {
const stream = fs.createWriteStream(filename);
for (const content of contents) {
const record = {
text: content.content,
source: content.url,
date: content.publishedDate,
metadata: {
title: content.title,
author: content.author
draft: false
}
};
stream.write(JSON.stringify(record) + '\n');
}
stream.end();
}
完整流程
async function buildTrainingDataset(topics, outputFile) {
console.log('Step 1: Collecting URLs...');
const urls = await collectURLs(topics);
console.log(`Collected ${urls.length} URLs`);
console.log('Step 2: Extracting content...');
const results = await extractBatch(urls);
const successful = results.filter(r => r.success);
console.log(`Extracted ${successful.length} articles`);
console.log('Step 3: Quality filtering...');
const filtered = successful.filter(r =>
filterQuality(r.content)
);
console.log(`${filtered.length} articles passed quality check`);
console.log('Step 4: Deduplication...');
const deduplicated = deduplication(filtered);
console.log(`${deduplicated.length} unique articles`);
console.log('Step 5: Saving...');
saveAsJSONL(deduplicated.map(r => r.content), outputFile);
console.log('Done!');
return {
total: urls.length,
extracted: successful.length,
filtered: filtered.length,
final: deduplicated.length
};
}
// 使用
const stats = await buildTrainingDataset(
['人工智能', '机器学习', '深度学习'],
'training_data.jsonl'
);
高级技巧
技巧1:分类数据集
按领域分类,方便平衡:
const categories = {
'tech': ['人工智能', '编程', '科技'],
'news': ['时事', '财经', '政治'],
'literature': ['小说', '散文', '诗歌']
};
for (const [category, topics] of Object.entries(categories)) {
await buildTrainingDataset(
topics,
`training_data_${category}.jsonl`
);
}
技巧2:时间范围控制
只要最近的内容:
function isRecent(content, months = 12) {
if (!content.publishedDate) return true; // 未知日期,保留
const publishDate = new Date(content.publishedDate);
const cutoff = new Date();
cutoff.setMonth(cutoff.getMonth() - months);
return publishDate >= cutoff;
}
技巧3:多样性采样
避免某个来源占比过高:
function diversifySources(contents, maxPerSource = 100) {
const bySource = {};
for (const content of contents) {
const domain = new URL(content.url).hostname;
if (!bySource[domain]) bySource[domain] = [];
bySource[domain].push(content);
}
const sampled = [];
for (const source of Object.values(bySource)) {
sampled.push(...source.slice(0, maxPerSource));
}
return sampled;
}
技巧4:增量更新
定期添加新数据:
async function incrementalUpdate(existingData, topics) {
// 加载已有URL
const existingURLs = new Set(existingData.map(d => d.source));
// 收集新URL
const allURLs = await collectURLs(topics);
const newURLs = allURLs.filter(url => !existingURLs.has(url));
console.log(`Found ${newURLs.length} new URLs`);
// 处理新数据
const newData = await processURLs(newURLs);
// 合并
return [...existingData, ...newData];
}
成本对比
方案A:自建爬虫
开发成本:
- 爬虫开发:1人月 = ¥30,000
- 内容提取:1人月 = ¥30,000
- 质量控制:0.5人月 = ¥15,000
- 总计:¥75,000
运营成本(月):
- 代理IP:¥10,000
- 服务器:¥3,000
- 维护:¥10,000
- 总计:¥23,000/月
首年总成本:¥75,000 + ¥23,000 × 12 = ¥351,000
方案B:使用Reader API
开发成本:
- API集成:0.5人日 = ¥2,000
- 数据管道:2人日 = ¥8,000
- 总计:¥10,000
运营成本(月):
- Reader API:¥5,000(100万次请求)
- 服务器:¥1,000
- 总计:¥6,000/月
首年总成本:¥10,000 + ¥6,000 × 12 = ¥82,000
节省:¥269,000(77%)
实际案例
案例:某AI公司的训练数据准备
背景:
- 训练7B中文模型
- 需要100GB高质量中文文本
- 团队3人,预算有限
方案:
- 使用SERP API收集URL
- 使用Reader API提取内容
- 自动化质量过滤和去重
执行:
- 收集了50万个URL
- 提取了40万篇文章
- 质量过滤后保留30万篇
- 去重后得到25万篇独特文章
- 总计120GB文本
时间:
- API集成和管道开发:3天
- 数据收集和处理:1周(自动运行)
- 总计:10天
成本:
- API费用:¥15,000
- 人力:3人 × 10天 = ¥30,000
- 总计:¥45,000
如果自建爬虫:
- 时间:至少2个月
- 成本:¥100,000+
- 法律风险:高
结果:
- 节省55%成本
- 缩短80%时间
- 降低法律风险
- 数据质量高
案例:某大学研究项目
背景:
- 研究中文NLP
- 需要特定领域数据(医疗)
- 预算极其有限
方案:
- 使用SearchCans免费额度测试
- 付费后按需使用
- 专注于高质量来源
执行:
- 精选100个权威医疗网站
- 提取1万篇高质量文章
- 人工审核关键样本
成本:
- API费用:¥3,000
- 学生劳动:免费
- 总计:¥3,000
成果:
- 发表了2篇论文
- 开源了数据集
- 推动了领域发展
最佳实践
1. 选择高质量来源
不要追求数量,要追求质量:
- 权威网站
- 专业出版物
- 经过编辑的内容
2. 平衡数据集
避免偏见:
- 多个领域
- 不同风格
- 多样化来源
3. 持续更新
语言在演进,数据也要:
- 定期添加新数据
- 更新过时内容
- 追踪新趋势
4. 记录来源
便于:
- 合规性审查
- 质量追溯
- 引用和致谢
5. 尊重版权
- 遵守robots.txt
- 尊重版权声明
- 合理使用原则
- 必要时获得许可
结语
LLM训练数据准备曾经是繁琐、昂贵、耗时的过程。
Reader API将其简化为:
- 收集URL
- 调用API
- 过滤和保存
几行代码,几天时间,几千元成本,就能构建高质量训练数据集。
这让:
- 小团队能训练自己的模型
- 研究者能快速实验
- 企业能降低成本
技术应该让AI民主化,而不是成为大公司的专利。
Reader API正是这样的工具。
相关阅读:
开始构建你的训练数据集。免费注册SearchCans,使用Reader API高效提取网络内容,获取¥30体验额度。