Eino的基础组件(四)文档加载与解析

深入理解 Eino 框架中文档处理组件的设计与实现,掌握 Document Loader、Parser、Transformer 的协作机制,构建高效的 RAG 文档处理流程

✨ 写在前面

当我们构建 RAG(检索增强生成)应用时,文档处理是最基础也是最关键的一环。模型需要从外部知识库中检索相关信息来增强回答的准确性,但在检索之前,我们必须先解决一个核心问题:如何把各种格式的文档(PDF、Word、网页、Markdown)加载进来,解析成统一的格式,再按需分割成合适的片段?

这个看似简单的流程,实际上隐藏着不少工程挑战。不同来源的文档(本地文件、网络 URL、对象存储)需要不同的加载方式;不同格式的文档(文本、PDF、HTML)需要不同的解析逻辑;长文档需要分割,但如何保持语义完整性?如何在处理过程中保留元信息(来源、时间戳、章节结构)供后续检索使用?

Eino 对这些问题做了清晰的分层设计。它把文档处理拆分成三个独立但协作的组件:Loader 负责加载Parser 负责解析Transformer 负责转换。这种分层不仅让每个组件的职责明确,更重要的是让你可以灵活组合——想换个文档来源?换个 Loader。想支持新格式?加个 Parser。想调整分割策略?换个 Transformer。组件之间通过标准的 Document 结构传递数据,整个流程就像流水线一样清晰可控。

这篇文章会从接口设计讲起,结合实战代码,帮你理解这套文档处理体系——无论你是在做知识库问答、文档分析还是智能客服,这些知识都是基础。


🗺️ 文档处理组件体系

Eino 的文档处理体系围绕一个核心数据结构和三个协作组件展开。

📄 Document 标准结构

所有组件之间传递的都是标准化的 Document 结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
type Document struct {
    // ID 是文档的唯一标识符
    ID string
    
    // Content 是文档的实际内容
    Content string
    
    // MetaData 存储文档的元信息
    MetaData map[string]any
}

这三个字段看似简单,但设计得很巧妙:

  • ID:在向量检索、去重、溯源场景中,ID 是关键。可以用文件路径、URL 或者自定义规则生成。
  • Content:文档的文本内容,是模型真正要处理的数据。
  • MetaData:文档的元数据,可以存储如下信息:文档从哪里来(source)、属于哪个章节(chapter)、向量表示是什么(embedding)、检索得分多少(score)——所有这些信息都通过 MetaData 在组件间传递。
    • 文档的来源信息
    • 文档的向量表示(用于向量检索)
    • 文档的分数(用于排序)
    • 文档的子索引(用于分层检索)
    • 其他自定义元数据

🔗 组件协作关系

三个组件各司其职,又紧密协作:

  • Document Loader(文档加载器):负责从不同来源加载原始内容,转换成 Document 列表。它的输入是 Source(包含 URI),输出是 []*Document
1
2
3
type Loader interface {
    Load(ctx context.Context, src Source, opts ...LoaderOption) ([]*schema.Document, error)
}
  • Document Parser(文档解析器):这不是独立组件,而是 Loader 内部使用的工具。它负责把原始字节流(io.Reader)解析成 Document。Parser 支持各种格式:文本、PDF、HTML、Markdown 等。
1
2
3
type Parser interface {
    Parse(ctx context.Context, reader io.Reader, opts ...Option) ([]*schema.Document, error)
}
  • Document Transformer(文档转换器):负责对 Document 列表进行转换操作:分割长文档、过滤不相关内容、合并小片段等。输入和输出都是 []*Document
1
2
3
type Transformer interface {
    Transform(ctx context.Context, src []*schema.Document, opts ...TransformerOption) ([]*schema.Document, error)
}

协作流程

1
原始文件/URL  →  Loader(内部调用 Parser)  →  []*Document  →  Transformer  →  []*Document  →  向量化存储

