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

576
core/quality_assessor.py Normal file
View File

@@ -0,0 +1,576 @@
"""
多维质量评估模块
对检索结果进行 4 维质量评估:
1. 相关性Relevance答案是否切题
2. 完整性Completeness信息是否充分
3. 准确性Accuracy是否有冲突信息
4. 覆盖率Coverage多角度覆盖
总阈值32/40 (80%)
使用方式:
from core.quality_assessor import QualityAssessor, assess_quality
assessor = QualityAssessor(llm_client)
result = assessor.assess(query, documents)
if result.total_score >= 32:
# 质量合格,继续生成
...
"""
from dataclasses import dataclass
from typing import List, Optional
from enum import Enum
from config import RAG_CHAT_MODEL
import logging
logger = logging.getLogger(__name__)
class QualityDimension(Enum):
"""质量评估维度"""
RELEVANCE = "relevance" # 相关性:答案是否切题
COMPLETENESS = "completeness" # 完整性:信息是否充分
ACCURACY = "accuracy" # 准确性:是否有冲突
COVERAGE = "coverage" # 覆盖率:多角度覆盖
@dataclass
class DimensionScore:
"""单个维度的评分"""
dimension: QualityDimension
score: int # 0-10 分
reason: str # 评分原因
issues: List[str] # 发现的问题
@dataclass
class QualityAssessment:
"""质量评估结果"""
relevance: DimensionScore
completeness: DimensionScore
accuracy: DimensionScore
coverage: DimensionScore
total_score: int # 总分0-40
is_sufficient: bool # 是否达标(>= 32
summary: str # 评估总结
recommendations: List[str] # 改进建议
class QualityAssessor:
"""
多维质量评估器
基于报告建议的 4 维评估框架,对检索结果进行全面质量检查。
"""
# 质量阈值(报告建议值)
QUALITY_THRESHOLD = 32 # 总分阈值40分制80%
DIMENSION_WEIGHTS = {
QualityDimension.RELEVANCE: 1.0,
QualityDimension.COMPLETENESS: 1.0,
QualityDimension.ACCURACY: 1.0,
QualityDimension.COVERAGE: 1.0,
}
def __init__(self, llm_client=None, model: str = None):
"""
初始化评估器
Args:
llm_client: LLM 客户端(用于语义评估)
model: 模型名称
"""
self.llm_client = llm_client
self.model = model or RAG_CHAT_MODEL
def assess(self, query: str, documents: List[str],
metadatas: List[dict] = None) -> QualityAssessment:
"""
评估检索结果质量
Args:
query: 用户查询
documents: 检索到的文档列表
metadatas: 文档元数据(可选)
Returns:
QualityAssessment: 质量评估结果
"""
if not documents:
return self._empty_assessment()
# 使用 LLM 进行语义评估
if self.llm_client:
return self._llm_assess(query, documents, metadatas)
else:
# 降级:基于规则的评估
return self._rule_based_assess(query, documents, metadatas)
def _llm_assess(self, query: str, documents: List[str],
metadatas: List[dict] = None) -> QualityAssessment:
"""
使用 LLM 进行语义质量评估
"""
# 构建文档摘要
doc_summary = self._summarize_documents(documents, metadatas)
prompt = f"""请对以下检索结果进行多维质量评估。
## 用户查询
{query}
## 检索到的文档摘要
{doc_summary}
## 评估要求
请从 4 个维度进行评分(每个维度 0-10 分):
1. **相关性Relevance**:文档内容是否直接回答用户问题?
- 10分完全相关直接回答
- 7-9分高度相关核心内容匹配
- 4-6分部分相关有侧面信息
- 0-3分几乎无关
2. **完整性Completeness**:信息是否充分完整?
- 10分信息完整可直接回答
- 7-9分基本完整缺少次要细节
- 4-6分部分完整缺少关键信息
- 0-3分信息严重不足
3. **准确性Accuracy**:文档之间是否有矛盾冲突?
- 10分信息一致无冲突
- 7-9分有轻微表述差异但不影响理解
- 4-6分有明显矛盾需要辨别
- 0-3分严重冲突信息不可靠
4. **覆盖率Coverage**:是否从多个角度/来源覆盖问题?
- 10分多来源、多角度全面覆盖
- 7-9分有多个相关来源
- 4-6分单一来源但有不同方面
- 0-3分覆盖角度单一
请以 JSON 格式返回评估结果:
```json
{{
"relevance": {{"score": 8, "reason": "原因", "issues": ["问题1"]}},
"completeness": {{"score": 7, "reason": "原因", "issues": []}},
"accuracy": {{"score": 9, "reason": "原因", "issues": []}},
"coverage": {{"score": 6, "reason": "原因", "issues": ["问题1"]}},
"summary": "整体评估总结",
"recommendations": ["建议1", "建议2"]
}}
```"""
try:
from core.llm_utils import call_llm
content = call_llm(
self.llm_client,
prompt,
self.model,
temperature=0.1,
max_tokens=800
)
if content is None:
return self._rule_based_assess(query, documents, metadatas)
return self._parse_llm_response(content)
except Exception as e:
logger.warning(f"LLM 质量评估失败: {e}")
return self._rule_based_assess(query, documents, metadatas)
def _parse_llm_response(self, content: str) -> QualityAssessment:
"""解析 LLM 返回的 JSON"""
import json
import re
# 提取 JSON 块
json_match = re.search(r'```json\s*([\s\S]*?)\s*```', content)
if json_match:
json_str = json_match.group(1)
else:
# 尝试直接解析
json_str = content
try:
data = json.loads(json_str)
except json.JSONDecodeError:
# 解析失败,返回默认评估
return self._default_assessment()
# 构建维度评分
relevance = DimensionScore(
dimension=QualityDimension.RELEVANCE,
score=min(10, max(0, data.get("relevance", {}).get("score", 5))),
reason=data.get("relevance", {}).get("reason", ""),
issues=data.get("relevance", {}).get("issues", [])
)
completeness = DimensionScore(
dimension=QualityDimension.COMPLETENESS,
score=min(10, max(0, data.get("completeness", {}).get("score", 5))),
reason=data.get("completeness", {}).get("reason", ""),
issues=data.get("completeness", {}).get("issues", [])
)
accuracy = DimensionScore(
dimension=QualityDimension.ACCURACY,
score=min(10, max(0, data.get("accuracy", {}).get("score", 5))),
reason=data.get("accuracy", {}).get("reason", ""),
issues=data.get("accuracy", {}).get("issues", [])
)
coverage = DimensionScore(
dimension=QualityDimension.COVERAGE,
score=min(10, max(0, data.get("coverage", {}).get("score", 5))),
reason=data.get("coverage", {}).get("reason", ""),
issues=data.get("coverage", {}).get("issues", [])
)
total_score = relevance.score + completeness.score + accuracy.score + coverage.score
return QualityAssessment(
relevance=relevance,
completeness=completeness,
accuracy=accuracy,
coverage=coverage,
total_score=total_score,
is_sufficient=total_score >= self.QUALITY_THRESHOLD,
summary=data.get("summary", ""),
recommendations=data.get("recommendations", [])
)
def _rule_based_assess(self, query: str, documents: List[str],
metadatas: List[dict] = None) -> QualityAssessment:
"""
基于规则的质量评估(降级方案)
"""
# 相关性:基于关键词匹配
relevance_score = self._assess_relevance(query, documents)
# 完整性:基于文档长度和数量
completeness_score = self._assess_completeness(documents)
# 准确性:假设一致(降级方案无法检测冲突)
accuracy_score = 8 # 默认较高分数
# 覆盖率:基于来源多样性
coverage_score = self._assess_coverage(documents, metadatas)
total_score = relevance_score + completeness_score + accuracy_score + coverage_score
return QualityAssessment(
relevance=DimensionScore(
dimension=QualityDimension.RELEVANCE,
score=relevance_score,
reason=f"关键词匹配评估(规则降级)",
issues=[]
),
completeness=DimensionScore(
dimension=QualityDimension.COMPLETENESS,
score=completeness_score,
reason=f"基于文档长度和数量评估(规则降级)",
issues=[]
),
accuracy=DimensionScore(
dimension=QualityDimension.ACCURACY,
score=accuracy_score,
reason="降级方案:假设信息一致",
issues=[]
),
coverage=DimensionScore(
dimension=QualityDimension.COVERAGE,
score=coverage_score,
reason=f"基于来源多样性评估(规则降级)",
issues=[]
),
total_score=total_score,
is_sufficient=total_score >= self.QUALITY_THRESHOLD,
summary="基于规则的质量评估LLM 不可用)",
recommendations=["建议启用 LLM 进行更精确的语义评估"]
)
def _assess_relevance(self, query: str, documents: List[str]) -> int:
"""评估相关性(关键词匹配)"""
try:
import jieba
# 提取查询关键词
query_words = set()
for word in jieba.cut(query):
word = word.strip()
if len(word) >= 2:
query_words.add(word.lower())
if not query_words:
return 5
# 计算每个文档的关键词覆盖率
coverages = []
for doc in documents:
doc_lower = doc.lower()
matched = sum(1 for word in query_words if word in doc_lower)
coverages.append(matched / len(query_words))
avg_coverage = sum(coverages) / len(coverages) if coverages else 0
# 映射到 0-10 分
if avg_coverage >= 0.8:
return 9
elif avg_coverage >= 0.6:
return 7
elif avg_coverage >= 0.4:
return 5
elif avg_coverage >= 0.2:
return 3
else:
return 1
except ImportError:
return 5
def _assess_completeness(self, documents: List[str]) -> int:
"""评估完整性(基于文档长度和数量)"""
if not documents:
return 0
# 文档数量评估
doc_count_score = min(3, len(documents)) # 最多 3 分
# 文档长度评估
total_length = sum(len(doc) for doc in documents)
if total_length >= 2000:
length_score = 7
elif total_length >= 1000:
length_score = 5
elif total_length >= 500:
length_score = 3
else:
length_score = 1
return min(10, doc_count_score + length_score)
def _assess_coverage(self, documents: List[str],
metadatas: List[dict] = None) -> int:
"""评估覆盖率(来源多样性)"""
if not metadatas:
# 无元数据,基于文档内容差异度
if len(documents) >= 3:
return 7
elif len(documents) >= 2:
return 5
else:
return 3
# 统计来源多样性
sources = set()
for meta in metadatas:
if isinstance(meta, dict):
source = meta.get("source", "")
if source:
sources.add(source)
source_count = len(sources)
if source_count >= 3:
return 9
elif source_count >= 2:
return 7
elif source_count == 1:
return 5
else:
return 3
def _summarize_documents(self, documents: List[str],
metadatas: List[dict] = None) -> str:
"""生成文档摘要用于 LLM 评估"""
summary_parts = []
for i, doc in enumerate(documents[:5], 1): # 最多 5 个文档
source = ""
if metadatas and i <= len(metadatas):
meta = metadatas[i - 1]
if isinstance(meta, dict):
source = meta.get("source", "未知来源")
page = meta.get("page", "")
if page:
source += f" (第{page}页)"
content = doc[:300] + "..." if len(doc) > 300 else doc
summary_parts.append(f"### 文档 {i} ({source})\n{content}")
return "\n\n".join(summary_parts)
def _empty_assessment(self) -> QualityAssessment:
"""空评估结果"""
return QualityAssessment(
relevance=DimensionScore(
dimension=QualityDimension.RELEVANCE,
score=0,
reason="无检索结果",
issues=[]
),
completeness=DimensionScore(
dimension=QualityDimension.COMPLETENESS,
score=0,
reason="无检索结果",
issues=[]
),
accuracy=DimensionScore(
dimension=QualityDimension.ACCURACY,
score=0,
reason="无检索结果",
issues=[]
),
coverage=DimensionScore(
dimension=QualityDimension.COVERAGE,
score=0,
reason="无检索结果",
issues=[]
),
total_score=0,
is_sufficient=False,
summary="无检索结果可供评估",
recommendations=["请尝试其他查询方式"]
)
def _default_assessment(self) -> QualityAssessment:
"""默认评估结果(解析失败时)"""
return QualityAssessment(
relevance=DimensionScore(
dimension=QualityDimension.RELEVANCE,
score=5,
reason="评估解析失败,使用默认分数",
issues=[]
),
completeness=DimensionScore(
dimension=QualityDimension.COMPLETENESS,
score=5,
reason="评估解析失败,使用默认分数",
issues=[]
),
accuracy=DimensionScore(
dimension=QualityDimension.ACCURACY,
score=5,
reason="评估解析失败,使用默认分数",
issues=[]
),
coverage=DimensionScore(
dimension=QualityDimension.COVERAGE,
score=5,
reason="评估解析失败,使用默认分数",
issues=[]
),
total_score=20,
is_sufficient=False,
summary="LLM 评估解析失败",
recommendations=["请检查 LLM 响应格式"]
)
def get_threshold_info(self) -> dict:
"""获取阈值信息"""
return {
"quality_threshold": self.QUALITY_THRESHOLD,
"max_score": 40,
"pass_percentage": f"{self.QUALITY_THRESHOLD / 40 * 100}%",
"dimensions": ["relevance", "completeness", "accuracy", "coverage"]
}
def create_assessor() -> QualityAssessor:
"""
创建质量评估器实例
自动从配置获取 LLM 客户端。
Returns:
QualityAssessor: 质量评估器实例
"""
try:
from openai import OpenAI
try:
from config import API_KEY, BASE_URL, RAG_CHAT_MODEL
MODEL = RAG_CHAT_MODEL
except ImportError:
from config import API_KEY, BASE_URL
MODEL = "qwen3.5-flash" # fallback
client = OpenAI(api_key=API_KEY, base_url=BASE_URL)
return QualityAssessor(llm_client=client, model=MODEL)
except Exception as e:
logger.warning(f"创建质量评估器失败,使用降级模式: {e}")
return QualityAssessor()
def assess_quality(query: str, documents: List[str],
metadatas: List[dict] = None) -> QualityAssessment:
"""
便捷函数:评估检索结果质量
Args:
query: 用户查询
documents: 检索到的文档列表
metadatas: 文档元数据(可选)
Returns:
QualityAssessment: 质量评估结果
"""
assessor = create_assessor()
return assessor.assess(query, documents, metadatas)
# ==================== 测试 ====================
if __name__ == "__main__":
import sys
if sys.platform == 'win32':
sys.stdout.reconfigure(encoding='utf-8')
print("=" * 60)
print("多维质量评估测试")
print("=" * 60)
# 测试用例
test_cases = [
{
"query": "公司报销制度是怎样的?",
"documents": [
"公司报销制度规定员工可以报销差旅费用,需提供发票和审批单。报销流程:提交申请 -> 部门审批 -> 财务审核 -> 打款。",
"差旅报销标准高铁一等座、飞机经济舱、住宿每天500元内。超过标准需特批。",
"报销时限费用发生后30天内提交逾期不予受理。"
],
"description": "高质量检索结果"
},
{
"query": "量子计算的基本原理",
"documents": [
"文档中提到了一些技术细节...",
"另一个不相关的内容..."
],
"description": "低质量检索结果"
}
]
assessor = QualityAssessor() # 不使用 LLM 的规则评估
print(f"\n阈值配置: {assessor.get_threshold_info()}")
print()
for i, case in enumerate(test_cases, 1):
print(f"测试 {i}: {case['description']}")
print(f"查询: {case['query']}")
print(f"文档数: {len(case['documents'])}")
result = assessor.assess(case['query'], case['documents'])
print(f"\n评分结果:")
print(f" 相关性: {result.relevance.score}/10 - {result.relevance.reason}")
print(f" 完整性: {result.completeness.score}/10 - {result.completeness.reason}")
print(f" 准确性: {result.accuracy.score}/10 - {result.accuracy.reason}")
print(f" 覆盖率: {result.coverage.score}/10 - {result.coverage.reason}")
print(f"\n 总分: {result.total_score}/40")
print(f" 达标: {'✅ 是' if result.is_sufficient else '❌ 否'} (阈值: 32)")
print(f" 总结: {result.summary}")
print()