如果你已经开发过 Agent,很可能遇到过这样的窘境:Agent 一开始表现得很聪明,但运行几轮之后就开始胡言乱语,再过一会儿甚至忘了自己的核心任务。
问题的根源往往只有一个:记忆系统的设计是错误的。

一个残酷现实:LLM 没有“记忆”,只有上下文
我们必须认清,大型语言模型(LLM)所谓的“记忆”,其本质是:
- 你单次请求(request)中喂给它的 token 序列。
- 模型不会主动记住上一次请求中的任何内容。
因此,许多 Agent 项目采取了一种看似直接的“记忆方案”:将所有历史对话记录,一股脑儿地塞进后续的提示词(Prompt)里。
结果可想而知:
- ❌ Token 消耗爆炸式增长。
- ❌ 调用成本完全失控。
- ❌ 上下文中的信息噪声越来越大。
- ❌ 模型因信息过载而抓不住重点,决策质量下降。
一个真正可用、可长期运行的 Agent,其记忆系统必须是分层设计的。
Agent 的三层记忆模型(核心认知框架)
下面这张图是你需要理解的“认知地图”,它描绘了一个健壮的 Agent 记忆体系:
┌────────────────────┐
│ Working Memory │ 当前上下文(短期)
│ (Short-Term) │
├────────────────────┤
│ Episodic Memory │ 事件 / 行为记录
│ (Long-Term Raw) │
├────────────────────┤
│ Semantic Memory │ 可检索的知识
│ (Vector / Index) │
└────────────────────┘
接下来,我们逐层拆解,并用 Rust 的方式来实现它们。
Working Memory:不是“越多越好”,而是“刚刚好”
问题:为什么上下文越长,Agent 反而越蠢?
因为 LLM 本身并不具备为你做“信息优先级排序”的能力。无限制地追加历史,只会让关键信息淹没在冗余对话中。
因此,Working Memory(工作记忆)的核心原则是:
只保留当前决策真正需要的信息。
Rust 实现:滑动窗口与信息过滤
一个基础的工作记忆结构可以这样定义:
pub struct WorkingMemory {
messages: Vec<ChatMessage>,
max_tokens: usize,
}
当向其中添加新消息时,你可以执行以下策略来保持其精炼:
- 丢弃过于陈旧的工具调用输出。
- 保留用户的初始目标(Goal)以及最近几轮的思考链(Reasoning)。
- 当消息积累到一定长度时,调用 LLM 先进行摘要,再保留摘要结果。
以下是“记忆压缩”的简化示例:
pub fn compact(&mut self) {
if self.messages.len() > 20 {
let summary = summarize_with_llm(&self.messages[..10]);
self.messages.drain(0..10);
self.messages.insert(0, summary);
}
}
📌 记忆的主动压缩与提炼,是 Agent 走向成熟的重要标志。
Episodic Memory:Agent 的“行为黑匣子”
这是许多教程中缺失,但在生产环境中必不可少的部分——事件记忆。
什么是 Episodic Memory?
简单来说,它是 Agent 每一次重要行为的不可变记录。例如:
{
"ts": "2026-01-04T01:21:00Z",
"step": 7,
"action": "tool_call",
"tool": "http_get",
"input": {"url": "..."},
"result": "timeout"
}
Rust 实现:仅追加(Append-only)的 JSONL 文件
为什么优先选择简单的文件格式而非数据库?这基于以下几点考量:
- 仅追加写入:确保即使在 Agent 崩溃时也不会丢失已记录的数据。
- 人类可读:便于开发者和运维人员直接查看、分析。
- 后期可处理:可以轻松进行离线回放、行为分析或用于微调。
实现代码可能如下:
pub struct EpisodicMemory {
writer: tokio::fs::File,
}
impl EpisodicMemory {
pub async fn record(&mut self, event: &Value) -> Result<()> {
use tokio::io::AsyncWriteExt;
self.writer.write_all(event.to_string().as_bytes()).await?;
self.writer.write_all(b"\n").await?;
Ok(())
}
}
一个没有事件记忆的 Agent,将几乎无法进行有效的调试和问题追溯。
真正拉开差距的:Semantic Memory(语义/向量记忆)
到了这一层,Agent 才真正开始“像人类一样学习”。它的核心作用可以用一句话概括:
当遇到与过去相似的问题或情境时,能够回忆起“之前发生了什么以及如何解决的”。
最小可用设计(从简开始)
我们不必一开始就引入复杂的向量数据库(如 Milvus, Pinecone)。一个可控、可嵌入的轻量级方案更能帮助理解本质。
数据结构
pub struct MemoryChunk {
pub id: Uuid,
pub embedding: Vec<f32>, // 向量嵌入
pub text: String, // 原始文本
}
存储方案(极简但有效)
- 使用 SQLite 存储元数据(如 ID、文本、时间戳)。
- 将向量嵌入(embeddings)作为 BLOB 存入 SQLite 或单独的文件。
- 检索时,将向量加载到内存中计算余弦相似度。这种方案在记忆条数不多时非常高效,也是理解 数据库 如何与 AI 结合的好起点。
Embedding 是“慢操作”,务必异步化
同步调用 Embedding 模型会严重阻塞 Agent 的主循环,这是一个常见的性能陷阱。
pub async fn embed(text: &str) -> Result<Vec<f32>> {
// 调用外部 Embedding API 或本地模型
}
核心原则:
- ❌ 禁止在 Agent 的主决策循环中同步调用 Embedding。
- ✅ 将需要记忆的内容发送到后台任务进行异步处理。
tokio::spawn(async move {
let emb = embed(&text).await?;
semantic_memory.insert(text, emb).await?;
});
检索策略:不是“最相似”,而是“最有用”
天真的做法是直接返回余弦相似度最高的几条记忆:
top_k_by_cosine_similarity(query_embedding)
但更有效的策略是设计一个综合评分函数,它可能考虑:
最终评分 = 语义相似度 × 时间衰减因子 × 历史成功率权重
score = similarity * freshness * success_rate
这提醒我们一个关键点:
Agent 的“记忆质量”(相关性、时效性、有效性),远比记忆的“数量”重要得多。
Semantic Memory 如何参与 Agent 推理?
语义记忆并非用作对话历史,而是在每轮 LLM 调用前,作为“参考资料”注入。基本流程如下:
1. 提取当前任务目标(Goal)或问题。
2. 将其转换为向量(Embedding)。
3. 从语义记忆中检索出最相关的 Top-K 条记录。
4. 将这些记录格式化后,拼接到 System Prompt 中。
示例 Prompt 片段:
Relevant past experiences:
- Previously, HTTP timeout was solved by lowering concurrency.
- Similar error occurred when RPC rate limit was exceeded.
📌 关键: 这些记忆是作为参考知识(Contextual Knowledge)提供,而不是原始对话记录,这能大幅提升 LLM 利用历史经验的能力。
记忆系统的四条工程铁律
- Working Memory 越精炼,Agent 的决策越聪明。
- Episodic Memory 是为了调试与审计,而不是为了直接参与推理。
- Semantic Memory 只应存储那些“值得被未来想起”的经验。
- 记忆系统本质上是策略设计,而不仅仅是数据结构的选择。
结语
当你为 Agent 系统完整地实现了这三层记忆模型,它便已经脱离了简单的 Demo 阶段。此时的 Agent 具备了:
- ✅ 可控的执行与并发能力
- ✅ 可预测、可管理的成本
- ✅ 可追溯、可调试的运行日志
- ✅ 能从经验中学习的潜力
- ✅ 可长期稳定运行的基础架构
这整套设计所体现的工程深度,已远超目前绝大多数开源 Agent 框架的简易实现。
如果你对构建此类可落地的 人工智能 应用感兴趣,欢迎在 云栈社区 交流更多实践细节。