Golang 大模型应用开发框架 Eino 全解(开篇)|基于Redis 向量索引的 RAG 系统

1,007 阅读8分钟

前言

大家好,这里是白泽。这篇文章将讲解如何使用 Redis 的向量检索与 LLM 构建一个 RAG 知识库,知识库存储内容是 Eino 框架的介绍。每次尝试从 Redis 向量索引中获取 top k 条相关信息,并使用 LLM 进行总结回复;当没有相关知识,则提示未查找到文档,限制大模型自由发挥。

使用到的技术栈如下:

语言:go1.22

工作流框架:Eino(字节开源的大模型工作流开发框架)

向量存储与检索:Redis

大语言模型:doubao-pro-32k-241215

向量化模型:doubao-embedding-large-text-240915

🌟项目已经开源,地址如下:github.com/BaiZe1998/g…

这里说明一下,当前案例中,索引构建阶段的代码取自:github.com/cloudwego/e…

系统架构

image.png

项目运行

  1. docker 启动默认知识库
cd eino_assistant
docker-compose up -d
# 通过这种方式启动的 redis 内置了一部分已经完成向量化的 Eino 文档数据
  1. 环境变量设置
# 在知识库构建阶段,需要使用到文档向量化的模型
# 在检索增强阶段,需要使用语言大模型进行总结回复
cd eino_assistant
source .env
  1. 启动 rag 系统
# 使用 redis 作为文档数据库,同时每次检索3条
go run eino/rag/cmd/main.go --redis=true --topk=3
  1. 测试
