package coordinator import ( "fmt" "regexp" "strings" "chat-agent-system/internal/ctxmgr" ) // RewriteResult 查询改写产物 type RewriteResult struct { Rewritten string `json:"rewritten"` // 补全后的完整独立问句(若无需改写则等于 rawInput) SearchQuery string `json:"search_query"` // 面向语义检索/KB优化的 Query(带槽位上下文) AppliedRules []string `json:"applied_rules"` // 命中的规则名(可解释、可观测) } var ( // R1 省略特征正则 ellipsisSuffixRe = regexp.MustCompile(`(?:呢|也|还是|怎么样|适用吗|如何|怎样|要多久)[??]*$`) ellipsisPrefixRe = regexp.MustCompile(`^(?:那|另外|还有|同时|那关于|再说|如果)\s*`) // R2 代词正则 pronounRe = regexp.MustCompile(`(这个项目|该项目|这项目|该文档|这份文档|那份报告|该报告|该器械|该产品|这个设备|这设备|上次说的|之前说的|它们|他们|它)`) // 标准法规提取正则 (复用模式) standardCodeRe = regexp.MustCompile(`(?i)(?:GB[/\s-T]*\d+(?:\.\d+)?(?:-\d{4})?|YY[/\s-T]*\d+(?:\.\d+)?(?:-\d{4})?|ISO[/\s-T]*\d+|IEC[/\s-T]*\d+|MDR|FDA|NMPA)`) // 常见谓语/疑问框架 commonPredicates = []string{ "适用吗", "适用的标准是什么", "怎么处理", "如何解决", "周期要多久", "预算是多少", "流程是怎样的", "要准备几台样机", "怎么准备", "要求是什么", "怎么规划", "需要满足什么规范", "怎么做", "符合要求吗", } ) // RewriteQuery 规则改写主入口 // 遵循确定性优先原则:零外部网络、零大模型时延,毫秒级就绪 func RewriteQuery( rawInput string, st *ctxmgr.ConversationTaskState, recent []DialogueTurn, proj ProjectInfo, ) RewriteResult { cleanInput := strings.TrimSpace(rawInput) if cleanInput == "" { return RewriteResult{Rewritten: "", SearchQuery: "", AppliedRules: nil} } appliedRules := make([]string, 0) rewritten := cleanInput anchor := "" if st != nil && st.TopicAnchor != "" { anchor = strings.TrimSpace(st.TopicAnchor) } // ------------------------------------------------------------- // 规则 R2: 代词消解 (Pronoun Resolution) // 例: "它的预算是多少" => "「数字化高频超声手术吸引系统」的预算是多少" // ------------------------------------------------------------- if pronounRe.MatchString(rewritten) { replacementEntity := resolvePronounEntity(rewritten, st, proj) if replacementEntity != "" { rewritten = pronounRe.ReplaceAllString(rewritten, replacementEntity) appliedRules = append(appliedRules, "pronoun_resolution") } } // ------------------------------------------------------------- // 规则 R1: 省略补全 (Ellipsis Completion) // 例: 上轮 "GB 9706.1 适用吗" -> 本轮 "YY 0505 呢" => "YY 0505 适用吗" // ------------------------------------------------------------- runes := []rune(cleanInput) isShort := len(runes) <= 16 hasEllipsisMarker := ellipsisSuffixRe.MatchString(cleanInput) || ellipsisPrefixRe.MatchString(cleanInput) isPureEntity := isLikelyPureEntity(cleanInput) isContinuation := strings.HasPrefix(cleanInput, "再") || strings.HasPrefix(cleanInput, "加上") || strings.HasPrefix(cleanInput, "还有") || strings.HasPrefix(cleanInput, "顺便") if anchor != "" && isShort && isContinuation && !strings.Contains(rewritten, anchor) { // ------------------------------------------------------------- // 规则 R3: 主题延续合并 (Topic Continuation) // 例: 上轮 "对比A和B的报价" -> 本轮 "再加上C呢" => "对比A和B的报价,再加上C呢" // ------------------------------------------------------------- rewritten = fmt.Sprintf("%s,%s", anchor, cleanInput) appliedRules = append(appliedRules, "topic_continuation") } else if anchor != "" && (isShort && (hasEllipsisMarker || isPureEntity)) && !strings.Contains(rewritten, anchor) { // ------------------------------------------------------------- // 规则 R1: 省略补全 (Ellipsis Completion) // 例: 上轮 "GB 9706.1 适用吗" -> 本轮 "YY 0505 呢" => "YY 0505 适用吗" // ------------------------------------------------------------- completed := completeEllipsis(cleanInput, anchor, st) if completed != "" && completed != cleanInput { rewritten = completed appliedRules = append(appliedRules, "ellipsis_completion") } } // ------------------------------------------------------------- // 规则 R4 & R5: 面向检索的增强 Query (SearchQuery) // ------------------------------------------------------------- searchQuery := rewritten if st != nil && len(st.CollectedSlots) > 0 { var slotParts []string if dev, ok := st.CollectedSlots["device_type"]; ok && dev != "" && !strings.Contains(searchQuery, dev) { slotParts = append(slotParts, "器械="+dev) } if mkt, ok := st.CollectedSlots["target_market"]; ok && mkt != "" && !strings.Contains(searchQuery, mkt) { slotParts = append(slotParts, "市场="+mkt) } if len(slotParts) > 0 { searchQuery = fmt.Sprintf("%s (%s)", searchQuery, strings.Join(slotParts, "; ")) appliedRules = append(appliedRules, "slot_context_completion") } } // 若未命中改写且句子过短,应用 R5 完整概念合成作为检索兜底 if len(appliedRules) == 0 && isShort && anchor != "" && !strings.Contains(cleanInput, anchor) { searchQuery = anchor + " " + cleanInput appliedRules = append(appliedRules, "concept_synthesis") } return RewriteResult{ Rewritten: rewritten, SearchQuery: searchQuery, AppliedRules: appliedRules, } } // resolvePronounEntity 寻找最匹配的代词指代实体 func resolvePronounEntity(query string, st *ctxmgr.ConversationTaskState, proj ProjectInfo) string { // 若代词偏向文档 if strings.Contains(query, "文档") || strings.Contains(query, "报告") { if st != nil && len(st.TopicEntities) > 0 { for _, e := range st.TopicEntities { if strings.Contains(e, "报告") || strings.Contains(e, "表") || strings.Contains(e, "书") { return fmt.Sprintf("「%s」", e) } } } } // 默认指代项目或器械产品实体 if proj.Name != "" { return fmt.Sprintf("「%s」", proj.Name) } if proj.DeviceType != "" { return fmt.Sprintf("「%s」", proj.DeviceType) } if st != nil && len(st.TopicEntities) > 0 { return fmt.Sprintf("「%s」", st.TopicEntities[0]) } return "项目" } // completeEllipsis 执行省略句与前序锚点的谓语框架合成 func completeEllipsis(cleanInput, anchor string, st *ctxmgr.ConversationTaskState) string { // 去除 "呢"、"?"、"那" 等修饰词得到核心实体 stripped := ellipsisSuffixRe.ReplaceAllString(cleanInput, "") stripped = ellipsisPrefixRe.ReplaceAllString(stripped, "") coreEntity := strings.TrimSpace(stripped) if coreEntity == "" { return "" } // 1. 标准强标编号替换:若锚点含强标号,且本轮输入也是强标号 anchorStandards := standardCodeRe.FindAllString(anchor, -1) inputStandards := standardCodeRe.FindAllString(coreEntity, -1) if len(anchorStandards) > 0 && len(inputStandards) > 0 { // 用新标准号替换锚点中的旧标准号 res := strings.Replace(anchor, anchorStandards[0], inputStandards[0], 1) return res } // 2. 谓语框架迁移:从锚点中提取疑问/谓语短语 for _, pred := range commonPredicates { if strings.Contains(anchor, pred) { return fmt.Sprintf("%s %s", coreEntity, pred) } } // 3. 通用合成:若锚点以问号结尾,提取疑问部分 if strings.HasSuffix(anchor, "?") || strings.HasSuffix(anchor, "?") { cleanAnchor := strings.TrimRight(anchor, "??") parts := strings.Split(cleanAnchor, " ") if len(parts) > 1 { predicatePart := parts[len(parts)-1] return fmt.Sprintf("%s %s?", coreEntity, predicatePart) } } return fmt.Sprintf("%s(关于%s)", anchor, coreEntity) } func isLikelyPureEntity(s string) bool { clean := strings.TrimSpace(s) // 标准编号本身是实体 if standardCodeRe.MatchString(clean) { return true } // 极短且无常见动词 if len([]rune(clean)) <= 8 && !strings.ContainsAny(clean, "是做有去看查改问") { return true } return false } func containsRule(rules []string, target string) bool { for _, r := range rules { if r == target { return true } } return false }