Files
QMDSearch/app/core/summarizer.py
T
kplam 2ab8b56a01 feat: 完成全量功能开发,包括前端管理后台与后端服务优化
此提交实现了完整的知识库管理系统:
1. 新增Vue3 + Antd Vue前端管理后台,包含登录、文档管理、检索、类目设置等完整页面
2. 重构后端LLM调用抽象层,支持Ollama与OpenAI兼容服务动态切换
3. 调整默认嵌入模型配置为本地bge-m3模式
4. 优化入库任务去重逻辑与缓存清理机制
5. 完善Docker镜像构建与docker-compose部署配置
6. 修复多项测试用例与兼容性问题
7. 新增运行时配置API,支持动态调整系统参数
2026-07-31 12:05:25 +08:00

142 lines
5.3 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.
"""文档三级总结模块
通过 LLMOllama 或 OpenAI 兼容服务,由 runtime_settings.models.summarize 决定)
对文档进行分级总结:
- 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.llm import LLMClient, create_llm_client
logger = structlog.get_logger()
# 文本长度阈值(字符数),低于此值触发 2.5 级回退
MIN_TEXT_LENGTH_FOR_L3 = 500
class Summarizer:
"""文档三级总结器"""
def __init__(self, ollama: LLMClient | None = None) -> None:
# 默认按 runtime_settings 选择 LLM 实现(Ollama 或 OpenAI 兼容);
# 测试可通过 ollama 参数注入替身。
self.ollama = ollama or create_llm_client("summarize")
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)