这种分层的好处是显而易见的:

  • Loader 专注于"从哪里加载",不关心格式解析
  • Parser 专注于"如何解析格式",不关心来源
  • Transformer 专注于"如何处理内容",不关心前面怎么来的

你可以自由组合:用 File Loader 加载本地 PDF(内部用 PDF Parser),然后用 Semantic Splitter 按语义分割;或者用 Web Loader 抓取网页(内部用 HTML Parser),然后用 Markdown Splitter 按标题分割。每个组件都可以独立替换和扩展。


📥 Document Loader 组件

Document Loader 是文档处理的入口,负责从各种数据源加载文档。它的主要作用是从不同来源(如网络 URL、本地文件等)加载文档内容,并将其转换为标准的文档格式。这个组件在处理需要从各种来源获取文档内容的场景中发挥重要作用,比如:

  • 从网络 URL 加载网页内容
  • 读取本地 PDF、Word 等格式的文档

🎪 接口定义

Loader 的接口非常简洁:

1
2
3
type Loader interface {
    Load(ctx context.Context, src Source, opts ...LoaderOption) ([]*schema.Document, error)
}
  • 功能:从指定的数据源加载文档
  • 参数:
    • ctx:上下文对象,用于传递请求级别的信息,同时也用于传递 Callback Manager
    • src:文档来源,包含文档的 URI 信息
    • opts:加载选项,用于配置加载行为
  • 返回值:
    • []*schema.Document:加载的文档列表
    • error:加载过程中的错误信息

📍 Source 数据源

Source 结构极简,只有一个字段:

1
2
3
type Source struct {
    URI string  // 统一资源标识符:可以是文件路径、网络 URL、S3 路径等
}

这个设计的巧妙之处在于:不同的 Loader 实现可以用自己的方式解释 URI

  • file.FileLoader 把 URI 当作本地文件路径
  • web.WebLoader 把 URI 当作网络 URL
  • s3.S3Loader 把 URI 当作 S3 对象路径

统一的接口,灵活的实现——这就是面向接口编程的威力。

🔧 独立使用示例

最简单的用法是直接调用 Loader:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import (
    "github.com/cloudwego/eino/components/document"
    "github.com/cloudwego/eino-ext/components/document/loader/file"
)

// 初始化文件加载器
loader, err := file.NewFileLoader(ctx, &file.FileLoaderConfig{
    UseNameAsID: true,  // 使用文件名作为 Document ID
    Parser:      &parser.TextParser{},  // 配置解析器
})

// 加载文档
filePath := "/path/to/document.md"
docs, err := loader.Load(ctx, document.Source{
    URI: filePath,
})

// 使用文档内容
log.Printf("文档内容: %v", docs[0].Content)
log.Printf("文档来源: %v", docs[0].MetaData["source"])

这里有个重要的设计细节:FileLoaderConfig 中可以配置 Parser。这意味着同一个 File Loader 可以通过不同的 Parser 支持不同格式——配置 PDF Parser 就能加载 PDF,配置 HTML Parser 就能加载 HTML。

🎼 编排中的使用

Loader 可以无缝集成到 Eino 的编排系统中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 在 Chain 中使用
chain := compose.NewChain[string, []*schema.Document]()
chain.AppendLoader(loader)

runnable, _ := chain.Compile()
result, _ := runnable.Invoke(ctx, filePath)

// 在 Graph 中使用
graph := compose.NewGraph[string, []*schema.Document]()
graph.AddLoaderNode("loader_node", loader)

编排的好处是可以把 Loader 和其他组件(Transformer、Embedding、Indexer)串联起来,构建完整的文档处理流水线。

🎛️ Option 与 Callback

Loader 支持运行时 Option 和 Callback 机制:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import (
    "github.com/cloudwego/eino/callbacks"
    callbacksHelper "github.com/cloudwego/eino/utils/callbacks"
)

