Announcement

👇Official Account👇

Welcome to join the group & private message

Article first/tail QR code

Skip to content

Golang 实现 RAG 系统:从 OpenAI API 到向量数据库完整实战

📖 引言

RAG(Retrieval-Augmented Generation,检索增强生成)是 2024-2025 年最热门的 AI 应用架构之一。它通过结合外部知识库检索和大语言模型生成能力,有效解决了 LLM 的幻觉问题和知识时效性限制。

本文将详细介绍如何使用 Golang 构建一个完整的 RAG 系统,包括:

  • 🔍 文档处理与 Chunking 策略
  • 🧠 向量化(Embedding)实现
  • 📊 向量数据库集成(Qdrant)
  • 🔎 语义检索优化
  • 💬 与 OpenAI API 集成生成回答
  • 🚀 生产环境最佳实践

🎯 什么是 RAG?

RAG 工作原理

用户问题 → 向量化 → 语义检索 → 召回相关文档 → 构建 Prompt → LLM 生成答案

RAG vs 传统 LLM

特性传统 LLMRAG 系统
知识来源训练数据(静态)外部知识库(动态)
时效性差(训练时间点)好(实时更新)
幻觉问题严重显著降低
成本高(需要大量 tokens)中等(仅检索相关内容)
可追溯性有(可引用来源)

🛠️ 技术栈选型

核心组件

go
// 我们将使用以下技术栈
- Go 1.21+               // 主开发语言
- OpenAI API (gpt-4)     // LLM 服务
- text-embedding-ada-002 // Embedding 模型
- Qdrant                 // 向量数据库
- gin                    // Web 框架
- go-openai              // OpenAI Go SDK

为什么选择 Golang?

  1. 高性能:并发处理大量文档
  2. 简单部署:单二进制文件
  3. 优秀的并发模型:goroutine 处理并行任务
  4. 丰富的生态:AI 相关库逐渐成熟

📦 环境准备

1. 安装依赖

bash
# 初始化项目
mkdir golang-rag-system
cd golang-rag-system
go mod init github.com/yourusername/golang-rag

# 安装核心依赖
go get github.com/sashabaranov/go-openai
go get github.com/qdrant/go-client
go get github.com/gin-gonic/gin
go get github.com/joho/godotenv

2. 配置环境变量

bash
# .env
OPENAI_API_KEY=sk-xxxxxxxxxx
QDRANT_URL=http://localhost:6333
QDRANT_API_KEY=your-qdrant-key
COLLECTION_NAME=documents

3. 启动 Qdrant(使用 Docker)

bash
docker run -p 6333:6333 -p 6334:6334 \
  -v $(pwd)/qdrant_storage:/qdrant/storage:z \
  qdrant/qdrant

💻 核心实现

1. 项目结构

golang-rag/
├── cmd/
│   └── main.go           # 入口文件
├── internal/
│   ├── embedding/        # Embedding 生成
│   │   └── openai.go
│   ├── vectordb/         # 向量数据库操作
│   │   └── qdrant.go
│   ├── chunker/          # 文档分块
│   │   └── text_splitter.go
│   ├── retriever/        # 检索器
│   │   └── retriever.go
│   └── rag/              # RAG 核心逻辑
│       └── pipeline.go
├── pkg/
│   └── models/           # 数据模型
│       └── document.go
├── .env
├── go.mod
└── go.sum

2. 数据模型定义

go
// pkg/models/document.go
package models

type Document struct {
    ID        string                 `json:"id"`
    Content   string                 `json:"content"`
    Metadata  map[string]interface{} `json:"metadata"`
    Embedding []float32              `json:"embedding,omitempty"`
}

type SearchResult struct {
    Document Document  `json:"document"`
    Score    float64   `json:"score"`
}

type RAGResponse struct {
    Answer    string         `json:"answer"`
    Sources   []SearchResult `json:"sources"`
    TokenUsed int            `json:"token_used"`
}

3. Embedding 生成器

go
// internal/embedding/openai.go
package embedding

import (
    "context"
    "fmt"
    "github.com/sashabaranov/go-openai"
)

type EmbeddingService struct {
    client *openai.Client
    model  string
}

func NewEmbeddingService(apiKey string) *EmbeddingService {
    return &EmbeddingService{
        client: openai.NewClient(apiKey),
        model:  openai.AdaEmbeddingV2,
    }
}

