From 719fd805e69c92ebfb3acecefcc47b170c7a9ebf Mon Sep 17 00:00:00 2001 From: fuzhongyun <15339891972@163.com> Date: Wed, 4 Feb 2026 11:20:11 +0800 Subject: [PATCH] =?UTF-8?q?fix:=201.=E8=B0=83=E6=95=B4=E5=8E=86=E5=8F=B2?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E7=94=A8=E6=B3=95=202.=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E5=B8=B8=E9=87=8F=203.=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E6=94=B9=E4=B8=8B=E7=9F=A5=E8=AF=86=E5=BA=93Query=E7=94=A8?= =?UTF-8?q?=E6=B3=95=204.=E5=A2=9E=E5=8A=A0=E5=8F=8B=E5=A5=BD=E8=BE=93?= =?UTF-8?q?=E5=87=BA=205.=E8=B0=83=E6=95=B4=E7=B3=BB=E7=BB=9F=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=E8=AF=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/biz/ding_talk_bot.go | 69 +++++++++++++++++----------------- internal/biz/do/handle.go | 59 ++++++++++++++++++++--------- internal/data/constants/bot.go | 8 ++++ 3 files changed, 84 insertions(+), 52 deletions(-) diff --git a/internal/biz/ding_talk_bot.go b/internal/biz/ding_talk_bot.go index 7f7a251..d009ade 100644 --- a/internal/biz/ding_talk_bot.go +++ b/internal/biz/ding_talk_bot.go @@ -161,11 +161,11 @@ func (d *DingTalkBotBiz) Do(ctx context.Context, requireData *entitys.RequireDat // handleSingleChat 单聊处理 // 先不接意图识别-仅提供问题处理 -func (d *DingTalkBotBiz) handleSingleChat(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) (err error) { +func (d *DingTalkBotBiz) handleSingleChat(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) error { // 1. 获取用户信息 user, err := d.botUserImpl.GetByStaffId(requireData.Req.SenderStaffId) if err != nil { - return + return err } requireData.ID = int32(user.UserID) requireData.UserInfo = &entitys.DingTalkUserInfo{ @@ -174,40 +174,24 @@ func (d *DingTalkBotBiz) handleSingleChat(ctx context.Context, requireData *enti Name: user.Name, } - // 2. 检查会话状态 (Redis) - // statusKey := fmt.Sprintf("ai_bot:session:status:%s", requireData.Req.SenderStaffId) - // status, _ := d.redisCli.Get(ctx, statusKey).Result() - - // if status == "WAITING_FOR_SYS_CONFIRM" { - // // 用户回复了系统名称 - // sysName := requireData.Req.Text.Content - // d.redisCli.Del(ctx, statusKey) - // return d.handleWithSpecificSys(ctx, requireData, sysName) - // } - - // 3. 获取历史记录 (最近6轮用户输入) - userHist, err := d.getRecentUserHistory(ctx, constants.ConversationTypeSingle, requireData.ID, 6) + // 2. 获取历史记录 (最近6轮用户输入) + userHist, err := d.getRecentUserHistory(ctx, constants.ConversationTypeSingle, requireData.ID, d.conf.Sys.SessionLen) if err != nil { return err } - // 4. 改写 Query (Query Rewriting) - rewrittenQuery, err := d.handle.RewriteQuery(ctx, userHist, requireData.Req.Text.Content) - var queryText = requireData.Req.Text.Content - if err == nil && rewrittenQuery != "" { - queryText = rewrittenQuery - } - - // 分类 - resolveResult, err := d.resolveSystemAndIssueType(ctx, requireData, queryText) + // 3. 系统&问题分类(意图识别阶段) + resolveResult, err := d.resolveSystemAndIssueType(ctx, requireData, userHist) if err != nil { return err } + log.Debugf("系统&分类结果: %s - %s", resolveResult.Sys.SysName, resolveResult.IssueType.Name) + // 4. 分类处理(后续考虑接各自的工作流/agent) switch resolveResult.IssueType.Code { - case "knowledge_qa": + case constants.IssueTypeKnowledgeQA: // 知识库问答 - return d.handleKnowledgeQA(ctx, requireData, queryText, resolveResult) + return d.handleKnowledgeQA(ctx, requireData, userHist, resolveResult) default: // 其他问题类型 // 系统为空,再次询问 if resolveResult.Sys.SysID == 0 { @@ -219,13 +203,21 @@ func (d *DingTalkBotBiz) handleSingleChat(ctx context.Context, requireData *enti } // 知识库问答 -func (d *DingTalkBotBiz) handleKnowledgeQA(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, queryText string, resolveResult *resolveSystemAndIssueTypeResult) error { +func (d *DingTalkBotBiz) handleKnowledgeQA(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, userHist []model.AiBotChatHi, resolveResult *resolveSystemAndIssueTypeResult) error { // 获取租户ID tenantId := constants.KnowledgeTenantIdDefault if resolveResult.Sys.KnowlegeTenantKey != "" { tenantId = resolveResult.Sys.KnowlegeTenantKey } + // 改写 Query (Query Rewriting) + rewrittenQuery, err := d.handle.RewriteQuery(ctx, userHist, requireData.Req.Text.Content) + var queryText = requireData.Req.Text.Content + if err == nil && rewrittenQuery != "" { + queryText = rewrittenQuery + } + log.Debugf("改写前后的Query: %s -> %s", requireData.Req.Text.Content, queryText) + // 获取知识库结果 isRetrieved, err := d.getKnowledgeAnswer(ctx, requireData, tenantId, queryText) if err != nil { @@ -276,7 +268,7 @@ type resolveSystemAndIssueTypeResult struct { } // 解析系统和问题类型 -func (d *DingTalkBotBiz) resolveSystemAndIssueType(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, queryText string) (*resolveSystemAndIssueTypeResult, error) { +func (d *DingTalkBotBiz) resolveSystemAndIssueType(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, userHist []model.AiBotChatHi) (*resolveSystemAndIssueTypeResult, error) { // 1. 获取所有系统和问题类型用于分类 allSys, err := d.sysImpl.FindAll() if err != nil { @@ -294,7 +286,7 @@ func (d *DingTalkBotBiz) resolveSystemAndIssueType(ctx context.Context, requireD }) // 2. LLM 分类 - classification, err := d.handle.ClassifyIssue(ctx, sysNames, issueTypeNames, requireData.Req.Text.Content) + classification, err := d.handle.ClassifyIssue(ctx, sysNames, issueTypeNames, requireData.Req.Text.Content, userHist) if err != nil { return nil, err } @@ -360,6 +352,8 @@ func (d *DingTalkBotBiz) getRecentUserHistory(ctx context.Context, conversationT // 在已知系统&问题类型的情况下进行分类并拉群 func (d *DingTalkBotBiz) fallbackToGroupCreation(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, resolveResult *resolveSystemAndIssueTypeResult) error { + entitys.ResText(requireData.Ch, "", fmt.Sprintf("\n检测到您想咨询 %s-%s 问题。", resolveResult.Sys.SysName, resolveResult.IssueType.Name)) + // 查找分配规则 rule, found, _ := d.issueImpl.IssueAssignRule.FindOne( d.issueImpl.WithSysID(resolveResult.Sys.SysID), @@ -371,7 +365,7 @@ func (d *DingTalkBotBiz) fallbackToGroupCreation(ctx context.Context, requireDat return nil } - var staffIds []string + var groupMember, groupMemberName []string if rule.ID != 0 { // 获取配置的用户 assignUsers, err := d.issueImpl.IssueAssignUser.FindAll(d.issueImpl.WithRuleID(rule.ID)) @@ -394,18 +388,23 @@ func (d *DingTalkBotBiz) fallbackToGroupCreation(ctx context.Context, requireDat return bu.UserID == au.UserID }) if found && botUser.StaffID != "" { - staffIds = append(staffIds, botUser.StaffID) + groupMember = append(groupMember, botUser.StaffID) + groupMemberName = append(groupMemberName, "@"+botUser.Name) } } } // 兜底处理人 - if len(staffIds) == 0 { - staffIds = []string{"17415698414368678"} + if len(groupMember) == 0 { + groupMember = []string{"17415698414368678"} } // 合并提问者 - staffIds = append([]string{requireData.Req.SenderStaffId}, staffIds...) + groupMember = append([]string{requireData.Req.SenderStaffId}, groupMember...) + groupMember = slice.Unique(groupMember) + + // 先回复用户 + entitys.ResText(requireData.Ch, "", fmt.Sprintf("\n已检索到处理人\n%s\n是否创建群聊?", strings.Join(groupMemberName, "、"))) // 发送确认卡片 groupName := fmt.Sprintf("[%s]-%s", resolveResult.IssueType.Name, resolveResult.Classification.Summary) @@ -413,7 +412,7 @@ func (d *DingTalkBotBiz) fallbackToGroupCreation(ctx context.Context, requireDat RobotCode: requireData.Req.RobotCode, ConversationId: requireData.Req.ConversationId, SenderStaffId: requireData.Req.SenderStaffId, - UserIds: staffIds, + UserIds: groupMember, GroupName: groupName, Summary: resolveResult.Classification.Summary, }) diff --git a/internal/biz/do/handle.go b/internal/biz/do/handle.go index 588f57c..3401f0c 100644 --- a/internal/biz/do/handle.go +++ b/internal/biz/do/handle.go @@ -131,27 +131,52 @@ type IssueClassification struct { } // ClassifyIssue 问题分类分析 -func (r *Handle) ClassifyIssue(ctx context.Context, systems []string, issueTypes []string, userInput string) (*IssueClassification, error) { +func (r *Handle) ClassifyIssue(ctx context.Context, systems []string, issueTypes []string, userInput string, userHist []model.AiBotChatHi) (*IssueClassification, error) { systemPrompt := fmt.Sprintf(`## 角色 -你是一个技术支持路由专家。输出必须是 JSON 格式。 -## 任务 -请分析用户的输入,并将其归类到最合适的系统和问题类型中。 -- 系统名称:必须是可用系统列表中的一个,若未提及可用系统关键词,则为"全局",不要臆想! -- 问题类型名称:必须是可用问题类型列表中的一个,若未提及可用问题类型关键词,则为空字符串 -- 问题简述:15字以内的问题简述(用于群聊命名) -- 分类判断理由:对系统名称和问题类型名称的判断理由 -## 背景与材料: -可用系统列表: [%s] -可用问题类型: [%s] -## 输出 -请仅以 JSON 格式回复,包含以下字段: -- sys_name: 系统名称 -- issue_type_name: 问题类型名称 -- summary: 问题简述 -- reason: 分类判断理由`, strings.Join(systems, ", "), strings.Join(issueTypes, ", ")) +你是一个技术支持路由专家。你的核心能力是通过深度语义分析和上下文回溯,将碎片化的用户输入通过时间先后拼接成完整的意图。输出必须是严格的 JSON 格式。 + +## 核心推理引擎(关键逻辑) + +请执行以下三步推理,不要只看当前这一句话: + +1. **构建完整意图**: + * 将“当前输入”与“历史对话”合并视为用户的完整诉求。 + * **特殊规则**:如果“当前输入”仅包含一个系统名称(例如用户只输入了“CRM”),这被视为**“上下文补充”**,而非新问题。此时,**必须保留最近历史对话中已识别的问题类型**。 + +2. **系统判定 (sys_name)** + * **策略**:覆盖式更新。 + * 如果当前输入提到了系统A,则 sys_name = 系统A(不管历史是什么)。 + * 如果当前输入未提系统,但历史有,则继承最近历史。 + * 如果都无,设为 "全局"。 + +3. **问题类型判定 (issue_type_name)** + * **策略**:回溯与推断。 + * **核心原则**:基于合并后的完整意图进行分析。推断出最近历史对话中的问题类型。 + * **严禁清空**:除非用户是在闲聊(如“你好”),否则绝不允许为空。如果当前句没提问题,但历史有,必须继承历史的 issue_type_name。 + +## 背景数据 +- 可用系统列表: [%s] +- 可用问题类型: [%s] + +## 输出格式 +{ + "sys_name": "系统名称", + "issue_type_name": "问题类型名称", + "summary": "15字内标题", + "reason": "1. 系统:说明来源(当前/历史/默认)。2. 问题类型:说明是基于哪句话推断的,或说明是继承了历史意图。" +}`, strings.Join(systems, ", "), strings.Join(issueTypes, ", ")) + + historyStr := strings.Builder{} + historyStr.WriteString("### 历史对话:\n") + for _, h := range userHist { + if h.Role == "user" { + historyStr.WriteString(fmt.Sprintf("%s:%s\n", h.CreateAt, h.Content)) + } + } messages := []api.Message{ {Role: "system", Content: systemPrompt}, + {Role: "assistant", Content: historyStr.String()}, {Role: "user", Content: userInput}, } diff --git a/internal/data/constants/bot.go b/internal/data/constants/bot.go index 8516626..7dba0fa 100644 --- a/internal/data/constants/bot.go +++ b/internal/data/constants/bot.go @@ -45,3 +45,11 @@ const ( PermissionTypeNone = 1 PermissionTypeDept = 2 ) + +// IssueType 问题类型 +const ( + IssueTypeKnowledgeQA = "knowledge_qa" // 知识问答 + IssueTypeUI = "ui" // UI需求 + IssueTypeBug = "bug" // Bug + IssueTypeDemand = "demand" // 开发需求 +)