04. langchaingo文档解析和分块策略

172 阅读3分钟

文档解析

文档解析技术的本质在于将格式各异、版式多样、元素多种的文档数据,包括段落、表格、标题、公式、多列、图片等文档区块,转化为阅读顺序正确的字符串信息。

数据除了存储到数据库中,还有的以 PDF、TXT、Word、PPT、Excel、CSV、Markdown、XML、HTML 等多种格式存储。 langchaingo的文档加载器在documentloaders下,如果langchaingo提供的文档加载器不满足,可以通过实现Loader接口打造自己的文档加载器

package main

import (
    "context"
    "errors"
    "fmt"
    "github.com/tmc/langchaingo/documentloaders"
    "github.com/tmc/langchaingo/schema"
    "github.com/tmc/langchaingo/textsplitter"
    "log"
    "os"
    "path/filepath"
)

func main() {
    docs, err := LoadDoc(context.Background(), "./docs")
    if err != nil {
       log.Fatalf("文件加载失败:%s", err.Error())
    }

    for _, doc := range docs {
       fmt.Printf("%s\n", doc)
    }
}

// LoadDoc 对目录或单个文件进行加载
func LoadDoc(ctx context.Context, fileName string) ([]schema.Document, error) {
    f, err := os.Lstat(fileName)
    if err != nil {
       return nil, err
    }

    // 加载单个文件
    if !f.IsDir() {
       return LoadFile(ctx, fileName)
    }

    // 加载文件夹下的文件
    dir, err := os.ReadDir(fileName)
    if err != nil {
       return nil, err
    }

    docs := make([]schema.Document, 0, 100)
    for _, d := range dir {
       rd, err := LoadDoc(ctx, filepath.Join(fileName, d.Name()))
       if err != nil {
          return nil, err
       }
       docs = append(docs, rd...)
    }
    return docs, nil
}

// LoadFile 读取文件内容信息
func LoadFile(ctx context.Context, fileName string) ([]schema.Document, error) {
    file, err := os.Open(fileName)
    if err != nil {
       return nil, err
    }
    defer file.Close()

    var (
       loader documentloaders.Loader
    )

    finfo, err := file.Stat()
    if err != nil {
       return nil, err
    }


    ext := filepath.Ext(fileName)
    switch ext {
    case ".md":
       loader = documentloaders.NewText(file)
    case ".txt":
       loader = documentloaders.NewText(file)
    case ".pdf":
       loader = documentloaders.NewPDF(file, finfo.Size())
    default:
       return nil, errors.New("不支持的文档类型:" + ext)
    }
    
    return loader.Load(ctx)
}

分块切片

文档数据(Documents)经过解析后,通过分块技术将信息内容划分为适当大小的文档片段(chunks),从而使系统能够高效处理和精准检索这些片段信息。分块的本质在于依据一定逻辑或语义原则,将较长文本拆解为更小的单元。

分块的目标在于确保每个片段在保留核心语义的同时,具备相对独立的语义完整性,从而使模型在处理时不必依赖广泛的上下文信息,增强检索召回的准确性。合理的分块能够确保检索到的片段与用户查询信息高度匹配,避免信息冗余或丢失。 有助于提升生成内容的连贯性,精心设计的独立语义片段可以降低模型对上下文的依赖,从而增强生成的逻辑性与一致性。还会影响系统的响应速度与效率,模型能够更快、更准确地处理和生成内容。

如果片段过大,可能导致向量无法精确捕捉内容的特定细节并且计算成本增加;若片段过小,则可能丢失上下文信息,导致句子碎片化和语义不连贯。

常见的分块策略包括:

  • 固定大小分块
  • 重叠分块
  • 递归分块
  • 文档特定分块
  • 语义分块
  • 混合分块
//// 不添加元数据,直接返回切割后的数据信息
//return loader.LoadAndSplit(ctx, spliter)

// 加载并拆分文档
docs, err := loader.Load(ctx)
if err != nil {
    return nil, err
}

texts := make([]string, len(docs))
metadatas := make([]map[string]interface{}, len(docs))
for i, doc := range docs {
    texts[i] = doc.PageContent
    meta := doc.Metadata
    for k, v := range metadata {
        if _, ok := meta[k]; !ok {
            meta[k] = v
        }
    }
    metadatas[i] = meta
}
return textsplitter.CreateDocuments(spliter, texts, metadatas)

本文代码