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)
}
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
:
|
Document
结构体是文档的标准格式:
- ID:文档的唯一标识符,用于在系统中唯一标识一个文档
- Content:文档的实际内容
- MetaData:文档的元数据,可以存储如下信息:
- 文档的来源信息
- 文档的向量表示(用于向量检索)
- 文档的分数(用于排序)
- 文档的子索引(用于分层检索)
- 其他自定义元数据
Embedding
Embedding 组件是一个用于将文本转换为向量表示的组件。它的主要作用是将文本内容映射到向量空间,使得语义相似的文本在向量空间中的距离较近。这个组件在以下场景中发挥重要作用:
- 文本相似度计算
- 语义搜索
- 文本聚类分析
|
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 是一个用于文档转换和处理的组件。它的主要作用是对输入的文档进行各种转换操作,如分割、过滤、合并等,从而得到满足特定需求的文档。这个组件可用于以下场景中:
- 将长文档分割成小段落以便于处理
- 根据特定规则过滤文档内容
- 对文档内容进行结构化转换
- 提取文档中的特定部分
|
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
}