Skip to main content

lc_core/cache/
llm_cache.rs

1// src/core/cache/llm_cache.rs
2//! LLM 调用缓存实现
3//!
4//! 基于内存的 LRU 缓存,缓存重复的 LLM 调用结果。
5//! 支持可选的 TTL 过期和最大条目限制。
6
7use crate::language_models::LLMResult;
8use lc_schema::Message;
9use std::collections::HashMap;
10use std::time::{Duration, Instant};
11use tokio::sync::RwLock;
12
13/// 缓存的 LLM 结果,包含过期时间
14#[derive(Debug, Clone)]
15pub struct CachedLLMResult {
16    /// LLM 返回结果
17    pub result: LLMResult,
18    /// 缓存时间戳
19    pub cached_at: Instant,
20}
21
22/// 缓存配置
23#[derive(Debug, Clone)]
24pub struct CacheConfig {
25    /// 最大缓存条目数(0 表示不限制)
26    pub max_entries: usize,
27    /// TTL 过期时间(None 表示永不过期)
28    pub ttl: Option<Duration>,
29    /// 是否启用
30    pub enabled: bool,
31}
32
33impl Default for CacheConfig {
34    fn default() -> Self {
35        Self {
36            max_entries: 1000,
37            ttl: Some(Duration::from_secs(3600)), // 默认 1 小时过期
38            enabled: true,
39        }
40    }
41}
42
43impl CacheConfig {
44    pub fn new() -> Self {
45        Self::default()
46    }
47
48    /// 禁用 TTL(永不过期)
49    pub fn no_ttl(mut self) -> Self {
50        self.ttl = None;
51        self
52    }
53
54    /// 设置 TTL
55    pub fn with_ttl(mut self, ttl: Duration) -> Self {
56        self.ttl = Some(ttl);
57        self
58    }
59
60    /// 设置最大条目数
61    pub fn with_max_entries(mut self, max: usize) -> Self {
62        self.max_entries = max;
63        self
64    }
65
66    /// 禁用缓存
67    pub fn disabled(mut self) -> Self {
68        self.enabled = false;
69        self
70    }
71}
72
73/// LLM 调用缓存
74///
75/// 缓存 LLM 调用的输入输出,避免相同请求重复调用 API。
76///
77/// # 示例
78/// ```ignore
79/// use langchainrust::core::cache::LLMCache;
80///
81/// let cache = LLMCache::new();
82/// cache.put("key", llm_result).await;
83///
84/// if let Some(cached) = cache.get("key").await {
85///     println!("缓存命中: {}", cached.result.content);
86/// }
87/// ```
88pub struct LLMCache {
89    config: CacheConfig,
90    store: RwLock<HashMap<String, CachedLLMResult>>,
91}
92
93impl LLMCache {
94    pub fn new() -> Self {
95        Self::with_config(CacheConfig::default())
96    }
97
98    pub fn with_config(config: CacheConfig) -> Self {
99        Self {
100            config,
101            store: RwLock::new(HashMap::new()),
102        }
103    }
104
105    /// 从消息列表生成缓存键
106    ///
107    /// 将消息列表序列化为 JSON 字符串作为键。
108    /// 包含 model 名称以确保不同模型的调用不互相影响。
109    /// 如果序列化失败,返回错误而非回退到空字符串(M34)。
110    pub fn build_key(messages: &[Message], model: &str) -> Result<String, String> {
111        let serialized = serde_json::to_string(messages)
112            .map_err(|e| format!("cache key serialization failed: {}", e))?;
113        Ok(format!("{}:{}", model, serialized))
114    }
115
116    /// 获取缓存结果
117    ///
118    /// 如果发现过期条目,会立即删除(H36)。命中时会刷新 `cached_at`,
119    /// 使缓存保持真 LRU 语义(Q7:命中后该条目成为最近使用)。
120    pub async fn get(&self, key: &str) -> Option<CachedLLMResult> {
121        if !self.config.enabled {
122            return None;
123        }
124
125        let store = self.store.read().await;
126        let entry = store.get(key)?;
127
128        // 检查 TTL
129        if let Some(ttl) = self.config.ttl {
130            if entry.cached_at.elapsed() > ttl {
131                // H36: 过期条目需要删除,先释放读锁再获取写锁
132                drop(store);
133                let mut store = self.store.write().await;
134                // Double-check after acquiring write lock
135                if let Some(entry) = store.get(key) {
136                    if entry.cached_at.elapsed() > ttl {
137                        store.remove(key);
138                    }
139                }
140                return None;
141            }
142        }
143
144        let result = entry.clone();
145
146        // Q7: 命中后刷新 LRU 时间戳。释放读锁后取写锁更新。
147        drop(store);
148        let mut store = self.store.write().await;
149        if let Some(entry) = store.get_mut(key) {
150            entry.cached_at = Instant::now();
151        }
152
153        Some(result)
154    }
155
156    /// 存入缓存结果
157    pub async fn put(&self, key: String, result: LLMResult) {
158        if !self.config.enabled {
159            return;
160        }
161
162        let mut store = self.store.write().await;
163
164        // 检查是否需要淘汰
165        if self.config.max_entries > 0 && store.len() >= self.config.max_entries {
166            // 移除最早的一条
167            if let Some(oldest_key) = store
168                .iter()
169                .min_by_key(|(_, v)| v.cached_at)
170                .map(|(k, _)| k.clone())
171            {
172                store.remove(&oldest_key);
173            }
174        }
175
176        store.insert(
177            key,
178            CachedLLMResult {
179                result,
180                cached_at: Instant::now(),
181            },
182        );
183    }
184
185    /// 清除缓存
186    pub async fn clear(&self) {
187        let mut store = self.store.write().await;
188        store.clear();
189    }
190
191    /// 获取缓存大小
192    pub async fn len(&self) -> usize {
193        let store = self.store.read().await;
194        store.len()
195    }
196
197    /// 缓存是否为空
198    pub async fn is_empty(&self) -> bool {
199        self.len().await == 0
200    }
201
202    /// 移除过期条目
203    pub async fn evict_expired(&self) -> usize {
204        if let Some(ttl) = self.config.ttl {
205            let mut store = self.store.write().await;
206            let before = store.len();
207            store.retain(|_, v| v.cached_at.elapsed() <= ttl);
208            before - store.len()
209        } else {
210            0
211        }
212    }
213}
214
215impl Default for LLMCache {
216    fn default() -> Self {
217        Self::new()
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224    use crate::language_models::TokenUsage;
225
226    fn make_result(content: &str) -> LLMResult {
227        LLMResult {
228            content: content.to_string(),
229            model: "test-model".to_string(),
230            token_usage: Some(TokenUsage {
231                prompt_tokens: 10,
232                completion_tokens: 5,
233                total_tokens: 15,
234            }),
235            tool_calls: None,
236            thinking_content: None,
237        }
238    }
239
240    #[tokio::test]
241    async fn test_cache_put_and_get() {
242        let cache = LLMCache::new();
243        let key = "test-key";
244        let result = make_result("Hello, world!");
245
246        cache.put(key.to_string(), result.clone()).await;
247        let cached = cache.get(key).await;
248
249        assert!(cached.is_some());
250        assert_eq!(cached.unwrap().result.content, "Hello, world!");
251    }
252
253    #[tokio::test]
254    async fn test_cache_miss() {
255        let cache = LLMCache::new();
256        let cached = cache.get("non-existent").await;
257        assert!(cached.is_none());
258    }
259
260    #[tokio::test]
261    async fn test_cache_clear() {
262        let cache = LLMCache::new();
263        cache.put("k1".to_string(), make_result("r1")).await;
264        cache.put("k2".to_string(), make_result("r2")).await;
265        assert_eq!(cache.len().await, 2);
266
267        cache.clear().await;
268        assert_eq!(cache.len().await, 0);
269    }
270
271    #[tokio::test]
272    async fn test_cache_disabled() {
273        let config = CacheConfig::new().disabled();
274        let cache = LLMCache::with_config(config);
275
276        cache.put("key".to_string(), make_result("test")).await;
277        let cached = cache.get("key").await;
278        assert!(cached.is_none());
279    }
280
281    #[tokio::test]
282    async fn test_cache_ttl_expiry() {
283        let config = CacheConfig::new().with_ttl(Duration::from_millis(10));
284        let cache = LLMCache::with_config(config);
285
286        cache.put("key".to_string(), make_result("test")).await;
287        assert!(cache.get("key").await.is_some());
288
289        // 等待过期
290        tokio::time::sleep(Duration::from_millis(20)).await;
291        assert!(cache.get("key").await.is_none());
292    }
293
294    #[tokio::test]
295    async fn test_cache_max_entries() {
296        let config = CacheConfig::new().with_max_entries(3).no_ttl();
297        let cache = LLMCache::with_config(config);
298
299        cache.put("a".to_string(), make_result("1")).await;
300        cache.put("b".to_string(), make_result("2")).await;
301        cache.put("c".to_string(), make_result("3")).await;
302        assert_eq!(cache.len().await, 3);
303
304        // 超过限制,淘汰最早的一条
305        cache.put("d".to_string(), make_result("4")).await;
306        assert_eq!(cache.len().await, 3);
307        // a 应该被淘汰
308        assert!(cache.get("a").await.is_none());
309    }
310
311    #[tokio::test]
312    async fn test_cache_get_refreshes_lru_order() {
313        // max_entries = 2: hit on "a" must make "a" the most-recent entry, so
314        // inserting "c" evicts "b" (the old LRU) instead of "a".
315        let config = CacheConfig::new().with_max_entries(2).no_ttl();
316        let cache = LLMCache::with_config(config);
317
318        cache.put("a".to_string(), make_result("1")).await;
319        cache.put("b".to_string(), make_result("2")).await;
320
321        // Hit "a" → refreshes its cached_at.
322        assert!(cache.get("a").await.is_some());
323
324        // Push past the cap: the LRU evicts "b", keeping "a".
325        cache.put("c".to_string(), make_result("3")).await;
326        assert!(cache.get("a").await.is_some());
327        assert!(cache.get("b").await.is_none());
328        assert!(cache.get("c").await.is_some());
329    }
330
331    #[tokio::test]
332    async fn test_cache_no_ttl() {
333        let config = CacheConfig::new().no_ttl();
334        let cache = LLMCache::with_config(config);
335
336        cache.put("key".to_string(), make_result("persist")).await;
337
338        // 即使等待许久也不应过期
339        tokio::time::sleep(Duration::from_millis(10)).await;
340        assert!(cache.get("key").await.is_some());
341    }
342
343    #[tokio::test]
344    async fn test_cache_evict_expired() {
345        // 用 0 TTL 确保立即过期
346        let config = CacheConfig::new().with_ttl(Duration::from_millis(0));
347        let cache = LLMCache::with_config(config);
348
349        cache.put("key".to_string(), make_result("test")).await;
350        tokio::time::sleep(Duration::from_millis(1)).await;
351
352        let evicted = cache.evict_expired().await;
353        assert_eq!(evicted, 1);
354        assert!(cache.is_empty().await);
355    }
356
357    #[tokio::test]
358    async fn test_cache_build_key() {
359        let messages = vec![Message::human("Hello"), Message::ai("Hi!")];
360        let key = LLMCache::build_key(&messages, "gpt-4").unwrap();
361        assert!(key.contains("gpt-4"));
362        assert!(key.contains("Hello"));
363    }
364
365    #[tokio::test]
366    async fn test_cache_is_empty() {
367        let cache = LLMCache::new();
368        assert!(cache.is_empty().await);
369
370        cache.put("key".to_string(), make_result("test")).await;
371        assert!(!cache.is_empty().await);
372    }
373}