Files

219 lines
8.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}