1. 为什么我要折腾聊天记录备份这件事
用了大半年豆包智能体,我陆陆续续攒了几十个对话窗口,有用来做知识库问答的,有专门跑文案润色的,还有几个是帮团队做客服话术训练的。直到上个月电脑主板挂了,重装系统之后打开豆包网页版,发现之前那些对话记录全没了——不是账号丢了,是本地缓存和部分会话上下文没同步上去。那一刻我才意识到,聊天数据备份这件事,不能指望平台自动帮你兜底。
后来我在几个技术群里问了一圈,发现遇到类似情况的人不少。有人是换了设备之后历史记录对不上,有人是想把某个智能体的优质回答整理成文档,还有人纯粹是想做批量导出方便做二次分析。但豆包官方目前并没有提供一个显眼的"一键导出全部对话"按钮,网页版只能一条条翻,客户端也没有批量操作入口。这就催生了一个很实际的需求:手动把聊天数据批量导出来,存成自己能掌控的格式。
这篇内容就是把我自己踩过的坑、试过的方案、以及最后跑通的一套流程完整记录下来。适合两类人看:一类是像我这样有几十上百个对话需要归档的普通用户,另一类是手里有多个智能体、想把对话数据拿去做训练语料或者分析的技术同学。整个过程不需要写复杂代码,但需要你对浏览器开发者工具和基本的文件操作有一点耐心。核心关键词就三个:豆包、智能体、批量导出,围绕它们把技术原理和操作步骤讲透。
2. 批量导出的底层逻辑:数据到底存在哪里
2.1 聊天记录的三种存在形态
在动手之前,得先搞清楚豆包的聊天数据到底以什么形式存在。我通过抓包和翻本地存储,梳理出来三种形态:
第一种是服务端会话记录。你每次发消息,请求会带着会话ID打到服务端,服务端把这一轮问答存进数据库。这部分数据你在网页版刷新之后还能看到,说明它是跟着账号走的。但问题是,服务端并没有开放一个"导出全部会话"的接口给普通用户,你只能通过会话列表逐个拉取。
第二种是浏览器本地缓存。网页版为了加快加载速度,会把最近打开的会话内容存在IndexedDB或者localStorage里。这部分数据的特点是读取快、结构清晰,但容量有限,而且清缓存就没了。我实测下来,网页版大概会缓存最近二三十个会话的完整消息体,更早的就需要重新请求。
第三种是客户端本地数据库。如果你用的是豆包电脑版客户端,它会在本地建一个SQLite数据库文件,把会话、消息、智能体配置都存进去。这个文件才是批量导出的"富矿",因为它是结构化的,一张表存会话,一张表存消息,字段清晰。但不同版本的客户端,数据库路径和表结构会有差异,需要你根据实际情况去定位。
提示:我下面讲的操作以网页版为主,因为网页版跨平台、不需要装客户端,通用性最强。客户端方案我会在第四节单独说,适合数据量特别大的情况。
2.2 为什么不能直接"复制粘贴"
有人可能会说,我直接打开每个对话,全选复制粘贴到Word里不就行了?我试过,十个对话以内还能忍,超过二十个就是纯体力活,而且复制出来的格式全乱,角色标识、时间戳、代码块全都混在一起。更麻烦的是,智能体的回答里经常有Markdown表格和代码块,粘贴到Word里直接变成一坨。
所以批量导出的核心诉求不是"能导出",而是"导出后还能用"。这就要求我们拿到的是结构化的数据,最好是JSON格式,每条消息带角色、时间、内容字段,这样后续无论是转成Markdown、导入知识库、还是做数据分析,都有得商量。
2.3 整体技术路线选型
我把可行的方案列了个表,对比一下各自的优劣:
| 方案 | 原理 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|---|
| 浏览器开发者工具抓接口 | 监听网络请求,拿到会话列表和消息接口 | 数据最全,结构清晰 | 需要手动翻页,接口可能变 | 几十到上百个会话 |
| 本地存储读取 | 从IndexedDB/localStorage提取缓存 | 速度快,无需网络 | 只有最近部分会话 | 快速备份近期对话 |
| 客户端数据库导出 | 直接读SQLite文件 | 一次性拿全量 | 需要定位文件,版本差异大 | 数据量特别大 |
| 页面自动化脚本 | 模拟点击逐条抓取 | 不依赖接口 | 慢,容易被限流 | 接口不可用时兜底 |
我最终采用的是接口抓取为主、本地存储为辅的组合方案。原因很简单:接口返回的数据最完整,包含会话标题、创建时间、消息列表;本地存储可以作为补充,把接口没覆盖到的近期缓存捞回来。两者一合并,基本能做到不丢数据。
3. 动手前的环境准备与关键参数
3.1 你需要准备的东西
在开始之前,确认手头有这几样:
- 一台能正常访问豆包网页版的电脑,Chrome或Edge浏览器都行,我用的Chrome 120以上版本。
- 登录你的豆包账号,确保能看到完整的会话列表。
- 一个用来存放导出文件的文件夹,建议单独建一个,比如
doubao_backup。 - 如果会话数量超过五十个,建议准备一个能处理JSON的工具,比如VS Code或者任何文本编辑器。
不需要装额外的软件,也不需要什么特殊权限。整个过程都在浏览器里完成。
3.2 关键参数:会话ID和分页机制
抓接口之前,得先理解两个关键参数。
会话ID(conversation_id):每个对话窗口都有一个唯一ID,通常是一串长字符串。你打开某个对话时,URL里或者请求参数里会带上它。批量导出的第一步,就是先把所有会话ID收集齐。
分页参数:会话列表接口一般不会一次性返回全部,而是分页的。常见的是offset和limit两个参数,limit控制每页返回多少条,offset控制从第几条开始。我实测下来,把limit设成50比较稳妥,太大容易超时,太小翻页次数多。
注意:不同时期豆包的接口参数名可能有变化,比如有的版本用
page和page_size。你在抓包的时候以实际看到的为准,不要死记我这里的参数名。
3.3 打开开发者工具的正确姿势
按F12打开开发者工具,切到Network面板,勾选Preserve log(保留日志),这样页面跳转时请求记录不会被清空。然后刷新一下豆包页面,你会看到一堆请求刷过去。
这时候在筛选框里输入conversation或者message,把无关的请求过滤掉。我一般会先点开一个对话,观察发出去哪些请求,找到返回消息列表的那个。通常它的Response是一个JSON,里面有data字段,里面套着messages数组。
找到目标接口之后,右键选择Copy as cURL,把它存到一个文本文件里。这一步很关键,因为后面我们要用这个请求模板去批量拉取不同会话的数据。
4. 完整实操流程:从抓包到落地成文件
4.1 第一步:收集全部会话ID
打开豆包的会话列表页面,在Network面板里找到返回会话列表的那个接口。它的Response大概长这样:
{ "data": { "conversations": [ {"id": "conv_abc123", "title": "文案润色助手", "created_at": 1700000000}, {"id": "conv_def456", "title": "知识库问答", "created_at": 1700000100} ], "has_more": true } }你要做的就是不断翻页,把每一页的conversations数组里的id字段提取出来,汇总成一个列表。手动翻页的话,改offset参数重新发请求就行。如果会话不多,翻个三五页就齐了。
我自己的做法是把这个接口的cURL复制出来,改一下offset,在终端里用curl循环跑,把结果存成一个个JSON文件。这样比在浏览器里点快得多。
# 示例:循环拉取会话列表,offset从0开始,每次加50 for offset in 0 50 100 150 200; do curl 'https://xxx/conversation/list?offset='$offset'&limit=50' \ -H 'cookie: 你的cookie' \ -o conv_list_$offset.json done跑完之后,用一段简单的Python把所有的id抽出来去重:
import json, glob ids = set() for f in glob.glob('conv_list_*.json'): data = json.load(open(f)) for conv in data['data']['conversations']: ids.add(conv['id']) print(len(ids)) with open('conv_ids.txt', 'w') as fp: fp.write('\n'.join(ids))这一步做完,你就有了一个纯文本的会话ID列表,一行一个。
4.2 第二步:批量拉取每个会话的消息
有了ID列表,接下来就是逐个拉消息。消息接口通常长这样:https://xxx/conversation/messages?conversation_id=xxx。同样用curl循环:
while read cid; do curl 'https://xxx/conversation/messages?conversation_id='$cid \ -H 'cookie: 你的cookie' \ -o messages_$cid.json sleep 1 done < conv_ids.txt这里我加了个sleep 1,目的是控制请求频率。实测下来,如果不加延迟,连续请求几十次之后会触发限流,返回429。加一秒延迟基本能稳住,一百个会话也就两分钟的事。
提示:cookie是有有效期的,一般几个小时到几天不等。如果你中途发现请求返回401或者跳登录页,重新登录一下,把新的cookie换上去就行。
4.3 第三步:数据清洗与格式统一
拉下来的原始JSON结构可能不太一致,有的消息内容在content字段,有的在text字段,角色标识有的用role: "user",有的用is_user: true。所以需要做一层清洗,统一成下面这种格式:
{ "conversation_id": "conv_abc123", "title": "文案润色助手", "created_at": "2024-01-01T10:00:00", "messages": [ {"role": "user", "content": "帮我把这段话润色一下", "time": "2024-01-01T10:00:05"}, {"role": "assistant", "content": "好的,以下是润色后的版本...", "time": "2024-01-01T10:00:08"} ] }清洗脚本用Python写,核心就是遍历每个messages_*.json,把字段映射到统一结构。我大概写了三十行代码就搞定了,关键是要处理一下空消息和异常字段,避免脚本中途崩掉。
import json, glob, os result = [] for f in glob.glob('messages_*.json'): try: raw = json.load(open(f, encoding='utf-8')) except Exception as e: print('skip', f, e) continue msgs = [] for m in raw.get('data', {}).get('messages', []): msgs.append({ 'role': 'user' if m.get('is_user') else 'assistant', 'content': m.get('content') or m.get('text') or '', 'time': m.get('created_at', '') }) result.append({ 'conversation_id': raw.get('data', {}).get('conversation_id', ''), 'title': raw.get('data', {}).get('title', ''), 'messages': msgs }) json.dump(result, open('doubao_backup.json', 'w', encoding='utf-8'), ensure_ascii=False, indent=2)跑完之后你会得到一个doubao_backup.json,里面是所有会话的完整消息。这个文件就是你的"数据资产",后面想怎么用都行。
4.4 第四步:转成可读格式
JSON虽然结构化好,但直接看不太方便。我一般会再写个小脚本,把它转成Markdown,每个会话一个文件,标题作为文件名,消息按角色分段。这样用任何Markdown阅读器都能看,也方便导入到笔记软件里。
import json, re data = json.load(open('doubao_backup.json', encoding='utf-8')) for conv in data: title = re.sub(r'[\\/:*?"<>|]', '_', conv['title'] or conv['conversation_id']) with open(f'output/{title}.md', 'w', encoding='utf-8') as fp: fp.write(f"# {conv['title']}\n\n") for m in conv['messages']: prefix = '**我**' if m['role'] == 'user' else '**豆包**' fp.write(f"{prefix}:{m['content']}\n\n")到这一步,整个批量导出流程就跑通了。从抓包到落地成Markdown,一百个会话大概花了我二十分钟,其中大部分时间是在等请求返回。
5. 常见问题与排查技巧实录
5.1 请求返回401或跳登录页
这是最常见的问题,九成是cookie过期了。解决办法很简单:重新登录豆包网页版,按F12打开开发者工具,在Network面板里随便找一个请求,把cookie请求头完整复制出来,替换掉脚本里的旧cookie。注意cookie里包含多个字段,要整段复制,不要只复制其中一部分。
如果换了新cookie还是401,检查一下请求头里有没有漏掉user-agent或者referer。有些接口会校验这两个字段,缺了就会被拒。
5.2 翻页翻到一半返回空列表
这种情况通常是offset设太大了,超出了实际会话总数。你可以在返回结果里看has_more字段,如果是false就说明到底了,不用再翻。另外有些版本的接口用cursor而不是offset,每次返回一个next_cursor,下次请求带上它就行。以实际抓到的为准。
5.3 消息内容出现乱码或截断
乱码一般是编码问题,确保你的脚本在读写文件时都指定了encoding='utf-8'。截断则可能是单条消息太长,接口做了长度限制。我遇到过一条智能体回答超过五千字被截成两段的情况,解决办法是把两段按时间顺序拼回去,或者直接保留原始JSON,不做截断处理。
5.4 请求频率过高被限流
前面提过,加sleep是最简单的办法。如果会话特别多,比如上千个,建议把sleep调到2到3秒,或者分批跑,跑一百个歇十分钟。别想着并发拉取,限流策略对并发更敏感,得不偿失。
5.5 客户端数据库找不到
如果你用的是电脑版客户端,数据库文件一般在安装目录下的data或者userdata文件夹里,文件名可能是chat.db或者message.sqlite。用SQLite工具打开之后,找conversation和message两张表。不同版本表名可能不一样,实在找不到就用关键词搜一下.db文件。找到之后导出成CSV,再转JSON,流程和网页版类似。
注意:操作客户端数据库之前先复制一份备份,别直接在原文件上改,万一搞坏了客户端就打不开了。
6. 几个让我少走弯路的实操心得
第一个心得是先小范围试跑。别一上来就把几百个会话全拉一遍,先拿三五个ID跑通整个流程,确认数据格式没问题、脚本没报错,再放量。我第一次就是贪快,结果脚本里一个字段名写错了,拉了两百个文件全是废的,白等半小时。
第二个心得是原始数据一定要留着。清洗后的JSON和Markdown是给人看的,但原始接口返回的JSON别删。万一后面发现某个字段清洗错了,还能从原始数据里重新提取。我专门建了个raw文件夹存原始文件,占不了多少空间。
第三个心得是定期备份比一次性导出更重要。我现在养成了习惯,每个月月初跑一次导出脚本,把新增的会话追加进去。这样即使某天账号出问题,最多也就丢一个月的记录。脚本可以做成增量式的,只拉created_at比上次备份时间新的会话,速度快很多。
第四个心得是注意智能体配置的备份。聊天记录只是数据的一部分,如果你自己搭建过智能体,它的提示词、知识库文件、参数配置同样重要。这些内容有的在会话里,有的在单独的配置接口里,导出的时候别漏了。我一般会把智能体配置单独存一个JSON,和聊天记录放一起。
最后说个细节:导出的Markdown文件如果会话标题有重复,会互相覆盖。我在脚本里加了个序号前缀,比如001_文案润色助手.md,这样就不会冲突了。小问题,但不注意的话会丢文件。
这套流程我跑了三个月,累计导出了四百多个会话,没出过什么大问题。核心就是理解数据在哪、怎么拿、怎么存这三件事。工具会变、接口会变,但思路是通用的。你要是也在用豆包智能体攒了一堆对话,不妨按这个路子试一次,跑通之后心里会踏实很多。