问题> agent 是什么
​
===== 检索到 3 个相关文档 =====
​
文档[1] 相似度: 0.7705  标题: 无标题
----------------------------------------
## **Agent 是什么**
Agent(智能代理)是一个能够感知环境并采取行动以实现特定目标的系统。在 AI 应用中,Agent 通过结合大语言模型的理解能力和预定义工具的执行能力,可以自主地完成复杂的任务。是未来 AI 应用到生活生产中...
​
文档[2] 相似度: 0.7606  标题: 无标题
----------------------------------------
## **总结**
介绍了使用 Eino 框架构建 Agent 的基本方法。通过 Chain、Tool Calling 和 ReAct 等不同方式,我们可以根据实际需求灵活地构建 AI Agent。
Agent 是 AI 技术发展的重要方向。它不仅能够理解用户意图,还能主动采取行动,通过�...
​
文档[3] 相似度: 0.7603  标题: 无标题
----------------------------------------
## **Agent 是什么**
Agent(智能代理)是一个能够感知环境并采取行动以实现特定目标的系统。在 AI 应用中,Agent 通过结合大语言模型的理解能力和预定义工具的执行能力,可以自主地完成复杂的任务。是未来 AI 应用到生活生产中...
​
==============================
​
​
回答:
Agent(智能代理)是一个能够感知环境并采取行动以实现特定目标的系统。在 AI 应用中,Agent 通过结合大语言模型的理解能力和预定义工具的执行能力,可以自主地完成复杂的任务。是未来 AI 应用到生活生产中主要的形态。
​
本文中示例的代码片段详见:[eino-examples/quickstart/taskagent](https://github.com/cloudwego/eino-examples/blob/master/quickstart/taskagent/main.go) 
  1. 提问知识库中不存在的信息
问题> 什么是大数据
​
===== 检索到 3 个相关文档 =====
​
文档[1] 相似度: 0.7647  标题: 无标题
----------------------------------------
---
Description: ""
date: "2025-01-07"
lastmod: ""
tags: []
title: Tool
weight: 0
---
​
文档[2] 相似度: 0.7488  标题: 无标题
----------------------------------------
---
Description: ""
date: "2025-01-06"
lastmod: ""
tags: []
title: Document
weight: 0
---
​
文档[3] 相似度: 0.7419  标题: 无标题
----------------------------------------
---
Description: ""
date: "2025-01-06"
lastmod: ""
tags: []
title: Embedding
weight: 0
---
​
==============================
​
​
回答:
很抱歉,我不知道什么是大数据,文档中没有提供相关信息。
  1. 在知识库中补充"大数据"相关信息
# 在 cmd/knowledgeindexing 目录下新建一个 big_data.md 文档,内容如下:
# 大数据
大数据(Big Data)是指规模庞大、结构复杂且无法通过传统数据处理工具在合理时间内进行有效捕捉、管理和处理的数据集合。其核心价值在于通过专业化分析挖掘数据中蕴含的信息,从而提升决策力、优化流程并创造新价值。
  1. 重新生成文档向量,将大数据信息添加到 Redis 索引中
yucong@yucongdeMacBook-Air eino_assistant % cd cmd/knowledgeindexing 
yucong@yucongdeMacBook-Air knowledgeindexing % go run ./
[start] indexing file: eino-docs/_index.md
[done] indexing file: eino-docs/_index.md, len of parts: 4
[start] indexing file: eino-docs/agent_llm_with_tools.md
[done] indexing file: eino-docs/agent_llm_with_tools.md, len of parts: 1
[start] indexing file: eino-docs/big_data.md
[done] indexing file: eino-docs/big_data.md, len of parts: 1 # 可以看到被切分了
index success
  1. 再次测试
问题> 什么是大数据

===== 检索到 3 个相关文档 =====

文档[1] 相似度: 0.8913  标题: 大数据
----------------------------------------
# 大数据
大数据(Big Data)是指规模庞大、结构复杂且无法通过传统数据处理工具在合理时间内进行有效捕捉、管理和处理的数据集合。其核心价值在于通过专业化分析挖掘数据中蕴含的信息,从而提升决策力、优化流程并创造�...

文档[2] 相似度: 0.7647  标题: 无标题
----------------------------------------
---
Description: ""
date: "2025-01-07"
lastmod: ""
tags: []
title: Tool
weight: 0
---

文档[3] 相似度: 0.7488  标题: 无标题
----------------------------------------
---
Description: ""
date: "2025-01-06"
lastmod: ""
tags: []
title: Document
weight: 0
---

==============================


回答:
大数据(Big Data)是指规模庞大、结构复杂且无法通过传统数据处理工具在合理时间内进行有效捕捉、管理和处理的数据集合。其核心价值在于通过专业化分析挖掘数据中蕴含的信息,从而提升决策力、优化流程并创造新价值。

核心业务流程

索引构建阶段

这一部分参见:eino_assistant/eino/knowledgeindexing 目录代码

流程图:

image-20250505111038955

索引的构建阶段,本质也是一个工作流,因此可以通过 Goland 的 Eino Dev 插件进行可视化绘制,完成之后点击生成流程框架代码,谈后填充一些业务实现即可:

image-20250505232622560

  • 文件加载:从文件系统读取Markdown文档
  • 文档分割:按标题、段落等逻辑单位将文档分割成小段(根据 # 拆分)
  • 向量生成:使用嵌入模型,将文本转换为高维向量(4096)
  • Redis存储:将文档内容、元数据和向量存储到Redis哈希结构中

检索阶段

参见:eino_assistant/eino/rag/retriver.go

  • 用户输入:用户在终端输入问题
  • 查询向量化:使用同样的嵌入模型将问题转换为向量
  • KNN搜索:在Redis中执行KNN(K近邻)向量搜索
  • 相关文档获取:获取与问题语义最相关的TopK个文档
// Retrieve 检索与查询最相关的文档
func (r *RedisRetriever) Retrieve(ctx context.Context, query string, topK int) ([]*schema.Document, error) {
	// 生成查询向量
	queryVectors, err := r.embedder.EmbedStrings(ctx, []string{query})
	if err != nil {
		return nil, fmt.Errorf("生成查询向量失败: %w", err)
	}

	if len(queryVectors) == 0 || len(queryVectors[0]) == 0 {
		return nil, fmt.Errorf("嵌入模型返回空向量")
	}

	queryVector := queryVectors[0]

	// 构建向量搜索查询
	searchQuery := fmt.Sprintf("(*)=>[KNN %d @%s $query_vector AS %s]",
		topK,
		redispkg.VectorField,
		redispkg.DistanceField)

	// 执行向量搜索
	res, err := r.client.Do(ctx,
		"FT.SEARCH", r.indexName, // 执行搜索的索引名称
		searchQuery,   // 向量搜索查询语句
		"PARAMS", "2", // 参数声明,后面有2个参数
		"query_vector", vectorToBytes(queryVector), // 查询向量的二进制表示
		"DIALECT", "2", // 查询方言版本
		"SORTBY", redispkg.DistanceField, // 结果排序字段
		"RETURN", "3", redispkg.ContentField, redispkg.MetadataField, redispkg.DistanceField, // 返回字段
	).Result()

	if err != nil {
		return nil, fmt.Errorf("执行向量搜索失败: %w", err)
	}

	// 将Redis结果转换为Document对象
	return r.parseSearchResults(res)
}

回答生成阶段

参见:eino_assistant/eino/rag/generator.go

  • 提示构建:将检索到的文档和用户问题组合成增强提示
  • LLM调用:将增强提示发送给大语言模型(ARK doubao)
  • 回答生成:模型根据提供的上下文生成针对用户问题的回答
// Generate 生成回答
func (g *ArkGenerator) Generate(ctx context.Context, query string, documents []*schema.Document) (string, error) {
	// 组合上下文信息
	context := ""
	if len(documents) > 0 {
		contextParts := make([]string, len(documents))
		for i, doc := range documents {
			// 如果元数据中有标题,添加标题信息
			titleInfo := ""
			if title, ok := doc.MetaData["title"].(string); ok && title != "" {
				titleInfo = fmt.Sprintf("标题: %s\n", title)
			}
			contextParts[i] = fmt.Sprintf("文档片段[%d]:\n%s%s\n", i+1, titleInfo, doc.Content)
		}
		context = strings.Join(contextParts, "\n---\n")
	}

	// 构建提示
	systemPrompt := "你是一个知识助手。基于提供的文档回答用户问题。如果文档中没有相关信息,请诚实地表明你不知道,不要编造答案。"
	userPrompt := query

	if context != "" {
		userPrompt = fmt.Sprintf("基于以下信息回答我的问题:\n\n%s\n\n问题:%s", context, query)
	}

	// 构建请求
	messages := []chatMessage{
		{Role: "system", Content: systemPrompt},
		{Role: "user", Content: userPrompt},
	}

	reqBody := chatRequest{
		Model:    g.modelName,
		Messages: messages,
	}

	// 序列化请求体
	jsonData, err := json.Marshal(reqBody)
	if err != nil {
		return "", fmt.Errorf("序列化请求失败: %w", err)
	}

	// 创建HTTP请求
	endpoint := fmt.Sprintf("%s/chat/completions", g.baseURL)
	req, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewBuffer(jsonData))
	if err != nil {
		return "", fmt.Errorf("创建HTTP请求失败: %w", err)
	}

	// 添加头信息
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", g.apiKey))

	// 发送请求
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return "", fmt.Errorf("发送请求失败: %w", err)
	}
	defer resp.Body.Close()

	// 读取响应
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", fmt.Errorf("读取响应失败: %w", err)
	}

	// 检查响应状态
	if resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("API返回错误: %s, 状态码: %d", string(body), resp.StatusCode)
	}

	// 解析响应
	var chatResp chatResponse
	if err := json.Unmarshal(body, &chatResp); err != nil {
		return "", fmt.Errorf("解析响应失败: %w", err)
	}

	// 提取回答
	if len(chatResp.Choices) > 0 {
		return chatResp.Choices[0].Message.Content, nil
	}

	return "", fmt.Errorf("API没有返回有效回答")
}

