openviking/parse/parsers/README.md
OpenViking 的解析系统负责将各种格式的原始文档转换为结构化的上下文信息,遵循 L0/L1/L2 三层信息模型。该系统采用模块化设计,支持多种文档格式,并允许用户通过协议扩展自定义解析器。
OpenViking 采用独特的三层信息模型来组织上下文内容,针对不同使用场景提供不同粒度的信息:
| 层级 | 文件 | 大小 | 用途 |
|---|---|---|---|
| L0 | .abstract.md | <200 token | 摘要,支持向量化检索、用于目录下的快速浏览 |
| L1 | .overview.md | <1000 token | 决策参考、内容导览 |
| L2 | content.md 或原始文件 | 文件完整内容 | 供详细阅读(仅遍历终点) |
设计出发点:不同场景需要不同粒度的内容。通过上下文信息的分层,我们允许 Agent 按需读取需要的信息并节省上下文窗口的 token 消耗。
一份技术文档的三层内容示例:
L0: "这是一份介绍 OpenViking 上下文数据库的技术文档,该文档主要介绍其中的上下文提取流程,涵盖资源解析和信息提取、存储和索引的流程规范,其中重点介绍了..."
L1: """
# 目录 (Line 1 ~ Line 10)
目录部分提供了本文的目录结构,共划分为 4 个一级章节,分别是资源解析、信息提取、存储机制、语义索引机制。
# 资源解析 (Line 11 ~ Line 50)
资源解析部分介绍了如何解析外部知识,包括文档、图片、视频等,包括具体的用例...
# 信息提取 (Line 51 ~ Line 100)
信息提取部分介绍了如何从资源中提取摘要和导览信息,以文本文件、图片、视频文件等作为具体示例...
# 存储机制 (Line 101 ~ Line 150)
存储机制部分介绍了如何将上下文存储到 VikingFS 中,包括文件系统、索引结构等。
# 语义索引机制 (Line 151 ~ Line 200)
语义索引机制介绍了如何将上下文存储到向量库中,支持语义检索。
"""
┌─────────────────────────────────────────────────────────────────────┐
│ 资源提取流程 (v5.0) │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ ┌─────────────────────────────────────────────┐ │
│ │ 文件 │───▶│ Parser │ │
│ │ PDF/MD │ │ ┌─────────────────────────────────────┐ │ │
│ │ 图片... │ │ │ 解析 + 创建文件和目录结构 │ │ │
│ └─────────┘ │ │ (无 LLM 调用) │ │ │
│ │ └──────────────┬──────────────────────┘ │ │
│ └─────────────────┼───────────────────────────┘ │
│ │ │
│ ▼ ParseResult │
│ │ (temp_dir_path) │
│ ┌─────────────────┴───────────────────────────┐ │
│ │ TreeBuilder │ │
│ │ ┌─────────┐ ┌──────────────────────────┐ │ │
│ │ │ 1.移动 │─▶│ 2.入队 SemanticQueue │ │ │
│ │ │ 到AGFS │ │ (自底向上处理) │ │ │
│ │ └─────────┘ └──────────────────────────┘ │ │
│ └─────────────────────┬───────────────────────┘ │
│ │ │
│ ▼ BuildingTree │
│ ┌─────────────────────┴───────────────────────┐ │
│ │ SemanticQueue (异步) │ │
│ │ ┌─────────────────────────────────────┐ │ │
│ │ │ SemanticProcessor (自底向上): │ │ │
│ │ │ 1. 收集子目录 abstract │ │ │
│ │ │ 2. 生成文件 summary (并发LLM) │ │ │
│ │ │ 3. 生成 .abstract.md (L0) │ │ │
│ │ │ 4. 生成 .overview.md (L1) │ │ │
│ │ │ 5. 直接调用向量化写入 │ │ │
│ │ └─────────────────────────────────────┘ │ │
│ └─────────────────────┬───────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────┴───────────────────────┐ │
│ │ AGFS + 向量库 │ │
│ │ L0/L1/L2 文件 + 向量化索引 │ │
│ └─────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
markdown.py)支持格式: .md, .markdown, .mdown, .mkd
核心文档解析器,所有文本类文档最终都通过 MarkdownParser 处理。采用 v5.0 简化架构:
章节名_1.md, 章节名_2.md章节名.md章节A_章节B_章节C.md.abstract.md.abstract.md (L0) 和 .overview.md (L1)pdf.py)支持格式: .pdf
统一 PDF 解析器,采用双策略转换:
pdfplumber 进行文本和表格提取MinerU API 进行高级 PDF 处理处理流程:PDF → Markdown → ParseResult,最终通过 MarkdownParser 处理。
html.py)支持格式: .html, .htm
使用 readabilipy 库提取可读内容,去除广告、导航等噪音元素,转换为 Markdown 后通过 MarkdownParser 处理。
text.py)支持格式: .txt, .text, .log, .csv, .tsv, .json, .yaml, .yml, .xml, .ini, .cfg, .conf
纯文本解析器,支持多种文本格式。对于结构化格式(JSON、YAML、XML)会尝试提取结构化信息。
code/*)支持来源: github 代码仓库等
代码解析器,支持语法高亮和代码结构分析。能识别函数、类、方法等代码元素,遵循 .gitignore 并忽略常见非代码目录。
media/*.py)支持格式:
.png, .jpg, .jpeg, .gif, .webp.mp4, .mov, .avi, .webm.mp3, .wav, .m4a, .flac多媒体解析器,使用 VLM(视觉语言模型)分析图像、视频和音频内容,生成文本描述。多媒体解析器当且仅当 add-resource 调用时只添加上述文件类型时生效。即:
多媒体文件的存储采用以下层级结构:
viking://resource/
├── images/ # 图片文件
│ └── 20240820/ # 上传日期(YYYYMMDD)
│ └── 20240820_123456_jpg/ # 文件文件夹(文件名_扩展名)
│ ├── .abstract.md # L0 摘要
│ ├── .overview.md # L1 概览
│ └── 20240820_123456.jpg # 原始文件
├── audio/ # 音频文件
│ └── 20240820/
│ └── my_song_mp3/
│ ├── .abstract.md
│ ├── .overview.md
│ └── my_song.mp3
└── video/ # 视频文件
└── 20240820/
└── my_video_mp4/
├── .abstract.md
├── .overview.md
└── my_video.mp4
详细说明:
媒体子目录: 在 viking://resource 下按类型划分为三个子目录
viking://resource/images: 存储未明确指定目标路径的图片文件viking://resource/audio: 存储未明确指定目标路径的音频文件viking://resource/video: 存储未明确指定目标路径的视频文件日期子目录: 每次上传的文件按当前日期(格式:YYYYMMDD)组织,而非文件内部元信息的时间
viking://resource/images/20240820/ 存储 2024年8月20日上传的所有图片文件文件夹: 为每个多媒体文件创建一个专属文件夹,命名规则为:文件名_扩展名(扩展名不含点)
20240820_123456.jpg → 创建文件夹 20240820_123456_jpg文件夹内容: 每个文件文件夹内包含:
_(因为 OpenViking URI 不允许包含空格)
photo 1.jpg → 保存为 photo_1.jpg.abstract.md (L0 层): 摘要信息(<200 token)
.overview.md (L1 层): 概览信息
base_parser.py)所有解析器的抽象基类,定义了解析器的标准接口:
class BaseParser(ABC):
@abstractmethod
async def parse(self, source: Union[str, Path], instruction: str = "", **kwargs) -> ParseResult:
"""从文件路径或内容字符串解析文档"""
pass
@abstractmethod
async def parse_content(self, content: str, source_path: Optional[str] = None, instruction: str = "", **kwargs) -> ParseResult:
"""直接解析文档内容"""
pass
@property
@abstractmethod
def supported_extensions(self) -> List[str]:
"""支持的文件扩展名列表"""
pass
registry.py)解析器注册表,提供自动解析器选择和注册机制:
class ParserRegistry:
def __init__(self, register_optional: bool = True):
"""初始化注册表并注册默认解析器"""
self._parsers: Dict[str, BaseParser] = {}
self._extension_map: Dict[str, str] = {}
# 注册核心解析器
self.register("text", TextParser())
self.register("markdown", MarkdownParser())
self.register("pdf", PDFParser())
self.register("html", HTMLParser())
self.register("code", CodeRepositoryParser())
def get_parser_for_file(self, path: Union[str, Path]) -> Optional[BaseParser]:
"""根据文件扩展名获取合适的解析器"""
pass
def register_custom(self, handler: "CustomParserProtocol", extensions: Optional[List[str]] = None, name: Optional[str] = None):
"""注册自定义解析器(协议方式)"""
pass
tree_builder.py)树构建器,负责将临时目录移动到 AGFS 并入队语义处理:
class TreeBuilder:
async def finalize_from_temp(
self,
temp_dir_path: str,
scope: str,
base_uri: Optional[str] = None,
source_path: Optional[str] = None,
source_format: Optional[str] = None,
) -> "BuildingTree":
"""
从临时目录最终化树结构:
1. 移动文件到 AGFS
2. 入队到 SemanticQueue(自底向上处理)
3. 扫描创建 Resource 对象
"""
pass
解析系统使用统一的 Resource 类型。
from openviking.parse.parsers.markdown import MarkdownParser
from openviking.parse.registry import get_registry
# 方式1:直接使用解析器
parser = MarkdownParser()
result = await parser.parse("document.md")
# 方式2:通过注册表自动选择
registry = get_registry()
result = await registry.parse("document.pdf") # 自动选择 PDFParser
from openviking.parse.parsers.base_parser import BaseParser
from openviking.parse.base import ParseResult, ResourceNode, NodeType, create_parse_result
class CustomParser(BaseParser):
@property
def supported_extensions(self) -> List[str]:
return [".xyz"]
async def parse(self, source: Union[str, Path], instruction: str = "", **kwargs) -> ParseResult:
content = self._read_file(source)
root = ResourceNode(type=NodeType.ROOT, title="Custom Document")
return create_parse_result(root=root, source_path=str(source), source_format="custom")
# 注册自定义解析器
registry = get_registry()
registry.register("custom", CustomParser())
from openviking.parse.custom import CustomParserProtocol
from typing import List
class MyParser:
@property
def supported_extensions(self) -> List[str]:
return [".myformat"]
def can_handle(self, source) -> bool:
return str(source).endswith(".myformat")
async def parse(self, source, **kwargs):
# 实现解析逻辑
pass
# 注册(自动包装为 BaseParser)
registry.register_custom(MyParser(), name="my_parser")
适合需要完整控制解析流程的场景:
from openviking.parse.parsers.base_parser import BaseParser
class MyParser(BaseParser):
@property
def supported_extensions(self) -> List[str]:
return [".myformat"]
async def parse(self, source, instruction="", **kwargs):
# 实现三阶段解析
# 1. 解析原始内容
# 2. 生成 L0/L1
# 3. 写入临时目录
pass
async def parse_content(self, content, source_path=None, instruction="", **kwargs):
# 类似实现
pass
适合已有解析逻辑,需要快速集成:
from openviking.parse.custom import CustomParserProtocol
class ExistingParser:
def parse_file(self, path):
# 现有解析逻辑
pass
# 适配器模式
class Adapter(CustomParserProtocol):
def __init__(self, existing_parser):
self.parser = existing_parser
@property
def supported_extensions(self):
return [".existing"]
def can_handle(self, source):
return True
async def parse(self, source, **kwargs):
result = self.parser.parse_file(source)
# 转换为 ParseResult
pass
适合简单转换场景:
from openviking.parse.registry import ParserRegistry
async def simple_parser(source, **kwargs):
content = Path(source).read_text()
# 简单处理
return create_parse_result(...)
registry = get_registry()
registry.register_callback(".simple", simple_parser, name="simple_parser")
get_abstract() / get_overview() / get_detail() 按需加载混合目录结构示例(章节有子章节时,小章节合并):
viking://resources/Python_异步编程指南/
├── .abstract.md # L0: 目录摘要
├── .overview.md # L1: 目录概览
├── Python_异步编程指南_第一章_asyncio_基础.md # 直接内容 + 第一章合并(均 < 800 tokens)
├── 第二章_高级模式/ # > 4000 tokens 且有子章节,变目录
│ ├── .abstract.md
│ ├── .overview.md
│ ├── 第二章_高级模式_并发控制.md # 直接内容 + 并发控制合并
│ └── 错误处理.md # 单独保存(>= 800 tokens)
└── 附录.md # < 4000 tokens,直接文件
超大章节无子章节时的切分:
第二章_高级模式/
├── .abstract.md
├── .overview.md
├── 第二章_高级模式_1.md # 按段落切分
├── 第二章_高级模式_2.md
└── 第二章_高级模式_3.md
viking://temp/abc123/
└── document/
├── Python_异步编程指南.md
├── 第一章_asyncio_基础.md
├── 第二章_高级模式/
│ ├── 第二章_高级模式.md
│ ├── 并发控制.md
│ └── 错误处理.md
└── 附录.md
注:临时目录只包含文件和目录结构,.abstract.md 和 .overview.md 由 SemanticProcessor 异步生成。
解析失败
LLM 调用失败
内存不足
import logging
logging.basicConfig(level=logging.DEBUG)
# 启用详细日志
parser = MarkdownParser()
result = await parser.parse("test.md", debug=True)
| 版本 | 日期 | 更新内容 |
|---|---|---|
| v5.0 | 2026-01-19 | 解析与语义分离,引入 SemanticQueue,支持混合目录结构 |
| v4.0 | 2026-01-07 | 引入临时目录架构,解析器自包含设计 |
| v3.0 | 2026-01-07 | 统一 Markdown 处理流程 |
| v2.0 | 2026-01-05 | 增加多媒体解析器支持 |
| v1.0 | 2026-01-04 | 初始解析系统架构 |