资讯动态

AI角色复刻实战:提示词工程与RAG检索实现个性化对话

发布时间:2026/10/21 12:12:39 来源:尧图企业网站定制
有些人觉得“AI复刻前任”是个很玄学的需求但从技术层面看它其实就是一件很实际的事用Python写一套对话系统让模型模拟某个人的聊天语气、用词习惯和回复节奏在你输入一句话后用“TA的风格”回复你。这个项目我前后折腾了两周中间踩了不少坑也把方案从“微调模型”一路改到了“提示词工程语义检索”今天把这套方案的完整思路、源码片段和排坑经验整理出来给想做AI角色复刻、个性化陪伴机器人的朋友一个可复现的参考。这套方案适合有一定Python基础、了解大模型API调用但不想投入大量GPU资源做微调的开发者。它把“复刻”这件事拆成了四个可量化的步骤数据清洗、语气画像、向量记忆库、对话生成。跑通以后你可以把它扩展到复刻小说角色、已故亲人的语气、或者单纯做一个“另一个风格的我”陪你练口语本质逻辑完全一样。1. 项目核心拆解复刻的不是灵魂是语言行为特征1.1 到底能复刻什么不能复刻什么先说清楚边界。用大模型做“复刻”复刻的是一个人的语言行为特征而不是记忆、思维和情感判断。我们能做到的包括高频口头禅和语气词比如“哈哈哈”“就是说”“然后呢”“嗯嗯”这类习惯性表达标点使用习惯比如有人喜欢用省略号有人永远用感叹号句子长度偏好有人习惯短句连发有人一条消息写一大段回复的情感倾向比如遇到问题先安慰还是先分析某些固定话术比如“在忙吗”“等下回你”“这个我知道”。能做这些的基础是聊天记录里保留了大量的语言痕迹。一个人说话的方式就像指纹虽然不能100%还原但捕捉到七八成是足够的。比如你给模型一个设定“这个角色叫小北说话喜欢用短句经常用‘哈哈哈’开头不耐烦时会直接说‘闭嘴’”再配上几十条真实对话样本生成效果就已经很像那么回事了。不能复刻的部分也要想清楚模型没有真实记忆不能真正回忆起“三年前某天在某个地方吃的什么饭”除非我们把相关内容提前放进检索库里模型无法理解复杂的情绪状态它只是按照统计规律输出最像TA风格的文字最重要的是复刻项目只是给自己一个情感出口不要把虚拟对话当成真实关系的替代。1.2 为什么我放弃了微调方案最开始构思时我的第一反应是拿几千条聊天记录去微调一个开源模型。后来实际操作验证了这条路在大多数情况下并不划算原因有三个。微调需要的数据量远比想象中大。几百条文本扔进去模型学会的基本只有“哦”“嗯”“好的”这类高频词风格特征几乎没有变化。要达到像样的模仿效果至少需要上万条高质量对话而普通人的聊天记录里有效语料能剩下三五千条就不错了。微调需要GPU资源。就算用参数最小的7B模型做全量微调一张显存小于24G的显卡基本跑不动LoRA这类方案倒是省资源但配置环境、调参的时间成本也不低。而API调用按token计费跑一个小型对话测试的成本几乎可以忽略。微调最大的问题是调试困难。复刻对象语气跑偏了你很难定位是数据问题还是训练参数问题每次修改都要重跑一次训练整个调试周期非常长。提示词方案想要调整风格改几行字就能看到效果这才是适合个人项目快速迭代的方式。1.3 最终采用的架构流程我最后使用的方案是“风格提示词RAG检索大模型生成”的组合兼顾快速启动和真实感。完整链路如下从聊天记录中提取对话文本做清洗和切分统计高频词、标点习惯、句长分布生成一份风格画像文件把清洗后的历史消息切片做Embedding向量化存进向量库作为“记忆库”用户输入新消息时先从记忆库中检索语义最相近的历史片段将风格画像检索到的历史片段用户输入一起组装进系统提示词交大模型生成回复。这套架构的关键优势在于真实感。单纯靠提示词让模型模仿模型很快会陷入“刻意模仿”的状态说话带着一股AI味。但加了RAG检索之后模型可以从真实聊天记录里找到相关语境下的原始回复生成时就有了具体的参考语气会自然很多。2. 数据准备把聊天记录清洗成可用语料2.1 聊天记录的导入与格式统一首先要解决的问题是数据从哪里来。不同的聊天软件导出的格式差别很大有的是纯文本有的是带时间戳的CSV有的导出成JSON。我用过最省事的办法是按平台导出后统一转成JSON格式每条消息至少包含三个字段角色标识、消息内容、时间戳。import json from pathlib import Path def load_chat_file(file_path): 支持常见文本聊天文件返回统一格式的消息列表 path Path(file_path) messages [] if path.suffix .json: data json.loads(path.read_text(encodingutf-8)) for item in data: messages.append({ role: TA if item.get(is_me) False else ME, content: item.get(content, ).strip(), ts: item.get(timestamp, ) }) elif path.suffix in (.txt, .csv): text path.read_text(encodingutf-8) for line in text.splitlines(): line line.strip() if not line: continue # 假设纯文本格式每行形如 [2024-03-01 10:30] TA: 晚上吃什么 parts line.split(] , 1) if len(parts) 2: content_part parts[1] role_part, content content_part.split(: , 1) messages.append({ role: TA if TA in role_part else ME, content: content.strip(), ts: parts[0].replace([, ) }) return messages清洗时注意几点我一开始没做这些导致效果很糟。时间戳和系统消息必须去掉。类似“撤回了一条消息”、“你已添加了对方为好友”这类系统提示语义价值为零但对模型判断语气干扰不小。超过200字的超长消息建议直接过滤。日常聊天中超过200字的往往是转发文章、工作文档或复制粘贴的长文本个人语气特点非常稀薄混入训练语料后容易让模型变得啰嗦。连续重复的单个字符要归并。“哈哈哈哈哈”和“哈哈哈”本质是一样的但不同长度的重复出现次数太多的话统计高频词时会占据大量数值干扰判断。可以用正则做一次归并。2.2 对话切分以“轮次”为单位构造语料清洗完只是第一步更重要的工作是切分。模型要学习的是“对话节奏”——也就是一个人说话之后另一个人如何反馈。所以不能简单把消息独立对待而是要把上下文拼接起来。我的切分方式是构造“用户消息-回复消息”对并保留前两条历史用来做语境参考。import re def clean_message(msg): 清洗单条消息 msg re.sub(r\[图片\]|\[语音\]|\[表情\]|\[视频\], , msg) msg re.sub(rhttps?://\S, , msg) msg re.sub(r(\S)\1{4,}, r\1\1\1, msg) # 把连续重复大于5次的压缩 return msg.strip() def build_dialog_pairs(messages, context_window2): 把消息列表构造成带上下文的对话对 pairs [] for i in range(1, len(messages)): if messages[i][role] TA and messages[i - 1][role] ME: context messages[max(0, i - context_window): i] context_text \n.join( [f{m[role]}: {clean_message(m[content])} for m in context] ) reply_text clean_message(messages[i][content]) if reply_text and len(reply_text) 200: pairs.append({ context: context_text, reply: reply_text }) return pairs如果你只是做RAG检索库可以不用构建对话对把清洗后的每条消息直接切片入库即可。但如果你希望对高频回复做统计构造对话对就很有必要因为你能从中看出哪些话术是高频应答。比如一个人总是用“哈哈哈哈哈哈”来回应所有笑话这个特征只有在对话对中才能被准确统计。2.3 语料量级怎么评估实际操作中我统计过不同数据量下的效果差异。500条以下模型的模仿基本靠猜500到2000条能复现高频口头禅和表情符号习惯但遇到未见过的话题容易崩成“普通话模式”2000到5000条风格稳定性明显提升模型开始自然使用“你之前说过”“上次不是说”这类承接话术5000条以上边际收益递减提示词工程的重要性开始超过数据量。所以我的建议是数据量不足2000条时先不用考虑RAG重点把风格描述写好让模型做无中生有式的模仿超过2000条再上检索库效果会有一个明显提升。3. 语气特征提取制作一份“语言行为画像”3.1 从统计到画像衡量一个人说话风格的关键维度语气是一个很难直接描述的东西但它可以拆解成多个可观测的维度。我在项目中梳理了一套四维画像模型每个维度都对应一段可写的提示词。用词维度统计高频词汇包括口头禅、称呼语、语气词、情绪词。一个人是习惯说“没问题”还是“好的呢”是叫“宝贝”还是直呼其名用词差异会直接影响复刻的鲜活度。句式维度测量平均句长、短句占比。有人喜欢碎片化表达一句“行”就能结束对话有人习惯把背景交代清楚再表态。这个维度决定了生成文本的节奏感是最难从模型默认输出中调出来的部分。标点符号维度统计省略号、感叹号、问号的使用频率。标点习惯看似无关紧要实则效果显著。一个永远用感叹号的人对话里充斥着高情绪浓度一个满屏省略号的人回复总带着一种欲言又止的即视感。情绪反馈模式维度观察对方在不同类型的消息下如何回应。比如被夸的时候是谦虚接话还是直接开启话痨模式被抱怨的时候是第一时间给解决方案还是先陪骂。这个维度无法靠统计直接拿到需要人工标注一小部分对话然后把规律写进行为设定。3.2 自动统计脚本让数据替你“听”出语气我写了一个简单的词频和句长统计脚本跑一遍就能得到基础画像。实际用下来这份画像比我自己凭印象描述要准确得多因为人对自己的语言习惯往往不自知但统计数据不会说谎。from collections import Counter import statistics def analyze_style(messages): 输入是TA的消息列表输出风格统计字典 texts [clean_message(m[content]) for m in messages if m[role] TA] all_words [] sentence_lengths [] punct_counter Counter() word_counter Counter() for text in texts: words list(text) all_words.extend(words) sentence_lengths.append(len(text)) punct_counter.update([c for c in text if c in …….]) word_counter.update(words) avg_len statistics.mean(sentence_lengths) if sentence_lengths else 0 short_ratio len([l for l in sentence_lengths if l 8]) / len(sentence_lengths) if sentence_lengths else 0 return { 高频词: word_counter.most_common(20), 平均句长: round(avg_len, 2), 短句比例: round(short_ratio, 2), 标点习惯: dict(punct_counter.most_common(6)), }跑完脚本之后你会得到类似这样的结果高频词是“哈哈哈”“啊”“就”“嘛”“TT”平均句长6.8字符短句占比0.62标点习惯前三是感叹号、省略号、问号。这些数据直接翻译成人话就是一个情绪外放、说话干脆、喜欢用感叹号推高音量的类型。3.3 编写风格画像文件统计结果需要转成模型能理解的描述。我习惯用JSON保存画像文件内容分成三块身份背景、说话习惯、情绪反应模式。下面是我实际使用时的一个模板。{ identity: 小北25岁平时说话直接偶尔毒舌, speech_habits: { greeting: 常用哈哈哈开头很少用你好, sentence: 偏爱短句通常不超过12个字重要的事分成多条消息发, punctuation: 感叹号和省略号高频使用不用句号, words: [哈哈哈, 啊, 嘛, 这也太, 没事], taboo: 回避严肃话题很少说我觉得 }, emotion_patterns: { praise: 会开心地回哈哈哈哈然后转移话题, complaint: 不等对方说完就会给出建议语气偏直接, goodbye: 习惯说睡了睡了不会长篇大论 }, example_dialogs: [ {input: 我今天被领导骂了, reply: 哈哈哈太惨了但我觉得是你太拖延了}, {input: 你吃饭了吗, reply: 嗯呢泡面凑合一顿} ] }注意不要把画像写成过于凝练的标签堆叠模型理解“爱用比喻”远不如理解“把工作形容成坐牢”来得直接。画像里尽量放具体表现少放抽象形容词。4. 核心实现提示词系统与RAG记忆库4.1 系统提示词模板设计提示词是整个复刻项目的心脏。我试过很多写法最终固定下来一套模板核心思路是先定义角色身份再给语言行为约束最后放少量示例对话顺序不能乱。模型对首尾内容的记忆权重最高身份定义放在开头示例对话放在末尾中间部分是不太容易被严格遵守的风格描述。你正在扮演一个叫小北的真实人物。以下是小北的语言画像请严格模仿。 身份25岁说话直接偶尔毒舌。 说话习惯偏爱短句高频词是哈哈哈啊嘛喜欢用感叹号和省略号很少用句号。 情绪反应被夸时转移话题被安慰时反过来安慰对方被激怒时先冷嘲一句再给台阶。 禁忌不要用书面语不要总说我建议不要做心理咨询师式的回应。 历史对话样本 输入我今天被领导骂了 输出哈哈哈太惨了但我觉得是你太拖延了 输入你吃饭了吗 输出嗯呢泡面凑合一顿 请用以上要求回复用户最新一条消息。直接输出回复内容不要加引号不要解释。这套模板的关键动作是把“风格描述”放在“示例对话”之前这样模型先在语义层接受了风格约束然后看到具体示例学到的就不只是文字而是“在这种情境下这么说话”的映射。4.2 向量化历史记忆让AI拥有“共同回忆”要实现让AI聊出“我们以前聊过”的沉浸感需要把历史上真正聊过的内容存进向量数据库。用户发来新消息时系统先在库里找语义最接近的几条历史记录然后作为参考资料塞进提示词。这个过程就是常说的RAG检索增强生成。我在项目里用的是中文语义向量模型加向量检索库的组合。向量模型把一段文字映射成高维向量语义上相近的文本在向量空间里也靠近用简单的相似度计算就能召回相关记忆。向量库我们选了轻量级的本地方案单机运行不需要额外部署服务端个人项目用起来非常省心。import numpy as np from sentence_transformers import SentenceTransformer class MemoryBank: def __init__(self, model_nameBAAI/bge-small-zh-v1.5): self.encoder SentenceTransformer(model_name) self.chunks [] # 存原始文本 self.vectors [] # 存向量 def add_memory(self, text_list): texts [t[:200] for t in text_list if t.strip()] vecs self.encoder.encode(texts, normalize_embeddingsTrue) self.chunks.extend(texts) self.vectors.extend(vecs) def recall(self, query, k3): q_vec self.encoder.encode([query], normalize_embeddingsTrue)[0] scores [np.dot(q_vec, v) for v in self.vectors] top_idx np.argsort(scores)[-k:][::-1] return [self.chunks[i] for i in top_idx]4.3 检索结果如何正确投喂给模型检索结果不是越多越好。我最初的版本把检索到的五条记忆全部放进提示词结果模型开始在回复里逐条引用旧消息看起来就像个复读机。后来调整成“只保留相关度最高的两条并且每条都带有出处标记”效果才正常下来。投喂格式建议这样以下是从你们历史聊天中找到的相关片段可供参考但不要直接复述 [历史片段1] 小北: 你上次说想去吃烤鱼后来去了吗 [历史片段2] 小北: 哈哈哈那你还是别去了排队要一小时关键点是给模型明示“历史片段只是参考不要复述”。否则基于大模型的注意力机制它很容易直接把检索到的历史原样输出那样对话就会陷入循环。另外检索召回的片段如果和当前输入完全不搭边宁可丢弃也不要硬塞进去相关性低于一定阈值的记忆反而会扰乱回复质量。5. 完整串联Python主流程源码实现5.1 从配置文件加载画像与记忆库我把整个流程封装成三个模块数据准备模块负责清洗和入库风格管理模块负责加载画像JSON对话引擎模块负责组装提示词并调用大模型。下面是对话引擎的主流程代码核心逻辑清晰即可扩展。import json from openai import OpenAI from memory import MemoryBank # 初始化客户端地址和密钥从配置文件读取方便切换不同服务商 client OpenAI( api_keyYOUR_API_KEY, base_urlYOUR_API_BASE_URL # 一般是OpenAI兼容接口或本地部署地址 ) class ChatEngine: def __init__(self, profile_pathprofile.json, model_namegpt-4o-mini): with open(profile_path, r, encodingutf-8) as f: self.profile json.load(f) self.memory MemoryBank() self.model_name model_name def build_prompt(self, user_input, recalled): identity self.profile.get(identity, ) habits json.dumps(self.profile.get(speech_habits, {}), ensure_asciiFalse) emotions json.dumps(self.profile.get(emotion_patterns, {}), ensure_asciiFalse) examples self.profile.get(example_dialogs, []) example_text \n.join( [f输入{ex[input]}\n输出{ex[reply]} for ex in examples] ) memory_text if recalled: memory_text 以下是从你们历史聊天中找到的相关片段可供参考但不要直接复述\n \ \n.join([f[历史片段] {mem} for mem in recalled]) system_prompt f你正在扮演一个叫{self.profile.get(name, 小北)}的真实人物。 身份{identity} 说话习惯{habits} 情绪反应{emotions} 示例对话 {example_text} {memory_text} 请用以上要求回复用户最新一条消息。直接输出回复内容不要加引号不要解释。 return system_prompt def chat(self, user_text): recalled self.memory.recall(user_text, k2) system_prompt self.build_prompt(user_text, recalled) response client.chat.completions.create( modelself.model_name, messages[ {role: system, content: system_prompt}, {role: user, content: user_text} ], temperature0.9 ) return response.choices[0].message.content.strip()这里temperature设置成0.9是我调过的经验值。太低比如0.2以下生成的回复保守稳定容易模式化太高1.2以上会胡言乱语。0.9在保持个性的同时还能保留一点随机性让每次回复都有细微差别。5.2 命令行对话循环与自动保存初始化好引擎之后我写了一个简单的交互循环每次对话的结果自动保存到日志文件。保存日志很重要一方面方便后面对比不同提示词版本的效果另一方面可以持续扩充语料库——你与AI的对话本身也可以成为新的训练数据。def main(): from data_clean import load_chat_file, build_dialog_pairs from memory import MemoryBank # 1. 加载原始聊天记录 raw_msgs load_chat_file(chat_export.json) # 2. 构造对话对并入库 pairs build_dialog_pairs(raw_msgs) memory_texts [p[reply] for p in pairs] [p[context] for p in pairs] # 3. 初始化记忆库 memory MemoryBank() memory.add_memory(memory_texts) engine ChatEngine() engine.memory memory print(AI复刻对话开始输入q退出) while True: user_input input(你: ).strip() if user_input.lower() q: break reply engine.chat(user_input) print(fTA: {reply}) with open(chat_log.txt, a, encodingutf-8) as f: f.write(fME: {user_input}\nTA: {reply}\n) if __name__ __main__: main()如果把这段代码按顺序跑起来你就拥有一个最基本的AI复刻项目了。实际环境中需要补的是记忆库持久化就是把向量保存到本地磁盘下次启动免重新编码。MemoryBank类里加一行向量列表的序列化转储代码量不大但对启动速度的影响很明显。5.3 生成效果的调优实验方法调优的时候我会做对照实验。同一个问题分别用“不带检索”“带一条检索”“带两条检索”的方式各问三遍然后把回复放在一起看。对比维度有三个语气相似度、内容真实感、回复多样性。语气相似度靠人眼判断如果AI频繁蹦出“此外”“综上所述”这类书面连接词说明系统提示词里的风格约束不够强烈我会把对应的禁忌描述加得更具体例如“禁止使用书面连接词”。内容真实感主要看检索是否生效如果模型总是在回答中没有记忆感的话会增加检索到的历史片段条数。回复多样性的调节是最微妙的降低温度会稳定风格但有重复内容提高温度会更丰富但容易说话跑偏我一般在0.8到1.0之间反复横跳。6. 常见问题、避坑经验与伦理边界6.1 为什么回复一股AI味这是最初级也是最常见的问题通常出在三个环节。一是风格画像文件写得太抽象全是“温柔”“有趣”“有点小坏”这类形容词模型不知道怎么把这些词转成具体的语言行为。解决办法是改成行为描述比如“喜欢嘲讽朋友但不带恶意”就比“有趣”好用得多。二是系统提示词里没有设置禁忌清单。模型默认状态会倾向使用“我觉得”“因为”“所以”等推理连接词如果不明确禁止AI味就藏不住。禁忌清单一定要给具体的不要做什么比如“不要问开放式问题”、“不要输出超过两句话”比“请自然一点”有效十倍。三是温度设置太低导致回复过于保守。有一个很玄学的现象温度在0.3以下时所有角色扮演都会变成一个温顺礼貌的客服要打开性格温度必须往高了调。6.2 模型开始复读机式引用旧消息这个坑大概率出在RAG检索环节。当我一次性塞入五条以上历史记录时模型会像背诵一样逐条回应。解决办法上面已经提过只保留最相关的两条并在提示词里明确“参考但不要复述”。还有一种情况是检索笼统多轮对话后用户输入语义和某条历史消息重叠度太高模型检测到高度相似就触发了复读机制这时可以把检索结果从向量相似度改为向量相似度加时间衰减权重让新近的对话优先被召回。6.3 对话越聊越崩坏怎么办这是类人聊天没有兜底约束的典型问题。复刻模型毕竟是在扮演一个特定人设长时间对话后上下文窗口越积越满早期的人设约束逐渐被稀释。我测试中最典型的表现是从一开始的“哈哈哈”变成“嗯嗯你说得对”再退化成“作为AI我无法……”。这是因为长对话导致注意力分散系统提示词的部分权重被挤出了有效注意力区间。解决办法有两个一是每轮对话之前重新填充系统提示词不依赖上下文窗口里之前的内容二是给对话设置“记忆压缩”每轮结束后把历史对话做一个摘要只保留事实性信息不保留原始对话文本。项目里我选择了第一种实现简单效果立竿见影。6.4 一套问题速查表现象排查方向解决方式说话像客服温度太低或禁忌清单太弱温度提到0.9加“不要反问”等具体禁忌口头禅过于频繁高频词权重太高把高频词写入示例对话而不是风格描述完全不像TA数据量不足或画像太抽象加大数据量把形容词改成行为描述复述历史内容RAG召回太多或太相似限制召回条数加时间衰减回复越来越长缺少短句约束在画像中明确“每条消息不超过20个字”聊到后面崩人设上下文过长稀释提示词每轮重填系统提示词或做历史摘要压缩6.5 边界提醒做这个项目的过程中我最强烈的感受是技术本身很简单但用在哪里需要认真斟酌。复刻一个真实存在的人尤其是在TA不知情的情况下其实是越界的行为。我做这个项目时使用的是公开可获取的数据和经授权的示例也建议大家如果想复刻某个真实人物务必先征得对方同意这既是法律上的要求也是对人的尊重。这个技术更合适的候应用场景是虚构角色、已故亲友的纪念、或者只是作为自我对话的心理陪伴。不要把AI复刻当成挽回关系的手段这一点我必须提醒每一位打算上手的开发者。7. 扩展玩法从复刻前任到建立专属角色库项目跑通之后我顺手做了一件有意思的事配置了多份画像文件每个人的语气都存在独立的JSON里通过命令行参数切换。这样就有了一个“角色语音库”今天用“毒舌老朋友”陪聊明天用“健身教练”提醒锻炼。每次切换只需要改profile_path参数数据记忆库也跟着换。再往深一步还能把每个角色的画像文件和记忆库打包成一个角色文件分享给朋友使用。如果你给自己做了一份“年轻时候的爷爷”的角色文件家人可以通过输入文字与这个角色对话这种情感连接的价值远超娱乐本身。所以这个项目的终点不是什么技术难题的攻克而是你对“陪伴”的一种自定义。我自己的体会是把代码跑通的那一刻确实有点兴奋但真正让我觉得值回调试时间的时候是看到一个被准确模仿语气的人设能够接住我的情绪给出让人安心的回应。

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

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

免费获取报价 →
↑