feat(auto-reply): 优化自动回复逻辑和知识库功能
- 将默认回复详细程度从"detailed"调整为"medium",前后端保持一致 - 新增话题切换检测逻辑,当用户主动要求换话题时提供引导回复 - 优化上下文处理机制,仅在指代型追问时注入历史对话,避免模型复读旧内容 - 改进知识库检索逻辑,区分自包含问题和指代型问题的上下文需求 - 完善知识库完整性指令,确保回复详细程度与知识展开程度一致 - 重构知识库重建逻辑,支持递归扫描子目录中的文件,修复索引为空的问题 - 增强素材匹配算法,引入强信号检测机制,避免仅凭模糊匹配误发素材 - 新增素材开场白AI生成功能,支持图片、视频、文档等类型智能描述 - 改进知识库重建通知,显示具体的文件数、分片数及失败统计信息
This commit is contained in:
@@ -46,7 +46,7 @@ func (e *AutoReplyEngine) getConfig() config.AutoReplyConfig {
|
||||
cfg.AI.MaxTokens = 700
|
||||
}
|
||||
if strings.TrimSpace(cfg.AI.ReplyDetail) == "" {
|
||||
cfg.AI.ReplyDetail = "detailed"
|
||||
cfg.AI.ReplyDetail = "medium"
|
||||
}
|
||||
if cfg.Knowledge.TopK <= 0 {
|
||||
cfg.Knowledge.TopK = 3
|
||||
@@ -69,8 +69,8 @@ func (e *AutoReplyEngine) askAI(question string, hits []KnowledgeChunk, msg auto
|
||||
return nil, fmt.Errorf("AI模型未配置")
|
||||
}
|
||||
systemPrompt := buildAutoReplySystemPrompt(cfg)
|
||||
msg.ContextText = e.recentContextPrompt(msg, 6)
|
||||
userPrompt := buildAutoReplyUserPrompt(question, hits, msg, cfg.ReplyPolicy.UnknownAnswerToken)
|
||||
msg.ContextText = e.contextPromptForQuestion(question, msg)
|
||||
userPrompt := buildAutoReplyUserPrompt(question, hits, msg, cfg.ReplyPolicy.UnknownAnswerToken, cfg)
|
||||
switch strings.ToLower(strings.TrimSpace(cfg.AI.Provider)) {
|
||||
case "local", "ollama":
|
||||
return callOllamaChat(cfg.AI, systemPrompt, userPrompt)
|
||||
@@ -88,7 +88,7 @@ func (e *AutoReplyEngine) askGeneralAI(question string, msg autoReplyMessage) (*
|
||||
return nil, fmt.Errorf("AI模型未配置")
|
||||
}
|
||||
systemPrompt := buildGeneralAutoReplySystemPrompt(cfg)
|
||||
msg.ContextText = e.recentContextPrompt(msg, 6)
|
||||
msg.ContextText = e.contextPromptForQuestion(question, msg)
|
||||
userPrompt := buildGeneralAutoReplyUserPrompt(question, msg)
|
||||
switch strings.ToLower(strings.TrimSpace(cfg.AI.Provider)) {
|
||||
case "local", "ollama":
|
||||
@@ -137,7 +137,7 @@ func buildAutoReplySystemPrompt(cfg config.AutoReplyConfig) string {
|
||||
if token == "" {
|
||||
token = "NO_ANSWER"
|
||||
}
|
||||
return prependAISystemPrompt(cfg, "你是企业微信客服。请基于提供的知识库片段,用自然亲切的语气回答客户问题。"+replyDetailInstruction(cfg)+"如果知识库里有详细内容,请完整展开说明,不要只列标题。知识库不足以确定答案时,只输出 "+token+"。不要编造政策、价格、承诺、库存或物流时效。客户要求人工、投诉、退款、合同、发票、赔偿或价格特殊审批时,也只输出 "+token+"。")
|
||||
return prependAISystemPrompt(cfg, "你是企业微信客服。请基于提供的知识库片段,用自然亲切的语气回答客户问题。"+replyDetailInstruction(cfg)+knowledgeCompletenessInstruction(cfg)+"知识库不足以确定答案时,只输出 "+token+"。不要编造政策、价格、承诺、库存或物流时效。客户要求人工、投诉、退款、合同、发票、赔偿或价格特殊审批时,也只输出 "+token+"。")
|
||||
}
|
||||
|
||||
func buildGeneralAutoReplySystemPrompt(cfg config.AutoReplyConfig) string {
|
||||
@@ -184,12 +184,26 @@ func replyDetailInstruction(cfg config.AutoReplyConfig) string {
|
||||
case "concise":
|
||||
return "回复简洁直接,1-2句话说清楚核心内容即可。"
|
||||
case "medium":
|
||||
return "回复适度详细,2-4句话,说明关键信息和注意事项。"
|
||||
return "回复适度详细,2-4句话,说明关键信息和注意事项,不要罗列大段条目。"
|
||||
default:
|
||||
return "回复详细充分,把知识库的相关内容完整说清楚,让客户能理解具体情况。语气要自然,像真人对话一样,不要用模板化的官方表达。"
|
||||
}
|
||||
}
|
||||
|
||||
// knowledgeCompletenessInstruction 控制"知识库片段要展开到多细"。
|
||||
// 这条指令必须与 replyDetailInstruction 一致,否则会出现"选了中等却仍写长文"的矛盾:
|
||||
// detailed 才要求完整展开;concise/medium 只挑与问题最相关的部分作答,避免又慢又被 max_tokens 截断。
|
||||
func knowledgeCompletenessInstruction(cfg config.AutoReplyConfig) string {
|
||||
switch strings.ToLower(strings.TrimSpace(cfg.AI.ReplyDetail)) {
|
||||
case "concise":
|
||||
return "只回答客户这一句问的内容,挑知识库里最相关的一点说清楚,不要把整段资料都搬出来。"
|
||||
case "medium":
|
||||
return "只针对客户当前的问题作答,从知识库里挑最相关的关键信息,不要把不相关的条目也一并列出。"
|
||||
default:
|
||||
return "如果知识库里有详细内容,请完整展开说明,不要只列标题。"
|
||||
}
|
||||
}
|
||||
|
||||
func effectiveReplyMaxTokens(cfg config.AIConfig) int {
|
||||
maxTokens := cfg.MaxTokens
|
||||
switch strings.ToLower(strings.TrimSpace(cfg.ReplyDetail)) {
|
||||
@@ -220,7 +234,7 @@ func buildGeneralAutoReplyUserPrompt(question string, msg autoReplyMessage) stri
|
||||
b.WriteString("\n客户问题:")
|
||||
b.WriteString(question)
|
||||
if contextText := strings.TrimSpace(msg.ContextText); contextText != "" {
|
||||
b.WriteString("\n\n最近对话上下文:\n")
|
||||
b.WriteString("\n\n最近对话上下文(仅供理解称呼和承接,请只回答“客户问题”那一句,不要主动延续之前的话题):\n")
|
||||
b.WriteString(contextText)
|
||||
}
|
||||
b.WriteString("\n请直接给客户一条友好、可发送的回复。")
|
||||
@@ -253,7 +267,7 @@ func buildNonTextAutoReplyUserPrompt(msg autoReplyMessage) string {
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func buildAutoReplyUserPrompt(question string, hits []KnowledgeChunk, msg autoReplyMessage, noAnswerToken string) string {
|
||||
func buildAutoReplyUserPrompt(question string, hits []KnowledgeChunk, msg autoReplyMessage, noAnswerToken string, cfg config.AutoReplyConfig) string {
|
||||
noAnswerToken = strings.TrimSpace(noAnswerToken)
|
||||
if noAnswerToken == "" {
|
||||
noAnswerToken = "NO_ANSWER"
|
||||
@@ -268,14 +282,16 @@ func buildAutoReplyUserPrompt(question string, hits []KnowledgeChunk, msg autoRe
|
||||
b.WriteString("\n客户问题:")
|
||||
b.WriteString(question)
|
||||
if contextText := strings.TrimSpace(msg.ContextText); contextText != "" {
|
||||
b.WriteString("\n\n最近对话上下文:\n")
|
||||
b.WriteString("\n\n最近对话上下文(仅供理解称呼和承接,请只回答“客户问题”那一句,不要主动延续之前的话题):\n")
|
||||
b.WriteString(contextText)
|
||||
}
|
||||
b.WriteString("\n\n知识库片段:\n")
|
||||
for i, hit := range compactKnowledgeHitsForAI(hits) {
|
||||
b.WriteString(fmt.Sprintf("[%d] 来源:%s 分数:%.3f\n%s\n\n", i+1, hit.Source, hit.Score, hit.Content))
|
||||
}
|
||||
b.WriteString("请基于上面的知识库片段回答客户问题。如果片段中有详细说明(比如具体步骤、标准、要求等),请完整地告诉客户,不要只列出标题。用自然的口语化表达,避免生硬的书面语。")
|
||||
b.WriteString("请基于上面的知识库片段回答客户问题。")
|
||||
b.WriteString(knowledgeCompletenessInstruction(cfg))
|
||||
b.WriteString("用自然的口语化表达,避免生硬的书面语。")
|
||||
if isGenericProductQuery(question) {
|
||||
b.WriteString("如果客户询问全部产品、产品线或产品总览,请根据片段中能确定的内容整理产品/产品线清单;只列能确定的产品,不要说“knowledge库”“根据知识库”“知识库内容无法确定具体产品”,不要输出空的 Markdown 列表或连续星号。")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user