支持私有化部署
AI知识库

53AI知识库

学习大模型的前沿技术与行业应用场景


Golang 基于 Redis 实现文档向量索引与检索系统(RAG)

发布日期:2025-05-07 08:03:54 浏览次数: 1549 作者:白泽talk
推荐语

掌握Golang和Redis,构建高效的文档向量索引与检索系统。

核心内容:
1. 使用Redis实现文档向量检索,构建RAG知识库
2. Eino框架介绍与大语言模型结合的技术栈
3. 系统架构详解与项目运行测试指南

杨芳贤
53A创始人/腾讯云(TVP)最具价值专家

 

前言

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

使用到的技术栈如下:

语言:go1.22

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

向量存储与检索:Redis

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

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

?项目已经开源,地址如下:https://github.com/BaiZe1998/go-learning

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

系统架构

系统架构回答生成阶段查询检索阶段索引构建阶段Markdown文件文件加载器文档分割器嵌入模型文档向量Redis向量数据库用户问题嵌入模型查询向量KNN向量搜索TopK相关文档提示构建增强提示大语言模型生成回答检索器\nRetrieverRAG系统生成器\nGenerator参数配置\ntopK等

项目运行

  1. 1. docker 启动默认知识库
cd eino_assistant
docker-compose up -d
通过这种方式启动的 redis 内置了一部分已经完成向量化的 Eino 文档数据
  1. 2. 环境变量设置
在知识库构建阶段,需要使用到文档向量化的模型
在检索增强阶段,需要使用语言大模型进行总结回复
cd eino_assistant
source .env
  1. 3. 启动 rag 系统
使用 redis 作为文档数据库,同时每次检索3条
go run eino/rag/cmd/main.go --redis=true --topk=3
  1. 4. 测试
问题> 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. 5. 提问知识库中不存在的信息
问题> 什么是大数据

===== 检索到 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. 6. 在知识库中补充"大数据"相关信息
在 cmd/knowledgeindexing 目录下新建一个 big_data.md 文档,内容如下:
大数据
大数据(Big Data)是指规模庞大、结构复杂且无法通过传统数据处理工具在合理时间内进行有效捕捉、管理和处理的数据集合。其核心价值在于通过专业化分析挖掘数据中蕴含的信息,从而提升决策力、优化流程并创造新价值。
  1. 7. 重新生成文档向量,将大数据信息添加到 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. 8. 再次测试
问题> 什么是大数据

===== 检索到 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 {
        returnnil, fmt.Errorf("生成查询向量失败: %w", err)
    }

    iflen(queryVectors) == 0 || len(queryVectors[0]) == 0 {
        returnnil, 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 {
        returnnil, 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) (stringerror) {
    // 组合上下文信息
    context := ""
    iflen(documents) > 0 {
        contextParts := make([]stringlen(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)
    }

    // 提取回答
    iflen(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("再见!")
}

53AI,企业落地大模型首选服务商

产品:场景落地咨询+大模型应用平台+行业解决方案

承诺:免费场景POC验证,效果验证后签署服务协议。零风险落地应用大模型,已交付160+中大型企业

联系我们

售前咨询
186 6662 7370
预约演示
185 8882 0121

微信扫码

添加专属顾问

回到顶部

加载中...

扫码咨询