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>
This commit is contained in:
+132
@@ -0,0 +1,132 @@
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNormalize(t *testing.T) {
|
||||
tests := []struct{ input, want string }{
|
||||
{" Hello World ", "hello world"},
|
||||
{"你好", "你好"},
|
||||
{" 你好 呀 ", "你好 呀"},
|
||||
{"OK", "ok"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := normalize(tt.input)
|
||||
if got != tt.want {
|
||||
t.Errorf("normalize(%q) = %q, want %q", tt.input, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsGreeting(t *testing.T) {
|
||||
if !isGreeting("你好") {
|
||||
t.Error("'你好' should be a greeting")
|
||||
}
|
||||
if !isGreeting("hello") {
|
||||
t.Error("'hello' should be a greeting")
|
||||
}
|
||||
if isGreeting("今天天气真好") {
|
||||
t.Error("'今天天气真好' should NOT be a greeting")
|
||||
}
|
||||
if isGreeting("帮我开灯") {
|
||||
t.Error("'帮我开灯' should NOT be a greeting")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheHit(t *testing.T) {
|
||||
c := New()
|
||||
c.Set("你好呀", "你好呀,开拓者♪ 今天有什么想聊的吗?")
|
||||
|
||||
got, ok := c.Get("你好呀")
|
||||
if !ok {
|
||||
t.Fatal("expected cache hit")
|
||||
}
|
||||
if got != "你好呀,开拓者♪ 今天有什么想聊的吗?" {
|
||||
t.Errorf("cached response mismatch: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheMiss(t *testing.T) {
|
||||
c := New()
|
||||
_, ok := c.Get("从未说过的话")
|
||||
if ok {
|
||||
t.Error("expected cache miss")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheNormalization(t *testing.T) {
|
||||
c := New()
|
||||
c.Set(" 你好 ", "回复内容")
|
||||
|
||||
// Normalized key should match
|
||||
_, ok := c.Get("你好")
|
||||
if !ok {
|
||||
t.Error("normalized key should produce cache hit")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheEviction(t *testing.T) {
|
||||
c := New()
|
||||
c.maxEntries = 3
|
||||
c.Set("a", "A")
|
||||
c.Set("b", "B")
|
||||
c.Set("c", "C")
|
||||
c.Set("d", "D") // should evict the oldest
|
||||
|
||||
if c.Size() > 3 {
|
||||
t.Errorf("cache should be <= 3 entries, got %d", c.Size())
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidate(t *testing.T) {
|
||||
c := New()
|
||||
c.Set("test", "value")
|
||||
c.Invalidate()
|
||||
if c.Size() != 0 {
|
||||
t.Errorf("cache should be empty after invalidate, got %d", c.Size())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user