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

🔧 使用说明

  1. 首次运行

    • 创建 memory_metadata.db(SQLite)
    • 创建 long_term_memory/ 目录(含 index.faissindex.pkl
    • 加载默认两条记忆
  2. 后续运行

    • 自动从磁盘恢复所有历史记忆
    • 新增记忆会立即写入 SQLite + FAISS + 磁盘
  3. 查看底层数据(可选):

    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
  • 是否可读:❌ 不要用 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 读取,例如你的程序中的:
    vec = vectorstore.index.reconstruct(i)
    
    或用 Python 脚本加载:
    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 ✅ 检索核心

💡 三个文件的功能

  1. memory_metadata.db 是“记忆的日记本” —— 写下来的文字,清晰可查;
  2. index.faiss 是“记忆的神经连接” —— 不可见但决定“联想速度”;
  3. 两者必须同步:新增记忆时,既要写日记(SQLite),也要长神经(FAISS)

✅ 查阅数据的方法

操作 命令
查看所有记忆内容 sqlite3 memory_metadata.db "SELECT content FROM memories;"
查看记忆数量 sqlite3 memory_metadata.db "SELECT COUNT(*) FROM memories;"
不要做 cat index.faisscat index.pkl(会输出乱码,无意义)

🎯 记住:只有 memory_metadata.db 是给你“看”的;另外两个是给程序“用”的。

posted @ 2026-01-31 14:27  船山薪火  阅读(7)  评论(0)    收藏  举报
![image](https://img2024.cnblogs.com/blog/3174785/202601/3174785-20260125205854513-941832118.jpg)