init: RAG 知识库服务初始提交

- 后端 API(Flask + Gunicorn)
- RAG 引擎(混合检索 + 云端 Reranker + 引用溯源)
- 文档解析(MinerU + 多格式支持)
- Docker 生产部署配置
- 排除前端项目、敏感配置、模型文件
This commit is contained in:
lacerate551
2026-06-04 17:35:27 +08:00
commit 100d1a06eb
158 changed files with 64534 additions and 0 deletions

711
knowledge/router.py Normal file
View File

@@ -0,0 +1,711 @@
"""
知识库路由器 - 智能选择查询目标
功能:
1. 查询意图分析 - 判断查询是否涉及特定部门
2. 知识库路由 - 根据意图和权限选择目标向量库
3. 单库优化 - 如果只需查询单库,避免不必要的并行检索
使用方式:
from knowledge.router import KnowledgeBaseRouter
router = KnowledgeBaseRouter()
# 获取目标向量库
target_kbs = router.route(
query="财务部的报销流程是什么",
role="user",
department="tech"
)
# 返回: ["public_kb", "dept_finance"] # 如果有权限
"""
import os
import re
import json
import logging
from typing import List, Dict, Optional, Tuple
from dataclasses import dataclass
from openai import OpenAI
# 导入配置
from config import API_KEY, BASE_URL, MODEL
# 导入 LLM 工具函数
from core.llm_utils import call_llm, parse_json_from_response
# 导入权限管理
from auth.gateway import get_accessible_collections, normalize_department_name
# 设置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# ==================== 部门关键词配置 ====================
# 部门关键词映射(可根据实际情况扩展)
DEPARTMENT_KEYWORDS = {
"finance": [
"财务", "报销", "发票", "预算", "支出", "收入", "成本",
"账目", "会计", "审计", "税务", "工资", "奖金", "补贴",
"费用", "付款", "收款", "借款", "报销单", "财务部"
],
"hr": [
"人事", "招聘", "入职", "离职", "考勤", "请假", "休假",
"员工", "培训", "绩效", "晋升", "调岗", "合同", "档案",
"社保", "公积金", "福利", "加班", "年假", "人事部", "人力资源"
],
"tech": [
"技术", "开发", "代码", "系统", "服务器", "数据库", "API",
"接口", "部署", "测试", "Bug", "需求", "架构", "运维",
"网络安全", "服务器", "云服务", "技术部", "研发", "IT"
],
"operation": [
"运营", "推广", "营销", "活动", "用户", "增长", "数据",
"分析", "客服", "售后", "投诉", "反馈", "运营部", "运营中心"
],
"marketing": [
"市场", "品牌", "宣传", "广告", "公关", "媒体", "推广",
"展会", "活动策划", "市场部", "营销部"
],
"legal": [
"法务", "合同", "法律", "诉讼", "合规", "风险", "版权",
"知识产权", "协议", "法务部"
],
"admin": [
"行政", "办公室", "会议室", "采购", "固定资产", "办公用品",
"印章", "档案", "行政部", "总务"
]
}
# 通用关键词(查询 public_kb
GENERAL_KEYWORDS = [
"公司", "企业", "组织", "介绍", "简介", "文化", "价值观",
"制度", "规定", "流程", "政策", "手册", "指南", "帮助",
"联系方式", "地址", "电话", "邮箱"
]
# ==================== 数据结构 ====================
@dataclass
class QueryIntent:
"""查询意图"""
is_general: bool # 是否为通用问题
department: Optional[str] # 涉及的部门(如果有)
confidence: float # 置信度
keywords: List[str] # 匹配到的关键词
reason: str # 判断理由
# ==================== 知识库路由器 ====================
class KnowledgeBaseRouter:
"""
知识库路由器
根据查询内容和用户权限,智能选择需要查询的向量库。
支持规则匹配和 LLM 意图分析两种方式。
"""
def __init__(self, use_llm: bool = True):
"""
初始化
Args:
use_llm: 是否使用 LLM 进行意图分析(更准确但更慢)
"""
self.use_llm = use_llm
self.llm_client = None
if use_llm:
try:
self.llm_client = OpenAI(api_key=API_KEY, base_url=BASE_URL)
logger.info("LLM 客户端初始化成功,将使用 LLM 进行意图分析")
except Exception as e:
logger.warning(f"LLM 客户端初始化失败: {e},将使用规则匹配")
self.use_llm = False
def route(
self,
query: str,
role: str,
department: str,
accessible_collections: List[str] = None
) -> List[str]:
"""
根据查询意图和用户权限,决定查询哪些向量库
Args:
query: 用户查询
role: 用户角色
department: 用户部门
accessible_collections: 可访问的向量库列表(可选)
Returns:
需要查询的向量库名称列表
"""
# 1. 获取可访问的向量库
if accessible_collections is None:
accessible_collections = get_accessible_collections(role, department)
if not accessible_collections:
logger.warning(f"用户无可访问的向量库: role={role}, dept={department}")
return []
# 2. 分析查询意图
intent = self.analyze_intent(query)
# 3. 根据意图选择目标库
target_kbs = self._select_knowledge_bases(
intent, accessible_collections, role, department
)
logger.info(
f"路由决策: query='{query[:30]}...', "
f"intent={intent.department or 'general'}, "
f"targets={target_kbs}"
)
return target_kbs
def analyze_intent(self, query: str) -> QueryIntent:
"""
分析查询意图
Args:
query: 用户查询
Returns:
QueryIntent 对象
"""
# 先尝试规则匹配(快速)
rule_intent = self._analyze_by_rules(query)
# 如果规则匹配置信度高,直接返回
if rule_intent.confidence > 0.8:
return rule_intent
# 否则使用 LLM 分析(更准确)
if self.use_llm and self.llm_client:
llm_intent = self._analyze_by_llm(query)
if llm_intent:
# 取两者中置信度高的
return llm_intent if llm_intent.confidence > rule_intent.confidence else rule_intent
return rule_intent
def _analyze_by_rules(self, query: str) -> QueryIntent:
"""基于规则的意图分析"""
query_lower = query.lower()
matched_departments = {}
matched_general = []
# 检查部门关键词
for dept, keywords in DEPARTMENT_KEYWORDS.items():
for keyword in keywords:
if keyword in query_lower:
if dept not in matched_departments:
matched_departments[dept] = []
matched_departments[dept].append(keyword)
# 检查通用关键词
for keyword in GENERAL_KEYWORDS:
if keyword in query_lower:
matched_general.append(keyword)
# 判断结果
if matched_departments:
# 找到匹配最多的部门
best_dept = max(
matched_departments.keys(),
key=lambda d: len(matched_departments[d])
)
keywords = matched_departments[best_dept]
confidence = min(0.9, 0.5 + len(keywords) * 0.1)
return QueryIntent(
is_general=False,
department=best_dept,
confidence=confidence,
keywords=keywords,
reason=f"匹配到部门关键词: {', '.join(keywords)}"
)
elif matched_general:
return QueryIntent(
is_general=True,
department=None,
confidence=0.7,
keywords=matched_general,
reason=f"匹配到通用关键词: {', '.join(matched_general)}"
)
else:
return QueryIntent(
is_general=False,
department=None,
confidence=0.3,
keywords=[],
reason="未匹配到关键词,需要查询所有可访问的库"
)
def _analyze_by_llm(self, query: str) -> Optional[QueryIntent]:
"""使用 LLM 进行意图分析"""
prompt = f"""分析以下问题的意图,判断:
1. 是否为通用问题(涉及公司整体、产品、文化等,不特指某部门)
2. 是否涉及特定部门(财务、人事、技术等)
问题:{query}
请直接返回 JSON 格式(不要包含其他内容):
{{"is_general": true/false, "department": "部门英文名或null", "confidence": 0.0-1.0}}
部门英文名对照:
- finance: 财务
- hr: 人事
- tech: 技术
- operation: 运营
- marketing: 市场
- legal: 法务
- admin: 行政
注意:
- 如果问题涉及多个部门,返回 null
- 如果问题明显指向某个部门,返回对应英文名
- confidence 表示判断置信度0-1之间"""
content = call_llm(
self.llm_client, prompt, MODEL,
temperature=0.1,
max_tokens=100
)
if content is None:
logger.warning("LLM 意图分析失败: 调用返回空")
return None
# 使用 parse_json_from_response 解析 JSON
result = parse_json_from_response(content)
if result is None:
logger.warning(f"LLM 意图分析失败: JSON 解析失败,原始内容: {content[:100]}")
return None
return QueryIntent(
is_general=result.get("is_general", False),
department=result.get("department"),
confidence=result.get("confidence", 0.5),
keywords=[],
reason="LLM 意图分析"
)
def _select_knowledge_bases(
self,
intent: QueryIntent,
accessible_collections: List[str],
role: str,
department: str
) -> List[str]:
"""
选择要查询的知识库
Args:
intent: 查询意图
accessible_collections: 可访问的向量库
role: 用户角色
department: 用户部门
Returns:
目标向量库列表
"""
result = []
public_kb = "public_kb"
# 通用问题:优先查 public_kb
if intent.is_general:
if public_kb in accessible_collections:
result.append(public_kb)
# 但也可能需要查其他库(取决于置信度)
if intent.confidence < 0.7:
result.extend([kb for kb in accessible_collections if kb not in result])
# 涉及特定部门
elif intent.department:
dept_kb = f"dept_{intent.department}"
# 检查是否有权限访问该部门
if dept_kb in accessible_collections:
result.append(dept_kb)
# 也查 public_kb可能有相关政策
if public_kb in accessible_collections and public_kb not in result:
result.append(public_kb)
else:
# 没有权限访问目标部门,查 public_kb
if public_kb in accessible_collections:
result.append(public_kb)
logger.info(
f"用户无权访问部门 {intent.department} 的知识库,"
f"只查 public_kb"
)
# 未识别意图
else:
# admin 查所有
if role == "admin":
result = accessible_collections
# 其他用户查 public 和本部门
else:
if public_kb in accessible_collections:
result.append(public_kb)
# 使用标准化的部门名称
normalized_dept = normalize_department_name(department)
if normalized_dept:
user_dept_kb = f"dept_{normalized_dept}"
if user_dept_kb in accessible_collections and user_dept_kb not in result:
result.append(user_dept_kb)
# 去重并保持顺序
seen = set()
unique_result = []
for kb in result:
if kb not in seen:
seen.add(kb)
unique_result.append(kb)
return unique_result
def get_routing_stats(self) -> Dict:
"""获取路由统计信息(用于监控)"""
return {
"use_llm": self.use_llm,
"department_keywords": {
dept: len(keywords)
for dept, keywords in DEPARTMENT_KEYWORDS.items()
},
"general_keywords_count": len(GENERAL_KEYWORDS)
}
# ==================== 版本感知检索 ====================
def route_with_version_awareness(
self,
query: str,
role: str,
department: str,
accessible_collections: List[str] = None,
include_deprecated: bool = False,
top_k: int = 5
) -> Dict:
"""
版本感知的路由
在普通路由基础上,额外查询已废止的相关文档,
为用户提供版本提示。
Args:
query: 用户查询
role: 用户角色
department: 用户部门
accessible_collections: 可访问的向量库列表
include_deprecated: 是否包含废止版本在结果中
top_k: 返回数量
Returns:
{
"target_collections": ["public_kb", "dept_finance"],
"version_hints": [
{
"document": "报销制度.pdf",
"status": "deprecated",
"message": "该文档已于2026-03-01废止"
}
]
}
"""
# 1. 获取目标向量库(复用现有逻辑)
target_kbs = self.route(query, role, department, accessible_collections)
if not target_kbs:
return {
"target_collections": [],
"version_hints": []
}
# 2. 查询是否有相关的废止版本
version_hints = []
if not include_deprecated:
version_hints = self._find_deprecated_versions(query, target_kbs, top_k=3)
logger.info(
f"版本感知路由: query='{query[:30]}...', "
f"targets={target_kbs}, hints={len(version_hints)}"
)
return {
"target_collections": target_kbs,
"version_hints": version_hints
}
def _find_deprecated_versions(
self,
query: str,
collections: List[str],
top_k: int = 3
) -> List[Dict]:
"""
查找与查询相关的已废止版本
Args:
query: 用户查询
collections: 目标向量库列表
top_k: 每个库返回数量
Returns:
已废止版本提示列表
"""
try:
from knowledge.manager import get_kb_manager
kb_manager = get_kb_manager()
# 获取查询向量
query_vector = self._get_query_vector(query)
if query_vector is None:
return []
# 使用知识库管理器查找废止版本
hints = kb_manager.find_deprecated_versions(
kb_names=collections,
query_vector=query_vector,
top_k=top_k
)
# 去重(同一文档只提示一次)
seen_docs = set()
unique_hints = []
for hint in hints:
doc_key = f"{hint['collection']}/{hint['document']}"
if doc_key not in seen_docs:
seen_docs.add(doc_key)
unique_hints.append(hint)
return unique_hints
except Exception as e:
logger.warning(f"查找废止版本失败: {e}")
return []
def _get_query_vector(self, query: str) -> Optional[List[float]]:
"""
获取查询向量
Args:
query: 查询文本
Returns:
查询向量失败返回None
"""
try:
# 尝试使用 RAGEngine 的 embedding_model
from core.engine import get_engine
embedding_model = get_engine().embedding_model
return embedding_model.encode(query).tolist()
except Exception as e:
logger.debug(f"无法从 RAGEngine 获取向量模型: {e}")
try:
# 尝试使用 sentence-transformers
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('BAAI/bge-base-zh-v1.5')
return model.encode(query).tolist()
except Exception as e:
logger.debug(f"Embedding 编码失败: {e}")
logger.warning("无法加载向量模型,跳过废止版本检测")
return None
def search_with_version_context(
self,
query: str,
role: str,
department: str,
top_k: int = 5
) -> Dict:
"""
带版本上下文的搜索
执行完整搜索流程:
1. 版本感知路由
2. 执行检索(只返回生效版本)
3. 返回结果 + 废止版本提示
Args:
query: 用户查询
role: 用户角色
department: 用户部门
top_k: 返回数量
Returns:
{
"results": [...], # 生效版本的检索结果
"version_hints": [...], # 废止版本提示
"target_collections": [...]
}
"""
from knowledge.manager import get_kb_manager
kb_manager = get_kb_manager()
# 1. 版本感知路由
route_result = self.route_with_version_awareness(
query, role, department, include_deprecated=False
)
target_kbs = route_result["target_collections"]
version_hints = route_result["version_hints"]
if not target_kbs:
return {
"results": [],
"version_hints": version_hints,
"target_collections": []
}
# 2. 执行检索(只返回生效版本)
query_vector = self._get_query_vector(query)
if query_vector is None:
return {
"results": [],
"version_hints": version_hints,
"target_collections": target_kbs
}
# 多库检索只返回active状态的chunks
search_result = kb_manager.search_multiple(
kb_names=target_kbs,
query_vector=query_vector,
query_text=query,
top_k=top_k,
use_bm25=True
)
# 过滤只返回active状态的chunks
active_results = []
if search_result.ids:
for i, (doc_id, doc, meta, score) in enumerate(zip(
search_result.ids,
search_result.documents,
search_result.metadatas,
search_result.distances
)):
if meta.get("status", "active") == "active":
active_results.append({
"id": doc_id,
"document": doc,
"metadata": meta,
"score": score
})
return {
"results": active_results[:top_k],
"version_hints": version_hints,
"target_collections": target_kbs
}
# ==================== 全局实例 ====================
_kb_router: Optional[KnowledgeBaseRouter] = None
def get_kb_router() -> KnowledgeBaseRouter:
"""获取全局知识库路由器实例"""
global _kb_router
if _kb_router is None:
_kb_router = KnowledgeBaseRouter()
return _kb_router
# ==================== 便捷函数 ====================
def route_query(
query: str,
role: str,
department: str,
accessible_collections: List[str] = None
) -> List[str]:
"""
路由查询到目标知识库(便捷函数)
Args:
query: 用户查询
role: 用户角色
department: 用户部门
accessible_collections: 可访问的向量库列表
Returns:
目标向量库列表
"""
router = get_kb_router()
return router.route(query, role, department, accessible_collections)
def route_query_with_version(
query: str,
role: str,
department: str,
accessible_collections: List[str] = None,
include_deprecated: bool = False
) -> Dict:
"""
版本感知的路由(便捷函数)
Args:
query: 用户查询
role: 用户角色
department: 用户部门
accessible_collections: 可访问的向量库列表
include_deprecated: 是否包含废止版本
Returns:
{
"target_collections": [...],
"version_hints": [...]
}
"""
router = get_kb_router()
return router.route_with_version_awareness(
query, role, department, accessible_collections, include_deprecated
)
def search_with_version_context(
query: str,
role: str,
department: str,
top_k: int = 5
) -> Dict:
"""
带版本上下文的搜索(便捷函数)
Args:
query: 用户查询
role: 用户角色
department: 用户部门
top_k: 返回数量
Returns:
{
"results": [...],
"version_hints": [...],
"target_collections": [...]
}
"""
router = get_kb_router()
return router.search_with_version_context(query, role, department, top_k)