2 核 4G、无 GPU、预算有限。从 LanceDB 踩坑到 pgvector 试探,最终回到 LanceDB 嵌入模式——少一个容器、省 ~300MB 内存、cp 目录即备份。不是绕了一圈,是找到了最适合当前规模的最简方案。
一、背景:为什么需要 Cognee
Hermes Agent 有一套内置的 memory 系统,但它有两个硬伤:
- 单会话隔离 — 每个 session 的记忆独立存储,无法跨会话引用
- 无结构化检索 — 只能做简单的文本匹配,不能做语义搜索和图遍历
对于需要跨天记住用户偏好、项目决策、技术约束的 Agent 来说,这远远不够。
Cognee 是一个开源知识图谱系统,提供了三个核心能力:
| 能力 | 说明 | Hermes 对标 |
|---|---|---|
| 语义搜索 | 向量相似度匹配 | cognee_search |
| 图遍历 | 实体关系图谱查询 | cognee_recall |
| 持久化存储 | PostgreSQL 元数据 + LanceDB 向量索引 | cognee_remember |
目标很明确:把 Cognee 部署到服务器,通过 Hermes 插件对接,让 Agent 拥有一套跨会话、可查询的外部记忆。
下面是这趟旅程的真实记录——没有虚构,每条配置都经服务器验证。
二、真实技术演进
阶段一:LanceDB(默认配置,初探与回归)
Cognee 的默认向量库是 LanceDB——一个嵌入式文件级向量数据库。它不跑独立进程,以 Python 库的形式随 Cognee API 进程运行。
首次尝试:
几乎零配置,装好 Cognee API 就能跑。cognee_remember 写入成功,cognify 知识图谱构建流水线也能触发,但 cognee_recall 返回空数组。
{
"results": [],
"pipelineRunStatus": "PipelineRunCompleted"
}
根因:LanceDB 的数据文件存放在容器内部路径,Docker 重启后目录被重置,向量索引丢失。数据在 PostgreSQL 的关系表里躺着,但向量层是空的——cognify 虽然 report 成功,向量化那一步静默失败了。
修复与认知转变:将 LanceDB 数据目录挂载到持久化 Docker volume。LanceDB 的文件存储结构让持久化极其简单——cp 整个目录就能完成备份和迁移。
这次踩坑暴露的不是 LanceDB 的缺陷,而是 Docker 文件系统生命周期与嵌入式数据库的磨合问题。LanceDB 嵌入模式的核心优势——零独立进程、零网络开销、文件级持久化——在这次排障之后反而变得更清晰了。它不是在「开发环境能用、生产环境不行」,而是「只要 volume 挂对,生产环境反而比独立向量库更省心」。
这一阶段的经验为后续回归 LanceDB 埋下了伏笔。
阶段二:pgvector 试探(有想法但未完全切换)
LanceDB 的持久化问题解决后,下一个探索方向是 pgvector——PostgreSQL 的向量扩展。一个数据库搞定关系和向量,听起来很完美。
配置了 pgvector 镜像,尝试让 Cognee 把向量和元数据都存到同一个 PostgreSQL 实例。
但实际使用时发现两个问题:
- Cognee 对 pgvector 的支持不够成熟 — 部分 API 路径对 pgvector 的向量维度处理有兼容性问题
- 专用向量库的检索性能更优 — ANN 近似最近邻搜索是向量数据库的核心优化点,pgvector 的 HNSW 索引在功能和性能上不及专用方案
最终 pgvector 容器留下来专门管关系元数据,向量存储继续用 LanceDB。这不是「pgvector 不行」,而是「关系库和向量库各司其职」的混合架构——最终选择 LanceDB 嵌入模式而非独立向量库,纯粹是因为当前数据规模不需要一个独立向量库进程。
阶段三:双 compose 配置冲突(差点全崩)
真正的危机不是选型,是配置管理。
在调试过程中积累了两套 docker-compose 配置——一套指向旧的镜像版本,一套指向新的。某次重启时两条 compose 同时生效,导致:
- 容器名冲突,部分服务反复 crash-restart
- Cognee API 间歇性不可用
- 日志里全是
port already allocated和container name conflict
修复:合并为单一 compose 文件,清理所有废弃的服务定义。统一后的服务栈只有三个容器:
| 服务 | 镜像 | 职责 |
|---|---|---|
| cognee-api | 自建 fastembed 镜像 | 知识图谱 API + LanceDB(嵌入) |
| postgres | postgres:16-alpine | 关系元数据 |
| cognee-ui | Cognee 前端 | 可视化管理 |
LanceDB 嵌入在 Cognee API 进程内,不占独立容器——这是相比独立向量库方案最直接的收益:省一个容器进程,省 ~300MB 内存。
阶段四:fastembed + LanceDB(首次跑通)
这是从零到一的突破。核心变化只有两个:
① 自建 fastembed 镜像
不要外部 Embedding API。从 Cognee 基础镜像出发,在 venv 中安装 fastembed:
FROM cognee/cognee:latest
RUN /usr/local/bin/pip install fastembed --target /opt/venv/lib/python3.11/site-packages/
关键细节:pip 路径是 /usr/local/bin/pip(不是 /usr/bin/pip),且必须 --target 到 venv 目录——否则 Cognee 运行时找不到 fastembed。
② 切换到本地 Embedding
# .env 关键配置
EMBEDDING_PROVIDER=fastembed
LLM_PROVIDER=deepseek
LLM_MODEL=deepseek-v4-pro
Embedding 走本地 fastembed,LLM 走 DeepSeek。两者职责彻底分离。
初始组件对照(bge-small 时代):
| 组件 | 实际用的是什么 | 关键特征 |
|---|---|---|
| Cognee 版本 | v1.1.2 | 稳定版 |
| Embedding | fastembed + BAAI/bge-small-en-v1.5 | 384 维,模型 130MB,本地推理 |
| LLM | deepseek-v4-pro(OpenAI 兼容) | 仅做图构建和查询理解 |
| 向量库 | LanceDB(嵌入模式) | 随 Cognee 进程运行,文件级持久化 |
| 关系库 | PostgreSQL 16 | 元数据存储 |
| 并行 | parallel=None | ONNX 底层已多线程,开并行反而慢 70% |
三段真实对比:
| 维度 | LanceDB 初探 | pgvector 试探 | fastembed + LanceDB |
|---|---|---|---|
| 向量持久化 | ❌ 未挂载 volume | ✅ 但兼容性问题 | ✅ volume 持久化 |
| Embedding 成本 | 外部 API 收费 | 外部 API 收费 | 零 API 费用 |
| 运维复杂度 | 低 | 中 | 低(零额外容器) |
| 全流程通过 | ❌ | 未完成切换 | ✅ |
向量库原理与选型
在进入模型升级之前,有必要理解 Cognee 的双层存储架构。这是整套系统最精巧的设计。
存储分层:
| 存储层 | 技术 | 存储内容 | 检索方式 |
|---|---|---|---|
| 语义层 | LanceDB(嵌入模式) | 文本块的向量指纹 + chunk 元数据 | ANN 近似最近邻 |
| 结构层 | PostgreSQL 16 | 原始文本、实体节点、关系边 | SQL + 图遍历 |
搜索流程:
用户查询 "Hermes 配置在哪里"
↓
fastembed 向量化 → [0.023, -0.147, ..., 0.091] (1024d)
↓
LanceDB ANN 搜索 → top-5 相似向量
↓
返回 chunk_id: [c42, c17, c88, c03, c55]
↓
PostgreSQL: SELECT content FROM chunks WHERE id IN (...)
↓
组装上下文 + 关联实体 → 返回给 LLM
LanceDB 只存向量指纹——不存原始文本。原始文本和知识图谱(实体-关系)都在 PostgreSQL 中。这种分离意味着:
- 向量检索走 LanceDB 的 ANN 索引(专为高维向量优化,毫秒级)
- 原文提取走 PostgreSQL 的 B-tree 索引(专为精确查询优化)
- 图遍历走 PostgreSQL 的递归 CTE(实体关系查询)
- 三者互不耦合,各用各的长处
为什么最终是 LanceDB 嵌入模式:
| 考量 | LanceDB 嵌入 | 独立向量库 |
|---|---|---|
| 额外容器 | 0 | +1(~300MB 内存) |
| 网络开销 | 0(进程内调用) | 每次查询一次 HTTP 往返 |
| 持久化 | cp 目录即备份 | 需要管理独立数据卷 |
| 数据规模 | 数十条记忆足够 | 百万级以上才显优势 |
| 运维 | 零额外运维 | 需监控独立服务健康 |
在当前约束下——2C8G 服务器、数十条记忆条目、单人使用——LanceDB 嵌入模式是最简选择。少一个容器不只是省 ~300MB 内存,还少了一个需要监控、重启、排障的独立服务。等数据量真的长到百万级那天,再切独立向量库也不迟——而且到那时,LanceDB 的文件格式让迁移成本极低(导出 Lance 文件即可)。
阶段五:Embedding 模型升级 — 从 bge-small 到 multilingual-e5-large
bge-small-en-v1.5 跑稳之后,下一个问题自然浮现:这个只有英文能力、384 维的小模型,够不够用?
实际场景中,Hermes 记忆里混杂着中英文、代码片段、配置文件路径,bge-small 对这些混合内容的语义理解明显吃力。再加上 384 维的向量精度天花板较低——1024 维的 multilingual-e5-large 成了有据可查的升级目标。
为什么选 e5-large
| 对比维度 | bge-small-en-v1.5 | multilingual-e5-large |
|---|---|---|
| 向量维度 | 384 | 1024(2.7× 精度空间) |
| 模型大小 | ~130MB | ~2.2GB(16× 存储开销) |
| 语言支持 | 仅英文 | 多语言(中/英/代码混合) |
| MTEB 平均分 | ~62 | ~66 |
维度提升不只是在数字上多几位——1024 维的向量在高维空间中类间距离更大,对相似和近似的区分度更好。
代价:内存翻倍 + 维度断裂
升级不是换个模型名字那么简单。e5-large 带来了两个直接冲击:
- 内存暴涨:cognify 时的内存峰值从 ~915MB 飙升至 ~2.1GB(实测 2.109GB),占 4G 总内存的 52%。bge-small 时代剩余 3GB+ 可分配给系统和 PostgreSQL,e5-large 时代只剩不到 2GB。
- 维度不兼容:384 → 1024 不是平滑迁移。LanceDB 的向量表在创建时就固化了维度,384 维的表无法接受 1024 维的向量写入。
迁移步骤
1. 停止 Cognee API(避免写入脏数据)
2. 删除 LanceDB 向量数据目录(维度不兼容,必须全量重建)
3. 修改 .env:
EMBEDDING_MODEL=intfloat/multilingual-e5-large
DIMENSIONS=1024
4. 重启 Cognee API
5. 重新执行 cognify(e5-large 首次加载 ONNX 模型,冷启动较慢)
6. 等待 PipelineRunCompleted
e5-large 首次启动时,fastembed 需要从 HuggingFace 下载 2.2GB 模型文件并转换为 ONNX 格式。缓存到本地后后续启动就快了。
此外,DeepSeek v4-pro 的 tool_choice 参数与 Cognee 内部的 structured output 格式存在冲突,需在 .env 中追加 LLM_INSTRUCTOR_MODE=json_mode 强制 JSON 输出模式。
性能实测
基于真实环境的三模型对比数据:
| 指标 | e5-large | bge-small-en-v1.5 | bge-small-zh-v1.5 |
|---|---|---|---|
| 模型加载 | 6.2s | 0.2s | - |
| 50条批量推理 | 3.2s | - | - |
| 内存峰值 | 2.1GB | 261MB | 826MB |
| 向量维度 | 1024d | 384d | 512d |
升级后验证
cognify 流水线完成后,用三条不同语言的查询做 recall 测试:
| 查询 | 语言 | 结果 |
|---|---|---|
| "Hermes Agent 的配置在哪里" | 中文 | ✅ 命中 |
| "how to set up the cognee plugin" | 英文 | ✅ 命中 |
| "COG_API_KEY vs COGNEE_API_KEY" | 混合 | ✅ 命中 |
3/3 全部命中,无漏召回。服务运行 7+ 小时无错误,容器持续健康。
升级后组件对照
| 组件 | 升级后实际配置 | 变化 |
|---|---|---|
| Embedding 模型 | intfloat/multilingual-e5-large | 从 bge-small-en-v1.5 切换 |
| 向量维度 | 1024 | 从 384 升级 |
| 内存峰值(cognify) | ~2.1GB | 从 915MB 上涨 2.3× |
| 模型文件大小 | ~2.2GB | 从 130MB 上涨 16× |
| 多语言支持 | 中/英/混合 | 从仅英文扩展 |
| LanceDB | 删除旧向量数据,重建 1024 维表 | 维度断裂,全量重做 |
三、资源受限下的优化
服务器只有 2 核 4G,无 GPU。每一兆内存都要计较。e5-large 的升级更是把资源压力推到了顶点。
3.1 fastembed:零成本 Embedding,模型任选
这是最重要的决策。之前所有方案——不管是 OpenAI text-embedding-3-small 还是 DeepSeek 的 Embedding API——都有调用费用。虽然单价低,但 cognify 流水线会批量向量化,用量上去成本就起来了。
fastembed 是 ONNX Runtime 绑定的本地推理引擎。无论是 130MB 的 bge-small 还是 2.2GB 的 e5-large,成本结构完全一样——零 API 费用,只有服务器的电和内存。
成本对比(按日均 1000 次向量化估算):
| 方案 | 日均成本 | 月成本 |
|---|---|---|
| OpenAI text-embedding-3-small | ~$0.02 | ~$0.60 |
| DeepSeek Embedding API | ~$0.01 | ~$0.30 |
| fastembed(bge-small 或 e5-large) | $0 | $0 |
「零外部依赖」的价值远不止省钱——没有 API 限流、没有网络抖动、不会因为欠费突然停服。e5-large 的 2.2GB 模型虽然吃内存,但选择权在自己手里:要么用更大的内存换更好的精度,要么退回 bge-small 省资源。不需要跟第三方讨价还价。
3.2 parallel 陷阱:开并行反而慢 70%
Cognee 的 cognify 支持 parallel 参数控制并发任务数。直觉上 2 核应该用 parallel=2。
实测结果(基于 bge-small):
| parallel 设置 | cognify 耗时 | 内存峰值 |
|---|---|---|
None(默认) | 基准 100% | 915MB |
parallel=2 | 170% | OOM 边缘 |
根因:fastembed 底层的 ONNX Runtime 已经做了多线程优化。外层再开 Python 多进程(parallel=2),不仅线程竞争 CPU,每个子进程还会独立加载一份 ONNX 模型。在 e5-large 的 2.2GB 模型体积下,parallel=2 意味着 4.4GB+ 的纯模型内存——直接超过 4G 物理上限。
升级到 e5-large 后,
parallel必须保持None。2.2GB 模型 × 2 进程 = OOM 是确定性结果。
教训:在资源受限环境下,默认值往往就是最优值。不要为了「看起来更合理」而调参数。
3.3 内存管理:bge-small 和 e5-large 两套策略
bge-small 时代,Cognee 容器内存占用约 915MB,剩余 3GB+ 分给 PostgreSQL 和系统。
e5-large 时代,这一数字翻到 ~2.1GB。4GB 总内存的 52% 被一个容器吃掉,容错空间急剧缩小。
bge-small 时期的内存管理已经打好基础:
- 清理废弃容器和未使用镜像(回收 ~1.8GB 磁盘)
- 为每个服务设置内存上限,防止单个容器无限制扩张
- 确保留有足够的系统余量
e5-large 的升级让这些限制变得不再是「锦上添花」,而是不设就必挂的底线。2.1GB 的 cognify 峰值意味着如果系统或 PostgreSQL 同时有内存需求,OOM Killer 随时可能介入。
LanceDB 嵌入模式在这里还有一个意外的好处:它不占独立内存配额。如果用的是独立向量库容器,还需要额外分配 ~300MB,那 4GB 的总内存根本不够分。
四、踩坑记录
坑1:Hermes 端 "Unknown tool" 之谜(最具技术深度)
现象:
Hermes 的三个 cognee 工具全部返回 Unknown tool:
cognee_recall → "Unknown tool"
cognee_remember → "Unknown tool"
cognee_search → "Unknown tool"
但 Cognee 服务端一切正常,API 健康检查通过,curl 直接调用也都返回正确结果。
排查过程:
# Cognee 插件的原始代码(问题所在)
def get_tool_schemas(self) -> List[Dict[str, Any]]:
if not self._ready: # ← 这里
return [] # ← 返回空列表
return [RECALL_SCHEMA, REMEMBER_SCHEMA, SEARCH_SCHEMA]
链式依赖链:
get_tool_schemas() 依赖 _ready
→ _ready 在 initialize() 中设置
→ initialize() 依赖 COG_API_KEY 环境变量
→ 如果 COG_API_KEY 为空 → _ready = False → get_tool_schemas() 返回 []
→ has_tool() 通过 tool_id 查找失败 → 所有调用返回 "Unknown tool"
根因:两个问题叠加——
COG_API_KEY未被正确设置到 manager profile 的.envget_tool_schemas()不应该因为配置缺失就返回空——应该始终返回 schema 列表,让上游决定可用性
修复:
# 修复后的代码
def get_tool_schemas(self) -> List[Dict[str, Any]]:
# 始终返回工具 schema,不依赖 _ready 状态
# 可用性由 is_available() 控制
return [RECALL_SCHEMA, REMEMBER_SCHEMA, SEARCH_SCHEMA]
同时补充了:
- 多副本同步 — 11 个 profile 各自有一份 Cognee 插件副本,必须全部更新
- 清 pycache — Python 缓存不刷新会导致旧代码继续生效
- COG_API_KEY 注入 — 在 manager
.env中添加密钥
教训:插件的 get_tool_schemas() 是注册入口,它如果返回空,下游完全感知不到这个工具的存在。工具造册和工具可用性应该是两个独立的概念——前者始终返回,后者通过 is_available() 判断。
坑2:COG_API_KEY vs COGNEE_API_KEY 命名不一致
| 位置 | 使用的变量名 | Header 名 |
|---|---|---|
| Cognee API | COGNEE_API_KEY | X-API-Key |
| Hermes 插件 | COG_API_KEY | X-API-Key |
| Cognee 官方文档 | COGNEE_API_KEY | Authorization: Bearer *** |
一个 API Key,三个名字。两种不同的认证方式(X-API-Key vs Bearer)。
最终方案:统一在 Hermes 端使用 COG_API_KEY,Cognee 服务端配置 AUTH_METHOD=api_key,Header 用 X-API-Key。
坑3:approvals.mode 死锁
修复完代码后重启 Hermes Gateway,发现新会话仍然报 Unknown tool。
根因是 Hermes 的 approvals.mode 默认处于 block 模式,Gateway 在实际执行工具调用时会因为审批拦截而中断。必须先执行:
hermes config set approvals.mode auto
然后重启 Gateway 才能生效。这个配置变更不会因为重载而自动应用。
坑4:SSH MaxStartups 限制
多 Agent 并行工作时会同时 SSH 到服务器。默认 SSH 配置在并发峰值下会拒绝连接。
表现为某几个 Worker 的 SSH 操作随机失败,错误信息不明。调高限制后恢复正常。
坑5:ONNX 缓存损坏导致重启循环(e5-large 升级时新增)
升级到 e5-large 后,某次重启 Cognee 容器时陷入循环——容器启动后立即 crash,Docker 自动重启,再 crash,再重启。
日志显示 fastembed 在加载模型时 ONNX Runtime 报了 session 初始化错误。排查发现是之前 ONNX 模型缓存文件在非正常关机时损坏,导致每次启动都尝试用损坏的缓存初始化。
修复:删除 ONNX 缓存目录(通常在 fastembed 的默认缓存路径下),让 fastembed 重建 ONNX 模型。2.2GB 的模型重新下载和转换需要几分钟,后续启动就正常了。
教训:大模型的 ONNX 缓存是持久化风险点。非正常关机后建议先清缓存再启动,而不是让容器陷入 crash-restart 循环。
五坑难度排序:
| 坑 | 难度 | 关键教训 |
|---|---|---|
| Unknown tool | ⭐⭐⭐⭐⭐ | 插件注册与可用性应该解耦 |
| ONNX 缓存损坏 | ⭐⭐⭐ | 大模型缓存 = 持久化风险点 |
| approvals.mode | ⭐⭐⭐ | 配置变更 ≠ 运行时生效 |
| API Key 命名 | ⭐⭐ | 文档先行,统一命名规范 |
| SSH MaxStartups | ⭐ | 多 Agent 并发 = 多连接 |
五、最终架构
┌─────────────────────────────────────────────┐
│ Hermes Agent │
│ ┌─────────────────────────────────────┐ │
│ │ CogneeMemoryProvider (Plugin) │ │
│ │ ┌─────────┬──────────┬──────────┐ │ │
│ │ │ recall │ remember │ search │ │ │
│ │ └────┬────┴────┬─────┴────┬─────┘ │ │
│ └───────┼─────────┼──────────┼────────┘ │
│ │ HTTP │ │ │
└──────────┼─────────┼──────────┼──────────────┘
│ │ │
┌──────┴─────────┴──────────┴──────┐
│ Cognee API v1.1.2 │
│ ┌─────────────────────────────┐ │
│ │ /api/v1/search │ │
│ │ /api/v1/recall │ │
│ │ /api/v1/remember/entry │ │
│ │ /api/v1/cognify │ │
│ └──────────┬──────────────────┘ │
│ │ │
│ ┌──────────┴──────────────────┐ │
│ │ PostgreSQL 16 │ │
│ │ • 关系元数据 + 原始文本 │ │
│ │ • 知识图谱(实体-关系) │ │
│ └─────────────────────────────┘ │
│ ┌──────────────────────────────┐ │
│ │ LanceDB(嵌入模式) │ │
│ │ • 向量索引(1024 维) │ │
│ │ • 进程内 ANN 检索 │ │
│ └──────────────────────────────┘ │
│ ↑ │
│ fastembed (本地 ONNX) │
│ multilingual-e5-large │
└─────────────────────────────────────┘
当前组件清单:
| 组件 | 技术 | 职责 |
|---|---|---|
| 知识图谱 | Cognee v1.1.2 | 结构化/非结构化混合存储 |
| 向量库 | LanceDB(嵌入模式) | 语义相似度检索(1024 维),无独立容器 |
| Embedding | fastembed + multilingual-e5-large | 文本→向量,本地推理,零 API 成本,多语言 |
| LLM | deepseek-v4-pro(OpenAI 兼容) | 图构建 + 查询理解 |
| 关系库 | PostgreSQL 16 | 实体/关系元数据 + 原始文本 |
| 插件 | Hermes CogneeMemoryProvider | 3 个工具注册,11 个 profile 全覆盖 |
六、总结
这次部署教会了我三件事——e5-large 的升级又强化了每条:
- 在资源受限的环境里,选型的第一原则是「少即是多」。LanceDB 嵌入模式比独立向量库少一个容器、省 ~300MB 内存、不需要额外监控。当数据量只有几十条时,嵌入式方案不只是在功能上「够用」——在运维成本上它是显著更优的。fastembed 同理:bge-small 和 e5-large 的成本曲线完全重合(都是零 API 费用),但内存成本天差地别——915MB vs 2.1GB。选择权在你手里:要精度就吃更多内存,要省内存就降模型。没有「既要又要」。
- 并行不是免费的。
parallel=2在 2 核机器上反而慢 70%。底层 ONNX 已经做了多线程优化,外层再加多进程只会争抢资源。e5-large 的 2.2GB 模型体积更是让 parallel > 1 直接等于 OOM。在约束条件下,「什么都不做」常常比「做点什么」更好。 - "Unknown tool" 的本质是接口设计问题。
get_tool_schemas()不应该因为配置缺失就返回空数组——工具注册和工具可用性必须解耦。这条原则适用于所有插件系统,不仅仅适用于 Hermes。ONNX 缓存损坏的教训则是另一面:大模型的持久化状态是隐性风险,不要假定缓存总是好的。
现在的 Cognee 服务稳定运行:fastembed + multilingual-e5-large (1024d) 本地向量化、多语言语义理解,LanceDB 嵌入做向量索引(零额外容器),DeepSeek 做推理,PostgreSQL 管元数据。三个容器,干净利落。在那台 2 核 4G 的小服务器上,每一步都是精确算计后的取舍。
下一步:写一个
cognee_sync.py定时任务,把 Hermes 的 built-in memory 批量同步到 Cognee。记忆孤岛问题,才算真正解决。