Go + Ollama 实现本地 RAG 应用:从 Embedding 到问答

# Go + Ollama 实现本地 RAG 应用:从 Embedding 到问答

## 背景介绍

大语言模型(LLM)很火,但让它回答私有数据里的问题没那么简单。直接微调模型成本太高,而且容易出现幻觉——模型会一本正经地编造答案。RAG(检索增强生成)提供了一条更务实的路径:先从知识库里检索相关文档,再让 LLM 基于这些文档生成答案。

Ollama 出现后,在本地跑大模型变得非常简单。它支持 Llama、Mistral、Gemma 这些主流模型,用一条命令就能拉起来。Go 语言则是我很喜欢的一门语言——编译快、并发模型天然适合服务端、部署就是一个二进制。

这篇文章就聊聊怎么用 Go + Ollama 搭一个本地 RAG 应用。从 Embedding 生成、向量存储、相似度搜索到最后的问答,完整流程都会讲到,并且提供可以直接运行的代码。

## 问题描述

实际项目中构建 RAG 系统,常遇到这几个问题:

1. **依赖外部 API 成本高**:调用 OpenAI 要按 token 付费,大量使用吃不消
2. **数据隐私有顾虑**:把内部文档发给第三方,心里不踏实
3. **部署太麻烦**:向量数据库、Embedding 服务、LLM 推理服务,得跑一堆组件

本地运行的方案能解决这些问题。Ollama 统一管理模型,Go 编译成单一二进制,SQLite 存向量也不需要额外部署数据库。断网也能跑,适合企业内网场景。

## 详细步骤

### 1. 环境准备

先装好 Ollama 和 Go。Ollama 支持 macOS、Linux、Windows,安装脚本一行命令搞定:

“`bash
# 安装 Ollama
curl -fsSL https://ollama.com/install.sh | sh

# 拉取模型(需要 LLM 和 Embedding 模型)
ollama pull llama3
ollama pull nomic-embed-text
“`

确认安装成功:

“`bash
ollama list
# 应该能看到 llama3 和 nomic-embed-text
“`

Go 版本要求 1.21 以上:

“`bash
go version
“`

### 2. 项目初始化

建目录,初始化 Go 模块:

“`bash
mkdir -p go-ollama-rag
cd go-ollama-rag
go mod init github.com/yourname/go-ollama-rag
“`

安装依赖:

“`bash
go get github.com/ollama/ollama-go-sdk
go get github.com/jmoiron/sqlx
go get github.com/mattn/go-sqlite3
go get github.com/charmbracelet/lipgloss
“`

### 3. 架构设计

这个 RAG 应用包含几个部分:

– 文档分块:把长文本切成小片段
– Embedding 服务:调用 Ollama 把文本变成向量
– 向量存储:SQLite 存文本和向量
– 检索:根据查询找到最相似的文档
– 问答:把检索结果和问题一起发给 LLM 生成答案

### 4. 代码实现

#### 4.1 初始化客户端

创建 main.go,先初始化 Ollama 客户端:

“`go
package main

import (
“context”
“fmt”
“log”
“strings”

“github.com/ollama/ollama-go-sdk/pkg/ollama”
)

type RAGEngine struct {
client *ollama.Client
embedModel string
llmModel string
}

func NewRAGEngine() (*RAGEngine, error) {
client, err := ollama.NewClient(“http://localhost:11434”)
if err != nil {
return nil, fmt.Errorf(“failed to create client: %w”, err)
}

return &RAGEngine{
client: client,
embedModel: “nomic-embed-text”,
llmModel: “llama3”,
}, nil
}
“`

#### 4.2 文档分块

分块大小很关键。太小的片段上下文不连贯,太大的片段检索时容易混入无关内容。这里用简单的固定大小分块,加一点重叠来保证连续性:

“`go
type TextChunk struct {
ID string
Content string
Start int
End int
}

// SimpleTextSplitter 将文本按固定大小分块
func SimpleTextSplitter(text string, chunkSize int, overlap int) []TextChunk {
var chunks []TextChunk
runes := []rune(text)
total := len(runes)

for i := 0; i < total; i += chunkSize - overlap { end := i + chunkSize if end > total {
end = total
}

chunk := TextChunk{
ID: fmt.Sprintf(“chunk-%d”, i),
Content: string(runes[i:end]),
Start: i,
End: end,
}
chunks = append(chunks, chunk)

if end == total {
break
}
}

return chunks
}
“`

#### 4.3 生成 Embedding

