eino

Eino

Eino字节跳动,基于 Golang 的大模型应用综合开发框架

入门案例

用 Eino 及 Ollama 快速搭建一个 LLM 应用。

func main() {
template := prompt.FromMessages(schema.FString,
schema.SystemMessage("你是一个{role}。"),
schema.UserMessage("问题: {question}"),
)

ctx := context.Background()
messages, err := template.Format(ctx, map[string]any{
"role": "乐于助人的助手",
"question": "如何学习 golang?",
})

if err != nil {
panic(err)
}

chatModel, err := ollama.NewChatModel(ctx, &ollama.ChatModelConfig{
BaseURL: "http://localhost:11434", // Ollama 服务地址
Model: "qwen2.5:14b", // 模型名称
})
if err != nil {
panic(err)
}

result, err := chatModel.Generate(ctx, messages)
if err != nil {
panic(err)
}
fmt.Printf("result.Content: %v\n", result.Content)
}

流式传输:

func main() {
template := prompt.FromMessages(schema.FString,
schema.SystemMessage("你是一个{role}。"),
schema.UserMessage("问题: {question}"),
)

ctx := context.Background()
messages, err := template.Format(ctx, map[string]any{
"role": "乐于助人的助手",
"question": "如何学习 golang?",
})

if err != nil {
panic(err)
}

chatModel, err := ollama.NewChatModel(ctx, &ollama.ChatModelConfig{
BaseURL: "http://localhost:11434", // Ollama 服务地址
Model: "qwen2.5:14b", // 模型名称
})
if err != nil {
panic(err)
}

streamResult, err := chatModel.Stream(ctx, messages)
if err != nil {
panic(err)
}

reportStream(streamResult)

}

func reportStream(sr *schema.StreamReader[*schema.Message]) {
defer sr.Close()

i := 0
for {
message, err := sr.Recv()
if err == io.EOF { // 流式输出结束
return
}
if err != nil {
log.Fatalf("recv failed: %v", err)
}
log.Printf("message[%d]: %+v\n", i, message)
i++
}
}

核心概念

Components 组件

Document Loader

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

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

示例:

func Run() {
ctx := context.Background()
loader, err := file.NewFileLoader(ctx, &file.FileLoaderConfig{
UseNameAsID: true,
})
if err != nil {
panic(err)
}

filePath := "Your File Path"
docs, err := loader.Load(ctx, document.Source{
URI: filePath,
})
if err != nil {
panic(err)
}

fmt.Println(docs[0].Content)
}

file.FileLoaderConfig 指定了 Loader 的参数配置,以及用哪个 Parser 来解析文档,FileLoader 核心方法 Load() 用于解析一个文档:

type FileLoaderConfig struct {
    UseNameAsID bool
    Parser      parser.Parser
}
// Parser is a document parser, can be used to parse a document from a reader.
type Parser interface {
    Parse(ctx context.Context, reader io.Reader, opts ...Option) ([]*schema.Document, error)
}
func (f *FileLoader) Load(ctx context.Context, src document.Source, opts ...document.LoaderOption) (docs []*schema.Document, err error)

自己实现一个按行解析的 parser.Parser

type LineParser struct{}

func (p *LineParser) Parse(ctx context.Context, reader io.Reader, opts ...parser.Option) ([]*schema.Document, error) {
var docs []*schema.Document

scanner := bufio.NewScanner(reader)
for scanner.Scan() {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}

line := scanner.Text()

doc := &schema.Document{
Content: line,
}
docs = append(docs, doc)
}

if err := scanner.Err(); err != nil {
return nil, err
}
return docs, nil
}

func Run() {
ctx := context.Background()
loader, err := file.NewFileLoader(ctx, &file.FileLoaderConfig{
UseNameAsID: true,
Parser: &LineParser{},
})
if err != nil {
panic(err)
}

filePath := "Your file path"
docs, err := loader.Load(ctx, document.Source{
URI: filePath,
})
if err != nil {
panic(err)
}

fmt.Println(docs[0].Content)
}

Document 结构体是文档的标准格式:

  • ID:文档的唯一标识符,用于在系统中唯一标识一个文档
  • Content:文档的实际内容
  • MetaData:文档的元数据,可以存储如下信息:
    • 文档的来源信息
    • 文档的向量表示(用于向量检索)
    • 文档的分数(用于排序)
    • 文档的子索引(用于分层检索)
    • 其他自定义元数据

Embedding

Embedding 组件是一个用于将文本转换为向量表示的组件。它的主要作用是将文本内容映射到向量空间,使得语义相似的文本在向量空间中的距离较近。这个组件在以下场景中发挥重要作用:

  • 文本相似度计算
  • 语义搜索
  • 文本聚类分析
