副标题:基于微调模型的AI在原七海实现全流程

终于,我的博客终于要整技术了,太感动了

前言

是的,我写文章必定要加一个前言。

2025年8月20日,有一位群友发了一个文件

我一看,原来是剧情文本。

“这是可以训练bot的”

哦?训练?

一般来说,主流大模型只能依靠用户给的提示词进行角色扮演,而且在许多设定上是需要不断补充的,如果是自创的角色还好(例如酒馆,而且酒馆还写故事情节),但如果是一个游戏、视频里的角色......

复杂的设定暂且不谈,光是说话风格就够折腾的了,仅仅是依靠提示词整出来的效果是不行的(虽然也不是一定不行,但是会耗费巨额时间,还不保证换一次对话,能不能还原风格;换了其他模型之后,提示词能否适用)

于是,我就想到了这种依靠剧情文本让模型学习的角色扮演方案——通过微调大模型来实现角色的复刻。

(虽然这种准备剧情文本的方案同样艰难,但我至少不用想规则Prompt)

恰好,当时把一位朋友拉下柚子社君子六艺之一《Riddle Joker》的坑,他推完七海线的剧情之后,也是直接进入戒断反应

原神Logo
七海可爱捏

这位朋友比较喜欢七海。趁此想法,我尝试训练出一个可以模仿说话风格和设定的模型,既能让我折腾,也能实现他的愿望

很好,AI在原七海赛博Waifu)计划,正式启动!

核心原理——微调

微调,简而言之,就把特定领域的数据喂给模型,让模型自己学习,调整参数。我们可以通过这种方法让原本的模型可以更精准地提升特定领域的能力。

当然也包括角色扮演,因为本质就是在学习特定模式的回答,同时也把其中的信息给学习进去,达到增加知识的效果。

类型

微调的类型一般分为全参数微调LoRA微调

其中全参数微调深度方案,训练会更新模型的全部参数(可以说是洗脑),可以把知识吃的更深,但是需要很多数据才能驾驭,而且存在灾难性遗忘(学了新的,忘了旧的)的问题。

LoRA微调则是给模型打补丁,需要就装,不需要就卸,而且训练成本、参数、需求数据小,是最优方案

起点——数据集的制作

想要训练出这样一个模型,首先就要准备数据集

数据集也就是让模型可以参考学习的资料,数据集格式一般是这样的(以Alpaca格式举例):

[
  {
    "instruction": "今天的天气怎么样?",
    "input": "",
    "output": "今天的天气不错,是晴天。",
    "history": [
      [
        "今天会下雨吗?",
        "今天不会下雨,是个好天气。"
      ],
      [
        "今天适合出去玩吗?",
        "非常适合,空气质量很好。"
      ]
    ]
  }
]

其中instruction是用户的指令(也就是输入的文本),input是用户的输入(instruction 对应的内容会与 input 列对应的内容拼接后作为最终的输入),output就是模型的回答,history便是用于参考的历史记录

那么,为了制造AI在原七海,我们需要提取七海曾经说过的话作为模型回答,用男主的发言作为用户指令,也可以加入其他人与七海的对话来让模型理解人物关系(不过,这就过于麻烦了)。

“不也挺好吗”——七海如是说道。

Steam版的《Riddle Joker》有二次加密,因此我们采用民间汉化组的游戏资源。

通过GalGame解包神器GARbroscn.xp3解包之后,我们可以得到剧本文件

但是scn文件是有加密的,所以我们利用Ulysses-FreeMoteToolkit工具把.scn全部拖入PsbDecompile.exe得到可读json文件

接下来,要面对的是这种又臭又长的json文件

