fix: 修复 AI 回复无法送达发送者 + 重复消息 + action角色泄露 + OS环境支持
广播逻辑重构: - AI 回复 (stream_start/response/stream_segments/multi_message/stream_end) 改用 broadcastToUser 发送给所有客户端 - 用户消息回显保持 broadcastToUserExcept 排除发送者 消息去重与角色修复: - CacheMessage(user) 移至回复生成后,避免本轮 LLM 调用出现重复用户消息 - action 角色消息在 DB 存储时映射为 assistant,DeepSeek 等模型不支持自定义角色 - stream_end defer 机制确保错误路径也会终止客户端思考指示器 OS 完整环境支持: - host 包重构为 HostBackend 接口 + Direct/WSL/Docker 三种后端 - 新增 os_exec/os_file/os_system 工具供 AI 在完整 Linux 环境中自由操作 其他: - 视觉模型注入 + 图片预处理后清空 Images 避免传给 Chat 模型 - 图片 URL 相对路径→绝对 URL 转换 - DevTools 链路追踪页面 + 重启修复 - 记忆搜索模糊匹配增强 - 后台思考定时调度支持 - 管理后台页面 (模型配置/用户管理等) - docs/api 更新广播机制说明 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -2,12 +2,15 @@ package subsession
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/yourname/cyrene-ai/pkg/logger"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/yourname/cyrene-ai/ai-core/internal/llm"
|
||||
"github.com/yourname/cyrene-ai/ai-core/internal/memory"
|
||||
"github.com/yourname/cyrene-ai/ai-core/internal/model"
|
||||
"github.com/yourname/cyrene-ai/pkg/logger"
|
||||
)
|
||||
|
||||
// MemoryRetriever 记忆检索接口
|
||||
@@ -16,9 +19,12 @@ type MemoryRetriever interface {
|
||||
}
|
||||
|
||||
// MemoryProvider 记忆检索子会话提供者
|
||||
// 职责:检索与当前对话相关的用户记忆,排序去重,返回结构化摘要
|
||||
// 职责:检索与当前对话相关的用户记忆,排序去重,返回结构化摘要。
|
||||
// 支持 LLM 驱动的模糊关键词扩展搜索。
|
||||
type MemoryProvider struct {
|
||||
retriever MemoryRetriever
|
||||
retriever MemoryRetriever
|
||||
llmAdapter *llm.Adapter
|
||||
memClient *memory.Client
|
||||
}
|
||||
|
||||
// NewMemoryProvider 创建记忆检索子会话提供者
|
||||
@@ -28,6 +34,12 @@ func NewMemoryProvider(retriever MemoryRetriever) *MemoryProvider {
|
||||
}
|
||||
}
|
||||
|
||||
// SetFuzzySearch enables LLM-driven fuzzy keyword expansion for broader memory retrieval.
|
||||
func (p *MemoryProvider) SetFuzzySearch(llmAdapter *llm.Adapter, memClient *memory.Client) {
|
||||
p.llmAdapter = llmAdapter
|
||||
p.memClient = memClient
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) Type() model.SubSessionType {
|
||||
return model.SubSessionMemory
|
||||
}
|
||||
@@ -93,6 +105,7 @@ func (p *MemoryProvider) Execute(ctx context.Context, subCtx []model.LLMMessage)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Phase 1: exact/keyword retrieval
|
||||
memories, err := p.retriever.Retrieve(ctx, userID, userMessage)
|
||||
if err != nil {
|
||||
logger.Printf("[memory-subsession] 记忆检索失败: %v", err)
|
||||
@@ -101,6 +114,20 @@ func (p *MemoryProvider) Execute(ctx context.Context, subCtx []model.LLMMessage)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
seen := make(map[string]bool)
|
||||
for _, m := range memories {
|
||||
seen[m.ID] = true
|
||||
}
|
||||
|
||||
// Phase 2: LLM-driven fuzzy keyword expansion + semantic search
|
||||
fuzzyMemories := p.fuzzySearch(ctx, userID, userMessage)
|
||||
for _, m := range fuzzyMemories {
|
||||
if !seen[m.ID] {
|
||||
seen[m.ID] = true
|
||||
memories = append(memories, m)
|
||||
}
|
||||
}
|
||||
|
||||
// 转换为 MemorySnippet
|
||||
snippets := make([]model.MemorySnippet, 0, len(memories))
|
||||
for _, m := range memories {
|
||||
@@ -117,7 +144,7 @@ func (p *MemoryProvider) Execute(ctx context.Context, subCtx []model.LLMMessage)
|
||||
if len(snippets) == 0 {
|
||||
result.Summary = "(没有找到相关记忆)"
|
||||
} else {
|
||||
result.Summary = fmt.Sprintf("检索到 %d 条相关记忆", len(snippets))
|
||||
result.Summary = fmt.Sprintf("检索到 %d 条相关记忆(含模糊匹配)", len(snippets))
|
||||
// 按重要性列出前几条
|
||||
topCount := len(snippets)
|
||||
if topCount > 3 {
|
||||
@@ -138,6 +165,74 @@ func (p *MemoryProvider) Execute(ctx context.Context, subCtx []model.LLMMessage)
|
||||
}
|
||||
|
||||
result.Memories = snippets
|
||||
logger.Printf("[memory-subsession] 完成: %s", result.Summary)
|
||||
logger.Printf("[memory-subsession] 完成: %s (精确=%d, 模糊=%d)", result.Summary, len(memories)-len(fuzzyMemories), len(fuzzyMemories))
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// fuzzySearch expands the user message into fuzzy keywords via LLM and performs semantic search.
|
||||
func (p *MemoryProvider) fuzzySearch(ctx context.Context, userID, userMessage string) []memory.MemoryEntry {
|
||||
if p.llmAdapter == nil || p.memClient == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
keywords := p.expandKeywords(ctx, userMessage)
|
||||
if len(keywords) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Printf("[memory-subsession] 模糊关键词: %v", keywords)
|
||||
|
||||
var allResults []memory.MemoryEntry
|
||||
seen := make(map[string]bool)
|
||||
|
||||
for _, kw := range keywords {
|
||||
results, err := p.memClient.QueryByText(ctx, userID, kw, "", 0, 5)
|
||||
if err != nil {
|
||||
logger.Printf("[memory-subsession] 模糊搜索 '%s' 失败: %v", kw, err)
|
||||
continue
|
||||
}
|
||||
for _, m := range results {
|
||||
if !seen[m.ID] {
|
||||
seen[m.ID] = true
|
||||
allResults = append(allResults, m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allResults
|
||||
}
|
||||
|
||||
// expandKeywords uses LLM to generate fuzzy/related search keywords from the user message.
|
||||
func (p *MemoryProvider) expandKeywords(ctx context.Context, message string) []string {
|
||||
prompt := fmt.Sprintf(
|
||||
"从以下对话消息中提取 3-5 个可用于模糊搜索记忆的关键词。这些关键词应该是:\n"+
|
||||
"- 与话题相关的抽象概念\n- 同义词和相关词\n- 更宽泛或更具体的相关概念\n"+
|
||||
"- 不要包含消息中已经出现的原词\n\n"+
|
||||
"用户消息:「%s」\n\n"+
|
||||
"只输出 JSON 字符串数组,例如:[\"关键词1\",\"关键词2\"]", message)
|
||||
|
||||
resp, err := p.llmAdapter.Chat(ctx, []model.LLMMessage{
|
||||
{Role: model.RoleSystem, Content: "你是记忆搜索专家。输出 JSON 字符串数组。"},
|
||||
{Role: model.RoleUser, Content: prompt},
|
||||
})
|
||||
if err != nil {
|
||||
logger.Printf("[memory-subsession] 关键词扩展失败: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
text := strings.TrimSpace(resp.Content)
|
||||
// Extract JSON array
|
||||
if idx := strings.Index(text, "["); idx >= 0 {
|
||||
if end := strings.LastIndex(text, "]"); end > idx {
|
||||
text = text[idx : end+1]
|
||||
}
|
||||
}
|
||||
|
||||
var keywords []string
|
||||
if err := json.Unmarshal([]byte(text), &keywords); err != nil {
|
||||
logger.Printf("[memory-subsession] 解析关键词 JSON 失败: %v (raw=%s)", err, resp.Content)
|
||||
return nil
|
||||
}
|
||||
|
||||
return keywords
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user