// GenerateEmbedding 生成单个文本的向量
func (s *EmbeddingService) GenerateEmbedding(ctx context.Context, text string) ([]float32, error) {
    req := openai.EmbeddingRequest{
        Input: []string{text},
        Model: s.model,
    }

    resp, err := s.client.CreateEmbeddings(ctx, req)
    if err != nil {
        return nil, fmt.Errorf("failed to create embedding: %w", err)
    }

    if len(resp.Data) == 0 {
        return nil, fmt.Errorf("no embedding data returned")
    }

    return resp.Data[0].Embedding, nil
}

// BatchGenerateEmbeddings 批量生成向量(优化性能)
func (s *EmbeddingService) BatchGenerateEmbeddings(ctx context.Context, texts []string) ([][]float32, error) {
    // OpenAI 单次最多支持 2048 个输入
    const batchSize = 100
    var allEmbeddings [][]float32

    for i := 0; i < len(texts); i += batchSize {
        end := i + batchSize
        if end > len(texts) {
            end = len(texts)
        }

        batch := texts[i:end]
        req := openai.EmbeddingRequest{
            Input: batch,
            Model: s.model,
        }

        resp, err := s.client.CreateEmbeddings(ctx, req)
        if err != nil {
            return nil, fmt.Errorf("batch embedding failed: %w", err)
        }

        for _, data := range resp.Data {
            allEmbeddings = append(allEmbeddings, data.Embedding)
        }
    }

    return allEmbeddings, nil
}

4. 文档分块器

go
// internal/chunker/text_splitter.go
package chunker

import (
    "strings"
    "unicode/utf8"
)

type TextSplitter struct {
    ChunkSize    int     // 每块字符数
    ChunkOverlap int     // 重叠字符数
}

func NewTextSplitter(chunkSize, overlap int) *TextSplitter {
    return &TextSplitter{
        ChunkSize:    chunkSize,
        ChunkOverlap: overlap,
    }
}

// SplitText 将长文本分割成多个 chunk
func (ts *TextSplitter) SplitText(text string) []string {
    if utf8.RuneCountInString(text) <= ts.ChunkSize {
        return []string{text}
    }

    var chunks []string
    runes := []rune(text)
    start := 0

    for start < len(runes) {
        end := start + ts.ChunkSize
        if end > len(runes) {
            end = len(runes)
        }

        // 尝试在句子边界分割
        if end < len(runes) {
            // 查找最近的句号、问号或换行符
            for i := end; i > start+ts.ChunkSize/2; i-- {
                if runes[i] == '' || runes[i] == '' || runes[i] == '\n' || runes[i] == '.' {
                    end = i + 1
                    break
                }
            }
        }

        chunk := string(runes[start:end])
        chunks = append(chunks, strings.TrimSpace(chunk))

        // 计算下一个起始位置(考虑重叠)
        start = end - ts.ChunkOverlap
        if start < 0 {
            start = 0
        }
    }

    return chunks
}

// SplitByParagraph 按段落分割(适用于结构化文档)
func (ts *TextSplitter) SplitByParagraph(text string) []string {
    paragraphs := strings.Split(text, "\n\n")
    var chunks []string

    currentChunk := ""
    for _, para := range paragraphs {
        para = strings.TrimSpace(para)
        if para == "" {
            continue
        }

        if utf8.RuneCountInString(currentChunk+para) <= ts.ChunkSize {
            if currentChunk != "" {
                currentChunk += "\n\n"
            }
            currentChunk += para
        } else {
            if currentChunk != "" {
                chunks = append(chunks, currentChunk)
            }
            // 如果单个段落超过 ChunkSize,进一步分割
            if utf8.RuneCountInString(para) > ts.ChunkSize {
                chunks = append(chunks, ts.SplitText(para)...)
            } else {
                currentChunk = para
            }
        }
    }

    if currentChunk != "" {
        chunks = append(chunks, currentChunk)
    }

    return chunks
}

5. 向量数据库操作

go
// internal/vectordb/qdrant.go
package vectordb

import (
    "context"
    "fmt"
    "github.com/google/uuid"
    pb "github.com/qdrant/go-client/qdrant"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
    "your-project/pkg/models"
)

type QdrantClient struct {
    client         pb.PointsClient
    collectionName string
}

func NewQdrantClient(address, collectionName string) (*QdrantClient, error) {
    conn, err := grpc.Dial(address, grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
        return nil, fmt.Errorf("failed to connect to Qdrant: %w", err)
    }

    return &QdrantClient{
        client:         pb.NewPointsClient(conn),
        collectionName: collectionName,
    }, nil
}