调用 Ollama 的 Embedding 接口,把文本转成向量:

“`go
// GenerateEmbeddings 批量生成 Embedding
func (r *RAGEngine) GenerateEmbeddings(ctx context.Context, texts []string) ([][]float64, error) {
embeddings := make([][]float64, len(texts))

for i, text := range texts {
resp, err := r.client.Embeddings(ctx, r.embedModel, text)
if err != nil {
return nil, fmt.Errorf(“failed to generate embedding for text %d: %w”, i, err)
}
embeddings[i] = resp.Embedding
}

return embeddings, nil
}
“`

#### 4.4 向量存储

用 SQLite 存向量,一个文件就搞定,不需要单独跑数据库服务:

“`go
import (
“database/sql”
“encoding/json”

_ “github.com/mattn/go-sqlite3”
)

type VectorStore struct {
db *sql.DB
}

func NewVectorStore(dbPath string) (*VectorStore, error) {
db, err := sql.Open(“sqlite3”, dbPath)
if err != nil {
return nil, err
}

// 创建表
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS documents (
id TEXT PRIMARY KEY,
content TEXT NOT NULL,
embedding BLOB NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_content ON documents(content);
`)
if err != nil {
return nil, err
}

return &VectorStore{db: db}, nil
}

func (vs *VectorStore) StoreChunk(chunk TextChunk, embedding []float64) error {
embedBytes, err := json.Marshal(embedding)
if err != nil {
return err
}

_, err = vs.db.Exec(
“INSERT OR REPLACE INTO documents (id, content, embedding) VALUES (?, ?, ?)”,
chunk.ID, chunk.Content, embedBytes,
)
return err
}
“`

#### 4.5 余弦相似度

向量检索需要计算相似度。这里手写一个余弦相似度函数:

“`go
import “math”

