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:
@@ -99,6 +99,43 @@ type Thinker struct {
|
||||
|
||||
// 思考计数器(用于周期性记忆维护,每 N 次思考触发一次)
|
||||
thinkCount int
|
||||
|
||||
// Phase 1 Step 4: 思考链 + 自主工具安全策略
|
||||
chain *ThinkChain
|
||||
autoToolPolicy *AutonomousToolPolicy
|
||||
|
||||
// Phase 2: 情感追踪
|
||||
emotionTracker *persona.EmotionTracker
|
||||
|
||||
// Phase 2: 主动消息决策守卫
|
||||
proactiveGuard *ProactiveGuard
|
||||
|
||||
// Phase 2: 在线状态追踪
|
||||
userOnline bool
|
||||
lastOnlineChange time.Time
|
||||
userSessionID string // 当前活跃的 session ID (用于重连)
|
||||
}
|
||||
|
||||
// AutonomousToolPolicy 自主思考工具调用安全策略
|
||||
type AutonomousToolPolicy struct {
|
||||
// 允许在自主思考中使用的工具白名单
|
||||
AllowedTools []string // iot_query, memory_search, web_search, calculator, datetime
|
||||
// 每轮最大工具调用次数
|
||||
MaxToolCallsPerRound int // 默认 5
|
||||
// 高风险操作每小时最大次数 (如 iot_control)
|
||||
MaxHighRiskPerHour int // 默认 10
|
||||
// 高风险工具列表
|
||||
HighRiskTools []string // iot_control
|
||||
}
|
||||
|
||||
// DefaultAutonomousToolPolicy 默认安全策略
|
||||
func DefaultAutonomousToolPolicy() *AutonomousToolPolicy {
|
||||
return &AutonomousToolPolicy{
|
||||
AllowedTools: []string{"iot_query", "iot_control", "memory_search", "web_search", "calculator", "datetime", "web_fetch"},
|
||||
MaxToolCallsPerRound: 5,
|
||||
MaxHighRiskPerHour: 10,
|
||||
HighRiskTools: []string{"iot_control"},
|
||||
}
|
||||
}
|
||||
|
||||
// SetMessagePusher 设置主动消息推送回调
|
||||
@@ -108,6 +145,40 @@ func (t *Thinker) SetMessagePusher(pusher func(string, string, string)) {
|
||||
t.messagePusher = pusher
|
||||
}
|
||||
|
||||
// SetEmotionTracker sets the emotion tracker.
|
||||
func (t *Thinker) SetEmotionTracker(et *persona.EmotionTracker) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
t.emotionTracker = et
|
||||
}
|
||||
|
||||
// UpdatePresence updates the user online status.
|
||||
// Called by the ai-core presence endpoint when gateway detects connect/disconnect.
|
||||
func (t *Thinker) UpdatePresence(online bool, sessionID string) {
|
||||
t.mu.Lock()
|
||||
wasOffline := !t.userOnline
|
||||
t.userOnline = online
|
||||
t.lastOnlineChange = time.Now()
|
||||
if sessionID != "" {
|
||||
t.userSessionID = sessionID
|
||||
t.activeSessionID = sessionID
|
||||
}
|
||||
t.mu.Unlock()
|
||||
|
||||
if online && wasOffline {
|
||||
log.Printf("[后台思考] 用户上线 (session=%s),触发重连思考", sessionID)
|
||||
// Trigger a return-thinking cycle after a short delay
|
||||
time.Sleep(2 * time.Second)
|
||||
t.performThink("user_returned")
|
||||
// Also update emotion tracker
|
||||
if t.emotionTracker != nil {
|
||||
t.emotionTracker.UpdateMood("user_returned")
|
||||
}
|
||||
} else if !online {
|
||||
log.Printf("[后台思考] 用户离线")
|
||||
}
|
||||
}
|
||||
|
||||
// ThinkerConfig 后台思考配置
|
||||
type ThinkerConfig struct {
|
||||
Enabled bool
|
||||
@@ -171,6 +242,9 @@ func NewThinker(
|
||||
pendingThoughts: make([]*PendingThought, 0),
|
||||
lastUserMessage: time.Now(),
|
||||
stopCh: make(chan struct{}),
|
||||
chain: NewThinkChain(10),
|
||||
autoToolPolicy: DefaultAutonomousToolPolicy(),
|
||||
proactiveGuard: DefaultProactiveGuard(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,16 +436,27 @@ func (t *Thinker) periodicThinkLoop() {
|
||||
sinceLastUser := time.Since(t.lastUserMessage)
|
||||
t.mu.Unlock()
|
||||
|
||||
// 跳过条件:用户最近在活动(30s 内有消息),说明正在对话中
|
||||
if sinceLastUser < 30*time.Second {
|
||||
log.Printf("[后台思考] 用户在 %v 前发过消息,跳过周期性触发 (留给事件驱动处理)", sinceLastUser.Round(time.Second))
|
||||
continue
|
||||
}
|
||||
// Phase 2: 离线时降低思考频率 (每30分钟一次,而非5分钟)
|
||||
t.mu.Lock()
|
||||
isOffline := !t.userOnline
|
||||
t.mu.Unlock()
|
||||
offlineMinGap := 30 * time.Minute
|
||||
|
||||
if sinceLastThink < t.minThinkGap {
|
||||
log.Printf("[后台思考] 距上次思考仅 %v,跳过周期性触发", sinceLastThink.Round(time.Second))
|
||||
continue
|
||||
}
|
||||
// 跳过条件:用户最近在活动(30s 内有消息),说明正在对话中
|
||||
if sinceLastUser < 30*time.Second {
|
||||
log.Printf("[后台思考] 用户在 %v 前发过消息,跳过周期性触发 (留给事件驱动处理)", sinceLastUser.Round(time.Second))
|
||||
continue
|
||||
}
|
||||
|
||||
if isOffline && sinceLastThink < offlineMinGap {
|
||||
log.Printf("[后台思考] 用户离线,距上次思考仅 %v,跳过 (离线模式最小间隔=%v)", sinceLastThink.Round(time.Second), offlineMinGap)
|
||||
continue
|
||||
}
|
||||
|
||||
if !isOffline && sinceLastThink < t.minThinkGap {
|
||||
log.Printf("[后台思考] 距上次思考仅 %v,跳过周期性触发", sinceLastThink.Round(time.Second))
|
||||
continue
|
||||
}
|
||||
|
||||
log.Printf("[后台思考] 周期性触发 (上次思考=%v前, 上次用户消息=%v前)", sinceLastThink.Round(time.Second), sinceLastUser.Round(time.Second))
|
||||
t.performThink("periodic")
|
||||
@@ -484,11 +569,11 @@ func (t *Thinker) performThink(triggerReason string) {
|
||||
{Role: model.RoleUser, Content: userPrompt},
|
||||
}
|
||||
|
||||
// 6. 准备工具定义
|
||||
openAITools := t.buildOpenAITools()
|
||||
// 6. 准备工具定义(通过自主工具策略过滤)
|
||||
openAITools := t.filterToolsByPolicy(t.buildOpenAITools())
|
||||
|
||||
// 7. 调用 LLM(支持工具调用,最多 3 轮)
|
||||
maxToolRounds := 3
|
||||
// 7. 调用 LLM(支持工具调用,策略限制轮数)
|
||||
maxToolRounds := t.autoToolPolicy.MaxToolCallsPerRound
|
||||
var finalContent string
|
||||
var totalToolCalls int
|
||||
var toolCallRecords []map[string]interface{}
|
||||
@@ -569,6 +654,21 @@ func (t *Thinker) performThink(triggerReason string) {
|
||||
// 8. 存储思考结果
|
||||
t.storeThought(finalContent, toolCallsJSON, totalToolCalls)
|
||||
|
||||
// 8.5 记录到思考链
|
||||
if t.chain != nil {
|
||||
conclusions, followUps := extractConclusions(finalContent)
|
||||
t.chain.Add(ThinkRecord{
|
||||
ID: generateID(),
|
||||
Content: finalContent,
|
||||
Conclusions: conclusions,
|
||||
FollowUps: followUps,
|
||||
ToolCalls: totalToolCalls,
|
||||
Trigger: triggerReason,
|
||||
Timestamp: time.Now(),
|
||||
})
|
||||
log.Printf("[后台思考] 思考链已记录 (序号=%d, 结论数=%d, 后续问题=%d)", t.chain.Size(), len(conclusions), len(followUps))
|
||||
}
|
||||
|
||||
log.Printf("[后台思考] 完成 (触发原因=%s, 内容长度=%d, 工具调用=%d次)", triggerReason, len(finalContent), totalToolCalls)
|
||||
|
||||
// 9. 周期性记忆维护(每 10 次思考触发一次)
|
||||
@@ -582,7 +682,11 @@ func (t *Thinker) performThink(triggerReason string) {
|
||||
// 关键改动:不再是"定期的自我反思",而是"自然的、人性化的内在想法"。
|
||||
// triggerReason 影响提示词的侧重点。
|
||||
func (t *Thinker) buildThinkingSystemPrompt(personaConfig *persona.PersonaConfig, triggerReason string) string {
|
||||
basePrompt := personaConfig.BuildSystemPrompt("开拓者", 1)
|
||||
mood, moodExpr, _ := "", "", 0.0
|
||||
if t.emotionTracker != nil {
|
||||
mood, moodExpr, _ = t.emotionTracker.GetCurrentMood()
|
||||
}
|
||||
basePrompt := personaConfig.BuildSystemPromptWithMood("开拓者", 1, mood, moodExpr)
|
||||
|
||||
var thinkingInstructions string
|
||||
|
||||
@@ -620,9 +724,10 @@ func (t *Thinker) buildThinkingSystemPrompt(personaConfig *persona.PersonaConfig
|
||||
- 如果他状态正常——有没有想在下次对话中聊的话题?` + noDisturbRules + `
|
||||
|
||||
其他规则:
|
||||
1. 用第三人称或自言自语的方式,不要直接对开拓者喊话。
|
||||
1. 反思部分用第三人称或自言自语的方式,不要直接对开拓者喊话。
|
||||
2. 只有开拓者状态正常且真的有必要时才写【主动消息】,不要硬找话题。
|
||||
3. 2-4句话即可。`
|
||||
3. 【主动消息】的内容必须直接对开拓者说话(用"你"称呼他),像主动找他聊天一样。反思是给自己看的,主动消息是发给他的——语气要区分开。
|
||||
4. 2-4句话即可。`
|
||||
|
||||
case "silence":
|
||||
thinkingInstructions = `
|
||||
@@ -756,6 +861,24 @@ func (t *Thinker) buildThinkingUserPrompt(
|
||||
sb.WriteString("\n【你记得的关于开拓者的事】\n(暂无相关记忆)\n")
|
||||
}
|
||||
|
||||
// 思考链:注入上一轮的结论和待续问题
|
||||
if t.chain != nil {
|
||||
lastConclusions := t.chain.LastConclusions(3)
|
||||
if len(lastConclusions) > 0 {
|
||||
sb.WriteString("\n【你上一轮思考的结论】\n")
|
||||
for _, c := range lastConclusions {
|
||||
sb.WriteString(fmt.Sprintf("- %s\n", c))
|
||||
}
|
||||
}
|
||||
lastFollowUps := t.chain.LastFollowUps()
|
||||
if len(lastFollowUps) > 0 {
|
||||
sb.WriteString("\n【你上次想继续思考的问题】\n")
|
||||
for _, f := range lastFollowUps {
|
||||
sb.WriteString(fmt.Sprintf("- %s\n", f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IoT 设备状态
|
||||
if deviceSummary != "" {
|
||||
sb.WriteString("\n" + deviceSummary)
|
||||
@@ -766,10 +889,32 @@ func (t *Thinker) buildThinkingUserPrompt(
|
||||
sb.WriteString("\n记住:这是日记,用第三人称或自言自语的方式。")
|
||||
sb.WriteString("\n⚠️ 如果开拓者正在休息/睡觉/忙碌——不要写【主动消息】。你可以在心里想他,但不要去打扰。")
|
||||
sb.WriteString("\n只有在你确认他现在是醒着、有空、且真的需要关心时,才写【主动消息】。")
|
||||
sb.WriteString("\n❗【主动消息】的内容必须直接对开拓者说话(用\"你\"来称呼他),就像你主动找他聊天一样自然。不要用第三人称或自言自语的方式写主动消息。")
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// filterToolsByPolicy 通过自主工具安全策略过滤工具列表
|
||||
func (t *Thinker) filterToolsByPolicy(tools []llm.OpenAITool) []llm.OpenAITool {
|
||||
if t.autoToolPolicy == nil || len(tools) == 0 {
|
||||
return tools
|
||||
}
|
||||
allowed := make(map[string]bool)
|
||||
for _, name := range t.autoToolPolicy.AllowedTools {
|
||||
allowed[name] = true
|
||||
}
|
||||
var filtered []llm.OpenAITool
|
||||
for _, tool := range tools {
|
||||
if allowed[tool.Function.Name] {
|
||||
filtered = append(filtered, tool)
|
||||
}
|
||||
}
|
||||
if len(filtered) < len(tools) {
|
||||
log.Printf("[后台思考] 工具策略过滤: %d/%d 工具可用", len(filtered), len(tools))
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
// buildOpenAITools 将工具注册中心的定义转换为 LLM 层的 OpenAITool 格式
|
||||
func (t *Thinker) buildOpenAITools() []llm.OpenAITool {
|
||||
if t.toolRegistry == nil || !t.toolRegistry.IsEnabled() {
|
||||
@@ -817,17 +962,29 @@ func (t *Thinker) storeThought(content string, toolCallsJSON string, toolCallCou
|
||||
pusher := t.messagePusher
|
||||
canPush := proactiveMsg != "" && pusher != nil
|
||||
if canPush {
|
||||
// 检查频率限制
|
||||
gapSinceLast := time.Since(t.lastProactiveMsgTime)
|
||||
minGap := t.proactiveMsgMinGap
|
||||
if minGap <= 0 {
|
||||
minGap = 30 * time.Minute
|
||||
}
|
||||
if gapSinceLast < minGap {
|
||||
log.Printf("[后台思考] 主动消息距上次仅 %v,跳过推送 (最小间隔=%v)", gapSinceLast.Round(time.Second), minGap)
|
||||
// Phase 2: 使用 ProactiveGuard 多维度评估
|
||||
urgency := ExtractUrgencyFromContent(proactiveMsg)
|
||||
if valid, reason := ValidateProactiveMessage(proactiveMsg); !valid {
|
||||
log.Printf("[后台思考] 主动消息内容校验失败: %s,跳过推送", reason)
|
||||
canPush = false
|
||||
} else {
|
||||
t.lastProactiveMsgTime = time.Now()
|
||||
}
|
||||
if canPush && t.proactiveGuard != nil {
|
||||
decision := t.proactiveGuard.Evaluate(time.Now(), t.lastProactiveMsgTime, urgency, "active")
|
||||
logDecision(decision)
|
||||
if !decision.ShouldSend {
|
||||
canPush = false
|
||||
} else {
|
||||
t.lastProactiveMsgTime = time.Now()
|
||||
t.proactiveGuard.RecordSend(time.Now())
|
||||
}
|
||||
} else if canPush {
|
||||
gapSinceLast := time.Since(t.lastProactiveMsgTime)
|
||||
if gapSinceLast < 30*time.Minute {
|
||||
log.Printf("[后台思考] 主动消息距上次仅 %v,跳过推送", gapSinceLast.Round(time.Second))
|
||||
canPush = false
|
||||
} else {
|
||||
t.lastProactiveMsgTime = time.Now()
|
||||
}
|
||||
}
|
||||
}
|
||||
t.mu.Unlock()
|
||||
|
||||
Reference in New Issue
Block a user