本文档面向开发、维护、部署和二次扩展人员,说明 Doc Diff Agent 的整体架构、核心数据结构、业务链路、存储模型、模型接入、测试与打包方式。
更新日期:2026-06-02
Doc Diff Agent 是一个面向 Windows 桌面的文档版本管理、语义比对和检索问答工具,主要服务于法规、制度、合同、规范类文档。应用以 PySide6 桌面程序运行,核心能力包括:
- 多格式文档导入、解析、版本管理和去重。
- 文档结构化为统一的
DocumentIR,并构建检索 chunk 与 FAISS 索引。 - 两个文档版本之间的章节对齐、段落/句子/表格行级语义匹配、LLM 差异分类。
- 差异结果持久化、风险等级判断、HTML/DOCX 报告导出。
- 基于 BM25 + FAISS 的混合检索问答,支持本地 SQLite 会话记忆,并兼容无页码文档的段落引用。
- 首页任务管理、任务恢复、删除任务、QA 会话管理、包含原始文档副本的备份恢复和更新检查。
| 层次 | 主要技术 | 说明 |
|---|---|---|
| 桌面 UI | PySide6 / Qt WebEngine | 主窗口、页面导航、表格、对比双栏 Web 视图、后台线程 |
| 工作流编排 | LangGraph | 导入、比对、问答三个工作流 |
| 文档解析 | pymupdf4llm + firecrawl-anydoc + MarkItDown | PDF、AnyDoc 支持格式、兼容文本格式显式三级路由 |
| 数据库 | SQLite | 文档、版本、chunk、任务、差异、QA 会话和 checkpoint |
| 向量索引 | FAISS-cpu | 每个文档版本独立索引 |
| 关键词检索 | rank-bm25 | 中文使用字符级 token 化 |
| 模型接入 | OpenAI Compatible API / sentence-transformers | 远程 chat + embedding,本地 embedding 可选 |
| 流式问答 | LangChain ChatOpenAI streaming | QA 页面逐 token 输出 |
| 报告导出 | python-docx / HTML | 导出 DOCX 和独立 HTML |
| 打包 | PyInstaller onedir + Inno Setup | 生成 Windows 离线安装器 |
| 测试 | pytest / pytest-qt | 当前全量测试 632 个通过 |
app/
agent/ LangGraph 工作流和 SQLite checkpointer
config/ 配置读写、API Key 加解密
core/
diff/ 章节对齐、语义匹配、差异分类
model/ 模型 provider、OpenAI 兼容适配、本地 embedding
parser/ 文档解析路由、Markdown 转 IR、chunk 构建
retrieval/ BM25、FAISS、RRF 混合检索
types.py 核心 dataclass 和枚举
db/ SQLite schema、仓储层、FAISS 文件存储
services/ 导入、比对、问答、报告、备份、更新检查服务
ui/ PySide6 主窗口、页面、主题系统
assets/ 图标、字体、HTML 模板
build/ PyInstaller spec 与 Inno Setup 脚本
tests/ 单元测试、集成测试、UI 测试
main.py 应用入口
项目按“UI -> 工作流/服务 -> core -> db”的方向组织:
PySide6 UI
-> AppContext 共享运行时对象
-> QThread worker 调用 agent graph 或 service
-> core/parser, core/diff, core/retrieval, core/model
-> db repository + SQLite + FAISS files
各层职责如下:
ui/:负责用户交互、主题、页面状态、后台线程启动、结果展示。agent/:定义导入、比对、QA 的 LangGraph 工作流;QA 使用 SQLite checkpointer 保存上下文。services/:提供可直接调用的业务服务,如报告导出、备份恢复、更新检查。core/:放置领域逻辑,包括解析、比对、检索、模型适配和核心类型。db/:数据库 schema、CRUD 仓储,以及 FAISS 索引文件的构建/加载。
UI 层通过 AppContext 共享 settings、SQLite connection、data directory、provider、embedder、LangChain model 和正在运行的对比任务集合。
入口文件是 main.py。
启动流程:
- 创建
QApplication,设置应用名、图标和字体。 - 读取
%APPDATA%\DocDiffAgent\config.json。 - 初始化主题系统
ThemeManager。 - 确定数据目录并初始化 SQLite 数据库。
- 根据设置构建:
provider:OpenAI Compatible chat + embedding provider。embedder:本地 sentence-transformers 或远程 embedding provider。lc_model:LangChainChatOpenAI流式模型。openai_client:供 MarkItDown OCR 插件使用。
- 创建
MainWindow和四个主页面:首页、对比、文档库、问答。 - 连接页面间信号,如首页打开/恢复任务、设置变更刷新 provider、QA 会话变更刷新首页。
- 刷新首页统计、文档库、对比版本列表和 QA 文档/任务列表。
数据目录的实际来源是 AppSettings.data_dir。默认设置中 data_dir 来自 %APPDATA%\DocDiffAgent;如果配置项为空,main.py 会退回到 %LOCALAPPDATA%\DocDiffAgent\data。
核心类型定义在 app/core/types.py。
所有解析后的文档都会转成统一结构:
DocumentIR
doc_id
title
file_hash
sections[]
section_id
title
level
paragraphs[]
paragraph_id
text
sentences[]
text
plain_text
设计目的:
- 屏蔽 PDF、Word、Excel、HTML 等格式差异。
- 让导入、检索、比对、QA、报告导出都使用同一份结构化表示。
- 支持大段落、表格和句子的细粒度拆分。
Chunk 是检索单元:
version_id指向一个文档版本。chunk_no保持原始顺序。section_path用于引用章节。text是检索文本。page_no是可选页码,缺省或不可用时为0。faiss_index_id映射到 FAISS 索引中的向量行号。
对比结果由 DiffResult 和多个 DiffItem 组成。差异类型包括:
新增删减微调实质修改重写格式变化
风险等级包括:
high:高风险medium:中风险low:低风险none:无风险
风险判断优先参考 LLM 对语义影响的判断;规则逻辑用于 fallback 或增强关键数值、否定词、义务词等明确触发项。新增、删减这类单侧文本也会根据硬触发词提升风险,避免重要义务或禁止条款被低估。
数据库初始化位于 app/db/schema.py。SQLite 启用:
PRAGMA journal_mode=WALPRAGMA foreign_keys=ON
主要表:
| 表 | 用途 |
|---|---|
documents |
文档元信息,按文件 hash 去重 |
document_versions |
同一文档的多个版本 |
chunks |
文档版本对应的检索 chunk |
compare_tasks |
对比任务状态、版本关系、结果路径 |
diff_items |
对比任务下的结构化差异详情 |
qa_sessions |
QA 会话元信息 |
qa_messages |
QA 用户/助手消息历史 |
qa_checkpoints |
LangGraph checkpoint 主表 |
qa_checkpoint_writes |
checkpoint pending writes |
qa_checkpoint_blobs |
checkpoint channel blob |
diff_items.risk_level 的 CHECK 约束支持 high / medium / low / none。代码包含旧库迁移逻辑,会重建缺少 none 的 legacy diff_items 表。
为了降低首页统计、文档库列表、检索、任务恢复和会话列表的查询成本,schema 初始化时会确保以下索引存在:
| 索引 | 主要用途 |
|---|---|
idx_documents_source_created |
按来源类型和创建时间列出文档 |
idx_document_versions_document_version |
查询同一文档的最新版本 |
idx_chunks_version_chunk_no |
按原始顺序加载版本 chunk |
idx_chunks_version_faiss_id |
将 FAISS 行号批量映射回 chunk |
idx_compare_tasks_created |
首页最近任务排序 |
idx_compare_tasks_status_created |
按状态恢复或统计任务 |
idx_diff_items_task_section |
加载对比任务差异和章节定位 |
idx_qa_sessions_updated |
QA 历史会话按更新时间排序 |
idx_qa_messages_session_rowid |
按写入顺序加载会话消息 |
应用数据目录下保存:
data_dir/
app.db
docs/
<file_hash>.<ext>
parsed/
<doc_id>.json
faiss/
<version_id>/
index.faiss
exports/
<compare_task_id>.json
说明:
docs/保存导入文件副本。parsed/保存DocumentIRJSON,供比对页完整文档双栏和后续任务复用。faiss/按版本保存索引,避免不同版本向量混在同一索引中。exports/保存对比任务 JSON 结果,HTML/DOCX 报告由用户选择路径导出。
导入工作流定义在 app/agent/ingest_graph.py。
file_check
-> parse_doc
-> save_document
-> build_embeddings
职责:
- 检查文件是否存在。
- 计算文件 hash。
- 如果不是“新增版本”模式,则检查
documents.file_hash是否已存在。 - 对重复导入给出明确提示:如需新增版本,应在文档库选择已有文档后点击“新增版本”。
调用 app/core/parser/router.py:
.pdf:使用pymupdf4llm_adapter.extract()。- Word、PowerPoint、Excel、OpenDocument、RTF、EPUB、CSV:使用
anydoc_adapter.extract()。 - HTML、JSON、XML、TXT、Markdown:使用
markitdown_adapter.extract()。
支持格式:
.pdf,
.doc, .docx, .docm,
.ppt, .pps, .pot, .pptx, .pptm, .ppsx, .ppsm,
.xls, .xlsx, .xlsm, .xlsb,
.odt, .ods, .odp, .rtf, .epub, .csv,
.html, .htm, .json, .xml, .txt, .md, .markdown
解析后会进行质量评估:
- 无章节结构或无段落内容:质量分低,提示可能是扫描件或解析失败。
- 平均段落过短:降低质量分。
- 过短段落比例过高:提示检查解析结果。
quality_score < 0.4时needs_ocr=True。
职责:
- 将原始文件复制到
docs/。 - 将
DocumentIR写入parsed/<doc_id>.json。 - 插入
documents和document_versions,或给已有文档插入新版本。 - 调用
build_chunks()生成 chunk 并插入chunks表。
如果当前配置中有 embedder,则调用 app/core/retrieval/indexer.py 构建 FAISS 索引,并回写 chunk 的 faiss_index_id。
PDF 当前优先使用 pymupdf4llm.to_markdown() 转 Markdown,再走统一 Markdown -> DocumentIR 解析逻辑。
优势:
- 对 PDF 文本抽取和版面结构更友好。
- 输出 Markdown,方便后续统一处理表格、标题和段落。
AnyDoc 支持的 Word、PowerPoint、Excel、OpenDocument、RTF、EPUB 和 CSV 由 anydoc.to_markdown() 在本地转换为 GitHub-Flavored Markdown。AnyDoc 的公开文档模型不携带页码,因此这些格式生成的 paragraph 保持 page_no=None。
HTML、JSON、XML、TXT、Markdown 等 AnyDoc 不支持的原有格式继续由 MarkItDown 转换。
当设置页配置了 OpenAI 兼容 API 后,MarkItDown 初始化时会启用插件链,供 markitdown-ocr 辅助处理可识别内容。
解析器会:
- 用
#、##、###识别章节层级。 - 用空行分割普通段落。
- 识别 Markdown 表格行,连续表格行保存为一个 paragraph。
- 对普通文本按中英文句末标点拆分 sentence。
- 对表格将每一行作为 sentence,方便后续表格行级比对。
markdown_cleanup.py 只删除 Markdown 表格单元格中的 =DISPIMG(...) 解析噪声。NaN、None、NA、N/A 等字面值作为原始内容保留。AnyDoc 与 MarkItDown 共用 markdown_ir.py 转换逻辑。
build_chunks(ir, version_id, max_chars=2000) 的策略:
- 普通短段落直接作为一个 chunk。
- 超过
2000字符的段落按sentences拆成句子级 chunk。 - 表格行在解析阶段已保存为 sentence,因此大表格也能拆成更细的检索单元。
索引流程:
- embedder 对所有 chunk 文本批量生成 embedding。
faiss_store.build_and_save()保存index.faiss。chunk_repo.update_faiss_ids()将 FAISS 行号回写到chunks.faiss_index_id。
当前 chunk 的 page_no 字段允许为空或为 0。QA 引用不会假设所有解析器都能产出页码:有有效页码时显示页码;没有页码时使用 chunk_no + 1 显示段落序号。
比对工作流定义在 app/agent/compare_graph.py。
parsed DocumentIR pair
-> align_sections
-> reconstruct_table_pairs
-> match_paragraphs (fresh embeddings of reconstructed in-memory text)
-> classify
-> persist diff JSON + reconstruction sidecar
如果是新任务:
- 插入
compare_tasks,状态从pending改为running。
如果是恢复任务:
- 校验任务存在。
- 删除旧
diff_items。 - 将任务重置为
running。
根据 baseline_version_id 和 target_version_id 从 document_versions.parsed_json_path 加载两个 DocumentIR。
如果解析 JSON 不存在,任务会标记为 failed。
structure_aligner.align_sections() 用章节标题相似度对齐两个文档结构。未匹配章节会保留为一侧为空的 section pair,用于生成新增或删减。
table_reconstruction_pipeline.reconstruct_table_pairs() 在章节对齐之后、语义匹配之前联合分析两个版本的跨页表格片段。它识别逻辑列、重复页眉/页脚边界和跨页续行;高置信度规则直接作出合并或保留决定,中置信度候选在 provider 可用时才交给 LLM 裁决。provider 不可用、裁决失败或返回无效结果时,中置信度候选保守地保持分离,不会猜测或补写原文。
重建只在 DocumentIR 的深拷贝上执行,并重新对齐重建后的章节。原始解析 JSON 和传入的 DocumentIR 不会被修改。后续 match_paragraphs() 针对重建后的内存文本重新计算 embedding,不复用导入阶段保存的 chunk 向量。
每个候选决定和实际变换都会记录在版本化、可重放的 sidecar:exports/<task_id>.reconstruction.json。sidecar 包含 schema/algorithm 版本、两侧 doc_id 与 file_hash、候选 ID、规则证据和冲突、可选 LLM 裁决,以及按顺序执行的列投影、边界删除、行/片段合并操作。差异 JSON 与 sidecar 先分别写入同目录临时文件,再发布正式文件;任一发布失败都会令对比任务标记为 failed,不会标记为完成。
对比页先加载原始 DocumentIR,校验 sidecar 的版本和两侧文档来源,再重放操作用于完整文档双栏。sidecar 缺失、不可读、JSON 无效、版本/来源不匹配或操作无效时,对比页记录带类别的 warning,并整体回退到两侧原始 IR;不会部分应用重建,也不会在展示时调用模型或检索服务。
表格重建与检索基础设施明确隔离:app/core/retrieval/searcher.py、Chunk.faiss_index_id 和磁盘中保存的 FAISS 索引仅供 retrieval/QA 使用。comparison reconstruction 不读取这些对象,不加载或重建已保存的 FAISS 索引,也不依赖检索 chunk。
semantic_matcher.match_paragraphs() 在已对齐章节内匹配段落。
关键策略:
- 对短段落直接按 paragraph 比对。
- 对超过
500字符或看起来像表格的 paragraph 拆成 sentence/table row 单元。 - 表格行使用第一列作为
match_key,相同 key 的行优先匹配。 - embedding cosine similarity 作为主评分。
- 规则惩罚会关注:
- 数字
- 否定词
- 义务词,如“应、须、必须、不得、禁止”
diff_classifier.classify() 将段落/句子/表格行 pair 转成结构化差异。
处理规则:
- baseline 为空、target 存在:
新增。 - target 为空、baseline 存在:
删减。 - 两侧都有文本:调用 LLM 或规则进行分类。
- 对拆分单元,如果忽略空白后相同,直接跳过,避免表格未变行进入结果。
LLM prompt 要求只输出 JSON:
{
"diff_type": "微调|实质修改|重写|格式变化",
"risk_level": "high|medium|low|none",
"explanation": "简短的差异说明"
}如果 LLM 调用失败或未配置 provider,会使用规则 fallback。
持久化内容:
diff_items表保存结构化差异。exports/<task_id>.json保存差异 JSON。exports/<task_id>.reconstruction.json保存可校验、可重放的表格重建 sidecar。compare_tasks状态更新为completed,并保存结果路径。
失败时任务状态更新为 failed。
app/ui/pages/compare_page.py 负责对比任务创建、恢复、结果展示和报告导出。
实现要点:
- 后台使用
_CompareWorker+QThread运行比对,避免阻塞 UI。 AppContext.active_compare_task_ids记录正在运行的任务,首页可显示“进行中”并避免重复恢复。- 中间双栏使用 WebEngine 展示完整文档。
- 双栏从
parsed_json_path加载完整DocumentIR,而不只展示差异片段。 - 差异卡片与双栏内容通过
diff_id和 WebBridge 联动。 - 点击差异卡片可滚动定位到对应内容位置。
- 点击双栏中的差异片段会同步选择差异卡片和筛选状态。
- Markdown 渲染会处理表格、行内格式和 HTML escape。
- 差异卡片在暗色模式下使用主题色而非半透明高饱和彩色背景。
QA 工作流定义在 app/agent/qa_graph.py。
resolve_scope
-> retrieve_chunks
-> generate_answer
-> attach_citations
支持范围:
| 范围 | 说明 |
|---|---|
| 当前文档 | 使用用户选择的一个版本 |
| 对比文档 | 使用对比任务的基准版和目标版,并附加差异结果上下文 |
| 文档库 | 使用所有标准文档的最新版本 |
| 全部 | 当前选择版本 + 标准文档库最新版本 |
app/core/retrieval/searcher.py 同时执行:
- FAISS 向量检索。
- BM25 关键词检索。
再用 Reciprocal Rank Fusion 合并排序:
score = 1 / (RRF_K + faiss_rank) + 1 / (RRF_K + bm25_rank)
默认 RRF_K = 60,默认返回 top_k=5。
性能相关实现:
- FAISS 命中会通过
chunk_repo.get_chunks_by_faiss_ids()批量映射回 chunk,避免逐条查询。 - 文档库范围通过
document_repo.list_latest_versions()一次取出各文档最新版本,避免重复扫描版本表。 app/db/faiss_store.py维护最多 2 个 FAISS 索引的 LRU 缓存,并用index.faiss的mtime_ns自动失效;重新构建索引时会清理旧缓存。app/core/retrieval/bm25_searcher.py维护最多 8 个 BM25 语料缓存;当单个版本 chunk 数超过 2000 时跳过缓存,避免在低内存机器上长期占用大量内存。
这些缓存都保持较小上限,目标是减少重复加载和重复建模,同时兼容性能较差或内存较小的运行环境。
当范围是“对比文档”且存在 compare_task_id 时:
- 先对两个文档版本执行 chunk 检索。
- 再从
diff_items重建DiffResult。 - 将差异统计和最多 20 条差异摘要加入 system prompt。
- 差异摘要优先按风险等级排序:高风险、中风险、低风险、无风险。
这样用户问“两者有什么差异”时,即使 chunk 检索没有命中,也能基于已持久化的对比结果回答。
为避免模型上下文过长,QA 生成前会进行字符预算控制:
- 检索 + 对比上下文默认预算:
12000字符。 - 历史消息总预算:
4000字符。 - 单条历史消息上限:
1200字符。 - 历史最多发送最近
6条消息。 - 超长内容会追加“已截断,已优先保留最相关内容”提示。
这些预算可以通过 LangGraph config 的 configurable 字段覆盖:
qa_context_char_budget
qa_history_char_budget
qa_history_message_char_limit
当前实现使用字符数近似 token 预算,优点是无需额外 tokenizer 依赖;缺点是不能精确匹配不同模型的 token 规则。
QA prompt 中的检索片段由 app/core/retrieval/context_format.py 统一格式化:
- 有章节时显示
章节:...。 page_no > 0时显示第 N 页。- 没有页码、页码为
0或解析器未提供页码时,显示段落:第 N 段。
这避免了无页码文档在回答中出现“第 0 页”或空引用。LangGraph QA 工作流和 qa_service.answer() 使用同一套格式化函数,保证 UI 流式问答和 service 问答行为一致。
QA 会话由两层持久化组成:
-
qa_sessions/qa_messages- 供 UI 显示会话列表、加载历史、删除会话。
- 保存用户消息和助手回复。
-
SQLiteCheckpointSaver- 供 LangGraph 恢复状态。
- 写入
qa_checkpoints、qa_checkpoint_writes、qa_checkpoint_blobs。 thread_id使用 QA session id。
删除会话时会同时删除:
qa_messagesqa_sessions- 对应
thread_id的 LangGraph checkpoint 三张表记录
首页“已完成问答”数量直接查询 qa_sessions,所以删除 QA 会话后会同步变化。
app/core/model/openai_compatible.py 封装 chat 和 embedding:
chat(messages)调用兼容 OpenAI Chat Completions 的接口。embed(texts)调用 embedding API。
由 factory.build_provider() 根据 ProviderConfig 创建。
如果设置启用了 local_embedding,且模型目录存在,则 get_embedder() 使用 LocalEmbeddingProvider。否则使用远程 provider 的 embedding 能力。
本地 embedding 的主要价值:
- 文档入库和检索不依赖远程 embedding API。
- 降低检索成本。
- 对离线或内网环境更友好。
QA 使用 app/core/model/lc_factory.py 创建 ChatOpenAI:
model来自 active provider 的chat_model。api_key和base_url来自 provider 配置。streaming=True。
如果未配置 lc_model,QA 会返回“请先在设置页面配置模型”。
配置文件位于:
%APPDATA%\DocDiffAgent\config.json
API Key 使用 cryptography.fernet.Fernet 加密。密钥由机器标识派生:
- Windows 优先读取
HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid。 - 失败时使用主机名、机器架构和固定盐组合。
这意味着配置文件不适合直接跨机器迁移。备份恢复到另一台机器后,API Key 可能无法解密,需要重新配置。
MainWindow 提供:
- 左侧导航栏。
- 首页、对比、文档库、问答四个页面容器。
- 设置入口。
- 主题切换按钮。
- HarmonyOS Sans 和 Font Awesome 字体加载。
首页展示:
- 文档数量。
- 对比任务数量。
- 已完成问答数量。
- 最近对比任务表格。
任务表格支持:
- 打开已完成任务。
- 查看进行中任务。
- 恢复意外中断的任务。
- 删除任务。
- 展示差异统计和风险数量。
文档库支持:
- 导入新文档。
- 为已有文档新增版本。
- 展示文档名称、类型、最新版本和总版本数。
- 后台线程导入,避免阻塞 UI。
对比页支持:
- 选择基准版本和目标版本。
- 创建或恢复任务。
- 查看差异概览、筛选差异类型和风险等级。
- 双栏完整文档对照。
- 差异卡片与文档定位联动。
- 导出 HTML / DOCX 报告。
问答页支持:
- 当前文档、对比文档、文档库、全部四种范围。
- 根据范围显示不同的文档或对比任务选择器。
- 流式输出。
- 引用检索片段。
- 回答生成中禁用发送按钮,避免重复提交同一问题。
- 历史会话列表、加载、删除、新建会话。
设置页支持:
- 配置 provider 名称、base URL、API Key、chat model、embedding model。
- 配置本地 embedding 模型路径。
- 配置数据目录。
- 切换主题。
- 备份与恢复。
- 检查更新。
耗时任务均放入后台线程:
- 文档导入:
LibraryPage._IngestWorker - 文档比对:
ComparePage._CompareWorker - QA 流式回答:
QaPage._QaWorker - 更新检查:
SettingsDialog._UpdateCheckThread
线程间通过 Qt Signal 通信,避免在后台线程直接操作 UI。
数据库 connection 当前由 AppContext 共享,并在 SQLite 初始化时设置 check_same_thread=False。这简化了页面和 worker 的协作,但也要求长任务尽量在仓储函数内保持短事务,避免 UI 与 worker 间长时间占用数据库写锁。
app/services/report_service.py 支持:
export_docx(result, output_path)export_html(result, output_path)
DOCX 报告包含:
- 标题和生成时间。
- 差异总数。
- 差异类型统计表。
- 每条差异的类型、章节、风险、相似度、基准文本、目标文本、说明。
HTML 报告是独立文件,包含内联 CSS 和 escaped 文本,避免用户文档中的 HTML 片段影响报告结构。
app/services/backup_service.py 将以下内容打包为 ZIP:
config.jsondata/app.dbdata/docs/data/faiss/data/parsed/
恢复时会覆盖对应文件。恢复逻辑会校验 ZIP 内路径,只允许写入配置文件和数据目录下的预期内容,避免带有 .. 的路径穿越条目写出目标目录。
注意:
- 如果备份迁移到另一台机器,API Key 可能因机器密钥不同而无法解密。
app/services/update_checker.py 从远端版本文件读取最新版本号,并与本地 APP_VERSION 比较。设置页会提示发现的新版本。
该机制只负责提示,不负责自动下载安装包。
打包文件:
build/doc_diff_agent.specbuild/installer.iss
标准流程:
pyinstaller build/doc_diff_agent.spec
iscc build/installer.issPyInstaller 产物:
dist/DocDiffAgent/
DocDiffAgent.exe
_internal/
Inno Setup 输出:
dist/DocDiffAgent-v1.0.1-setup.exe
当前安装器脚本语言使用 Inno Setup 内置英文语言文件。应用本体 UI 文案仍由项目代码控制,主要为中文。
测试目录按模块组织:
| 目录 | 覆盖内容 |
|---|---|
tests/test_agent/ |
LangGraph 工作流、QA 流式、SQLite checkpointer |
tests/test_db/ |
SQLite schema、仓储层、FAISS 文件存储 |
tests/test_diff/ |
章节对齐、语义匹配、差异分类、表格行比对 |
tests/test_model/ |
OpenAI provider、本地 embedding、LangChain factory |
tests/test_parser/ |
AnyDoc、MarkItDown、PDF、共享 Markdown IR、清理、OCR 接口与解析路由 |
tests/test_retrieval/ |
BM25、FAISS indexer、混合检索 |
tests/test_services/ |
导入、比对、QA service、报告、备份、更新 |
tests/ui/ |
首页、文档库、对比页、QA 页交互与样式 |
tests/test_ui/ |
主窗口和主题基础导入测试 |
推荐命令:
.\.venv\Scripts\python.exe -m pytest -q如果当前 Windows 临时目录权限异常,可显式设置临时目录:
$tmp = Join-Path (Get-Location) ".tmp\pytest"
New-Item -ItemType Directory -Path $tmp -Force | Out-Null
$env:TEMP = $tmp
$env:TMP = $tmp
.\.venv\Scripts\python.exe -m pytest -q -p no:cacheprovider最近一次验证结果:
632 passed, 3 warnings
实现步骤:
- 在
app/core/model/新增 provider 类,实现BaseProvider。 - 在
factory.build_provider()中增加类型分支。 - 在设置页增加对应配置字段。
- 增加 provider 单元测试。
实现步骤:
- 在
SUPPORTED_EXTENSIONS中加入扩展名。 - 在
router.parse_document()中增加路由。 - 输出必须转换为
DocumentIR。 - 增加 parser 测试和导入服务测试。
可以扩展:
- LLM prompt 中的风险规则。
_rule_classify()中的数值、日期、主体、义务词识别。- diff item 的结构化字段,例如影响对象、条款编号、变更摘要。
注意保持 risk_level 仍落在 high / medium / low / none,否则数据库 CHECK 会失败。
当前 QA 使用字符预算控制上下文长度。若要更精确,可引入 tokenizer:
- 按模型类型选择 tokenizer。
- 对 system context、history、question 分配 token budget。
- 保留高风险差异和高分检索命中。
- 在 UI 上提示“部分上下文已裁剪”。
- 当前主要面向 Windows 桌面环境。
- Azure provider 分支已预留,但当前会抛出
NotImplementedError。 - SQLite connection 使用
check_same_thread=False,应避免长事务。 - QA 本地记忆不支持多设备同步。
- 配置文件中的 API Key 依赖机器派生密钥,不适合直接跨机器复制。
- QA 引用页码依赖解析器输出;无页码或页码不可用时会显示段落位置。
- BM25 与 FAISS 缓存上限偏保守,优先保证低性能环境的内存稳定性。
- 对比风险等级依赖 LLM 输出质量;规则 fallback 只覆盖数字、否定词、义务词等明确模式。
- QA 上下文预算当前按字符裁剪,不是严格 token 裁剪。
常见改动对应测试:
| 改动类型 | 建议优先测试 |
|---|---|
| 文档解析 | tests/test_parser/, tests/test_services/test_ingest_service.py |
| 文档比对 | tests/test_diff/, tests/test_agent/test_compare_graph.py |
| 检索问答 | tests/test_retrieval/, tests/test_db/test_faiss_store.py, tests/test_agent/test_qa_graph.py, tests/test_services/test_qa_service.py, tests/test_agent/test_qa_stream.py |
| QA 会话 | tests/test_db/test_qa_repo.py, tests/test_agent/test_sqlite_checkpointer.py, tests/ui/test_qa_page.py |
| UI 样式和主题 | tests/ui/, tests/test_ui/test_theme.py |
| 打包 | 手动执行 PyInstaller + Inno Setup,并检查 dist/ 产物 |
开发时建议遵守:
- 领域逻辑优先放在
core/或services/,UI 只负责展示和用户交互。 - 新增数据库字段时同步修改 schema、仓储层和迁移测试。
- 新增 workflow 节点时确保失败路径会写入明确
status和error。 - 对用户可见行为增加 UI 或 service 测试。
- 对大文件、安装包、缓存产物保持
.gitignore覆盖,避免误提交。