角色卡可以写进提示词,也可以用 LoRA 写进权重
上一篇我写到:判断可以交给模型,但责任不行。这一篇换个更窄、也更技术的问题——一个角色的”人格”,到底存在哪?
如果你玩过 SillyTavern(或任何角色扮演前端),大概见过”角色卡”:一张 PNG,里面塞一段 JSON,写着名字、外貌、性格、开场白、对话示例。拖进去,模型就”变成”这个角色。
但那张卡其实活在提示词层——每一轮对话,它都被塞进上下文。这一篇我想把它往下一层推:不塞进提示词,而是写进权重。
提示词层的角色:灵活,但有三个代价
SillyTavern 的角色卡有明确的规范:主流的 V2(chara_card_v2)把字段收进一个 data 对象里——name、description、personality、scenario、first_mes(开场白)、mes_example(对话示例),外加 system_prompt、alternate_greetings、creator_notes,以及一个 character_book(世界书:关键词触发时才注入设定)。V3 在这个基础上又加了素材、多语言备注等。整段 JSON 以 base64 存在 PNG 的 tEXt 块里,所以一张图就是一张卡。
它的好处很实在:不用训练、即插即用、随时换。但它有三个代价。
- 永久的 token 税。
description和personality是”常驻字段”,每一轮都注入;卡片越厚,挤掉的历史越多——这是”聊着聊着它忘事”的头号原因。 - 会漂。 角色住在上下文里,本质是”你每一轮都在提醒它是谁”。对话一长、话题一偏,它就容易出戏。
- 它只是换了上下文。 说到底,模型的权重一个字没变。你换的是”读什么”,不是”它是谁”。
模型层:把角色写进权重
换个思路:如果角色不是”被读到的”,而是”长在模型里”的呢?
这就是微调(fine-tuning)——具体说,是 LoRA / QLoRA(标题里的 LoRA 是统称,QLoRA 是它在量化底座上的版本)。做法是在冻结的底座旁边,训练一小撮低秩矩阵(适配器,通常只有几十到一两百 MB)。训练完,把适配器融合(fuse)回底座,你就得到一个”天生就是这个角色”的模型。
对比一下:提示词层每轮付 token 税,模型层是训练一次性付清、推理时零额外开销——因为适配器已经并进权重了。
我为什么选 QLoRA
先说硬件,因为这决定了一切。我用的是一台 MacBook Air(M3 / 24 GB 统一内存 / 512 GB SSD)。
按公开的内存档位:24 GB 统一内存大约能 cover LoRA 的 7–8B、或 QLoRA 的 8–14B。我盯上的底座是 12B,所以 LoRA 偏紧、QLoRA 正合适(QLoRA = 底座压到 4-bit,只训 16-bit 的 LoRA 适配器,内存占用大幅下降)。
框架用 Apple 自家的 MLX,对应命令行工具 mlx-lm——它原生支持 LoRA / DoRA / QLoRA,一条命令就能开训:
pip install "mlx-lm[train]"
python -m mlx_lm.lora \
--model mlx-community/gemma-4-12B-it-4bit \
--train --data ./data --iters 1000 \
--batch-size 2 --grad-checkpoint
底座是 Gemma 4 12B。这里有个容易被忽略的边界:Ollama 官网上已经有它的 MLX 构建 gemma4:12b-mlx,拉下来跑推理很顺(256K 上下文);但要微调,得用同源的 MLX / HuggingFace 权重——Ollama 那个封装是给推理用的,不能直接拿去训。所以我的分工是:Ollama 管跑,mlx-lm 管训。训练完 mlx_lm.fuse 把适配器合并回底座,得到一个可独立运行的模型。
数据:让角色从”剧本”里学
我做的是剧本杀角色。这类角色的核心不是”知道很多事实”,而是稳定的说话方式——口癖、语气、在不同处境下的反应。所以训练数据要的不是百科,而是对话。
语料来源这件事我只说一句:先想清楚版权归谁。 能用得踏实的,无非三类——自己原创的角色对话(自己写的剧本、自己设计的角色,最干净,也最贴角色);公有领域的经典文本;许可写明白、照着许可用的开放语料。反过来,直接拿仍在版权期内的小说、影视对白去训,风险不该靠”我只是本地跑跑”来兜底。所以这一步,我用的是自己能说清来源的对话。
用法上,统一转成 mlx-lm 的 JSONL chat 格式:system 放这个角色的卡面(人设、处境),user/assistant 放对话——模型学的就是”在这个人设下,下一句该怎么说”。
{"messages": [
{"role": "system", "content": "你是剧本《××》里的××,说话带几分江湖气,从不直说心事……"},
{"role": "user", "content": "你昨晚到底去没去过那间屋子?"},
{"role": "assistant", "content": "……我说没去过,你信么?"}
]}
硬件这条线(全公开)
- MacBook Air(Mac15,12)· Apple M3(8 核:4 性能 + 4 能效)· 24 GB 统一内存 · 512 GB 内置 SSD + 外接 1 TB SSD
两个我踩到、但常被忽略的坑:
- Air 没有风扇。 持续训练会热,热了就降频——同样的迭代数,笔记本会比带风扇的机器慢。所以别拿”跑通一次”就当性能基线。
-
磁盘不像内存那么紧,但外接扩容有得有失。 本地微调官方建议留 20–50 GB 空闲(底座 + 数据集 + 检查点 + 合并后的权重);我这台是 512 GB,日常够用,谈不上瓶颈。真要放大规模,最高性价比的办法是外接 SSD——但别只看容量:
- 利:便宜(苹果的内置存储加价很凶,外接 1 TB 划算得多)、不占系统盘、模型和数据能拔下来换机复用。
- 弊与风险:带宽与延迟不如内置 NVMe,USB 3.x 更比 Thunderbolt / USB4 慢一截,训练时的数据加载与 checkpoint 读写容易被拖成瓶颈;长时间大量读写会发热降速,偶发掉盘还会中断长训练;便携性也差;若格成 exFAT,还会踩不支持符号链接 / 权限、性能差的坑。
- 可行性:可行,但分工要清楚——大而冷的东西(权重、数据集、检查点)放外接,小而热的(代码、工作目录)留内置;盘格成 APFS、优先 Thunderbolt / USB4、注意散热,敏感数据记得加密。我自己的做法就是这样:模型和数据放外接 1 TB SSD,系统盘只留系统与日常。
结果与边界
(下面这些量级,是按公开基准推算、结合这台机器特性得出的;实测若有出入,以实测为准。)
跑得动,也跑得完。 12B 的 QLoRA 在这台 24 GB 的机器上,峰值内存大致在 8–11 GB 的量级(公开实测里 14B 的 QLoRA 约 9 GB,12B 只会更低),留足了系统余量,不会把机器压死。速度上:公开基准里,7B 的 QLoRA 在 M2 Max 32 GB 上训 5,000 条大约 90 分钟;12B 参数量约 1.7 倍,再叠上 Air 没有风扇、持续跑会降频,我预期同规模数据要 2–4 小时。数据集按千级对话、2 个 epoch、batch-size 2 算,iters 落在 1,000–2,000 这个量级。
模型层 vs 提示词层,差在哪:
| 提示词层(角色卡) | 模型层(LoRA) | |
|---|---|---|
| 长对话稳定性 | 几十轮后容易出戏 | 明显更稳,口癖是”长”在权重里的 |
| 推理成本 | 每轮付 token 税,卡面常驻上下文 | fuse 后零额外开销,上下文留给对话本身 |
| 灵活性 | 随换随用,一张卡就是一个角色 | 一个适配器≈一个角色,换角色要重训 |
| 前置成本 | 零 | 要数据、要训练、要时间 |
代价要说清。 微调有两把刀:灾难性遗忘和过拟合。数据太少,模型会退化成”复读机”——只会重复训练集里的句式;学得太狠,又会损伤底座原本的能力。所以数据质量比数量重要,rank、alpha、学习率都要往保守里调,grad-checkpoint 该开就开。
最后回到上一篇那条线。把角色写进权重,说到底还是本地、私有的一步——数据不出这台机器,模型在自己手里。但”角色可以被固化”不等于”判断可以被托管”:人格可以写进权重,责任不行。 它演得再像,那个”像不像、能不能用、要不要发”,仍然得有人来担。