Eino-Embedding 实战:向量化与语义检索应用

Embedding(嵌入)是 RAG(检索增强生成)系统的核心环节,负责将文本转换为稠密向量,使机器能够"理解"语义并完成相似度计算。本文介绍 Eino-Embedding 的使用方法及典型应用场景,代码链接


目录


1. Embedding 核心概念

Embedding(嵌入) 是将离散文本映射为连续稠密向量的技术。语义相近的文本在向量空间中距离更近,从而可以通过余弦相似度来度量语义相关性。

文本 → Embedding 模型 → 向量(e.g. 1024 维浮点数数组)

在 RAG 系统中,Embedding 通常用于:

  1. 离线建库:将知识库文档全部向量化,存入向量数据库
  2. 在线检索:将用户问题向量化,通过向量相似度找到最相关的 Top-K 文档
  3. 上下文增强:将检索到的文档作为上下文喂给 LLM,提升回答质量

2. 环境准备

安装依赖

go get github.com/cloudwego/eino-ext/components/embedding/ark

配置 API Key

Eino-Embedding 支持多种 Embedding 实现,本文的 Demo 使用字节跳动火山引擎的 ARK(Doubao Embedding)服务。

export ARK_API_KEY="your-ark-api-key"
export ARK_EMBEDDING_MODEL="doubao-embedding-large"  # 或其他支持的模型
export DEEPSEEK_API_KEY="your-deepseek-api-key"      # 用于 RAG 中的 LLM 生成

API Key 获取地址:火山引擎 ARK 控制台


3. 基础文本向量化

3.1 创建 Embedder

使用 eino-ext/components/embedding/ark 包创建 ARK Embedding 模型:

import "github.com/cloudwego/eino-ext/components/embedding/ark"

embedder, err := ark.NewEmbedder(ctx, &ark.EmbeddingConfig{
    APIKey: os.Getenv("ARK_API_KEY"),
    Model:  os.Getenv("ARK_EMBEDDING_MODEL"),
})
if err != nil {
    log.Fatalf("创建 Embedder 失败: %v", err)
}

3.2 批量向量化文本

texts := []string{
    "Go 是一门编程语言",
    "Python 是一门编程语言",
    "今天天气真好",
}

vectors, err := embedder.EmbedStrings(ctx, texts)
if err != nil {
    log.Fatalf("向量化失败: %v", err)
}

// 查看结果
for i, text := range texts {
    fmt.Printf("文本 %d: %s\n", i+1, text)
    fmt.Printf("  向量维度: %d\n", len(vectors[i]))
    fmt.Printf("  前5维: %v\n\n", vectors[i][:5])
}

3.3 计算余弦相似度

两个向量之间的语义相似度通常用余弦相似度衡量,值域为 [-1, 1],越接近 1 表示越相似。

func cosineSimilarity(a, b []float64) float64 {
    if len(a) != len(b) {
        return 0
    }

    var dotProduct, normA, normB float64
    for i := range a {
        dotProduct += a[i] * a[i]
        normA += a[i] * a[i]
        normB += b[i] * b[i]
    }

    if normA == 0 || normB == 0 {
        return 0
    }

    return dotProduct / (math.Sqrt(normA) * math.Sqrt(normB))
}

运行结果示例:

文本 1: Go 是一门编程语言
  向量维度: 1024
  前5维: [0.0231, -0.0542, 0.0918, -0.0123, 0.0765]

文本 2: Python 是一门编程语言
  向量维度: 1024
  前5维: [0.0198, -0.0481, 0.0889, -0.0097, 0.0712]

文本 3: 今天天气真好
  向量维度: 1024
  前5维: [-0.0312, 0.0421, 0.0018, 0.0876, -0.0153]

文本1 和 文本2 的相似度: 0.9234   // 高相似(都是编程语言)
文本1 和 文本3 的相似度: 0.1342   // 低相似(无关联)

可以看到,“Go 是一门编程语言” 与 “Python 是一门编程语言” 的相似度远高于与"今天天气真好"的相似度,证明 Embedding 确实捕获了语义信息。


4. 文档知识库向量化与 RAG

4.1 文档分割

在实际场景中,知识库中的文档往往很长,需要先进行分块(Chunk),将每块单独向量化:

// 简单固定窗口分割
chunkSize := 100
chunkOverlap := 20

