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))
	}
}

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