尝试该工具
AI 前任聊天 → 从聊天记录中提炼模拟角色
从凌晨 3 点开始的问题
我做 AI 前任聊天,是因为睡不着。我反复读着旧微信消息,心想:能不能从这些记录里提炼她的表达方式,做成一个能对话的影子?不是替代她——只是一个影子。映出她曾经写字的方式、常用的短语,以及温柔时和不耐烦时不同的语气。
但当我开始构建这个项目时,遇到了一个与AI或语言模型完全无关的瓶颈: 谁说了什么?
当你上传聊天导出文件——比如微信HTML文件、Telegram JSON文件——发件人信息是明确标注的。很简单。但大多数人没有干净的导出文件。他们只有截图。成百上千张截图。而一张截图只是像素。没有元数据,没有发件人字段,没有结构化数据。只有两列文字气泡的图像,你必须判断哪些是她的,哪些是你的。
一旦认错,AI 人格就会混成一团——一半是你,一半是她,完全分不清。你说“我想你”,它却用你自己过去的话回答。这不是释怀,而是一座镜子迷宫。
版本 1:基于像素的头像检测(以及为何失败)
我的第一个方案是显而易见的: 检测头像。在微信、QQ和抖音中,每条消息旁边都有一个小头像——对方在左边,你自己的在右边。如果我能检测到这些头像区域,就能为每条消息标注发件人。
算法的工作原理如下:
- 扫描图像左侧的15%和右侧的15%,寻找非背景像素的聚类(头像块)
- 当在左侧发现一个聚类时,将其对应的Y轴范围标记为‘他们的消息’;右侧为‘你的消息’
- 对每个消息区域进行OCR识别,并分配检测到的发送方标签
它有时能工作。头像清晰、布局规整的截图更容易处理,但这篇手记没有留下可复现的准确率基准。以下情况仍容易失败:
- 裁剪后的截图,头像被截断
- 群聊,双方均有多个头像
- 抖音,头像为圆形,有时会与消息气泡重叠
- WhatsApp,其头像显示方式不同
- 包含贴纸、图片或语音消息的任意截图,这些内容干扰了像素扫描器
备用方案是气泡颜色检测——在微信中绿色气泡表示你,白色表示对方;在抖音中蓝色气泡表示你,白色表示对方。但气泡颜色会因主题、深色模式和安卓版本而变化,这是一个建立在另一个脆弱规则之上的脆弱启发式方法
我需要一种从根本上不同的方法
版本2:使用Tesseract的边界框加上基于位置的检测
突破性进展出现在我意识到Tesseract.js不仅返回文本,还返回每行文本的边界框。每行文本都带有bbox.x0、bbox.x1、bbox.y0、bbox.y1——即该行在页面上的像素坐标
这一点改变了思路,因为聊天消息具有空间结构:
- 对方消息从屏幕的左侧开始
- 你的消息从屏幕的右侧开始
- 这一规律在微信、QQ、抖音、WhatsApp、Telegram等几乎所有聊天应用中都成立
因此,我不再试图检测头像或气泡颜色,而是直接观察文本的起始位置。以下是核心算法:
// Get image midpoint
const MID_X = imageWidth / 2;
for (const line of ocrLines) {
const bbox = line.bbox;
const lineLeft = bbox.x0; // Where the line starts
const lineCenterX = (bbox.x0 + bbox.x1) / 2;
let sender;
if (lineLeft < MID_X * 0.6) {
// Left portion → ex-partner
sender = '对方';
} else if (lineLeft > MID_X * 0.8) {
// Right portion → you
sender = '我';
} else if (lineCenterX < MID_X * 0.75) {
// Center-left → likely ex-partner
sender = '对方';
} else if (lineCenterX > MID_X * 1.1) {
// Center-right → likely you
sender = '我';
} else {
// Ambiguous → carry previous sender
sender = currentSender || '';
}
}
阈值(0.6、0.8、0.75、1.1)是图像中间点的启发式倍数,不是整张图片的宽度百分比,也不是经过验证的准确率。主要信号为左边缘(x0),中心位置作为补充;不同裁剪、布局或语言仍可能导致误标。
这种方法是:
- 无主题依赖: 不关心深色模式、气泡颜色或头像是否可见
- 无应用依赖: 支持微信、QQ、抖音、WhatsApp、Telegram 等所有平台
- 抗裁剪: 只要保持左右结构,即可正常工作
- 速度快: 不需要像素扫描——仅通过坐标数学运算,Tesseract 已提供
过滤界面文字与 OCR 噪声
聊天截图 OCR 也可能识别出与对话无关的界面文字,例如:
- 时间戳,如 "16:24" 和 "06/02 20:35"
- 界面标签,如 "发送消息"(Send Message)和 "消息"(Messages)
- 导航元素、电池指示、信号条被渲染为文本
- 图像压缩产生的随机噪声
在基于位置的检测之前,必须先过滤掉这些噪声。垃圾过滤器使用三条规则:
// Rule 1: Skip timestamps
if (/^\d{1,2}[:\/]\d{2}/.test(text) && text.length < 12) continue;
// Rule 2: Skip UI elements
if (/^(发送消息|发送|消息|Message|Send)/i.test(text)) continue;
// Rule 3: Skip lines with >70% non-meaningful characters
const cjk = text.match(/[\u4e00-\u9fff\u3400-\u4dbf]/g) || [];
const letters = text.match(/[a-zA-Z]/g) || [];
const meaningful = cjk.length + letters.length;
if (meaningful / text.length < 0.3 && text.length > 4) continue;
过滤器能排除部分无关内容,也有明确限制:当前代码跳过不足两个字符的行,所以“嗯”这样的单字回复可能丢失。超过四个字符的行,如果中日韩汉字或拉丁字母占比不足 30%,也会被过滤。请检查解析后的记录;过滤过程并非无损。
步骤 1.5:发送者确认步骤
基于位置的检测很好,但并非完美。有些截图布局异常。有些消息居中显示(如系统通知)。有些用户以横屏模式截图,导致左右结构颠倒。
与其试图算法性处理所有边缘情况,我增加了 步骤 1.5:发送者确认 。在 OCR 处理和初步发送者标注之后,工具会暂停,并向用户展示前 15 个检测到的消息及其分配的发送者:
- 被标记为
[对方](前任伴侣)的消息显示在左侧 - 被标记为
[我](你)的消息显示在右侧 - 一个 交换发送者 按钮可翻转所有标签,如果检测结果将发送者弄反了
- 一个 确认 按钮将进入提炼阶段
这是一个刻意的设计选择: 让算法承担繁重的工作,但保留人类在最终决策中的参与。 如果采用完全自动化检测且无需人工审核的方案,将会产生无声错误的结果,从而破坏整个角色的完整性。一个10秒的确认步骤,可以避免数小时的扭曲对话。
OCR预处理:为什么‘想’和‘相’很重要
中文 OCR 可能混淆形近字。“想”由“相”与“心”组成,并非只差一横。低分辨率图片若丢失下方部件,可能把“我想你”识别为“我相你”,造成改变原意的错误。修正时仍应以原截图为准。
解决方案是:在OCR之前进行激进的预处理
- 2x 上采样: 每个检测到的消息区域都通过双线性插值扩大到原始尺寸的两倍。这为每个字符笔画提供了更多的像素信息。
- 灰度转换: 颜色信息对字符识别无用,反而会引入噪声。转为灰度图简化了识别问题。
- 对比度拉伸: 将像素值重新映射,使最暗的像素变为纯黑,最亮的变为纯白。这能有效增强字符边缘与彩色聊天气泡背景之间的对比度。
预处理可能让部分字符更易识别,但这里没有可复现的准确率百分比。后续 AI 校正请求会根据上下文建议修正,也可能补写不存在的词或误判发送者。重要内容应与截图对照,不要假定 AI 能发现全部错误。
共享的Tesseract工作线程:从分钟到秒
我早期犯过一个性能错误:对每个聊天气泡单独调用Tesseract.recognize()。每次调用都会创建一个新的WASM工作线程,加载语言模型,执行识别,然后终止线程。对于包含30+个消息区域的截图,这意味着30+次工作线程启动,每次耗时5–10秒。总OCR耗时:3–5分钟每张截图。
解决方案是采用共享工作线程模式:
// Create ONE worker, reuse for all zones
const worker = await Tesseract.createWorker('chi_sim+eng');
const results = [];
for (const zone of messageZones) {
const { data } = await worker.recognize(canvasForZone);
results.push(data);
}
await worker.terminate();
处理多个区域时,复用工作线程可避免反复初始化。当前实现对预处理后的截图进行一次识别,然后结束该线程。上面的代码示例用于说明复用思路,并非当前逐图的完整生命周期,也不保证每台设备达到同一速度。
角色提炼:6 步流程
完成解析与发送者检查后,AI 阶段调用用户配置的服务商。本手记最初讨论的是 DeepSeek 接入,当前工具也支持兼容服务商。导入文字受请求长度限制,并适用服务商费用和数据保留政策:
- OCR校正 + 发送者ID — DeepSeek审查原始OCR文本,纠正误识别字符,并根据对话上下文验证发送者标签
- 初步分析——AI 总结请求中包含的片段,不一定读取每一条导入消息
- 记忆提取 — 识别核心记忆、共同经历和重要时刻
- 角色提炼 — 构建详细的人物画像:语言习惯、情感倾向、常用表达、幽默风格、沟通习惯
- 系统提示生成 — 将角色转化为带有身份保护规则的聊天系统提示
- 聊天就绪 — 用户现在可以与提炼出的角色进行对话
关键是区分对话上下文与归属于对方的消息。经过前面的消息条数筛选后,当前提炼请求最多取 8,000 字符上下文和 4,000 字符对方消息。提示要求从 [对方] 提取表达风格,仅把 [我] 当作背景。这些是指令,不保证模型不会混淆或编造。
如果没有这种分离,AI将会把两个人的说话方式混合成一个混乱的人格。严格的仅提取前任消息,才是让这个角色显得像他们,而不是你们两人的关键。
身份守护机制:防止人格漂移
早期测试中一个令我不安的问题是人格漂移:聊得久了,AI 会开始模仿用户的说话方式。如果你常说“haha”和“lol”,它也跟着说;如果你语气温暖亲昵,它也会模仿这种温暖,而不管原先提炼的表达风格。
解决方法是在每个系统提示中加入一个身份守护规则:
You are [name], NOT the user.
Never mirror the user's speech patterns.
Your personality is fixed based on the distilled persona.
Do not adapt to the user's tone, mood, or vocabulary.
后续请求会继续包含人格描述,以帮助维持预期风格。模型不像人一样记忆,上下文限制和生成波动仍可能导致偏移。重复指令能减少一类不一致来源,但不保证忠实复制真人。
IndexedDB:人格的栖息地(以及消亡之地)
所有人格数据都存储在浏览器的IndexedDB中——两个对象存储区:
persona—— 提炼后的人格档案、姓名、头像、记忆摘要、修正记录chats—— 对话历史,带有自动递增的ID
为何选择IndexedDB而非localStorage?有两个原因:
- 容量:IndexedDB 比 localStorage 更适合较大的结构化记录,但仍有浏览器规定的配额。存储可能失败或被清理,隐私模式的保留方式也可能不同。
- 结构化存储: IndexedDB原生支持二进制数据(如头像图片)和复杂对象,无需经过JSON序列化开销。
还有 /let-go。当前工具会先打开带短倒计时的确认框,确认之后才删除。它清除本工具在当前浏览器中的人格、对话、服务商设置及当前密钥,工具内不能撤销。原文件、下载的导出、同意与语言偏好,以及服务商侧记录不在此删除范围内。放下的情感含义属于个人,软件操作则有明确且有限的范围。
同意覆盖层
根据真人聊天记录创建模拟人格,是值得认真考虑的事。我在提炼开始前加入了5 秒确认倒计时,用户会看到:
你即将基于真实的聊天记录创建一个人工智能人格。该人格将模拟另一个人的沟通风格。请花一点时间思考,确认这是否是你真正想做的事。
倒计时无法跳过。你无法点击‘我理解’,直到计时器归零。这并非法律形式,而是一次真正的暂停。如果5秒感觉太长,也许你并不适合进行这项操作。
最难修复的bug并非技术问题
这个项目最难处理的 bug,不是 OCR 准确率、发送者阈值或人格漂移,而是凌晨 3 点坐在桌前的我。我拿自己的聊天记录测试,看着 AI 用她的语气写出她曾对我说过的话。那一刻,我只能合上电脑,出去绕着街区走一圈。
我制作了一个工具,我知道它可能有点不健康。我知道和你失去的人的AI版本对话,并不能等同于处理悲伤。我知道/let-go只是一个数字仪式,而不是真正的释怀。但有时候,你现在的处境和你真正需要到达的地方之间,距离太远,无法一步跨越。这个工具就是一座踏脚石。你踩上去,然后就把它丢在身后。
如果你使用这个工具,请在你准备好的时候使用/let-go。如果你永远都不准备,那就请和真人谈谈。
技术架构概览
| 组件 | 技术 | 原因 |
|---|---|---|
| OCR引擎 | Tesseract.js(WASM) | 客户端运行,支持中文和英文,返回文本边界框 |
| AI模型 | DeepSeek(用户API密钥) | 浏览器直接请求服务商;模型质量、费用和保留政策因服务商而异 |
| 本地存储 | IndexedDB | 本地结构化存储,受浏览器配额与清理机制限制 |
| 发件人检测 | 基于边界框位置 + AI验证 | 以位置为主,DeepSeek为辅,最终需用户确认 |
| 图像预处理 | Canvas 2D API | 2x 上采样、灰度化、对比度拉伸——全部在客户端完成 |
| 数据生命周期 | /let-go → 清空 IndexedDB | 确认后本地删除,工具内不能撤销;外部副本另行处理 |
如果我重新来过,我会怎么做
如果我从头开始重新构建这个项目,我会做出以下改变:
- 从边界框开始,而不是逐像素扫描。 我花了两天时间在头像检测上,后来才意识到 Tesseract 已经免费提供了位置数据。
- 更早加入确认步骤。 发送方确认(步骤 1.5)是后期添加的,但应该从一开始就有。仅靠算法检测总会存在边缘情况;让用户进行验证,既更可靠,也更尊重用户。
- 一开始就复用 Tesseract 工作线程。对每个区域单独调用
recognize(),是早期影响性能的设计失误,增加了用户等待时间。 - 首先设计 /let-go 功能。 我先构建了聊天功能,再实现删除机制。回顾起来,这个工具的短暂存在特性应该作为第一个实现的功能,而不是最后才实现的。
常见问题
OCR 发送方识别是如何工作的?
Tesseract.js 提供每行边界框。当前启发式规则把 x0 与图像中间点的 0.6、0.8 倍比较,等于整图宽度的 30%、40%;之后再参考中心 X 的 37.5%、55% 阈值,最后沿用上一发送者或标为未知。这些几何规则可能判断错误,继续前需检查发送者预览。
为什么不使用 AI 进行发送方识别,而采用基于位置的检测?
该工具实际上结合了两种方法。基于位置的检测通过 Tesseract 边界框提供初始标签,然后 DeepSeek 进行二次处理,用于 OCR 错误校正和发送方验证。基于位置的方法在结构良好的聊天截图中更快、更可靠,而 AI 处理阶段可以发现错误并处理边缘情况。
人格数据是如何存储的?
人格与聊天记录保存在当前浏览器的 IndexedDB。AI 请求将选定上下文发给所配置服务商,并适用其保留政策;普通网站统计与资源请求另行处理。/let-go 要求确认后才清除本工具的本地记录、设置及当前密钥,不会删除所有网站的 IndexedDB 或外部副本。
最大的技术挑战是什么?
发送方识别问题。早期版本使用基于像素的头像和气泡颜色检测,这种方法脆弱,对多种截图格式都失效。转而采用 Tesseract.js 的边界框位置检测,并增加用户确认步骤(步骤 1.5),显著提升了准确性,同时保持了用户对结果的控制权。
当你准备好了
AI 前任聊天——本地解析与存储,直接请求所选 AI 服务商,通过 /let-go 确认本地删除。