From 4af1a9a5db5f8eeea49484c98de63ca02b0462ca Mon Sep 17 00:00:00 2001 From: st <2663600842@qq.com> Date: Sat, 25 Jul 2026 11:26:10 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=9B=86=E6=88=90=E6=84=8F=E5=9B=BE?= =?UTF-8?q?=E5=88=86=E6=9E=90=E4=B8=8E=E7=94=A8=E6=88=B7=E4=B8=8A=E4=B8=8B?= =?UTF-8?q?=E6=96=87=E6=B3=A8=E5=85=A5\n\n-=20=E6=96=B0=E5=A2=9Eintent=5Fa?= =?UTF-8?q?nalyzer=EF=BC=8C=E8=BD=BB=E9=87=8F=E6=84=8F=E5=9B=BE=E8=AF=86?= =?UTF-8?q?=E5=88=AB=EF=BC=8C=E9=97=AE=E5=80=99/=E9=97=B2=E8=81=8A?= =?UTF-8?q?=E7=AD=89=E5=9C=BA=E6=99=AF=E8=B7=B3=E8=BF=87RAG\n-=20chat=5Fmo?= =?UTF-8?q?de.go:=20=E6=A3=80=E7=B4=A2=E5=89=8D=E5=81=9A=E6=84=8F=E5=9B=BE?= =?UTF-8?q?=E5=88=86=E6=9E=90=EF=BC=8C=E6=9D=A1=E4=BB=B6=E8=B7=B3=E8=BF=87?= =?UTF-8?q?=E6=A3=80=E7=B4=A2\n-=20chat=5Fservice.go:=20=E5=8A=A0=E8=BD=BD?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E5=9F=BA=E6=9C=AC=E4=BF=A1=E6=81=AF=E5=B9=B6?= =?UTF-8?q?=E6=B3=A8=E5=85=A5EnhancedContext\n-=20chat=5Fservice=5Fprompt.?= =?UTF-8?q?go:=20System=20Prompt=E6=B3=A8=E5=85=A5=E5=BD=93=E5=89=8D?= =?UTF-8?q?=E6=97=B6=E9=97=B4/=E7=94=A8=E6=88=B7=E5=90=8D/=E8=A7=92?= =?UTF-8?q?=E8=89=B2\n-=20context=5Finterface.go:=20EnhancedContext?= =?UTF-8?q?=E6=96=B0=E5=A2=9EUserCtx=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/service/chat_mode.go | 20 ++- internal/service/chat_service.go | 23 ++- internal/service/chat_service_prompt.go | 42 +++++- internal/service/context_interface.go | 1 + internal/service/intent_analyzer.go | 187 ++++++++++++++++++++++++ 5 files changed, 261 insertions(+), 12 deletions(-) create mode 100644 internal/service/intent_analyzer.go diff --git a/internal/service/chat_mode.go b/internal/service/chat_mode.go index 16d4486..ffccf6b 100644 --- a/internal/service/chat_mode.go +++ b/internal/service/chat_mode.go @@ -45,14 +45,23 @@ func (s *chatService) processMessage(ctx context.Context, userID, sessionID stri chatModel := client.ChatModel() - // Step 2+3: 检索(条件改写,优先速度) - sendProgressEvent(eventCh, "正在检索知识库...") + // Step 2: 轻量意图分析,决定是否需要检索 + intent := AnalyzeIntent(req.Content) + logger.Infof("意图识别: sessionID=%s, intent=%s, confidence=%.2f, reason=%s, skipRetrieval=%v", + sessionID, intent.Intent, intent.Confidence, intent.Reason, intent.SkipRetrieval) var sources []dto.SourceInfo var retrieveResult rag.Result - needRewrite := len(history) > 0 && needsQueryRewrite(req.Content) - if needRewrite { + if intent.SkipRetrieval { + sendProgressEvent(eventCh, "正在整理回答...") + // 问候/身份/元问题/闲聊/列表查询等直接跳过 RAG,由 System Prompt 指导回答 + } else { + // Step 3: 检索(条件改写,优先速度) + sendProgressEvent(eventCh, "正在检索知识库...") + + needRewrite := len(history) > 0 && needsQueryRewrite(req.Content) + if needRewrite { // 并行:查询改写 + 原始查询先行检索 g, gCtx := errgroup.WithContext(ctx) @@ -97,10 +106,11 @@ func (s *chatService) processMessage(ctx context.Context, userID, sessionID stri return } } +} // Step 4: 组装 Prompt(用原始问题,改写后的查询仅用于检索) sendProgressEvent(eventCh, "正在整理资料...") - messages := buildMessages(history, req.Content, retrieveResult, enhancedCtx.Summary, enhancedCtx.Memories, enhancedCtx.RetrievalBudget) + messages := buildMessages(history, req.Content, retrieveResult, enhancedCtx.Summary, enhancedCtx.Memories, enhancedCtx.UserCtx, enhancedCtx.RetrievalBudget) // Step 5: LLM 流式生成 assistantMsgID := uuid.New().String() diff --git a/internal/service/chat_service.go b/internal/service/chat_service.go index 0bab2a7..2d4a481 100644 --- a/internal/service/chat_service.go +++ b/internal/service/chat_service.go @@ -217,7 +217,10 @@ func (s *chatService) initContext(ctx context.Context, userID, sessionID, modelI maxCtx := llm.GetEffectiveMaxContextLength(modelID, client.MaxContextLength()) historyBudget, retrievalBudget, memoryBudget := calculateContextBudgets(maxCtx) - // 3. 使用 ContextService 构建增强上下文 + // 3. 加载用户基本信息 + userCtx := s.loadUserContext(ctx, userID) + + // 4. 使用 ContextService 构建增强上下文 var enhancedCtx *EnhancedContext if s.contextSvc != nil { t1 = time.Now() @@ -242,15 +245,29 @@ func (s *chatService) initContext(ctx context.Context, userID, sessionID, modelI RetrievalBudget: retrievalBudget, } } + enhancedCtx.UserCtx = userCtx - logger.Infof("增强上下文: 历史 %d 条(预算 %d), 记忆 %d 条(预算 %d), 检索预算 %d, 摘要存在=%v, 模型窗口=%d", + logger.Infof("增强上下文: 历史 %d 条(预算 %d), 记忆 %d 条(预算 %d), 检索预算 %d, 摘要存在=%v, 模型窗口=%d, 用户=%s", len(enhancedCtx.History), enhancedCtx.HistoryBudget, len(enhancedCtx.Memories), memoryBudget, - enhancedCtx.RetrievalBudget, enhancedCtx.Summary != nil, maxCtx) + enhancedCtx.RetrievalBudget, enhancedCtx.Summary != nil, maxCtx, userCtx.Username) logger.Infof("[Timing] initContext 总耗时: cost=%dms", time.Since(t0).Milliseconds()) return client, enhancedCtx, nil } +// loadUserContext 加载用户基本信息,失败时返回空上下文(不阻断主流程) +func (s *chatService) loadUserContext(ctx context.Context, userID string) UserContext { + if s.userRepo == nil || userID == "" { + return NewUserContext(entity.User{}) + } + user, err := s.userRepo.FindByID(userID) + if err != nil { + logger.Warnf("加载用户信息失败, userID=%s: %v", userID, err) + return NewUserContext(entity.User{}) + } + return NewUserContext(*user) +} + // calculateContextBudgets 根据模型最大上下文窗口,分配历史、检索、记忆的 token 预算 func calculateContextBudgets(maxContextLength int) (historyBudget, retrievalBudget, memoryBudget int) { if maxContextLength <= 0 { diff --git a/internal/service/chat_service_prompt.go b/internal/service/chat_service_prompt.go index df720ff..7b1e186 100644 --- a/internal/service/chat_service_prompt.go +++ b/internal/service/chat_service_prompt.go @@ -3,6 +3,7 @@ package service import ( "fmt" "strings" + "time" "unicode/utf8" "github.com/cloudwego/eino/schema" @@ -12,6 +13,28 @@ import ( "solvify-agent/pkg/tokenutil" ) +// UserContext 注入到 System Prompt 的用户上下文信息 +type UserContext struct { + ID string + Username string + Role string + TimeStr string +} + +// NewUserContext 创建用户上下文,TimeStr 使用当前时间 +func NewUserContext(user entity.User) UserContext { + roleText := "普通用户" + if user.Role == 2 { + roleText = "管理员" + } + return UserContext{ + ID: user.ID, + Username: user.Username, + Role: roleText, + TimeStr: time.Now().Format("2006-01-02 15:04:05(Monday)"), + } +} + const ( // maxContextTokens 检索结果注入 Prompt 的最大 token 预算(估算值) maxContextTokens = 3000 @@ -102,8 +125,8 @@ const quickModeSystemPrompt = `你是 Solvify-Agent(Solvify 知识助理), - 结构清晰:必要时用小标题或列表,避免一整段堆砌` // buildMessages 组装快速检索模式的 LLM 消息列表 -func buildMessages(history []entity.ChatMessage, question string, retrieveResult rag.Result, summary *entity.ChatSummary, memories []entity.UserMemory, retrievalBudget int) []*schema.Message { - systemPrompt := buildEnhancedSystemPrompt(quickModeSystemPrompt, summary, memories) +func buildMessages(history []entity.ChatMessage, question string, retrieveResult rag.Result, summary *entity.ChatSummary, memories []entity.UserMemory, userCtx UserContext, retrievalBudget int) []*schema.Message { + systemPrompt := buildEnhancedSystemPrompt(quickModeSystemPrompt, summary, memories, userCtx) messages := []*schema.Message{ schema.SystemMessage(systemPrompt), } @@ -129,10 +152,21 @@ func buildMessages(history []entity.ChatMessage, question string, retrieveResult return messages } -// buildEnhancedSystemPrompt 在基础 System Prompt 上注入摘要和记忆 -func buildEnhancedSystemPrompt(base string, summary *entity.ChatSummary, memories []entity.UserMemory) string { +// buildEnhancedSystemPrompt 在基础 System Prompt 上注入时间、用户信息、摘要和记忆 +func buildEnhancedSystemPrompt(base string, summary *entity.ChatSummary, memories []entity.UserMemory, userCtx UserContext) string { var extras []string + // 注入当前时间和用户上下文 + userInfo := "## 当前信息\n" + userInfo += "- 当前时间:" + userCtx.TimeStr + "\n" + if userCtx.Username != "" { + userInfo += "- 用户:" + userCtx.Username + "\n" + } + if userCtx.Role != "" { + userInfo += "- 角色:" + userCtx.Role + "\n" + } + extras = append(extras, userInfo) + if summary != nil && summary.Summary != "" { extras = append(extras, "## 本次对话摘要\n"+summary.Summary) } diff --git a/internal/service/context_interface.go b/internal/service/context_interface.go index 6906098..c10ea56 100644 --- a/internal/service/context_interface.go +++ b/internal/service/context_interface.go @@ -22,6 +22,7 @@ type EnhancedContext struct { History []entity.ChatMessage Summary *entity.ChatSummary Memories []entity.UserMemory + UserCtx UserContext HistoryBudget int // 实际使用的历史消息 token 预算 RetrievalBudget int // 实际使用的检索上下文 token 预算 } diff --git a/internal/service/intent_analyzer.go b/internal/service/intent_analyzer.go new file mode 100644 index 0000000..934b242 --- /dev/null +++ b/internal/service/intent_analyzer.go @@ -0,0 +1,187 @@ +package service + +import ( + "strings" + "unicode/utf8" +) + +// Intent 表示识别出的用户意图类型 +type Intent string + +const ( + // IntentQuestion 知识库问答类(默认) + IntentQuestion Intent = "question" + // IntentGreeting 问候类 + IntentGreeting Intent = "greeting" + // IntentIdentity 询问助手身份/模型 + IntentIdentity Intent = "identity" + // IntentMeta 询问能力、模式、帮助等元问题 + IntentMeta Intent = "meta" + // IntentChitchat 闲聊/结束语/致谢等 + IntentChitchat Intent = "chitchat" + // IntentListQuery 列表/元数据查询(快速模式无法完成) + IntentListQuery Intent = "list_query" +) + +// IntentResult 轻量意图分析结果 +type IntentResult struct { + Intent Intent + Confidence float64 + SkipRetrieval bool + Reason string +} + +// AnalyzeIntent 基于规则的轻量意图分析 +// 不调用 LLM,仅通过关键词和句式识别常见意图 +func AnalyzeIntent(query string) IntentResult { + q := strings.TrimSpace(query) + if q == "" { + return IntentResult{ + Intent: IntentQuestion, + Confidence: 1.0, + SkipRetrieval: false, + Reason: "空问题按问答处理", + } + } + + lower := strings.ToLower(q) + + // 1. 问候 + if isGreeting(lower) { + return IntentResult{ + Intent: IntentGreeting, + Confidence: 0.9, + SkipRetrieval: true, + Reason: "命中问候关键词", + } + } + + // 2. 身份/模型类 + if isIdentity(lower) { + return IntentResult{ + Intent: IntentIdentity, + Confidence: 0.95, + SkipRetrieval: true, + Reason: "询问助手身份或模型", + } + } + + // 3. 元问题(能力/帮助/模式) + if isMeta(lower) { + return IntentResult{ + Intent: IntentMeta, + Confidence: 0.85, + SkipRetrieval: true, + Reason: "询问能力、帮助或模式", + } + } + + // 4. 闲聊/结束语/致谢 + if isChitchat(lower) { + return IntentResult{ + Intent: IntentChitchat, + Confidence: 0.85, + SkipRetrieval: true, + Reason: "闲聊、致谢或结束语", + } + } + + // 5. 列表/元数据查询(快速模式建议切深度模式) + if isListQuery(lower) { + return IntentResult{ + Intent: IntentListQuery, + Confidence: 0.8, + SkipRetrieval: true, + Reason: "查询列表/元数据,快速模式无法完成", + } + } + + // 默认按问答处理 + return IntentResult{ + Intent: IntentQuestion, + Confidence: 0.7, + SkipRetrieval: false, + Reason: "未命中特殊意图,按知识库问答处理", + } +} + +func isGreeting(q string) bool { + greetings := []string{ + "你好", "您好", "嗨", "hello", "hi", "hey", + "早上好", "上午好", "中午好", "下午好", "晚上好", + "好久不见", "在吗", "在不在", "在么", + } + for _, g := range greetings { + if q == g || strings.HasPrefix(q, g) { + return true + } + } + return false +} + +func isIdentity(q string) bool { + patterns := []string{ + "你是谁", "你是什么", "你叫", "你是哪个", + "什么模型", "哪个模型", "你是gpt", "你是chatgpt", + "你是claude", "你是通义", "你是千问", "你是deepseek", + "你是kimi", "你是豆包", "who are you", "what are you", + "what model", "which model", "your name", + } + for _, p := range patterns { + if strings.Contains(q, p) { + return true + } + } + return false +} + +func isMeta(q string) bool { + patterns := []string{ + "你能做什么", "你能干嘛", "你会什么", "你有什么用", + "你能帮我", "你可以帮我", "你可以做什么", "你有什么能力", + "怎么使用", "如何使用", "使用说明", "使用帮助", + "帮助", "help", "怎么切换", "如何切换", + "深度模式", "快速模式", "切换模式", "smart-reasoning", + } + for _, p := range patterns { + if strings.Contains(q, p) { + return true + } + } + return false +} + +func isChitchat(q string) bool { + // 纯短句结束语/致谢 + phrases := []string{ + "谢谢", "感谢", "多谢", "谢了", + "再见", "拜拜", "拜", "bye", "goodbye", + "好的", "OK", "ok", "知道了", "明白了", "清楚了", + "辛苦了", "牛", "赞", "厉害", + } + for _, p := range phrases { + if q == p || strings.HasPrefix(q, p+" ") || strings.HasPrefix(q, p+"。") { + return true + } + } + // 只有一个表情或极短无意义内容 + if utf8.RuneCountInString(q) <= 2 && (strings.ContainsAny(q, "👍😊😀🙏🎉👋") || q == "。" || q == "。。") { + return true + } + return false +} + +func isListQuery(q string) bool { + patterns := []string{ + "有哪些知识库", "知识库有哪些", "有什么知识库", + "有哪些文档", "文档有哪些", "有什么文档", + "有哪些文件", "文件有哪些", "上传了哪些", + "列出", "列举", "列表", "清单", + } + for _, p := range patterns { + if strings.Contains(q, p) { + return true + } + } + return false +}