Files
LLMWiki/example-analysis.md
2026-06-06 07:44:26 +08:00

15 KiB
Raw Permalink Blame History

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~2LLM 分析 + 生成)

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 等后端提取)

去重策略:

  • 使用 ingestImageExtractionPromises Map 缓存 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 中剥离 ![](url) 引用
    • 跳过后续的图片注入
    • 净效果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.5Review 建议(可选)

触发条件:

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 无效字符
  ├─ 禁止保留设备名(CONPRNAUXNULCOM1-9LPT1-9
  └─ 禁止 NUL/控制字符

写入策略(按路径类型):

路径类型 策略
wiki/log.md 追加模式append
wiki/index.md / wiki/overview.md 全覆盖overwrite
内容页entities/concepts/等) 合并模式merge

合并策略三层:

  1. Frontmatter 数组字段sources、tags、related→ 并集合并
  2. 正文内容不同 → LLM 生成合并版本
  3. 锁定字段type、title、created→ 保留原值updated 更新为当天

合并失败回退:

  • LLM 合并失败 → 使用传入正文 + 数组字段并集
  • 合并前备份原文件到 .llm-wiki/page-history/

语言守卫:

contentMatchesTargetLanguage(content, target): boolean
  ├─ 剥离 frontmatter + 代码块 + 数学块
  ├─ 检测剩余文本语言
  └─ CJK 目标接受 CJK 变体,Latin 目标接受 Latin 家族

3.9 Step 4~6收尾

Step 4Review 项解析

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() 待实现 未实现
图片提取管道 待实现 未实现