进阶:用 ChromaDB + 语义搜索给 Agent 装上真正的长期记忆
上次我们用 SQLite + 关键词匹配实现了"跨会话记忆"。
但"我的猫叫什么"和"宠物的名字是?"在关键词匹配下可能找不到交集。
这次我们引入向量数据库 和语义嵌入,让记忆检索真正理解含义。

为什么要从 SQLite 升级到 ChromaDB?
| 方案 | 检索方式 | 能理解语义? | 适用场景 |
|---|---|---|---|
| SQLite + LIKE | 关键词包含 | ❌ 只能字面匹配 | 小规模、确定性查询 |
| ChromaDB + 嵌入 | 向量相似度 | ✅ "宠物"与"猫"会被归为近义 | 模糊回忆、开放式对话 |
JS/TS 视角 :这就相当于你从 Array.filter() 升级到了 Pinecone 或 pgvector------数据存在专门的向量数据库里,通过计算向量距离来找"最相似"的记忆。
技术栈
- Ollama + Qwen3:0.6b:本地对话与事实提取(脑子)
- ChromaDB:向量数据库,负责记忆的存储与语义检索(海马体)
- sentence-transformers:将文本转换成向量,让 ChromaDB 能算相似度(嵌入引擎)
ChromaDB 自带
SentenceTransformerEmbeddingFunction,我们可以直接用,省去手动调sentence-transformers。
环境准备
bash
pip install ollama chromadb sentence-transformers
如果下载慢,可先设置镜像:
pip install chromadb -i https://pypi.tuna.tsinghua.edu.cn/simple
模型就绪(若未拉取):
bash
ollama pull qwen3:0.6b
第一步:初始化 ChromaDB 记忆库
ChromaDB 可以持久化到本地目录,数据不会丢。
python
# chroma_memory.py
import chromadb
from chromadb.utils import embedding_functions
# 使用轻量嵌入模型,所有计算在本地,无需联网
EMBEDDING_MODEL = "all-MiniLM-L6-v2"
embedding_fn = embedding_functions.SentenceTransformerEmbeddingFunction(
model_name=EMBEDDING_MODEL
)
class ChromaLongTermMemory:
def __init__(self, collection_name: str = "agent_memory", persist_dir: str = "./chroma_db"):
self.client = chromadb.PersistentClient(path=persist_dir)
# 获取或创建集合,指定嵌入函数
self.collection = self.client.get_or_create_collection(
name=collection_name,
embedding_function=embedding_fn,
metadata={"hnsw:space": "cosine"} # 使用余弦相似度
)
def add_memory(self, content: str, source: str = "user"):
"""添加一条记忆,自动生成嵌入并存储"""
# 用当前时间戳作为唯一 ID(简单起见)
import time
mem_id = str(int(time.time() * 1000000)) # 微秒级时间戳
self.collection.add(
documents=[content],
metadatas=[{"source": source}],
ids=[mem_id]
)
def search_memories(self, query: str, n_results: int = 5) -> list[str]:
"""语义检索:返回最相关的 n 条记忆"""
if self.collection.count() == 0:
return []
results = self.collection.query(
query_texts=[query],
n_results=min(n_results, self.collection.count())
)
# 提取文档列表
documents = results.get("documents", [[]])[0]
return documents
def clear_all(self):
# 删除集合并重建
self.client.delete_collection(self.collection.name)
self.collection = self.client.get_or_create_collection(
name=self.collection.name,
embedding_function=embedding_fn
)
JS/TS 类比:
typescript
// 如果用 Node.js 和 chromadb 包
import { ChromaClient } from "chromadb";
const client = new ChromaClient({ path: "./chroma_db" });
const collection = await client.getOrCreateCollection({
name: "agent_memory",
embeddingFunction: new SentenceTransformerEmbeddingFunction({ model: "all-MiniLM-L6-v2" })
});
// 添加文档时自动生成向量
await collection.add({ ids: [id], documents: [text] });
// 查询
const results = await collection.query({ queryTexts: [query], nResults: 5 });
第二步:事实提取器(与之前类似,但可以复用)
python
# fact_extractor.py
import ollama
MODEL = "qwen3:0.6b"
EXTRACTION_PROMPT = """从以下对话中提取值得长期记住的关键事实。
只输出事实,每条一行,不要解释。若无事实输出"无"。
对话:
{conversation}
提取的事实:"""
def extract_facts(conversation: str) -> list[str]:
response = ollama.generate(model=MODEL, prompt=EXTRACTION_PROMPT.format(conversation=conversation))
text = response["response"].strip()
if text == "无":
return []
return [line.strip() for line in text.split("\n") if line.strip()]
第三步:带语义记忆的聊天主程序
python
# semantic_chat.py
import ollama
from chroma_memory import ChromaLongTermMemory
from fact_extractor import extract_facts
MODEL = "qwen3:0.6b"
SYSTEM_PROMPT = """你是小记,一个有长期记忆的助手。
下面是你记得的关于用户的信息,请在回答时自然地体现出这些记忆。"""
# 初始化记忆库
memory = ChromaLongTermMemory()
# 短期上下文
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
print("🧠 语义长期记忆聊天机器人 (ChromaDB 版本)")
print("💡 告诉我你的爱好、宠物、名字,然后退出重启看看它记不记得!")
print("输入 /bye 退出,/clear 清空记忆")
while True:
user_input = input("\n🧑 你: ")
if user_input == "/bye":
break
if user_input == "/clear":
memory.clear_all()
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
print("✅ 记忆已清空")
continue
# 1. 语义检索相关长期记忆
retrieved = memory.search_memories(user_input, n_results=5)
memory_context = ""
if retrieved:
memory_context = "【你记得以下用户信息】:\n- " + "\n- ".join(retrieved)
print(f"🔍 检索到 {len(retrieved)} 条记忆")
# 2. 动态拼接系统提示
dynamic_system = SYSTEM_PROMPT + "\n\n" + memory_context if memory_context else SYSTEM_PROMPT
messages[0] = {"role": "system", "content": dynamic_system}
messages.append({"role": "user", "content": user_input})
# 3. 调用模型
response = ollama.chat(model=MODEL, messages=messages)
assistant_msg = response["message"]["content"]
messages.append({"role": "assistant", "content": assistant_msg})
print(f"🤖 小记: {assistant_msg}")
# 4. 提取新事实并存入 ChromaDB
recent_turn = f"用户: {user_input}\n助手: {assistant_msg}"
facts = extract_facts(recent_turn)
for fact in facts:
memory.add_memory(fact, source="extracted")
print(f"💾 新记忆: {fact}")
效果演示
第一次对话:
bash
🧑 你: 我叫小明,我喜欢打篮球和弹吉他
🤖 小记: 小明你好!篮球和吉他都很有趣呢。
💾 新记忆: 用户叫小明
💾 新记忆: 用户喜欢打篮球
💾 新记忆: 用户喜欢弹吉他
🧑 你: /bye
第二次启动后:
🧑 你: 我有哪些爱好?
🔍 检索到 3 条记忆
🤖 小记: 小明,我记得你喜欢打篮球和弹吉他。
注意:即使你的问题是"我有哪些爱好",它也能通过语义相似性找到"喜欢打篮球"、"喜欢弹吉他"------这就是向量检索的威力。
与 SQLite 方案的对比
| 查询内容 | SQLite (关键词) | ChromaDB (语义) |
|---|---|---|
| "我的猫叫什么" | 找到含"猫"的记忆 | 可能找到"宠物叫花花" |
| "我爱好什么" | 需要关键词"爱好" | 找到"喜欢打篮球" |
| "上次说的那个朋友" | 几乎不可能 | 有一定概率,取决于上下文 |
JS/TS 开发者的完全对应版
如果你用 TypeScript 实现同样的功能,会是这样的结构:
typescript
import { ChromaClient } from "chromadb";
import ollama from "ollama";
// 嵌入函数直接用 chromadb 默认的(或 Transformer.js 的)
const client = new ChromaClient({ path: "./chroma_db" });
const collection = await client.getOrCreateCollection({
name: "agent_memory",
embeddingFunction: "all-MiniLM-L6-v2" // 伪代码示意
});
// 添加记忆
await collection.add({
ids: [id],
documents: [content],
metadatas: [{ source: "extracted" }]
});
// 检索
const results = await collection.query({ queryTexts: [query], nResults: 5 });
const facts = results.documents[0];
核心逻辑完全一致:存储时把文档转成向量,查询时用查询向量找最近邻。Python 版和 TS 版只是 API 语法的区别。
进阶优化方向
- 混合检索:结合关键词过滤 + 语义检索,适合需要精确匹配的场景(如日期、金额)。
- 记忆更新:当用户说"我的猫不叫花花了,叫蛋蛋",可以删除旧记忆或覆盖更新。
- 记忆权重:根据时间衰减,越新的记忆相似度越高。
- 用户隔离 :给记忆加
user_id字段,支持多用户。
总结
- ChromaDB 是本地可用的向量数据库,对接
sentence-transformers后就能实现语义记忆。 - 相比 SQLite 方案,升级代价很小(几行代码),检索质量提升显著。
- 整个流程依然是:提取事实 → 生成向量 → 存库 → 查询时语义检索 → 注入上下文。
- 对 JS/TS 开发者来说,这就是把
localStorage换成向量数据库,前端有transformers.js,后端有chromadb,完全对应。