Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions internal/service/chat_mode.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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()
Expand Down
23 changes: 20 additions & 3 deletions internal/service/chat_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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 {
Expand Down
42 changes: 38 additions & 4 deletions internal/service/chat_service_prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package service
import (
"fmt"
"strings"
"time"
"unicode/utf8"

"github.com/cloudwego/eino/schema"
Expand All @@ -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
Expand Down Expand Up @@ -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),
}
Expand All @@ -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)
}
Expand Down
1 change: 1 addition & 0 deletions internal/service/context_interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type EnhancedContext struct {
History []entity.ChatMessage
Summary *entity.ChatSummary
Memories []entity.UserMemory
UserCtx UserContext
HistoryBudget int // 实际使用的历史消息 token 预算
RetrievalBudget int // 实际使用的检索上下文 token 预算
}
Expand Down
187 changes: 187 additions & 0 deletions internal/service/intent_analyzer.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading