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