// UpsertDocuments 插入或更新文档
func (q *QdrantClient) UpsertDocuments(ctx context.Context, docs []models.Document) error {
    points := make([]*pb.PointStruct, 0, len(docs))

    for _, doc := range docs {
        if doc.ID == "" {
            doc.ID = uuid.New().String()
        }

        // 转换 metadata 为 payload
        payload := make(map[string]*pb.Value)
        payload["content"] = &pb.Value{
            Kind: &pb.Value_StringValue{StringValue: doc.Content},
        }

        for k, v := range doc.Metadata {
            if strVal, ok := v.(string); ok {
                payload[k] = &pb.Value{
                    Kind: &pb.Value_StringValue{StringValue: strVal},
                }
            }
        }

        point := &pb.PointStruct{
            Id: &pb.PointId{
                PointIdOptions: &pb.PointId_Uuid{Uuid: doc.ID},
            },
            Vectors: &pb.Vectors{
                VectorsOptions: &pb.Vectors_Vector{
                    Vector: &pb.Vector{Data: doc.Embedding},
                },
            },
            Payload: payload,
        }

        points = append(points, point)
    }

    _, err := q.client.Upsert(ctx, &pb.UpsertPoints{
        CollectionName: q.collectionName,
        Points:         points,
    })

    return err
}

// Search 语义搜索
func (q *QdrantClient) Search(ctx context.Context, queryVector []float32, topK int) ([]models.SearchResult, error) {
    resp, err := q.client.Search(ctx, &pb.SearchPoints{
        CollectionName: q.collectionName,
        Vector:         queryVector,
        Limit:          uint64(topK),
        WithPayload:    &pb.WithPayloadSelector{SelectorOptions: &pb.WithPayloadSelector_Enable{Enable: true}},
    })

    if err != nil {
        return nil, fmt.Errorf("search failed: %w", err)
    }

    results := make([]models.SearchResult, 0, len(resp.Result))
    for _, point := range resp.Result {
        content := ""
        metadata := make(map[string]interface{})

        if payload := point.Payload; payload != nil {
            if contentVal, ok := payload["content"]; ok {
                if strVal := contentVal.GetStringValue(); strVal != "" {
                    content = strVal
                }
            }

            for k, v := range payload {
                if k != "content" {
                    if strVal := v.GetStringValue(); strVal != "" {
                        metadata[k] = strVal
                    }
                }
            }
        }

        results = append(results, models.SearchResult{
            Document: models.Document{
                ID:       point.Id.GetUuid(),
                Content:  content,
                Metadata: metadata,
            },
            Score: float64(point.Score),
        })
    }

    return results, nil
}

6. RAG 核心管道

go
// internal/rag/pipeline.go
package rag

import (
    "context"
    "fmt"
    "strings"
    "github.com/sashabaranov/go-openai"
    "your-project/internal/embedding"
    "your-project/internal/vectordb"
    "your-project/pkg/models"
)

type RAGPipeline struct {
    embeddingService *embedding.EmbeddingService
    vectorDB         *vectordb.QdrantClient
    llmClient        *openai.Client
    topK             int
}

func NewRAGPipeline(
    embService *embedding.EmbeddingService,
    vdb *vectordb.QdrantClient,
    openaiKey string,
    topK int,
) *RAGPipeline {
    return &RAGPipeline{
        embeddingService: embService,
        vectorDB:         vdb,
        llmClient:        openai.NewClient(openaiKey),
        topK:             topK,
    }
}

// Query 执行 RAG 查询
func (r *RAGPipeline) Query(ctx context.Context, question string) (*models.RAGResponse, error) {
    // 1. 将问题向量化
    queryVector, err := r.embeddingService.GenerateEmbedding(ctx, question)
    if err != nil {
        return nil, fmt.Errorf("failed to generate query embedding: %w", err)
    }

    // 2. 检索相关文档
    searchResults, err := r.vectorDB.Search(ctx, queryVector, r.topK)
    if err != nil {
        return nil, fmt.Errorf("failed to search documents: %w", err)
    }

    if len(searchResults) == 0 {
        return &models.RAGResponse{
            Answer:  "抱歉,我没有找到相关信息。",
            Sources: []models.SearchResult{},
        }, nil
    }

    // 3. 构建上下文
    context := r.buildContext(searchResults)

    // 4. 调用 LLM 生成回答
    answer, tokenUsed, err := r.generateAnswer(ctx, question, context)
    if err != nil {
        return nil, fmt.Errorf("failed to generate answer: %w", err)
    }

    return &models.RAGResponse{
        Answer:    answer,
        Sources:   searchResults,
        TokenUsed: tokenUsed,
    }, nil
}