// 创建 Callback Handler
handler := &callbacksHelper.LoaderCallbackHandler{
    OnStart: func(ctx context.Context, info *callbacks.RunInfo, input *document.LoaderCallbackInput) context.Context {
        log.Printf("开始加载文档: %s\n", input.Source.URI)
        return ctx
    },
    OnEnd: func(ctx context.Context, info *callbacks.RunInfo, output *document.LoaderCallbackOutput) context.Context {
        log.Printf("加载完成,共 %d 个文档\n", len(output.Docs))
        return ctx
    },
    OnError: func(ctx context.Context, info *callbacks.RunInfo, err error) context.Context {
        log.Printf("加载失败: %v\n", err)
        return ctx
    },
}

// 使用 Callback
helper := callbacksHelper.NewHandlerHelper().Loader(handler).Handler()
docs, err := loader.Load(ctx, document.Source{URI: filePath}, compose.WithCallbacks(helper))

Callback 在生产环境中非常有用:

  • 记录加载耗时
  • 监控失败率
  • 追踪文档来源
  • 集成到 APM 系统

🏪 现有实现

Eino 生态提供了三个开箱即用的 Loader:

  1. File Loader:加载本地文件系统的文档
  2. Web Loader:加载网络 URL 指向的文档
  3. S3 Loader:加载 S3 兼容存储系统的文档

这三个 Loader 覆盖了大部分场景。如果你的文档存储在数据库、消息队列或其他系统中,可以参考这些实现自己扩展。


🔬 Document Parser 工具

Document Parser 是一个用于解析文档内容的工具包。它不是一个独立的组件,而是作为 Document Loader 的内部工具,用于将不同格式的原始内容解析成标准的文档格式。Parser 支持:

  • 解析不同格式的文档内容(如文本、PDF、Markdown 等)
  • 根据文件扩展名自动选择合适的解析器 (eg:ExtParser)
  • 为解析后的文档添加元数据信息

🎨 Parser 接口

Parser 的接口定义:

1
2
3
type Parser interface {
    Parse(ctx context.Context, reader io.Reader, opts ...Option) ([]*schema.Document, error)
}

核心特点

  • 功能:从 Reader 中解析文档内容
  • 参数:
    • ctx:上下文对象
    • reader:提供原始内容的 Reader,输入是 io.Reader(字节流),不关心数据从哪来
    • opts:解析选项,通过 opts 传递元信息(URI、额外的 MetaData)
  • 返回值:
    • []*schema.Document:解析后的文档列表
    • error:解析过程中的错误

📋 通用选项

Parser 提供了两个通用选项:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
type Options struct {
    URI       string           // 文档的 URI,用于 ExtParser 选择解析器
    ExtraMeta map[string]any   // 额外的元信息,会合并到每个 Document 的 MetaData 中
}

// 使用方式
docs, err := parser.Parse(ctx, reader, 
    parser.WithURI(filePath),
    parser.WithExtraMeta(map[string]any{
        "source": "local",
        "timestamp": time.Now(),
    }),
)

ExtraMeta 非常有用——你可以在解析阶段就注入元信息,后续的 Transformer、Retriever 都能访问到这些信息。

📚 内置解析器

Eino 提供了两个基础 Parser:

  • TextParser(文本解析器):最简单的解析器,直接把输入当作文本:
1
2
3
textParser := parser.TextParser{}
docs, _ := textParser.Parse(ctx, strings.NewReader("Hello World"))
log.Printf("内容: %v", docs[0].Content)  // 输出: Hello World
  • ExtParser(扩展名解析器):根据文件扩展名自动选择解析器:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import (
    "github.com/cloudwego/eino-ext/components/document/parser/html"
    "github.com/cloudwego/eino-ext/components/document/parser/pdf"
)

// 配置不同格式的解析器
htmlParser, _ := html.NewParser(ctx, &html.Config{
    Selector: gptr.Of("body"),  // 只提取 body 标签内容
})
pdfParser, _ := pdf.NewPDFParser(ctx, &pdf.Config{})

