Files
rag/api/feedback_routes.py
lacerate551 183a57e7f1 refactor(api): 统一响应格式迁移 + 异步任务系统 + 状态码体系完善
- 将全部路由文件(12个)的 jsonify 响应迁移至 success_response/error_response 统一格式
- 修复 sync_routes.py error_response 参数错误(P0)
- 新增异步任务系统:task_registry + task_routes
- 新增状态码:TASK_NOT_FOUND(4014)、TASK_CONFLICT(4015)、REINDEX_ERROR(5040)
- 修正 task_routes/exam_pkg 中语义不匹配的状态码
- 更新 curl 测试手册、后端对接规范文档
- 添加缓存性能报告和 Redis 迁移计划
2026-06-05 22:56:00 +08:00

508 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
问答质量闭环 API
路由:
- POST /feedback - 提交反馈
- GET /feedback/stats - 反馈统计
- GET /feedback/list - 反馈列表
- GET /feedback/bad-cases - Bad Case 分析(管理员)
- GET /feedback/blacklist - Chunk 黑名单(管理员)
- GET /reports/weekly - 周报告
- GET /reports/monthly - 月报告
- GET /faq - FAQ列表
- POST /faq - 新增FAQ (管理员,需二次确认)
- PUT /faq/<faq_id> - 更新FAQ (管理员)
- DELETE /faq/<faq_id> - 删除FAQ (管理员)
- POST /faq/<faq_id>/approve - 批准FAQ并同步知识库 (管理员)
- GET /faq/suggestions - FAQ建议列表 (管理员)
- POST /faq/suggestions/<id>/approve - 批准建议并同步知识库 (管理员)
- POST /faq/suggestions/<id>/reject - 拒绝建议 (管理员)
安全设计(二次确认机制):
所有 FAQ 入库都需要管理员二次确认,防止错误数据污染知识库:
1. 用户反馈 → FAQ 建议 (pending)
2. 管理员创建 → FAQ 草稿 (draft)
3. 二次确认 → 同步 ChromaDB (approved)
反馈飞轮机制:
1. 用户反馈 → 自动沉淀为 FAQ 建议(复合分数 > 0.5
2. 管理员批准 → FAQ 同步到 ChromaDB问题扩写 + 向量化)
3. 检索时 → FAQ 分数加权 + 时间衰减 + 黑名单过滤
4. LLM 生成 → FAQ 作为 Golden Context 融合回答
"""
from flask import Blueprint, request, jsonify
from auth.gateway import require_gateway_auth, require_role
from core.status_codes import (
SUCCESS, CREATED, DELETE_SUCCESS, UPDATE_SUCCESS,
BAD_REQUEST, NOT_FOUND, INTERNAL_ERROR
)
from api.response_utils import success_response, error_response
import logging
logger = logging.getLogger(__name__)
feedback_bp = Blueprint('feedback', __name__)
# 延迟初始化:在 Blueprint 注册时通过 app.config 获取
_feedback_db = None
_feedback_service = None
def _get_feedback_db():
global _feedback_db
if _feedback_db is None:
from services.feedback import FeedbackDB
_feedback_db = FeedbackDB()
return _feedback_db
def _get_feedback_service():
global _feedback_service
if _feedback_service is None:
from services.feedback import FeedbackService
_feedback_service = FeedbackService(_get_feedback_db())
return _feedback_service
@feedback_bp.route('/feedback', methods=['POST'])
@require_gateway_auth
def submit_feedback():
"""提交反馈"""
data = request.get_json()
session_id = data.get('session_id')
query = data.get('query')
answer = data.get('answer')
rating = data.get('rating') # 1=赞, -1=踩
sources = data.get('sources', [])
reason = data.get('reason', '')
user_id = data.get('user_id', '')
if not session_id or not query or rating is None:
return error_response("MISSING_PARAMS", BAD_REQUEST, "缺少必要参数", http_status=400)
if rating not in [1, -1]:
return error_response("INVALID_PARAMS", BAD_REQUEST, "rating 必须是 1 或 -1", http_status=400)
try:
feedback_service = _get_feedback_service()
result = feedback_service.submit_feedback(
session_id=session_id,
query=query,
answer=answer or "",
rating=rating,
sources=sources,
reason=reason,
user_id=user_id
)
return success_response(data={
"feedback_id": result['feedback_id'],
"faq_suggested": result.get('faq_suggested', False),
"suggestion_id": result.get('suggestion_id')
})
except Exception as e:
logger.error(f"反馈操作异常: {e}")
return error_response("OPERATION_FAILED", INTERNAL_ERROR, "操作失败,请稍后重试", http_status=500)
@feedback_bp.route('/feedback/stats', methods=['GET'])
@require_gateway_auth
def get_feedback_stats():
"""获取反馈统计"""
start_date = request.args.get('start_date')
end_date = request.args.get('end_date')
try:
feedback_db = _get_feedback_db()
stats = feedback_db.get_feedback_stats(start_date, end_date)
return success_response(data={
"stats": stats
})
except Exception as e:
logger.error(f"反馈操作异常: {e}")
return error_response("OPERATION_FAILED", INTERNAL_ERROR, "操作失败,请稍后重试", http_status=500)
@feedback_bp.route('/feedback/list', methods=['GET'])
@require_gateway_auth
def get_feedback_list():
"""获取反馈列表"""
rating = request.args.get('rating', type=int)
user_id = request.args.get('user_id')
start_date = request.args.get('start_date')
end_date = request.args.get('end_date')
limit = request.args.get('limit', 100, type=int)
try:
feedback_db = _get_feedback_db()
feedbacks = feedback_db.get_feedbacks(
rating=rating,
user_id=user_id,
start_date=start_date,
end_date=end_date,
limit=limit
)
return success_response(data={
"feedbacks": feedbacks,
"total": len(feedbacks)
})
except Exception as e:
logger.error(f"反馈操作异常: {e}")
return error_response("OPERATION_FAILED", INTERNAL_ERROR, "操作失败,请稍后重试", http_status=500)
@feedback_bp.route('/reports/weekly', methods=['GET'])
@require_gateway_auth
def get_weekly_report():
"""获取周报告"""
try:
feedback_service = _get_feedback_service()
report = feedback_service.generate_report("weekly")
return success_response(data={
"report": report.to_dict()
})
except Exception as e:
logger.error(f"反馈操作异常: {e}")
return error_response("OPERATION_FAILED", INTERNAL_ERROR, "操作失败,请稍后重试", http_status=500)
@feedback_bp.route('/reports/monthly', methods=['GET'])
@require_gateway_auth
def get_monthly_report():
"""获取月报告"""
try:
feedback_service = _get_feedback_service()
report = feedback_service.generate_report("monthly")
return success_response(data={
"report": report.to_dict()
})
except Exception as e:
logger.error(f"反馈操作异常: {e}")
return error_response("OPERATION_FAILED", INTERNAL_ERROR, "操作失败,请稍后重试", http_status=500)
@feedback_bp.route('/faq', methods=['GET'])
@require_gateway_auth
def get_faq_list():
"""获取FAQ列表"""
status = request.args.get('status')
limit = request.args.get('limit', 50, type=int)
try:
feedback_db = _get_feedback_db()
faqs = feedback_db.get_faqs(status=status, limit=limit)
return success_response(data={
"faqs": faqs,
"total": len(faqs)
})
except Exception as e:
logger.error(f"反馈操作异常: {e}")
return error_response("OPERATION_FAILED", INTERNAL_ERROR, "操作失败,请稍后重试", http_status=500)
@feedback_bp.route('/faq', methods=['POST'])
@require_gateway_auth
@require_role('admin')
def create_faq():
"""
新增FAQ管理员- 创建后需二次确认同步
安全设计:
1. 管理员创建的 FAQ 默认状态为 'draft'
2. 需要通过 /faq/<id>/approve 接口二次确认
3. 确认后才同步到 ChromaDB
"""
data = request.get_json()
question = data.get('question')
answer = data.get('answer')
if not question or not answer:
return error_response("MISSING_PARAMS", BAD_REQUEST, "缺少问题或答案", http_status=400)
try:
from services.feedback import FAQ
feedback_db = _get_feedback_db()
# 管理员创建也进入 draft 状态,需要二次确认
faq = FAQ(
question=question,
answer=answer,
source_documents=data.get('source_documents', []),
status='draft' # 强制为 draft需要二次确认
)
faq_id = feedback_db.add_faq(faq)
return success_response(data={
"faq_id": faq_id,
"status": "draft",
"message": "FAQ已创建请通过 /faq/<id>/approve 接口确认后生效"
})
except Exception as e:
logger.error(f"反馈操作异常: {e}")
return error_response("OPERATION_FAILED", INTERNAL_ERROR, "操作失败,请稍后重试", http_status=500)
@feedback_bp.route('/faq/<int:faq_id>/approve', methods=['POST'])
@require_gateway_auth
@require_role('admin')
def approve_faq(faq_id):
"""
批准FAQ并同步到知识库管理员二次确认
适用于:
1. 管理员手动创建的 FAQ
2. 从 FAQ 建议转为正式的 FAQ
"""
try:
feedback_db = _get_feedback_db()
# 检查 FAQ 状态
faq = feedback_db.get_faq(faq_id)
if not faq:
return error_response("NOT_FOUND", NOT_FOUND, "FAQ不存在", http_status=404)
if faq.get('status') == 'approved':
return success_response(message="FAQ已经是批准状态")
# 更新状态为 approved
feedback_db.update_faq(faq_id, {"status": "approved"})
# 同步到知识库
feedback_service = _get_feedback_service()
sync_success = feedback_service._sync_faq_to_knowledge_base(
faq_id=faq_id,
question=faq['question'],
answer=faq['answer']
)
return success_response(data={
"faq_id": faq_id,
"sync_status": "synced" if sync_success else "sync_failed",
"message": "FAQ已批准并同步到知识库"
})
except Exception as e:
logger.error(f"反馈操作异常: {e}")
return error_response("OPERATION_FAILED", INTERNAL_ERROR, "操作失败,请稍后重试", http_status=500)
@feedback_bp.route('/faq/<int:faq_id>', methods=['PUT'])
@require_gateway_auth
@require_role('admin')
def update_faq(faq_id):
"""更新FAQ管理员- 自动同步向量库"""
data = request.get_json()
try:
feedback_db = _get_feedback_db()
# 获取更新前的 FAQ 信息(用于判断是否需要重新同步)
old_faq = feedback_db.get_faq(faq_id)
if not old_faq:
return error_response("NOT_FOUND", NOT_FOUND, "FAQ不存在", http_status=404)
updated = feedback_db.update_faq(faq_id, data)
if not updated:
return error_response("UPDATE_FAILED", INTERNAL_ERROR, "FAQ更新失败", http_status=500)
# 检查是否需要重新同步向量库question 或 answer 变更时)
need_sync = False
if data.get('question') and data['question'] != old_faq.get('question'):
need_sync = True
if data.get('answer') and data['answer'] != old_faq.get('answer'):
need_sync = True
sync_status = "skipped"
if need_sync and old_faq.get('status') == 'approved':
feedback_service = _get_feedback_service()
# 先删除旧向量
feedback_service._delete_faq_vectors(faq_id)
# 获取更新后的 FAQ 信息
updated_faq = feedback_db.get_faq(faq_id)
# 重新同步
sync_success = feedback_service._sync_faq_to_knowledge_base(
faq_id,
updated_faq['question'],
updated_faq['answer']
)
sync_status = "synced" if sync_success else "sync_failed"
return success_response(data={
"message": "FAQ更新成功",
"sync_status": sync_status
})
except Exception as e:
logger.error(f"反馈操作异常: {e}")
return error_response("OPERATION_FAILED", INTERNAL_ERROR, "操作失败,请稍后重试", http_status=500)
@feedback_bp.route('/faq/<int:faq_id>', methods=['DELETE'])
@require_gateway_auth
@require_role('admin')
def delete_faq(faq_id):
"""
删除FAQ管理员- 同步删除向量库数据
由于 FAQ 存储在独立的集合中,删除时可以精确清理,
不会影响普通文档向量库。
"""
try:
feedback_db = _get_feedback_db()
# 先获取 FAQ 信息(用于删除向量)
faq = feedback_db.get_faq(faq_id)
# 删除数据库记录
deleted = feedback_db.delete_faq(faq_id)
if deleted and faq:
# 同步删除向量库中的 FAQ 向量
feedback_service = _get_feedback_service()
feedback_service._delete_faq_vectors(faq_id)
return success_response(data={"deleted": deleted}, message="FAQ删除成功" if deleted else "FAQ不存在")
except Exception as e:
logger.error(f"反馈操作异常: {e}")
return error_response("OPERATION_FAILED", INTERNAL_ERROR, "操作失败,请稍后重试", http_status=500)
@feedback_bp.route('/faq/suggestions', methods=['GET'])
@require_gateway_auth
@require_role('admin')
def get_faq_suggestions():
"""获取FAQ建议列表管理员"""
status = request.args.get('status', 'pending')
limit = request.args.get('limit', 50, type=int)
try:
feedback_db = _get_feedback_db()
suggestions = feedback_db.get_faq_suggestions(status=status, limit=limit)
return success_response(data={
"suggestions": suggestions,
"total": len(suggestions)
})
except Exception as e:
logger.error(f"反馈操作异常: {e}")
return error_response("OPERATION_FAILED", INTERNAL_ERROR, "操作失败,请稍后重试", http_status=500)
@feedback_bp.route('/faq/suggestions/<int:suggestion_id>/approve', methods=['POST'])
@require_gateway_auth
@require_role('admin')
def approve_faq_suggestion(suggestion_id):
"""
批准FAQ建议并同步到知识库二次确认
请求体(可选):
{
"answer": "管理员修改后的答案"
}
"""
data = request.get_json() or {}
answer_override = data.get('answer')
try:
feedback_service = _get_feedback_service()
result = feedback_service.approve_and_sync_faq(
suggestion_id,
answer_override=answer_override
)
if result.get('success'):
return success_response(data={
"faq_id": result['faq_id'],
"sync_status": result.get('sync_status'),
"message": "FAQ建议已批准并同步到知识库"
})
else:
return error_response("APPROVE_FAILED", BAD_REQUEST, result.get('error', '批准失败'), http_status=400)
except Exception as e:
logger.error(f"反馈操作异常: {e}")
return error_response("OPERATION_FAILED", INTERNAL_ERROR, "操作失败,请稍后重试", http_status=500)
@feedback_bp.route('/faq/suggestions/<int:suggestion_id>/reject', methods=['POST'])
@require_gateway_auth
@require_role('admin')
def reject_faq_suggestion(suggestion_id):
"""拒绝FAQ建议管理员"""
try:
feedback_db = _get_feedback_db()
rejected = feedback_db.reject_faq_suggestion(suggestion_id)
return success_response(data={
"rejected": rejected,
"message": "FAQ建议已拒绝" if rejected else "建议不存在"
})
except Exception as e:
logger.error(f"反馈操作异常: {e}")
return error_response("OPERATION_FAILED", INTERNAL_ERROR, "操作失败,请稍后重试", http_status=500)
# ==================== Bad Case 分析接口 ====================
@feedback_bp.route('/feedback/bad-cases', methods=['GET'])
@require_gateway_auth
@require_role('admin')
def get_bad_cases():
"""
获取负反馈 Bad Case 列表(管理员)
用于分析和改进 RAG 系统:
- 识别高频失败查询
- 发现知识库盲区
- 优化检索策略
"""
limit = request.args.get('limit', 20, type=int)
try:
feedback_service = _get_feedback_service()
# 获取低分问题
bad_cases = feedback_service.get_low_rating_queries(limit=limit)
# 获取黑名单来源
blacklisted_sources = feedback_service.get_low_rated_sources(min_count=3)
# 标记处理状态
for case in bad_cases:
case['status'] = 'pending' # pending/resolved/ignored
return success_response(data={
"bad_cases": bad_cases,
"blacklisted_sources": blacklisted_sources,
"suggestions": [
"补充到知识库(针对知识盲区)",
"添加到 Query Rewrite 规则(针对表达歧义)",
"标记为知识库盲区(暂不处理)"
]
})
except Exception as e:
logger.error(f"反馈操作异常: {e}")
return error_response("OPERATION_FAILED", INTERNAL_ERROR, "操作失败,请稍后重试", http_status=500)
@feedback_bp.route('/feedback/blacklist', methods=['GET'])
@require_gateway_auth
@require_role('admin')
def get_chunk_blacklist():
"""
获取 Chunk 黑名单(管理员)
返回被多次点踩的来源,用于在检索时降权或过滤
"""
min_dislikes = request.args.get('min_dislikes', 3, type=int)
try:
feedback_service = _get_feedback_service()
blacklist = feedback_service.get_chunk_blacklist(min_dislikes=min_dislikes)
return success_response(data={
"blacklist": list(blacklist),
"count": len(blacklist),
"usage": "在检索时过滤这些来源以提升回答质量"
})
except Exception as e:
logger.error(f"反馈操作异常: {e}")
return error_response("OPERATION_FAILED", INTERNAL_ERROR, "操作失败,请稍后重试", http_status=500)