var chunks []*schema.Document
textRunes := []rune(longText)

for i := 0; i < len(textRunes); i += chunkSize - chunkOverlap {
    end := i + chunkSize
    if end > len(textRunes) {
        end = len(textRunes)
    }

    chunk := &schema.Document{
        Content: string(textRunes[i:end]),
        MetaData: map[string]any{
            "source": "go_intro.txt",
            "chunk":  len(chunks) + 1,
        },
    }
    chunks = append(chunks, chunk)

    if end >= len(textRunes) {
        break
    }
}

实际生产中推荐使用 eino-ext 提供的 RecursiveSplitterHeaderSplitterSemanticSplitter 进行更智能的分割。

4.2 知识库向量化与检索

将知识库文档全部向量化后,通过计算查询向量与文档向量的相似度完成语义检索:

// 向量化所有知识库文档
var vectors [][]float64
for _, doc := range docs {
    vec, err := embedder.EmbedStrings(ctx, []string{doc.Content})
    if err != nil {
        log.Fatalf("向量化失败: %v", err)
    }
    vectors = append(vectors, vec[0])
}

// 向量化用户查询
query := "Go 语言的并发是如何实现的?"
queryVec, err := embedder.EmbedStrings(ctx, []string{query})

// 计算相似度并排序
type docScore struct {
    doc   *schema.Document
    score float64
}

var scores []docScore
for i, doc := range docs {
    similarity := cosineSimilarity(queryVec[0], vectors[i])
    scores = append(scores, docScore{doc: doc, score: similarity})
}

// 取 Top 2
sort.Slice(scores, func(i, j int) bool {
    return scores[i].score > scores[j].score
})

topDocs := make([]*schema.Document, 0, 2)
for i := 0; i < 2 && i < len(scores); i++ {
    topDocs = append(topDocs, scores[i].doc)
}

4.3 RAG:结合 LLM 生成回答

将检索到的相关文档作为上下文,连同用户问题一起发给 LLM,生成基于知识的回答:

import "github.com/cloudwego/eino-ext/components/model/deepseek"

// 创建 DeepSeek ChatModel
chatModel, err := deepseek.NewChatModel(ctx, &deepseek.ChatModelConfig{
    APIKey:  os.Getenv("DEEPSEEK_API_KEY"),
    Model:   "deepseek-chat",
    BaseURL: "https://api.deepseek.com",
})

// 构建上下文
contextStr := ""
for _, doc := range retrievedDocs {
    contextStr += doc.Content + "\n"
}

messages := []*schema.Message{
    schema.SystemMessage(fmt.Sprintf("你是一个专业的技术助手。请基于以下知识回答用户问题:\n\n%s", contextStr)),
    schema.UserMessage(query),
}

response, err := chatModel.Generate(ctx, messages)
if err != nil {
    log.Fatalf("生成失败: %v", err)
}

fmt.Println(response.Content)

5. 交互式文档问答系统

将以上能力组合成一个完整的 DocumentQA 系统,支持加载文档、向量建库和交互式问答:

5.1 系统架构

用户提问
    │
    ▼
┌─────────────────┐
│  向量化问题      │  ← EmbedStrings(query)
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│  语义相似度计算  │  ← cosineSimilarity(queryVec, docVec)
│  取 Top 3       │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│  构建上下文      │  ← 拼接 Top-K 文档内容
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│  LLM 生成回答   │  ← DeepSeek ChatModel
└─────────────────┘
         │
         ▼
    返回答案

5.2 DocumentQA 结构体

type DocumentQA struct {
    embedder  *ark.Embedder
    chatModel *deepseek.ChatModel
    documents []*schema.Document  // 知识库文档
    vectors   [][]float64         // 对应的向量
}

5.3 加载文档

func (qa *DocumentQA) LoadDocuments(ctx context.Context, docs []*schema.Document) error {
    fmt.Printf("正在加载 %d 个文档...\n", len(docs))

    // 提取文本内容
    texts := make([]string, len(docs))
    for i, doc := range docs {
        texts[i] = doc.Content
    }

    // 批量向量化
    vectors, err := qa.embedder.EmbedStrings(ctx, texts)
    if err != nil {
        return err
    }

    qa.documents = docs
    qa.vectors = vectors

    fmt.Printf("成功加载并向量化 %d 个文档\n", len(docs))
    return nil
}

