Files
Cyrene/backend/ai-core/internal/cache/response_cache.go
T
AskaEth 87214b9441 feat: Phase 1+2 架构进化 — 连续思考链/主动消息决策/情感状态机/离线自主思考 (86文件)
Phase 1 (基础设施):
- ThinkChain 思考链连续性 + 差异化思考提示词 (persistent)
- AutonomousToolPolicy 工具安全策略 (safe/unsafe/conditional)
- MessageScheduler 自适应消息节奏 (Idle/Available/Busy)
- SessionEnrichmentStore 渐进式上下文丰富 (5层)
- ConversationBus 事件总线 + ResponseCache (dedup)
- pkg/logger 统一日志 + 所有 handler 替换 fmt.Printf
- NPE 守卫/链路优化/数据库表修复/Go workspace

Phase 2 (人格交互):
- EmotionState/EmotionTracker 情感状态机 (5种心情, 情绪衰减)
- ProactiveGuard 主动消息多维决策 (静默时段/紧急度/频率/校验)
- Gateway↔ai-core 在线状态感知链路 (presence notification)
- 离线思考频率控制 + 重连问候 + 离线消息排队

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 15:25:12 +08:00

133 lines
3.1 KiB
Go

// Package cache provides a response cache for skipping redundant LLM calls
// on semantically similar inputs (greetings and common IoT commands).
package cache
import (
"strings"
"sync"
"time"
)
// Entry is a cached LLM response.
type Entry struct {
FullContent string
CachedAt time.Time
AccessCount int
}
// ResponseCache caches LLM responses keyed by normalized user input.
// It uses separate TTLs for greetings (longer) and other queries (shorter).
type ResponseCache struct {
mu sync.RWMutex
entries map[string]*Entry
maxEntries int
greetingTTL time.Duration
defaultTTL time.Duration
}
// New creates a new ResponseCache with sensible defaults.
func New() *ResponseCache {
return &ResponseCache{
entries: make(map[string]*Entry),
maxEntries: 200,
greetingTTL: 10 * time.Minute,
defaultTTL: 30 * time.Second,
}
}
// Get returns a cached response for the given input if it exists and hasn't expired.
func (c *ResponseCache) Get(input string) (string, bool) {
key := normalize(input)
c.mu.RLock()
entry, ok := c.entries[key]
c.mu.RUnlock()
if !ok {
return "", false
}
ttl := c.defaultTTL
if isGreeting(input) {
ttl = c.greetingTTL
}
if time.Since(entry.CachedAt) > ttl {
c.mu.Lock()
delete(c.entries, key)
c.mu.Unlock()
return "", false
}
c.mu.Lock()
entry.AccessCount++
c.mu.Unlock()
return entry.FullContent, true
}
// Set stores a response in the cache.
func (c *ResponseCache) Set(input, response string) {
key := normalize(input)
c.mu.Lock()
defer c.mu.Unlock()
// Evict oldest entries if at capacity
if len(c.entries) >= c.maxEntries {
var oldestKey string
var oldestTime time.Time
for k, v := range c.entries {
if oldestKey == "" || v.CachedAt.Before(oldestTime) {
oldestKey = k
oldestTime = v.CachedAt
}
}
if oldestKey != "" {
delete(c.entries, oldestKey)
}
}
c.entries[key] = &Entry{
FullContent: response,
CachedAt: time.Now(),
AccessCount: 0,
}
}
// Invalidate clears all cached entries.
func (c *ResponseCache) Invalidate() {
c.mu.Lock()
c.entries = make(map[string]*Entry)
c.mu.Unlock()
}
// Size returns the current number of cached entries.
func (c *ResponseCache) Size() int {
c.mu.RLock()
defer c.mu.RUnlock()
return len(c.entries)
}
// normalize produces a cache key from user input.
func normalize(input string) string {
s := strings.TrimSpace(strings.ToLower(input))
// Collapse multiple spaces
parts := strings.Fields(s)
return strings.Join(parts, " ")
}
// isGreeting returns true if the input looks like a simple greeting/small-talk
// that can be cached with a longer TTL.
func isGreeting(input string) bool {
normalized := normalize(input)
greetings := []string{
"你好", "嗨", "嘿", "哈喽", "hello", "hi", "hey",
"早上好", "下午好", "晚上好", "晚安", "早安", "午安",
"在吗", "在不在", "在么",
"谢谢", "多谢", "感谢", "thanks", "thank you",
"好的", "ok", "okay", "行", "可以",
"再见", "拜拜", "bye", "byebye",
"嗯", "哦", "噢",
}
for _, g := range greetings {
if normalized == g {
return true
}
}
return false
}