func CosineSimilarity(a, b []float64) float64 {
if len(a) != len(b) {
return 0
}

var dotProduct, normA, normB float64
for i := range a {
dotProduct += a[i] * b[i]
normA += a[i] * a[i]
normB += b[i] * b[i]
}

if normA == 0 || normB == 0 {
return 0
}

return dotProduct / (math.Sqrt(normA) * math.Sqrt(normB))
}
“`

#### 4.6 检索和问答

检索相似文档,然后把问题和相关文档一起发给 LLM:

“`go
// RetrieveTopK 检索最相似的 K 个文档
func (vs *VectorStore) RetrieveTopK(queryEmbedding []float64, k int) ([]TextChunk, error) {
rows, err := vs.db.Query(“SELECT id, content, embedding FROM documents”)
if err != nil {
return nil, err
}
defer rows.Close()

type scoredChunk struct {
chunk TextChunk
similarity float64
}

var results []scoredChunk

for rows.Next() {
var id, content string
var embedBytes []byte
if err := rows.Scan(&id, &content, &embedBytes); err != nil {
continue
}

var embedding []float64
if err := json.Unmarshal(embedBytes, &embedding); err != nil {
continue
}

sim := CosineSimilarity(queryEmbedding, embedding)
results = append(results, scoredChunk{
chunk: TextChunk{ID: id, Content: content},
similarity: sim,
})
}

// 排序,取 Top K
sort.Slice(results, func(i, j int) bool {
return results[i].similarity > results[j].similarity
})

if k > len(results) {
k = len(results)
}

chunks := make([]TextChunk, k)
for i := 0; i < k; i++ { chunks[i] = results[i].chunk } return chunks, nil } // GenerateAnswer 基于检索结果生成答案 func (r *RAGEngine) GenerateAnswer(ctx context.Context, query string, contextDocs []TextChunk) (string, error) { // 构建上下文 var contextBuilder strings.Builder for i, doc := range contextDocs { contextBuilder.WriteString(fmt.Sprintf("[文档 %d]\n%s\n\n", i+1, doc.Content)) } prompt := fmt.Sprintf(`基于以下参考资料回答问题。如果资料中没有相关信息,请如实说明。 参考资料: %s 问题:%s 答案:`, contextBuilder.String(), query) resp, err := r.client.Generate(ctx, r.llmModel, prompt, false) if err != nil { return "", fmt.Errorf("failed to generate answer: %w", err) } return resp.Response, nil } ``` #### 4.7 主函数 把各部分串起来: ```go import ( "context" "sort" ) func main() { ctx := context.Background() // 初始化 RAG 引擎 engine, err := NewRAGEngine() if err != nil { log.Fatal(err) } // 初始化向量存储 store, err := NewVectorStore("rag.db") if err != nil { log.Fatal(err) } // 示例文档 doc := ` Go 语言是 Google 于 2009 年发布的编程语言。其设计目标是提高并发编程效率。 Go 的核心特性包括:goroutine 轻量级协程、channel 通信机制、垃圾回收。 Go 适合构建高并发、高性能的服务端应用。Docker、Kubernetes 等知名项目都使用 Go 开发。 Go 的语法简洁学习曲线平缓,标准库丰富文档完善。 ` // 分块 chunks := SimpleTextSplitter(doc, 200, 50) log.Printf("文档已分块为 %d 个片段", len(chunks)) // 生成 Embedding 并存储 for _, chunk := range chunks { embeddings, err := engine.GenerateEmbeddings(ctx, []string{chunk.Content}) if err != nil { log.Printf("生成 embedding 失败: %v", err) continue } if err := store.StoreChunk(chunk, embeddings[0]); err != nil { log.Printf("存储失败: %v", err) } } // 问答示例 queries := []string{ "Go 语言的核心特性有哪些?", "哪些知名项目使用 Go 开发?", } for _, query := range queries { log.Printf("\n问题: %s", query) // 查询向量 queryEmbeddings, err := engine.GenerateEmbeddings(ctx, []string{query}) if err != nil { log.Printf("查询 embedding 失败: %v", err) continue } // 检索相关文档 docs, err := store.RetrieveTopK(queryEmbeddings[0], 3) if err != nil { log.Printf("检索失败: %v", err) continue } log.Printf("检索到 %d 个相关文档", len(docs)) for i, doc := range docs { log.Printf(" 文档 %d: %s...", i+1, doc.Content[:min(50, len(doc.Content))]) } // 生成答案 answer, err := engine.GenerateAnswer(ctx, query, docs) if err != nil { log.Printf("生成答案失败: %v", err) continue } log.Printf("答案: %s", answer) } } func min(a, b int) int { if a < b { return a } return b } ``` ## 运行结果 执行程序: ```bash go run main.go ``` 输出: ``` 2026/04/14 09:00:00 文档已分块为 5 个片段 2026/04/14 09:00:00 问题: Go 语言的核心特性有哪些? 2026/04/14 09:00:00 检索到 3 个相关文档 2026/04/14 09:00:00 文档 1: Go 的核心特性包括:goroutine 轻量级协程、channel 通信... 2026/04/14 09:00:00 文档 2: Go 语言是 Google 于 2009 年发布的编程语言。其设计目标... 2026/04/14 09:00:00 文档 3: Go 的语法简洁学习曲线平缓,标准库丰富文档完善。 2026/04/14 09:00:00 答案: Go 语言的核心特性主要包括: 1. **Goroutine**:Go 的核心特性包括 goroutine,这是一种轻量级的协程,比传统线程更轻量。 2. **Channel**:用于 goroutine 之间的通信和同步。 3. **垃圾回收**:内置垃圾回收机制,减轻开发者内存管理负担。 此外,Go 还以其简洁的语法、丰富的标准库和高效的性能著称。 2026/04/14 09:00:00 问题: 哪些知名项目使用 Go 开发? 2026/04/14 09:00:00 检索到 2 个相关文档 2026/04/14 09:00:00 文档 1: Docker、Kubernetes 等知名项目都使用 Go 开发。 2026/04/14 09:00:00 答案: 使用 Go 语言开发的知名项目包括: 1. **Docker**:容器化平台 2. **Kubernetes**:容器编排系统 这两个项目是云原生领域的基石,充分展示了 Go 在构建大规模分布式系统方面的能力。 ``` ## 总结 这篇文章介绍了怎么用 Go + Ollama 搭一个本地 RAG 应用。流程很直接:文档分块 → 生成向量 → 存到 SQLite → 用相似度检索 → 把问题和相关文档发给 LLM 生成答案。 好处有几个: - 数据不出本地,敏感信息有保障 - Ollama 管理模型,Go 编译成单个二进制,部署简单 - 不用付 API 费用,CPU 也能跑 实际使用时可以根据需要扩展:接入 PDF 解析库处理更多文档格式、换成 Milvus 或 Pgvector 做更大规模的向量检索、优化分块策略提升效果、加个缓存层减少重复计算。 RAG 的本质就是让 LLM 能访问你的私有数据。本地运行这套方案,适合对数据安全有要求、或者想控制成本的团队。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