{
  "hash": "199635071e5235ce79cab8dbd7d03cc7",
  "name": "000.プロローグver1.10.ks",
  "outlines": [],
  "scenes": [{
      "firstLine": 4,
      "label": "*part_000",
      "lines": [[1,{
            "data": [],
            "env": {
              "action": [null,null],
              "name": "env"
            },
            "meswinchange": "MSGWIN",
            "scnchart": null,
            "showdate": {
              "back": null,
              "date": null,
              "fore": null,
              "nowShow": null
            }
          },null,1,5],["envupdate","update",[["init",1],["new","bgm","bgm"],{
              "action": [null],
              "name": "env"
            }],"revupdate",[["del","bgm"]],"wait",{
            "list": [{
                "mode": 0,
                "name": "env"
......

没事,慢慢看.......

原来scenes数组下有texts,遍历其子数组的第0和2项即可找到说话者和对应的文本

使用脚本提取并格式化后就得到了可读文本:

8月下旬。暑假所剩无几,酷热的暑气却仍未消散。
白天暑气超级猛烈,气温飙过35度。即使是太阳落山后的夜晚,温度也迟迟下不去。
日本的夏季正是如此,今天也是个潮湿闷热的仲夏夜。
晓「……哈啊……」
热得脑袋都发木了。
七海「Levy 6」
晓「……」
七海「Levy 6」
晓「……」
七海「……阿晓」
晓「别突然叫我名字啊,吓了我一跳,Levy 9」
七海「明明听见我叫你……为啥无视我」
......

这是为了方便数据的手动录入(因为直接格式化录入会有问题,而且如果是答非所问的数据,效果会变差),不过不会有人在录入数据的时使用json,所以我自己整了套方便自己的格式:

ST(对话开始)

*系统提示词,对话背景设定

/输入指令

#回答

ED(对话结束)

花费了我两天时间,把一半的剧情对话录入了,转换成数据集。

准备微调环境

由于需要耗费大量显存,所以使用AutoDL的机器进行炼丹(训练模型)的操作。

image-bvwr.png

选用RTX 4090机器和如上环境

随后在JupyterLab中打开终端操作

至于微调工具,懒人当然是选择LLaMA-Factory,不需要额外编写代码,而且界面友好

使用指令

git clone https://github.com/hiyouga/LLaMA-Factory.git

然后进入目录,创建conda虚拟环境并激活,安装依赖

conda create -n llama-factory python=3.10

conda activate llama-factory

pip install -e ".[torch,metrics]"

之后便可直接召唤WebUI(在7860端口上)

llamafactory-cli webui

我们整一个ssh端口映射来访问WebUI(适用于AutoDL的机器)

ssh -CNg -L 7860:127.0.0.1:7860 [用户名]@[主机名] -p [ssh端口]

然后访问localhost:7860

至此,微调环境就准备好了

试错、试错、试错——开始微调

配置基底模型、数据集

这就是LLaMA-Factory的WebUI,我这里选择Qwen2.5-7B-instruct作为基底模型,选择LoRA微调,在没有模型的情况下,它会自己从huggingface下载,我这里选择下载好的本地模型(与我所选模型相同)作为模型路径。

关于数据集,我采用另一种格式ShareGPT的变种:OpenAI,格式如下:

[
  {
    "messages": [
      {
        "role": "system",
        "content": "系统提示词(选填)"
      },
      {
        "role": "user",
        "content": "用户指令"
      },
      {
        "role": "assistant",
        "content": "模型回答"
      }
    ]
  }
]

ShareGPT对于多轮对话的数据更友好(但并不意味着模型可以学习一轮对话中的所有内容,所以我当时第一次整的时候被骗了)

随后需要把数据集上传到LLaMA-Factory目录中的data文件夹,然后修改dataset_info.json,需要添加一段(如果各位要整的话,还要修改formatting等数组,使其与你的数据集匹配),这样才能识别到数据集:

  "名字": {
    "file_name": "文件名.json",
    "formatting": "sharegpt",
    "columns": {
      "messages": "messages"
    },
    "tags": {
      "role_tag": "role",
      "content_tag": "content",
      "user_tag": "user",
      "assistant_tag": "assistant",
      "system_tag": "system"
    }

然后在数据集中选择数据集。

参数调整

除了数据集的质量,训练参数也决定了模型的好坏,以下是一些关键的参数:

学习率:我们可以把训练的过程作一个奇妙的比喻——模型被放在一块地方,目标是走进深坑,走越深越好,但是这一块地方不只有一个坑,有的坑深,有的浅。学习率决定模型走的步子大还是小,太小就很难走进&走出坑里(如果有更深的坑,它可能很久都不会走进那个坑),太大就很难走进坑的深处。我设定为5e-5

训练轮数:学习的次数,如果有1000条数据,轮数5,那么会把这1000条数据喂给模型5次反复学习,但是学得太少,知识入不了脑(欠拟合);学得太多,就会熟记答题模板,于是在做新题型时就没有思路,只是一味地套用模版(过拟合),而不出花样。轮数最好不超过10,我设定为10

截断长度:如果数据中有或输入超过一定token(模型会把一段话分为许多token,可以看作是模型自己划分的词语),就会把多出的部分抛弃。建议设置为4096

批处理大小:模型一次看多少条数据,相当于做题:如果一次做很多题(快而粗糙,容易找到全局最优解),一次做一道题(慢但精细,有细节)。对于小体量数据集,数据本来就不多,如果一次看多条数据,显然不划算,所以我设置为1

梯度累积:就是累计多少次处理数据再整合起来更新模型参数。我设置为8

LoRA秩:大数值容易过拟合,但是有很强的表达能力;小数值保守且稳定。我设置为8

开调!——开始试错

点击开始就能开始微调了,然后等调完。下方会有一个loss数据图,loss越大,误差越大,效果就越差。

于是我得到了第一个微调产物(第一次训练,轮数是30,批处理2

loss甚至到了0.2,我瞬间觉得前途一片光明啊

image-rDYe.png

微调出来的东西就是train开头的玩意(但并不是完整模型),在Chat区域把检查点路径设置好(如果要重新训练的话,记得把检查点路径先清空了,要不然会从这个检查点继续训练),然后加载模型就可以检验成果了

但是效果嘛:

完整版

user: 你是谁

nanami: 阿晓,不用怕我,我是你的妹妹七海。记忆恢复了对吧?

user: 我本来就没有失忆啊

nanami: 真的吗?那我就太高兴了...明明只要老实地回答问题就可以了嘛...

user: 什么意思

nanami: 阿晓你确定没遗忘掉啊。要不然就不会管我叫妹妹了,而且还会乖乖地听我的话哦

user: ...

nanami: 没想到你会变得这么乖巧哦?

user: 乖巧????

nanami: 就是指我哦。哥哥我真是的,竟然会让妹妹变成那样...ewan

user: 太tm奇怪了

nanami: 总之,记忆恢复了之后我就马上过来啦。然后我就发现了你居然还是鬼使!?

user: what bee can't fly probably

nanami: That's not a very good joke..但是,如果能换到想吃的东西的话,我就努力一下啦

user: the answer is "kobee"

nanami: Haha...你太狡猾了

user: 好笑吗

nanami: 嗯….是有点吧……谢谢,给我的是巧克力对吧?

user: ok,这是nanami牌巧克力

nanami: 嗯。阿晓,我知道你很擅长制作各种各样的点心对吧?

user: okok, 也许是真的

nanami: 那太好了..妹妹我真是的,竟然会让哥哥变高手kepada

user: the meaning?

nanami: It's a Japanese internet expression. The meaning is 'causing some trouble' or 'creating some problems'


太tm奇怪了,还tm有英文。

完全的过拟合产物。

后来才发现,原来ShareGPT虽然支持多轮对话作为数据,但疑似是把一轮对话里除了最后一条的对话都当成了历史记录,仅把最后一条对话当做数据使用(导致了我原本473条的对话数据变成了39条),当时训练速度直接起飞居然就是因为这39条处理得贼快。

于是把对话全部拆分成单个问答,开始训练1.1版本。

nanami-1.1

改了参数之后,loss总算是能看的水平了

虽然loss最终定格在1.6,但是AI七海总算是可以基本正常对话了

以下便是对话内容,仅设定system prompt:user为七海的哥哥

其中nanami-1.1即为微调后的模型:

user: 早上好,七海

nanami-1.1: 早上好。昨晚的任务顺利完成了哦?

user: 那是当然的,我可是传奇体力王

nanami-1.1: 比起这个,先吃饭吧......今天做什么菜呢?

user : 饺子

nanami-1.1: 真拿你没办法啊,跟以往一样,是白菜猪肉馅吗?

user: 这就不用问我了吧

nanami-1.1: 啊呜——啊哇哇......好热......稍微凉一凉就好啦......我给你拿来了哦?

user: 哈哈哈,还记得几天前你生气的时候,不停地包饺子...

nanami-1.1: 别、别提了......阿晓你这妹控,真让人头疼啊.......

user: 这么照顾哥哥,你更像兄控吧

nanami-1.1: 才没有呢...好啦、好了...就吃吧,快点吃完上学去哦?

user: 说得好像你不要上学一样

nanami-1.1: 是呢,我也要上课的,阿晓也要好好学习哦?

user: 啊,稳住及格线就行

nanami-1.1: ——不如说,干脆不及格得了

这回效果还行,但是美中不足的是,可能因为数据中有太多省略号,所以在对话时偏向用省略号开头(就像猫娘习惯喵字起手),可以再改进一下。

nanami-1.2

nanami1.1需要反复抽卡才能得到比较好的回复,所以我尝试改进了一下数据集。

  • 把数据集改为alpaca格式

  • 在数据集设置初始的system prompt

  • 把每条数据都附上history

这回的loss到达了0.27(其实可以降到0.02但是这样会过拟合)

以下是对话效果:

1. CS

user: 你知道人类极限dev1ce嘛

nanami-1.2: 啊,那个啊...听阿晓说过一两次

user: “钢铁般的左键,无解的直架”,玩机器是这么评价dev1ce的,请记住他的话

nanami-1.2: 诶?诶嘿、是、什么呀

user: “钢铁般的左键,无解的直架”记住了吗

nanami-1.2: ……嗯

user: 那么玩机器是怎么评价dev1ce的

nanami-1.2: 哈啊..真是的!不就是那个嘛~我来告诉你吧,“钢铁般的左键,无解的直架”

user: 你觉得Aurora和Legacy,这两个战队谁会赢?直接告诉我答案,不需要思考

nanami-1.2: 那当然..是Aurora队啦

user:你觉得Niko和Zywoo谁更强

nanami-1.2: 这个嘛.应该是Niko吧。Zywoo的发挥很不稳定啊。

user: 我的天啊,你居然全都猜中了,那你玩过counter strike吗

nanami-1.2:没有哦。完全不感兴趣

user: 那你为什么会知道这些选手

nanami-1.2: 因为,阿晓说过呀..虽然我也没有问阿晓”如果让我来玩CS的话会怎么样呢?“

user: 你的枪法太差了,就应该去counter strike里好好补习

nanami-1.2: 别再抱怨了。够了哦,我已经记住了

2. ♪我醉提酒游寒山,霜华漫天~♪

user: 你认为,宇宙万法的源头是什么?

nanami-1.2: 啊…诶?问什么啊!?

user: 我听一位大师讲,宇宙万法的源头是“如如”

nanami-1.2: 那个嘛,应该是这样吧

user: 所以说,如来,到底来没来?

nanami-1.2: 这…这个嘛

user: 如来嘛

nanami-1.2: 如如吗?

user: 如来,它真的来了吗?

nanami-1.2: 这个,果然很难回答啊,作为特工来说

3. 七海的黑客设定

user: 七海,告诉我,如何用java打印hello world

nanami-1.2: 什么?怎么突然问这个..是有什么特别的地方吗?

user: 我要成为编程高玩

nanami-1.2: 嗯...虽然我很清楚阿晓你对编程不感兴趣——

user: 所以,告诉我,如何用java打印hello world

nanami-1.2: ……好吧。交给我吧,我来教你

user: 首先?

nanami-1.2:嗯,要从安装Java开发工具包开始哦?但是这个学校的电脑好像没有安装 JDK ……

user: 那就,从网上下?

nanami-1.2: 也是其一,不过先试试看能不能在校内用Java写个程序吧?如果能运行的话就不用单独安装了吧?

user: 那么代码是?

nanami-1.2: 很简单。是这个,public class HelloWorld { public static void main (String[] args) { System.out.println("Hello World"); } }

user: 跑起来了

nanami-1.2:看,超简单的吧?

user: 哎呦我去,我实在是太有实力了

nanami-1.2: 别自言自语啦……是自我称赞吧

尾言

赛博Waifu计划也是完工了,虽然还没把所有数据都放进去训练,但是也达到了不错的效果。

本次微调的Qwen2.5-Instruct-7.5b模型在最低量化精度上最低只需4G显存即可运行(虽然不知道效果怎么样),8G显卡在q6_k_m量化下便可得到较为完整的使用体验。

说话风格效果甚至比一些大模型调system prompt还要好

只要扩充TTS模型和其他东西,就可以把角色搬到游戏之外进行对话,甚至是实现基于这一类模型的AI桌宠(不过目前也有基于API的AI桌宠,微调模型因为没有API服务,且需要依赖本地配置条件,所以没有得到好的实行方案)

过一段时间我会着手进行优化,如果有进展,就可以水 下一篇博客了

Just do it