开发者通过微调LLM生成特定文档风格,展示LLM个性化定制在代码生成和文档工作流中的实践价值。
在我对 2030 年的预测中,我曾写道,技术写作者将开始使用专用 LLM,并让它们在性能强劲的本地硬件上运行。我已经从一些工程领域意见领袖那里,看到了向“本地优先”转变的迹象,但我们还没有真正走到那一步,部分原因在于联网的前沿模型要强大得多。不过,这并不意味着我们不能做实验。上周我做的正是这样一项实验:尝试微调一个 instruct model,让它像 20 世纪 80、90 年代的软件技术写作者那样写作。
要训练一个私有的本地模型,让它像 90 年代的技术写作者一样写作,需要海量的书面材料。比如说,如果我想微调一个模型,让它模仿我的写作风格,那么仅靠这个博客远远不够,因为截至本文发布时,它的总字数才勉强达到 10 万。要进行充分训练,需要更多样本,而这些样本既不容易找到,也不容易制作。唯一快捷的办法,就是使用现成的语料库。那么,我能去哪里找到这样的语料库?
来认识一下 Bitsavers:这是一个收集并扫描旧计算机手册和宣传册的网站。它是一座价值惊人的计算机历史与早期技术写作资料库,在世界各地都有镜像站点。我很喜欢 90 年代的 Microsoft 手册,因此选择了 Microsoft 合集作为训练材料的来源。这个合集包含 1977 年至 2005 年间出版、如今已经绝版的文档,共计超过 3700 万词,涵盖早期系统和 SDK。
我下载了经过 OCR 处理的文本文件,然后用经典好用的 Python 脚本清理其中的识别瑕疵和冗余内容,比如索引和 frontmatter。接下来,我通过 OpenRouter 调用一个便宜又快速的模型 gemma-4-26b,根据段落是否容易理解,将每个段落分类为“keep”或“drop”。第二轮处理花了大约 8 美元。尽管经过了这两轮清洗,训练数据中仍然残留着一些我直到后来才发现的噪声,不过对于这次测试来说,基本没有大碍。
我按照段落和章节边界,将清洗后的文本拆分为训练样本:遇到标题就切分,同时保持代码块完整。根据 Claude 的建议,每个文本块限制在大约 512 个 token。每个文本块还会配上一条从模板生成的合成指令。最终,我得到了 192,456 个 JSONL 格式的样本,也就是每行一个 JSON 对象。我原本也可以使用一个小模型生成更好的指令和问题,但我是个没什么耐心的人。
在理想世界里,我应该有几百万美元闲置资金,可以拿来烧掉,创建属于自己的 LLM——Fabrice。可惜我离有钱还差得很远,否则我也不会在这里写这篇文章。所以 Fabrice 的替代方案就是微调:调整模型的“权重”,使它生成的每个 token 都受到训练材料的影响。我喜欢把微调想象成用几艘拖船,轻轻改变一座巨大冰山的移动轨迹:只推一点点,刚好产生想要的效果。
为什么选择微调,而不是检索增强生成(RAG)之类的方法?因为在这次实验里,我关注的并不是检索事实——那是 RAG 擅长的场景——而是让 LLM 无论掌握多少上下文知识,都能以特定方式行事,并采用特定风格写作。与完整训练相比,微调不需要海量数据,因此成本更低。另外,也没什么特别的理由:我一直想试试微调这项技术,看看它究竟有多大的可行性。
我的电脑使用的是一张相当老旧的显卡。为了避免花上几天甚至几周时间在本地微调模型,我选择了 Runpod。这是一项面向 AI 开发者的在线服务,以相对低廉的价格提供按需 pod,其中预先配置了 GPU 和各种工具。例如,每小时不到 6 美元,你就能租用 Nvidia B200 这头猛兽,它拥有 192GB 显存。该服务还提供了方便的 API,并配有可配置的自动充值和成本控制机制。
决定微调模型之后,我向 Claude 咨询了实现这一目标最稳妥的方法。最终我们选择了 QLoRA(Quantized Low-Rank Adaptation,量化低秩适配)。它并不通过修改 LLM 的每一个权重来完成微调,而是将这些权重“冻结”,再在上层放置一个 adapter。这个 adapter 是一个小文件,能够重新塑造模型的行为——如果你愿意,也可以把它想象成一张面具。QLoRA 中的 Q 表示最终结果经过了量化,也就是压缩,从而降低内存需求。
你还跟得上吗?很好。如果你觉得这里的信息密度很高,那是因为它确实很高。
如今,在家里使用 LLM 做任何事情,都是一场妥协:你要么牺牲时间,要么花钱,要么收敛自己的雄心壮志。我试图在这些因素之间取得平衡,在不到一个周末的时间里做出一些有意义的成果。我选择在两个模型上尝试微调:Llama 3.1 8B Instruct 和 Qwen 2.5 7B Instruct。以它们的规模——大约 8B——在 MacBook Air 上运行绰绰有余。我还测试了一个 Llama base model,它没有接受过回答问题的训练。
我在多种不同条件下测试了微调效果,包括改变训练材料的规模——使用子集或完整语料库——改变 epoch 数量,也就是训练轮次,以及改变 rank 等结构参数。我对这一切的理解只能算浅尝辄止,但我相信我的 Agent 能做出正确选择。当然,我也很开心地在每一步都对它提出质疑。比如,在某些情况下,3 个 epoch 可能导致“过拟合”;在 LLM 的世界里,这意味着训练过头了。真有意思。
adapter 只能应用于你为其执行微调的目标模型。每个 adapter 训练完成后,我都会把它们导出到笔记本电脑上,再进行转换和量化,生成 GGUF LoRA 文件。接着,我将其注册为一个本地 Ollama 模型,以便在笔记本电脑上运行基准测试。这种在本地进行转换的方式速度更快,而且不需要 GPU,不过与完全合并的模型相比,推理速度会稍慢一些。对于眼前这项测试,我并不太在意速度。
训练所有测试条件下的 adapter 大概花了一整天,包括中途休息的时间,总成本为 50 美元。在这个过程中,我损失了两个 adapter:Runpod 对预算毫不留情,一旦余额归零,它就会立刻删除 pod——没错,这确实给我上了一课。Claude 负责设置每次运行任务,并通过 Runpod API 跟进执行情况。Claude Code 的 /goal 命令对于循环完成各个阶段相当有帮助。现在回头看,我当时应该直接用 YOLO mode 运行。
下表展示了我比较的所有模型及其测试条件:
我向每个模型提出了同样的 prompt:
记录 malloc() 的文档。它是一个基础的 C 函数,训练材料中可能包含相关信息。
记录一个虚构的 Win32 API 函数 ConnectWifi()。训练材料中不存在它。
以 20 世纪 90 年代 Microsoft 的风格解释什么是 REST API(时代错位测试)。
你可以在这个 gist 中看到所有问题和回答。
在 malloc() 测试中,未经修改的模型生成了类似 README 风格的现代 Markdown 文档,而经过微调的模型则使用了符合当时年代的结构,其中包含 Synopsis 区块、Return Value 章节等内容。对于虚构的 ConnectWifi() 函数,只有训练了 3 个 epoch 的模型维持住了这个虚构设定,把它当作真实存在的函数来编写文档;其他模型则打破了第四面墙,为了遵循自身的内部知识而抵抗训练所形成的风格。
REST API 练习也相当有意思:Llama Instruct 40k 失败了,只生成了一些平淡无奇的营销文案。Claude 认为,这是因为 Llama 经历了大量强化训练(RLHF),以使它表现得更友好、更容易理解。Qwen 的微调版本则更好地维持了目标语域,生成了符合当时结构的文档,将 HTTP method 名称用作动词,并采用正式的章节标题。其中 Qwen 192k 表现最强,它的开头就像 Windows 2000 Resource Kit 中的一个章节。
让我再重复一遍:一个 7B 模型,在 90 年代的文档上接受训练,然后用一个 21 世纪初的概念进行测试,最终写出了令人信服的章节开头,甚至可能被误认为是真正的同期材料。风格成功迁移了。哇。另一方面,base model 并没有接受过回答问题的训练,它只接受过文本补全训练,因此表现得一塌糊涂:它几乎随机地喷吐原始语料,生成了数百行垃圾内容。base model 根本没有“回答这个问题”或“完成这段内容”的概念。
实验的最后,我比较了不同 rank 对 Qwen 模型的影响。所有模型都训练 1 个 epoch,rank 则分别设为 8 和 16。如果我的理解没错,rank 8 意味着每个 adapter 矩阵只能描述 8 种相互独立的模式。就像你只有 8 个旋钮可以调节。旋钮如此之少,adapter 就没法表现得太聪明:它必须完全押注于训练数据中最强、重复次数最多的模式。理论上,rank 16 的表达能力更强,也更加细腻。
rank 对比结果表明,更小、自由度更少的 adapter 更容易全身心投入虚构设定;rank 16 的 adapter 则更容易“逃离”语料库。结果还表明,只训练 1 个 epoch,同时使用适中的 rank 16,会导致更频繁的幻觉:adapter 的表达能力足以让它联想到相关概念,但训练强化程度又不足以让它牢牢抓住 prompt 真正想表达的内容。rank 和 epoch 似乎会相互影响——就像操作一台调音台。有趣的是,adapter 越便宜,它模仿起来反而越诚实。
这些经过微调的模型,非常擅长模仿 90 年代末期的 Microsoft 技术写作者。语料库为模型烙印上了相应的风格和口吻,也带来了一部分知识,同时基本保留了模型描述新概念的能力。这一流程的成本相对较低,可以用来制作针对特定任务的高效小模型,例如进行写作风格审查,或者按照公司内部的 style guide 起草新文档。
不过,要做到这一点,过程并不轻松。微调模型虽然便宜,但需要大量高质量训练数据,而这些数据并不容易制作。即便你设法拿到了训练数据,也需要选择一个合理、并且有能力接受额外训练的底层模型。之后,你还要面对大量可调参数。想把微调模型调到恰到好处的甜蜜点,是一件相当耗时的事。
令人安心的结论是,这样的模型永远无法取代人类技术写作者,只能增强他们的能力。微调模型与未经微调的同类一样缺乏判断力,而且需要大量引导。Fabrice 还得继续等下去。