feat: 完善来源标注徽章功能和配置优化
后端改进: - 改进 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 个单元测试用例,全部通过 - 验证清单项、列表项、陈述句等各种格式的标注 文档: - 添加实现方案文档 - 添加用户使用指南
This commit is contained in:
@@ -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/
|
||||
```
|
||||
|
||||
所有测试通过,代码编译成功。
|
||||
@@ -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)
|
||||
@@ -73,16 +73,16 @@ const mdComponents: Components = {
|
||||
if (href === "#cite-kb") {
|
||||
const label = String(children).replace(/^知识库:/, "");
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 mx-0.5 text-xs font-medium rounded-md bg-blue-50 text-blue-700 border border-blue-200 align-middle">
|
||||
<BookOpen className="h-3 w-3 shrink-0" />
|
||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 mx-0.5 text-xs font-semibold rounded bg-blue-100 text-blue-700 border border-blue-300 align-middle hover:bg-blue-200 transition-colors cursor-pointer">
|
||||
<BookOpen className="h-3.5 w-3.5 shrink-0" />
|
||||
<span>{label}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (href === "#cite-ai") {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 mx-0.5 text-xs font-medium rounded-md bg-amber-50 text-amber-700 border border-amber-200 align-middle whitespace-nowrap">
|
||||
<BrainCircuit className="h-3 w-3 shrink-0" />
|
||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 mx-0.5 text-xs font-semibold rounded bg-orange-100 text-orange-700 border border-orange-300 align-middle whitespace-nowrap hover:bg-orange-200 transition-colors cursor-pointer">
|
||||
<BrainCircuit className="h-3.5 w-3.5 shrink-0" />
|
||||
<span>AI建议</span>
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
+21
-5
@@ -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=
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user