05. 基于本地知识库的智能问答系统

157 阅读2分钟

当我们使用模型进行问答的时候, 我们想要基于本地的文件进行回答,我们需要搭建自己的本地知识库。具体流程分为下面几步

  1. 使用文档加载器加载本地文件
  2. 使用分割器对文件进行分割
  3. 将分割后的数据存储到向量数据库中
  4. 问答时对分割后的文档进行检索
  5. 把问题和文档传给模型,让模型使用文档提示生成答案

本文代码

数据载入和分割器

这部分信息可以查看 # 04. langchaingo文档解析和分块策略

这次我对文件加载进行了修改,添加了元数据信息,目的是方便导入时检查出重复数据,避免二次导入

var (
    ops = []textsplitter.Option{
       textsplitter.WithChunkSize(512),    // 切割后的大小块
       textsplitter.WithChunkOverlap(128), // 相邻文本之间的重叠
       textsplitter.WithCodeBlocks(true),  // 包含代码块
    }

    FilenameKey = "filename"
    UpdatedTime = "updated_time"
)

func (c *Client) 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
       spliter textsplitter.TextSplitter
    )

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

    // 定义元数据,方便检查重复导入数据
    metadata := map[string]string{
       FilenameKey: filename,
       UpdatedTime: finfo.ModTime().Format(time.DateTime),
    }
    ext := filepath.Ext(filename)
    switch ext {
    case ".md":
       loader = documentloaders.NewText(file)
       spliter = textsplitter.NewMarkdownTextSplitter(ops...)
    case ".txt":
       loader = documentloaders.NewText(file)
       spliter = textsplitter.NewRecursiveCharacter(ops...)
    case ".pdf":
       loader = documentloaders.NewPDF(file, finfo.Size())
       spliter = textsplitter.NewRecursiveCharacter(ops...)
    default:
       return nil, errors.New("不支持的文档类型:" + ext)
    }

    // 加载并拆分文档
    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)
}

将文档存储到向量库中

这部分信息可以查看# 03. 使用mongodb-atlas嵌入(Embedding)

func (c *Client) AddDocuments(ctx context.Context, filename string) ([]string, error) {
    docs, files, err := c.load(ctx, filename)
    if err != nil {
       return nil, err
    }

    // 获取表中所有数据
    list := make([]Vector, 0, len(files))
    cursor, err := c.mongo.coll.Find(ctx, bson.M{"metadata.filename": bson.M{"$in": files}})
    if err != nil {
       return nil, err
    }
    if err = cursor.All(ctx, &list); err != nil {
       return nil, err
    }
    fileExistsMap := make(map[string]string)
    for _, doc := range list {
       k := doc.Metadata[FilenameKey].(string)
       fileExistsMap[k] = doc.Metadata[UpdatedTime].(string)
    }

    // 去除重复文档数据
    newdocs := make([]schema.Document, 0, len(docs))
    for _, doc := range docs {
       key := doc.Metadata[FilenameKey].(string)
       val := doc.Metadata[UpdatedTime].(string)
       if fileExistsMap[key] != val {
          newdocs = append(newdocs, doc)
       }
    }
    if len(newdocs) < 1 {
       return []string{}, nil
    }

    store, err := c.GetStore()
    if err != nil {
       return nil, err
    }
    return store.AddDocuments(ctx, newdocs)
    
    // qwen2.5:3b模型不支持过滤函数, 如果你使用其他模型时,可以尝试使用这个进行过滤
    // return store.AddDocuments(ctx, newdocs, vectorstores.WithDeduplicater(func(ctx context.Context, doc schema.Document) bool {
    //      return true
    // }))
}

基于知识库进行问答

这部分代码是创建一个 retrievalQA链,这是一个检索问答模型,用于生成问题的答案, llm负责回答问题, retriveal负责根据问题检索相关的文档作为知识信息和问题一起传递给大模型。

func (c *Client) Chain(ctx context.Context, query string) (map[string]any, error) {
    store, err := c.GetStore()
    if err != nil {
       return nil, err
    }
    qa := chains.NewRetrievalQAFromLLM(c.LLM, vectorstores.ToRetriever(store, 10))
    return qa.Call(ctx, map[string]interface{}{"query": query})
}