尧图精选

ChatArchive:AI聊天记录归档与全文检索系统实现

🕒 发布时间:2026/9/8 3:39:31 📁 来源:尧图网络
在实际开发场景里AI 聊天工具用得越深越容易遇到同一个问题对话记录散落在网页端、桌面端、命令行和不同模型的官方后台里过几天想找回一次 prompt 调优过程、一段故障排查结论或一套议题讨论往往只能靠记忆力翻找。ChatArchive 要解决的正是这个问题。它的定位不是聊天前端而是 AI 聊天记录的归档与再利用系统把分散的对话统一收拢到本地数据库提供结构化管理、全文检索、标签组织和 Markdown/JSON 导出能力让历史聊天从“一次性消耗品”变成可以检索、复用和沉淀的知识资产。本文会从零实现一个最小可运行的 ChatArchive 服务技术栈选用 Python FastAPI SQLite数据库使用 SQLite 内置的 FTS5 全文检索并在检索部分单独处理中文查询的问题。整体不引入消息队列、不依赖外部搜索引擎所有功能都可以在一台开发机上跑通。学完之后你会理解聊天归档类系统最核心的收、存、查、导四个环节是如何设计的也能知道在什么阶段需要引入向量检索、多用户和加密备份等生产级能力。1. 先想清楚 ChatArchive 要解决什么问题1.1 聊天记录和普通日志本质上不一样很多人会把聊天记录当成日志来管理这是归档系统设计里第一个需要纠正的认知。普通服务日志是单条、平铺、无对话语境的记录的主要目标是排障而 AI 聊天记录天然带有“回合”和“上下文”同一段对话里有 system、user、assistant 多种角色。一个问题的答案往往依赖前面几条消息的上下文。用户可能在同一场对话里反复调整 prompt形成一版 prompt 的演进过程。对话携带来源OpenAI、Claude、本地模型、命令行工具和模型标识。如果归档时只保存 message 内容而不保存角色、时间、会话归属、模型和元数据后续检索到的结果就失去了可追溯性。比如搜到一条 assistant 回复却不知道当时用的什么模型、哪一天、在哪个会话里回答的那这条记录只能当素材看不能当工程依据。因此在数据模型设计上ChatArchive 一开始就要区分会话表和消息表而不是把所有内容塞进一张宽表。1.2 在线聊天与离线归档的职责边界ChatArchive 不承担实时对话功能。它和在线聊天系统的边界可以这样划分在线聊天系统负责产生对话交互要快状态要实时。归档系统负责保存对话写入后基本不可变核心价值是稳定和可检索。归档系统需要兼容不同来源所以导入格式必须统一。归档系统要有导出能力避免厂商锁定和本地数据丢失。这里还要说清楚一个容易误解的地方项目标题里的“重铸 AI 聊天荣光”可以理解为让被丢弃的聊天记录重新产生价值。第一阶段不需要做模型调用先把历史数据管起来第二阶段才把归档内容当作知识库检索结果注入新 prompt实现基于历史对话的问答。这样分阶段落地项目难度和风险都可控。1.3 最小功能范围实现 ChatArchive 之前先圈定最小闭环要包含哪些能力。下面的表是第一阶段的功能范围模块功能是否必须说明导入接收 JSONL 格式的对话数据必须统一格式后兼容各来源存储会话表 消息表持久化必须保存角色、内容、时间、模型检索基于关键词的全文检索必须SQLite FTS5 实现导出Markdown 和 JSON 导出必须便于阅读和迁移标签为会话打标签建议辅助分类第一阶段可做基础版统计消息量、时间分布、模型分布扩展第二阶段再做语义检索向量化 相似度查询扩展需要额外依赖和模型文件多用户登录、权限隔离生产化本地单机可先不做按这个范围ChatArchive 的核心链路是导入一段对话 - 拆成会话和消息落库 - 建立全文索引 - 通过 API 检索 - 导出成可阅读或可迁移的格式。2. 技术选型与环境准备用最小依赖跑通第一版2.1 为什么选 Python FastAPI SQLite选型不是越重越好而是要和问题规模匹配。ChatArchive 第一版是本地单机工具数据量在几万到几十万条消息量级SQLite 完全够用而且能避免引入数据库服务器带来的运维成本。各组件的作用如下FastAPI提供 REST API自带 OpenAPI 文档配合 Pydantic 做请求体校验开发效率高。SQLite单文件数据库支持 WAL 模式自带 FTS5 全文检索扩展适合归档类轻量应用。标准库 sqlite3第一版直接用原生 SQL避免 ORM 引入过多抽象也让读者能看清 SQL 走向。uvicornFastAPI 的 ASGI 服务容器本地开发和测试都方便。如果后续数据量上来了可以把存储层替换为 PostgreSQL检索层替换为 Elasticsearch 或向量数据库但接口层和导入导出格式可以保持不变。2.2 开发环境清单建议按下面的版本准备环境组件版本建议用途Python3.10 及以上运行环境FastAPI0.110 及以上Web 框架uvicorn0.29 及以上ASGI 服务Pydantic2.x数据校验pytest8.x接口测试SQLite3.35 及以上数据库需支持 FTS5打开终端执行下面的命令创建项目和虚拟环境mkdir chatarchive cd chatarchive python -m venv .venv source .venv/bin/activate pip install fastapi0.110 uvicorn[standard]0.29 pydantic2 pip install pytest安装完之后可以检查一下 SQLite 版本和 FTS5 是否可用python -c import sqlite3; print(sqlite3.sqlite_version) python -c import sqlite3; connsqlite3.connect(:memory:); print(conn.execute(CREATE VIRTUAL TABLE t USING fts5(x); select 1 from t).fetchone())如果第二条命令报no such module: fts5说明当前 Python 编译时未启用 FTS5建议换用系统自带 Python 或重新安装支持 FTS5 的 Python 构建。这个问题在常见问题部分会再展开。2.3 项目目录结构第一版采用扁平结构文件少、依赖清晰方便学习和扩展chatarchive/ ├── app.py # FastAPI 应用与路由 ├── db.py # SQLite 连接、建库、事务封装 ├── models.py # Pydantic 请求与响应模型 ├── schema.sql # 建表 SQL ├── importer.py # JSONL 导入解析 ├── exporter.py # Markdown / JSON 导出 ├── search.py # FTS5 全文检索与 LIKE 回退 └── tests/ └── test_api.py # 接口测试这个结构里db.py是数据访问层search.py独立出来是因为中文全文检索有特殊处理单独成文件方便后续替换为向量检索实现。3. 数据库设计聊天归档的核心是会话与消息的时间线3.1 为什么必须拆成会话表和消息表聊天记录天然是“一对多”的结构一个会话包含多条消息消息之间有严格的先后顺序。如果不拆分直接把所有消息存在一张表里会出现几个问题无法高效查询“某个会话的全部消息”。会话级别的信息标题、来源、模型、标签每条消息都要重复造成冗余。删除或归档一个会话时需要手动删除多条记录容易残留脏数据。所以 schema 里用conversations保存会话元数据用messages保存消息明细通过外键关联。3.2 建表 SQL 与字段解释schema.sql内容如下PRAGMA journal_mode WAL; PRAGMA foreign_keys ON; PRAGMA busy_timeout 5000; CREATE TABLE IF NOT EXISTS conversations ( id INTEGER PRIMARY KEY AUTOINCREMENT, external_id TEXT UNIQUE, title TEXT NOT NULL DEFAULT , source TEXT NOT NULL DEFAULT unknown, model TEXT, tags TEXT NOT NULL DEFAULT [], created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, conversation_id INTEGER NOT NULL, role TEXT NOT NULL CHECK (role IN (system, user, assistant, tool)), content TEXT NOT NULL, created_at TEXT NOT NULL, metadata_json TEXT NOT NULL DEFAULT {}, FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS idx_messages_conv_time ON messages(conversation_id, created_at); CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5( content, tokenize trigram );字段设计里有几个关键决策需要理解external_id是来源系统里的原始会话 ID加 UNIQUE 约束是为了导入幂等。同一个会话重复导入时可以跳过或更新而不是产生重复数据。role用 CHECK 约束限定取值范围避免脏数据进入表。tags以 JSON 数组的字符串形式保存。第一版不做标签规范化只做辅助分类。created_at统一存 ISO 8601 字符串且建议统一存 UTC 时间展示时再转本地时区。混合时区是归档系统最常见的时间混乱来源。messages_fts使用 FTS5tokenize 选trigram。这个选择对中文检索很重要后面会专门解释。3.3 外键与 WAL 的细节建表使用PRAGMA foreign_keys ON启用外键约束否则ON DELETE CASCADE不会生效。每次建立连接后都要执行这个 PRAGMA而不是只在建库时执行一次因为外键约束是按连接级别生效的。WAL 模式的好处是读写并发能力更好读操作不会阻塞写操作适合“导入历史数据的同时还要支持检索”的场景。busy_timeout 5000设置 5 秒锁等待时间避免多进程写入时直接报database is locked。这段 SQL 里没有设置content参数messages_fts是独立的全文字段。这样实现最简单代价是消息更新或删除时全文索引里的数据也需要同步更新。第一版以追加写入为主几乎不更新消息所以这个取舍是划算的。4. 核心代码实现把收、存、查、导四个动作串起来4.1 数据库连接与事务封装db.py负责连接管理、建库和事务封装。为了控制篇幅这里用标准库sqlite3实现不引入 ORMimport sqlite3 from contextlib import contextmanager DB_PATH chatarchive.db def get_conn(): conn sqlite3.connect(DB_PATH, timeout5) conn.row_factory sqlite3.Row conn.execute(PRAGMA foreign_keys ON) conn.execute(PRAGMA journal_mode WAL) conn.execute(PRAGMA busy_timeout 5000) return conn contextmanager def transaction(): conn get_conn() try: yield conn conn.commit() except Exception: conn.rollback() raise finally: conn.close() def init_db(): import os if not os.path.exists(schema.sql): raise FileNotFoundError(缺少 schema.sql) with open(schema.sql, r, encodingutf-8) as f: schema f.read() with transaction() as conn: conn.executescript(schema)这里使用上下文管理器封装事务yield之后如果函数体没抛异常就 commit否则 rollback。所有写操作都通过这个上下文管理器执行能避免手写try/except/commit/rollback带来的遗漏。4.2 存储会话与消息的 Dao 层db.py里继续加入保存会话和消息的逻辑。保存一段完整对话时必须在一个事务里同时写入会话、消息和全文索引任何一步失败都不能留下半截数据def create_conversation_with_messages(conversation_data, messages): now conversation_data.get(created_at) if not now: now get_utc_now() updated_at conversation_data.get(updated_at) or now tags_json json.dumps(conversation_data.get(tags, []), ensure_asciiFalse) with transaction() as conn: cur conn.execute( INSERT INTO conversations (external_id, title, source, model, tags, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?) , ( conversation_data.get(external_id), conversation_data.get(title, ), conversation_data.get(source, unknown), conversation_data.get(model), tags_json, now, updated_at, ), ) conversation_id cur.lastrowid for msg in messages: message_created_at msg.get(created_at) or now cur conn.execute( INSERT INTO messages (conversation_id, role, content, created_at, metadata_json) VALUES (?, ?, ?, ?, ?) , ( conversation_id, msg[role], msg[content], message_created_at, json.dumps(msg.get(metadata, {}), ensure_asciiFalse), ), ) conn.execute( INSERT INTO messages_fts(rowid, content) VALUES (?, ?), (cur.lastrowid, msg[content]), ) return conversation_id这里要注意messages_fts插入的是rowid它必须对应messages.id否则检索联表时会错位。sqlite3的lastrowid在 insert 后可直接取到自增主键。4.3 Pydantic 请求模型models.py定义接口的请求结构让 FastAPI 自动完成参数校验from typing import Optional from pydantic import BaseModel class MessageIn(BaseModel): role: str content: str created_at: Optional[str] None metadata: dict {} class ConversationIn(BaseModel): title: Optional[str] external_id: Optional[str] None source: str unknown model: Optional[str] None tags: list [] messages: list[MessageIn]role这里先不做枚举校验数据库层的 CHECK 约束会兜底。如果请求里传入了非法 roleSQLite 会在这条消息写入时抛异常事务回滚整个会话都不会落库。这个设计是有意的导入接口应该坚持“一条消息非法整段对话不入库”而不是留下残缺数据。4.4 FastAPI 路由保存、列表、详情、删除app.py中实现最核心的几个接口from fastapi import FastAPI, HTTPException from fastapi.responses import PlainTextResponse from db import init_db, create_conversation_with_messages, get_conversation_list, get_conversation_detail, delete_conversation from models import ConversationIn app FastAPI() app.on_event(startup) def startup(): init_db() app.post(/api/conversations) def create_conversation(payload: ConversationIn): messages [m.model_dump() for m in payload.messages] if not messages: raise HTTPException(status_code400, detailmessages 不能为空) conversation_id create_conversation_with_messages(payload.model_dump(), messages) return {id: conversation_id, status: ok} app.get(/api/conversations) def list_conversations(page: int 1, page_size: int 20): return get_conversation_list(pagepage, page_sizepage_size) app.get(/api/conversations/{conversation_id}) def conversation_detail(conversation_id: int): detail get_conversation_detail(conversation_id) if not detail: raise HTTPException(status_code404, detail会话不存在) return detail app.delete(/api/conversations/{conversation_id}) def conversation_delete(conversation_id: int): ok delete_conversation(conversation_id) if not ok: raise HTTPException(status_code404, detail会话不存在) return {status: ok}get_conversation_list、get_conversation_detail和delete_conversation是db.py里的查询函数。delete_conversation删除会话时由于外键ON DELETE CASCADE已启用messages 和 messages_fts 的旧数据需要额外处理独立 FTS 表不受外键影响所以删除会话后必须手动删除对应 FTS 行或在应用层先查会话关联的所有 message id 再删除索引。这里建议在删除逻辑里补一步def delete_conversation(conversation_id): with transaction() as conn: rows conn.execute( SELECT id FROM messages WHERE conversation_id ?, (conversation_id,) ).fetchall() ids [r[id] for r in rows] if ids: placeholders ,.join(? * len(ids)) conn.execute(fDELETE FROM messages_fts WHERE rowid IN ({placeholders}), ids) conn.execute(DELETE FROM conversations WHERE id ?, (conversation_id,)) return True这里用 f-string 拼接占位符是因为IN子句需要可变数量的参数但参数值仍然由?占位不会引入注入风险。4.5 中文全文检索为什么 FTS5 要用 trigramSQLite FTS5 默认的unicode61分词器按空格和标点分词对英文很友好但对中文很不友好。中文句子没有空格unicode61会把整句话当成一个 token用户搜索“如何排查端口”时整体短语匹配不到前缀匹配也失效。trigram分词器会把文本切成连续的三字符片段。例如“如何排查端口占用”会被分成“如何排、何排查、排查端、查端口、端口占、口占用”等多个三字 ngram子串匹配能力显著增强。但 trigram 也有一个限制少于三个字符的查询词无法作为有效 token 匹配。因此在search.py里需要做长度判断和回退import sqlite3 def escape_phrase(term: str) - str: return term.replace(, ) def search_messages(db_path, q, limit20): conn sqlite3.connect(db_path) conn.row_factory sqlite3.Row try: if len(q) 3: rows conn.execute( SELECT m.id, m.conversation_id, m.role, m.content, m.created_at, c.title AS conv_title, c.model FROM messages m JOIN conversations c ON c.id m.conversation_id WHERE m.content LIKE ? ORDER BY m.created_at DESC LIMIT ? , (f%{q}%, limit), ).fetchall() else: phrase escape_phrase(q) rows conn.execute( SELECT m.id, m.conversation_id, m.role, m.content, m.created_at, c.title AS conv_title, c.model FROM messages_fts f JOIN messages m ON m.id f.rowid JOIN conversations c ON c.id m.conversation_id WHERE messages_fts MATCH ? ORDER BY rank LIMIT ? , (phrase, limit), ).fetchall() return [dict(r) for r in rows] finally: conn.close()两点解释MATCH查询里使用双引号包裹用户输入是把查询当作短语匹配用户输入中的双引号必须转义为两个双引号否则会破坏 FTS 查询语法。小于 3 个字符的回退用LIKE因为 trigram 无法匹配短词。比如用户搜“AI”trigram 索引无法命中直接走LIKE %AI%更可靠。检索结果保留了conversation_id、role、created_at这样前端展示时可以拼出“哪场对话里的谁在什么时候说了什么”。4.6 导入JSONL 统一格式importer.py处理导入文件。第一版定义统一的 JSONL 格式每行一个 JSON 对象{external_id: conv-001, title: 排查端口占用, source: cli, model: gpt-4o, created_at: 2025-01-01T10:00:00Z, tags: [linux, 网络], messages: [{role: user, content: 如何查看 8080 端口被谁占用, created_at: 2025-01-01T10:00:01Z}, {role: assistant, content: 使用 lsof -i :8080 或 ss -lptn sport :8080, created_at: 2025-01-01T10:00:05Z}]}导入函数按行读取、逐条校验并统计成功和失败行数import json def import_jsonl(path, create_func): success, failed 0, 0 with open(path, r, encodingutf-8) as f: for line_no, line in enumerate(f, 1): line line.strip() if not line: continue try: obj json.loads(line) messages obj.get(messages, []) if not messages: raise ValueError(messages 为空) create_func(obj, messages) success 1 except Exception as exc: failed 1 print(f第 {line_no} 行导入失败: {exc}) return {success: success, failed: failed}导入失败时不中断整个文件而是跳过失败行并记录行号方便使用者修复数据后重新导入。external_id的唯一约束在重复导入时会抛异常实测需要把它也统计为“跳过”而不是“失败”这里可以根据业务语义决定。4.7 导出Markdown 与 JSONexporter.py负责把会话导出成两种格式。JSON 导出适合迁移Markdown 导出适合阅读和沉淀。def to_markdown(detail): title detail[title] or 未命名对话 meta ( f 来源: {detail[source]} | 模型: {detail[model] or 未知} f | 时间: {detail[created_at]} ) lines [f# {title}, , meta, ] for msg in detail[messages]: role msg[role] content msg[content].strip() lines.append(f## {role}) lines.append() lines.append(content) lines.append() return \n.join(lines)导出接口返回PlainTextResponse并设置 UTF-8 编码app.get(/api/export/{conversation_id}) def export_conversation(conversation_id: int, format: str markdown): detail get_conversation_detail(conversation_id) if not detail: raise HTTPException(status_code404, detail会话不存在) if format markdown: return PlainTextResponse(to_markdown(detail), media_typetext/markdown; charsetutf-8) if format json: return detail raise HTTPException(status_code400, detailformat 仅支持 markdown 或 json)JSON 导出直接返回detail结构它包含会话元数据和消息数组。从 ChatGPT、Claude 等平台导出的官方数据需要写对应格式的转换器统一成上面的 JSONL 结构后再导入。5. 启动服务并用 curl 跑通整个闭环5.1 启动 FastAPI 服务在项目根目录执行uvicorn app:app --reload --host 0.0.0.0 --port 8000启动后访问http://127.0.0.1:8000/docs可以看到 FastAPI 自动生成的 Swagger 接口文档。开发阶段用--reload生产环境建议关掉自动重载并配合进程管理器。5.2 创建一段对话用 curl 新增一个会话curl -X POST http://127.0.0.1:8000/api/conversations \ -H Content-Type: application/json \ -d { external_id: conv-001, title: 如何排查端口占用, source: cli, model: gpt-4o, tags: [linux, 网络], messages: [ {role: user, content: 如何查看 8080 端口被谁占用}, {role: assistant, content: 可以使用 lsof -i :8080 或 ss -lptn sport :8080 查看进程} ] }响应应类似{id: 1, status: ok}这里 shell 嵌套引号比较繁琐实际开发建议用 Swagger 文档或 Python 脚本提交。r 上面的示例如果粘贴到终端报错可以把 JSON 保存到payload.json再用下面的命令提交curl -X POST http://127.0.0.1:8000/api/conversations \ -H Content-Type: application/json \ -d payload.json5.3 查询会话列表和详情curl -s http://127.0.0.1:8000/api/conversations | python -m json.tool curl -s http://127.0.0.1:8000/api/conversations/1 | python -m json.tool详情接口返回里messages数组应该按创建时间正序排列。如果发现顺序错乱检查导入时created_at字段是否完整以及查询函数里是否按created_at排序。5.4 检索并导出检索中文和多字词curl -s --get --data-urlencode q端口 http://127.0.0.1:8000/api/search | python -m json.tool导出 Markdowncurl -s http://127.0.0.1:8000/api/export/1?formatmarkdown预期输出类似# 如何排查端口占用 来源: cli | 模型: gpt-4o | 时间: 2025-01-01T10:00:00Z ## user 如何查看 8080 端口被谁占用 ## assistant 可以使用 lsof -i :8080 或 ss -lptn sport :8080 查看进程到这一步收、存、查、导四个环节已经完整跑通。5.5 自动化测试接口测试可以用 FastAPI 的TestClient完成避免每次手工启动服务。tests/test_api.py示例from fastapi.testclient import TestClient from app import app client TestClient(app) def test_create_conv(): resp client.post(/api/conversations, json{ title: 测试对话, model: test-model, messages: [ {role: user, content: 你好}, {role: assistant, content: 你好有什么可以帮你} ] }) assert resp.status_code 200 conv_id resp.json()[id] detail client.get(f/api/conversations/{conv_id}).json() assert len(detail[messages]) 2 search client.get(/api/search, params{q: 你好}).json() assert len(search[results]) 1运行测试pytest -q测试时建议使用独立测试数据库避免污染开发数据。可以在db.py里用环境变量控制数据库路径或者测试 fixture 里临时替换DB_PATH。6. 常见问题与排查链路6.1 检索不到数据或 MATCH 报错问题现象可能原因检查方式处理方案no such module: fts5当前 Python 的 SQLite 构建未包含 FTS5python -c import sqlite3; print(sqlite3.sqlite_version)并尝试上述建库测试换用系统 Python或安装带 FTS5 的构建中文搜索不到结果trigram 对短词无效或查询含特殊字符确认查询词长度检查是否少于 3 个字符少于 3 字符走 LIKE 回退超过 3 字符走 FTSMATCH 查询抛语法错误用户输入包含双引号等特殊字符打印实际传给MATCH的字符串对用户输入做双引号转义并整体加短语引号全文索引里有旧数据更新消息后未同步 FTS 表直接查询messages_fts计数对比建立触发器或在应用层同步删除、更新索引排查顺序建议先确认是否真的写入成功了再确认 FTS 表里有没有对应行最后确认查询词长度和转义逻辑。这三个点按顺序检查能覆盖大部分检索问题。6.2 导入后消息顺序混乱现象是详情接口返回的消息顺序和原始 JSONL 不一致。原因通常是created_at字段缺失或格式不统一比如有的行是 ISO 8601有的是时间戳有的没有。检查方式查看导入时是否打印了messages 为空或行解析异常。用 Python 读取源文件检查每行的created_at是否存在。查询数据库里该会话的created_at列对比可见值是否杂乱。处理建议是导入函数里做统一规范化缺省时间用当前 UTC 时间时间戳格式转成 ISO 8601解析失败时直接判定该行导入失败而不是默认取当前时间掩盖污染数据。6.3 并发写入时报 database is locked本地工具一般单用户使用但在导入大文件时如果同时有多个进程或线程写入SQLite 会报锁错误。排查顺序是否开启了 WAL 模式。是否设置了busy_timeout。是否存在长时间的写事务没有提交。是否有多个实例同时指向同一个数据库文件。处理方案是把busy_timeout设为 5000 毫秒写操作用单一事务批次提交避免逐条自动提交。生产环境如果并发写入量很大再考虑迁移 PostgreSQL。6.4 FTS 表删数据不同步这是一个隐蔽问题。由于第一版使用独立 FTS 表删除会话时如果只删conversations和messagesmessages_fts里仍然残留旧索引检索时 join 不到messages行导致结果丢失。处理方案在删除逻辑里补充手动删除 FTS 索引行。更好的长期方案是改用 FTS5 external content 表并创建触发器或者封装统一的删除函数所有删除都走这一个入口。6.5 导出文件中文乱码Windows 下保存text/markdown响应时可能会乱码。原因通常是响应头没有声明charsetutf-8。上面的导出接口已经在media_type里带了charsetutf-8保存时再用 Python 读取并写入文本文件即可curl -s http://127.0.0.1:8000/api/export/1?formatmarkdown -o conv1.md读取时显式指定 UTF-8with open(conv1.md, encodingutf-8) as f: content f.read()7. 生产化增强与最佳实践7.1 发布前检查清单本地跑通之后如果要接到真实工作流里建议按下面的清单逐项确认数据库备份归档系统的数据价值高发布前确认有没有定时备份脚本和恢复演练。导入幂等性重复导入同一份 JSONL 时external_id冲突如何处理要明确是跳过还是覆盖。索引同步消息更新、删除时FTS 索引是否同步处理。编码统一所有读写文件显式指定 UTF-8避免平台默认编码差异。时区统一入库时间全部转 UTC导出时再转本地时区。日志与监控FastAPI 进程是否有访问日志导入失败是否记录到独立日志文件。性能验证用一份数万条消息的真实数据压测检索接口确认响应时间在可接受范围。脱敏与安全聊天记录可能包含敏感信息本地数据库文件是否有加密或访问控制。反向依赖导出 JSON 是否能被原系统再次导入迁移链路要双向可用。7.2 检索增强从关键词到语义FTS5 只能解决字面检索。如果历史聊天里用户问“端口被占了”但新搜索词是“端口冲突”字面匹配会漏掉。第二阶段建议引入向量检索使用本地 embedding 模型把消息内容向量化。用 SQLite 的sqlite-vec扩展或独立向量数据库保存向量。查询时先做语义召回再做关键词过滤或重排。实现时要注意向量化和索引的更新策略新消息入库后异步生成向量不能阻塞导入主流程。7.3 把归档数据变成对话上下文ChatArchive 最有价值的生产用法是让历史聊天参与新一轮对话。流程可以设计为用户提问。从归档库检索相关历史消息。把命中结果按时间线拼成上下文片段。与用户问题一起发给大模型。模型基于历史记录回答当前问题。这个流程本质上是检索增强生成也就是 RAG。关键点在于检索结果不能只按相似度截断还要保留角色和会话归属避免把不同会话的碎片混在一块导致模型误解。7.4 扩展方向速查方向建议实现方式适用阶段Web 管理界面Vue/React 前端对接现有 API有交互需求后多格式导入编写 ChatGPT、Claude 官方导出转换器迁移旧数据标签与收藏会话表加收藏字段增加标签过滤数据量增长后统计报表按天、模型、来源聚合消息量做使用分析多用户权限增加用户表和会话归属字段团队共享部署数据库加密SQLCipher 或应用层对称加密敏感字段本地隐私要求高7.5 对新手最有价值的练习ChatArchive 是一个很适合练习后端工程的项目因为它涉及文件解析、数据建模、全文检索、接口设计、异常处理多个知识面。建议按顺序做四个练习把 ChatGPT 官方导出数据写成转换器统一成 ChatArchive 的 JSONL 格式。给会话列表接口增加按时间范围过滤和按标签过滤。为 search 接口增加分页和搜索结果高亮片段。把 FTS 表切换为 external content 模式用触发器自动同步索引。做完这四个练习基本就能理解一个归档类系统从数据入口到检索出口的完整链路。ChatArchive 的价值不在于代码复杂度而在于它把聊天记录从“用完即走的对话”变成了“可沉淀、可复用、可追溯的工程资产”。实际项目里接入的时候优先保证导入幂等、时间线完整和导出可用这三件事做好工具就能真正用起来。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →