Eino-Embedding 实战:向量化与语义检索应用
Eino-Embedding 实战:向量化与语义检索应用
Embedding(嵌入)是 RAG(检索增强生成)系统的核心环节,负责将文本转换为稠密向量,使机器能够"理解"语义并完成相似度计算。本文介绍 Eino-Embedding 的使用方法及典型应用场景,代码链接。
目录
1. Embedding 核心概念
Embedding(嵌入) 是将离散文本映射为连续稠密向量的技术。语义相近的文本在向量空间中距离更近,从而可以通过余弦相似度来度量语义相关性。
文本 → Embedding 模型 → 向量(e.g. 1024 维浮点数数组)
在 RAG 系统中,Embedding 通常用于:
- 离线建库:将知识库文档全部向量化,存入向量数据库
- 在线检索:将用户问题向量化,通过向量相似度找到最相关的 Top-K 文档
- 上下文增强:将检索到的文档作为上下文喂给 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提供的RecursiveSplitter、HeaderSplitter或SemanticSplitter进行更智能的分割。
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 组件也提供了与这些向量数据库的标准对接接口,可以进一步简化开发。
更多推荐


所有评论(0)