- 后端 API(Flask + Gunicorn) - RAG 引擎(混合检索 + 云端 Reranker + 引用溯源) - 文档解析(MinerU + 多格式支持) - Docker 生产部署配置 - 排除前端项目、敏感配置、模型文件
272 lines
10 KiB
Python
272 lines
10 KiB
Python
"""
|
||
Agentic RAG - 查询重写 Mixin
|
||
|
||
包含查询改写、实体补全、专业术语映射等方法
|
||
"""
|
||
|
||
import re
|
||
import logging
|
||
|
||
from .agentic_base import logger, MODEL
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class QueryRewriteMixin:
|
||
"""查询重写方法"""
|
||
|
||
def _rewrite_query(self, query: str, history: list = None,
|
||
strategy: str = "professional") -> str:
|
||
"""
|
||
增强版查询重写:将口语化表达转为专业术语
|
||
|
||
Args:
|
||
query: 原始查询
|
||
history: 对话历史(用于实体补全)
|
||
strategy: 重写策略
|
||
- professional: 口语化→专业术语
|
||
- expand: 扩展关键词
|
||
- clarify: 消歧义
|
||
- entity: 实体补全
|
||
|
||
Returns:
|
||
str: 重写后的查询
|
||
"""
|
||
# 尝试多种策略组合
|
||
rewritten = query
|
||
|
||
# 策略1: 口语化→专业术语映射
|
||
if strategy in ["professional", "all"]:
|
||
rewritten = self._apply_professional_mapping(rewritten)
|
||
|
||
# 策略2: 实体补全(利用对话历史)
|
||
if strategy in ["entity", "all"] and history:
|
||
rewritten = self._complete_entities(rewritten, history)
|
||
|
||
# 策略3: LLM 深度重写(仅在需要时调用)
|
||
if strategy in ["professional", "all"]:
|
||
llm_rewritten = self._llm_rewrite(rewritten)
|
||
if llm_rewritten and len(llm_rewritten) > len(rewritten) * 0.5:
|
||
rewritten = llm_rewritten
|
||
|
||
return rewritten
|
||
|
||
def _apply_professional_mapping(self, query: str) -> str:
|
||
"""应用口语化→专业术语映射"""
|
||
TERM_MAPPING = {
|
||
"报销": "差旅报销 费用报销 报销审批",
|
||
"请假": "休假申请 请假审批 考勤管理",
|
||
"加班": "加班申请 工时管理 加班审批",
|
||
"工资": "薪酬管理 工资发放 薪资结构",
|
||
"合同": "合同管理 合同签署 合同审批",
|
||
"流程": "审批流程 业务流程 工作流",
|
||
"制度": "管理制度 规章制度 企业规范",
|
||
"规定": "管理规定 制度规定 政策要求",
|
||
"几天": "时限 审批时限 办理时限",
|
||
"多久": "处理时效 审批周期 办理周期",
|
||
"多少": "标准 额度 限额 标准",
|
||
"能不能": "是否允许 是否可以 权限",
|
||
"人事": "人力资源 HR 人力部门",
|
||
"财务": "财务部 财务部门 财务管理",
|
||
"技术": "技术部 研发部 IT部门",
|
||
}
|
||
|
||
result = query
|
||
for colloquial, professional in TERM_MAPPING.items():
|
||
if colloquial in query:
|
||
result = result.replace(colloquial, f"{colloquial} {professional.split()[0]}")
|
||
|
||
return result
|
||
|
||
def _complete_entities(self, query: str, history: list) -> str:
|
||
"""实体补全:利用对话历史补充缺失的实体"""
|
||
if not history:
|
||
return query
|
||
|
||
# 图片指代识别
|
||
image_reference = self._detect_image_reference(query, history)
|
||
if image_reference:
|
||
return image_reference
|
||
|
||
# 获取最近用户消息
|
||
last_user_msg = None
|
||
for msg in reversed(history):
|
||
if msg.get("role") == "user":
|
||
last_user_msg = msg.get("content", "")
|
||
break
|
||
|
||
if not last_user_msg:
|
||
return query
|
||
|
||
# 检查当前查询是否缺少主语
|
||
BUSINESS_KEYWORDS = ["报销", "出差", "请假", "工资", "合同", "审批", "流程",
|
||
"制度", "规定", "标准", "金额", "时间"]
|
||
|
||
has_subject = any(kw in query for kw in BUSINESS_KEYWORDS)
|
||
|
||
if not has_subject:
|
||
try:
|
||
import jieba
|
||
entities = []
|
||
for word in jieba.cut(last_user_msg):
|
||
word = word.strip()
|
||
if len(word) >= 2 and any(kw in word for kw in BUSINESS_KEYWORDS):
|
||
entities.append(word)
|
||
|
||
if entities:
|
||
return f"{entities[0]} {query}"
|
||
except ImportError:
|
||
pass
|
||
|
||
return query
|
||
|
||
def _detect_image_reference(self, query: str, history: list) -> str:
|
||
"""检测图片指代查询并重写"""
|
||
IMAGE_REFERENCE_PATTERNS = [
|
||
r'这[张些]图片', r'那[张些]图片', r'上面的图片', r'刚才的图片',
|
||
r'这[张些]图', r'那[张些]图', r'上面的图', r'刚才的图',
|
||
r'解释一下这[张些]图', r'说明一下这[张些]图',
|
||
r'这[张些]是什么图', r'图[里内]是什么', r'图片[里内]是什么',
|
||
]
|
||
|
||
is_image_reference = False
|
||
for pattern in IMAGE_REFERENCE_PATTERNS:
|
||
if re.search(pattern, query):
|
||
is_image_reference = True
|
||
break
|
||
|
||
if not is_image_reference:
|
||
return ""
|
||
|
||
last_images = []
|
||
for msg in reversed(history):
|
||
if msg.get("role") == "assistant":
|
||
metadata = msg.get("metadata", {})
|
||
if isinstance(metadata, dict):
|
||
images = metadata.get("images", [])
|
||
if images:
|
||
for img in images[:5]:
|
||
if isinstance(img, dict):
|
||
desc = img.get("description", "")
|
||
img_type = img.get("type", "图片")
|
||
if desc:
|
||
last_images.append(f"{img_type}:{desc}")
|
||
elif isinstance(img, str):
|
||
last_images.append(f"图片:{img}")
|
||
|
||
if not last_images:
|
||
content = msg.get("content", "")
|
||
if "图片" in content or "图表" in content or "图" in content:
|
||
sentences = content.split("。")
|
||
for sentence in sentences:
|
||
if "图片" in sentence or "图表" in sentence:
|
||
last_images.append(sentence.strip())
|
||
|
||
if last_images:
|
||
break
|
||
|
||
if last_images:
|
||
image_context = " ".join(last_images[:3])
|
||
question_intent = re.sub(
|
||
r'这[张些]图片?|那[张些]图片?|上面的图片?|刚才的图片?|解释一下|说明一下',
|
||
'', query
|
||
).strip()
|
||
|
||
if question_intent:
|
||
return f"{image_context} {question_intent}"
|
||
else:
|
||
return f"详细解释:{image_context}"
|
||
|
||
return query
|
||
|
||
def _extract_image_context_from_history(self, history: list) -> str:
|
||
"""从对话历史中提取图片上下文"""
|
||
if not history:
|
||
return ""
|
||
|
||
for msg in reversed(history):
|
||
if msg.get("role") == "assistant":
|
||
metadata = msg.get("metadata", {})
|
||
images = metadata.get("images", [])
|
||
content = msg.get("content", "")
|
||
|
||
image_descriptions = []
|
||
|
||
if images:
|
||
for i, img in enumerate(images[:5], 1):
|
||
if isinstance(img, dict):
|
||
desc = img.get("description", "")
|
||
img_type = img.get("type", "图片")
|
||
source = img.get("source", "")
|
||
page = img.get("page", "")
|
||
|
||
img_info = f"图片{i}:{img_type}"
|
||
if desc:
|
||
img_info += f",描述:{desc}"
|
||
if source:
|
||
img_info += f",来源:{source}"
|
||
if page:
|
||
img_info += f",第{page}页"
|
||
image_descriptions.append(img_info)
|
||
|
||
if not image_descriptions:
|
||
if "图片" in content or "图表" in content:
|
||
sentences = content.split("。")
|
||
for sentence in sentences:
|
||
if "图片" in sentence or "图表" in sentence:
|
||
image_descriptions.append(sentence.strip())
|
||
if len(image_descriptions) >= 3:
|
||
break
|
||
|
||
if image_descriptions:
|
||
return "\n".join(image_descriptions)
|
||
|
||
return ""
|
||
|
||
def _answer_image_reference(self, enhanced_query: str, history: list) -> str:
|
||
"""回答图片引用问题"""
|
||
from core.llm_utils import call_llm
|
||
|
||
messages = [
|
||
{"role": "system", "content": "你是一个专业的助手,请根据提供的图片信息回答用户的问题。"}
|
||
]
|
||
|
||
for h in history[-4:]:
|
||
if h.get("role") in ["user", "assistant"]:
|
||
messages.append({"role": h["role"], "content": h.get("content", "")})
|
||
|
||
messages.append({"role": "user", "content": enhanced_query})
|
||
|
||
try:
|
||
result = call_llm(
|
||
self.client, "", MODEL,
|
||
temperature=0.3,
|
||
max_tokens=1000,
|
||
messages=messages
|
||
)
|
||
return result or ""
|
||
except Exception as e:
|
||
logger.error(f"图片引用回答失败: {e}")
|
||
return f"抱歉,回答图片问题时出现错误:{str(e)}"
|
||
|
||
def _llm_rewrite(self, query: str) -> str:
|
||
"""LLM 深度重写查询"""
|
||
from core.llm_utils import call_llm
|
||
|
||
prompt = f"""请将以下用户问题改写为更专业、更清晰的表达,保持原意不变。
|
||
|
||
原问题:{query}
|
||
|
||
改写后的问题:"""
|
||
|
||
try:
|
||
rewritten = call_llm(
|
||
self.client, prompt, MODEL,
|
||
temperature=0.3,
|
||
max_tokens=100
|
||
)
|
||
return rewritten.strip() if rewritten else query
|
||
except Exception as e:
|
||
logger.warning(f"LLM 重写失败: {e}")
|
||
return query
|