// 创建扩展名解析器
extParser, _ := parser.NewExtParser(ctx, &parser.ExtParserConfig{
    Parsers: map[string]parser.Parser{
        ".html": htmlParser,
        ".pdf":  pdfParser,
    },
    FallbackParser: parser.TextParser{},  // 未知格式时的后备解析器
})

// 使用时,ExtParser 会根据 URI 的扩展名自动选择解析器
file, _ := os.Open("./document.pdf")
docs, _ := extParser.Parse(ctx, file, parser.WithURI("./document.pdf"))

ExtParser 是实际项目中最常用的 Parser——你不需要手动判断文件类型,它会根据扩展名自动路由到对应的解析器。

🔌 与 Loader 集成

Parser 主要在 Loader 内部使用:

1
2
3
4
5
6
7
8
// 创建 File Loader 时配置 Parser
loader, err := file.NewFileLoader(ctx, &file.FileLoaderConfig{
    UseNameAsID: true,
    Parser:      extParser,  // 使用扩展名解析器
})

// 之后加载任何文件,Loader 都会自动调用 Parser 解析
docs, err := loader.Load(ctx, document.Source{URI: "./document.pdf"})

这种设计的优势是:Loader 和 Parser 解耦。Loader 只负责"从哪里读",Parser 只负责"怎么解析"。你可以用同一个 File Loader 加载不同格式的文档,只需要配置不同的 Parser。

🛠️ 自定义实现

如果需要支持特殊格式(比如公司内部的文档格式),可以实现自己的 Parser:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
type CustomParser struct {
    defaultEncoding string
    defaultMaxSize  int64
}

func (p *CustomParser) Parse(ctx context.Context, reader io.Reader, opts ...parser.Option) ([]*schema.Document, error) {
    // 1. 获取通用选项
    commonOpts := parser.GetCommonOptions(&parser.Options{}, opts...)
    
    // 2. 获取自定义选项
    myOpts := &myOptions{
        Encoding: p.defaultEncoding,
        MaxSize:  p.defaultMaxSize,
    }
    myOpts = parser.GetImplSpecificOptions(myOpts, opts...)
    
    // 3. 解析逻辑
    content, err := io.ReadAll(reader)
    if err != nil {
        return nil, err
    }
    
    // 4. 构造 Document,合并元信息
    doc := &schema.Document{
        Content:  string(content),
        MetaData: commonOpts.ExtraMeta,  // 使用传入的额外元信息
    }
    
    return []*schema.Document{doc}, nil
}

关键点:

  • 通过 GetCommonOptions 获取通用选项(URI、ExtraMeta)
  • 通过 GetImplSpecificOptions 获取自定义选项
  • ExtraMeta 合并到 Document 的 MetaData 中

⚗️ Document Transformer 组件

Document Transformer 是一个用于文档转换和处理的组件,负责对已加载的文档进行转换处理,它的主要作用是对输入的文档进行各种转换操作,如分割、过滤、合并等,从而得到满足特定需求的文档。这个组件可用于以下场景中:

  • 将长文档分割成小段落以便于处理
  • 根据特定规则过滤文档内容
  • 对文档内容进行结构化转换
  • 提取文档中的特定部分

🎯 Transform 接口

Transformer 的接口定义:

1
2
3
type Transformer interface {
    Transform(ctx context.Context, src []*schema.Document, opts ...TransformerOption) ([]*schema.Document, error)
}

核心特点

  • 功能:对输入的文档进行转换处理
  • 参数:
    • ctx:上下文对象,用于传递请求级别的信息,同时也用于传递 Callback Manager
    • src:待处理的文档列表
    • opts:可选参数,用于配置转换行为
  • 返回值:
    • []*schema.Document:转换后的文档列表
    • error:转换过程中的错误信息

🧩 使用场景

