知识库
大模型拥有海量的通用知识,但它的知识只停留在训练数据截止的那一刻,它也不了解新鲜发生的事情,它也不了解任何私有数据,比如公司内部文件,项目文档等等。
如果问到大模型知识盲区时,它大概率会一本正经的胡说八道,这种就是AI幻觉。
为了解决这个问题,诞生了一个技术方案,给大模型提供一个知识库,在进行回答问题时,它不再仅仅依赖自己“脑中”的记忆,而是会先去知识库里查找最相关的资料,然后结合这些资料和自身的推理能力,给出一个精准、可靠的答复。
这个技术有一个标准名称就是RAG(Retrieval-Augmented Generation)
,即“检索增强生成”。
RAG主要有两个阶段:
- 知识准备阶段
- 知识检索增强阶段
1. 知识准备阶段
此阶段主要是收集知识(数据源/语料库),然后将数据进行向量化(Embedding),并存储到向量数据库中。
如果数据比较大,比如一篇文档50页,我们需要将文档切分成有意义的小段落或者小区块,这个过程叫做分块。
这里可能有人会有一个疑问?为什么要分块?
- 知识库的目的是根据用户的问题,检索出相关的数据,然后一起提交给大模型
- 如果不进行分块,提交给大模型的就是一整个大的文档,相当于提交给大模型大量无关的内容,造成大模型困惑,无法准确的回答用户的问题
- 大模型是有上下文窗口限制的,就是一次性大模型可以处理的文本总量,超过这个量,大模型就无法处理了
- 分块可以确保检索到的知识精确,内容长度又不会太长(如果使用的付费模型,每一个token都代表了一定的费用,如何确保经济性,也是我们考虑的重点)
eino框架在这个阶段,提供了一些组件来实现:
1.1 Document Loader
文档加载组件,它的主要作用是从不同来源(如网络 URL、本地文件等)加载文档内容,并将其转换为标准的文档格式。
type Loader interface {
//从指定的数据源加载文档
Load(ctx context.Context, src Source, opts ...LoaderOption) ([]*schema.Document, error)
}
2
3
4
Source:定义了文档的来源,有一个URI字段,文档的统一资源标识符,可以填网络URL或者本地文件路径
type Source struct { //文档来源 网络URL或者本地文件路径 URI string }
1
2
3
4Document:文档的标准格式
type Document struct { // ID 是文档的唯一标识符 ID string // Content 是文档的内容 Content string // MetaData 用于存储文档的元数据信息 MetaData map[string]any }
1
2
3
4
5
6
7
8LoaderOption:加载的选项,每一个loader都有自己的配置
1.1.1 fileLoader
fileLoader用于读取本地文件,读取文件时,会根据文件的后缀自动选择解析器,默认是TextParser(文本解析器),目前支持的有pdf解析器,docx解析器,html解析器,xlsx解析器。
go get github.com/cloudwego/eino-ext/components/document/loader/file
func main() {
ctx := context.Background()
loader, err := file.NewFileLoader(ctx, &file.FileLoaderConfig{
UseNameAsID: true,
})
if err != nil {
panic(err)
}
filePath := "./loader/test.md"
docs, err := loader.Load(ctx, document.Source{
URI: filePath,
})
if err != nil {
panic(err)
}
for _, doc := range docs {
println(doc.String())
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
如果想要读取word文档,需要配置docx解析器。
go get github.com/cloudwego/eino-ext/components/document/parser/docx
func main() {
ctx := context.Background()
docxParser, err := docx.NewDocxParser(ctx, &docx.Config{})
if err != nil {
panic(err)
}
extParser, err := parser.NewExtParser(ctx, &parser.ExtParserConfig{
FallbackParser: parser.TextParser{},
Parsers: map[string]parser.Parser{
".docx": docxParser,
},
})
if err != nil {
panic(err)
}
loader, err := file.NewFileLoader(ctx, &file.FileLoaderConfig{
UseNameAsID: true,
Parser: extParser,
})
if err != nil {
panic(err)
}
filePath := "./loader/test.docx"
docs, err := loader.Load(ctx, document.Source{
URI: filePath,
})
if err != nil {
panic(err)
}
for _, doc := range docs {
println(doc.String())
for k, v := range doc.MetaData {
fmt.Printf("%v:%v \n", k, v)
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
1.1.2 urlloader
url加载器,用于从网络URL中加载文档内容,默认使用的是htmlParser。
go get github.com/cloudwego/eino-ext/components/document/loader/file
func main() {
ctx := context.Background()
loader, err := url.NewLoader(ctx, &url.LoaderConfig{})
if err != nil {
panic(err)
}
docs, err := loader.Load(ctx, document.Source{
URI: "https://www.cloudwego.io/zh/docs/eino/core_modules/components/document_loader_guide",
})
if err != nil {
panic(err)
}
for _, doc := range docs {
println(doc.String())
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1.1.3 说明
- Loader可以自定义,实现Loader接口即可
- Parser可以自定义,实现Parser接口即可
https://github.com/cloudwego/eino-ext/tree/main/components/document/parser
- 这个链接中有eino官方实现的几个解析器,包括docx,html,pdf,xlsx
1.2 向量化(Embedding)
文档加载后,我们需要将其进行向量化,Embedding组件就是用来将文本转换为向量表示。
type Embedder interface {
//将一组文本转换为向量表示
EmbedStrings(ctx context.Context, texts []string, opts ...Option) ([][]float64, error)
}
2
3
4
在eino-ext(https://github.com/cloudwego/eino-ext
)项目中,帮我们实现了一些:
- ark:火山引擎实现
- catch:缓存向量,避免重复计算,目前支持redis
- dashscope:阿里百炼实现
- gemini:gemini实现
- ollama:ollama实现
- openai:openai实现
- qianfan:百度千帆实现
- tencentcloud:腾讯混元模型实现
1.2.1 ollama实现
go get github.com/cloudwego/eino-ext/components/embedding/ollama
func main() {
ctx := context.Background()
embedder, err := ollama.NewEmbedder(ctx, &ollama.EmbeddingConfig{
Model: "modelscope.cn/nomic-ai/nomic-embed-text-v1.5-GGUF:latest",
BaseURL: "http://127.0.0.1:11434",
})
if err != nil {
panic(err)
}
embeddings, err := embedder.EmbedStrings(ctx, []string{"hello world"})
if err != nil {
panic(err)
}
fmt.Printf("%v \n", embeddings)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1.2.2 阿里百炼实现
go get github.com/cloudwego/eino-ext/components/embedding/dashscope
func main() {
ctx := context.Background()
embedder, err := dashscope.NewEmbedder(ctx, &dashscope.EmbeddingConfig{
APIKey: "自己的key",
Model: "text-embedding-v4",
})
if err != nil {
panic(err)
}
embeddings, err := embedder.EmbedStrings(ctx, []string{"hello world"})
if err != nil {
panic(err)
}
fmt.Printf("%v \n", embeddings)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1.3 文档处理(Transformer )
Transformer 是一个用于文档转换和处理的组件。它的主要作用是对输入的文档进行各种转换操作,如分割、过滤、合并等,从而得到满足特定需求的文档。
type Transformer interface {
Transform(ctx context.Context, src []*schema.Document, opts ...TransformerOption) ([]*schema.Document, error)
}
2
3
在https://github.com/cloudwego/eino-ext/tree/main/components/document/transformer/splitter
中提供了四种实现:
- recursive:将长文档按照指定大小递归的进行切分
- markdown:根据md文档的标题层级(#号)结构进行分割
- semantic:基于语义相似度将长文档切分成更小的片段
- html:根据html文件中的标题层级(h1-h6)进行分割
1.3.1 html
go get github.com/cloudwego/eino-ext/components/document/transformer/splitter/html
var commonSuccessHTML = `<!DOCTYPE html>
<html>
<body>
<div>
<h1>H1</h1>
<p>H1 content1</p>
<div>
<h2>H2.1</h2>
<p>H2.1 content</p>
<h3>H3.1</h3>
<p>H3.1 content</p>
<h3>H3.2</h3>
<p>H3.2 content</p>
<h2>H2.2</h2>
<p>H2.2 content</p>
</div>
<div>
<h2>H2.3</h2>
<p>H2.3 content</p>
</div>
<div>
<p>H1 content2</p>
</div>
<br>
<p>H1 content3</p>
</div>
<div>
<h2>H2.4</h2>
<p>H2.4 content</p>
</div>
<div>
<p>content</p>
</div>
</body>
</html>`
func main() {
ctx := context.Background()
splitter, err := html.NewHeaderSplitter(ctx, &html.HeaderConfig{
Headers: map[string]string{
"h1": "h1",
"h2": "h2",
"h3": "h3",
"h4": "h4",
"h5": "h5",
"h6": "h6",
},
})
if err != nil {
panic(err)
}
docs, err := splitter.Transform(ctx, []*schema.Document{
{
ID: "1",
Content: commonSuccessHTML,
},
})
if err != nil {
panic(err)
}
for _, doc := range docs {
println(doc.String())
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
1.3.2 markdown
go get github.com/cloudwego/eino-ext/components/document/transformer/splitter/markdown
var mdContent = `
# 这是一级标题
这是一级标题内容
## 这是二级标题
这是二级标题内容
### 这是三级标题
这是三级标题内容
`
func main() {
ctx := context.Background()
splitter, err := markdown.NewHeaderSplitter(ctx, &markdown.HeaderConfig{
Headers: map[string]string{
"#": "h1",
"##": "h2",
"###": "h3",
"####": "h4",
"#####": "h5",
"######": "h6",
},
TrimHeaders: true, //是否在输出中保留标题行 true 不保留 false 保留
})
if err != nil {
panic(err)
}
docs, err := splitter.Transform(ctx, []*schema.Document{
{
ID: "1",
Content: mdContent,
},
})
if err != nil {
panic(err)
}
for _, doc := range docs {
println(doc.String())
println("=========================")
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
1.3.3 recursive
go get github.com/cloudwego/eino-ext/components/document/transformer/splitter/recursive
配置参数说明:
ChunkSize
:必需参数,指定目标片段的大小OverlapSize
:片段之间的重叠大小,用于保持上下文连贯性Separators
:分隔符列表,按优先级顺序使用LenFunc
:自定义文本长度计算函数,默认使用len()
KeepType
:分隔符保留策略,可选值:KeepTypeNone
:不保留分隔符KeepTypeStart
:在片段开始处保留分隔符KeepTypeEnd
:在片段结尾处保留分隔符
func main() {
ctx := context.Background()
splitter, err := recursive.NewSplitter(ctx, &recursive.Config{
ChunkSize: 10, // 必需:目标片段大小
OverlapSize: 2, // 可选:片段重叠大小
Separators: []string{"\n", ".", "?", "!", "!"}, // 可选:分隔符列表
LenFunc: nil, // 可选:自定义长度计算函数
KeepType: recursive.KeepTypeNone, // 可选:分隔符保留策略
})
if err != nil {
panic(err)
}
docs, err := splitter.Transform(ctx, []*schema.Document{
{
ID: "1",
Content: `
这是第一个段落,包含了一些内容。
这是第二个段落。这个段落有多个句子!这些句子通过标点符号分隔。
这是第三个段落。这里有更多的内容。`,
},
})
if err != nil {
panic(err)
}
for _, doc := range docs {
println(doc.String())
println("=========================")
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
1.3.4 semantic
go get github.com/cloudwego/eino-ext/components/document/transformer/splitter/semantic
配置参数说明:
Embedding
:必需参数,用于生成文本向量的嵌入器实例BufferSize
:上下文缓冲区大小,用于在计算语义相似度时包含更多上下文信息MinChunkSize
:最小片段大小,小于此大小的片段会被合并Separators
:用于初始分割的分隔符列表,按顺序使用Percentile
:分割阈值的百分位数,范围 0-1,越大分割越少LenFunc
:自定义文本长度计算函数,默认使用len()
func main() {
ctx := context.Background()
embedder, err := ollama.NewEmbedder(ctx, &ollama.EmbeddingConfig{
Model: "modelscope.cn/nomic-ai/nomic-embed-text-v1.5-GGUF:latest",
BaseURL: "http://127.0.0.1:11434",
})
if err != nil {
panic(err)
}
splitter, err := semantic.NewSplitter(ctx, &semantic.Config{
Embedding: embedder, // 必需:用于生成文本向量的嵌入器
BufferSize: 2, // 可选:上下文缓冲区大小
MinChunkSize: 100, // 可选:最小片段大小
Separators: []string{"\n", ".", "?", "!"}, // 可选:分隔符列表
Percentile: 0.9, // 可选:分割阈值百分位数
LenFunc: nil, // 可选:自定义长度计算函数
})
if err != nil {
panic(err)
}
docs, err := splitter.Transform(ctx, []*schema.Document{
{
ID: "1",
Content: `这是第一段内容,包含了一些重要信息。
这是第二段内容,与第一段语义相关。
这是第三段内容,主题已经改变。
这是第四段内容,继续讨论新主题。`,
},
})
if err != nil {
panic(err)
}
for _, doc := range docs {
println(doc.String())
println("=========================")
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
1.4 存储索引(Indexer)
向量化后的数据需要存储到向量数据库中,常用的向量数据库有:
- Chroma
- Elasticsearch
- Faiss
- Weaviate
- Milvus
- pgvector
- redis
- 等等
Indexer 组件是一个用于存储和索引文档的组件。
type Indexer interface {
//存储文档并建立索引
Store(ctx context.Context, docs []*schema.Document, opts ...Option) (ids []string, err error)
}
2
3
4
在eino-ext
中,提供了4中存储库的支持:
- es8
- milvus
- redis
- volc_vikingdb:VikingDB 向量数据库是火山引擎提供的一款大规模云原生向量数据库
1.4.1 Redis
在 Redis 中进行向量搜索,需要使用 Redis Stack,因为它包含了 RediSearch 模块,这个模块提供了强大的向量相似性搜索(VSS)功能
docker部署新的redis实例:
docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latest
go get github.com/redis/go-redis/v9
go get github.com/cloudwego/eino-ext/components/indexer/redis
2
这里我们使用Redis做为示例,其他使用方式一致。
package main
import (
"context"
"fmt"
"github.com/cloudwego/eino-ext/components/document/transformer/splitter/recursive"
"github.com/cloudwego/eino-ext/components/embedding/ollama"
ri "github.com/cloudwego/eino-ext/components/indexer/redis"
"github.com/cloudwego/eino/schema"
"github.com/redis/go-redis/v9"
)
func main() {
ctx := context.Background()
client := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
UnstableResp3: true,
Protocol: 2,
})
keyPrefix := "eino_doc:"
indexName := "doc_index"
slice, err2 := client.Do(ctx, "FT._LIST").StringSlice()
if err2 != nil {
panic(err2)
}
indexExists := false
for _, v := range slice {
if v == indexName {
indexExists = true
break
}
}
//if indexExists {
// result, err2 := client.FTDropIndex(ctx, indexName).Result()
// if err2 != nil {
// panic(err2)
// }
// fmt.Println(result)
//}
if !indexExists {
//创建索引
result, err := client.FTCreate(ctx, indexName, &redis.FTCreateOptions{
OnHash: true,
Prefix: []any{keyPrefix},
}, &redis.FieldSchema{
FieldName: "content",
FieldType: redis.SearchFieldTypeText,
Weight: 1,
}, &redis.FieldSchema{
FieldName: "vector_content",
FieldType: redis.SearchFieldTypeVector,
VectorArgs: &redis.FTVectorArgs{
FlatOptions: &redis.FTFlatOptions{
Type: "FLOAT64", // BFLOAT16 / FLOAT16 / FLOAT32 / FLOAT64. BFLOAT16 and FLOAT16 require v2.10 or later.
Dim: 384, // keeps same with dimensions of Embedding
DistanceMetric: "L2", // L2 / IP / COSINE
},
},
}).Result()
if err != nil {
panic(err)
}
fmt.Println(result)
}
embedder, err := ollama.NewEmbedder(ctx, &ollama.EmbeddingConfig{
Model: "modelscope.cn/nomic-ai/nomic-embed-text-v1.5-GGUF:latest",
BaseURL: "http://127.0.0.1:11434",
})
if err != nil {
panic(err)
}
splitter, err := recursive.NewSplitter(ctx, &recursive.Config{
ChunkSize: 10, // 必需:目标片段大小
OverlapSize: 2, // 可选:片段重叠大小
Separators: []string{"\n", ".", "?", "!"}, // 可选:分隔符列表
LenFunc: nil, // 可选:自定义长度计算函数
KeepType: recursive.KeepTypeNone, // 可选:分隔符保留策略
IDGenerator: func(ctx context.Context, originalID string, splitIndex int) string {
return fmt.Sprintf("%s_%d", originalID, splitIndex)
},
})
if err != nil {
panic(err)
}
docs, err := splitter.Transform(ctx, []*schema.Document{
{
ID: "testDoc",
Content: `
That is a very happy person。
That is a happy dog。
Today is a sunny day。`,
},
})
if err != nil {
panic(err)
}
indexer, err := ri.NewIndexer(ctx, &ri.IndexerConfig{
Client: client,
KeyPrefix: keyPrefix,
BatchSize: len(docs),
Embedding: embedder,
})
if err != nil {
panic(err)
}
ids, err := indexer.Store(ctx, docs)
if err != nil {
panic(err)
}
fmt.Printf("%v", ids)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
1.4.2 redis向量查询
package main
import (
"context"
"encoding/binary"
"fmt"
"math"
"github.com/cloudwego/eino-ext/components/embedding/ollama"
"github.com/redis/go-redis/v9"
)
func main() {
ctx := context.Background()
// --- 1. 连接到 Redis ---
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379", // Redis Stack 服务的地址
UnstableResp3: true,
Protocol: 2,
})
indexName := "doc_index"
// 构建 KNN 查询
// `*=>[KNN 2 @vector_content $blob]` 的含义:
// - `*`: 匹配所有文档 (我们不过滤元数据)。
// - `=>`: 表示这是一个混合查询,我们主要关心右边的向量部分。
// - `[KNN 2 @vector_content $blob]`: 在 `vector_content` 字段上执行一个 K-最近邻查询,
// 查找 2 个最近邻。`$blob` 是一个参数,我们将把查询向量的二进制数据传递给它。
// DIALECT 2 是必须的,用于支持这种现代的查询语法。
k := 2
query := fmt.Sprintf("*=>[KNN %d @vector_content $blob AS score]", k)
searchContent := "That is a happy person"
embedder, err := ollama.NewEmbedder(ctx, &ollama.EmbeddingConfig{
Model: "modelscope.cn/nomic-ai/nomic-embed-text-v1.5-GGUF:latest",
BaseURL: "http://127.0.0.1:11434",
})
if err != nil {
panic(err)
}
embeddings, err := embedder.EmbedStrings(ctx, []string{searchContent})
if err != nil {
panic(err)
}
// 使用 redis.NewSearch 来构建带参数的查询
searchResult, err := rdb.FTSearchWithArgs(ctx, indexName, query, &redis.FTSearchOptions{
Params: map[string]interface{}{
"blob": vector2Bytes(embeddings[0]),
},
DialectVersion: 2,
Return: []redis.FTSearchReturn{
{
FieldName: "content",
},
{
FieldName: "score",
},
},
}).Result()
if err != nil {
panic(err)
}
for _, v := range searchResult.Docs {
fmt.Printf("%v:%v \n", v.Fields["content"], v.Fields["score"])
}
}
func vector2Bytes(vector []float64) []byte {
float32Arr := make([]float32, len(vector))
for i, v := range vector {
float32Arr[i] = float32(v)
}
bytes := make([]byte, len(float32Arr)*4)
for i, v := range float32Arr {
binary.LittleEndian.PutUint32(bytes[i*4:], math.Float32bits(v))
}
return bytes
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
2. 知识检索增强阶段
这个阶段有以下步骤:
- 用户提出问题
- 进行查询处理,将用户的问题转换为向量
- 从知识库中进行检索,查出最相关的知识片段
- 增强提示词,将知识库片段和原有的提示词合并,提交给大模型
- 大模型给出最终回答
eino框架在这个阶段,提供了一个组件Retriever
。
2.1 Retriever 组件
Retriever 组件是一个用于从各种数据源检索文档的组件。它的主要作用是根据用户的查询(query)从文档库中检索出最相关的文档。
type Retriever interface {
//根据查询检索相关文档
Retrieve(ctx context.Context, query string, opts ...Option) ([]*schema.Document, error)
}
2
3
4
在eino-ext中,提供了以下几种的检索实现:
- dify
- es8
- milvus
- redis
- volc_knowledge
- volc_vikingdb
这里我们演示使用Redis的方式。
2.1.1 Redis
在前面,我们已经使用过redis做向量查询,但没有使用Retriever 组件,这次我们使用Retriever 组件来进行实现。
首先先添加依赖:
github.com/cloudwego/eino-ext/components/retriever/redis
package main
import (
"context"
"fmt"
"github.com/cloudwego/eino-ext/components/embedding/ollama"
rr "github.com/cloudwego/eino-ext/components/retriever/redis"
"github.com/redis/go-redis/v9"
)
func main() {
ctx := context.Background()
client := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Protocol: 2,
UnstableResp3: true,
})
embedder, err := ollama.NewEmbedder(ctx, &ollama.EmbeddingConfig{
Model: "modelscope.cn/nomic-ai/nomic-embed-text-v1.5-GGUF:latest",
BaseURL: "http://127.0.0.1:11434",
})
if err != nil {
panic(err)
}
r, err := rr.NewRetriever(ctx, &rr.RetrieverConfig{
Client: client,
Index: "doc_index",
Embedding: embedder,
})
if err != nil {
panic(err)
}
docs, err := r.Retrieve(ctx, "dog")
if err != nil {
panic(err)
}
for _, v := range docs {
fmt.Printf("ID:%s, CONTENT:%v \n", v.ID, v.Content)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
3. 完整实例
3.1 创建知识库源文件
创建一个名为 knowledge_base.txt 的文件,并填入以下内容。这将是我们的“私有知识”。
Eino 是一个为 Go 语言设计的 AI 应用开发框架。
它的核心目标是简化和标准化 AI 应用的开发流程,特别是那些涉及大型语言模型(LLM)的应用。
Eino 提供了模块化的组件,包括模型调用、文档加载、文本分割、向量嵌入和知识检索等。
该框架由 CloudWeGo 团队开发,旨在提高 Go 开发者构建复杂 AI 应用的效率。
Eino 的吉祥物是一只聪明可爱的海豚。
2
3
4
5
3.2 知识库准备
func createRedisIndexIfNotExists(ctx context.Context, rdb *redis.Client) error {
indices, err := rdb.Do(ctx, "FT._LIST").StringSlice()
if err != nil {
return err
}
for _, v := range indices {
if v == redisIndexName {
return nil
}
}
_, err = rdb.FTCreate(ctx, redisIndexName, &redis.FTCreateOptions{
OnHash: true,
Prefix: []any{redisKeyPrefix},
}, &redis.FieldSchema{FieldName: "content", FieldType: redis.SearchFieldTypeText},
&redis.FieldSchema{
FieldName: "vector_content",
FieldType: redis.SearchFieldTypeVector,
VectorArgs: &redis.FTVectorArgs{
FlatOptions: &redis.FTFlatOptions{
Type: "FLOAT64",
Dim: 384,
DistanceMetric: "L2",
},
},
},
).Result()
if err != nil {
return err
}
return nil
}
func prepareKnowledge(ctx context.Context, rdb *redis.Client) error {
//1. 检查索引是否存在,不存在则创建
if err := createRedisIndexIfNotExists(ctx, rdb); err != nil {
return err
}
loader, err := file.NewFileLoader(ctx, &file.FileLoaderConfig{
UseNameAsID: true,
})
if err != nil {
return err
}
docs, err := loader.Load(ctx, document.Source{
URI: knowledgeFilePath,
})
if err != nil {
return err
}
splitter, err := recursive.NewSplitter(ctx, &recursive.Config{
ChunkSize: 200, // 每个块的目标大小
OverlapSize: 20, // 块之间的重叠大小,以保持上下文连续性
})
if err != nil {
return err
}
splitDocs, err := splitter.Transform(ctx, docs)
if err != nil {
return err
}
embedder, err := ollama.NewEmbedder(ctx, &ollama.EmbeddingConfig{
Model: embeddingModelName,
BaseURL: ollamaBaseURL,
})
if err != nil {
return err
}
indexer, err := ri.NewIndexer(ctx, &ri.IndexerConfig{
Client: rdb,
KeyPrefix: redisKeyPrefix,
Embedding: embedder,
})
if err != nil {
return err
}
ids, err := indexer.Store(ctx, splitDocs)
if err != nil {
return err
}
fmt.Printf("%v", ids)
return nil
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
3.3 检索增强
package main
import (
"context"
"fmt"
"log"
"strings"
"github.com/cloudwego/eino-ext/components/document/loader/file"
"github.com/cloudwego/eino-ext/components/document/transformer/splitter/recursive"
"github.com/cloudwego/eino-ext/components/embedding/ollama"
ri "github.com/cloudwego/eino-ext/components/indexer/redis"
ollama2 "github.com/cloudwego/eino-ext/components/model/ollama"
rr "github.com/cloudwego/eino-ext/components/retriever/redis"
"github.com/cloudwego/eino/components/document"
"github.com/cloudwego/eino/components/prompt"
"github.com/cloudwego/eino/schema"
"github.com/redis/go-redis/v9"
)
const (
redisIndexName = "test_index"
redisKeyPrefix = "test_doc:"
knowledgeFilePath = "./test.txt"
embeddingModelName = "modelscope.cn/nomic-ai/nomic-embed-text-v1.5-GGUF:latest"
ollamaBaseURL = "http://localhost:11434"
chatModelName = "modelscope.cn/Qwen/Qwen3-32B-GGUF:latest"
)
func prepareKnowledge(ctx context.Context, rdb *redis.Client) error {
//1. 检查索引是否存在,不存在则创建
if err := createRedisIndexIfNotExists(ctx, rdb); err != nil {
return err
}
loader, err := file.NewFileLoader(ctx, &file.FileLoaderConfig{
UseNameAsID: true,
})
if err != nil {
return err
}
docs, err := loader.Load(ctx, document.Source{
URI: knowledgeFilePath,
})
if err != nil {
return err
}
splitter, err := recursive.NewSplitter(ctx, &recursive.Config{
ChunkSize: 200, // 每个块的目标大小
OverlapSize: 20, // 块之间的重叠大小,以保持上下文连续性
})
if err != nil {
return err
}
splitDocs, err := splitter.Transform(ctx, docs)
if err != nil {
return err
}
embedder, err := ollama.NewEmbedder(ctx, &ollama.EmbeddingConfig{
Model: embeddingModelName,
BaseURL: ollamaBaseURL,
})
if err != nil {
return err
}
indexer, err := ri.NewIndexer(ctx, &ri.IndexerConfig{
Client: rdb,
KeyPrefix: redisKeyPrefix,
Embedding: embedder,
})
if err != nil {
return err
}
ids, err := indexer.Store(ctx, splitDocs)
if err != nil {
return err
}
fmt.Printf("%v", ids)
return nil
}
func main() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Protocol: 2,
UnstableResp3: true,
})
if err := prepareKnowledge(ctx, rdb); err != nil {
panic(err)
}
//阶段2
userQuestion := "Eino 框架是什么?它的吉祥物是什么?"
if err := answerQuestion(ctx, rdb, userQuestion); err != nil {
panic(err)
}
}
func answerQuestion(ctx context.Context, rdb *redis.Client, question string) error {
embedder, err := ollama.NewEmbedder(ctx, &ollama.EmbeddingConfig{
Model: embeddingModelName,
BaseURL: ollamaBaseURL,
})
if err != nil {
return err
}
retriever, err := rr.NewRetriever(ctx, &rr.RetrieverConfig{
Client: rdb,
Index: redisIndexName,
Embedding: embedder,
TopK: 3,
})
if err != nil {
return err
}
retrievedDocs, err := retriever.Retrieve(ctx, question)
if err != nil {
return err
}
if len(retrievedDocs) == 0 {
log.Println("未能从知识库中找到相关信息。")
return nil
}
var contextBuilder strings.Builder
for i, doc := range retrievedDocs {
log.Printf(" - 相关片段 %d: %s\n", i+1, strings.ReplaceAll(doc.Content, "\n", " "))
contextBuilder.WriteString(doc.Content)
contextBuilder.WriteString("\n\n")
}
ragTemplate := prompt.FromMessages(
schema.FString,
schema.SystemMessage(`
你是一个智能助手。请根据下面提供的上下文信息来回答用户的问题。请确保你的回答完全基于所给的上下文,不要使用任何外部知识。如果上下文中没有足够信息来回答问题,请直接说“根据所提供的信息,我无法回答该问题。
--- 上下文 ---
{context}
`),
schema.UserMessage("{question}"),
)
var vars = map[string]any{
"question": question,
"context": contextBuilder.String(),
}
messages, err := ragTemplate.Format(ctx, vars)
if err != nil {
return err
}
log.Println("--- 最终发送给 LLM 的提示词 ---")
for _, msg := range messages {
log.Printf("[%s]: %s\n", msg.Role, msg.Content)
}
log.Println("---------------------------------")
chatModel, err := ollama2.NewChatModel(ctx, &ollama2.ChatModelConfig{
BaseURL: ollamaBaseURL,
Model: chatModelName,
})
if err != nil {
return err
}
stream, err := chatModel.Stream(ctx, messages)
if err != nil {
return err
}
defer stream.Close()
log.Println("\n--- AI 回答 ---")
for {
chunk, err := stream.Recv()
if err != nil {
break // 流结束或发生错误
}
print(chunk.Content)
}
println() // 换行
return nil
}
func createRedisIndexIfNotExists(ctx context.Context, rdb *redis.Client) error {
indices, err := rdb.Do(ctx, "FT._LIST").StringSlice()
if err != nil {
return err
}
for _, v := range indices {
if v == redisIndexName {
return nil
}
}
_, err = rdb.FTCreate(ctx, redisIndexName, &redis.FTCreateOptions{
OnHash: true,
Prefix: []any{redisKeyPrefix},
}, &redis.FieldSchema{FieldName: "content", FieldType: redis.SearchFieldTypeText},
&redis.FieldSchema{
FieldName: "vector_content",
FieldType: redis.SearchFieldTypeVector,
VectorArgs: &redis.FTVectorArgs{
FlatOptions: &redis.FTFlatOptions{
Type: "FLOAT64",
Dim: 384,
DistanceMetric: "L2",
},
},
},
).Result()
if err != nil {
return err
}
return nil
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208