51dc8dc4f6
- FastAPI + Qdrant + Redis + Ollama 技术栈 - L1→L2→L3→chunk 四层分层检索(dense + sparse RRF 融合) - 文档三级总结与 2.5 级回退 - query 解析路由与分类 - /admin 管理页面
139 lines
5.1 KiB
Python
139 lines
5.1 KiB
Python
"""文档三级总结模块
|
|
|
|
通过 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)
|