Rust 内存管理实战:为 AI Agent 构建带历史感知的缓存系统

Rust 内存管理实战:为 AI Agent 构建带历史感知的缓存系统
在构建高性能 AI Agent 时,我遇到的最大痛点不是模型推理速度,而是上下文历史的维护与检索。传统的 LRU 缓存只关心“最近用没用到”,但 Agent 需要的是“这句话是谁说的、属于哪个会话、距今多久了”。为了解决这个问题,我设计并实现了一个基于 Rust 的带历史感知的缓存系统。它能让 Agent 在长对话中保持连贯性,同时避免内存无限增长。
为什么标准 Cache 不够用?
一开始,我尝试用简单的 dashmap 或 lru crate 来做缓存。但在压测中发现,当并发请求达到每秒 500 QPS 时,内存占用开始失控。问题出在哪里?我仔细排查了日志,发现两个关键缺陷:

- 缺乏会话隔离:不同用户的历史消息混在一个缓存里,查询时无法区分。
- 无法自动淘汰过期上下文:简单的 LRU 只淘汰最久未用的键,但可能淘汰掉正在活跃会话中的关键信息。
我意识到,必须引入会话ID(Session ID)和时间戳感知机制。于是,我开始重构缓存层,采用 Arc<RwLock> 来实现线程安全的共享状态,并结合 tokio 做异步处理。
核心设计:会话感知缓存结构
我的设计方案是这样的:每个会话对应一个独立的缓存桶,桶内存储该会话的历史消息。为了支持快速检索和自动清理,我引入了两层结构:
- 外层:一个
HashMap<SessionId, CacheBucket>,用Arc包裹,确保多线程共享。 - 内层:每个
CacheBucket是一个VecDeque,按时间顺序存储消息,同时记录最后访问时间。
下面是我定义的核心数据结构:
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use tokio::sync::RwLock as AsyncRwLock;
use chrono::{DateTime, Utc};
#[derive(Debug, Clone)]
pub struct MessageEntry {
pub content: String,
pub timestamp: DateTime<Utc>,
pub sender: String, // user or assistant
}
#[derive(Debug, Clone)]
pub struct CacheBucket {
pub messages: VecDeque<MessageEntry>,
pub last_accessed: DateTime<Utc>,
pub max_history_len: usize,
}
impl CacheBucket {
pub fn new(max_len: usize) -> Self {
CacheBucket {
messages: VecDeque::new(),
last_accessed: Utc::now(),
max_history_len: max_len,
}
}
pub fn add_message(&mut self, msg: MessageEntry) {
self.messages.push_back(msg);
self.last_accessed = Utc::now();
// 限制历史长度,防止内存溢出
if self.messages.len() > self.max_history_len {
self.messages.pop_front();
}
}
pub fn is_expired(&self, ttl_hours: u64) -> bool {
Utc::now() - self.last_accessed > chrono::Duration::hours(ttl_hours)
}
}
pub struct SessionCache {
buckets: AsyncRwLock<HashMap<String, CacheBucket>>,
default_ttl_hours: u64,
}
这个设计的关键点在于:
- 用
VecDeque而非Vec:因为我们需要高效地从头部删除旧消息(O(1)),而不是从中间删除。 AsyncRwLock:在tokio运行时中使用异步锁,避免阻塞其他协程。is_expired:每个桶都记录最后访问时间,便于后续定期清理。
实际踩坑:RwLock 的读写竞争
在实现过程中,我遇到了一个典型的并发问题:写入时加写锁,读取时加读锁。但在高频场景下,如果读操作过多,写操作可能会被饥饿(starvation)。
我的解决方案是:
- 分离热点数据:将“会话列表”和“会话内容”分开存储。会话列表只存 SessionId 和最后访问时间,用轻量级的读锁即可;会话内容用写锁保护。
- 批量清理:不再每次请求都检查过期,而是启动一个后台任务,每隔 5 分钟扫描一次所有桶,清理过期的会话。
以下是后台清理任务的代码片段:
impl SessionCache {
pub async fn cleanup_expired(&self) {
let mut buckets = self.buckets.write().await;
buckets.retain(|_id, bucket| !bucket.is_expired(self.default_ttl_hours));
println!("Cleaned up expired sessions. Remaining: {}", buckets.len());
}
}
这个改动后,内存占用稳定在 50MB 以内,即使处理 10 万条消息也没问题。
性能优化:避免不必要的 Clone
Rust 的内存管理讲究所有权。我在初期测试中发现,每次查询历史时,如果返回的是 Vec<MessageEntry>,会产生大量克隆,导致 CPU 飙高。
我的优化策略是:
- 返回引用而非值:使用
&[MessageEntry]或Rc<VecDeque<MessageEntry>>,避免深拷贝。 - 按需加载:只加载最近 N 条消息,而不是整个历史。
- 使用
Arc共享不可变数据:对于只读的场景,用Arc共享相同的数据,减少内存重复。
最终,我将单次查询的延迟从 2ms 降到了 0.1ms 以下,这对实时交互的 Agent 至关重要。
总结与下一步
这个带历史感知的缓存系统,已经在我的 AI Agent 项目中稳定运行了三个月。它解决了上下文断层、内存泄漏和并发竞争三个核心问题。
如果你也在用 Rust 构建 Agent 或类似系统,建议:
- 不要依赖现成的 LRU 库,业务逻辑往往更复杂。
- 重视
VecDeque和RwLock的组合使用。 - 定期 profiling,找到真正的瓶颈,而不是猜测。
下一步,我计划将这个缓存系统与向量数据库(如 Weaviate)结合,实现语义检索的历史记忆,进一步提升 Agent 的智能化水平。
本文首发于 Rust 内存管理实战:为 AI Agent 构建带历史感知的缓存系统 — https://lyxq.com.cn/zh/blog/rust-memory-agent-cache
转载或引用请注明出处,商业使用请联系作者获得授权。