fix:1.调整钉钉单聊问题路由负责人整体逻辑 2.增加机器人中间回复 3. 单元测试,提示词调整

This commit is contained in:
fuzhongyun 2026-02-03 18:50:51 +08:00
parent c1971e71c1
commit c174ab683a
4 changed files with 155 additions and 130 deletions

View File

@ -8,6 +8,7 @@ import (
"ai_scheduler/internal/data/constants" "ai_scheduler/internal/data/constants"
"ai_scheduler/internal/data/impl" "ai_scheduler/internal/data/impl"
"ai_scheduler/internal/data/model" "ai_scheduler/internal/data/model"
"ai_scheduler/internal/domain/tools/common/knowledge_base"
"ai_scheduler/internal/entitys" "ai_scheduler/internal/entitys"
"ai_scheduler/internal/pkg" "ai_scheduler/internal/pkg"
"ai_scheduler/internal/tools" "ai_scheduler/internal/tools"
@ -162,22 +163,27 @@ func (d *DingTalkBotBiz) Do(ctx context.Context, requireData *entitys.RequireDat
// 先不接意图识别-仅提供问题处理 // 先不接意图识别-仅提供问题处理
func (d *DingTalkBotBiz) handleSingleChat(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) (err error) { func (d *DingTalkBotBiz) handleSingleChat(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) (err error) {
// 1. 获取用户信息 // 1. 获取用户信息
requireData.UserInfo, err = d.dingTalkUser.GetUserInfoFromBot(ctx, requireData.Req.SenderStaffId, dingtalk.WithId(1)) user, err := d.botUserImpl.GetByStaffId(requireData.Req.SenderStaffId)
if err != nil { if err != nil {
return return
} }
requireData.ID = int32(requireData.UserInfo.UserId) requireData.ID = int32(user.UserID)
requireData.UserInfo = &entitys.DingTalkUserInfo{
UserId: int(user.UserID),
StaffId: user.StaffID,
Name: user.Name,
}
// 2. 检查会话状态 (Redis) // 2. 检查会话状态 (Redis)
statusKey := fmt.Sprintf("ai_bot:session:status:%s", requireData.Req.SenderStaffId) // statusKey := fmt.Sprintf("ai_bot:session:status:%s", requireData.Req.SenderStaffId)
status, _ := d.redisCli.Get(ctx, statusKey).Result() // status, _ := d.redisCli.Get(ctx, statusKey).Result()
if status == "WAITING_FOR_SYS_CONFIRM" { // if status == "WAITING_FOR_SYS_CONFIRM" {
// 用户回复了系统名称 // // 用户回复了系统名称
sysName := requireData.Req.Text.Content // sysName := requireData.Req.Text.Content
d.redisCli.Del(ctx, statusKey) // d.redisCli.Del(ctx, statusKey)
return d.handleWithSpecificSys(ctx, requireData, sysName) // return d.handleWithSpecificSys(ctx, requireData, sysName)
} // }
// 3. 获取历史记录 (最近6轮用户输入) // 3. 获取历史记录 (最近6轮用户输入)
userHist, err := d.getRecentUserHistory(ctx, constants.ConversationTypeSingle, requireData.ID, 6) userHist, err := d.getRecentUserHistory(ctx, constants.ConversationTypeSingle, requireData.ID, 6)
@ -192,76 +198,96 @@ func (d *DingTalkBotBiz) handleSingleChat(ctx context.Context, requireData *enti
queryText = rewrittenQuery queryText = rewrittenQuery
} }
// 构造识别对象 // 分类
rec := &entitys.Recognize{ resolveResult, err := d.resolveSystemAndIssueType(ctx, requireData, queryText)
Ch: requireData.Ch,
SystemPrompt: d.defaultPrompt(),
UserContent: &entitys.RecognizeUserContent{
Text: queryText,
},
}
// 5. 调用知识库
isRetrieved, err := d.groupConfigBiz.handleKnowledge(ctx, rec, nil, requireData.Req)
if err != nil { if err != nil {
return err return err
} }
switch resolveResult.IssueType.Code {
case "knowledge_qa":
// 知识库问答
return d.handleKnowledgeQA(ctx, requireData, queryText, resolveResult)
default: // 其他问题类型
// 系统为空,再次询问
if resolveResult.Sys.SysID == 0 {
entitys.ResText(requireData.Ch, "", "\n抱歉我无法确定您咨询的是哪个系统。请告诉我具体系统名称直连天下系统、货易通系统以便我为您准确解答或安排对应的技术支持。")
return nil
}
return d.fallbackToGroupCreation(ctx, requireData, resolveResult)
}
}
// 知识库问答
func (d *DingTalkBotBiz) handleKnowledgeQA(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, queryText string, resolveResult *resolveSystemAndIssueTypeResult) error {
// 获取租户ID
tenantId := constants.KnowledgeTenantIdDefault
if resolveResult.Sys.KnowlegeTenantKey != "" {
tenantId = resolveResult.Sys.KnowlegeTenantKey
}
// 获取知识库结果
isRetrieved, err := d.getKnowledgeAnswer(ctx, requireData, tenantId, queryText)
if err != nil {
return err
}
if isRetrieved { if isRetrieved {
return nil return nil
} }
// 6. Fallback: 分类 -> 规则 -> 拉群 // 未匹配&全局 -> 明确具体系统
return d.fallbackToGroupCreation(ctx, requireData) if !isRetrieved && resolveResult.Sys.SysID == 0 {
} entitys.ResText(requireData.Ch, "", "\n抱歉知识库未命中无法回答您的问题。\n若您的问题是某一具体系统的请告诉我具体系统名称直连天下系统、货易通系统以便我为您准确解答。")
// handleWithSpecificSys 处理用户明确指定的系统
func (d *DingTalkBotBiz) handleWithSpecificSys(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, sysName string) error {
// 1. 查找系统
var sys model.AiSy
cond := builder.NewCond().And(builder.Eq{"sys_name": sysName})
err := d.sysImpl.GetOneBySearchToStrut(&cond, &sys)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
entitys.ResText(requireData.Ch, "", "抱歉,我还是没有找到名为“"+sysName+"”的系统。请联系管理员确认系统名称。")
return nil return nil
} }
return err // 未匹配&指定系统 -> 拉群卡片
if !isRetrieved && resolveResult.Sys.SysID != 0 {
entitys.ResText(requireData.Ch, "", fmt.Sprintf("\n抱歉%s知识库未命中无法回答您的问题。即将为您创建群聊解答。", resolveResult.Sys.SysName))
return d.fallbackToGroupCreation(ctx, requireData, resolveResult)
} }
// 2. 既然已经明确了系统直接尝试拉群这里假设问题类型为“其他”或由LLM再次分析 return nil
// 为简化,这里再次调用分类逻辑,但带上已确定的系统
return d.fallbackToGroupCreationWithSys(ctx, requireData, &sys)
} }
// getRecentUserHistory 获取最近的用户输入历史 // 获取知识库问答结果
func (d *DingTalkBotBiz) getRecentUserHistory(ctx context.Context, conversationType constants.ConversationType, id int32, limit int) ([]model.AiBotChatHi, error) { func (d *DingTalkBotBiz) getKnowledgeAnswer(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, tenantId string, queryText string) (bool, error) {
var his []model.AiBotChatHi // 请求知识库工具
cond := builder.NewCond(). knowledgeBase := knowledge_base.New(d.conf.KnowledgeConfig)
And(builder.Eq{"his_type": conversationType}). knowledgeResp, err := knowledgeBase.Query(&knowledge_base.QueryRequest{
And(builder.Eq{"id": id}). TenantID: tenantId, // 后续动态接参
And(builder.Eq{"role": "user"}) Query: queryText,
Mode: constants.KnowledgeModeMix,
_, err := d.chatHis.GetListToStruct(&cond, &dataTemp.ReqPageBo{Limit: limit}, &his, "his_id desc") Stream: true,
Think: false,
OnlyRAG: true,
})
if err != nil { if err != nil {
return nil, err return false, fmt.Errorf("请求知识库工具失败err: %v", err)
}
return his, nil
} }
// fallbackToGroupCreation 分类并拉群 // 读取知识库SSE数据
func (d *DingTalkBotBiz) fallbackToGroupCreation(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) error { return d.groupConfigBiz.readKnowledgeSSE(knowledgeResp, requireData.Ch, true)
}
type resolveSystemAndIssueTypeResult struct {
Sys model.AiSy
IssueType model.AiIssueType
Classification *do.IssueClassification
}
// 解析系统和问题类型
func (d *DingTalkBotBiz) resolveSystemAndIssueType(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, queryText string) (*resolveSystemAndIssueTypeResult, error) {
// 1. 获取所有系统和问题类型用于分类 // 1. 获取所有系统和问题类型用于分类
allSys, err := d.sysImpl.FindAll() allSys, err := d.sysImpl.FindAll()
if err != nil { if err != nil {
return err return nil, err
} }
sysNames := slice.Map(allSys, func(_ int, sys model.AiSy) string { sysNames := slice.Map(allSys, func(_ int, sys model.AiSy) string {
return sys.SysName return sys.SysName
}) })
allIssueTypes, err := d.issueImpl.IssueType.FindAll() allIssueTypes, err := d.issueImpl.IssueType.FindAll()
if err != nil { if err != nil {
return err return nil, err
} }
issueTypeNames := slice.Map(allIssueTypes, func(_ int, it model.AiIssueType) string { issueTypeNames := slice.Map(allIssueTypes, func(_ int, it model.AiIssueType) string {
return it.Name return it.Name
@ -270,8 +296,7 @@ func (d *DingTalkBotBiz) fallbackToGroupCreation(ctx context.Context, requireDat
// 2. LLM 分类 // 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)
if err != nil { if err != nil {
// 分类失败,使用兜底 return nil, err
return d.createDefaultGroup(ctx, requireData, "系统无法识别")
} }
// 3. 匹配系统 // 3. 匹配系统
@ -292,70 +317,60 @@ func (d *DingTalkBotBiz) fallbackToGroupCreation(ctx context.Context, requireDat
} }
} }
if sys.SysID == 0 { return &resolveSystemAndIssueTypeResult{
Sys: sys,
IssueType: issueType,
Classification: classification,
}, nil
}
// 判断全局是否存在该规则 // handleWithSpecificSys 处理用户明确指定的系统
_, found, _ := d.issueImpl.IssueAssignRule.FindOne( // func (d *DingTalkBotBiz) handleWithSpecificSys(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, sysName string) error {
d.issueImpl.WithSysID(0), // // 1. 查找系统
d.issueImpl.WithIssueTypeID(issueType.ID), // var sys model.AiSy
// cond := builder.NewCond().And(builder.Eq{"sys_name": sysName})
// err := d.sysImpl.GetOneBySearchToStrut(&cond, &sys)
// if err != nil {
// if errors.Is(err, sql.ErrNoRows) {
// entitys.ResText(requireData.Ch, "", "抱歉,我还是没有找到名为“"+sysName+"”的系统。请联系管理员确认系统名称。")
// return nil
// }
// return err
// }
// // 2. 既然已经明确了系统直接尝试拉群这里假设问题类型为“其他”或由LLM再次分析
// // 为简化,这里再次调用分类逻辑,但带上已确定的系统
// return d.fallbackToGroupCreationWithSys(ctx, requireData, &sys)
// }
// getRecentUserHistory 获取最近的用户输入历史
func (d *DingTalkBotBiz) getRecentUserHistory(ctx context.Context, conversationType constants.ConversationType, id int32, limit int) ([]model.AiBotChatHi, error) {
var his []model.AiBotChatHi
cond := builder.NewCond().
And(builder.Eq{"his_type": conversationType}).
And(builder.Eq{"id": id}).
And(builder.Eq{"role": "user"})
_, err := d.chatHis.GetListToStruct(&cond, &dataTemp.ReqPageBo{Limit: limit}, &his, "his_id desc")
if err != nil {
return nil, err
}
return his, nil
}
// 在已知系统&问题类型的情况下进行分类并拉群
func (d *DingTalkBotBiz) fallbackToGroupCreation(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, resolveResult *resolveSystemAndIssueTypeResult) error {
// 查找分配规则
rule, found, _ := d.issueImpl.IssueAssignRule.FindOne(
d.issueImpl.WithSysID(resolveResult.Sys.SysID),
d.issueImpl.WithIssueTypeID(resolveResult.IssueType.ID),
d.issueImpl.WithStatus(1), d.issueImpl.WithStatus(1),
) )
if !found { if !found {
// 无法明确系统,且全局无该能力,询问用户 entitys.ResText(requireData.Ch, "", fmt.Sprintf("\n抱歉当前系统未配置路由规则 %s-%s请联系管理员配置。", resolveResult.Sys.SysName, resolveResult.IssueType.Name))
statusKey := fmt.Sprintf("ai_bot:session:status:%s", requireData.Req.SenderStaffId)
d.redisCli.Set(ctx, statusKey, "WAITING_FOR_SYS_CONFIRM", time.Hour)
entitys.ResText(requireData.Ch, "", "抱歉,我无法确定您咨询的是哪个系统。请告诉我具体系统名称(如:直连天下系统、货易通系统),以便我为您安排对应的技术支持。")
return nil return nil
} }
}
return d.fallbackToGroupCreationWithSys(ctx, requireData, &sys)
}
// fallbackToGroupCreationWithSys 在已知系统的情况下进行分类并拉群
func (d *DingTalkBotBiz) fallbackToGroupCreationWithSys(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, sys *model.AiSy) error {
// 1. 获取所有问题类型
allIssueTypes, err := d.issueImpl.IssueType.FindAll()
if err != nil {
return err
}
issueTypeNames := slice.Map(allIssueTypes, func(_ int, it model.AiIssueType) string {
return it.Name
})
// 2. LLM 再次分类(确定问题类型和简述)
classification, err := d.handle.ClassifyIssue(ctx, []string{sys.SysName}, issueTypeNames, requireData.Req.Text.Content)
if err != nil {
return d.createDefaultGroup(ctx, requireData, "问题类型识别失败")
}
issueType, found, err := d.issueImpl.IssueType.FindOne(d.issueImpl.WithName(classification.IssueTypeName))
if !found {
log.Errorf("issue type %s not found; err: %v", classification.IssueTypeName, err)
return fmt.Errorf("问题类型 %s 不存在", classification.IssueTypeName)
}
// 3. 查找分配规则
rule, found, err := d.issueImpl.IssueAssignRule.FindOne(
d.issueImpl.WithSysID(sys.SysID),
d.issueImpl.WithIssueTypeID(issueType.ID),
d.issueImpl.WithStatus(1),
)
if !found {
// 创建默认分配规则 - 暂不考虑并发,有唯一索引
rule = model.AiIssueAssignRule{
SysID: sys.SysID,
IssueTypeID: issueType.ID,
Status: 1,
}
if err := d.issueImpl.IssueAssignRule.Create(&rule); err != nil {
log.Errorf("create assign rule for sys %s and issue type %s failed; err: %v", sys.SysName, issueType.Name, err)
return fmt.Errorf("创建分配规则 %s-%s 失败", sys.SysName, issueType.Name)
} else {
log.Infof("create assign rule for sys %s and issue type %s success; rule id: %d", sys.SysName, issueType.Name, rule.ID)
}
}
var staffIds []string var staffIds []string
if rule.ID != 0 { if rule.ID != 0 {
// 获取配置的用户 // 获取配置的用户
@ -392,15 +407,15 @@ func (d *DingTalkBotBiz) fallbackToGroupCreationWithSys(ctx context.Context, req
// 合并提问者 // 合并提问者
staffIds = append([]string{requireData.Req.SenderStaffId}, staffIds...) staffIds = append([]string{requireData.Req.SenderStaffId}, staffIds...)
// 4. 发送确认卡片 // 发送确认卡片
groupName := fmt.Sprintf("[%s]-%s", classification.IssueTypeName, classification.Summary) groupName := fmt.Sprintf("[%s]-%s", resolveResult.IssueType.Name, resolveResult.Classification.Summary)
return d.SendGroupCreationConfirmCard(ctx, &SendGroupCreationConfirmCardParams{ return d.SendGroupCreationConfirmCard(ctx, &SendGroupCreationConfirmCardParams{
RobotCode: requireData.Req.RobotCode, RobotCode: requireData.Req.RobotCode,
ConversationId: requireData.Req.ConversationId, ConversationId: requireData.Req.ConversationId,
SenderStaffId: requireData.Req.SenderStaffId, SenderStaffId: requireData.Req.SenderStaffId,
UserIds: staffIds, UserIds: staffIds,
GroupName: groupName, GroupName: groupName,
Summary: classification.Summary, Summary: resolveResult.Classification.Summary,
}) })
} }
@ -759,7 +774,7 @@ func (d *DingTalkBotBiz) SendGroupCreationConfirmCard(ctx context.Context, param
} }
// 构建卡片 OutTrackId // 构建卡片 OutTrackId
outTrackId := constants.BuildCardOutTrackId(params.ConversationId, params.RobotCode) outTrackId := constants.BuildCardOutTrackId(params.SenderStaffId, params.RobotCode)
// 准备可见人员列表 // 准备可见人员列表
var recipients []*string var recipients []*string
@ -804,13 +819,13 @@ func (d *DingTalkBotBiz) SendGroupCreationConfirmCard(ctx context.Context, param
"target_user_ids": tea.String(strings.Join(params.UserIds, ",")), "target_user_ids": tea.String(strings.Join(params.UserIds, ",")),
}, },
}, },
ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{ OpenSpaceId: tea.String("dtv1.card//IM_ROBOT." + params.SenderStaffId),
SupportForward: tea.Bool(false), ImRobotOpenDeliverModel: &card_1_0.CreateAndDeliverRequestImRobotOpenDeliverModel{
}, SpaceType: tea.String("IM_ROBOT"),
OpenSpaceId: tea.String("dtv1.card//im_group." + params.ConversationId),
ImGroupOpenDeliverModel: &card_1_0.CreateAndDeliverRequestImGroupOpenDeliverModel{
RobotCode: tea.String(params.RobotCode), RobotCode: tea.String(params.RobotCode),
Recipients: recipients, },
ImRobotOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImRobotOpenSpaceModel{
SupportForward: tea.Bool(false),
}, },
}) })
return err return err

