
1. 为什么数据格式是LLM应用的第一道坎做LLM应用开发这几年我踩过最多的坑不是模型选型也不是提示词设计而是数据格式。听起来好像没什么技术含量但实际项目里百分之七八十的诡异bug都跟数据格式有关系。你可能觉得JSON、CSV这些东西有什么好讲的但当你把几百条训练数据喂给模型结果loss曲线跟心电图一样乱跳的时候你就知道格式问题有多要命了。LLM常见数据格式这个话题说白了就是搞清楚大语言模型在整个生命周期里——从预训练、微调、推理到评估——到底吃什么样的数据每种格式长什么样什么场景该用什么格式以及格式转换时有哪些坑。这篇文章适合所有正在做LLM相关工作的开发者不管你是刚入门想搞清楚基本概念还是已经做过几个项目但总在数据环节翻车都能从这里找到可以直接抄作业的方案。我自己的经验是很多人花大量时间研究模型架构和训练技巧却在数据格式上非常随意觉得“能跑就行”。但实际上数据格式决定了你的数据处理流水线能不能复用、训练能不能稳定收敛、推理结果能不能被下游系统正确解析。格式选错了后面全是返工。2. LLM全生命周期中的数据格式全景2.1 预训练阶段从原始文本到Token序列预训练阶段的数据格式相对“粗放”核心就是把海量文本变成模型能吃的token序列。这个阶段最常见的是纯文本格式.txt、.jsonl每条数据就是一个长文本片段。但这里有个容易被忽略的细节文档之间的分隔符。我见过有人直接把几千篇文档拼成一个巨大的txt文件结果模型学出来的东西前言不搭后语。正确的做法是在文档之间加入特殊的分隔token比如|endoftext|让模型知道“这里是一篇新文档的开始”。预训练数据的典型格式是这样的{text: 这是一篇文档的完整内容..., meta: {source: web, date: 2024-01}} {text: 这是另一篇文档..., meta: {source: book, date: 2023-06}}每条JSON一行这种格式叫JSONLJSON Lines。为什么用JSONL而不是标准JSON数组因为JSONL可以流式读取不需要把整个文件加载到内存里。预训练数据动辄几十上百GB用标准JSON数组格式光解析就能把你的内存撑爆。注意预训练数据里的meta字段不是给模型看的是给你自己做数据溯源和过滤用的。别把meta也tokenize了喂给模型那是浪费算力。2.2 微调阶段指令格式的百花齐放到了微调阶段数据格式就复杂多了。因为微调的目标是让模型学会“按照指令做事”所以数据必须包含指令、输入、输出这几个部分。但具体怎么组织不同框架、不同模型有不同的偏好。最常见的是Alpaca格式{ instruction: 将以下句子翻译成英文, input: 今天天气真好, output: The weather is really nice today. }还有ShareGPT格式这种格式支持多轮对话{ conversations: [ {from: human, value: 帮我写一个Python函数}, {from: gpt, value: 好的这是一个示例函数...}, {from: human, value: 能加上类型注解吗}, {from: gpt, value: 当然可以修改后的版本如下...} ] }这两种格式的区别很关键。Alpaca格式适合单轮指令任务结构简单但表达多轮对话很别扭。ShareGPT格式天然支持多轮但解析起来稍微复杂一点。我个人的选择是如果你的任务以单轮为主用Alpaca格式就够了如果涉及多轮对话、角色扮演、复杂交互果断上ShareGPT格式。2.3 推理阶段输入输出的格式约定推理阶段的数据格式往往被忽视但它直接决定了你的API能不能被下游系统正确调用。最常见的推理输入格式就是messages数组{ model: your-model, messages: [ {role: system, content: 你是一个 helpful assistant}, {role: user, content: 你好} ], temperature: 0.7, max_tokens: 1024 }这个格式看起来简单但有几个坑第一system message不是所有模型都支持有些模型会直接忽略它第二多轮对话的历史消息怎么截断是按token数还是按轮数这个策略直接影响推理成本和效果第三输出格式的控制如果你需要模型返回JSON最好在prompt里明确说明并且用response_format参数如果API支持来约束。2.4 评估阶段让结果可量化评估阶段的数据格式核心是“可对比”。你需要一个标准答案reference和模型输出prediction放在一起才能计算各种指标。{ question: 中国的首都是哪里, reference: 北京, prediction: 中国的首都是北京。, metrics: {exact_match: 0, f1: 0.67} }这里有个经验评估数据的格式最好和训练数据保持一致这样你可以直接用同一套数据加载代码。但评估数据需要额外增加reference字段而且往往需要人工标注所以量不会太大格式上可以稍微“奢侈”一点把能加的元信息都加上。3. 核心格式深度拆解与实操要点3.1 JSONLLLM数据处理的万能容器JSONL是我最推荐的数据格式没有之一。它的核心优势是每行一个独立的JSON对象可以逐行读取、逐行处理、逐行写入。这意味着你可以处理比内存大得多的数据集而且中途出错不会导致整个文件损坏。但JSONL有几个实操要点必须注意第一编码问题。JSONL文件必须是UTF-8编码而且不能有BOM头。我遇到过好几次因为文件带了BOM头导致第一行解析失败的情况。用Python读取时指定encodingutf-8而不是utf-8-sig写入时也要确保不写入BOM。第二换行符问题。JSONL的每一行必须以\n结尾不能是\r\n。在Windows上编辑的文件经常是\r\n到了Linux服务器上解析就会多出一个\r字符。解决方案是在读取时用line.strip()去掉首尾空白或者统一用newline\n打开文件。第三特殊字符转义。JSON标准要求对双引号、反斜杠、控制字符进行转义。但有些数据里包含未转义的特殊字符导致json.loads()报错。我的做法是在写入时用json.dumps(obj, ensure_asciiFalse)读取时用json.loads(line)并且加上try-except来跳过损坏的行。import json def read_jsonl(path): data [] with open(path, r, encodingutf-8) as f: for line_num, line in enumerate(f, 1): line line.strip() if not line: continue try: data.append(json.loads(line)) except json.JSONDecodeError as e: print(f第{line_num}行解析失败: {e}) return data提示处理大规模JSONL时不要一次性读入内存。用生成器逐行yield配合多进程处理效率会高很多。3.2 Parquet大规模训练数据的首选当数据量超过几个GBJSONL的读取速度就成了瓶颈。这时候Parquet格式的优势就体现出来了。Parquet是列式存储压缩率高读取速度快而且支持schema演化。但Parquet不是万能的。它的缺点是不像JSONL那样人类可读调试起来麻烦。而且Parquet的写入需要依赖pyarrow或fastparquet库环境配置稍微复杂一点。我通常的做法是原始数据用JSONL存储方便检查和修改预处理完成后转换成Parquet用于训练。转换代码大概长这样import pandas as pd df pd.read_json(data.jsonl, linesTrue) df.to_parquet(data.parquet, compressionsnappy, indexFalse)选择snappy压缩是因为它在压缩率和解压速度之间平衡得比较好。gzip压缩率更高但解压慢zstd介于两者之间。对于训练数据我一般用snappy因为训练时数据加载速度直接影响GPU利用率。3.3 对话格式的细节陷阱多轮对话数据的格式看似简单但细节特别多。以ShareGPT格式为例from字段的值可以是human、gpt、system、tool等不同框架对这些值的处理方式不一样。第一个坑是角色映射。有些框架只认user和assistant不认human和gpt。你需要做一个映射表role_map { human: user, gpt: assistant, system: system, tool: tool }第二个坑是对话轮次的截断。多轮对话数据往往很长超过模型的最大上下文长度。截断策略有两种从前面截断保留最近的对话和从后面截断保留最早的对话。对于对话任务通常保留最近的对话更合理因为最近的上下文对当前回复影响最大。第三个坑是loss计算。在多轮对话中你通常只想计算assistant回复部分的loss不想计算user输入部分的loss。这需要在数据预处理时给不同部分打上不同的label mask。具体做法是把user部分的label设为-100PyTorch的ignore_index只保留assistant部分的label。def mask_user_tokens(input_ids, labels, tokenizer): # 找到assistant回复的起始位置 # 将user部分的label设为-100 pass这个操作看起来简单但实际实现时需要考虑tokenizer的特殊token、对话模板等因素很容易出错。建议先用小批量数据验证mask是否正确再全量处理。3.4 指令格式的模板化处理指令微调数据的格式虽然灵活但为了训练稳定最好模板化。所谓模板化就是把instruction、input、output按照固定模板拼接成一个字符串。比如Alpaca的模板是Below is an instruction that describes a task. Write a response that appropriately completes the request. ### Instruction: {instruction} ### Input: {input} ### Response: {output}这个模板不是随便设计的。### Instruction:、### Input:、### Response:这些标记词起到了分隔作用让模型能区分不同部分。模板的设计直接影响微调效果我试过不同的模板发现标记词的选择和顺序确实会影响模型对指令的遵循程度。注意模板一旦确定训练和推理时必须保持一致。我见过有人在训练时用了模板A推理时忘了加模板直接把用户输入扔给模型结果模型完全懵了。4. 格式转换与数据流水线实战4.1 从原始数据到训练格式的完整流程实际项目中你拿到的原始数据往往是各种奇奇怪怪的格式CSV、Excel、数据库导出、网页抓取结果。把这些数据转换成LLM能吃的格式需要一个完整的数据流水线。我的一般流程是原始数据统一转成JSONL每条数据保留所有原始字段数据清洗去重、去空、去异常字符格式转换根据任务类型转换成Alpaca或ShareGPT格式质量过滤用规则或模型打分过滤低质量数据划分训练集/验证集/测试集转换成Parquet用于训练这个流程看起来线性但实际上经常需要来回迭代。比如质量过滤后发现有问题的数据可能要回到清洗步骤重新处理。4.2 数据清洗的实操细节数据清洗是格式转换中最耗时的环节。我总结了几条经验去重不只是完全匹配去重还要做近似去重。两条数据如果只有标点符号不同本质上还是重复的。可以用MinHash或SimHash做近似去重阈值一般设在0.8到0.9之间。去空不只是去掉空字符串还要去掉只有空白字符、只有标点符号、只有停用词的数据。这些数据对训练没有帮助反而会引入噪声。异常字符处理包括去掉不可见字符如零宽空格、统一全角半角、统一换行符。这些字符在JSONL里可能看不出来但会影响tokenize结果。import re import unicodedata def clean_text(text): # 去掉不可见字符 text re.sub(r[\u200b-\u200f\u2028-\u202f], , text) # 统一全角半角 text unicodedata.normalize(NFKC, text) # 统一换行符 text text.replace(\r\n, \n).replace(\r, \n) # 去掉首尾空白 text text.strip() return text4.3 格式验证与自动化测试数据格式转换完成后必须做验证。我见过太多因为格式问题导致训练失败的案例而且往往是在训练了几个小时之后才发现浪费大量时间和算力。我的做法是写一套自动化测试在数据进入训练之前跑一遍检查每条数据是否包含必需的字段检查字段类型是否正确检查文本长度是否在合理范围内检查是否有重复数据检查特殊token是否被正确处理def validate_alpaca_data(data): required_fields [instruction, input, output] for i, item in enumerate(data): for field in required_fields: if field not in item: raise ValueError(f第{i}条数据缺少字段: {field}) if not isinstance(item[field], str): raise ValueError(f第{i}条数据的{field}不是字符串) if len(item[instruction]) 0: raise ValueError(f第{i}条数据的instruction为空) return True这套测试跑一遍可能只要几秒钟但能帮你避免几小时的无效训练。4.4 大数据量下的性能优化当数据量达到百万级别格式转换的性能就成了问题。纯Python的循环处理太慢需要用一些优化技巧。第一个技巧是用pandas或polars做批量处理。pandas的apply虽然也不快但比纯Python循环好很多。polars更快但API需要适应一下。第二个技巧是用多进程。Python的multiprocessing库可以把数据分片每个进程处理一片最后合并。注意进程数不要超过CPU核心数否则反而会变慢。第三个技巧是用流式处理。不要一次性把所有数据加载到内存用生成器逐批读取、逐批处理、逐批写入。这样内存占用恒定可以处理任意大小的数据集。from multiprocessing import Pool def process_chunk(chunk): return [convert_format(item) for item in chunk] def parallel_process(data, chunk_size10000, num_workers8): chunks [data[i:ichunk_size] for i in range(0, len(data), chunk_size)] with Pool(num_workers) as p: results p.map(process_chunk, chunks) return [item for sublist in results for item in sublist]5. 常见问题与排查技巧实录5.1 格式问题速查表问题现象可能原因排查方法解决方案训练loss不下降数据格式错误label全被mask打印几条数据的label检查mask逻辑模型输出乱码编码问题BOM头未处理用hexdump查看文件头统一用UTF-8无BOM解析报错JSONL中有未转义字符逐行try-except定位清洗特殊字符训练速度慢数据格式读取效率低对比JSONL和Parquet读取速度转Parquet格式多轮对话混乱角色映射错误检查role字段值统一角色命名推理结果无法解析输出格式未约束检查prompt和response_format明确输出格式要求5.2 那些年我踩过的格式坑第一个坑JSONL文件最后一行没有换行符。有些工具在写入JSONL时最后一行不写\n导致某些读取库解析失败。解决方案是在写入时确保每行都以\n结尾包括最后一行。第二个坑Parquet的schema不一致。不同批次的数据如果字段类型不一致比如一批是int另一批是string合并时会报错。解决方案是在写入Parquet之前显式指定schema或者统一转换成string类型。第三个坑对话数据中混入了空轮次。有些对话数据里human和gpt交替出现但中间有空的消息。这种数据会导致训练时loss计算错误。解决方案是在预处理时过滤掉空消息并确保human和gpt严格交替。第四个坑特殊token被当作普通文本处理。比如|endoftext|如果被tokenizer拆成多个token就失去了特殊标记的作用。解决方案是在tokenizer中添加special tokens并在数据预处理时正确使用。提示每次处理新数据时先拿100条做小规模测试确认格式正确后再全量处理。这个习惯帮我省了至少几十个小时的无效训练时间。5.3 格式转换的自动化脚本为了提高效率我写了一套格式转换脚本支持常见格式之间的互转。核心思路是定义一个中间格式Internal Representation所有格式先转成中间格式再从中间格式转成目标格式。class DataItem: def __init__(self, instruction, input, output, conversationsNone): self.instruction instruction self.input input self.output output self.conversations conversations or [] def alpaca_to_internal(item): return DataItem( instructionitem[instruction], inputitem.get(input, ), outputitem[output] ) def internal_to_sharegpt(item): conversations [] if item.instruction: human_text item.instruction if item.input: human_text \n item.input conversations.append({from: human, value: human_text}) if item.output: conversations.append({from: gpt, value: item.output}) return {conversations: conversations}这套脚本的好处是新增一种格式只需要写两个函数一个转成中间格式一个从中间格式转出。不用为每对格式写转换函数大大减少了工作量。5.4 数据质量监控格式正确不代表数据质量高。我还会做一些数据质量监控比如统计instruction和output的长度分布发现异常值统计词频发现重复模式用模型对数据打分过滤低质量样本定期抽样人工检查这些监控指标可以帮助你及时发现数据问题避免“垃圾进垃圾出”。6. 格式选型的决策框架6.1 不同场景的格式推荐场景推荐格式理由预训练JSONL ParquetJSONL方便处理Parquet方便训练单轮指令微调Alpaca JSONL结构简单兼容性好多轮对话微调ShareGPT JSONL天然支持多轮表达能力强推理APImessages JSON标准化程度高各框架都支持评估JSONL 额外字段方便对比和计算指标大规模训练Parquet读取快压缩率高6.2 格式设计的几个原则第一人类可读优先。在数据处理的早期阶段用人类可读的格式JSONL方便调试。到了训练阶段再转成高性能格式Parquet。第二保持一致性。训练、推理、评估的数据格式尽量保持一致减少格式转换带来的bug。第三预留扩展字段。数据格式设计时留一些meta字段方便后续添加新信息不用改格式。第四版本化。数据格式如果有变更要记录版本号避免新旧数据混用导致问题。6.3 从格式看框架生态不同训练框架对数据格式的偏好其实反映了它们的设计哲学。比如LLaMA-Factory支持多种格式说明它追求通用性而一些专用框架只支持一种格式说明它追求极简。选择框架时数据格式的兼容性是一个重要考量。如果你已经有大量Alpaca格式的数据选一个原生支持Alpaca的框架会省很多事。如果你需要多轮对话就要选支持ShareGPT格式的框架。我个人在实际操作中的体会是不要为了用某个框架而强行转换数据格式。数据格式转换看似简单但每次转换都可能引入bug。更好的做法是选一个兼容性好的框架或者自己写数据加载器直接支持你的原始格式。最后再分享一个小技巧不管用什么格式都保留一份原始数据的备份。格式转换过程中如果发现问题可以随时回滚。我见过有人转换完数据后把原始数据删了结果发现转换有问题只能重新收集数据那才叫绝望。