English · 简体中文
文档加载器¶
在 v0.7.0 (beta) 中添加。真实来源:
lib/documents/document.mli、lib/documents/*_loader.mli和lib/documents/directory_loader.mli中的 OCaml 类型。
PAR 的文档加载器将真实文件(文本、Markdown、HTML、CSV、PDF、Word)转换为 Document.t 记录,直接接入 RAG 管道。加载 PDF,用 Chunking 分块,用 Runtime.embed 嵌入向量,存入 Vector_store,再通过 Runtime.invoke_with_rag 查询。加载器处理文件 I/O、格式解析和元数据提取,让你专注于管道本身。
完整的 RAG 管道(embeddings、向量存储、分块、检索)见 RAG API。文档加载器产出的 Document.t 值直接馈入该管道。
Document 类型¶
加载后的文档是一个包含三个字段的记录:
type t = {
content : string; (** extracted plain text *)
metadata : (string, Yojson.Safe.t) Hashtbl.t; (** source-derived fields *)
source : string; (** file path or URI *)
}
content 字段是提取出的纯文本。source 字段是原始文件路径或 URI。metadata 哈希表保存加载器从源文件获取的所有信息:文件名、文件大小、页码、列值、YAML frontmatter 等。
metadata 的类型是 (string, Yojson.Safe.t) Hashtbl.t,与 Vector_store.document 的 metadata 字段类型一致。这意味着加载器输出可以直接传递给 Vector_store.add,无需转换。
元数据辅助函数¶
Meta 子模块提供便捷的构造方法:
open Par.Document.Meta
let m = empty () (* 创建空哈希表 *)
let m = singleton "source" (`String "a.md") (* 单条目哈希表 *)
add m "page" (`Int 3) (* 添加 Yojson 值 *)
add_string m "file_type" "text/markdown" (* String 的简写 *)
add_int m "file_size" 4096 (* Int 的简写 *)
(* 在 Yojson 之间往返转换 *)
let json = to_yojson m in
let m' = of_yojson json
加载错误¶
加载器失败时返回 Load_error.t 变体:
| 变体 | 含义 |
|---|---|
File_not_found of string |
文件在给定路径不存在 |
Permission_denied of string |
文件存在但无法读取 |
Unsupported_format of string |
加载器不支持该文件类型 |
Extraction_failed of string * string |
解析或提取失败(消息、异常信息) |
Workspace_rejected of Types.error_category |
路径被 Workspace.admit 拒绝(安全检查) |
使用 Document.load_error_to_string 获取人类可读的错误消息:
加载器 make 模式¶
每个格式加载器都遵循两阶段构造函数模式:
module My_loader : sig
val make : Workspace.workspace -> string ->
(unit -> Document.t list, Document.load_error) result
end
make 接受一个 workspace(用于路径安全验证)和文件路径。成功时返回一个 thunk,调用时读取文件并产出 Document.t list。如果路径无效,make 本身立即返回 Error,不读取任何文件。提取过程中的异常会被优雅捕获(记录日志,返回空列表)。
实现自定义加载器¶
要添加新文件格式的支持,编写一个满足加载器构造函数模式的模块。每个内置加载器都遵循这种形状:
module My_loader : sig
val make : Workspace.workspace -> string ->
(unit -> Document.t list, Document.load_error) result
end
make 接受一个 workspace(用于路径安全验证)和文件路径。成功时返回一个 thunk,调用时读取文件并产出 Document.t list。失败时返回 load_error。
一个最小的自定义加载器:
open Par
let make ws path =
match Workspace.admit ws path with
| Error e -> Error (Document.Workspace_rejected e)
| Ok sandboxed ->
let full_path = Workspace.to_string sandboxed in
Ok (fun () ->
let ic = open_in full_path in
Fun.protect
~finally:(fun () -> close_in ic)
(fun () ->
let content = really_input_string ic (in_channel_length ic) in
let meta = Document.Meta.empty () in
Document.Meta.add_string meta "file_type" "application/x-myformat";
Ok [{ Document.content; metadata = meta; source = path }]))
内置加载器¶
PAR 提供六个格式加载器。每个从单个文件产出 Document.t list。
Text_loader (.txt)¶
读取纯文本文件,产出一个带标准元数据的 Document.t。
open Par
let ws = Workspace.of_cwd () in
match Text_loader.make ws "notes.txt" with
| Error e -> prerr_endline (Document.load_error_to_string e)
| Ok load ->
let docs = load () in
Printf.printf "Loaded %d document(s)\n" (List.length docs)
元数据字段: file_path、file_name、file_size、file_type = "text/plain"。
Markdown_loader (.md)¶
通过 Omd.of_string 解析 Markdown 并遍历 AST 提取纯文本(标题、段落、代码块、列表)。文件开头 --- 分隔符之间的 YAML frontmatter 通过 Yaml.of_string 解析,与标准字段合并到元数据中。
open Par
let ws = Workspace.of_cwd () in
match Markdown_loader.make ws "README.md" with
| Error e -> prerr_endline (Document.load_error_to_string e)
| Ok load ->
let docs = load () in
List.iter (fun doc ->
Printf.printf "Source: %s\n" doc.Document.source;
Printf.printf "Content length: %d\n" (String.length doc.Document.content)
) docs
元数据字段: file_path、file_name、file_size、file_type = "text/markdown",加上任何 YAML frontmatter 键值对。
Html_loader (.html)¶
读取 HTML 文件,去除 <script> 和 <style> 元素,通过 lambdasoup 提取可见文本内容。
open Par
let ws = Workspace.of_cwd () in
match Html_loader.make ws "page.html" with
| Error e -> prerr_endline (Document.load_error_to_string e)
| Ok load ->
let docs = load () in
Printf.printf "Extracted %d document(s)\n" (List.length docs)
元数据字段: file_path、file_name、file_size、file_type = "text/html"。
Csv_loader (.csv)¶
读取 CSV 文件。表头行定义列名。每行数据(不含表头)产出一个 Document.t:
content:按"column_name: value\n"格式排列的键值行。metadata:每个列名以csv_为前缀映射到其值(作为String),加上row_index、file_path、file_name、file_size、file_type = "text/csv"。source:输入文件路径。
open Par
let ws = Workspace.of_cwd () in
match Csv_loader.make ws "users.csv" with
| Error e -> prerr_endline (Document.load_error_to_string e)
| Ok load ->
let docs = load () in
Printf.printf "Loaded %d rows as documents\n" (List.length docs)
Pdf_loader (.pdf)¶
使用 camlpdf 的简单文本流提取从 PDF 中提取文本。每页产出一个 Document.t,metadata["page"] 设为页码。
open Par
let ws = Workspace.of_cwd () in
match Pdf_loader.make ws "paper.pdf" with
| Error e -> prerr_endline (Document.load_error_to_string e)
| Ok load ->
let docs = load () in
Printf.printf "Extracted %d pages\n" (List.length docs)
元数据字段: file_path、file_name、file_size、file_type = "application/pdf"、page(int,从 1 开始)。
局限性¶
范围妥协 — 见 ROADMAP v0.7.0 第 2 节 #9
PDF 加载器使用简单文本流提取,而非布局保留提取。这是一个有意的范围妥协,附有退役计划。
- 不保留布局: 多栏 PDF 产出交错文本。列在输出中被混合在一起。
- 不支持 OCR: 扫描件或纯图片 PDF 无法提取任何内容。此加载器背后没有 Tesseract 或 OCR 引擎。
- 表格和复杂布局可能产出质量较差的输出。
这覆盖了大约 80% 的文本型 PDF(单栏或简单布局的研究论文、文档、报告)。
布局感知提取的触发条件: 如果下游集成报告失败率超过 20%,或者 v0.8 规划开始时(以先到者为准)。迁移路径是替换 Pdf_loader.extract_text 的内部实现而不更改公共接口,因为 Document.t -> Document.t 契约保持不变。
Docx_loader (.docx)¶
从 Word .docx 文件中提取纯文本。.docx 格式是 OOXML ZIP 容器;加载器用 camlzip 打开它,读取 word/document.xml,并用 xmlm 解析 XML,从 <w:t> 元素中恢复可见文本。
open Par
let ws = Workspace.of_cwd () in
match Docx_loader.make ws "report.docx" with
| Error e -> prerr_endline (Document.load_error_to_string e)
| Ok load ->
let docs = load () in
Printf.printf "Extracted %d document(s)\n" (List.length docs)
元数据字段: file_path、file_name、file_size(磁盘上文件的实际大小,而非提取文本的长度)、file_type = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"。
工作原理¶
.docx 文件是包含名为 word/document.xml 的 OOXML 部件的 ZIP 归档。加载器:
- 用 camlzip(
Zip.open_in)打开 ZIP 容器。 - 读取
word/document.xml条目。 - 用 xmlm 流式解析 XML,从
<w:t>元素(WordprocessingML 文本运行)中累积字符数据。 - 将结构元素映射为空白:
<w:p>(段落)→ 换行,<w:tab>→ 制表符,<w:br>(换行符)→ 换行。
仅捕获 <w:t> 元素内的字符数据,因此域指令(<w:instrText>)和修订删除文本(<w:delText>)被排除。
局限性¶
- 不保留格式: 粗体、斜体、字号、颜色、样式均被丢弃 —— 只有纯文本保留。
- 表格被扁平化: 表格单元格文本按文档顺序提取,不保留行列结构。
- 不支持图片: 嵌入的图片、图表、形状不产出任何内容。
- 不支持页眉/页脚: 仅读取文档正文(
word/document.xml);页眉、页脚、脚注(ZIP 中的独立部件)不被提取。 - 不支持 .doc(旧格式): 不支持旧的二进制 Word
.doc格式。仅处理.docx(OOXML,Word 2007+)。
Directory_loader¶
目录加载器递归扫描目录树,根据文件扩展名将每个文件分发到对应的格式加载器。循环符号链接会被检测并跳过。
open Par
let ws = Workspace.of_cwd () in
match Directory_loader.load ws "docs/" with
| Error e -> prerr_endline (Document.load_error_to_string e)
| Ok docs ->
Printf.printf "Loaded %d documents from directory\n" (List.length docs)
default_map 覆盖 .txt、.md、.html、.csv、.pdf 和 .docx。未知扩展名通过 Logs.warn 跳过。单个文件的错误会被记录并跳过(扫描不会中止)。
自定义扩展名映射¶
你可以覆盖或扩展映射表:
open Par
(* 为 .json 文件添加自定义加载器 *)
let json_loader ws path =
match Workspace.admit ws path with
| Error e -> Error (Document.Workspace_rejected e)
| Ok () ->
Ok (fun () ->
let ic = open_in path in
let content = really_input_string ic (in_channel_length ic) in
close_in ic;
let meta = Document.Meta.empty () in
Document.Meta.add_string meta "file_type" "application/json";
[{ Document.content; metadata = meta; source = path }])
let my_map = (".json", json_loader) :: Directory_loader.default_map in
match Directory_loader.load ws ~map:my_map "data/" with
| Error e -> prerr_endline (Document.load_error_to_string e)
| Ok docs -> Printf.printf "Loaded %d documents\n" (List.length docs)
loader_fn 的类型是 Workspace.workspace -> string -> (unit -> Document.t list, Document.load_error) result,与每个内置加载器的 make 函数签名一致。
与 RAG 的组合¶
完整管道:加载文档,分块,嵌入向量,存储向量,通过 invoke_with_rag 查询。
open Par
open Types
let ws = Workspace.of_cwd () in
(* 1. 从目录加载文档 *)
let docs = Directory_loader.load ws ~map:Directory_loader.default_map "knowledge_base/"
|> Result.get_ok in
(* 2. 对每个文档分块 *)
let all_chunks =
List.concat_map (fun doc ->
let chunks = Chunking.chunk_recursive
~text:doc.Document.content ~max_size:1000 ~overlap:200 in
List.map (fun c ->
({ Vector_store.id = Printf.sprintf "%s_%04d" doc.Document.source (Random.int 10000);
content = c.Chunking.text;
metadata = Some (Document.Meta.to_yojson doc.Document.metadata) },
c.Chunking.text) (* 将被实际向量替换 *)
) chunks
) docs
in
(* 3. 嵌入 chunks *)
let texts = List.map snd all_chunks in
match Runtime.embed rt texts with
| Error e ->
prerr_endline ("embed failed: " ^ Runtime.string_of_error_category e)
| Ok vecs ->
(* 4. 存入向量索引 *)
let doc_vecs = List.mapi (fun i (doc, _) ->
({ doc with Vector_store.id = Printf.sprintf "doc_%04d" i }, vecs.(i))
) all_chunks in
(match Vector_store.add store doc_vecs with
| Ok () ->
Printf.printf "Indexed %d chunks\n" (List.length doc_vecs);
(* 5. 通过 RAG 查询 *)
let (answer, _) = Runtime.invoke_with_rag rt
~agent_id:"rag_agent"
~message:"这个文档说了什么关于 X 的内容?"
~k:4
~vector_store:(Some store)
() in
Printf.printf "Answer: %s\n" answer
| Error e ->
prerr_endline ("store add failed: " ^ Runtime.string_of_error_category e))
要点:
Document.content作为~text参数传入Chunking.chunk_recursive。Document.Metadata.to_yojson将哈希表转换为Yojson.Safe.t,用于Vector_store.document.metadata。- 每个
Document.t的source字段成为向量存储文档 id 的基础。 - 加载和分块步骤与 embedding provider 无关。将 Mock 替换为 OpenAI 只需修改运行时配置。
错误处理¶
所有加载器返回 (unit -> Document.t list, load_error) result。模式如下:
match My_loader.make ws path with
| Error e ->
(* 处理错误:文件不存在、权限不足等 *)
prerr_endline (Document.load_error_to_string e)
| Ok load ->
let docs = load () in
(* 处理文档 *)
...
两步模式(make 返回 thunk,调用 thunk 产出文档)将路径验证与 I/O 分离。如果路径无效,会立即收到错误而不读取任何文件。提取过程中的异常会被捕获并记录日志(thunk 返回空列表而非崩溃)。
对于 Directory_loader,单个文件的错误会被记录并跳过,而不是中止整个扫描。这是有意的设计:知识库中一个损坏的 PDF 不应该阻止旁边的 Markdown 文件被索引。
另请参阅¶
- RAG API — embeddings、向量存储、分块、
invoke_with_rag - Streaming API —
invoke_stream、token 流式输出 - Agent API —
Runtime.invoke、agent 配置 - SDK 概览 — 模块映射与架构