资讯动态

JSONL语料如何用vLLM批量生成Wiki风格Markdown文档

发布时间:2026/10/19 8:38:59 来源:尧图企业网站定制
1. 从零开始如何把零散语料批量变成结构化Wiki文档最近在做一个知识库扩容的活儿核心任务是把手里一大坨非结构化的对话记录、操作纪要转成结构清晰的百科式文档。数据本身是JSONL格式几万行的体量每一行都是长短不一的自然语言片段。手写转文档肯定不现实找外包标注成本又太高于是就有了这个项目——generate_wiki_md_from_jsonl_vllm。这个项目做的事情很简单从JSONL格式的原始语料出发先做字段清洗和分段处理再用vLLM做离线批量推理逐条生成维基风格的Markdown文本最后落盘成结构化的.md文档。整个链路是可以断点续跑的不会因为某几行数据格式异常就推倒重来。如果你手里刚好有一堆对话日志、客服记录、操作手册草稿或者是在做RAG知识库的前置语料清洗需要把这些内容快速整理成条目清晰的文档这个方案能帮你跑通整条生产链路。下面我把完整的思路、代码骨架和踩过的坑一次说清楚。1.1 为什么不用逐条调API而是选vLLM离线批处理最直觉的做法是一条条调用在线大模型接口循环处理每行JSONL拿到结果就写文件。数据量小的时候没问题一旦来到万级行、十万级token的体量这种方案的短板就非常明显网络往返占大头并发控制写起来麻烦而且中间任何一条请求超时都要自己处理重试逻辑。vLLM在这类场景里的价值在于把“逐条调用”变成“批量吞吐”。它内部的Continuous Batching机制会把多条请求拼在一个batch里推进显存利用率明显比逐条串行高。我在测试环境对比过同样100条数据、同样模型vLLM离线批处理的总耗时大约是串行请求的四分之一到五分之一而且不需要自己管并发。离线推理的API使用起来也很直接把prompt列表一次性传进去它会返回等长的输出列表这个特性非常契合按行处理JSONL的需求。另外用本地推理服务还有个隐形优势数据不用离开内网。对于很多语料带敏感属性的场景能把处理过程全部放在本地或内网集群本身就是一个比“上传到云端处理”更稳妥的选择。1.2 项目到底解决了什么问题如果只从代码规模看这个项目不大核心就是一个脚本加一个prompt模板。但它的定位是一条完整的数据处理流水线的一部分自动化程度决定了它能复用到多少场景。先说输入侧。JSONL文件每一行都是一个JSON对象但实际语料远没有标准生产数据那么规矩常见的情况包括字段名不统一有的行叫content有的行叫text有的行叫body同一行的文本内容里混着标签、时间戳、乱码字符存在空行、重复行、只有字段没有值的“空壳记录”脚本里最先要处理的就是这些脏数据。要有一个预清洗环节把字段归一化、过滤无效行、去重输出一个干净的JSONL中间文件。这一步虽然技术含量不高但决定了下游生成质量的上限。再说生成侧。给模型一条raw文本要求它输出“维基风格的Markdown文档”这本身是个很有挑战的指令。模型的输出很容易跑偏比如直接复述原文、格式混用、无中生有地添加事实。要约束它必须靠结构化的prompt模板加上few-shot示例这两部分我花了不少时间调试后面会详细展开。2. JSONL数据准备与清洗细节2.1 先搞懂JSONL的数据长什么样JSONLJSON Lines每一行是一个独立的JSON对象行与行之间没有分隔符。它比大型JSON数组好在可以逐行读取不占内存也方便流式处理所以日志类、语料类的数据经常用这个格式。举个例子一行日志型数据可能是这样{id: 10001, source: conversation, text: 用户咨询如何配置网关技术支持工程师建议修改路由表..., timestamp: 1690000000}而另外一批数据可能是这样{dialogue_id: a12, utterances: [你好我想了解一下退款流程, 好的请提供订单号...]}同一个文件里字段不统一是最常见的坑。因为不同来源的数据可能来自不同系统导出脚本不同字段名自然五花八门。清洗的第一步就是建立一套“标准字段映射”把所有别名统一映射到一个内部字段上。提示清洗脚本中我把所有可用的文本字段按优先级拼接比如优先取text其次取content再次取utterances数组合并最后兜底取body。这样不会漏数据同时把id、source这类元信息保留在独立字段里方便后续追溯。2.2 清洗过程中必须处理的几个问题问题一文本长度极不均匀。有的行只有十几个字有的行能有两三千字。过短的文本单独生成文档没有意义我设了个最短长度阈值比如少于30个字符的直接跳过过长的文本则需要分段。分段策略不要按字数硬切最好按语义边界比如标点符号、换行符、对话轮次切换的位置来切。硬切会把一个完整的事件拆成前后两段两段分别生成文档时事实一致性很难保证。问题二重复内容多。语料里经常出现完全重复的行或者高度相似的段落比如系统自动推送的通知。清洗时我用了一个轻量去重方案对文本做规范化去掉多余空白、统一大小写然后计算哈希完全重复的直接去掉高度相似的用简单的编辑距离阈值过滤。这一层能帮推理侧省钱少处理几千条重复数据。问题三不可见字符和编码问题。JSONL文件可能是从不同系统导出来的编码可能是UTF-8也可能是带BOM的UTF-8甚至部分行里面混着全角空格、特殊控制字符。清洗步骤里统一做一次strip全角转半角连续空白压缩成单个空格控制字符直接移除。这些看起来不起眼的处理真的会在生成质量上体现出来。清洗完的数据建议写成一个cleaned.jsonl结构如下{id: 10001, meta: {source: conversation}, content: 用户咨询如何配置网关技术支持工程师建议修改路由表...}这个中间格式同时保留了原始元信息和标准化后的正文下游prompt构造时直接从这里读取就够了。3. 提示词设计与生成策略3.1 维基风格Markdown到底要求什么在写prompt之前得先定义清楚“维基风格Markdown”具体交给模型什么形态的输出。我自己的定义是必须有标题层级文章有一个主标题下面按逻辑分成若干章节章节标题尽量是名词性短语比如“功能概述”“安装步骤”“常见问题”而不是一句话内容以说明性文字为主中性、客观尽量不保留对话双方的人称适当使用列表、表格、代码块来组织信息长度控制在一个合理的范围不能一段话就结束模型本身并不是不懂这些要求而是因为没有约束时它会偷懒倾向于复述原文而不是重新组织。所以prompt的核心任务是“强制”它做组织整理而不是做摘要或改写。3.2 一套可复用的prompt模板实际跑下来我用了这么一套结构角色设定 任务说明 输出格式要求 强约束规则 few-shot示例。具体模板如下你是一名资深技术文档编辑擅长把零散的非结构化内容整理成结构清晰、表述客观的百科条目。 请根据下面提供的原始素材整理出一份维基风格的Markdown文档。 要求 1. 文档必须包含以下结构的Markdown标题# 主标题、## 一级章节、### 二级章节 2. 内容要重新组织不能简单复述原文不要出现对话人称 3. 信息不足时可以在正文中用有待补充标注不要凭空编造细节 4. 尽量使用列表、表格、步骤说明等方式呈现内容 5. 全文控制在500-800字以内 原始素材 {input_text} 请直接输出Markdown文档。这个模板有几处关键设计。“不能简单复述原文”是为了抑制模型的偷懒行为。我在测试中发现如果不加这句很多输出就是把原文语序微调一下换几个词就交差了。“信息不足时标注有待补充”是为了防幻觉。维基风格要求客观中立模型为了满足“文档完整”的隐含要求很容易在信息不充分时自作主张地补细节这是最危险的行为。加了这句之后模型会更谨慎不知道的地方会主动留空。“控制在500-800字”是一个经验值。太短说明模型没有展开太长则说明它在灌水。实际因为输入长度差异很大后来我把字数要求改成了按输入长度的比例比如“输出长度约为原文的60%到80%”效果更稳定。3.3 few-shot示例的用法与布局模板里如果没有示例模型输出的稳定性会很差。我加了三个示例分别对应三种常见输入形态示例一一段客服对话记录 → 结构化FAQ文档示例二一段操作流水日志 → 步骤说明文档示例三一段零散的产品描述 → 百科条目文档每个示例都完整标注了输入原文和期望输出的Markdown文档。few-shot示例占用的token不少但对格式稳定性的提升非常明显。在专业模型上示例能够隐式地告诉模型“Markdown标题要这样用”“列表要这样排”“人物对话要这样转化”。这个收益远大于为每个示例多花的几百个token的成本。注意示例里的内容要和真实场景相关。如果你的语料主要是技术问答就不要放购物场景的示例领域错位会让模型不知道该按哪个风格走。3.4 关键生成参数怎么调vLLM的SamplingParams里有几个参数值得反复调试。我最终使用的配置如下from vllm import SamplingParams sampling_params SamplingParams( temperature0.2, top_p0.9, max_tokens2048, repetition_penalty1.05, stop[|endoftext|] )温度设在0.2是经过好几轮测试后的选择。维基风格文档讲究准确、稳定不需要太多创造性输出温度太高会给“幻觉”可乘之机。实测温度从0.2调到0.8之后编造细节的案例数量明显上升格式也开始出现变化所以这个场景下不建议用高温度。max_tokens设2048是因为生成结果如果超过这个长度后半段会被截断而截断的Markdown文档经常只有半个表格或者残缺代码块。长度限制宁可设大一点让模型在没写完时自然收尾。repetition_penalty的1.05是一个很微妙的值。太低会让模型在长文本里重复句式太高则会扭曲它正常的表达习惯导致用词生硬。如果你跑出来的文档里有大量重复的段落开头优先检查这个参数。3.5 面对长文本的分块处理策略输入超过模型上下文窗口是绕不开的问题。原始语料里有不少长对话记录动辄几千字不能整段塞进去。分块策略采用“章节感知切分”逻辑如下遍历原始文本优先按\n\n切分把文本块尽量控制在1500字以内如果按段落切分后还有超大块再按句号、问号、感叹号等边界切分相邻的、语义上属于同一主题的段落优先合并到同一块每个文本块独立生成一个Markdown文档片段最后按顺序拼接。这里有一个细节值得注意拼接后会出现多个“主标题”也就是每个块生成的文档都有一个#标题。处理办法是在合并时丢弃第二块及以后的主标题只保留其下一级内容同时在前面的主标题下方插入一个“更多内容”的索引段落。4. 核心脚本实现从JSONL到Markdown的完整链路4.1 整体流程架构脚本的整体流程可以画成一条类似流水线的结构我在实现时分成了四个模块预处理、批处理生成、响应解析、落盘合并。职责分离的好处是任何一个环节出问题可以单独重跑不用从零开始。cleaned.jsonl -- prompt_builder -- vllm_offline_batch -- output.jsonl -- md_merger -- final.md4.2 预处理模块核心代码这里展示第一个模块的核心逻辑import json import re import hashlib def normalize_text(text: str) - str: # 全角转半角、压缩空白、移除控制字符 text text.replace(\u3000, ).replace(\ufeff, ) text re.sub(r[\x00-\x08\x0b\x0c\x0e-\x1f], , text) text re.sub(r\s, , text) return text.strip() def clean_jsonl(input_path: str, output_path: str, min_len: int 30): seen set() out_f open(output_path, w, encodingutf-8) with open(input_path, r, encodingutf-8) as f: for line_no, line in enumerate(f, 1): try: obj json.loads(line) except json.JSONDecodeError: # 记录坏行号方便溯源 print(f[warn] line {line_no}: JSON parse failed, skipped) continue content (obj.get(text) or obj.get(content) or obj.get(body) or .join(obj.get(utterances, []))) if not content or len(content) min_len: continue content normalize_text(content) digest hashlib.md5(content.encode()).hexdigest() if digest in seen: continue seen.add(digest) record {id: obj.get(id, str(line_no)), meta: {source: obj.get(source, unknown)}, content: content} out_f.write(json.dumps(record, ensure_asciiFalse) \n) out_f.close() print(fdone, output written to {output_path})很多数据清洗问题都能在这个环节拦截住。比如JSON解析失败这一条看起来很简单但实际跑万行文件时经常能碰到几十行坏数据如果不打印行号后面排查会非常头痛。4.3 调用vLLM离线批处理的代码骨架vLLM离线批处理的核心调用逻辑很简洁代码如下from vllm import LLM, SamplingParams def build_prompt(record: dict) - str: # 拼上系统级instruction与前面的prompt模板拼装 template open(prompt_template.txt, encodingutf-8).read() return template.replace({input_text}, record[content]) def batch_generate(records, model_nameQwen/Qwen2.5-7B-Instruct, max_input_len8192): llm LLM(modelmodel_name, max_model_lenmax_input_len) prompts [build_prompt(r) for r in records] sampling_params SamplingParams(temperature0.2, top_p0.9, max_tokens2048, repetition_penalty1.05) outputs llm.generate(prompts, sampling_params) results [] for record, output in zip(records, outputs): generated output.outputs[0].text results.append({ id: record[id], meta: record.get(meta, {}), input_content: record[content], generated_md: generated }) return results初看这段代码很简单但实际上vLLM在模型加载阶段耗时很长如果每条数据单独调用一次LLM()大部分时间都浪费在了模型初始化上。这个脚本设计成一次性加载模型、传入所有prompt、最后统一回收结果这才是离线批处理的正确用法。如果你手里的机器显存不够大模型加载后就已经吃了大部分显存batch_size过大会导致OOM。vLLM虽然有Continuous Batching的优化但它不会无限度地容下所有请求。实际跑的时候需要控制一次传入的prompt批大小比如每次传200条生成循环处理完所有数据。4.4 响应解析与容错处理vLLM返回的generated文本有时并不完全是干净的Markdown——开头可能会多几句“好的这是整理后的文档”之类的赘语结尾偶尔会有半截没写完的句子。响应解析模块要处理这些问题def clean_generated(text: str) - str: # 去掉前缀碎语 for policy in [好的, 当然, 根据要求, 以下, 根据提供的]: idx text.find(policy) if idx ! -1 and idx 10: text text[idx len(policy):] break # 去掉开头和结尾的多余空行 text text.strip() # 避免半截结尾截断到最后一个可见的# 标题 lines text.splitlines() # 如果最后一行很可能是未完成段落缺少结尾标点丢弃它 if lines and lines[-1].rstrip() and not re.search(r[。.!?]$, lines[-1].strip()): # 保守做法不丢掉因为可能是列表项 pass return text“保留或丢弃最后一行”我纠结了很久最后的结论是不要轻易丢。因为生成结果里最后一行经常是列表项或代码块结尾虽然缺少句号但截断造成的信息损失远大于格式瑕疵。如果只是篇幅超限被截断那么前文该有的内容已经有了后补的代价又太高所以保留不完整的尾部比强行删掉更合理。生成的整个结果写成一个generated.jsonl每行结构包含id、input_content、generated_md这样即使后面发现某个文档生成质量不合格也可以定位到原始输入重新生成不需要再读原始文件做二次解析。4.5 合并生成最终的Wiki Markdown文件合并模块做的事情是把分散的结果串成一个完整文件。核心逻辑如下def merge_to_wiki(results, output_md_path: str, top_title: str 知识库总览): out_lines [f# {top_title}, ] for idx, item in enumerate(results, 1): md item[generated_md].strip() lines md.splitlines() # 处理每个片段中的主标题统一降级并追加序号 if lines and lines[0].startswith(# ): lines[0] f## {idx}. {lines[0][2:]} merged \n.join(lines) out_lines.append(merged) out_lines.append() with open(output_md_path, w, encodingutf-8) as f: f.write(\n.join(out_lines))这里有个小细节如果每片生成的文档都有#主标题直接拼接会制造出多个顶级标题。代码里把第一条之后的所有主标题降为二级标题是整个合并环节里最容易忽略的问题。5. 实操参数选择与运行配置5.1 模型选择拿捏质量与资源的平衡整个实验我试过3B、7B、14B三个量级的模型。3B模型生成速度快显存占用小但格式稳定性不太够经常出现该换行不换行、列表符号混用的问题。7B模型在格式和内容质量上有质的提升是性价比最高的档位。14B模型内容逻辑更顺但在任务相对机械的场景里质量的提升并不足以弥补推理耗时翻倍的代价。最终选型建议如果是10万行以下的中等规模语料、对质量要求不极端的场景7B模型完全够用如果语料领域非常垂直且下游对结构化程度要求极高可以考虑14B档位。5.2 批大小的设置策略vLLM的batch_size不是一个显式的参数而是通过传入的prompt列表长度隐式控制。一次传入所有数据并不总是最好的选择。实践下来我按5000字左右的总输入一个批次来切既照顾了显存占用也保证了批内的动态规划不会失控。如果机器是单卡24G显存跑7B模型加4K上下文一次传100到300条数据比较稳。5.3 断点续跑与幂等性生成流程跑几个小时很常见但过程中难免有机器重启、显存报错、手动中断的情况。断点续跑的设计非常关键我的做法是每一批数据生成完立即落盘到generated.jsonl而不是全部完成后再一次性写每一批数据使用独立的batch_xxx.jsonl文件名在最后校验阶段再合并重启时自动检查哪些id已经有结果存在跳过已经生成的记录用独立的输入记录id来追踪进度这样即使中断重新启动脚本也可以精确地从断点继续不需要重新处理已完成的批次。6. 常见问题与排查技巧6.1 输入数据引起的生成问题症状一某些文档生成得莫名其妙。排查思路优先看那条记录清洗后的content字段是否太短。如果输入本身就只有一句话“不能退款”任何prompt也变不出花来。处理办法是提高最短长度阈值同时在生成前对每条content做一个长度检查不足阈值时直接跳。症状二长文本被截断结尾明显不完整。排查思路检查这条记录的输入长度是否超过了模型的最大上下文窗口。vLLM对超长输入的处理是抛异常不会静默截断。如果你的输入长度在窗口内但生成长度超了max_tokens那应该调大max_tokens。如果已经是2048还是截断建议削减单条输入长度而不是继续加大max_tokens——在超长输入的场景下模型的中段注意力会退化后半段内容质量明显下降。6.2 输出格式相关问题症状三模型在输出中保留了对话口吻甚至出现“客服说”“用户问”这样的表达。解决方案把prompt里的“不要出现对话人称”改成“用第三人称转述所有对话内容”并且补一条few-shot示例。模型在few-shot里看到“这段对话被转写为客观的问答条目”后行为会明显改善。症状四输出的Markdown里出现不存在的表格。表格是维基文档里非常有表达力的格式但也是幻觉重灾区。模型为了填满表格会自行编造数据。这里我加了硬性规则“如果不确定表格数据不要使用表格。”实测加了这个约束后表格类的编造大幅减少。6.3 资源与速度问题症状五多卡环境下显存分配不均单卡OOM。vLLM会自动做张量并行但只在你显式设置tensor_parallel_size时才会多卡并行。如果不开默认单卡跑小显存机器容易OOM开太大的tensor_parallel_size又会因为通信开销拖慢整体速度。具体的值要看你的模型大小和卡间带宽需要实测几组数值对比。症状六生成速度不稳定忽快忽慢。能导致这个现象的原因很多最常见的是同一时间还有其他任务在占用GPU计算。另外batch里的prompt长度差异极大时连续批处理的表现也会波动。我建议在分批时按输入长度做一次排序短文本批次放前面长文本批次放后面这样每批内的计算负载更均匀整体吞吐更稳定。6.4 内容质量问题的终极兜底模型不是每次都能一次生成合格文档的有时候输出格式完全错乱或者内容大量偏离原始素材。对这种情况我的兜底方案是解析生成的Markdown结构统计标题数量、段落数量、列表数量低于阈值比如标题数为0、列表数为0的记录标记为low_quality对low_quality记录换更高温度再重试一次重试后仍然不合格的输出到一个单独的failed.txt人工处理或者保留原始素材等待下游其他方案这个兜底机制虽然不能保证每一个文档都堪称完美但能确保进入最终合并文件中的内容至少通过一次机器质检而不是像个黑盒一样让不合格内容混入知识库。7. 我的一点实操体会这套流程跑通之后最明显的收益不是生成了多少份文档而是建立起了一条从“原始非结构化语料”到“可检索结构化文档”的全自动管线。我一个很深的体会是提示词工程对这个项目的贡献比代码结构更大。代码逻辑不难无非是读JSONL、调接口、写文件、拼接字符串真正决定产出质量的是prompt模板里那几句约束和三个few-shot示例。很多人觉得调API生成文档是“随手一调”的事实际跑过之后才知道约束模型的重组行为比让它自由发挥要难得多。好的模板应该把模型的发挥空间严格限制在“重新组织已有内容”上而不是“自由生成新内容”。另一个体会是数据清洗花的时间占比比我预想的高得多。一批真实的语料永远有你没想到的脏数据形态可能是编码问题、字段缺失、语义重复。清洗做得越透下游生成越省心。如果清洗环节偷懒模型会把数据里的噪声“忠实”地转述出来生成结果的质量上限被输入质量锁死prompt再好也很难救回来。还有一个建议不要把生成结果直接当最终答案。机器生成的文档适合做初稿和素材投入知识库前最好有一个人工抽检环节。我跑完一轮后从生成结果里随机抽了100篇检查准确率大概在85%左右大致能直接用的程度。剩下15%的问题主要是细节事实错误、语气不一致、部分内容缺失这些是任何生成式方案都难以完全避免的。把它定位成一个“高效初稿生成器”人工只做筛选和少量修正整体的效率提升已经非常可观了。如果你手头也有类似的非结构化语料需要整理成文档可以按这套思路搭一条自己的流水线。模型和参数都可以换但“先清洗、再分批生成、后合并校验”的核心链路是通用的。把这条链路跑顺了换个数据源、换个输出格式都只是改改模板和字段映射的事儿。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