ai-chatbot / app /services /llm_cache.py
shuaiwang
fix: independent persist executor + reset pending_push on failure
8420e1e
Raw
History Blame Contribute Delete
2.7 kB
"""LLM 响应缓存 (内存 LRU).
为什么需要:
- 个人使用场景下, 同一问题反复问很常见
- 命中时直接跳过 LLM 调用 + 流式回放 token
- 节省 API 费用 + 缩短延迟
设计:
- key = sha256( (query + top_doc_ids + temperature) ) -- 只缓存"标准问答", 不缓存工具调用
- value = 完整回答内容 + 引用 + 工具调用结果
- LRU 容量可配 (默认 200)
- 进程内, 重启清空 (避免引入 Redis)
"""
from __future__ import annotations
import hashlib
import logging
from dataclasses import dataclass
from typing import Any
from cachetools import LRUCache
from app.config import settings
logger = logging.getLogger(__name__)
@dataclass
class CachedAnswer:
content: str
citations: list[dict[str, Any]]
tool_calls: list[dict[str, Any]]
tokens: list[str] # 预切好的 token 序列, 流式回放
_cache: LRUCache | None = None
_hits = 0
_misses = 0
def _make_key(query: str, top_doc_ids: list[str], temperature: float) -> str:
"""缓存 key. 包含 query + 命中文档 id + 温度, 避免不同上下文错命中."""
payload = f"{query.strip()}|{','.join(sorted(top_doc_ids))}|{temperature:.2f}"
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
def get_cache() -> LRUCache:
global _cache
if _cache is None:
size = settings.llm_cache_size if settings.llm_cache_enabled else 0
_cache = LRUCache(maxsize=size)
logger.info("LLM cache initialized: enabled=%s size=%d", settings.llm_cache_enabled, size)
return _cache
def lookup(query: str, top_doc_ids: list[str], temperature: float) -> CachedAnswer | None:
if not settings.llm_cache_enabled:
return None
key = _make_key(query, top_doc_ids, temperature)
hit = get_cache().get(key)
global _hits, _misses
if hit is not None:
_hits += 1
logger.debug("LLM cache HIT key=%s", key[:12])
else:
_misses += 1
return hit
def store(query: str, top_doc_ids: list[str], temperature: float, answer: CachedAnswer) -> None:
if not settings.llm_cache_enabled:
return
key = _make_key(query, top_doc_ids, temperature)
get_cache()[key] = answer
logger.debug("LLM cache STORE key=%s tokens=%d", key[:12], len(answer.tokens))
def stats() -> dict[str, Any]:
return {
"enabled": settings.llm_cache_enabled,
"size": len(get_cache()),
"max_size": get_cache().maxsize,
"hits": _hits,
"misses": _misses,
"hit_rate": (_hits / max(_hits + _misses, 1)),
}
def clear() -> None:
global _cache, _hits, _misses
_cache = None
_hits = 0
_misses = 0
logger.info("LLM cache cleared")