Transformer 的典型场景:

  • 场景 1:分割长文档:RAG 应用中,长文档需要分割成小片段才能有效检索。Eino 提供了多种分割策略:

    • Markdown Splitter:按 Markdown 标题层级分割

    • Recursive Splitter:递归分割,保证每个片段不超过指定长度

    • Semantic Splitter:基于语义相似度分割,保持语义完整性

  • 场景 2:过滤内容:过滤掉不相关的文档(比如根据关键词、长度、元信息)。

场景 3:提取特定部分:从文档中提取特定章节、段落或结构化数据。

🔄 独立与编排使用

独立使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import (
    "github.com/cloudwego/eino-ext/components/document/transformer/splitter/markdown"
)

// 初始化 Markdown 分割器
transformer, _ := markdown.NewHeaderSplitter(ctx, &markdown.HeaderConfig{
    Headers: map[string]string{
        "##": "",  // 按二级标题分割
    },
})

// 转换文档
markdownDoc := &schema.Document{
    Content: "## 第一章\n内容1\n## 第二章\n内容2",
}
transformedDocs, _ := transformer.Transform(ctx, []*schema.Document{markdownDoc})

for idx, doc := range transformedDocs {
    log.Printf("片段 %d: %v", idx, doc.Content)
}

编排中使用

1
2
3
4
5
6
7
// 在 Chain 中使用
chain := compose.NewChain[[]*schema.Document, []*schema.Document]()
chain.AppendDocumentTransformer(transformer)

// 在 Graph 中使用
graph := compose.NewGraph[[]*schema.Document, []*schema.Document]()
graph.AddDocumentTransformerNode("transformer_node", transformer)

📦 现有实现

Eino 生态提供了三种分割器:

Markdown Splitter

按 Markdown 标题层级分割,适合结构化文档。

Recursive Splitter

递归分割策略,适合通用文本:

  • 先按段落分割
  • 如果段落过长,再按句子分割
  • 如果句子还长,再按字符分割

Semantic Splitter

基于语义相似度的智能分割,保持语义连贯性。需要 Embedding 模型支持。

🏗️ 自定义实现

自定义 Transformer 的模板:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
type MyTransformer struct {
    chunkSize int
    overlap   int
}

func (t *MyTransformer) Transform(ctx context.Context, src []*schema.Document, opts ...document.TransformerOption) ([]*schema.Document, error) {
    // 1. 处理选项
    options := &myOptions{
        ChunkSize: t.chunkSize,
        Overlap:   t.overlap,
    }
    options = document.GetTransformerImplSpecificOptions(options, opts...)
    
    // 2. 触发开始回调
    ctx = callbacks.OnStart(ctx, &document.TransformerCallbackInput{
        Input: src,
    })
    
    // 3. 执行转换逻辑
    docs, err := t.doTransform(ctx, src, options)
    
    // 4. 处理错误和结束回调
    if err != nil {
        ctx = callbacks.OnError(ctx, err)
        return nil, err
    }
    
    ctx = callbacks.OnEnd(ctx, &document.TransformerCallbackOutput{
        Output: docs,
    })
    
    return docs, nil
}

func (t *MyTransformer) doTransform(ctx context.Context, src []*schema.Document, opts *myOptions) ([]*schema.Document, error) {
    var result []*schema.Document
    
    for _, doc := range src {
        // 分割逻辑:按 ChunkSize 分割,保留 Overlap 重叠
        chunks := splitWithOverlap(doc.Content, opts.ChunkSize, opts.Overlap)
        
        for i, chunk := range chunks {
            // 构造新的 Document,继承原 MetaData
            newDoc := &schema.Document{
                ID:       fmt.Sprintf("%s_chunk_%d", doc.ID, i),
                Content:  chunk,
                MetaData: copyMetaData(doc.MetaData),  // 继承元信息
            }
            // 添加分块信息
            newDoc.MetaData["chunk_index"] = i
            newDoc.MetaData["parent_id"] = doc.ID
            
            result = append(result, newDoc)
        }
    }
    
    return result, nil
}

关键点

  • 新生成的 Document 应该继承原 Document 的 MetaData
  • 可以添加额外的元信息(chunk_index、parent_id)供后续使用
  • 触发 Callback 以支持监控和追踪

