第 8 章 存储后端与 LLM 路由 实验用 inmemory,上线用 Postgres。本章讲清三种存储后端的差异、pgvector 部署,以及 Chat / Embedding / Vision 分 Profile 路由的生产配置。 8.1 为什么要关心存储后端 需求 | inmemory | sqlite | postgres 数据重启后保留 | ❌ | ✅ | ✅ 多进程 / 多实例 | ❌ | 有限 | ✅ 大规模向量检索 | 暴力 | 暴力 | pgvector 运维复杂度 | 零 | 低 | 中 推荐场景 | 单元测试 | 个人项目 | 生产 memU 通过统一的 Repository 契约抽象四种仓库(Resource / Item / Category /
实验用 inmemory,上线用 Postgres。本章讲清三种存储后端的差异、pgvector 部署,以及 Chat / Embedding / Vision 分 Profile 路由的生产配置。
| 需求 | inmemory | sqlite | postgres |
|---|---|---|---|
| 数据重启后保留 | ❌ | ✅ | ✅ |
| 多进程 / 多实例 | ❌ | 有限 | ✅ |
| 大规模向量检索 | 暴力 | 暴力 | pgvector |
| 运维复杂度 | 零 | 低 | 中 |
| 推荐场景 | 单元测试 | 个人项目 | 生产 |
memU 通过统一的 Repository 契约抽象四种仓库(Resource / Item / Category / Relation),切换 provider 不改业务代码。
service = MemoryService( llm_profiles={"default": {"api_key": "..."}}, database_config={ "metadata_store": {"provider": "inmemory"}, }, )
特点:
service = MemoryService( llm_profiles={"default": {"api_key": "..."}}, database_config={ "metadata_store": { "provider": "sqlite", "path": "./memu_data.db", }, }, )
特点:
docker run -d --name memu-postgres \ -e POSTGRES_USER=postgres \ -e POSTGRES_PASSWORD=postgres \ -e POSTGRES_DB=memu \ -p 5432:5432 \ pgvector/pgvector:pg16
export POSTGRES_DSN=postgresql+psycopg://postgres:postgres@127.0.0.1:5432/memu
import os from memu import MemoryService service = MemoryService( llm_profiles={ "default": { "api_key": os.environ["OPENAI_API_KEY"], "chat_model": "gpt-4o-mini", }, "embedding": { "api_key": os.environ["OPENAI_API_KEY"], "embed_model": "text-embedding-3-small", }, }, database_config={ "metadata_store": { "provider": "postgres", "dsn": os.environ["POSTGRES_DSN"], }, }, retrieve_config={"method": "rag"}, )
ddl_mode="create" 时会尝试 CREATE EXTENSION IF NOT EXISTS vectorpip install "memu-py[postgres]"
MemoryService │ ▼ Database (protocol) ├── ResourceRepo ├── MemoryItemRepo ├── MemoryCategoryRepo └── CategoryItemRepo
每条记录可带 scope 列(user_id 等),由 UserConfig 注入。
Workflow 每个步骤可通过配置指定 Profile:
| 步骤能力 | 典型 Profile |
|---|---|
| 文本提取、摘要 | default |
| embedding | embedding |
| 图片描述 | vision |
| 音频转写 | transcribe |
| Markdown 合成 | synthesis |
步骤配置键名(概念性):
chat_llm_profileembed_llm_profilellm_profile(通用)未指定时回退到 default。
| backend | 说明 |
|---|---|
sdk |
官方 OpenAI SDK,稳定 |
httpx |
纯 HTTP,适配 OpenRouter、Doubao、Grok 等 |
OpenRouter 示例:
MemoryService( llm_profiles={ "default": { "provider": "openrouter", "client_backend": "httpx", "base_url": "https://openrouter.ai", "api_key": os.environ["OPENROUTER_API_KEY"], "chat_model": "anthropic/claude-3.5-sonnet", "embed_model": "openai/text-embedding-3-small", }, }, database_config={"metadata_store": {"provider": "inmemory"}}, )
llm_profiles={ "default": { "api_key": OPENAI_KEY, "chat_model": "gpt-4o", }, "embedding": { "api_key": OPENAI_KEY, "embed_model": "text-embedding-3-small", }, "vision": { "api_key": OPENAI_KEY, "chat_model": "gpt-4o", }, }
llm_profiles={ "default": { "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1", "api_key": QWEN_KEY, "chat_model": "qwen-max", "client_backend": "sdk", }, "embedding": { "base_url": "https://api.voyageai.com/v1", "api_key": VOYAGE_KEY, "embed_model": "voyage-3.5-lite", }, }
原则:Chat 与 Embedding 不必同一 Provider;Embedding 模型一经选定,尽量避免中途更换(向量空间不兼容)。
memU 内置两级拦截器(高级):
概念性用法:
def log_llm_call(event): print(f"LLM {event.profile}: tokens={event.usage}") # 注册方式依版本 API 而定,详见官方文档 Interceptor 章节
用于统计 Token、审计 Prompt、对接 Langfuse / Datadog。
| 瓶颈 | 现象 | 对策 |
|---|---|---|
| 暴力向量搜索 | Item > 5 万条变慢 | 上 Postgres + pgvector |
| LLM 提取 | memorize 慢 | 小模型提取 + 异步队列 |
| 并发写入 | 速率限制 | 队列 + 背压 |
| 大文档 | 超 context | 预切块(应用层) |
SQLite / inmemory 的向量搜索是 O(n) 扫描——官方 ADR 已说明这是可移植性与性能的权衡。
pg_dump -h 127.0.0.1 -U postgres memu > memu_backup.sql
await service.export_memory_files( user={"user_id": "u1"}, # output_dir 由 memory_files_config 指定 )
导出树与数据库独立——适合审计、Git 版本化记忆(第 10 章)。
memu-py[postgres]。「存储后端切换」示例实际跑通内存与 SQLite 两种后端的对比,核心是 SQLite 的跨进程持久化验证。下面是完整脚本:
"""存储后端持久化对比完整示例。 验证 inmemory(重启丢失)与 sqlite(落盘持久)的差异: 进程 A 用 sqlite 写入 → 释放实例 → 进程 B 重新打开同一文件,检索仍能命中。 依赖:pip install memu-py (需配置 LLM API Key) """ import asyncio import json import tempfile import os from memu import MemoryService CONVERSATION = [ {"role": "user", "content": "我喜欢用 sqlite 做本地实验,生产用 postgres。"}, {"role": "assistant", "content": "合理的分层。"}, ] def write_temp_json(obj): p = os.path.join(tempfile.gettempdir(), "storage_demo.json") with open(p, "w", encoding="utf-8") as f: json.dump(obj, f, ensure_ascii=False) return p def build_sqlite(db_path): """sqlite:数据落盘到单文件,重启进程后仍在。""" return MemoryService( llm_profiles={"default": {"api_key": "your_api_key", "chat_model": "gpt-4o-mini"}}, database_config={"metadata_store": {"provider": "sqlite", "path": db_path}}, retrieve_config={"method": "rag"}, ) async def main(): user_id = "storage_test" url = write_temp_json(CONVERSATION) db_path = os.path.join(tempfile.gettempdir(), "memu_sqlite_demo.db") # 第一次:写入 svc_a = build_sqlite(db_path) await svc_a.memorize(resource_url=url, modality="conversation", user={"user_id": user_id}) print("进程 A 写入完成,释放实例。") del svc_a # 第二次:重新打开同一文件,检索应能命中 svc_b = build_sqlite(db_path) ctx = await svc_b.retrieve( queries=[{"role": "user", "content": {"text": "存储偏好"}}], where={"user_id": user_id}, ) n = len(ctx.get("items", [])) print(f"进程 B 重新打开后召回 items: {n}") print("(sqlite 跨进程持久化验证通过:数据未随实例释放而丢失)") # Postgres 配置模板(需自备实例): print("\nPostgres 配置模板:") print(' database_config={"metadata_store": {') print(' "provider": "postgres",') print(' "dsn": "postgresql://memu:memu@localhost:5432/memu",') print(' "use_pgvector": True}})') print(" 启动:docker run -d -e POSTGRES_PASSWORD=memu -p 5432:5432 " "pgvector/pgvector:pg16") if __name__ == "__main__": asyncio.run(main())
💡 SQLite 那段是「持久化」最直观的验证——亲手看到「释放实例再重开,数据还在」,就理解了内存后端与落盘后端的本质区别。多 Provider 路由(chat 与 embedding 用不同厂商)的配置写法,参见第 4 章 llm_profiles 详解。
下一章:第 9 章 — 智能体集成(LangGraph、Cloud API)。
参见:第 4 章配置详解;附录 C 数据库相关报错。