feat(auto-reply): 优化自动回复逻辑和知识库功能

- 将默认回复详细程度从"detailed"调整为"medium",前后端保持一致
- 新增话题切换检测逻辑,当用户主动要求换话题时提供引导回复
- 优化上下文处理机制,仅在指代型追问时注入历史对话,避免模型复读旧内容
- 改进知识库检索逻辑,区分自包含问题和指代型问题的上下文需求
- 完善知识库完整性指令,确保回复详细程度与知识展开程度一致
- 重构知识库重建逻辑,支持递归扫描子目录中的文件,修复索引为空的问题
- 增强素材匹配算法,引入强信号检测机制,避免仅凭模糊匹配误发素材
- 新增素材开场白AI生成功能,支持图片、视频、文档等类型智能描述
- 改进知识库重建通知,显示具体的文件数、分片数及失败统计信息
This commit is contained in:
2026-06-26 14:25:35 +08:00
parent 1517be2a25
commit 849090a627
12 changed files with 809 additions and 40 deletions

View File

@@ -149,6 +149,15 @@ func (e *AutoReplyEngine) previousUserQuestion(msg autoReplyMessage) string {
return ""
}
// contextPromptForQuestion 仅在当前问题是指代型追问时,才把历史对话注入 AI prompt。
// 自包含问题(含“换话题”)不带历史,避免模型顺着旧话题继续答、甚至逐字复读上一条。
func (e *AutoReplyEngine) contextPromptForQuestion(question string, msg autoReplyMessage) string {
if !questionReferencesContext(question) {
return ""
}
return e.recentContextPrompt(msg, 6)
}
func (e *AutoReplyEngine) recentContextPrompt(msg autoReplyMessage, maxEntries int) string {
entries := e.contextEntriesForMessage(msg)
if len(entries) == 0 {
@@ -184,14 +193,85 @@ func (e *AutoReplyEngine) recentContextPrompt(msg autoReplyMessage, maxEntries i
}
func (e *AutoReplyEngine) contextualSearchText(question string, msg autoReplyMessage) string {
contextText := e.recentContextPrompt(msg, 6)
question = strings.TrimSpace(question)
// 只有“指代型追问”(如“它多少钱”“刚才那个再说说”)才需要把历史对话拼进检索 query。
// 自包含问题(如“今天星期几”“换个话题”)一旦带上历史,会把上一个话题的知识高分召回,
// 导致顺着旧话题继续答,甚至在 temperature=0 时逐字复读上一条回复。
if !questionReferencesContext(question) {
return question
}
contextText := e.recentContextPrompt(msg, 6)
if contextText == "" {
return question
}
return contextText + "\n当前问题" + question
}
// topicSwitchPhrases 是“客户主动要求换话题”的常见说法(短语本身不含新话题)。
var topicSwitchPhrases = []string{
"换个话题", "换一个话题", "换个问题", "换一个问题", "换话题", "换个方向",
"聊点别的", "说点别的", "聊别的", "说别的", "不聊这个", "不说这个", "别聊这个", "别说这个",
}
// strongAnaphoraTokens 基本只在“追问上文”时出现,命中即视为指代型问题。
var strongAnaphoraTokens = []string{
"它", "它们", "这个", "那个", "这款", "那款", "这种", "那种", "这台", "那台",
"上面", "刚才", "刚刚", "接着", "继续", "还有没有", "还有别的", "展开说",
"详细说", "多说点", "再说说", "具体点", "上一个", "上一条", "前面说", "之前说", "你说的",
}
// weakAnaphoraTokens 是较弱的指代词,仅在很短的问句里才视为追问,
// 避免“今天他要不要来”这类自带主语的完整问题被误判为指代。
var weakAnaphoraTokens = []string{"这", "那", "它", "这是", "那是", "这些", "那些"}
// questionReferencesContext 判断当前问题是否依赖上文(指代型追问)。
func questionReferencesContext(question string) bool {
text := normalizeGreetingText(question)
if text == "" {
return false
}
for _, token := range strongAnaphoraTokens {
if strings.Contains(text, normalizeGreetingText(token)) {
return true
}
}
if len([]rune(text)) <= 8 {
for _, token := range weakAnaphoraTokens {
if strings.Contains(text, normalizeGreetingText(token)) {
return true
}
}
}
return false
}
// isPureTopicSwitchMessage 判断消息是否“只是要求换话题”而没有带上新的问题。
// 这类消息应给一句干净的引导,不能把上一个话题的知识或回复拖过来。
func isPureTopicSwitchMessage(content string) bool {
text := normalizeGreetingText(content)
if text == "" {
return false
}
matched := false
for _, phrase := range topicSwitchPhrases {
normalized := normalizeGreetingText(phrase)
if strings.Contains(text, normalized) {
text = strings.ReplaceAll(text, normalized, "")
matched = true
}
}
if !matched {
return false
}
// 去掉切换短语和常见语气词后,几乎没有剩余内容,才算“纯换话题”。
text = strings.Trim(text, "吧把了啊呀呢嘛吗好的我们咱们来")
return len([]rune(text)) <= 2
}
func topicSwitchGuidanceAnswer() string {
return "好的,那咱们聊点别的。您还想了解些什么,直接发我就行。"
}
func (e *AutoReplyEngine) contextEntriesForMessage(msg autoReplyMessage) []autoReplyContextEntry {
key := e.contextKeyForMessage(msg)
e.mu.Lock()