// buildContext 构建上下文提示词
func (r *RAGPipeline) buildContext(results []models.SearchResult) string {
    var sb strings.Builder
    sb.WriteString("以下是相关的参考信息:\n\n")

    for i, result := range results {
        sb.WriteString(fmt.Sprintf("【参考 %d\n%s\n\n", i+1, result.Document.Content))
    }

    return sb.String()
}

// generateAnswer 调用 LLM 生成最终答案
func (r *RAGPipeline) generateAnswer(ctx context.Context, question, context string) (string, int, error) {
    prompt := fmt.Sprintf(`你是一个专业的技术助手。请基于以下参考信息回答用户的问题。

%s

用户问题:%s

请注意:
1. 仅基于参考信息回答,不要编造内容
2. 如果参考信息不足,请明确说明
3. 回答要准确、专业、易懂
4. 可以引用参考信息的编号

你的回答:`, context, question)

    req := openai.ChatCompletionRequest{
        Model: openai.GPT4TurboPreview,
        Messages: []openai.ChatCompletionMessage{
            {
                Role:    openai.ChatMessageRoleSystem,
                Content: "你是一个专业的技术助手,擅长基于提供的参考资料准确回答问题。",
            },
            {
                Role:    openai.ChatMessageRoleUser,
                Content: prompt,
            },
        },
        Temperature: 0.7,
        MaxTokens:   1000,
    }

    resp, err := r.llmClient.CreateChatCompletion(ctx, req)
    if err != nil {
        return "", 0, err
    }

    if len(resp.Choices) == 0 {
        return "", 0, fmt.Errorf("no response from LLM")
    }

    return resp.Choices[0].Message.Content, resp.Usage.TotalTokens, nil
}

7. HTTP API 接口

go
// cmd/main.go
package main

import (
    "context"
    "log"
    "net/http"
    "os"

    "github.com/gin-gonic/gin"
    "github.com/joho/godotenv"
    "your-project/internal/chunker"
    "your-project/internal/embedding"
    "your-project/internal/rag"
    "your-project/internal/vectordb"
    "your-project/pkg/models"
)

type Server struct {
    ragPipeline *rag.RAGPipeline
    embService  *embedding.EmbeddingService
    vectorDB    *vectordb.QdrantClient
    chunker     *chunker.TextSplitter
}

func main() {
    // 加载环境变量
    if err := godotenv.Load(); err != nil {
        log.Println("Warning: .env file not found")
    }

    // 初始化服务
    embService := embedding.NewEmbeddingService(os.Getenv("OPENAI_API_KEY"))
    
    vectorDB, err := vectordb.NewQdrantClient(
        os.Getenv("QDRANT_URL"),
        os.Getenv("COLLECTION_NAME"),
    )
    if err != nil {
        log.Fatal("Failed to connect to Qdrant:", err)
    }

    ragPipeline := rag.NewRAGPipeline(embService, vectorDB, os.Getenv("OPENAI_API_KEY"), 5)
    textSplitter := chunker.NewTextSplitter(500, 50)

    server := &Server{
        ragPipeline: ragPipeline,
        embService:  embService,
        vectorDB:    vectorDB,
        chunker:     textSplitter,
    }

    // 设置路由
    r := gin.Default()
    
    r.POST("/ingest", server.handleIngest)
    r.POST("/query", server.handleQuery)
    r.GET("/health", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"status": "healthy"})
    })

    log.Println("Server starting on :8080")
    if err := r.Run(":8080"); err != nil {
        log.Fatal("Failed to start server:", err)
    }
}

// handleIngest 处理文档导入
func (s *Server) handleIngest(c *gin.Context) {
    var req struct {
        Text     string                 `json:"text" binding:"required"`
        Metadata map[string]interface{} `json:"metadata"`
    }

    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    ctx := context.Background()

    // 1. 分块
    chunks := s.chunker.SplitText(req.Text)

    // 2. 生成向量
    embeddings, err := s.embService.BatchGenerateEmbeddings(ctx, chunks)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate embeddings"})
        return
    }

    // 3. 构建文档
    docs := make([]models.Document, len(chunks))
    for i, chunk := range chunks {
        docs[i] = models.Document{
            Content:   chunk,
            Metadata:  req.Metadata,
            Embedding: embeddings[i],
        }
    }

    // 4. 存储到向量数据库
    if err := s.vectorDB.UpsertDocuments(ctx, docs); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to store documents"})
        return
    }

    c.JSON(http.StatusOK, gin.H{
        "message":    "Documents ingested successfully",
        "chunk_count": len(chunks),
    })
}