import (
"context"
"fmt"

"github.com/cloudwego/eino-ext/components/embedding/ollama"
)

func Run() {
ctx := context.Background()
embedder, _ := ollama.NewEmbedder(ctx, &ollama.EmbeddingConfig{
BaseURL: "http://118.193.43.101:11434",
Model: "bge-m3:567m",
Timeout: 0,
})
vectorIds, _ := embedder.EmbedStrings(ctx, []string{"hello", "how are you"})

fmt.Println(vectorIds)
}

Embedder 维护了 LLM 的一个 HTTP Client:

func NewEmbedder(ctx context.Context, config *EmbeddingConfig) (*Embedder, error) {
// ...

var httpClient *http.Client
if config.HTTPClient != nil {
httpClient = config.HTTPClient
} else {
httpClient = &http.Client{Timeout: config.Timeout}
}

baseURL, err := url.Parse(config.BaseURL)
if err != nil {
return nil, fmt.Errorf("invalid base URL: %w", err)
}
cli := api.NewClient(baseURL, httpClient) // ollama 官方提供的 apiCient
return &Embedder{
cli: cli,
conf: config,
}, nil
}

embedder.EmbedStrings 通过 ollama apiClient 调用生成对应的 embedding 向量。

Document Transformer

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

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

"github.com/cloudwego/eino-ext/components/document/transformer/splitter/markdown"
"github.com/cloudwego/eino/schema"
)

func Run() {
ctx := context.Background()
transformer, _ := markdown.NewHeaderSplitter(ctx, &markdown.HeaderConfig{
Headers: map[string]string{
"##": "",
},
})

markdownDoc := &schema.Document{
Content: "## Title 1 \nHello World\n## Title 2\nWorld Hello",
}

transformedDocs, _ := transformer.Transform(ctx, []*schema.Document{markdownDoc})

for idx, doc := range transformedDocs {
fmt.Printf("doc segment %v: %v", idx, doc.Content)
}
}

Transformer 是一个接口,对输入的文档进行转换。其中,src 是待处理的文档列表,返回值是处理完成的文档列表。

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

具体看一下 HeaderSplitter 是怎么实现的 Transformer 接口,来对文档进行处理的。

func (h *headerSplitter) Transform(ctx context.Context, docs []*schema.Document, opts ...document.TransformerOption) ([]*schema.Document, error) {
    var ret []*schema.Document
    for _, doc := range docs {
        result := h.splitText(ctx, doc.Content)
        for i := range result {
            nDoc := &schema.Document{
                ID:       h.idGenerator(ctx, doc.ID, i),
                Content:  result[i].chunk,
                MetaData: deepCopyAnyMap(doc.MetaData),
            }
            if nDoc.MetaData == nil {
                nDoc.MetaData = make(map[string]any, len(result[i].meta))
            }
            for k, v := range result[i].meta {
                nDoc.MetaData[k] = v
            }
            ret = append(ret, nDoc)
        }
    }
    return ret, nil
}
func (h *headerSplitter) splitText(ctx context.Context, text string) []splitResult {
    var recordedMetaList []metaRecord
    recordedMetaMap := make(map[string]string)
    var currentLines []string
    var bInCodeBlock bool
    var openingFence string
    var ret []splitResult
    lines := strings.Split(text, "\n")
    for _, line := range lines {
        if len(line) == 0 {
            continue
        }
        line = strings.TrimSpace(line)
        // check is in code block
        // check if the line starts with headers
        bNewHeader := false
        for header, name := range h.headers {
            if strings.HasPrefix(line, header) && (len(line) == len(header) || line[len(header)] == ' ') {
                if len(currentLines) > 0 {
                    ret = append(ret, splitResult{
                        chunk: strings.Join(currentLines, "\n"),
                        meta:  deepCopyMap(recordedMetaMap),
                    })
                    currentLines = currentLines[:0]
                }

                if !h.trimHeaders {
                    currentLines = append(currentLines, line)
                }

                newLevel := len(header)
                for i := len(recordedMetaList) - 1; i >= 0; i-- {
                    if recordedMetaList[i].level >= newLevel {
                        delete(recordedMetaMap, recordedMetaList[i].name)
                        recordedMetaList = recordedMetaList[:i]
                    } else {
                        break
                    }
                }

                data := strings.TrimSpace(line[len(header):])
                recordedMetaList = append(recordedMetaList, metaRecord{
                    name:  name,
                    level: newLevel,
                    data:  data,
                })
                recordedMetaMap[name] = data

                bNewHeader = true
                break
            }
        }
        if !bNewHeader {
            currentLines = append(currentLines, line)
        }
    }
    ret = append(ret, splitResult{
        chunk: strings.Join(currentLines, "\n"),
        meta:  deepCopyMap(recordedMetaMap),
    })
    return ret
}