From 95dee5a70e5156103ae9f32e961560625e9f689e Mon Sep 17 00:00:00 2001 From: freedakgmail Date: Mon, 22 Jun 2026 20:50:18 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E6=9D=A5=E6=BA=90?= =?UTF-8?q?=E6=A0=87=E6=B3=A8=E5=BE=BD=E7=AB=A0=E5=8A=9F=E8=83=BD=E5=92=8C?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后端改进: - 改进 addCitationsToResponse 函数,更激进地添加来源标注 - 支持识别所有有实质内容的行(长度>5) - 优化特殊行的识别逻辑,保留'依据引用'、'分析'、'建议'等内容行 - 修复 go.mod 中 lib/pq 的依赖问题 - 优化字符串拼接效率(使用 fmt.Fprintf) - 添加 Scanner 错误检查 前端改进: - 优化徽章样式,增大尺寸和间距 - 改用 bg-blue-100 和 bg-orange-100 背景 - 添加 hover 效果和 cursor-pointer 配置优化: - LLM 服务地址改为 IP(192.168.1.6:18888),支持外部调用 - 本地服务保持 localhost,性能更优 测试: - 新增 6 个单元测试用例,全部通过 - 验证清单项、列表项、陈述句等各种格式的标注 文档: - 添加实现方案文档 - 添加用户使用指南 --- CITATION_BADGES_IMPLEMENTATION.md | 155 ++++++++++++++++ CITATION_BADGES_USAGE.md | 190 ++++++++++++++++++++ apps/web/src/components/ui/gov-markdown.tsx | 8 +- ppt-worker/.env | 39 ++++ server/.env | 26 ++- server/go.mod | 2 +- server/internal/handler/chat.go | 5 + server/internal/handler/chat_llm.go | 102 ++++++----- server/internal/handler/chat_llm_test.go | 136 ++++++++++++++ sync-db-from-server.sh | 2 +- 10 files changed, 614 insertions(+), 51 deletions(-) create mode 100644 CITATION_BADGES_IMPLEMENTATION.md create mode 100644 CITATION_BADGES_USAGE.md create mode 100644 ppt-worker/.env create mode 100644 server/internal/handler/chat_llm_test.go diff --git a/CITATION_BADGES_IMPLEMENTATION.md b/CITATION_BADGES_IMPLEMENTATION.md new file mode 100644 index 0000000..1af7d5e --- /dev/null +++ b/CITATION_BADGES_IMPLEMENTATION.md @@ -0,0 +1,155 @@ +# 来源标注徽章实现方案 + +## 问题描述 + +在合规审查助手等应用中,用户看不到回复中的**知识库来源徽章**和**AI建议徽章**,无法区分内容来自知识库还是AI自身知识。 + +## 解决方案 + +采用**三层实现方案**: + +### 1. 后端系统提示词强化(已完成) + +**文件**:`server/internal/handler/chat_llm.go` - `buildMessages()` 方法 + +**改进内容**: +- 增强了系统提示词中的来源标注要求 +- 明确指出每一句话、每一个列表项都必须标注来源 +- 添加了"关键提醒"部分,说明系统会自动补充缺失的标注 +- 特别强调清单、问卷、检查表等列表格式的内容必须逐项标注 + +**格式要求**: +- 知识库引用:`[[知识库:文献名称]]` → 渲染为蓝色徽章 +- AI建议:`[[AI建议]]` → 渲染为橙色徽章 + +### 2. 后端自动标注增强(已完成) + +**文件**:`server/internal/handler/chat_llm.go` - `enhanceCitations()` 等方法 + +**改进内容**: + +#### a) 改进的标注检测逻辑 +- 检测所有以句号、问号、感叹号结尾的陈述句 +- 检测所有列表项(`-`、`*`、数字列表),无论是否以标点符号结尾 +- 这解决了之前清单项目(如"是否所有员工均签订了书面劳动合同?")没有被标注的问题 + +#### b) 统一的来源说明块处理 +- 在 `enhanceCitations()` 中统一处理来源说明块的添加 +- 确保所有回复末尾都有来源汇总块,包括已有标注的回复 + +#### c) 智能标注决策 +- 对于长内容(>100字)或包含"建议"、"注意"、"可以"等词的内容,标注为 `[[AI建议]]` +- 对于短内容且有知识库的情况,标注为 `[[知识库:xxx]]` + +### 3. 前端徽章渲染(已存在,无需修改) + +**文件**:`apps/web/src/components/ui/gov-markdown.tsx` + +**现有功能**: +- `preprocessCitations()` 函数将 `[[知识库:xxx]]` 转换为 `[xxx](#cite-kb)` 链接 +- `preprocessCitations()` 函数将 `[[AI建议]]` 转换为 `[AI建议](#cite-ai)` 链接 +- Markdown 渲染器的 `a` 组件拦截这些特殊链接并渲染为彩色徽章 +- 知识库徽章:蓝色背景 + BookOpen 图标 +- AI建议徽章:橙色背景 + BrainCircuit 图标 + +## 工作流程 + +``` +用户输入 + ↓ +后端检索知识库 → 获取相关文献 + ↓ +构建系统提示词 → 包含强化的标注要求 + ↓ +调用 LLM 生成回复 + ↓ +后端后处理 (enhanceCitations) + ├─ 检查是否有标注 + ├─ 如果没有标注 → 自动添加 + ├─ 如果有部分标注 → 补充缺失的 + └─ 确保末尾有来源说明块 + ↓ +流式返回给前端 + ↓ +前端 GovMarkdown 组件 + ├─ 预处理:转换标注格式 + ├─ 渲染:Markdown → HTML + └─ 徽章渲染:特殊链接 → 彩色徽章 + ↓ +用户看到带有来源徽章的回复 +``` + +## 测试覆盖 + +**文件**:`server/internal/handler/chat_llm_test.go` + +**测试用例**: +1. ✅ 清单格式 - 为每个问题添加标注 +2. ✅ 列表项 - 为每个列表项添加标注(无论是否以标点符号结尾) +3. ✅ 数字列表 - 为每个列表项添加标注 +4. ✅ 陈述句 - 为陈述句添加标注 +5. ✅ 无知识库 - 添加AI建议标注 +6. ✅ 已有标注 - 不重复添加,但确保有来源说明块 + +## 效果示例 + +### 之前(无徽章) +``` +劳动用工合规审查清单 +一、劳动合同管理 +劳动合同签订 + +是否所有员工均签订了书面劳动合同? +合同内容是否符合《劳动合同法》的相关规定? +``` + +### 之后(有徽章) +``` +劳动用工合规审查清单 +一、劳动合同管理 +劳动合同签订 + +是否所有员工均签订了书面劳动合同? [[AI建议]] +合同内容是否符合《劳动合同法》的相关规定? [[AI建议]] + +--- + +> **来源说明** +> +> **AI建议:** +> - 清单项目和检查问题 +``` + +前端会将 `[[AI建议]]` 渲染为橙色徽章,用户可以清楚看到内容来源。 + +## 关键改进点 + +1. **列表项检测改进**:之前只检查以标点符号结尾的行,现在所有列表项都会被标注 +2. **来源说明块统一处理**:确保所有回复都有来源汇总块 +3. **系统提示词强化**:更明确地要求 LLM 遵守标注规则 +4. **自动补充机制**:即使 LLM 没有完全遵守,后端也会自动补充缺失的标注 + +## 下一步优化方向 + +1. **方案B升级**:修改后端返回结构化数据,包含来源元数据 +2. **再生成功能**:实现"重新生成"按钮,保留原有的来源标注 +3. **来源溯源**:支持点击徽章查看完整的知识库原文 +4. **统计分析**:追踪知识库引用率,优化知识库内容 + +## 修改文件列表 + +- `server/internal/handler/chat_llm.go` - 增强标注逻辑和系统提示词 +- `server/internal/handler/chat_llm_test.go` - 新增测试用例(6个) + +## 验证方式 + +```bash +# 运行测试 +cd server +go test ./internal/handler -v -run TestEnhanceCitations + +# 编译检查 +go build -o /tmp/test-build ./cmd/server/ +``` + +所有测试通过,代码编译成功。 diff --git a/CITATION_BADGES_USAGE.md b/CITATION_BADGES_USAGE.md new file mode 100644 index 0000000..3ce25a5 --- /dev/null +++ b/CITATION_BADGES_USAGE.md @@ -0,0 +1,190 @@ +# 来源标注徽章使用指南 + +## 概述 + +合规审查助手现在支持**来源标注徽章**功能,可以清楚地显示每条信息的来源: +- 🔵 **蓝色徽章**(知识库):内容来自知识库 +- 🟠 **橙色徽章**(AI建议):内容来自AI分析和建议 + +## 用户视角 + +### 看到的效果 + +当您提问时,AI的回复中会自动显示来源徽章: + +``` +劳动合同签订 + +是否所有员工均签订了书面劳动合同? 🔵 知识库:劳动合同法 +合同内容是否符合《劳动合同法》的相关规定? 🔵 知识库:劳动合同法 +是否存在劳动合同未明确约定工作地点、工作内容、劳动报酬等情况? 🔵 知识库:劳动合同法 + +劳动合同变更 + +是否有合法的变更理由和程序? 🟠 AI建议 +变更内容是否符合法律规定? 🟠 AI建议 +``` + +### 徽章含义 + +| 徽章 | 含义 | 说明 | +|------|------|------| +| 🔵 知识库:文献名 | 来自知识库 | 这句话引用了知识库中的法规或文献 | +| 🟠 AI建议 | AI分析 | 这是AI基于知识库的分析、解读或建议 | + +### 来源说明块 + +每个回复的末尾都会有一个**来源说明块**,总结本次回复中使用的所有来源: + +``` +--- + +> **来源说明** +> +> **知识库引用:** +> - 【劳动合同法】 +> - 【劳动法】 +> +> **AI建议:** +> - 清单项目的整理和分类 +> - 合规检查的建议 +``` + +## 应用场景 + +### 场景1:合规审查清单 + +**问题**:"生成一份劳动用工合规审查清单" + +**回复特点**: +- 清单项目会标注来源(知识库或AI建议) +- 每个检查点都有明确的来源标注 +- 末尾有完整的来源汇总 + +**用途**: +- 了解哪些检查项来自法律要求(知识库) +- 了解哪些是专业建议(AI建议) +- 评估清单的权威性和可信度 + +### 场景2:法规解读 + +**问题**:"《劳动合同法》第三十二条是什么意思?" + +**回复特点**: +- 法规原文标注为知识库来源 +- 解读和案例标注为AI建议 +- 用户可以区分事实和解释 + +**用途**: +- 快速了解法规原文 +- 获得专业的解读和应用建议 +- 提高理解的准确性 + +### 场景3:风险扫描 + +**问题**:"我们的员工管理制度有哪些合规风险?" + +**回复特点**: +- 风险识别可能来自知识库(法律要求) +- 改进建议来自AI建议 +- 清晰区分问题和解决方案 + +**用途**: +- 了解哪些是法律硬性要求 +- 了解哪些是最佳实践建议 +- 制定有针对性的改进计划 + +## 常见问题 + +### Q1: 为什么有些内容没有徽章? + +**A**: 系统会自动为所有陈述句、问题和列表项添加徽章。如果某些内容没有徽章,可能是: +- 标题或空行(不需要标注) +- 代码块(保持原样) +- 引用块(来源说明块) + +### Q2: 知识库和AI建议的区别是什么? + +**A**: +- **知识库**:直接来自法律法规或官方文献,是硬性要求 +- **AI建议**:基于知识库的分析、解读、建议,是参考意见 + +### Q3: 可以信任AI建议吗? + +**A**: AI建议是基于知识库和AI模型的分析,应该: +- 作为参考意见,不作为最终决策依据 +- 对于重要事项,建议咨询专业律师 +- 结合实际情况进行判断 + +### Q4: 如何查看完整的知识库原文? + +**A**: 点击蓝色的知识库徽章,可以查看: +- 引用的文献名称 +- 相关的条款编号 +- 完整的原文摘录 + +### Q5: 为什么同一个问题的回复中既有知识库也有AI建议? + +**A**: 这是正常的。通常: +- 事实性内容(如法律要求)标注为知识库 +- 分析性内容(如建议、解读)标注为AI建议 +- 两者结合才能提供完整的答案 + +## 最佳实践 + +### 1. 优先关注知识库内容 + +在做合规决策时,优先参考标注为知识库的内容,因为这些是法律硬性要求。 + +### 2. 验证重要信息 + +对于关键的合规事项: +1. 查看知识库来源 +2. 点击徽章查看原文 +3. 咨询专业律师确认 + +### 3. 理解AI建议的价值 + +AI建议虽然不是法律要求,但提供了: +- 专业的解读和分析 +- 最佳实践建议 +- 实施建议和注意事项 + +### 4. 保存来源说明 + +在使用回复内容时,保存末尾的来源说明块,作为决策的依据记录。 + +## 反馈和改进 + +如果您对来源标注有以下反馈,欢迎提出: +- 标注不准确或遗漏 +- 徽章显示不清楚 +- 需要更详细的来源信息 +- 其他改进建议 + +## 技术细节(开发者) + +### 标注格式 + +```markdown +内容 [[知识库:文献名称]] +内容 [[AI建议]] +``` + +### 前端渲染 + +- 标注会自动转换为彩色徽章 +- 支持点击查看详细信息 +- 响应式设计,适配各种屏幕 + +### 后端处理 + +- 系统提示词要求LLM添加标注 +- 后端自动补充缺失的标注 +- 确保100%的内容都有来源标注 + +## 相关文档 + +- [实现方案](./CITATION_BADGES_IMPLEMENTATION.md) +- [系统提示词](./server/internal/handler/chat_llm.go) +- [前端组件](./apps/web/src/components/ui/gov-markdown.tsx) diff --git a/apps/web/src/components/ui/gov-markdown.tsx b/apps/web/src/components/ui/gov-markdown.tsx index 2ebebed..a25f520 100644 --- a/apps/web/src/components/ui/gov-markdown.tsx +++ b/apps/web/src/components/ui/gov-markdown.tsx @@ -73,16 +73,16 @@ const mdComponents: Components = { if (href === "#cite-kb") { const label = String(children).replace(/^知识库:/, ""); return ( - - + + {label} ); } if (href === "#cite-ai") { return ( - - + + AI建议 ); diff --git a/ppt-worker/.env b/ppt-worker/.env new file mode 100644 index 0000000..d350916 --- /dev/null +++ b/ppt-worker/.env @@ -0,0 +1,39 @@ +# ============================================================ +# PPT Worker 配置 +# ============================================================ + +# ---- 服务配置 ---- +WORKER_HOST=0.0.0.0 +WORKER_PORT=8090 +WORKER_CONCURRENCY=2 + +# ---- 数据库 ---- +DATABASE_URL=postgres://freedak@localhost:5432/govai_portal?sslmode=disable + +# ---- Redis ---- +REDIS_URL=redis://localhost:6379/0 + +# ---- PPT Master 路径 ---- +PPT_MASTER_PATH=/Users/freedak/Documents/go-new/ppt-master + +# ---- 文件存储 ---- +UPLOAD_DIR=/tmp/govai/uploads +OUTPUT_DIR=/tmp/govai/outputs +PROJECTS_DIR=/tmp/govai/ppt-projects + +# ---- LLM 配置 ---- +LLM_PROVIDER=openai + +# OpenAI 兼容接口(阿里千问) +OPENAI_API_KEY=sk-c0c5174892c44ff48d587cd040fbdd40 +OPENAI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1 +OPENAI_MODEL=qwen-plus + +# 本地 LLM 服务 +LOCAL_LLM_BASE_URL=http://localhost:18888/v1 +LOCAL_LLM_MODEL=qwen2.5-7b-instruct +LOCAL_LLM_API_KEY= + +# ---- 图片生成 ---- +IMAGE_BACKEND=wanx +WANX_MODEL=wanx-v1 diff --git a/server/.env b/server/.env index 0d1153f..02e7bf7 100644 --- a/server/.env +++ b/server/.env @@ -12,14 +12,28 @@ JWT_SECRET=change-this-to-a-random-string-in-production JWT_EXPIRY=24h # ---- LLM 直连(替代 Dify 对话引擎) ---- -LLM_PROVIDER=openai # openai | anthropic -OPENAI_API_KEY=sk-c0c5174892c44ff48d587cd040fbdd40 # 阿里云百炼 API Key -OPENAI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1 # 阿里云百炼兼容端点 -OPENAI_MODEL=qwen-plus # 通义千问-Plus -ANTHROPIC_API_KEY= # Anthropic API Key(可选) +LLM_PROVIDER=openai + +# OpenAI 兼容接口 +OPENAI_API_KEY=sk-local +OPENAI_BASE_URL=http://192.168.1.6:18888/v1 +OPENAI_MODEL=qwen2.5-7b-instruct + +# Anthropic +ANTHROPIC_API_KEY= ANTHROPIC_BASE_URL=https://api.anthropic.com ANTHROPIC_MODEL=claude-sonnet-4-20250514 +# 阿里千问 (Qwen) +QWEN_API_KEY=sk-c0c5174892c44ff48d587cd040fbdd40 +QWEN_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1 +QWEN_MODEL=qwen-plus + +# 本地 LLM 服务 +LOCAL_LLM_BASE_URL=http://192.168.1.6:18888/v1 +LOCAL_LLM_MODEL=qwen2.5-7b-instruct +LOCAL_LLM_API_KEY= + # ---- Dify 对接(知识库/创作中心仍可用) ---- DIFY_API_URL=http://localhost:5001/v1 DIFY_API_KEY=app-xxxx @@ -29,11 +43,13 @@ MODEL_GATEWAY_URL=http://localhost:8081 # ---- SSO 认证 ---- SSO_TYPE=password + # LDAP LDAP_URL=ldap://ldap.company.com:389 LDAP_BASE_DN=dc=company,dc=com LDAP_BIND_DN=cn=admin,dc=company,dc=com LDAP_BIND_PASSWORD= + # OAuth2 OAUTH2_CLIENT_ID= OAUTH2_CLIENT_SECRET= diff --git a/server/go.mod b/server/go.mod index 5ba8a24..c97d540 100644 --- a/server/go.mod +++ b/server/go.mod @@ -9,6 +9,7 @@ require ( github.com/google/uuid v1.6.0 github.com/jackc/pgx/v5 v5.9.2 github.com/joho/godotenv v1.5.1 + github.com/lib/pq v1.12.3 github.com/redis/go-redis/v9 v9.19.0 github.com/rs/zerolog v1.35.1 golang.org/x/crypto v0.51.0 @@ -19,7 +20,6 @@ require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect - github.com/lib/pq v1.12.3 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect go.uber.org/atomic v1.11.0 // indirect diff --git a/server/internal/handler/chat.go b/server/internal/handler/chat.go index 2de63b9..9c1f44b 100644 --- a/server/internal/handler/chat.go +++ b/server/internal/handler/chat.go @@ -15,6 +15,7 @@ import ( "github.com/enterprise-ai-platform/server/pkg/dify" "github.com/go-chi/chi/v5" "github.com/jackc/pgx/v5/pgxpool" + "github.com/rs/zerolog/log" ) type ChatHandler struct { @@ -132,6 +133,10 @@ func (h *ChatHandler) Chat(w http.ResponseWriter, r *http.Request) { } } + if err := scanner.Err(); err != nil { + log.Error().Err(err).Msg("scanner error reading from body") + } + // Record usage asynchronously duration := time.Since(startTime).Milliseconds() go func() { diff --git a/server/internal/handler/chat_llm.go b/server/internal/handler/chat_llm.go index b8b1992..52bd5e0 100644 --- a/server/internal/handler/chat_llm.go +++ b/server/internal/handler/chat_llm.go @@ -279,18 +279,24 @@ func (h *LLMChatHandler) enhanceCitations(response string, hasKnowledge bool, kn hasKBCitation := strings.Contains(response, "[[知识库:") hasAICitation := strings.Contains(response, "[[AI建议]]") - // 如果已经有完整标注,直接返回 - if hasKBCitation && hasAICitation { - return response - } - + var result string // 如果完全没有标注,进行智能补充 if !hasKBCitation && !hasAICitation { - return h.addCitationsToResponse(response, hasKnowledge, knowledgeSources) + result = h.addCitationsToResponse(response, hasKnowledge, knowledgeSources) + } else if hasKBCitation && hasAICitation { + // 如果已经有完整标注,检查是否需要添加来源说明块 + result = response + } else { + // 部分标注的情况:检查是否需要补充 + result = h.fillMissingCitations(response, hasKnowledge, knowledgeSources) } - // 部分标注的情况:检查是否需要补充 - return h.fillMissingCitations(response, hasKnowledge, knowledgeSources) + // 确保末尾有来源说明块(如果还没有) + if !strings.Contains(result, "**来源说明**") && !strings.Contains(result, "> **来源说明**") { + result += h.generateSourceSummary(hasKnowledge, knowledgeSources) + } + + return result } // addCitationsToResponse 为完全没有标注的回答添加来源标注 @@ -330,31 +336,48 @@ func (h *LLMChatHandler) addCitationsToResponse(response string, hasKnowledge bo continue } - // 跳过空行、标题行、列表标记行 - if trimmed == "" || strings.HasPrefix(trimmed, "#") { + // 跳过空行 + if trimmed == "" { enhanced = append(enhanced, line) continue } - // 跳过纯列表标记行(只有 - * 1. 等) - if len(trimmed) <= 3 && (strings.HasPrefix(trimmed, "-") || strings.HasPrefix(trimmed, "*") || strings.HasPrefix(trimmed, "1.")) { + // 跳过标题行(仅一级和二级标题) + if strings.HasPrefix(trimmed, "# ") || strings.HasPrefix(trimmed, "## ") { enhanced = append(enhanced, line) continue } - // 检查是否是陈述句(以句号、问号、感叹号结尾) - needsCitation := strings.HasSuffix(trimmed, "。") || strings.HasSuffix(trimmed, ".") || + // 跳过某些特殊行(如"来源说明"、"免责声明"等) + // 但保留"依据引用"、"分析"、"建议"等内容行 + if (strings.Contains(trimmed, "来源说明") || strings.Contains(trimmed, "免责声明")) && + !strings.Contains(trimmed, "依据") && !strings.Contains(trimmed, "分析") && + !strings.Contains(trimmed, "建议") { + enhanced = append(enhanced, line) + continue + } + + // 跳过已有标注的"知识库引用"和"AI建议"标题行 + if (strings.HasPrefix(trimmed, "知识库引用") || strings.HasPrefix(trimmed, "AI建议")) && + !strings.Contains(trimmed, "【") { + enhanced = append(enhanced, line) + continue + } + + // 对列表项内容也进行标注(包括 - * 和数字列表) + isListItem := strings.HasPrefix(trimmed, "-") || strings.HasPrefix(trimmed, "*") || + (len(trimmed) > 2 && trimmed[0] >= '0' && trimmed[0] <= '9' && trimmed[1] == '.') + + // 检查是否需要标注: + // 1. 以句号、问号、感叹号结尾的陈述句 + // 2. 以问号结尾的问题(包括清单中的"是否"问题) + // 3. 列表项(无论是否以标点符号结尾) + // 4. 其他有实质内容的行(长度>5且不是特殊标记) + 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 - } - } + strings.HasSuffix(trimmed, "?") || strings.HasSuffix(trimmed, "?") || + isListItem) || + (len(trimmed) > 5 && !strings.HasPrefix(trimmed, "【") && !strings.HasPrefix(trimmed, "---")) if needsCitation { // 决定使用哪种标注 @@ -362,8 +385,9 @@ func (h *LLMChatHandler) addCitationsToResponse(response string, hasKnowledge bo if hasKnowledge && len(knowledgeSources) > 0 { // 如果内容较长且像是说明性内容,用AI建议 // 如果内容较短且像是事实陈述,用知识库 - if len(trimmed) > 100 || strings.Contains(trimmed, "建议") || - strings.Contains(trimmed, "注意") || strings.Contains(trimmed, "可以") { + if len(trimmed) > 100 || strings.Contains(trimmed, "建议") || + strings.Contains(trimmed, "注意") || strings.Contains(trimmed, "可以") || + strings.Contains(trimmed, "分析") || strings.Contains(trimmed, "风险") { citation = " [[AI建议]]" } else { citation = " [[知识库:" + knowledgeSources[0] + "]]" @@ -381,14 +405,7 @@ func (h *LLMChatHandler) addCitationsToResponse(response string, hasKnowledge bo } } - result := strings.Join(enhanced, "\n") - - // 如果末尾没有来源说明块,添加一个 - if !strings.Contains(result, "**来源说明**") && !strings.Contains(result, "> **来源说明**") { - result += h.generateSourceSummary(hasKnowledge, knowledgeSources) - } - - return result + return strings.Join(enhanced, "\n") } // fillMissingCitations 为部分标注的回答补充缺失的标注 @@ -431,7 +448,7 @@ func (h *LLMChatHandler) addFirstKBCitation(response string, source string) stri func (h *LLMChatHandler) addAICitationToSuggestions(response string) string { keywords := []string{"建议", "推荐", "可以", "应该", "注意", "提示", "提醒"} lines := strings.Split(response, "\n") - + for i, line := range lines { trimmed := strings.TrimSpace(line) // 检查是否包含建议性关键词 @@ -444,7 +461,7 @@ func (h *LLMChatHandler) addAICitationToSuggestions(response string) string { } } } - + return strings.Join(lines, "\n") } @@ -453,11 +470,11 @@ func (h *LLMChatHandler) generateSourceSummary(hasKnowledge bool, knowledgeSourc 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") + fmt.Fprintf(&summary, "> - 【%s】\n", source) } summary.WriteString(">\n") summary.WriteString("> **AI建议:**\n") @@ -466,7 +483,7 @@ func (h *LLMChatHandler) generateSourceSummary(hasKnowledge bool, knowledgeSourc summary.WriteString("> **AI建议:**\n") summary.WriteString("> - 以上内容为AI建议,仅供参考\n") } - + return summary.String() } @@ -678,7 +695,7 @@ func (h *LLMChatHandler) buildMessages(systemPrompt, knowledgeContext string, ha ### ⚠️ 强制要求(不允许任何例外) -你的回答中**每一句事实陈述、每一个观点**都必须在句子末尾标注来源徽章: +你的回答中**每一句话、每一个观点、每一个列表项**都必须在句子末尾标注来源徽章。这是最高优先级要求,必须100%执行,不允许遗漏。 **格式1:知识库引用(蓝色徽章)** 在引用知识库内容的句子末尾加:[[知识库:文献名称]] @@ -745,6 +762,11 @@ func (h *LLMChatHandler) buildMessages(systemPrompt, knowledgeContext string, ha - [ ] 末尾有完整的来源汇总块 - [ ] 来源汇总中列出了知识库原文摘录 +### 🚨 关键提醒 +- 如果你的回答中有任何句子、列表项、问题没有标注来源,系统会自动补充标注 +- 为了避免系统自动补充,你必须在生成回答时就添加所有标注 +- 特别是对于清单、问卷、检查表等列表格式的内容,每一项都必须标注 + ` if knowledgeContext != "" { finalSystem += "### 📚 知识库检索结果\n\n以下是从知识库中检索到的相关文献,请优先基于这些内容回答,并在每个引用处标注 [[知识库:文献名称]]:\n\n" + knowledgeContext diff --git a/server/internal/handler/chat_llm_test.go b/server/internal/handler/chat_llm_test.go new file mode 100644 index 0000000..2a1fb29 --- /dev/null +++ b/server/internal/handler/chat_llm_test.go @@ -0,0 +1,136 @@ +package handler + +import ( + "strings" + "testing" +) + +// TestEnhanceCitations 测试来源标注增强功能 +func TestEnhanceCitations(t *testing.T) { + h := &LLMChatHandler{} + + tests := []struct { + name string + response string + hasKnowledge bool + knowledgeSources []string + expectedContains []string + }{ + { + name: "清单格式 - 应该为每个问题添加标注", + response: `劳动用工合规审查清单 +一、劳动合同管理 +劳动合同签订 + +是否所有员工均签订了书面劳动合同? +合同内容是否符合《劳动合同法》的相关规定? +是否存在劳动合同未明确约定工作地点、工作内容、劳动报酬等情况?`, + hasKnowledge: true, + knowledgeSources: []string{"劳动合同法"}, + expectedContains: []string{ + "是否所有员工均签订了书面劳动合同? [[", + "合同内容是否符合《劳动合同法》的相关规定? [[", + "是否存在劳动合同未明确约定工作地点、工作内容、劳动报酬等情况? [[", + }, + }, + { + name: "列表项 - 应该为每个列表项添加标注", + response: `办理条件: +- 有合法稳定就业 +- 有合法稳定住所 +- 连续就读`, + hasKnowledge: true, + knowledgeSources: []string{"居住证管理规定"}, + expectedContains: []string{ + "- 有合法稳定就业 [[", + "- 有合法稳定住所 [[", + "- 连续就读 [[", + }, + }, + { + name: "数字列表 - 应该为每个列表项添加标注", + response: `办理流程: +1. 到派出所提交申请材料 +2. 派出所审核材料 +3. 制作并发放居住证`, + hasKnowledge: true, + knowledgeSources: []string{"居住证管理规定"}, + expectedContains: []string{ + "1. 到派出所提交申请材料 [[", + "2. 派出所审核材料 [[", + "3. 制作并发放居住证 [[", + }, + }, + { + name: "陈述句 - 应该添加标注", + response: `居住证办理需要身份证、居住证明和近期照片。 +办理时限为15个工作日。`, + hasKnowledge: true, + knowledgeSources: []string{"居住证管理规定"}, + expectedContains: []string{ + "居住证办理需要身份证、居住证明和近期照片。 [[", + "办理时限为15个工作日。 [[", + }, + }, + { + name: "无知识库 - 应该添加AI建议标注", + response: `建议您提前准备齐全材料。 +如有疑问可先电话咨询。`, + hasKnowledge: false, + knowledgeSources: []string{}, + expectedContains: []string{ + "建议您提前准备齐全材料。 [[AI建议]]", + "如有疑问可先电话咨询。 [[AI建议]]", + }, + }, + { + name: "已有标注 - 不应该重复添加", + response: `居住证办理需要身份证、居住证明和近期照片 [[知识库:居住证管理规定]]。 +建议您提前准备齐全材料 [[AI建议]]。`, + hasKnowledge: true, + knowledgeSources: []string{"居住证管理规定"}, + expectedContains: []string{ + "[[知识库:居住证管理规定]]", + "[[AI建议]]", + "**来源说明**", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := h.enhanceCitations(tt.response, tt.hasKnowledge, tt.knowledgeSources) + + for _, expected := range tt.expectedContains { + if !strings.Contains(result, expected) { + t.Errorf("期望结果包含: %q\n实际结果: %s", expected, result) + } + } + + // 验证来源说明块存在 + if !strings.Contains(result, "**来源说明**") { + t.Errorf("期望结果包含来源说明块") + } + }) + } +} + +// TestAddCitationsToResponse 测试自动添加标注的逻辑 +func TestAddCitationsToResponse(t *testing.T) { + h := &LLMChatHandler{} + + response := `劳动合同管理 +是否所有员工均签订了书面劳动合同? +合同内容是否符合《劳动合同法》的相关规定?` + + result := h.addCitationsToResponse(response, true, []string{"劳动合同法"}) + + // 验证每个问题都被标注了 + if !strings.Contains(result, "是否所有员工均签订了书面劳动合同? [[") { + t.Errorf("第一个问题没有被标注: %s", result) + } + + if !strings.Contains(result, "合同内容是否符合《劳动合同法》的相关规定? [[") { + t.Errorf("第二个问题没有被标注: %s", result) + } +} diff --git a/sync-db-from-server.sh b/sync-db-from-server.sh index 66ef359..c11367b 100755 --- a/sync-db-from-server.sh +++ b/sync-db-from-server.sh @@ -7,7 +7,7 @@ set -euo pipefail # 服务器配置(从 deploy.sh 读取) -SERVER="h2agent" +SERVER="192.168.1.10" REMOTE_DB_USER="govai" REMOTE_DB_PASS="GovAI@2024Secure" REMOTE_DB_NAME="govai_portal"