15 KiB
15 KiB
example.ts 代码分析文档
一、文件概述
example.ts 是 LLMWiki 项目的**核心导入管道(Ingest Pipeline)**实现,负责将源文档(PDF/DOCX/PPTX/Markdown 等)自动转换为结构化的 Wiki 页面。
核心职责:
- 读取源文件内容
- 提取嵌入图片并生成描述
- 通过 LLM 两阶段分析(分析 → 生成)将源文档转化为 Wiki 页面
- 处理长文档的分块分析
- 写入文件并管理缓存
- 生成嵌入向量(可选)
二、整体架构
源文件输入
│
▼
┌─────────────────────────────────────────────────┐
│ autoIngest() │
│ (项目级锁入口) │
│ │
│ ┌─ Step 0: 缓存检查 │
│ │ ├─ HIT → 跳至图片处理 → 返回 │
│ │ └─ MISS → 继续全量管道 │
│ │ │
│ ┌─ Step 0.5: 图片提取 │
│ │ └─ extractAndSaveSourceImages() │
│ │ │
│ ┌─ Step 0.6: 图片描述(Caption) │
│ │ └─ captionMarkdownImages() │
│ │ │
│ ┌─ 长文档预算检查 │
│ │ ├─ 超长 → analyzeLongSourceInChunks() │
│ │ └─ 正常 → 直接传入 │
│ │ │
│ ┌─ Step 1: 分析阶段 │
│ │ └─ streamChat(buildAnalysisPrompt()) │
│ │ │
│ ┌─ Step 2: 生成阶段 │
│ │ └─ streamChat(buildGenerationPrompt()) │
│ │ │
│ ┌─ Step 2.5: Review 建议(可选) │
│ │ └─ streamChat(buildReviewSuggestionPrompt()) │
│ │ │
│ ┌─ Step 3: 文件写入 │
│ │ └─ writeFileBlocks() │
│ │ │
│ ┌─ Step 3.5: 图片注入源摘要页 │
│ │ └─ injectImagesIntoSourceSummary() │
│ │ │
│ ┌─ Step 4: Review 项解析 │
│ │ └─ parseReviewBlocks() │
│ │ │
│ ┌─ Step 5: 缓存保存 │
│ │ └─ saveIngestCache() │
│ │ │
│ ┌─ Step 6: 嵌入生成(可选) │
│ │ └─ embedPage() │
│ └───────────────────────────────────────────────────┘
│
▼
Wiki 页面输出
三、核心流程详解
3.1 入口:autoIngest()
autoIngest(
projectPath: string, // 项目根目录
sourcePath: string, // 源文件路径
llmConfig: LlmConfig, // LLM 配置
signal?: AbortSignal, // 取消信号
folderContext?: string, // 文件夹上下文(用于分类提示)
): Promise<string[]> // 返回已写入文件路径列表
关键设计:
- 使用
withProjectLock()确保同一项目不会并发执行多个导入任务 - 锁的必要性:分析阶段读取
wiki/index.md,生成阶段覆盖它,不串行化会导致互相覆盖
3.2 缓存机制
检查逻辑:
checkIngestCache(projectPath, sourceIdentity, sourceContent)
├─ 源内容未变化 → 返回缓存的文件列表(跳过 LLM 调用)
└─ 源内容有变化 → 返回 null(走全量管道)
缓存命中时的行为:
- 仍然执行图片提取(用户可能在旧版本导入过,当时没有图片功能)
- 仍然执行图片描述注入
- 跳过 Step 1~2(LLM 分析 + 生成)
3.3 图片处理管道
Step 0.5:图片提取
extractAndSaveSourceImages(projectPath, sourcePath, sourceSummarySlug)
↓
wiki/media/<source-slug>/image1.png
wiki/media/<source-slug>/image2.png
...
支持格式: PDF、PPTX、DOCX(通过 pdfium 等后端提取)
去重策略:
- 使用
ingestImageExtractionPromisesMap 缓存 Promise - 键 =
项目路径 + 源路径 + slug + 文件大小:修改时间 - 最大 32 个缓存条目,超出时淘汰最旧
Step 0.6:图片描述(Caption)
captionMarkdownImages(projectPath, enrichedSourceContent, captionLlm, {
shouldCaption: (url) => url.startsWith(ourMediaPrefix), // 只处理当前导入的图片
urlToAbsPath: (url) => promptImageUrlToAbs(pp, url),
concurrency: mmCfg.concurrency,
onProgress: (done, total) => activity.updateItem(...),
})
为什么需要描述:
- 空
alt的图片在文本摘要时会被 LLM 静默丢弃 - 有了描述后,alt 文本携带足够语义,生成 LLM 会保留图片引用
描述缓存:
- SHA-256 键值缓存(
.llm-wiki/image-caption-cache.json) - 跨文档去重(共享的 logo/图表模板只描述一次)
主开关行为:
- 当
multimodalConfig.enabled = false时:- 不仅跳过 Caption LLM 调用
- 还会从
sourceContent中剥离引用 - 跳过后续的图片注入
- 净效果:Wiki 侧完全不引用图片
3.4 长文档处理
触发条件: enrichedSourceContent.length > sourceBudget
预算计算:
sourceBudget = maxCtx - responseReserve - stableReserve - instructionReserve
// 范围限制在 [8000, min(300000, maxCtx * 0.6)]
分块策略:
splitSourceIntoSemanticChunks(content, targetChars, overlapChars)
↓
按段落/章节边界分割,每块带 8% 重叠(800~3000 字符)
分块分析流程:
对每个 chunk:
├─ streamChat(buildChunkAnalysisSystemPrompt(), buildChunkAnalysisUserPrompt())
├─ 提取 "Chunk Analysis" 和 "Updated Global Digest"
├─ 更新全局摘要(最长 15000 字符)
└─ 保存检查点(支持中断恢复)
检查点机制:
- 路径:
.llm-wiki/ingest-progress/<slug>-<hash>.json - 包含:源哈希、块数量、已完成进度、全局摘要、各块分析
- 兼容性验证:版本、源哈希、源长度、块参数全部匹配才恢复
最终合并:
Consolidated Long-Document Analysis
├─ Final Global Digest
└─ Per-Chunk Analyses
3.5 Step 1:分析阶段
System Prompt 结构:
buildAnalysisPrompt(purpose, index, sourceContent)
├─ 角色设定:专业研究分析师
├─ 语言规则
├─ 分析框架:
│ ├─ 关键实体(人物、组织、产品、数据集、工具)
│ ├─ 关键概念(理论、方法、技术、现象)
│ ├─ 主要论点与发现
│ ├─ 与现有 Wiki 的关联
│ ├─ 矛盾与张力
│ └─ 建议
├─ Wiki 目的(可选)
└─ 当前 Wiki 索引(可选)
User Message 结构:
Analyze this source document:
**File:** ${sourceIdentity}
**Folder context:** ${folderContext} (可选)
---
${sourceContext}
LLM 参数:
temperature: 0.1(低随机性,确保一致性)reasoning.mode: "off"(关闭推理模式)max_tokens: 4096
3.6 Step 2:生成阶段
System Prompt 结构:
buildGenerationPrompt(schema, purpose, index, sourceFileName, ...)
├─ 角色设定:Wiki 维护者
├─ 语言规则
├─ 源文件信息
├─ 项目 Schema(如有)
├─ 生成清单:
│ 1. 源摘要页(wiki/sources/<slug>.md)
│ 2. 实体页(wiki/entities/ 或 schema 定义目录)
│ 3. 概念页(wiki/concepts/ 或 schema 定义目录)
│ 4. 更新 wiki/index.md
│ 5. 日志条目(wiki/log.md)
│ 6. 更新 wiki/overview.md
├─ Frontmatter 规则(严格 YAML 格式)
├─ Review 块类型
└─ 输出格式(FILE 块 + 可选 REVIEW 块)
User Message 结构:
Source document to process: **${sourceIdentity}**
The Stage 1 analysis below is CONTEXT to inform your output.
Do NOT echo its tables, bullet points, or prose.
## Stage 1 Analysis (context only — do not repeat)
${analysis}
## Source Context
${sourceContext}
---
Now emit the FILE blocks for the wiki files derived from **${sourceIdentity}**.
Your response MUST begin with `---FILE:` as the very first characters.
LLM 参数:
max_tokens根据上下文窗口动态计算:- 默认:8192
- 128K 上下文:16384
- 256K 上下文:24576
- 512K 上下文:32768
3.7 Step 2.5:Review 建议(可选)
触发条件:
shouldRunDedicatedReviewStage(generation):
generation.length >= 10000 // 响应足够长
|| countFileBlocks(generation) >= 4 // 生成了足够多文件
|| /---REVIEW:/.test(generation) // 已有 REVIEW 块
System Prompt:
buildReviewSuggestionPrompt(purpose, index, sourceIdentity, analysis, sourceContext, generation)
├─ 角色:识别高价值后续研究项
├─ 仅输出 REVIEW 块(不生成 Wiki 页面)
├─ 来源上下文(截断到预算内)
└─ 生成的 Wiki 输出(截断到预算内)
3.8 Step 3:文件写入
解析 FILE 块:
parseFileBlocks(text): ParseFileBlocksResult
├─ blocks: ParsedFileBlock[]
└─ warnings: string[]
解析器修复的问题:
| 问题 | 描述 | 修复方式 |
|---|---|---|
| H1 | Windows CRLF 行尾 | 先规范化为 LF |
| H2 | 流截断导致最后一个块丢失 | 发出警告 |
| H3 | 标记空白/大小写变体 | 正则不区分大小写,容忍空白 |
| H5 | 代码块内的字面量 ---END FILE--- |
追踪 fence 状态,fence 内不关闭 |
| H6 | 空路径块静默丢弃 | 发出警告 |
路径安全校验:
isSafeIngestPath(p): boolean
├─ 必须是 wiki/ 下的路径
├─ 禁止绝对路径
├─ 禁止 .. 片段
├─ 禁止 Windows 无效字符
├─ 禁止保留设备名(CON、PRN、AUX、NUL、COM1-9、LPT1-9)
└─ 禁止 NUL/控制字符
写入策略(按路径类型):
| 路径类型 | 策略 |
|---|---|
wiki/log.md |
追加模式(append) |
wiki/index.md / wiki/overview.md |
全覆盖(overwrite) |
| 内容页(entities/concepts/等) | 合并模式(merge) |
合并策略三层:
- Frontmatter 数组字段(sources、tags、related)→ 并集合并
- 正文内容不同 → LLM 生成合并版本
- 锁定字段(type、title、created)→ 保留原值,updated 更新为当天
合并失败回退:
- LLM 合并失败 → 使用传入正文 + 数组字段并集
- 合并前备份原文件到
.llm-wiki/page-history/
语言守卫:
contentMatchesTargetLanguage(content, target): boolean
├─ 剥离 frontmatter + 代码块 + 数学块
├─ 检测剩余文本语言
└─ CJK 目标接受 CJK 变体,Latin 目标接受 Latin 家族
3.9 Step 4~6:收尾
Step 4:Review 项解析
parseReviewBlocks(text, sourcePath): ReviewItem[]
├─ 类型:contradiction / duplicate / missing-page / suggestion
├─ OPTIONS:预定义选项
├─ PAGES:受影响页面
└─ SEARCH:优化搜索查询(用于 Deep Research)
Step 5:缓存保存
- 仅当无硬失败(磁盘满、权限错误等)时保存
- 软丢弃(语言不匹配、路径穿越拒绝)不影响缓存
Step 6:嵌入生成
- 跳过 index、log、overview 页面
- 从 frontmatter 提取 title
- 调用
embedPage()生成向量
四、辅助入口:startIngest() 和 executeIngestWrites()
这两个函数服务于交互式聊天模式(用户在 UI 中手动触发导入):
startIngest()
- 设置聊天模式为 "ingest"
- 提前提取图片(与 LLM 分析并行)
- 发送分析请求并流式展示结果
executeIngestWrites()
- 接收聊天历史作为上下文
- 构建生成提示
- 解析 FILE 块并写入文件
- 注入图片引用到源摘要页
五、关键数据结构
FILE 块格式
---FILE: wiki/path/to/page.md---
---
type: entity
title: Example Entity
created: 2026-04-29
updated: 2026-04-29
tags: [example, demo]
related: [related-slug-1]
sources: ["source-file.pdf"]
---
# Example Entity
Body content here.
---END FILE---
REVIEW 块格式
---REVIEW: suggestion | Precise Title---
Description of the gap and why it matters.
OPTIONS: Create Page | Skip
PAGES: wiki/page1.md, wiki/page2.md
SEARCH: query 1 | query 2 | query 3
---END REVIEW---
检查点结构
interface LongSourceCheckpoint {
version: 1
sourceIdentity: string
sourceHash: string // FNV-1a 64-bit hash
sourceLength: number
sourceBudget: number
targetChars: number
overlapChars: number
chunkTotal: number
completedThrough: number // 已完成到第几个块
globalDigest: string // 全局摘要
analyses: string[] // 各块分析
updatedAt: number
}
六、常量配置
| 常量 | 值 | 说明 |
|---|---|---|
LONG_SOURCE_MIN_BUDGET |
8,000 | 长文档最小预算 |
LONG_SOURCE_MAX_SINGLE_PASS_BUDGET |
300,000 | 长文档单次最大预算 |
LONG_SOURCE_CHUNK_MIN |
12,000 | 分块最小大小 |
LONG_SOURCE_CHUNK_MAX |
60,000 | 分块最大大小 |
LONG_SOURCE_DIGEST_MAX |
15,000 | 全局摘要最大长度 |
LONG_SOURCE_CHUNK_ANALYSIS_MAX |
40,000 | 单块分析最大长度 |
REVIEW_STAGE_MIN_SIGNAL_CHARS |
10,000 | Review 阶段最小信号字符 |
REVIEW_STAGE_MIN_FILE_BLOCKS |
4 | Review 阶段最小文件块数 |
七、与 Python 项目的映射关系
| TypeScript 模块 | Python 对应模块 | 状态 |
|---|---|---|
autoIngest() |
src/main.py::main() |
部分实现 |
buildAnalysisPrompt() |
src/prompt.py::build_analysis_prompt() |
已实现 |
sourceIdentityForPath() |
src/source.py::source_identity_for_path() |
已实现 |
streamChat() |
src/llm.py::query_analysis() |
已实现 |
parseFileBlocks() |
待实现 | 未实现 |
writeFileBlocks() |
待实现 | 未实现 |
splitSourceIntoSemanticChunks() |
待实现 | 未实现 |
analyzeLongSourceInChunks() |
待实现 | 未实现 |
checkIngestCache() |
待实现 | 未实现 |
| 图片提取管道 | 待实现 | 未实现 |