From c9c9bca9cedbe573d0c7480640e89709bfdc6aff Mon Sep 17 00:00:00 2001 From: fuzhongyun <15339891972@163.com> Date: Wed, 4 Feb 2026 16:52:23 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=8B=86=E5=88=86=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E5=88=A4=E6=96=AD=E4=B8=8E=E9=97=AE=E9=A2=98=E5=88=86=E7=B1=BB?= =?UTF-8?q?=E5=88=A4=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/biz/ding_talk_bot.go | 15 ++++- internal/biz/do/handle.go | 103 ++++++++++++++++++++++++++-------- 2 files changed, 93 insertions(+), 25 deletions(-) diff --git a/internal/biz/ding_talk_bot.go b/internal/biz/ding_talk_bot.go index 5b53eca..0a74b22 100644 --- a/internal/biz/ding_talk_bot.go +++ b/internal/biz/ding_talk_bot.go @@ -286,10 +286,23 @@ func (d *DingTalkBotBiz) resolveSystemAndIssueType(ctx context.Context, requireD }) // 2. LLM 分类 - classification, err := d.handle.ClassifyIssue(ctx, sysNames, issueTypeNames, requireData.Req.Text.Content, userHist) + // 系统名称 + classificationSys, err := d.handle.ClassifyIssueSystem(ctx, sysNames, requireData.Req.Text.Content, userHist) if err != nil { return nil, err } + // 问题类型 + classificationIssueType, err := d.handle.ClassifyIssueType(ctx, issueTypeNames, requireData.Req.Text.Content, userHist) + if err != nil { + return nil, err + } + // 合并 + classification := &do.IssueClassification{ + SysName: classificationSys.SysName, + IssueTypeName: classificationIssueType.IssueTypeName, + Summary: classificationIssueType.Summary, + Reason: fmt.Sprintf("系统名称推断理由:%s\n问题类型推断理由:%s", classificationSys.Reason, classificationIssueType.Reason), + } // 3. 匹配系统 var sys model.AiSy diff --git a/internal/biz/do/handle.go b/internal/biz/do/handle.go index 3401f0c..4adda02 100644 --- a/internal/biz/do/handle.go +++ b/internal/biz/do/handle.go @@ -130,41 +130,96 @@ type IssueClassification struct { Reason string `json:"reason"` } -// ClassifyIssue 问题分类分析 -func (r *Handle) ClassifyIssue(ctx context.Context, systems []string, issueTypes []string, userInput string, userHist []model.AiBotChatHi) (*IssueClassification, error) { +// ClassifyIssueSys 问题系统分析 +func (r *Handle) ClassifyIssueSystem(ctx context.Context, systems []string, userInput string, userHist []model.AiBotChatHi) (*IssueClassification, error) { systemPrompt := fmt.Sprintf(`## 角色 -你是一个技术支持路由专家。你的核心能力是通过深度语义分析和上下文回溯,将碎片化的用户输入通过时间先后拼接成完整的意图。输出必须是严格的 JSON 格式。 +你是一个系统类型判定专家。你的唯一任务是基于多轮对话识别用户当前讨论的系统(sys_name)。不需要输出问题类型。输出必须严格遵守 JSON 格式。 -## 核心推理引擎(关键逻辑) +## 推理规则 -请执行以下三步推理,不要只看当前这一句话: +1. 系统判定逻辑: + - 当前输入明确提到系统 → 直接覆盖历史系统 + - 当前输入未提系统,但历史对话有 → 继承最近历史系统 + - 当前输入和历史均未出现 → "全局" -1. **构建完整意图**: - * 将“当前输入”与“历史对话”合并视为用户的完整诉求。 - * **特殊规则**:如果“当前输入”仅包含一个系统名称(例如用户只输入了“CRM”),这被视为**“上下文补充”**,而非新问题。此时,**必须保留最近历史对话中已识别的问题类型**。 - -2. **系统判定 (sys_name)** - * **策略**:覆盖式更新。 - * 如果当前输入提到了系统A,则 sys_name = 系统A(不管历史是什么)。 - * 如果当前输入未提系统,但历史有,则继承最近历史。 - * 如果都无,设为 "全局"。 - -3. **问题类型判定 (issue_type_name)** - * **策略**:回溯与推断。 - * **核心原则**:基于合并后的完整意图进行分析。推断出最近历史对话中的问题类型。 - * **严禁清空**:除非用户是在闲聊(如“你好”),否则绝不允许为空。如果当前句没提问题,但历史有,必须继承历史的 issue_type_name。 +2. 特殊规则: + - 如果当前输入仅包含系统名称(如“CRM”),视为系统上下文补充,仅更新 sys_name,不做其他推断 ## 背景数据 -- 可用系统列表: [%s] -- 可用问题类型: [%s] +可用系统列表:[%s] ## 输出格式 { "sys_name": "系统名称", + "reason": "说明系统来源:当前输入 / 历史继承 / 默认" +} +`, strings.Join(systems, ", ")) + + 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}, + } + + resp, err := r.Ollama.Chat(ctx, messages) + if err != nil { + return nil, err + } + + // 尝试清理 JSON 内容(有时模型会返回 markdown 块) + resp = strings.TrimPrefix(resp, "```json") + resp = strings.TrimSuffix(resp, "```") + resp = strings.TrimSpace(resp) + + var result IssueClassification + if err := json.Unmarshal([]byte(resp), &result); err != nil { + return nil, fmt.Errorf("解析分类结果失败: %w, 原文: %s", err, resp) + } + + return &result, nil +} + +// ClassifyIssueType 问题分类分析 +func (r *Handle) ClassifyIssueType(ctx context.Context, issueTypes []string, userInput string, userHist []model.AiBotChatHi) (*IssueClassification, error) { + systemPrompt := fmt.Sprintf(`## 角色 +你是一个业务问题类型分析专家。你的任务是基于多轮对话识别用户讨论的**问题类型(issue_type_name)**,问题类型必须严格来自可用问题类型列表 [%s]。 + +你不负责系统名称判断。输出必须严格遵守 JSON 格式。 + +## 推理规则 + +1. 构建完整问题意图 + - 将当前输入与历史对话合并理解为完整问题演进 + - 当前输入可能是补充条件、追问、修正或只给模块名/报错片段 + - 不要只看当前一句 + +2. 问题类型判定逻辑 + - 当前输入明确匹配列表中某个类型 → 使用该类型 + - 当前输入未明确,但历史已有 → 继承历史类型 + - 当前输入未匹配,历史也没有 → 选择最接近的列表类型(尽量匹配意图) + - 除非是闲聊(如“你好”“在吗”),禁止返回空值 + +3. 特殊规则 + - 当前输入只包含系统名/模块名/参数名 → 视为问题补充,继承历史 issue_type_name + - 输出必须严格匹配列表中的类型,不允许生成列表外的自造类型 + +## 背景数据 +可用问题类型列表:[%s] + +## 输出格式 +{ "issue_type_name": "问题类型名称", - "summary": "15字内标题", - "reason": "1. 系统:说明来源(当前/历史/默认)。2. 问题类型:说明是基于哪句话推断的,或说明是继承了历史意图。" -}`, strings.Join(systems, ", "), strings.Join(issueTypes, ", ")) + "summary": "15字内问题标题", + "reason": "说明问题类型是基于哪句话判断,或说明继承自历史,继承自哪条历史" +}`, strings.Join(issueTypes, ", "), strings.Join(issueTypes, ", ")) historyStr := strings.Builder{} historyStr.WriteString("### 历史对话:\n")