219 lines
8.2 KiB
Go
219 lines
8.2 KiB
Go
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
|
||
}
|