💼 实战:RAG 文档处理流程

现在我们把三个组件串联起来,构建一个完整的 RAG 文档处理流程。

📖 完整处理链路

典型的 RAG 文档处理包含以下步骤:

1
2
3
4
5
6
7
本地 PDF 文件 
  → File Loader(内部使用 PDF Parser)
  → []*Document(完整文档)
  → Markdown Splitter
  → []*Document(分割后的片段)
  → Embedding(向量化)
  → Indexer(存储到向量数据库)

🧪 代码示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import (
    "github.com/cloudwego/eino/components/document"
    "github.com/cloudwego/eino/compose"
    "github.com/cloudwego/eino-ext/components/document/loader/file"
    "github.com/cloudwego/eino-ext/components/document/parser/pdf"
    "github.com/cloudwego/eino-ext/components/document/transformer/splitter/recursive"
)

func buildRAGPipeline(ctx context.Context) error {
    // 1. 创建 PDF Parser
    pdfParser, err := pdf.NewPDFParser(ctx, &pdf.Config{})
    if err != nil {
        return err
    }
    
    // 2. 创建 File Loader,配置 PDF Parser
    loader, err := file.NewFileLoader(ctx, &file.FileLoaderConfig{
        UseNameAsID: true,
        Parser:      pdfParser,
    })
    if err != nil {
        return err
    }
    
    // 3. 创建 Recursive Splitter
    splitter, err := recursive.NewRecursiveSplitter(ctx, &recursive.Config{
        ChunkSize:   1000,  // 每个片段最多 1000 字符
        ChunkOverlap: 200,  // 片段间重叠 200 字符
    })
    if err != nil {
        return err
    }
    
    // 4. 构建 Chain
    chain := compose.NewChain[string, []*schema.Document]()
    
    // 添加 Loader 节点
    chain.AppendLoader(loader)
    
    // 添加 Transformer 节点
    chain.AppendDocumentTransformer(splitter)
    
    // 5. 编译并运行
    runnable, err := chain.Compile(ctx)
    if err != nil {
        return err
    }
    
    // 6. 处理文档
    pdfPath := "/path/to/knowledge-base.pdf"
    docs, err := runnable.Invoke(ctx, pdfPath)
    if err != nil {
        return err
    }
    
    // 7. 输出结果
    log.Printf("共分割成 %d 个片段", len(docs))
    for i, doc := range docs {
        log.Printf("片段 %d (ID: %s):", i, doc.ID)
        log.Printf("  内容长度: %d", len(doc.Content))
        log.Printf("  来源: %v", doc.MetaData["source"])
        log.Printf("  chunk_index: %v", doc.MetaData["chunk_index"])
    }
    
    // 8. 后续步骤:向量化和存储
    // embedding, _ := openai.NewEmbedding(...)
    // indexer, _ := vdb.NewIndexer(...)
    // ...
    
    return nil
}

这个流程的亮点

  • 链式组合:Loader 和 Transformer 通过 Chain 无缝衔接
  • 元信息传递:文档来源、分块索引等信息自动传递
  • 可替换性:想换成 Web Loader 加载网页?只需要替换 Loader,其他代码不变
  • 可扩展性:想加入过滤逻辑?再加一个 Transformer 节点即可

💎 最佳实践

✅ MetaData 管理

MetaData 是文档处理的"记忆",合理使用 MetaData 可以大大提升系统的可维护性:

推荐的 MetaData 字段

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
doc.MetaData = map[string]any{
    // 来源信息
    "source":      "/path/to/file.pdf",
    "source_type": "local_file",
    "loaded_at":   time.Now(),
    
    // 结构信息
    "parent_id":    "doc_123",
    "chunk_index":  0,
    "total_chunks": 5,
    
    // 检索信息(后续添加)
    "embedding":    []float32{...},
    "score":        0.95,
    
    // 业务信息
    "category":    "技术文档",
    "author":      "张三",
    "create_date": "2026-08-20",
}

