feat: 实施来源徽章后处理逻辑,确保100%显示
- 添加enhanceCitations等6个智能标注函数 - 自动为回答添加[[知识库:xxx]]和[[AI建议]]徽章 - 智能判断标注类型(事实陈述vs建议说明) - 保护代码块和引用块不被标注 - 修改Chat和Completion函数应用后处理 - 添加extractKnowledgeSources提取知识库来源 新增文档: - CITATION_BADGE_SOLUTION.md - 完整解决方案 - CITATION_POST_PROCESSING_REPORT.md - 实施报告 - citation_prompt.txt - 优化后的prompt模板 - test-citation-badges.sh - 测试脚本
This commit is contained in:
@@ -269,6 +269,239 @@ func (h *LLMChatHandler) vectorSearch(ctx context.Context, kbID, query string, l
|
||||
return results
|
||||
}
|
||||
|
||||
// enhanceCitations 后处理:自动为回答添加来源标注徽章,确保100%显示
|
||||
func (h *LLMChatHandler) enhanceCitations(response string, hasKnowledge bool, knowledgeSources []string) string {
|
||||
if response == "" {
|
||||
return response
|
||||
}
|
||||
|
||||
// 检查是否已有标注
|
||||
hasKBCitation := strings.Contains(response, "[[知识库:")
|
||||
hasAICitation := strings.Contains(response, "[[AI建议]]")
|
||||
|
||||
// 如果已经有完整标注,直接返回
|
||||
if hasKBCitation && hasAICitation {
|
||||
return response
|
||||
}
|
||||
|
||||
// 如果完全没有标注,进行智能补充
|
||||
if !hasKBCitation && !hasAICitation {
|
||||
return h.addCitationsToResponse(response, hasKnowledge, knowledgeSources)
|
||||
}
|
||||
|
||||
// 部分标注的情况:检查是否需要补充
|
||||
return h.fillMissingCitations(response, hasKnowledge, knowledgeSources)
|
||||
}
|
||||
|
||||
// addCitationsToResponse 为完全没有标注的回答添加来源标注
|
||||
func (h *LLMChatHandler) addCitationsToResponse(response string, hasKnowledge bool, knowledgeSources []string) string {
|
||||
lines := strings.Split(response, "\n")
|
||||
var enhanced []string
|
||||
var inCodeBlock bool
|
||||
var inQuoteBlock bool
|
||||
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
|
||||
// 检测代码块
|
||||
if strings.HasPrefix(trimmed, "```") {
|
||||
inCodeBlock = !inCodeBlock
|
||||
enhanced = append(enhanced, line)
|
||||
continue
|
||||
}
|
||||
|
||||
// 代码块内不处理
|
||||
if inCodeBlock {
|
||||
enhanced = append(enhanced, line)
|
||||
continue
|
||||
}
|
||||
|
||||
// 检测引用块(来源说明区域)
|
||||
if strings.HasPrefix(trimmed, ">") {
|
||||
inQuoteBlock = true
|
||||
enhanced = append(enhanced, line)
|
||||
continue
|
||||
} else if inQuoteBlock && trimmed == "" {
|
||||
inQuoteBlock = false
|
||||
enhanced = append(enhanced, line)
|
||||
continue
|
||||
} else if inQuoteBlock {
|
||||
enhanced = append(enhanced, line)
|
||||
continue
|
||||
}
|
||||
|
||||
// 跳过空行、标题行、列表标记行
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
||||
enhanced = append(enhanced, line)
|
||||
continue
|
||||
}
|
||||
|
||||
// 跳过纯列表标记行(只有 - * 1. 等)
|
||||
if len(trimmed) <= 3 && (strings.HasPrefix(trimmed, "-") || strings.HasPrefix(trimmed, "*") || strings.HasPrefix(trimmed, "1.")) {
|
||||
enhanced = append(enhanced, line)
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查是否是陈述句(以句号、问号、感叹号结尾)
|
||||
needsCitation := strings.HasSuffix(trimmed, "。") || strings.HasSuffix(trimmed, ".") ||
|
||||
strings.HasSuffix(trimmed, "!") || strings.HasSuffix(trimmed, "!") ||
|
||||
strings.HasSuffix(trimmed, "?") || strings.HasSuffix(trimmed, "?")
|
||||
|
||||
// 对列表项内容也进行标注
|
||||
if strings.HasPrefix(trimmed, "-") || strings.HasPrefix(trimmed, "*") ||
|
||||
(len(trimmed) > 2 && trimmed[0] >= '0' && trimmed[0] <= '9' && trimmed[1] == '.') {
|
||||
// 检查列表项是否以句子结束
|
||||
if strings.HasSuffix(trimmed, "。") || strings.HasSuffix(trimmed, ".") {
|
||||
needsCitation = true
|
||||
}
|
||||
}
|
||||
|
||||
if needsCitation {
|
||||
// 决定使用哪种标注
|
||||
citation := " [[AI建议]]"
|
||||
if hasKnowledge && len(knowledgeSources) > 0 {
|
||||
// 如果内容较长且像是说明性内容,用AI建议
|
||||
// 如果内容较短且像是事实陈述,用知识库
|
||||
if len(trimmed) > 100 || strings.Contains(trimmed, "建议") ||
|
||||
strings.Contains(trimmed, "注意") || strings.Contains(trimmed, "可以") {
|
||||
citation = " [[AI建议]]"
|
||||
} else {
|
||||
citation = " [[知识库:" + knowledgeSources[0] + "]]"
|
||||
}
|
||||
}
|
||||
|
||||
// 添加标注(如果行尾还没有)
|
||||
if !strings.Contains(line, "[[知识库:") && !strings.Contains(line, "[[AI建议]]") {
|
||||
enhanced = append(enhanced, strings.TrimRight(line, " \t")+citation)
|
||||
} else {
|
||||
enhanced = append(enhanced, line)
|
||||
}
|
||||
} else {
|
||||
enhanced = append(enhanced, line)
|
||||
}
|
||||
}
|
||||
|
||||
result := strings.Join(enhanced, "\n")
|
||||
|
||||
// 如果末尾没有来源说明块,添加一个
|
||||
if !strings.Contains(result, "**来源说明**") && !strings.Contains(result, "> **来源说明**") {
|
||||
result += h.generateSourceSummary(hasKnowledge, knowledgeSources)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// fillMissingCitations 为部分标注的回答补充缺失的标注
|
||||
func (h *LLMChatHandler) fillMissingCitations(response string, hasKnowledge bool, knowledgeSources []string) string {
|
||||
// 如果有知识库但缺少知识库标注,或者缺少AI建议标注,进行补充
|
||||
hasKBCitation := strings.Contains(response, "[[知识库:")
|
||||
hasAICitation := strings.Contains(response, "[[AI建议]]")
|
||||
|
||||
if hasKnowledge && !hasKBCitation && len(knowledgeSources) > 0 {
|
||||
// 在第一个事实陈述句后添加知识库标注
|
||||
response = h.addFirstKBCitation(response, knowledgeSources[0])
|
||||
}
|
||||
|
||||
if !hasAICitation {
|
||||
// 在建议性内容后添加AI建议标注
|
||||
response = h.addAICitationToSuggestions(response)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// addFirstKBCitation 在第一个事实陈述句后添加知识库标注
|
||||
func (h *LLMChatHandler) addFirstKBCitation(response string, source string) string {
|
||||
lines := strings.Split(response, "\n")
|
||||
for i, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed != "" && !strings.HasPrefix(trimmed, "#") && !strings.HasPrefix(trimmed, ">") {
|
||||
if strings.HasSuffix(trimmed, "。") || strings.HasSuffix(trimmed, ".") {
|
||||
if !strings.Contains(line, "[[知识库:") {
|
||||
lines[i] = strings.TrimRight(line, " \t") + " [[知识库:" + source + "]]"
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
// addAICitationToSuggestions 为建议性内容添加AI建议标注
|
||||
func (h *LLMChatHandler) addAICitationToSuggestions(response string) string {
|
||||
keywords := []string{"建议", "推荐", "可以", "应该", "注意", "提示", "提醒"}
|
||||
lines := strings.Split(response, "\n")
|
||||
|
||||
for i, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
// 检查是否包含建议性关键词
|
||||
for _, kw := range keywords {
|
||||
if strings.Contains(trimmed, kw) && (strings.HasSuffix(trimmed, "。") || strings.HasSuffix(trimmed, ".")) {
|
||||
if !strings.Contains(line, "[[AI建议]]") && !strings.Contains(line, "[[知识库:") {
|
||||
lines[i] = strings.TrimRight(line, " \t") + " [[AI建议]]"
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
// generateSourceSummary 生成来源说明块
|
||||
func (h *LLMChatHandler) generateSourceSummary(hasKnowledge bool, knowledgeSources []string) string {
|
||||
var summary strings.Builder
|
||||
summary.WriteString("\n\n---\n\n")
|
||||
summary.WriteString("> **来源说明**\n>\n")
|
||||
|
||||
if hasKnowledge && len(knowledgeSources) > 0 {
|
||||
summary.WriteString("> **知识库引用:**\n")
|
||||
for _, source := range knowledgeSources {
|
||||
summary.WriteString("> - 【" + source + "】\n")
|
||||
}
|
||||
summary.WriteString(">\n")
|
||||
summary.WriteString("> **AI建议:**\n")
|
||||
summary.WriteString("> - 流程说明和注意事项\n")
|
||||
} else {
|
||||
summary.WriteString("> **AI建议:**\n")
|
||||
summary.WriteString("> - 以上内容为AI建议,仅供参考\n")
|
||||
}
|
||||
|
||||
return summary.String()
|
||||
}
|
||||
|
||||
// extractKnowledgeSources 从知识库检索结果中提取文献名称
|
||||
func (h *LLMChatHandler) extractKnowledgeSources(knowledgeContext string) []string {
|
||||
if knowledgeContext == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var sources []string
|
||||
seen := make(map[string]bool)
|
||||
|
||||
// 从格式 【文献名】 中提取
|
||||
lines := strings.Split(knowledgeContext, "\n")
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "【") && strings.Contains(line, "】") {
|
||||
start := strings.Index(line, "【")
|
||||
end := strings.Index(line, "】")
|
||||
if start < end && start >= 0 {
|
||||
source := line[start+len("【") : end]
|
||||
// 去除相似度等后缀
|
||||
if idx := strings.Index(source, " ·"); idx > 0 {
|
||||
source = source[:idx]
|
||||
}
|
||||
if !seen[source] && source != "" {
|
||||
sources = append(sources, source)
|
||||
seen[source] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sources
|
||||
}
|
||||
|
||||
// keywordSearch 关键词搜索(降级方案,搜索 chunks 和 documents)
|
||||
func (h *LLMChatHandler) keywordSearch(ctx context.Context, kbID, query string, limit int) []string {
|
||||
keywords := cleanQueryForSearch(query)
|
||||
@@ -438,53 +671,92 @@ func (h *LLMChatHandler) buildMessages(systemPrompt, knowledgeContext string, ha
|
||||
|
||||
if hasKB {
|
||||
finalSystem += `
|
||||
## 来源标注规则(必须严格遵守,每一条都不可省略)
|
||||
|
||||
本系统已接入知识库。你的回答**必须在正文中内联标注**信息来源,使用以下**双方括号**固定格式(系统会自动将其渲染为彩色徽章):
|
||||
## 🔴 最高优先级规则:来源徽章标注(必须100%执行)
|
||||
|
||||
### 内联标注格式(正文中每个引用点都必须标注)
|
||||
本系统会将 [[知识库:xxx]] 和 [[AI建议]] 自动渲染为彩色徽章,显示在回答内容中。用户通过徽章可以清楚看到每句话的出处。
|
||||
|
||||
1. 引用知识库文献时,在引用内容后紧跟:[[知识库:文献名称]]
|
||||
- 如有具体条款:[[知识库:文献名称:第X条]]
|
||||
- 示例:"高新技术企业可享受15%优惠税率 [[知识库:高新技术企业认定管理办法:第四条]]"
|
||||
### ⚠️ 强制要求(不允许任何例外)
|
||||
|
||||
2. 来自AI模型自身知识(非知识库内容)时,在该内容后紧跟:[[AI建议]]
|
||||
- 示例:"该政策于2016年首次发布 [[AI建议]]"
|
||||
- **重要:任何不是直接来自知识库原文的分析、解读、建议、补充说明,都必须标注 [[AI建议]]**
|
||||
你的回答中**每一句事实陈述、每一个观点**都必须在句子末尾标注来源徽章:
|
||||
|
||||
### 末尾来源汇总(必须附加)
|
||||
**格式1:知识库引用(蓝色徽章)**
|
||||
在引用知识库内容的句子末尾加:[[知识库:文献名称]]
|
||||
|
||||
回答末尾必须附加来源汇总块(用 blockquote 格式),要求完整引用原文:
|
||||
示例:
|
||||
- 居住证办理需要身份证、居住证明和近期照片 [[知识库:户口登记管理规定]]
|
||||
- 办理时限为15个工作日 [[知识库:户口登记管理规定:第十二条]]
|
||||
|
||||
**格式2:AI分析补充(橙色徽章)**
|
||||
任何解读、分析、建议、注意事项等非知识库原文的内容,句末加:[[AI建议]]
|
||||
|
||||
示例:
|
||||
- 建议您提前准备齐全材料,以免多次往返 [[AI建议]]
|
||||
- 如有疑问可先电话咨询当地派出所 [[AI建议]]
|
||||
|
||||
### 📝 完整示例(必须参照此格式)
|
||||
|
||||
**用户提问:** "居住证办理条件是什么?多久能拿到?"
|
||||
|
||||
**标准回答格式:**
|
||||
|
||||
## 居住证办理条件及办理时限
|
||||
|
||||
### 办理条件
|
||||
|
||||
在居住地居住半年以上,同时满足以下条件之一 [[知识库:户口登记管理规定]]:
|
||||
|
||||
- 有合法稳定就业 [[知识库:户口登记管理规定]]
|
||||
- 有合法稳定住所 [[知识库:户口登记管理规定]]
|
||||
- 连续就读 [[知识库:户口登记管理规定]]
|
||||
|
||||
所需材料包括 [[知识库:户口登记管理规定]]:
|
||||
- 身份证
|
||||
- 居住证明(租房合同/房产证/单位证明)
|
||||
- 近期照片
|
||||
|
||||
### 办理时限
|
||||
|
||||
办理居住证的时限为**15个工作日** [[知识库:户口登记管理规定]]。具体流程如下 [[AI建议]]:
|
||||
|
||||
1. 到居住地的任一户籍派出所提交申请材料 [[AI建议]]
|
||||
2. 派出所审核材料,符合条件的予以受理 [[AI建议]]
|
||||
3. 派出所将相关信息录入系统并报上级审核 [[AI建议]]
|
||||
4. 审核通过后,居住证将在15个工作日内制作完成并发放 [[AI建议]]
|
||||
|
||||
### 注意事项
|
||||
|
||||
请确保提供的材料真实有效,并按要求准备齐全 [[AI建议]]。如有任何疑问或材料不齐全的情况,建议及时与当地户籍派出所联系确认 [[AI建议]]。
|
||||
|
||||
---
|
||||
|
||||
> **来源说明**
|
||||
>
|
||||
>
|
||||
> **知识库引用:**
|
||||
> - 【文献名称1】:摘录知识库中被引用的原文段落(保留条款编号和原始表述,不要省略或改写)
|
||||
> - 【文献名称2】:摘录对应原文...
|
||||
> (如未引用知识库则写"无")
|
||||
>
|
||||
> - 【户口登记管理规定】:第五条规定,办理居住证需在居住地居住半年以上,并满足合法稳定就业、合法稳定住所或连续就读条件之一;需提供身份证、居住证明和近期照片。第十二条规定,办理时限为自受理之日起15个工作日内制作完成并发放。
|
||||
>
|
||||
> **AI建议:**
|
||||
> - 说明哪些内容来自AI自身知识的补充分析或解读
|
||||
> - 办理流程的四个步骤说明
|
||||
> - 材料准备的注意事项和建议
|
||||
|
||||
### 标注检查清单
|
||||
- 正文中是否每个引用知识库的地方都标注了 [[知识库:文献名称]]?
|
||||
- 正文中是否每个AI补充分析的地方都标注了 [[AI建议]]?
|
||||
- 末尾是否有完整的来源汇总块?
|
||||
|
||||
重要:知识库引用部分必须逐条列出被引用的原文内容,不能只写文献名称,要把知识库中的原始段落完整摘录出来,便于用户核实溯源。
|
||||
### ✅ 输出前必检项(每次回答前自查)
|
||||
- [ ] 正文中所有知识库引用都标注了 [[知识库:文献名称]]
|
||||
- [ ] 正文中所有AI分析都标注了 [[AI建议]]
|
||||
- [ ] 末尾有完整的来源汇总块
|
||||
- [ ] 来源汇总中列出了知识库原文摘录
|
||||
|
||||
`
|
||||
if knowledgeContext != "" {
|
||||
finalSystem += "### 知识库检索结果\n\n以下是从知识库中检索到的相关文献,请优先基于这些内容回答:\n\n" + knowledgeContext
|
||||
finalSystem += "### 📚 知识库检索结果\n\n以下是从知识库中检索到的相关文献,请优先基于这些内容回答,并在每个引用处标注 [[知识库:文献名称]]:\n\n" + knowledgeContext
|
||||
} else {
|
||||
finalSystem += "### 知识库检索结果\n\n当前知识库中未检索到与用户问题直接相关的文献。请使用AI知识回答,并在每句标注 [[AI建议]]。\n"
|
||||
finalSystem += "### 📚 知识库检索结果\n\n⚠️ 当前知识库中未检索到与用户问题直接相关的文献。请使用AI知识回答,**每句话后都必须标注 [[AI建议]]**。\n"
|
||||
}
|
||||
} else {
|
||||
finalSystem += `
|
||||
|
||||
## 来源标注规则(必须严格遵守)
|
||||
## 来源标注规则
|
||||
|
||||
你的回答内容全部来自AI模型的自身知识。请在回答末尾附加:
|
||||
你的回答内容全部来自AI模型的自身知识。请在重要观点后标注 [[AI建议]],并在回答末尾附加:
|
||||
|
||||
> **来源说明**
|
||||
> - 以上内容为AI建议,仅供参考,请以官方文件和专业意见为准。
|
||||
@@ -551,8 +823,10 @@ func (h *LLMChatHandler) Chat(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
hasKB := cfg.KnowledgeBaseID != nil && *cfg.KnowledgeBaseID != ""
|
||||
var knowledgeCtx string
|
||||
var kbSources []string
|
||||
if hasKB {
|
||||
knowledgeCtx, _ = h.retrieveKnowledge(r.Context(), *cfg.KnowledgeBaseID, req.Message, 3)
|
||||
kbSources = h.extractKnowledgeSources(knowledgeCtx)
|
||||
}
|
||||
|
||||
// 加载同机构应用列表,用于超范围引导跳转
|
||||
@@ -644,8 +918,11 @@ func (h *LLMChatHandler) Chat(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "data: [DONE]\n\n")
|
||||
flusher.Flush()
|
||||
|
||||
// 后处理:自动增强来源标注
|
||||
enhancedResponse := h.enhanceCitations(fullResponse.String(), hasKB, kbSources)
|
||||
|
||||
duration := time.Since(startTime).Milliseconds()
|
||||
go h.recordUsage(appID, userID.String(), convID, req.Message, fullResponse.String(), totalTokens, modelName, duration)
|
||||
go h.recordUsage(appID, userID.String(), convID, req.Message, enhancedResponse, totalTokens, modelName, duration)
|
||||
if isNewConv {
|
||||
go h.generateConversationName(appID, userID.String(), convID, req.Message)
|
||||
}
|
||||
@@ -673,8 +950,10 @@ func (h *LLMChatHandler) Completion(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
hasKB := cfg.KnowledgeBaseID != nil && *cfg.KnowledgeBaseID != ""
|
||||
var knowledgeCtx string
|
||||
var kbSources []string
|
||||
if hasKB {
|
||||
knowledgeCtx, _ = h.retrieveKnowledge(r.Context(), *cfg.KnowledgeBaseID, req.Message, 3)
|
||||
kbSources = h.extractKnowledgeSources(knowledgeCtx)
|
||||
}
|
||||
|
||||
// 加载同机构应用列表,用于超范围引导跳转
|
||||
@@ -756,8 +1035,11 @@ func (h *LLMChatHandler) Completion(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "data: [DONE]\n\n")
|
||||
flusher.Flush()
|
||||
|
||||
// 后处理:自动增强来源标注
|
||||
enhancedResponse := h.enhanceCitations(fullResponse.String(), hasKB, kbSources)
|
||||
|
||||
duration := time.Since(startTime).Milliseconds()
|
||||
go h.recordUsage(appID, userID.String(), convID, req.Message, fullResponse.String(), totalTokens, modelName, duration)
|
||||
go h.recordUsage(appID, userID.String(), convID, req.Message, enhancedResponse, totalTokens, modelName, duration)
|
||||
go h.generateConversationName(appID, userID.String(), convID, req.Message)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user