第八章:Go语言大模型调用框架 - Eino (RAG)
·
1. 构建知识库
根据第七章的向量化和Embedding数据库的知识,通过模拟用户上传的文件进行分块保存。
在实际应用中,原始文档通常较长,直接对整个文档进行向量化会丢失细粒度的语义信息。因此,我们需要先将文档切分成合适的文本块(Chunk)。常见的分块策略包括:
- 固定大小分块:按字符数或 Token 数切分,简单高效,但可能切断语义完整的句子。
- 递归字符文本分割:按段落、句子、单词的优先级递归切分,能更好地保留语义边界。
- 语义分块:利用 Embedding 模型计算句子间的相似度,在语义转折处切分,效果最佳但计算成本较高。
当用户提问时,系统会将问题同样向量化,然后在向量数据库中执行近似最近邻(ANN)搜索,召回最相关的文本块。常用的检索方式包括:
- 余弦相似度:衡量向量方向上的相似程度。
- 点积相似度:适用于归一化后的向量。
- 欧氏距离:衡量向量空间中的绝对距离。
通过合理配置分块大小、重叠窗口和检索 Top-K 数量,可以显著提升 RAG 系统的回答质量。
1.1 模拟用户上传的文件
# Go语言核心知识库
## Goroutine与并发
Go语言的并发模型基于CSP(Communicating Sequential Processes)理论。goroutine是Go运行时管理的轻量级线程,创建一个goroutine只需要大约2KB的栈空间,而操作系统线程通常需要1-8MB。goroutine的调度由Go运行时的GMP调度器负责,G代表goroutine,M代表操作系统线程,P代表逻辑处理器。调度器会把大量的goroutine复用到少量的操作系统线程上执行,实现了高效的并发。启动一个goroutine非常简单,只需要在函数调用前加上go关键字。
## Channel通信
Channel是goroutine之间通信的管道,它是类型安全的。Go语言的设计哲学是"不要通过共享内存来通信,而要通过通信来共享内存"。Channel分为无缓冲Channel和有缓冲Channel两种。无缓冲Channel的发送和接收操作是同步的,发送方会阻塞直到接收方准备好;有缓冲Channel内部有一个队列,只要队列没满发送就不会阻塞。使用select语句可以同时监听多个Channel的读写事件。Channel在关闭后仍然可以读取,读取到的是零值,可以通过v, ok := <-ch的方式判断Channel是否已关闭。
## 接口与多态
Go语言的接口是隐式实现的,不需要像Java那样显式声明implements。只要一个类型实现了接口定义的所有方法,它就自动满足该接口。这种设计让Go的接口非常灵活——你可以为第三方库的类型定义新接口,而不需要修改它的源代码。空接口interface{}(Go 1.18之后可以写成any)可以接收任何类型的值,常用于需要处理未知类型的场景。接口的底层实现由两个指针组成:一个指向类型信息(itab),一个指向实际数据。
## 错误处理
Go语言采用显式的错误返回机制,函数通过返回error类型的值来表示执行是否成功。这和Java/Python的try-catch异常机制完全不同。Go社区推崇"errors are values"的理念,把错误当作普通的值来传递和处理。errors.New和fmt.Errorf用于创建错误,errors.Is用于判断错误链中是否包含特定错误,errors.As用于从错误链中提取特定类型的错误。Go 1.13引入的%w格式化动词可以包装错误,形成错误链,方便在上层代码中追溯根因。panic/recover机制用于处理真正不可恢复的异常情况,不应该用于常规的错误处理。
## Context上下文
context包是Go并发编程中不可或缺的组件。它的核心作用是在goroutine之间传递取消信号、超时控制和请求级别的值。context.Background()和context.TODO()是两个根Context。context.WithCancel创建一个可以手动取消的Context,context.WithTimeout和context.WithDeadline创建带超时的Context,context.WithValue在Context中存储键值对。Context的设计原则是:它应该作为函数的第一个参数传递,不应该存储在结构体中;Context的取消是级联的,父Context取消后所有子Context也会被取消。在HTTP服务、数据库操作、RPC调用等场景中,Context是控制超时和取消的标准方式。
## Go Module依赖管理
Go Module是Go官方的依赖管理方案,从Go 1.11开始引入,Go 1.16成为默认模式。go.mod文件记录了模块路径和所有依赖的版本号,go.sum文件保存了每个依赖的哈希校验值,用于验证依赖的完整性。go mod init初始化一个新模块,go mod tidy自动添加缺失的依赖并移除不需要的依赖,go get用于添加或更新特定依赖。Go Module采用语义化版本控制(Semantic Versioning),v2及以上的大版本需要在模块路径中加上版本后缀。replace指令可以用本地路径替代远程依赖,常用于本地开发调试。
## GC垃圾回收
Go语言的垃圾回收器采用并发的三色标记清除算法。三种颜色分别代表:白色是未被扫描的对象(GC结束后会被回收),灰色是已被扫描但其引用的对象还未全部扫描的对象,黑色是已被扫描且其引用的对象也已全部扫描的对象。GC的触发条件包括:堆内存增长达到GOGC设定的比例(默认100%,即翻倍时触发)、手动调用runtime.GC()、或者距离上次GC超过2分钟。从Go 1.5开始,GC的STW(Stop The World)时间已经控制在毫秒级。Go 1.19引入了GOMEMLIMIT环境变量,可以设置软内存上限来更精细地控制GC行为。
1.2 文本分块并向量化存储
package main
import (
"context"
"fmt"
"github.com/cloudwego/eino-ext/components/document/loader/file"
"github.com/cloudwego/eino-ext/components/document/transformer/splitter/markdown"
einoOpenAI "github.com/cloudwego/eino-ext/components/embedding/openai"
einoIndexer "github.com/cloudwego/eino-ext/components/indexer/milvus2"
"github.com/cloudwego/eino/components/document"
"github.com/cloudwego/eino/components/document/parser"
"github.com/milvus-io/milvus/client/v2/milvusclient"
"log"
)
const (
collectionName = "go_knowledge_rag"
embeddingDim = 1024
)
func main() {
ctx := context.Background()
// ====== 1. 加载文档 ======
loader, err := file.NewFileLoader(ctx, &file.FileLoaderConfig{
UseNameAsID: true,
Parser: parser.TextParser{},
})
if err != nil {
log.Fatalf("创建 FileLoader 失败: %v", err)
return
}
docs, err := loader.Load(ctx, document.Source{URI: "testdata/go_knowledge.md"})
if err != nil {
log.Fatalf("加载文档失败: %v", err)
return
}
fmt.Printf("✅ 加载了 %d 篇文档,总长度: %d 字符\n", len(docs), len([]rune(docs[0].Content)))
// ====== 2. Markdown 分块 ======
splitter, err := markdown.NewHeaderSplitter(ctx, &markdown.HeaderConfig{
Headers: map[string]string{
"#": "h1",
"##": "h2",
},
})
if err != nil {
log.Fatalf("创建 Splitter 失败: %v", err)
}
chunks, err := splitter.Transform(ctx, docs)
if err != nil {
log.Fatalf("分块失败: %v", err)
}
fmt.Printf("✅ 分块完成,共 %d 个块\n", len(chunks))
// 将标题信息拼接到内容前面,提升检索效果
// 同时为每个 chunk 生成唯一 ID(避免 Milvus 重复主键错误)
for i, chunk := range chunks {
var titlePrefix string
if h1, ok := chunk.MetaData["h1"].(string); ok {
titlePrefix += h1
}
if h2, ok := chunk.MetaData["h2"].(string); ok {
titlePrefix += " > " + h2
}
if titlePrefix != "" {
chunk.Content = titlePrefix + "\n\n" + chunk.Content
}
chunk.ID = fmt.Sprintf("%s_chunk_%d", chunk.ID, i)
}
// 打印分块结果预览
for i, chunk := range chunks {
content := []rune(chunk.Content)
preview := string(content)
if len(content) > 60 {
preview = string(content[:60]) + "..."
}
fmt.Printf(" 块 %d: %s\n", i+1, preview)
}
// ====== 3. 初始化 Embedding ======
dim := embeddingDim
embedder, err := einoOpenAI.NewEmbedder(ctx, &einoOpenAI.EmbeddingConfig{
APIKey: "你的API KEY", // API KEY
BaseURL: "https://api-inference.modelscope.cn/v1/",
Model: "Qwen/Qwen3-Embedding-0.6B",
Dimensions: &dim,
})
if err != nil {
log.Fatalf("创建 Embedder 失败: %v", err)
}
// ====== 4. 存入 Milvus ======
indexer, err := einoIndexer.NewIndexer(ctx, &einoIndexer.IndexerConfig{
ClientConfig: &milvusclient.ClientConfig{Address: "127.0.0.1:19530"},
Collection: collectionName,
Embedding: embedder,
Vector: &einoIndexer.VectorConfig{
Dimension: embeddingDim,
MetricType: einoIndexer.COSINE,
IndexBuilder: einoIndexer.NewHNSWIndexBuilder().WithM(16).WithEfConstruction(200),
},
})
if err != nil {
log.Fatalf("创建 Indexer 失败: %v", err)
return
}
ids, err := indexer.Store(ctx, chunks)
if err != nil {
log.Fatalf("存储文档失败: %v", err)
}
fmt.Printf("\n✅ 成功存入 %d 个文档块到 Milvus,Collection: %s\n", len(ids), collectionName)
fmt.Println("索引构建完成,可以启动 RAG Agent 了!")
}
2. 调用知识库数据进行问答
package main
import (
"context"
"fmt"
einoOpenAI "github.com/cloudwego/eino-ext/components/embedding/openai"
_ "github.com/cloudwego/eino-ext/components/indexer/milvus2"
"github.com/cloudwego/eino-ext/components/model/openai"
einoRetriever "github.com/cloudwego/eino-ext/components/retriever/milvus2"
"github.com/cloudwego/eino-ext/components/retriever/milvus2/search_mode"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/components/tool/utils"
"github.com/cloudwego/eino/compose"
"github.com/cloudwego/eino/flow/agent/react"
"github.com/cloudwego/eino/schema"
"github.com/milvus-io/milvus/client/v2/milvusclient"
"log"
"strings"
)
const (
collectionName = "go_knowledge_rag"
embeddingDim = 1024
)
// 知识库检索请求
type SearchRequest struct {
Query string `json:"query" jsonschema:"description=用于检索知识库的查询文本,应该是一个清晰的问题或关键词短语"`
}
// 知识库检索结果
type SearchResponse struct {
Results string `json:"results"`
Count int `json:"count"`
}
func main() {
ctx := context.Background()
// ====== 1. 初始化 Embedding(给 Retriever 用) ======
dim := embeddingDim
embedder, err := einoOpenAI.NewEmbedder(ctx, &einoOpenAI.EmbeddingConfig{
APIKey: "你的API KEY", // API KEY
BaseURL: "https://api-inference.modelscope.cn/v1/",
Model: "Qwen/Qwen3-Embedding-0.6B",
Dimensions: &dim,
})
if err != nil {
log.Fatalf("创建 Embedder 失败: %v", err)
return
}
// ====== 2. 初始化 Retriever ======
retriever, err := einoRetriever.NewRetriever(ctx, &einoRetriever.RetrieverConfig{
ClientConfig: &milvusclient.ClientConfig{Address: "127.0.0.1:19530"},
Collection: collectionName,
Embedding: embedder,
TopK: 3,
SearchMode: search_mode.NewApproximate(einoRetriever.COSINE)})
if err != nil {
log.Fatalf("创建 Retriever 失败: %v", err)
return
}
// ====== 3. 创建检索工具 ======
searchFn := func(ctx context.Context, req *SearchRequest) (*SearchResponse, error) {
docs, err := retriever.Retrieve(ctx, req.Query)
if err != nil {
return nil, fmt.Errorf("知识库检索失败: %w", err)
}
if len(docs) == 0 {
return &SearchResponse{
Results: "未检索到相关内容",
Count: 0,
}, nil
}
// 把检索到的文档块拼成一段格式化的文本
var sb strings.Builder
for i, doc := range docs {
score := doc.MetaData["score"]
sb.WriteString(fmt.Sprintf("【参考资料 %d】(相似度: %v)\n", i+1, score))
sb.WriteString(doc.Content)
sb.WriteString("\n\n")
}
return &SearchResponse{
Results: sb.String(),
Count: len(docs),
}, nil
}
schemaTool := utils.NewTool(&schema.ToolInfo{
Name: "search_knowledge",
Desc: "从Go语言知识库中检索与查询相关的技术文档。当用户提问关于Go语言的技术问题时,使用此工具检索相关知识。",
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
"query": {
Type: schema.String,
Desc: "检索查询文本,应该是清晰的问题或关键词",
Required: true,
},
}),
}, searchFn)
// ====== 4. 创建 ChatModel ======
chatModel, err := openai.NewChatModel(ctx, &openai.ChatModelConfig{
BaseURL: "https://api-inference.modelscope.cn/v1/",
APIKey: "你的API KEY", // API KEY
Model: "Qwen/Qwen3.5-35B-A3B",
})
if err != nil {
log.Fatalf("创建 ChatModel 失败: %v", err)
}
// ====== 5. 创建 ReAct Agent ======
agent, err := react.NewAgent(ctx, &react.AgentConfig{
ToolCallingModel: chatModel,
ToolsConfig: compose.ToolsNodeConfig{
Tools: []tool.BaseTool{schemaTool},
},
MaxStep: 5,
MessageModifier: func(ctx context.Context, input []*schema.Message) []*schema.Message {
systemPrompt := `你是一个专业的Go语言技术助手,负责回答用户关于Go语言的技术问题。
你有一个知识库检索工具 search_knowledge,当用户提出Go语言相关的技术问题时,你应该:
1. 先使用 search_knowledge 工具检索相关知识
2. 基于检索到的参考资料来回答问题
3. 回答要准确、详细,并结合参考资料中的具体信息
4. 如果检索结果不包含足够的信息,坦诚告知用户
回答风格要求:
- 用中文回答
- 讲解清晰、深入浅出
- 如果涉及代码概念,给出简要说明`
messages := make([]*schema.Message, 0, len(input)+1)
messages = append(messages, schema.SystemMessage(systemPrompt))
messages = append(messages, input...)
return messages
},
})
if err != nil {
log.Fatalf("创建 Agent 失败: %v", err)
}
// ====== 6. 测试问答 ======
questions := []string{
"Go语言的goroutine和操作系统线程有什么区别?",
"Go里面怎么处理错误?和Java的异常机制有什么不同?",
"什么是Context?在什么场景下会用到?",
}
for _, question := range questions {
fmt.Printf("\n🙋 用户提问: %s\n", question)
fmt.Println(strings.Repeat("-", 60))
answer, err := agent.Generate(ctx, []*schema.Message{
schema.UserMessage(question),
})
if err != nil {
fmt.Printf("❌ Agent 执行失败: %v\n", err)
continue
}
fmt.Printf("🤖 Agent 回答:\n%s\n", answer.Content)
fmt.Println(strings.Repeat("=", 60))
}
}
2. 多轮问答
package main
import (
"bufio"
"context"
"fmt"
"log"
"os"
"strings"
einoOpenAI "github.com/cloudwego/eino-ext/components/embedding/openai"
"github.com/cloudwego/eino-ext/components/model/openai"
einoRetriever "github.com/cloudwego/eino-ext/components/retriever/milvus2"
"github.com/cloudwego/eino-ext/components/retriever/milvus2/search_mode"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/components/tool/utils"
"github.com/cloudwego/eino/compose"
"github.com/cloudwego/eino/flow/agent/react"
"github.com/cloudwego/eino/schema"
"github.com/milvus-io/milvus/client/v2/milvusclient"
)
const (
collectionName = "go_knowledge_rag"
embeddingDim = 1024
)
type SearchRequest struct {
Query string `json:"query" jsonschema:"description=用于检索知识库的查询文本"`
}
type SearchResponse struct {
Results string `json:"results"`
Count int `json:"count"`
}
func main() {
ctx := context.Background()
// 初始化组件(与上一节相同,这里合并写)
dim := embeddingDim
embedder, err := einoOpenAI.NewEmbedder(ctx, &einoOpenAI.EmbeddingConfig{
APIKey: "你的API KEY",
BaseURL: "https://api-inference.modelscope.cn/v1/",
Model: "Qwen/Qwen3-Embedding-0.6B",
Dimensions: &dim,
})
if err != nil {
log.Fatalf("创建 Embedder 失败: %v", err)
return
}
retriever, err := einoRetriever.NewRetriever(ctx, &einoRetriever.RetrieverConfig{
ClientConfig: &milvusclient.ClientConfig{Address: "localhost:19530"},
Collection: collectionName,
TopK: 3,
SearchMode: search_mode.NewApproximate(einoRetriever.COSINE),
Embedding: embedder,
})
if err != nil {
log.Fatalf("创建 Retriever 失败: %v", err)
}
searchFn := func(ctx context.Context, req *SearchRequest) (*SearchResponse, error) {
docs, err := retriever.Retrieve(ctx, req.Query)
if err != nil {
return nil, fmt.Errorf("检索失败: %w", err)
}
if len(docs) == 0 {
return &SearchResponse{Results: "未检索到相关内容", Count: 0}, nil
}
var sb strings.Builder
for i, doc := range docs {
score := doc.MetaData["score"]
sb.WriteString(fmt.Sprintf("【参考资料 %d】(相似度: %v)\n%s\n\n", i+1, score, doc.Content))
}
return &SearchResponse{Results: sb.String(), Count: len(docs)}, nil
}
searchTool := utils.NewTool(
&schema.ToolInfo{
Name: "search_knowledge",
Desc: "从Go语言知识库中检索与查询相关的技术文档",
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
"query": {Type: schema.String, Desc: "检索查询文本", Required: true},
}),
},
searchFn,
)
chatModel, err := openai.NewChatModel(ctx, &openai.ChatModelConfig{
BaseURL: "https://api-inference.modelscope.cn/v1/",
APIKey: "你的API KEY",
Model: "Qwen/Qwen3.5-35B-A3B",
})
if err != nil {
log.Fatalf("创建 ChatModel 失败: %v", err)
}
agent, err := react.NewAgent(ctx, &react.AgentConfig{
ToolCallingModel: chatModel,
ToolsConfig: compose.ToolsNodeConfig{
Tools: []tool.BaseTool{searchTool},
},
MaxStep: 5,
MessageModifier: func(ctx context.Context, input []*schema.Message) []*schema.Message {
systemPrompt := `你是一个专业的Go语言技术助手。当用户提出Go技术问题时,先用search_knowledge工具检索知识库,再基于检索结果回答。回答用中文,要准确详细。`
messages := make([]*schema.Message, 0, len(input)+1)
messages = append(messages, schema.SystemMessage(systemPrompt))
messages = append(messages, input...)
return messages
},
})
if err != nil {
log.Fatalf("创建 Agent 失败: %v", err)
}
// ====== 多轮对话循环 ======
fmt.Println("🤖 Go语言知识库助手已启动,输入问题开始对话(输入 quit 退出)")
fmt.Println(strings.Repeat("=", 60))
var history []*schema.Message
scanner := bufio.NewScanner(os.Stdin)
for {
fmt.Print("\n🙋 你: ")
if !scanner.Scan() {
break
}
input := strings.TrimSpace(scanner.Text())
if input == "" {
continue
}
if input == "quit" || input == "exit" {
fmt.Println("👋 再见!")
break
}
// 把用户消息追加到历史
history = append(history, schema.UserMessage(input))
// 用完整历史调用 Agent
answer, err := agent.Generate(ctx, history)
if err != nil {
fmt.Printf("❌ 出错了: %v\n", err)
continue
}
fmt.Printf("\n🤖 助手: %s\n", answer.Content)
// 把 Agent 回答追加到历史
history = append(history, schema.AssistantMessage(answer.Content, nil))
}
}
更多推荐


所有评论(0)