5.4 查询处理

func (qa *DocumentQA) Query(ctx context.Context, question string) (string, error) {
    // 1. 向量化问题
    questionVectors, err := qa.embedder.EmbedStrings(ctx, []string{question})
    questionVector := questionVectors[0]

    // 2. 计算相似度
    type docScore struct {
        doc   *schema.Document
        score float64
    }

    scores := make([]docScore, len(qa.documents))
    for i := range qa.documents {
        similarity := cosineSimilarity(questionVector, qa.vectors[i])
        scores[i] = docScore{doc: qa.documents[i], score: similarity}
    }

    // 3. 排序,取 Top 3,相似度阈值 > 0.7
    sort.Slice(scores, func(i, j int) bool {
        return scores[i].score > scores[j].score
    })

    topDocs := make([]*schema.Document, 0, 3)
    for i := 0; i < 3 && i < len(scores); i++ {
        if scores[i].score > 0.7 {
            topDocs = append(topDocs, scores[i].doc)
        }
    }

    if len(topDocs) == 0 {
        return "抱歉,我在文档中找不到相关信息。", nil
    }

    // 4. 构建上下文,发给 LLM
    context := "相关文档内容:\n\n"
    for i, doc := range topDocs {
        context += fmt.Sprintf("%d. %s\n\n", i+1, doc.Content)
    }

    messages := []*schema.Message{
        schema.SystemMessage(fmt.Sprintf(`你是一个专业的文档问答助手。
请根据以下文档内容回答用户问题。如果文档中没有相关信息,请如实告知。

%s`, context)),
        schema.UserMessage(question),
    }

    response, err := qa.chatModel.Generate(ctx, messages)
    if err != nil {
        return "", err
    }

    return response.Content, nil
}

6. 完整代码:DocumentQA 系统

以下是 lab06/case/docs_question.go 的完整代码,实现了从初始化到交互式问答的完整流程:

package main

import (
    "bufio"
    "context"
    "fmt"
    "log"
    "math"
    "os"
    "sort"
    "strings"

    ark "github.com/cloudwego/eino-ext/components/embedding/ark"
    deepseek "github.com/cloudwego/eino-ext/components/model/deepseek"
    "github.com/cloudwego/eino/schema"
)

type DocumentQA struct {
    embedder  *ark.Embedder
    chatModel *deepseek.ChatModel
    documents []*schema.Document
    vectors   [][]float64
}

func NewDocumentQA(arkAPIKey, arkModel, deepseekAPIKey string) (*DocumentQA, error) {
    ctx := context.Background()

    embedder, err := ark.NewEmbedder(ctx, &ark.EmbeddingConfig{
        APIKey: arkAPIKey,
        Model:  arkModel,
    })
    if err != nil {
        return nil, err
    }

    chatModel, err := deepseek.NewChatModel(ctx, &deepseek.ChatModelConfig{
        APIKey:  deepseekAPIKey,
        Model:   "deepseek-chat",
        BaseURL: "https://api.deepseek.com",
    })
    if err != nil {
        return nil, err
    }

    return &DocumentQA{embedder: embedder, chatModel: chatModel}, nil
}

func (qa *DocumentQA) LoadDocuments(ctx context.Context, docs []*schema.Document) error {
    texts := make([]string, len(docs))
    for i, doc := range docs {
        texts[i] = doc.Content
    }

    vectors, err := qa.embedder.EmbedStrings(ctx, texts)
    if err != nil {
        return err
    }

    qa.documents = docs
    qa.vectors = vectors
    fmt.Printf("成功加载并向量化 %d 个文档\n", len(docs))
    return nil
}

