🏠

document_loader_contracts_and_options 模块详解

概述

document_loader_contracts_and_options 模块是 Eino 框架中文档摄取与解析系统的核心契约层。如果你把整个文档处理流水线想象成一个食品加工工厂,这个模块就是定义"什么是原材料"、"什么是加工机器"以及"如何配置机器"的设计图纸。

它解决的核心问题是:在保持组件接口统一的前提下,让不同的文档加载器(PDF加载器、DOCX加载器、网页加载器等)能够各自拥有灵活的配置选项,同时还能与框架的回调系统、解析器系统无缝协作。


架构概览

flowchart TB subgraph "调用方" APP[应用代码] end subgraph "document_loader_contracts_and_options" SRC[Source
文档来源] subgraph "option.go - 选项系统" LO[LoaderOption
加载器选项] TO[TransformerOption
转换器选项] LCOM[LoaderOptions
通用选项容器] end subgraph "interface.go - 核心接口" LDR[Loader 接口] TR[Transformer 接口] end subgraph "callback_extra_loader.go - 回调契约" LCI[LoaderCallbackInput] LCO[LoaderCallbackOutput] end end subgraph "依赖模块" SCH[schema.Document] PAR[parser.Option] CAL[callbacks] end APP --> SRC APP --> LO APP --> LDR LDR --> SCH LO --> PAR LO --> LCOM LCI --> CAL LCO --> CAL

核心组件职责

组件 职责 关键设计
Source 携带文档的 URI,充当所有加载器的输入凭证 极简数据结构,仅包含 URI 字段
Loader 接口 定义从 Source 加载文档的统一契约 返回 []*schema.Document,支持可变选项
Transformer 接口 定义文档转换(切分、过滤等)的统一契约 接收文档切片,返回处理后的文档切片
LoaderOption 统一的选项封装类型 双通道设计:通用选项 + 实现特定选项
LoaderOptions 承载所有加载器都需要的通用配置 目前主要包含 ParserOptions
LoaderCallbackInput/Output 回调系统的事件载荷 支持与通用回调系统双向转换

设计理念:双通道选项模式

这是本模块最核心、也最需要理解的设计决策。

解决的问题

想象一下:你正在开发一个文档处理框架。你定义了一个 Loader 接口:

type Loader interface {
    Load(ctx context.Context, src Source, opts ...LoaderOption) ([]*schema.Document, error)
}

现在有不同的实现:

  • PDFLoader 需要配置:页码范围、是否提取图片、OCR语言
  • WebLoader 需要配置:超时时间、请求头、是否保留HTML标签
  • DOCXLoader 需要配置:是否提取表格、样式处理模式

问题来了:如果你让所有选项都放在同一个 LoaderOptions 结构体里,那么:

  1. 这个结构体会无限膨胀,包含所有加载器的所有选项
  2. 新增一个加载器就要修改公共接口
  3. 不相关的选项会互相污染

传统方案的缺陷

方案 问题
继承/泛型 Go 不支持;即使支持也会导致类型耦合
每个 loader 定义自己的 LoadWithXXX 方法 破坏接口统一性,无法通过接口编程
使用 map[string]any 失去类型安全,编译时无法发现错误

本模块的解决方案:双通道函数式选项

type LoaderOption struct {
    // 通道1:通用选项 - 所有加载器都需要的配置
    apply func(opts *LoaderOptions)
    
    // 通道2:实现特定选项 - 用类型擦除实现灵活扩展
    implSpecificOptFn any
}

工作原理

  1. 通用选项(通过 apply 字段):框架定义的标准选项,如 WithParserOptions()。这些选项会被应用到公共的 LoaderOptions 结构体。

  2. 实现特定选项(通过 implSpecificOptFn 字段):每个加载器可以定义自己的选项函数,如 WithPDFPageRange()。这些选项通过类型擦除存储,在运行时被安全地提取回正确的类型。

// 加载器作者定义自己的选项结构
type PDFLoaderOptions struct {
    PageRange string  // 如 "1-10"
    OCRLang   string
}

// 定义选项函数(这是加载器作者的工作)
func WithPageRange(pr string) LoaderOption {
    return WrapLoaderImplSpecificOptFn(func(o *PDFLoaderOptions) {
        o.PageRange = pr
    })
}

// 在 Load 方法中提取选项
func (p *PDFLoader) Load(ctx context.Context, src Source, opts ...LoaderOption) ([]*schema.Document, error) {
    // 提取实现特定的选项
    myOpts := GetLoaderImplSpecificOptions(&PDFLoaderOptions{
        OCRLang: "en",  // 默认值
    }, opts...)
    
    // 提取通用选项
    commonOpts := GetLoaderCommonOptions(&LoaderOptions{}, opts...)
    
    // 使用配置...
}

为什么选择这个设计?

考量 决策 理由
类型安全 使用泛型 WrapLoaderImplSpecificOptFn[T] 编译时类型检查,避免 map[string]any 的运行时错误
接口统一 所有 loader 实现同一个 Loader 接口 框架可以统一调用,无需关心具体实现
可扩展性 新增加载器无需修改框架代码 加载器在自己的包里定义选项,框架无感知
向后兼容 通用选项用 apply 函数延迟应用 新增通用选项不影响现有实现

数据流分析

典型调用路径

用户代码
    │
    ▼
创建 Source (URI)
    │
    ▼
调用 Loader.Load(ctx, source, opts...)
    │
    ├─► 提取通用选项 (GetLoaderCommonOptions)
    │       │
    │       ▼
    │    传递给 Parser
    │
    └─► 提取实现特定选项 (GetLoaderImplSpecificOptions)
            │
            ▼
         使用自定义配置加载文档
            │
            ▼
    返回 []*schema.Document

