RAG 与向量检索

检索增强生成(Retrieval-Augmented Generation,RAG)让模型基于自有知识回答问题:先将文档向量化入库,提问时检索最相关的片段作为上下文,再交给模型生成答案。这是企业知识库、客服问答等场景的标准架构。

整体流程

离线索引:文档 → documentloaders 加载 → textsplitter 切分 → Embedding → 向量存储
在线问答:用户提问 → Embedding → 向量检索 Top-K → 拼装 Prompt → LLM 生成答案

向量存储:以 Chroma 为例

langchaingo 的 vectorstores 包支持 Chroma、pgvector、Milvus、Redis 等多种后端,接口统一。以 Chroma 为例:

docker run -d -p 8000:8000 chromadb/chroma
package main

import (
    "context"
    "fmt"
    "log"
    "os"

    "github.com/tmc/langchaingo/schema"
    "github.com/tmc/langchaingo/vectorstores"
    "github.com/tmc/langchaingo/vectorstores/chroma"
)

func main() {
    ctx := context.Background()

    store, err := chroma.New(
        chroma.WithChromaURL("http://localhost:8000"),
        chroma.WithOpenAIAPIKey(os.Getenv("OPENAI_API_KEY")), // 用于向量化
        chroma.WithNameSpace("golang-docs"),                  // 集合命名空间
    )
    if err != nil {
        log.Fatal(err)
    }

    // 写入文档
    _, err = store.AddDocuments(ctx, []schema.Document{
        {PageContent: "Go 通过 goroutine 提供轻量级并发,初始栈仅 2KB。", Metadata: map[string]any{"source": "concurrency.md"}},
        {PageContent: "channel 是 goroutine 之间类型安全的通信管道。", Metadata: map[string]any{"source": "channel.md"}},
        {PageContent: "context 用于传递取消信号与超时控制。", Metadata: map[string]any{"source": "context.md"}},
    })
    if err != nil {
        log.Fatal(err)
    }

    // 相似度检索
    docs, err := store.SimilaritySearch(ctx, "如何实现并发通信?", 2,
        vectorstores.WithScoreThreshold(0.7),
    )
    if err != nil {
        log.Fatal(err)
    }
    for _, d := range docs {
        fmt.Println(d.PageContent, d.Metadata)
    }
}
Tip

生产环境向量库选型:已有 PostgreSQL 时优先 pgvector(无需新增基础设施);需要大规模检索或混合过滤时考虑 Milvus、Qdrant;本地实验用 Chroma 最快。

文档加载与切分

真实文档通常很长,必须切分为合适大小的片段(chunk)再入库。langchaingo 提供 documentloaderstextsplitter

package main

import (
    "context"

    "github.com/tmc/langchaingo/documentloaders"
    "github.com/tmc/langchaingo/schema"
    "github.com/tmc/langchaingo/textsplitter"
)

func indexFile(ctx context.Context, store interface {
    AddDocuments(context.Context, []schema.Document) ([]string, error)
}, path, content string) error {
    loader := documentloaders.NewText(content) // 另有 NewHTML、NewPDF 等

    // 递归字符切分:优先按段落、句子边界切分
    splitter := textsplitter.NewRecursiveCharacter(
        textsplitter.WithChunkSize(500),    // 每段约 500 字符
        textsplitter.WithChunkOverlap(50),  // 相邻段落重叠,保留上下文
    )

    docs, err := loader.LoadAndSplit(ctx, splitter)
    if err != nil {
        return err
    }

    // 为每个片段附加来源元数据,便于答案溯源
    for i := range docs {
        docs[i].Metadata = map[string]any{"file": path}
    }
    _, err = store.AddDocuments(ctx, docs)
    return err
}
Tip

切分参数经验值:chunkSize 取 300–800 字符,chunkOverlap 取 chunk 的 10%–15%。切分过小丢失语义,过大稀释相关性。Markdown/代码文档建议按标题或函数边界自定义切分。

检索问答链

chains.RetrievalQA 将"检索 → 填充 Prompt → 生成答案"封装为一条链:

package main

import (
    "context"
    "fmt"

    "github.com/tmc/langchaingo/chains"
    "github.com/tmc/langchaingo/llms/openai"
    "github.com/tmc/langchaingo/vectorstores"
    "github.com/tmc/langchaingo/vectorstores/chroma"
)

func main() {
    ctx := context.Background()

    store, err := chroma.New(
        chroma.WithChromaURL("http://localhost:8000"),
        chroma.WithOpenAIAPIKey(os.Getenv("OPENAI_API_KEY")),
        chroma.WithNameSpace("golang-docs"),
    )
    if err != nil {
        panic(err)
    }

    llm, err := openai.New()
    if err != nil {
        panic(err)
    }

    // 将向量存储包装为检索器,每次取 Top-3 相关片段
    qaChain := chains.NewRetrievalQAFromLLM(llm,
        vectorstores.ToRetriever(store, 3),
    )

    answer, err := chains.Call(ctx, qaChain, map[string]any{
        "query": "goroutine 和线程有什么区别?",
    })
    if err != nil {
        panic(err)
    }
    fmt.Println(answer["result"])
}

示例需 import "os"

完整的 RAG 服务骨架

将索引与问答拆分为两个接口,结合 HTTP 服务并发能力:

package main

import (
    "context"
    "encoding/json"
    "log"
    "net/http"
    "time"
)

type App struct {
    qaChain chains.Chain
}

// POST /api/index:离线写入文档
func (a *App) handleIndex(w http.ResponseWriter, r *http.Request) {
    var in struct {
        Content string `json:"content"`
    }
    if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    // ... loader + splitter + store.AddDocuments(略)
    w.WriteHeader(http.StatusAccepted)
}

// POST /api/ask:在线检索问答
func (a *App) handleAsk(w http.ResponseWriter, r *http.Request) {
    var in struct {
        Query string `json:"query"`
    }
    if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
    defer cancel()

    out, err := chains.Call(ctx, a.qaChain, map[string]any{"query": in.Query})
    if err != nil {
        http.Error(w, "检索问答失败", http.StatusBadGateway)
        return
    }
    _ = json.NewEncoder(w).Encode(map[string]any{"answer": out["result"]})
}

func main() {
    // 初始化 store、llm、qaChain(见上文)
    _ = chains.Chain(nil)
    app := &App{}

    http.HandleFunc("POST /api/index", app.handleIndex)
    http.HandleFunc("POST /api/ask", app.handleAsk)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

:::warning RAG 工程要点:

  • 检索质量决定答案质量:关注召回率(Top-K 是否包含相关片段)与排序效果。
  • 答案应附带引用来源(利用片段 Metadata),便于用户核验。
  • Embedding 模型与文档语言需匹配,中文场景选择中文友好的向量化模型。
  • 索引更新与问答共享向量库时注意并发写入,参考 sync 包。 :::

小结

  • RAG = 离线索引(加载 → 切分 → 向量化)+ 在线问答(检索 → 生成)。
  • vectorstores 统一了多后端接口,vectorstores.ToRetriever 将存储接入 RetrievalQA 链。
  • 切分参数与元数据溯源是 RAG 落地质量的关键工程细节。