func (qa *DocumentQA) Query(ctx context.Context, question string) (string, error) {
    questionVectors, err := qa.embedder.EmbedStrings(ctx, []string{question})
    if err != nil {
        return "", err
    }
    questionVector := questionVectors[0]

    type docScore struct {
        doc   *schema.Document
        score float64
    }

    scores := make([]docScore, len(qa.documents))
    for i := range qa.documents {
        scores[i] = docScore{
            doc:   qa.documents[i],
            score: cosineSimilarity(questionVector, qa.vectors[i]),
        }
    }

    sort.Slice(scores, func(i, j int) bool {
        return scores[i].score > scores[j].score
    })

    topDocs := make([]*schema.Document, 0, 3)
    for i := 0; i < 3 && i < len(scores); i++ {
        if scores[i].score > 0.7 {
            topDocs = append(topDocs, scores[i].doc)
        }
    }

    if len(topDocs) == 0 {
        return "抱歉,我在文档中找不到相关信息。", nil
    }

    context := "相关文档内容:\n\n"
    for i, doc := range topDocs {
        context += fmt.Sprintf("%d. %s\n\n", i+1, doc.Content)
    }

    messages := []*schema.Message{
        schema.SystemMessage(fmt.Sprintf(`你是一个专业的文档问答助手。
请根据以下文档内容回答用户问题。如果文档中没有相关信息,请如实告知。

%s`, context)),
        schema.UserMessage(question),
    }

    response, err := qa.chatModel.Generate(ctx, messages)
    if err != nil {
        return "", err
    }

    return response.Content, nil
}

func main() {
    qa, err := NewDocumentQA(
        os.Getenv("ARK_API_KEY"),
        os.Getenv("ARK_EMBEDDING_MODEL"),
        os.Getenv("DEEPSEEK_API_KEY"),
    )
    if err != nil {
        log.Fatalf("创建问答系统失败: %v", err)
    }

    docs := []*schema.Document{
        {Content: "Eino 是基于 Go 语言的 AI 应用开发框架,由字节跳动开源。"},
        {Content: "Eino 提供了 ChatModel、Embedding、Retriever 等丰富的组件。"},
        {Content: "Eino 支持 Chain 和 Graph 两种编排方式,可以灵活组合组件。"},
        {Content: "React Agent 是 Eino 中的智能代理,能够自主调用工具完成任务。"},
        {Content: "Eino 支持多种大模型,包括 OpenAI、ARK、Ollama 等。"},
    }

    ctx := context.Background()
    if err := qa.LoadDocuments(ctx, docs); err != nil {
        log.Fatalf("加载文档失败: %v", err)
    }

    // 交互式问答
    scanner := bufio.NewScanner(os.Stdin)
    fmt.Println("\n=== 文档问答系统(输入 'exit' 退出)===")

    for {
        fmt.Print("问题: ")
        if !scanner.Scan() {
            break
        }

        question := strings.TrimSpace(scanner.Text())
        if question == "exit" {
            fmt.Println("再见!")
            break
        }
        if question == "" {
            continue
        }

        answer, err := qa.Query(ctx, question)
        if err != nil {
            fmt.Printf("查询失败: %v\n", err)
            continue
        }

        fmt.Printf("\n回答: %s\n\n", answer)
    }
}

func cosineSimilarity(a, b []float64) float64 {
    if len(a) != len(b) {
        return 0
    }

    var dotProduct, normA, normB float64
    for i := range a {
        dotProduct += a[i] * b[i]
        normA += a[i] * a[i]
        normB += b[i] * b[i]
    }

    if normA == 0 || normB == 0 {
        return 0
    }

    return dotProduct / (math.Sqrt(normA) * math.Sqrt(normB))
}

运行效果:

=== 文档问答系统(输入 'exit' 退出)===
问题: Eino 支持哪些大模型?

回答: Eino 支持多种大模型,包括 OpenAI、ARK、Ollama 等。

问题: exit
再见!

7. 总结

本文基于四个渐进式 Demo 介绍了 Eino-Embedding 的核心用法:

Demo核心能力说明
text_embedding.go基础向量化单文本/批量文本转向量
document/document_split.go文档分割 + 建库长文档分块,向量入库,Top-K 语义检索,RAG
memory/memory_embedding.go余弦相似度验证语义相近文本相似度高,无关文本相似度低
case/docs_question.go完整 RAG 问答系统交互式 DocumentQA,支持阈值过滤

核心流程总结:

文档 → 分割(Chunk) → Embedding → 向量存储
                                    ↑
用户问题 → Embedding → 相似度计算 → Top-K 检索 → 上下文构建 → LLM 生成回答

在实际生产环境中,可将向量存储替换为 Milvus、Pinecone、Chroma 等向量数据库,以支持海量文档的快速检索。Eino 生态中的 Retriever 组件也提供了与这些向量数据库的标准对接接口,可以进一步简化开发。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