MetaData 的传递规则

  • Loader 加载时注入来源信息
  • Parser 解析时注入格式信息
  • Transformer 转换时继承父文档的 MetaData,并添加分块信息
  • Retriever 检索时添加得分信息

🚨 错误处理

文档处理过程中可能遇到各种错误:文件不存在、格式不支持、解析失败等。推荐的错误处理策略:

  • 使用 Callback 统一处理
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
handler := &callbacksHelper.LoaderCallbackHandler{
    OnError: func(ctx context.Context, info *callbacks.RunInfo, err error) context.Context {
        // 记录日志
        log.Printf("文档加载失败: %v", err)
        
        // 上报监控
        metrics.RecordError("document_loader", err)
        
        // 保存失败记录
        saveFailedDocument(ctx, info, err)
        
        return ctx
    },
}
  • 返回有意义的错误信息
1
2
3
if err != nil {
    return nil, fmt.Errorf("加载文档失败 [%s]: %w", src.URI, err)
}

⚡ 性能优化

  • 批量处理:如果要处理大量文档,尽量批量操作而不是逐个处理:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 不推荐:逐个处理
for _, filePath := range filePaths {
    docs, _ := loader.Load(ctx, document.Source{URI: filePath})
    process(docs)
}

// 推荐:批量加载后统一处理
var allDocs []*schema.Document
for _, filePath := range filePaths {
    docs, _ := loader.Load(ctx, document.Source{URI: filePath})
    allDocs = append(allDocs, docs...)
}
transformedDocs, _ := transformer.Transform(ctx, allDocs)
  • 复用 Parser 实例:Parser 的创建可能涉及资源初始化(比如 PDF 库),应该复用实例而不是每次都创建:
1
2
3
4
5
6
// 创建一次
pdfParser, _ := pdf.NewPDFParser(ctx, &pdf.Config{})

// 复用多次
loader1, _ := file.NewFileLoader(ctx, &file.FileLoaderConfig{Parser: pdfParser})
loader2, _ := web.NewWebLoader(ctx, &web.WebLoaderConfig{Parser: pdfParser})
  • 控制并发:大量文档处理时,使用 goroutine 池控制并发数,避免资源耗尽:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
semaphore := make(chan struct{}, 10)  // 最多 10 个并发
var wg sync.WaitGroup

for _, filePath := range filePaths {
    wg.Add(1)
    semaphore <- struct{}{}  // 获取许可
    
    go func(path string) {
        defer wg.Done()
        defer func() { <-semaphore }()  // 释放许可
        
        docs, _ := loader.Load(ctx, document.Source{URI: path})
        process(docs)
    }(filePath)
}

wg.Wait()

📝 总结

Eino 的文档处理体系通过 Loader、Parser、Transformer 三个协作组件,构建了一套清晰、灵活、可扩展的文档处理架构。核心设计理念是:

  1. 职责分离:加载、解析、转换各司其职,互不干扰
  2. 标准化接口:所有组件通过 Document 结构传递数据,元信息通过 MetaData 传递
  3. 灵活组合:通过 Chain、Graph 可以自由组合各种处理流程
  4. 可扩展性:每个组件都可以自定义实现,满足特殊需求

在实际的 RAG 应用中,文档处理是最基础的一环。理解了这套体系,你就可以:

  • 从各种来源加载文档(本地、网络、对象存储)
  • 解析各种格式(文本、PDF、HTML、Markdown)
  • 按需转换文档(分割、过滤、提取)
  • 保留完整的元信息供后续检索使用

下一篇文章,我们会继续探讨 Eino 的 Embedding 和 Retriever 组件,看看如何把处理好的文档向量化并高效检索。


参考资料

最后更新于 2026-08-25 16:25 UTC
그 경기 끝나고 좀 멍하기 있었는데 여러분 이제 살면서 여러가
使用 Hugo 构建
主题 StackJimmy 设计