View File

@ -132,15 +132,23 @@ type IssueClassification struct {
// ClassifyIssue 问题分类分析 // 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) (*IssueClassification, error) {
systemPrompt := fmt.Sprintf(`你是一个技术支持路由专家请分析用户的输入并将其归类到最合适的系统和问题类型中 systemPrompt := fmt.Sprintf(`## 角色
你是一个技术支持路由专家输出必须是 JSON 格式
## 任务
请分析用户的输入并将其归类到最合适的系统和问题类型中
- 系统名称必须是可用系统列表中的一个若未提及可用系统关键词则为"全局"不要臆想
- 问题类型名称必须是可用问题类型列表中的一个若未提及可用问题类型关键词则为空字符串
- 问题简述15字以内的问题简述用于群聊命名
- 分类判断理由对系统名称和问题类型名称的判断理由
## 背景与材料
可用系统列表: [%s] 可用系统列表: [%s]
可用问题类型: [%s] 可用问题类型: [%s]
## 输出
请仅以 JSON 格式回复包含以下字段 请仅以 JSON 格式回复包含以下字段
- sys_name: 系统名称若未提及系统关键词则为"全局" - sys_name: 系统名称
- issue_type_name: 问题类型名称 - issue_type_name: 问题类型名称
- summary: 15字以内的问题简述用于群命名 - summary: 问题简述
- reason: 分类判断理由系统名称判断理由`, strings.Join(systems, ", "), strings.Join(issueTypes, ", ")) - reason: 分类判断理由`, strings.Join(systems, ", "), strings.Join(issueTypes, ", "))
messages := []api.Message{ messages := []api.Message{
{Role: "system", Content: systemPrompt}, {Role: "system", Content: systemPrompt},

View File

@ -542,7 +542,7 @@ func (g *GroupConfigBiz) readKnowledgeSSE(resp io.ReadCloser, channel chan entit
// 知识库未命中 输出提示后中断 // 知识库未命中 输出提示后中断
if delta.XRagStatus == constants.KnowledgeRagStatusMiss { if delta.XRagStatus == constants.KnowledgeRagStatusMiss {
var missContent string = "知识库未检测到匹配信息,即将为您创建群聊解决问题。" var missContent string = "知识库未检测到匹配信息。"
entitys.ResStream(channel, "", missContent) entitys.ResStream(channel, "", missContent)
return false, nil return false, nil
} }

View File

@ -31,8 +31,10 @@ func (k BotUserImpl) GetByStaffId(staffId string) (*model.AiBotUser, error) {
func (k BotUserImpl) GetByUserIds(userIds []int32) ([]model.AiBotUser, error) { func (k BotUserImpl) GetByUserIds(userIds []int32) ([]model.AiBotUser, error) {
var data []model.AiBotUser var data []model.AiBotUser
cond := builder.NewCond() cond := builder.NewCond()
cond = cond.And(builder.In("user_id", userIds)) for _, userId := range userIds {
_, err := k.GetListToStruct2(&cond, nil, &data) cond = cond.Or(builder.Eq{"user_id": userId})
}
_, err := k.GetListToStruct(&cond, nil, &data, "user_id")
return data, err return data, err
} }