回调机制的数据流

Loader.Load()
    │
    ├─► [TimingOnStart] 
    │         │
    │         ▼
    │    创建 LoaderCallbackInput
    │         │
    │         ▼
    │    回调处理器 (可修改输入)
    │
    ├─► 执行实际加载逻辑
    │
    └─► [TimingOnEnd]
              │
              ▼
         创建 LoaderCallbackInput + LoaderCallbackOutput
              │
              ▼
         回调处理器 (可修改输出)

回调输入/输出类型提供了转换函数 ConvLoaderCallbackInputConvLoaderCallbackOutput,可以与通用的 callbacks.CallbackInputcallbacks.CallbackOutput 进行双向转换。


与其他模块的关系

依赖关系(被依赖)

上游模块 关系说明
文档加载器实现 所有具体的加载器(PDF、DOCX、Web等)都实现本模块定义的 Loader 接口
文档转换器实现 所有转换器(切分器、过滤器等)都实现 Transformer 接口
parser 模块 LoaderOptions 中包含 ParserOptions,加载器可将解析任务委托给解析器

依赖关系(依赖他人)

下游模块 关系说明
schema.Document 加载器返回的文档类型定义在此
callbacks 回调输入输出类型与框架回调系统对接
parser.Option 解析器选项类型,用于配置文档解析过程

子模块说明

本模块包含以下逻辑子模块,文档将分别展开:

  1. document_loader_contracts_and_options-interface - 核心接口定义

    • Source 文档来源结构
    • Loader 加载器接口
    • Transformer 转换器接口
  2. document_loader_contracts_and_options-option - 选项系统

    • LoaderOption / TransformerOption 选项封装
    • LoaderOptions 通用选项容器
    • 选项提取函数:类型安全的双通道设计
  3. document_loader_contracts_and_options-callback_extra_loader - 回调契约

    • LoaderCallbackInput 加载开始时的回调输入
    • LoaderCallbackOutput 加载完成时的回调输出
    • 类型转换函数

潜在陷阱与注意事项

1. 选项类型匹配陷阱

// ❌ 错误:传递了错误类型的选项函数
opts := []LoaderOption{
    WrapLoaderImplSpecificOptFn(func(o *PDFLoaderOptions) { // 期望 PDFLoaderOptions
        o.PageRange = "1-10"
    }),
}
// 但加载器定义为 DOCXLoaderOptions!

GetLoaderImplSpecificOptions 使用类型断言 func(*T) 进行匹配,如果类型不匹配,选项会被静默忽略,不会报错。这是静默失败的设计——因为类型不匹配本身就是调用方的错误。

建议:为每种加载器创建独立的选项构造函数,放在对应的加载器包中,避免用户传错类型。

2. 通用选项与应用特定选项的优先级

当同一个选项既作为通用选项又作为实现特定选项传递时,它们是独立的通道,不会冲突。但需要注意的是:

// 两者都会被应用,但作用于不同的结构体
opts := []LoaderOption{
    WithParserOptions(parser.WithURI("...")),  // 通用选项 -> LoaderOptions.ParserOptions
    WrapLoaderImplSpecificOptFn(myOpts...),    // 实现特定选项 -> MyLoaderOptions
}

3. nil 指针处理

// GetLoaderImplSpecificOptions 如果传入 nil,会创建新的空结构体
opts := GetLoaderImplSpecificOptions(nil, opts...)
// 等价于:
opts := GetLoaderImplSpecificOptions(&MyOptions{}, opts...)

这个设计是合理的,因为它避免了 nil 指针解引用,但开发者需要意识到默认值的提供方式:如果需要默认值,应该在 base 参数中提供:

// ✅ 正确:提供默认值
opts := GetLoaderImplSpecificOptions(&MyOptions{
    Timeout: 30 * time.Second,
}, opts...)

// ❌ 可能不符合预期:默认值丢失
opts := GetLoaderImplSpecificOptions(nil, opts...)

4. 回调转换的边界情况

// ConvLoaderCallbackInput 对非预期类型返回 nil
input := ConvLoaderCallbackInput(someOtherType)
// input == nil,不会panic,但可能导致回调不执行

扩展点

为新加载器创建选项

package myloader

// 1. 定义自己的选项结构
type MyLoaderOptions struct {
    Timeout time.Duration
    Retry   int
}

// 2. 创建选项函数
func WithTimeout(t time.Duration) document.LoaderOption {
    return document.WrapLoaderImplSpecificOptFn(func(o *MyLoaderOptions) {
        o.Timeout = t
    })
}

func WithRetry(r int) document.LoaderOption {
    return document.WrapLoaderImplSpecificOptFn(func(o *MyLoaderOptions) {
        o.Retry = r
    })
}

// 3. 在 Load 方法中使用
func (m *MyLoader) Load(ctx context.Context, src document.Source, opts ...document.LoaderOption) ([]*schema.Document, error) {
    myOpts := document.GetLoaderImplSpecificOptions(&MyLoaderOptions{
        Timeout: 10 * time.Second,  // 默认值
        Retry:   3,
    }, opts...)
    
    // 使用 myOpts.Timeout, myOpts.Retry...
}

总结

这个模块的设计哲学可以概括为:在保持接口统一的同时拥抱实现多样性

  • 对框架:提供稳定的 LoaderTransformer 接口,无惧具体实现的变化
  • 对加载器开发者:提供完全的自由度来定义自己的配置选项,同时通过类型安全的泛型避免运行时错误
  • 对最终用户:通过统一的 API 调用不同的加载器,配置方式保持一致性

理解这个模块的关键在于理解"双通道选项模式"——它用类型擦除实现了 Go 中少见的高级类型系统特性,同时保持了 Go 的编译时检查和运行时效率。

On this page