Initial commit: QMDSearch 分层信息检索服务

- FastAPI + Qdrant + Redis + Ollama 技术栈
- L1→L2→L3→chunk 四层分层检索(dense + sparse RRF 融合)
- 文档三级总结与 2.5 级回退
- query 解析路由与分类
- /admin 管理页面
This commit is contained in:
2026-07-29 21:24:40 +08:00
commit 51dc8dc4f6
83 changed files with 10794 additions and 0 deletions
+138
View File
@@ -0,0 +1,138 @@
"""文档三级总结模块
通过 Ollama 本地小模型对文档进行分级总结:
- L1: 总结(一句话高度概括)
- L2: 大纲(主要章节和关键主题)
- L3: 内容大纲(每个章节的详细内容摘要)
当文档内容不足以支撑三级总结时,自动降级为 2.5 级(L1 + L2.5 内容大纲)。
"""
import structlog
from app.core.headings import parse_headings, render_outline
from app.models.document import DocumentSummary, SummaryLevel
from app.services.ollama import OllamaClient
logger = structlog.get_logger()
# 文本长度阈值(字符数),低于此值触发 2.5 级回退
MIN_TEXT_LENGTH_FOR_L3 = 500
class Summarizer:
"""文档三级总结器"""
def __init__(self, ollama: OllamaClient | None = None) -> None:
self.ollama = ollama or OllamaClient()
async def summarize(self, text: str, *, title: str = "") -> DocumentSummary:
"""对文档文本进行三级总结
Args:
text: 文档纯文本内容
title: 文档标题(可选,辅助总结)
Returns:
DocumentSummary: 包含各级总结的结果
"""
text_length = len(text.strip())
# 生成 L1 总结
l1_summary = await self._generate_l1(text, title)
logger.info("L1 总结完成", title=title, l1=l1_summary[:100])
# 判断是否需要 2.5 级回退
use_fallback = text_length < MIN_TEXT_LENGTH_FOR_L3
if use_fallback:
logger.info("文档内容不足,使用 2.5 级回退", text_length=text_length)
# L2.5: 直接生成内容大纲(跳过大纲层)
l2_half = await self._generate_l2_half(text, title, l1_summary)
return DocumentSummary(
l1_summary=l1_summary,
l2_outline=None,
l3_content_outline=l2_half,
level=SummaryLevel.L2_HALF,
)
# 生成 L2 大纲:优先使用文档原生标题树(结构导航,不调 LLM),
# 无结构文本(标题数 < 2)回退 LLM 生成
headings = parse_headings(text)
if len(headings) >= 2:
l2_outline = render_outline(headings)
logger.info("L2 大纲完成", title=title, outline_source="headings", heading_count=len(headings))
else:
l2_outline = await self._generate_l2(text, title, l1_summary)
logger.info("L2 大纲完成", title=title, outline_source="llm")
# 生成 L3 内容大纲
l3_content_outline = await self._generate_l3(text, title, l1_summary, l2_outline)
logger.info("L3 内容大纲完成", title=title)
return DocumentSummary(
l1_summary=l1_summary,
l2_outline=l2_outline,
l3_content_outline=l3_content_outline,
level=SummaryLevel.L3,
)
async def _generate_l1(self, text: str, title: str) -> str:
"""生成 L1 总结:一句话高度概括"""
prompt = (
"请用一句话对以下文档内容进行高度概括,要求简洁精炼,"
"突出文档的核心主题和关键信息。\n\n"
)
if title:
prompt += f"文档标题:{title}\n\n"
prompt += f"文档内容:\n{text}"
return await self.ollama.generate(prompt)
async def _generate_l2(self, text: str, title: str, l1_summary: str) -> str:
"""生成 L2 大纲:主要章节和关键主题"""
prompt = (
"请提取以下文档的主要章节结构和关键主题,"
"以大纲形式呈现。每个主题用一行表示,"
"格式为:序号. 主题名称\n\n"
f"文档总结:{l1_summary}\n\n"
)
if title:
prompt += f"文档标题:{title}\n\n"
prompt += f"文档内容:\n{text}"
return await self.ollama.generate(prompt)
async def _generate_l3(
self, text: str, title: str, l1_summary: str, l2_outline: str
) -> str:
"""生成 L3 内容大纲:每个章节的详细内容摘要"""
prompt = (
"请对以下文档的每个章节/主题进行详细的内容摘要,"
"格式为:\n"
"## 章节名称\n"
"详细摘要内容...\n\n"
f"文档总结:{l1_summary}\n\n"
f"文档大纲:\n{l2_outline}\n\n"
)
if title:
prompt += f"文档标题:{title}\n\n"
prompt += f"文档内容:\n{text}"
return await self.ollama.generate(prompt)
async def _generate_l2_half(self, text: str, title: str, l1_summary: str) -> str:
"""生成 L2.5 内容大纲(2.5 级回退)
跳过大纲层,直接对短文本生成详细摘要。
"""
prompt = (
"请对以下文档内容进行详细摘要,"
"涵盖所有关键信息点。以要点形式呈现。\n\n"
f"文档总结:{l1_summary}\n\n"
)
if title:
prompt += f"文档标题:{title}\n\n"
prompt += f"文档内容:\n{text}"
return await self.ollama.generate(prompt)