通过真实案例说明 Agent 缺乏长期记忆的问题,展示用向量数据库实现检索式记忆层可将查询延迟压至 60ms 并显著提升对话连贯性。
对工作记忆、长期记忆以及支撑 Agent 智能的向量存储的一次实践视角审视。
迪拜一家物流公司请我修复他们的客服 Agent。它没有幻觉问题,响应也不慢。投诉更微妙也更棘手:每次对话都从零开始。客户会详细解释上周二提出的配送政策问题,而 Agent 的回应仿佛它从未听说过这位客户。因为从技术上讲确实如此——在会话之间,这个 Agent 的记忆就像金鱼:上下文窗口在聊天关闭的瞬间就清空了。
客户的话让我琢磨了好几天:"它回答得很好,但不记得我们。"
这不是聊天机器人的问题。这是记忆问题。在接下来一个月里,我重建了这个 Agent 的记忆层,而真正带来改变的不是更大的模型或更长的提示词——是一个向量数据库。基于检索的长期记忆,将一个每个会话都要重新解释自己的系统,变成了一个能在 60 毫秒内记住客户订单历史、首选联系方式和历史工单的系统。
这篇文章是我学到的全部:Agent 记忆实际上是什么、为什么向量数据库成为默认存储、如何接入一个、以及那些让我花了大量调试时间的生产级错误。
我要说得精确一些,因为这个词在每篇博客和厂商宣讲中都被滥用了。当工程师说"Agent 记忆"时,他们通常指的是三件截然不同的事情,而把它们混在一起就是构建出既昂贵又不可靠的系统的原因。
工作记忆。当前上下文窗口中的所有内容:系统提示词、迄今为止的对话、当前任务状态,以及最近的工具输出。这是 Agent 的短期注意力。它的硬上限是模型的上下文长度,而且每塞入一个 token 成本就会增加。工作记忆是 Agent"思考"的地方,也是每个 Agent 无论你是否主动要求都具备的一种记忆。
长期记忆。Agent 知道的、不在当前窗口中的所有内容。客户的订单历史。完整的政策手册。他们提交过的每一个历史工单。这些无法放在提示词里因为太大了,所以它存在于外部,按需检索。这是一种跨会话改变 Agent 行为的记忆,也是本文要讨论的记忆类型。
情景记忆。这个 Agent 在过去运行中实际做了什么——它采取的行动、犯过的错误、产生的结果。在严肃的部署中,这是一个可以查询的日志,你用它来让未来的运行更聪明。听起来像论文,但它实际上就是一个带好查询的数据库。
一个对我帮助很大的心智模型:工作记忆是 CPU 缓存,长期记忆是磁盘,情景记忆是审计日志。它们服务不同目的,应该分别设计,而不是把所有东西塞进一个提示词。
这是大多数教程跳过的部分。向量数据库并不是为 LLM 发明的,理解这一点有助于理解为什么它们是记忆的正确工具。
向量搜索是信息检索和推荐领域数十年前的想法。问题:给定用户查询,找到相似的条目——相似的新闻文章、相似的商品、相似的文档。早期系统使用关键词匹配,但只要词汇发散就会失败("我的包裹迟了"不包含"配送延迟")。大约在 2017-2019 年,大规模服务证明,将内容嵌入高维向量并进行近似最近邻(ANN)搜索,比关键词匹配能恢复更多语义相似性。HNSW 和 IVF 等算法被构建出来让这件事变得更快——HNSW 在单台机器上以个位数毫秒延迟服务数百万向量,这就是它仍然是大多数向量存储默认索引类型的原因。
LLM 改变的是嵌入的成本。突然间,你可以通过一次 API 调用嵌入任何文本——不仅仅是精心管理的产品目录。"嵌入这个文档、存储向量、按相似性检索"从一个研究项目变成了一个标准库调用。这就是向量数据库从细分市场走向默认选择的全部原因:嵌入层被商品化了,而搜索层早已久经考验。
对 Agent 的启示:向量数据库给你的 Agent 提供了一种按语义(而非精确文本)查找相关记忆的方式。这正是说"我的包裹卡住了"的客户所需要的——一个记忆系统,知道他们两周前提交过关于海关延迟的投诉,即使两个短语没有任何词汇重叠。
向量数据库是一种专用存储,索引向量并返回查询向量的最近邻。对 Agent 记忆,你这样接入它:
embed(chunk) ──▶ vector_db.upsert(id, vector, metadata)
│
user question ──▶ embed(question) ──▶ vector_db.search(top_k) ──▶ context
有四个移动部件很重要,每一个都有生产级影响:
嵌入模型。将文本转换为向量的函数。OpenAI 的 text-embedding-3-small 提供高达 1,536 维(可配置降至 512),每百万 token 约花 $0.02。开源选项如 bge-small 或 all-MiniLM-L6-v2 提供 384 维,可以在自有硬件上免费运行。维度数量在质量和成本与索引大小之间做权衡;768 是一个理智的生产级默认值。
向量存储。我的短名单,附有诚实的权衡:
CREATE EXTENSION,你的向量就放在关系数据旁边。在 decent 实例上用 HNSW 索引做 top-k 搜索在百万向量规模下保持在 10ms 以下。这是我 90% 生产工作的默认选择。分块(Chunking)。长文档在嵌入前被切分。我从 500-800 字符的块开始,有 50-100 字符的重叠;确切大小取决于你的内容。我遵循的规则:块应该是一个想法的长度,因为检索返回的是块而非文档,你的 Agent 读取的是你返回给它的任何内容。
元数据。在向量旁边存储来源、时间戳和访问控制标签。你会不断基于这些做筛选——"只检索这个客户的工单"、"只检索仍在生效的政策"。没有元数据筛选你就无法构建每个用户私有的记忆,而每个用户隐私是一个产品需求,不是可选项。
让我把它说具体。以下是我会部署的最小记忆层,使用 pgvector 所以你保留现有的 Postgres。首先是 schema:
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE agent_memory (
id BIGSERIAL PRIMARY KEY,
content TEXT NOT NULL,
embedding VECTOR(1536),
user_id TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX ON agent_memory USING hnsw (embedding vector_cosine_ops);
然后是检索侧:
import psycopg
from openai import OpenAI
client = OpenAI() # any OpenAI-compatible endpoint
def embed(text: str) -> list[float]:
r = client.embeddings.create(
model="text-embedding-3-small",
input=text,
)
return r.data[0].embedding
def remember(user_id: str, content: str) -> None:
with psycopg.connect(DB_URL) as conn:
conn.execute(
"INSERT INTO agent_memory (content, embedding, user_id) "
"VALUES (%s, %s, %s)",
(content, embed(content), user_id),
)
def recall(user_id: str, query: str, top_k: int = 5) -> str:
vec = embed(query)
with psycopg.connect(DB_URL) as conn:
rows = conn.execute(
"""
SELECT content
FROM agent_memory
WHERE user_id = %s
ORDER BY embedding <=> %s::vector
LIMIT %s
""",
(user_id, vec, top_k),
).fetchall()
return "\n---\n".join(r[0] for r in rows)
在 Agent 循环内部,你在响应前检索,然后在事后写回:
def agent_turn(user_id: str, message: str) -> str:
context = recall(user_id, message) # long-term memory
system = (
"You are a support agent. Use the provided memory about this "
"customer's history. If it is empty, ask for details. Be concise."
)
resp = client.chat.completions.create(
model="your-model",
messages=[
{"role": "system", "content": system},
{"role": "user",
"content": f"CUSTOMER MEMORY:\n{context}\n\nQUERY: {message}"},
],
)
answer = resp.choices[0].message.content
remember(user_id, f"User asked: {message} | We answered: {answer}")
return answer
这就是全部技巧。嵌入、存储、检索、注入,然后在事后写回发生了什么。这位客户在这个功能上线后第一次回来时,Agent 已经认识他们了,因为 recall 在每次对话中都运行。
添加记忆修复了"它不记得我们",然后引入了一组新的失败模式。这些是我真正花了大量调试时间的,按痛苦程度排序:
过时的记忆比没有记忆更糟。如果政策变了而旧块仍在存储里,Agent 会以完全自信引用过时版本。修复:在元数据中存储版本或 expires_at,在查询时筛选。记忆需要一个生命周期,而不仅仅是一个插入日期。
分块做得差。我曾经为了"节省嵌入调用"把合同按 4,000 字符分块,结果 Agent 从半个条款中作答。检索质量始于块边界也终于块边界。保持块只有一个想法,并像测试模型一样测试你的块大小。
盲目的余弦相似度。向量搜索找到的是相似文本,而非正确文本。询问退款的客户会检索到所有曾经写过的退款政策。修复:混合搜索——将向量相似性与关键词(BM25)匹配结合起来——在最终进入提示词之前对前 20 条结果进行重排序步骤。
上下文溢出。Top-k 看起来无害,直到你每轮返回五个 800 字符的块,这会悄悄吃掉你的工作记忆和 token 预算。我检索 top 3-5,将每个块限制在约 800 字符,并在每个环境中测量每轮 token 数。
成本悄然增长。对每条消息和每条回复进行嵌入会累积:每天几百万 token 时,嵌入仍然便宜,但存储索引在增长,每次检索都会给你的延迟预算增加一次网络调用和一次嵌入调用。测量每轮检索成本;它应该保持在 LLM 调用本身之下。
隐私和保留。一旦记忆是每个用户持久化的,你就在存储个人数据。你需要作用域(按 user_id 筛选)、保留策略,以及能够应要求删除用户记忆的能力。监管者会来问。在他们来之前构建它。
无声的质量衰减。没有损失函数告诉你检索正在退化。你需要一个评估集——50-100 个真实查询配上你期望被检索的块——而且每次改变分块、嵌入模型或索引时都必须运行它。recall@k 是要追踪的数字,它衰减得比人们预期的快。
我有习惯告诉客户什么时候不要构建他们要求的东西,这篇文章同样需要这种诚实。以下是你不需要向量数据库的时候:
你的知识可以放在提示词里。一个 30 项的 FAQ、一组固定的公司政策、你只参考一次的手册——加载到系统提示词或小型查找表里。没有嵌入调用,没有索引,没有漂移。
你需要精确的、关系型的答案。"用户 X 上个月下了多少订单?"是 SQL 查询,而向量搜索会愉快地返回一个相似的错误答案。如果问题需要精确的连接和聚合,使用做连接和聚合的数据库。
新鲜度比语义更重要。如果答案必须反映最近五秒的数据,夜间重建的向量索引是错误的工具。从真相来源直接检索。
你的内容在措辞上没有变化。如果用户和文档总是使用相同的词汇,关键词搜索能以一小部分运营成本给你 95% 的价值。
我给客户的决策规则:当同一个问题以多种表述出现、答案语料库太大放不进提示词时,才使用向量数据库。否则,最简单可行的就是正确答案。
在你称一个 Agent"有记忆能力"之前,运行这个清单:
当我为那家物流客户上线记忆层时,差异不是理论上的。回头客不再重新解释自己了。Agent 调出客户的配送历史,记住他们的首选联系方式,并按名称引用历史工单。会话处理量下降,解决率上升,客户的提问从"它记得我们吗?"变成了"我们能让它记住更多吗?"
这才是你想处于的轨迹。你的 Agent 的智能被它能回忆的内容质量所限制,而不是背后模型的大小。给它一个快速、有作用域、对自己知道什么诚实记忆层——Agent 才能终于成为一个在昨天基础上构建而不是每天晚上都遗忘的系统。