# 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 能访问你的私有数据。本地运行这套方案,适合对数据安全有要求、或者想控制成本的团队。