// handleQuery 处理查询请求
func (s *Server) handleQuery(c *gin.Context) {
    var req struct {
        Question string `json:"question" binding:"required"`
    }

    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    ctx := context.Background()
    response, err := s.ragPipeline.Query(ctx, req.Question)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }

    c.JSON(http.StatusOK, response)
}

🧪 测试与使用

1. 导入文档

bash
curl -X POST http://localhost:8080/ingest \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Golang 是一门由 Google 开发的开源编程语言,以其简洁的语法、出色的并发性能和快速的编译速度而闻名。Go 语言特别适合构建高性能的服务器端应用、微服务架构和云原生应用。",
    "metadata": {
      "source": "golang-introduction",
      "category": "programming-language"
    }
  }'

2. 查询问题

bash
curl -X POST http://localhost:8080/query \
  -H "Content-Type: application/json" \
  -d '{
    "question": "Golang 有什么特点?"
  }'

响应示例

json
{
  "answer": "基于参考信息,Golang 的主要特点包括:\n1. 简洁的语法\n2. 出色的并发性能\n3. 快速的编译速度\n4. 特别适合构建高性能服务器端应用、微服务架构和云原生应用",
  "sources": [
    {
      "document": {
        "id": "xxx-xxx-xxx",
        "content": "Golang 是一门由 Google 开发的...",
        "metadata": {
          "source": "golang-introduction"
        }
      },
      "score": 0.89
    }
  ],
  "token_used": 245
}

🚀 生产环境优化

1. 性能优化

使用连接池

go
// 为 OpenAI API 配置 HTTP 客户端连接池
httpClient := &http.Client{
    Timeout: 30 * time.Second,
    Transport: &http.Transport{
        MaxIdleConns:        100,
        MaxIdleConnsPerHost: 10,
        IdleConnTimeout:     90 * time.Second,
    },
}

config := openai.DefaultConfig(apiKey)
config.HTTPClient = httpClient
client := openai.NewClientWithConfig(config)

缓存 Embedding

go
// 使用 Redis 缓存常见查询的 embedding
type CachedEmbeddingService struct {
    embService *embedding.EmbeddingService
    cache      *redis.Client
    ttl        time.Duration
}

func (c *CachedEmbeddingService) GetEmbedding(ctx context.Context, text string) ([]float32, error) {
    // 生成缓存 key
    key := fmt.Sprintf("emb:%s", hashText(text))
    
    // 尝试从缓存获取
    cached, err := c.cache.Get(ctx, key).Bytes()
    if err == nil {
        return deserializeEmbedding(cached), nil
    }
    
    // 缓存未命中,生成新的 embedding
    emb, err := c.embService.GenerateEmbedding(ctx, text)
    if err != nil {
        return nil, err
    }
    
    // 存入缓存
    c.cache.Set(ctx, key, serializeEmbedding(emb), c.ttl)
    return emb, nil
}

2. 可观测性

添加日志和指标

go
import (
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promauto"
)

var (
    queryDuration = promauto.NewHistogramVec(
        prometheus.HistogramOpts{
            Name: "rag_query_duration_seconds",
            Help: "RAG query duration in seconds",
        },
        []string{"status"},
    )

    embeddingRequests = promauto.NewCounterVec(
        prometheus.CounterOpts{
            Name: "embedding_requests_total",
            Help: "Total number of embedding requests",
        },
        []string{"status"},
    )
)

func (r *RAGPipeline) Query(ctx context.Context, question string) (*models.RAGResponse, error) {
    start := time.Now()
    defer func() {
        duration := time.Since(start).Seconds()
        queryDuration.WithLabelValues("success").Observe(duration)
    }()

    // ... 原有逻辑
}

3. 错误处理与重试

go
import "github.com/cenkalti/backoff/v4"

