"""文档三级总结模块 通过 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)