主循环

func main() {
	// 定义命令行参数
	useRedis := flag.Bool("redis", true, "是否使用Redis进行检索增强")
	topK := flag.Int("topk", 3, "检索的文档数量")

	flag.Parse()

	// 检查环境变量
	env.MustHasEnvs("ARK_API_KEY")

	// 构建RAG系统
	ctx := context.Background()
	ragSystem, err := rag.BuildRAG(ctx, *useRedis, *topK)
	if err != nil {
		fmt.Fprintf(os.Stderr, "构建RAG系统失败: %v\n", err)
		os.Exit(1)
	}

	// 显示启动信息
	if *useRedis {
		fmt.Println("启动RAG系统 (使用Redis检索)")
	} else {
		fmt.Println("启动RAG系统 (不使用检索)")
	}
	fmt.Println("输入问题或输入'exit'退出")

	// 创建输入扫描器
	scanner := bufio.NewScanner(os.Stdin)

	// 主循环
	for {
		fmt.Print("\n问题> ")

		// 读取用户输入
		if !scanner.Scan() {
			break
		}

		input := strings.TrimSpace(scanner.Text())
		if input == "" {
			continue
		}

		// 检查退出命令
		if strings.ToLower(input) == "exit" {
			break
		}

		// 处理问题
		answer, err := ragSystem.Answer(ctx, input)
		if err != nil {
			fmt.Fprintf(os.Stderr, "处理问题时出错: %v\n", err)
			continue
		}

		// 显示回答
		fmt.Println("\n回答:")
		fmt.Println(answer)
	}

	if err := scanner.Err(); err != nil {
		fmt.Fprintf(os.Stderr, "读取输入时出错: %v\n", err)
	}

	fmt.Println("再见!")
}

小节

下一章讲解如何通过这个 RAG 知识库,总揽 Eino 的框架生态,敬请期待。

公众号【白泽talk】,Golang|AI 大模型应用开发相关知识星球:白泽说 ,添加: baize_talk02 咨询加入~