func (s *EmbeddingService) GenerateEmbeddingWithRetry(ctx context.Context, text string) ([]float32, error) {
    var result []float32
    
    operation := func() error {
        var err error
        result, err = s.GenerateEmbedding(ctx, text)
        return err
    }

    // 指数退避重试策略
    expBackoff := backoff.NewExponentialBackOff()
    expBackoff.MaxElapsedTime = 30 * time.Second

    err := backoff.Retry(operation, expBackoff)
    return result, err
}

📊 进阶技巧

结合关键词检索和语义检索:

go
type HybridRetriever struct {
    vectorDB    *vectordb.QdrantClient
    keywordDB   *ElasticsearchClient
    vectorWeight float64  // 0-1,向量检索权重
}

func (h *HybridRetriever) Search(ctx context.Context, query string, topK int) ([]models.SearchResult, error) {
    // 1. 并行执行两种检索
    var vectorResults, keywordResults []models.SearchResult
    var wg sync.WaitGroup
    wg.Add(2)

    go func() {
        defer wg.Done()
        vectorResults, _ = h.vectorDB.Search(ctx, queryVector, topK)
    }()

    go func() {
        defer wg.Done()
        keywordResults, _ = h.keywordDB.Search(ctx, query, topK)
    }()

    wg.Wait()

    // 2. 融合结果(RRF - Reciprocal Rank Fusion)
    return h.fuseResults(vectorResults, keywordResults), nil
}

2. ReRank 重排序

使用 Cohere ReRank API 或本地模型提升检索精度:

go
func (r *RAGPipeline) ReRank(ctx context.Context, query string, docs []models.Document) ([]models.Document, error) {
    // 调用 Cohere ReRank API
    client := cohere.NewClient(os.Getenv("COHERE_API_KEY"))
    
    response, err := client.Rerank(ctx, &cohere.RerankRequest{
        Query:     query,
        Documents: extractContents(docs),
        TopN:      5,
        Model:     "rerank-multilingual-v2.0",
    })
    
    if err != nil {
        return nil, err
    }

    // 根据 ReRank 分数重新排序
    reranked := make([]models.Document, len(response.Results))
    for i, result := range response.Results {
        reranked[i] = docs[result.Index]
    }

    return reranked, nil
}

3. 多模态 RAG

支持图片、表格等多模态内容:

go
// 使用 GPT-4 Vision 提取图片内容
func (s *MultiModalService) ExtractImageContent(ctx context.Context, imageURL string) (string, error) {
    req := openai.ChatCompletionRequest{
        Model: openai.GPT4VisionPreview,
        Messages: []openai.ChatCompletionMessage{
            {
                Role: openai.ChatMessageRoleUser,
                MultiContent: []openai.ChatMessagePart{
                    {
                        Type: openai.ChatMessagePartTypeText,
                        Text: "请详细描述这张图片中的内容,包括文字、图表、关键信息等。",
                    },
                    {
                        Type: openai.ChatMessagePartTypeImageURL,
                        ImageURL: &openai.ChatMessageImageURL{
                            URL: imageURL,
                        },
                    },
                },
            },
        },
    }

    resp, err := s.client.CreateChatCompletion(ctx, req)
    // ... 处理响应
}

🎓 最佳实践总结

✅ DO

  1. 合理设置 Chunk Size

    • 技术文档:300-500 字符
    • 对话数据:200-300 字符
    • 长篇文章:500-800 字符
  2. 添加元数据

    • 文档来源、时间戳、分类
    • 便于过滤和追溯
  3. 监控成本

    • 记录 token 使用量
    • 使用缓存减少 API 调用
  4. 测试召回质量

    • 准备测试集
    • 计算 MRR、NDCG 等指标

❌ DON'T

  1. 不要盲目增加 topK(成本高,噪音多)
  2. 不要忽略错误处理(API 调用可能失败)
  3. 不要硬编码 prompt(使用配置文件管理)
  4. 不要忽略安全性(API Key 管理、输入验证)

🔗 相关资源

📝 总结

本文详细介绍了如何使用 Golang 构建一个完整的 RAG 系统,从基础架构到生产优化都有涉及。RAG 技术正在快速发展,建议持续关注最新进展:

  • GraphRAG:基于知识图谱的检索
  • Self-RAG:自我反思的 RAG 系统
  • Adaptive RAG:根据查询自适应选择策略

希望这篇文章能帮助你快速上手 Golang + RAG 开发,构建出色的 AI 应用!


关键词:#Golang #RAG #AI #LLM #VectorDatabase #OpenAI #Qdrant #SemanticSearch #Embedding #智能问答

相关文章推荐

上次更新于: