共计 3254 个字符,预计需要花费 9 分钟才能阅读完成。
一、为什么需要聊天归档?
每次调用 ChatGPT API 生成的对话,默认只存在于内存中。如果遇到服务重启、程序崩溃或 API 限流,这些有价值的对话数据就会永久丢失。我曾因为服务器宕机丢失过两周的调试记录,不得不重新复现问题——这种经历相信很多开发者都遇到过。

更麻烦的是,当需要回溯历史对话时:
- 无法通过自然语言搜索历史记录
- 跨会话的关联分析几乎不可能
- 重要对话片段分散在各个终端
二、技术方案选型
2.1 临时存储方案
- JSON 文件存储
适合快速验证原型,但随着数据量增大会遇到: - 文件读写性能下降
- 并发写入冲突
-
全文检索困难
-
内存缓存
如 Redis 等方案,但存在: - 数据持久化风险
- 内存成本高
2.2 数据库归档方案
推荐使用 SQLite 作为起点,因为:
- 零配置,单文件管理
- 支持标准 SQL 语法
- 事务特性 (ACID) 保障数据安全
- 轻松扩展为 MySQL/PostgreSQL
三、核心实现步骤
3.1 获取对话记录
使用 OpenAI 官方库获取对话上下文:
import openai
# 建议将 API_KEY 放入环境变量
openai.api_key = os.getenv('OPENAI_API_KEY')
def get_chat_completion(messages):
try:
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=messages
)
return response.choices[0].message.content
except Exception as e:
print(f"API 调用失败: {str(e)}")
return None
3.2 SQLite 存储设计
创建带索引的对话表:
import sqlite3
from datetime import datetime
# 数据库初始化
conn = sqlite3.connect('chat_archive.db')
cursor = conn.cursor()
# 建表 SQL(包含时间戳和角色标识)create_table_sql = """
CREATE TABLE IF NOT EXISTS chat_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
role TEXT CHECK(role IN ('user', 'assistant')) NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
tokens INTEGER
);
"""
# 创建索引提高查询效率
index_sql = """
CREATE INDEX IF NOT EXISTS idx_session ON chat_logs(session_id);
CREATE INDEX IF NOT EXISTS idx_timestamp ON chat_logs(created_at);
"""
cursor.execute(create_table_sql)
cursor.execute(index_sql)
conn.commit()
3.3 实现归档功能
完整的数据入库示例:
def save_to_db(session_id, role, content, tokens=None):
try:
cursor.execute(
"""INSERT INTO chat_logs
(session_id, role, content, tokens)
VALUES (?, ?, ?, ?)""",
(session_id, role, content, tokens)
)
conn.commit()
except sqlite3.Error as e:
print(f"数据库写入失败: {str(e)}")
conn.rollback()
# 实际调用示例
messages = [{"role": "user", "content": "如何优化 Python 代码?"}]
response = get_chat_completion(messages)
if response:
save_to_db("session_123", "user", messages[0]['content'])
save_to_db("session_123", "assistant", response)
四、性能优化技巧
4.1 批量插入
使用 executemany 减少 IO 操作:
batch_data = [("session_123", "user", "问题 1", 10),
("session_123", "assistant", "回答 1", 20)
]
cursor.executemany("INSERT INTO chat_logs (session_id, role, content, tokens) VALUES (?, ?, ?, ?)",
batch_data
)
4.2 索引优化
除了基础索引外,可以考虑:
- 对常用搜索字段 (content) 添加 FTS5 全文搜索
- 对时间范围查询添加组合索引
CREATE VIRTUAL TABLE IF NOT EXISTS chat_search USING fts5(content, tokenize="porter");
五、安全防护措施
5.1 敏感信息过滤
在入库前清洗数据:
import re
def sanitize_input(text):
# 移除信用卡号等敏感信息
text = re.sub(r'\b(?:\d[ -]*?){13,16}\b', '[REDACTED]', text)
return text
5.2 数据加密
对敏感字段进行 AES 加密:
from cryptography.fernet import Fernet
# 密钥应妥善保管
key = Fernet.generate_key()
cipher_suite = Fernet(key)
def encrypt_content(text):
return cipher_suite.encrypt(text.encode())
def decrypt_content(cipher_text):
return cipher_suite.decrypt(cipher_text).decode()
六、常见问题处理
6.1 API 限流应对
- 实现指数退避重试机制
- 使用 tenacity 库简化重试逻辑
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10))
def safe_api_call(messages):
return get_chat_completion(messages)
6.2 连接泄漏预防
推荐使用上下文管理器:
with sqlite3.connect('chat_archive.db') as conn:
# 所有数据库操作在此代码块内
pass # 退出时自动关闭连接
七、扩展方向建议
7.1 多用户支持
升级为 PostgreSQL 并添加:
- 用户认证表
- 权限管理字段
- 会话隔离机制
7.2 可视化界面
使用 Streamlit 快速搭建查询页:
import streamlit as st
# 搜索历史对话
search_term = st.text_input("搜索对话内容")
if search_term:
results = cursor.execute(
"SELECT * FROM chat_logs WHERE content LIKE ?",
(f"%{search_term}%",)
).fetchall()
st.table(results)
实践心得
这套方案在我团队落地后,历史对话检索时间从原来的手动翻找平均 15 分钟,降低到 3 秒内精准定位。特别在以下场景体现价值:
- 复盘技术讨论时的完整上下文
- 追踪 AI 回答的质量变化
- 构建内部知识库素材
建议先从小规模实现开始,逐步迭代功能。完整代码已放在 GitHub 仓库(示例链接),欢迎交流优化建议。
正文完
