28-day6-双存储架构:SQLite3 元数据 + FAISS 向量索引
双存储架构:SQLite3 元数据 + FAISS 向量索引
我们将实现一个 真正的长期记忆系统,具备以下特性:
| 功能 | 实现方式 |
|---|---|
| 记忆文本持久化 | 用 SQLite3 存储每条记忆的原始内容、时间戳等 |
| 向量持久化 | 用 FAISS 的 save_local() / load_local() 保存/加载向量索引 |
| 启动时自动恢复 | 若存在本地数据库,则加载;否则初始化 |
| 新增记忆同步保存 | 每次添加后,同时写入 SQLite 和 FAISS,并保存到磁盘 |
💡 注意:FAISS 本身不存文本,只存向量和 ID 映射。所以我们用 SQLite 存文本 + 元数据,FAISS 存向量,两者通过 文档 ID 关联。
✅ 完整程序:vector_memory_3.py
tee vector_memory_3.py << 'EOF'
# 在 vector_memory_2.py 基础上增加 SQLite3 + FAISS 持久化
# vector_memory_3_enhanced.py
# 在 vector_memory_3.py 基础上,融合程序2的交互优化:
# - 查询优化("我" → "用户",去除疑问词)
# - 更清晰的提示与反馈
# - 保留 SQLite + FAISS 持久化能力
import os
import sqlite3
import numpy as np
from datetime import datetime
from dotenv import load_dotenv
import dashscope
from dashscope import TextEmbedding
from langchain_community.vectorstores import FAISS
from langchain_core.embeddings import Embeddings
from typing import List
import warnings
warnings.filterwarnings("ignore", message="Relevance scores must be between 0 and 1")
load_dotenv()
dashscope.api_key = os.getenv("DASHSCOPE_API_KEY")
# === 配置 ===
DB_PATH = "long_term_memory" # FAISS 向量库保存路径(目录)
SQLITE_DB = "memory_metadata.db" # SQLite 元数据文件
COSINE_THRESHOLD = 0.2
DEBUG_MODE = True
class DashScopeEmbeddings(Embeddings):
def __init__(self, model: str = "text-embedding-v2"):
self.model = model
def embed_documents(self, texts: List[str]) -> List[List[float]]:
texts = [t.strip() for t in texts if t.strip()]
if not texts:
return []
response = TextEmbedding.call(model=self.model, input=texts)
if response.status_code != 200:
raise RuntimeError(f"Embedding failed: {response}")
return [item["embedding"] for item in response.output["embeddings"]]
def embed_query(self, text: str) -> List[float]:
return self.embed_documents([text])[0]
# === 新增:查询优化函数 ===
def optimize_query_for_search(query: str) -> str:
"""优化查询以提高记忆检索准确率"""
# 1. 统一人称:我 → 用户
query = query.replace("我", "用户")
# 2. 去掉常见疑问词和标点
remove_words = ["吗", "呢", "什么", "哪里", "?", "?", "。", ".", "谁", "为什么", "怎么", "是否"]
for word in remove_words:
query = query.replace(word, "")
return query.strip()
# === 初始化 SQLite 表 ===
def init_sqlite():
conn = sqlite3.connect(SQLITE_DB)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS memories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
content TEXT NOT NULL,
created_at TEXT NOT NULL
)
''')
conn.commit()
conn.close()
# === 从 SQLite 加载所有记忆文本 ===
def load_memories_from_sqlite():
if not os.path.exists(SQLITE_DB):
return []
conn = sqlite3.connect(SQLITE_DB)
cursor = conn.cursor()
cursor.execute("SELECT content FROM memories ORDER BY id")
rows = cursor.fetchall()
conn.close()
return [row[0] for row in rows]
# === 向 SQLite 添加新记忆 ===
def save_memory_to_sqlite(content: str):
conn = sqlite3.connect(SQLITE_DB)
cursor = conn.cursor()
cursor.execute(
"INSERT INTO memories (content, created_at) VALUES (?, ?)",
(content, datetime.now().isoformat())
)
conn.commit()
conn.close()
# === 主程序初始化 ===
init_sqlite()
embeddings = DashScopeEmbeddings()
# 尝试从磁盘加载 FAISS 向量库
if os.path.exists(DB_PATH):
print("📂 从磁盘加载长期记忆...")
try:
vectorstore = FAISS.load_local(
DB_PATH, embeddings, allow_dangerous_deserialization=True
)
except Exception as e:
print(f"⚠️ 加载 FAISS 失败,重新初始化: {e}")
memories = load_memories_from_sqlite()
if not memories:
memories = ["用户喜欢喝美式咖啡", "用户住在杭州"]
vectorstore = FAISS.from_texts(memories, embeddings)
vectorstore.save_local(DB_PATH)
else:
memories = load_memories_from_sqlite()
if not memories:
memories = ["用户喜欢喝美式咖啡", "用户住在杭州"]
print("🆕 初始化长期记忆库...")
vectorstore = FAISS.from_texts(memories, embeddings)
vectorstore.save_local(DB_PATH)
# === 启动信息 ===
total_mem = len(vectorstore.index_to_docstore_id)
print("🧠 长期记忆系统(带持久化 + 交互优化)")
print(f"📌 相似度阈值: {COSINE_THRESHOLD} | 调试模式: {'ON' if DEBUG_MODE else 'OFF'}")
print(f"💾 当前记忆数: {total_mem} 条(来自磁盘)")
print("\n✅ 输入示例:")
print(" - 我喜欢喝茶") # 保存记忆
print(" - 我平时喜欢做什么?") # 触发检索(自动优化为“用户平时喜欢做”)
print(" - show all") # 查看所有记忆及向量摘要
print(" - quit\n")
# === 主循环 ===
while True:
try:
user_input = input("🗨️ 你: ").strip()
except (KeyboardInterrupt, EOFError):
print("\n👋 再见!")
break
if not user_input:
continue
if user_input.lower() == "quit":
print("👋 再见!")
break
if user_input.lower() == "show all":
total = len(vectorstore.index_to_docstore_id)
print(f"\n📚 所有长期记忆(共 {total} 条):")
if total == 0:
print(" (暂无记忆)")
else:
for i in range(total):
doc_id = vectorstore.index_to_docstore_id[i]
doc = vectorstore.docstore.search(doc_id)
vec = vectorstore.index.reconstruct(i)
norm = float(np.linalg.norm(vec))
preview = ", ".join(f"{x:.3f}" for x in vec[:5])
print(f"\n[{i}] 文本: {doc.page_content}")
print(f" 向量维度: {vec.shape[0]}")
print(f" 前5维: [{preview}, ...]")
print(f" L2范数: {norm:.4f} {'✅' if abs(norm - 1.0) < 1e-3 else '⚠️'}")
print()
continue
# 判断是否为问题
is_question = (
user_input.endswith('?') or
user_input.endswith('?') or
any(w in user_input for w in ["什么", "吗", "呢", "为什么", "怎么", "谁", "是否", "why", "how", "what", "when"])
)
if is_question:
print(f"\n🔍 检索: 「{user_input}」")
optimized_query = optimize_query_for_search(user_input)
if DEBUG_MODE and optimized_query != user_input:
print(f"✨ 优化查询: 「{user_input}」 → 「{optimized_query}」")
results = vectorstore.similarity_search_with_relevance_scores(optimized_query, k=5)
relevant = [(doc, score) for doc, score in results if score >= COSINE_THRESHOLD]
if DEBUG_MODE:
print("📊 所有结果(余弦相似度):")
for doc, score in results:
print(f" [{score:.3f}] {doc.page_content}")
if relevant:
print("✅ 回忆起:")
for i, (doc, _) in enumerate(relevant, 1):
print(f" {i}. {doc.page_content}")
else:
print("🤔 没有足够相关的记忆(可以说得更明确些)")
print()
else:
if not user_input.startswith("我"):
print("💡 提示:请以「我...」开头,例如「我喜欢音乐」\n")
continue
memory = "用户" + user_input[1:]
if len(memory) < 4:
print("⚠️ 内容太短,请说完整句子\n")
continue
# 同时保存到 SQLite 和 FAISS,并持久化
save_memory_to_sqlite(memory)
vectorstore.add_texts([memory])
vectorstore.save_local(DB_PATH) # 立即保存向量库
print(f"💾 长期记住: 「{memory}」(已写入磁盘)\n")
EOF
🔧 使用说明
-
首次运行:
- 创建
memory_metadata.db(SQLite) - 创建
long_term_memory/目录(含index.faiss和index.pkl) - 加载默认两条记忆
- 创建
-
后续运行:
- 自动从磁盘恢复所有历史记忆
- 新增记忆会立即写入 SQLite + FAISS + 磁盘
-
查看底层数据(可选):
sqlite3 memory_metadata.db "SELECT * FROM memories;" ls long_term_memory/ # 应包含 index.faiss, index.pkl
✅ 程序特点
- 清晰分离关注点:
- SQLite:存人类可读的文本、时间、ID
- FAISS:存高维向量,支持快速语义检索
- 真正“长期”:关机、重启、换终端都不丢记忆
- 安全可靠:每次添加都落盘,避免丢失
- 可审计:用
sqlite3命令行即可查看所有记忆
现在,你的 AI 助手终于拥有了可持久、可追溯、可解释的长期记忆!如需进一步扩展(如按时间过滤、删除记忆、导出 JSON 等),也可以在此基础上继续迭代。
数据文件
📁 1. memory_metadata.db —— 人类可读的记忆文本库(SQLite3)✅
- 是什么:标准的 SQLite3 数据库文件。
- 存什么:你所有记忆的原始文本、时间戳等元数据。
- 是否可读:✅ 可以查看!但不能用
cat直接看(因为是二进制格式,但结构化)。 - 如何查看:
# 查看所有记忆(推荐)
sqlite3 memory_metadata.db "SELECT * FROM memories;"
# 或进入交互式 shell
sqlite3 memory_metadata.db
> .mode column
> .headers on
> SELECT * FROM memories;
> .quit
✅ 输出示例:
id content created_at
---------- -------------------- -------------------
1 用户喜欢喝美式咖啡 2026-01-31T14:20:00
2 用户住在杭州 2026-01-31T14:20:00
3 用户喜欢爬山 2026-01-31T14:25:12
💡 这是你真正能读懂的记忆内容,也是系统重启时重建向量库的依据。
📁 2. long_term_memory/index.pkl —— 文档元数据映射(Python Pickle)⚠️
- 是什么:Python 的
pickle序列化文件(由 LangChain 生成)。 - 存什么:
- 文档 ID 到
Document对象的映射(如"0" → Document(page_content="用户喜欢...")) - 向量索引(FAISS)到文档 ID 的映射(
index_to_docstore_id)
- 文档 ID 到
- 是否可读:❌ 不要用
cat!它是二进制,乱码且可能含不安全代码。 - 如何查看(仅调试用):
import pickle
with open("long_term_memory/index.pkl", "rb") as f:
data = pickle.load(f)
print(data.keys()) # 通常有 'docstore', 'index_to_docstore_id'
print(data['docstore']._dict) # 所有文本内容(与 SQLite 重复)
⚠️ 注意:
index.pkl中其实也存了文本内容,但我们故意用 SQLite 替代它作为权威存储,更安全、可审计。
📁 3. long_term_memory/index.faiss —— 高维向量索引(FAISS 二进制)❌
- 是什么:Facebook FAISS 库的原生索引文件。
- 存什么:所有记忆对应的 1536 维向量(浮点数),以高效检索结构(如 FlatIP)存储。
- 是否可读:❌ 绝对不要用
cat!纯二进制,全是乱码。 - 如何查看:只能通过 FAISS API 读取,例如你的程序中的:
或用 Python 脚本加载:vec = vectorstore.index.reconstruct(i)import faiss index = faiss.read_index("long_term_memory/index.faiss") print(index.ntotal) # 向量总数 vec = index.reconstruct(0) # 第0个向量
🔬 它是“AI 的潜意识”——高效但不可直接阅读。
🧪 对比表
| 文件 | 类型 | 可读性 | 查看方式 | 是否重要 |
|---|---|---|---|---|
memory_metadata.db |
SQLite3 数据库 | ✅ 人类可读 | sqlite3 命令 |
✅ 权威记忆源 |
index.pkl |
Python Pickle | ❌ 二进制(含文本副本) | Python pickle.load() |
⚠️ 辅助,可重建 |
index.faiss |
FAISS 向量索引 | ❌ 二进制(纯向量) | FAISS API | ✅ 检索核心 |
💡 三个文件的功能
memory_metadata.db是“记忆的日记本” —— 写下来的文字,清晰可查;index.faiss是“记忆的神经连接” —— 不可见但决定“联想速度”;- 两者必须同步:新增记忆时,既要写日记(SQLite),也要长神经(FAISS)。
✅ 查阅数据的方法
| 操作 | 命令 |
|---|---|
| 查看所有记忆内容 | sqlite3 memory_metadata.db "SELECT content FROM memories;" |
| 查看记忆数量 | sqlite3 memory_metadata.db "SELECT COUNT(*) FROM memories;" |
| 不要做 | cat index.faiss 或 cat index.pkl(会输出乱码,无意义) |
🎯 记住:只有
memory_metadata.db是给你“看”的;另外两个是给程序“用”的。
浙公网安备 33010602011771号