构建我的 AI 镜像:使用个人聊天记录微调 GPT-4o
我最近进行了一项有趣的实验:尝试使用 OpenAI 的 GPT-4o 模型,通过我个人的即时通讯聊天记录进行微调,以创建一个能模仿我聊天风格的「AI 镜像」。尽管在数据过滤阶段遇到了严峻挑战,导致可用数据量锐减,但最终还是成功地微调出了一个模型,它在一定程度上复刻了我的部分沟通范式。
在线体验
你可以在下面的组件中与我的 AI 镜像进行简单的对话。请注意,出于隐私考虑,部署时使用的 Prompt 移除了所有可能涉及个人身份信息 (PII) 的内容,并且模型本身没有学到敏感信息,所以它的表现可能与我私下测试时有所不同。
如果你对整个过程感兴趣,可以继续阅读以下内容。 (这篇文章最初是用英文写的,但写完后我才意识到由于训练数据的问题,模型微调后通常只会用中文回答,导致英文读者无法理解。因此,我将文章翻译回中文,部分语句可能会有些不自然。)
这个流程比我预想的要简单许多,使用 OpenAI 的 API 来进行微调的情况下,主要工作基本上都集中在前期的数据预处理阶段。整个过程涉及数据导出、格式转换、内容过滤、数据集分割和模型微调。以下是详细步骤和其中的关键发现。
数据准备与格式化
一切始于数据。我首先从常用的即时通讯软件中导出了我的个人聊天记录(此步骤已获得聊天记录中涉及的朋友同意)。原始数据(通常是 JSON 格式)需要转换成 OpenAI 微调任务所要求的特定 JSONL 格式。我通过将朋友的发言作为user,将我的发言作为assistant,并使用一个符合我说话方式的instructions来描述我的角色,来生成最终的 JSONL 文件。
为此,我编写了一个简单的 Python 脚本来合成数据集。这个脚本的内核逻辑大致如下:
function convert_chat_data(input_json_path, output_jsonl_path, user_id, assistant_id, instructions):
// 1. 读取输入的 JSON 聊天记录文档
chat_data = load_json(input_json_path)
// 2. 初始化一个列表来存储处理后的对话片段
openai_conversations = []
current_conversation_messages = []
// 3. 遍历原始消息
for message in chat_data['messages']:
// 提取发送者 ID 和文本内容 (处理复杂文本结构)
sender = message['from_id']
text = extract_text(message['text'])
timestamp = parse_timestamp(message['date'])
// 跳过非目标用户或无有效文本的消息
if sender not in [user_id, assistant_id] or not text:
continue
// 确定角色 ('user' 或 'assistant')
role = 'user' if sender == user_id else 'assistant'
// 4. 合并短时间内的连续消息
if should_merge_with_previous(message, last_message):
append_text_to_last_message(current_conversation_messages, text)
else:
// 添加上一条合并后的消息 (如果存在)
add_merged_message_to_conversation(current_conversation_messages, last_merged_message)
// 开始新的消息块
start_new_merged_message(text, role, timestamp)
// 5. 如果消息间时间间隔过长,则结束当前对话片段,开始新的片段
if time_gap_exceeds_threshold(timestamp, last_message_timestamp):
// 清理并保存上一个对话片段 (确保格式正确,如用户消息开头)
if current_conversation_messages is valid:
// 在每个对话片段前添加系统提示
final_messages = [ {'role': 'system', 'content': instructions} ] + cleanup_conversation(current_conversation_messages)
openai_conversations.append({'messages': final_messages})
// 重置当前对话片段
current_conversation_messages = []
// 处理最后一个对话片段...
// 6. 将所有处理好的对话片段写入输出的 JSONL 文档
write_to_jsonl(output_jsonl_path, openai_conversations)这个脚本的关键在于正确地将对话流分割成符合 OpenAI 要求的样本格式,每个样本都以包含详细角色定义的系统提示开始,然后是 user 和 assistant 交替的消息。
内容过滤:意想不到的挑战
在将数据用于微调之前,必须通过 OpenAI 的内容审核,以确保符合其使用政策。我使用了 OpenAI 的 Moderation API 来自动过滤数据,并编写了一个简单的脚本来处理这个流程。该脚本异步地处理每一条对话数据,调用 Moderation API 进行检查,并根据返回的分类分数判断是否合规。
然而,我很快遇到了一个棘手的问题。即使使用了 Moderation API,我的微调任务仍然因为「存在违规数据」而被拒绝。经过一番研究,我在 OpenAI 开发者论坛找到了答案:微调过程使用的内容审核标准比标准的 Moderation API 调用严格得多。具体来说,微调似乎采用了一个极低的内部阈值(据讨论可能是 0.001),远低于公开 API 默认或通常使用的阈值。
这个发现对我来说非常震惊。这意味着大量在我看来完全正常的日常对话,因为触及了这个极其敏感的内部标准而被标记为不合规。结果是,我最初准备的近十万轮对话数据,在经过这个严格过滤后,最终只剩下了一百多条!
这无疑是一个巨大的挫折,严重影响了可用于训练的数据量。尽管如此,我还是决定用这仅存的一百多条数据继续尝试。这也让我开始考虑,对于这类个性化微调,未来或许可以探索使用限制更少的开源 LLM。
数据集分割与微调
在过滤之后,我使用了一个简单的脚本将剩余的数据集随机打乱,并按一定比例(例如 80/20)分割为训练集和验证集。对于数据量如此之小的情况,其实也可以选择将所有数据都用作训练。
使用 OpenAI 的 API 进行微调本身相对简单直接。准备好训练集和验证集文档,然后通过 API 调用创建一个微调任务即可。
一个需要特别注意的参数是 learning_rate_multiplier。这不是学习率本身,而是应用于模型预设学习率的一个乘数因子。OpenAI 的默认值(高达 1.8 )对于微调任务来说通常过高。根据 Azure OpenAI 的文档和社区经验,建议将此值设置得低得多,例如在 0.02 到 0.2 的范围内,以获得更稳定的微调效果。
微调过程通常很快,对于我这样的小数据集,仅需几十分钟即可完成。另外,推荐提前撰写一个脚本来检查数据集格式的准确性并统计一些基本信息,如 token 数量分布等,这有助于在任务开始前发现潜在问题。
效果与反思
微调完成后,我获得了一个自定义模型(ft:gpt-4o-...:my-ai-mirror:...)。我在设置了包含详细角色扮演指示的 System Prompt 后,与这个「AI 镜像」进行了一些对话。
考虑到训练数据只有微不足道的一百多条,效果可以说还不错。AI 在一定程度上捕捉到了我平时和朋友聊天时的一些语气和习惯,例如过于简洁、有时略显「爱搭不理」的风格。
但这也引出了一个重要的反思:这个 AI 镜像是不完整的。它学习的数据仅仅来源于我和特定朋友在即时通讯软件上的对话。这种对话场景下的我,只是真实我的一个侧面,并不能代表我的全部。它无法完全复刻我在不同情境、与不同人交流时的多样性,更不用说那些未曾通过文本表达的思想和情感了。
因此,虽然这是一个有趣的技术尝试,但称之为完整的「AI 镜像」还为时过早。它更像是一个基于特定数据集的、带有我个人色彩的聊天机器人。
随后,我编写了一个简单的 Preact 组件,并将其嵌入到这篇文章中,就是你在开头看到的部分。未来有时间的话,我可能会再写一篇关于这个组件实现细节的文章。
总结与展望
通过这次实验,我成功地体验了使用 GPT-4o 微调个人聊天数据的全过程。最大的挑战来自于 OpenAI 极其严格的内容审核机制,导致可用数据量急剧减少。尽管如此,用有限的数据微调出的模型仍然展现出了一定的个性化效果。
这次尝试也让我更深刻地认识到,当前的 AI 技术在真正「克隆」一个人的复杂性方面还有很长的路要走。数据的来源和质量极大地限制了最终模型的表现和代表性。
未来,我可能会考虑使用数据限制更宽松的开源 LLM 进行类似实验,或者探索更丰富、更多样化的数据来源,以期构建一个更全面、更真实的「AI 镜像」。