用 Go 搭 RAG 管道:从文档解析到向量检索

📅 2026/7/19 16:29:19
用 Go 搭 RAG 管道:从文档解析到向量检索
一个做企业内部知识库问答的项目技术选型阶段被问到用 Go 做 RAG 是不是自找麻烦——Python 有 LangChain、LlamaIndex生态更成熟。这话不假但团队基础设施就是 Go 栈再加一套 Python 来跑 RAG意味着多维护一条部署线、多学一个语言生态还得解决跨语言调用的延迟和兼容性。实际跑下来结论比预想的好Go 标准库加上两三个外部依赖能搭出一个生产可用的 RAG 管道。这篇文章拆解每个环节的工程选择。RAG 管道的基本结构RAG 的核心链条不复杂文档 → 切块 → 向量化 → 存储 → 检索 → 拼接提示 → 生成回答。每个环节的 Go 实现都有不止一种走法。文档PDF/Word/Markdown→ 文本提取 → 分块 ↓ Embedding APIOpenAI / 本地模型 ↓ 向量数据库pgvector / Weaviate→ 相似度检索 ↓ 拼接上下文 原始问题 → LLM 生成最终回答文档解析与分块RAG 的第一步是把手里的文档转成纯文本。Go 在这块的库不算多但核心场景够用。PDF 解析github.com/ledongthuc/pdf和github.com/unidoc/unipdf商业许可证。开源方案对纯文本 PDF 够用遇到扫描件需要 OCR 时还是得走外部服务。Word 文档github.com/unidoc/unioffice或者github.com/nguyenthenguyen/docx。docx 本质是 ZIP 包里的 XML用archive/zipencoding/xml也能自己提取文本但 docx 的段落格式和表格布局处理起来需要额外工作。Markdown 处理github.com/gomarkdown/markdown可以解析成 AST然后按标题层级决定分块边界。这是个好策略——按 # 或 ## 切出来的块天然有语义边界比固定 token 数切割更合理。import github.com/gomarkdown/markdown/ast func ChunkByHeading(doc ast.Node) []Chunk { var chunks []Chunk // 遍历 AST遇到 Heading 节点就开始一个新块 // 后续的段落、列表、代码块都属于当前块 return chunks }固定 token 数切分是兜底方案适合没有明确标题结构的文档func ChunkByTokens(text string, maxTokens int) []string { // 按空格和标点估算 token 数每个 token ~4 字符 words : strings.Fields(text) var chunks []string for i : 0; i len(words); i maxTokens { end : i maxTokens if end len(words) { end len(words) } chunks append(chunks, strings.Join(words[i:end], )) } return chunks }两种方法结合起来先用标题切然后对过长的段落做 token 数二次切分。Embedding调用模型接口文本转向量这一步大部分团队走的是调用外部 Embedding API——OpenAI 的 text-embedding-3-small 或者本地部署的 bge-m3 模型。Go 标准库的net/http处理这类 API 调用已经很稳。并发请求时用sync.WaitGroup或 errgroup 控制并行度func BatchEmbed(ctx context.Context, texts []string, batchSize int) ([][]float32, error) { g, ctx : errgroup.WithContext(ctx) g.SetLimit(10) // 限制并发数 results : make([][]float32, len(texts)) for i : 0; i len(texts); i batchSize { batch : texts[i:min(ibatchSize, len(texts))] start : i g.Go(func() error { embeds, err : callEmbeddingAPI(ctx, batch) if err ! nil { return err } results[start] embeds returnnil }) } if err : g.Wait(); err ! nil { returnnil, err } return flatten(results), nil }errgroup 的SetLimit在这里是关键——Embedding API 通常有速率限制不加控制直接全量并发会触发 429。向量存储与检索向量数据库的选择直接影响检索阶段的延迟和召回率。pgvector如果团队已经在用 PostgreSQLpgvector 是最简单的选择——不需要额外运维一套基础设施。Go 这边用github.com/pgvector/pgvector-go配合database/sql或pgx使用。import github.com/pgvector/pgvector-go type Document struct { ID int64 Content string Embedding pgvector.Vector } // 插入向量 _, err : db.Exec(ctx, INSERT INTO documents (content, embedding) VALUES ($1, $2), doc.Content, pgvector.NewVector(embedding)) // 相似度检索 rows, err : db.Query(ctx, SELECT content FROM documents ORDER BY embedding - $1 LIMIT 5, pgvector.NewVector(queryEmbedding))-是 L2 距离是余弦距离。对于 RAG 场景余弦距离通常是更好的选择。Weaviate如果向量量级到了百万以上或者需要混合搜索向量 关键词 BM25 过滤条件Weaviate 的 Go 客户端 v4.0 提供了完整的 GraphQL 接口。从工程角度看一个务实的起步策略是先用 pgvector 跑起来——只要团队已经用了 PostgreSQL不需要新引入任何中间件。当检索延迟成为瓶颈后再评估独立向量数据库。检索增强生成用户提问进来后流程分三步把问题向量化在向量库中找到最相关的 K 个块把块内容和问题一起发给 LLMtype RAGPipeline struct { Embedder func(ctx context.Context, text string) ([]float32, error) Retriever func(ctx context.Context, query []float32, topK int) ([]string, error) Generator func(ctx context.Context, prompt string) (string, error) } func (p *RAGPipeline) Ask(ctx context.Context, question string) (string, error) { // 1. 问题向量化 queryVec, err : p.Embedder(ctx, question) if err ! nil { return, err } // 2. 检索相关文档 chunks, err : p.Retriever(ctx, queryVec, 5) if err ! nil { return, err } // 3. 拼接提示词 context : strings.Join(chunks, \n---\n) prompt : fmt.Sprintf(基于以下上下文回答问题。如果上下文中没有相关信息请明确说明。 上下文 %s 问题%s, context, question) // 4. 生成回答 return p.Generator(ctx, prompt) }这个结构的好处是每个环节都可以独立替换——想把 Embedder 从 OpenAI 换成本地模型改一行初始化代码就行。Go 做 RAG 的取舍用 Go 做 RAG 有几个现实问题。Embedding 和生成模型的本地部署是一个。如果团队要求全部内网运行不用外部 API那需要在本机跑 ONNX 或 llama.cpp 的推理进程Go 这边通过 gRPC 或 HTTP 调用。跟 Python 生态的直接调用相比多了一层 IPC但换来的是语言栈统一。中文分词精度也是一个。Go 的github.com/go-ego/gse和github.com/yanyiwu/gojieba能满足基本需求但遇到专业领域术语时比如熔断降级应该作为一个语义单元而不是切成熔断降级需要准备自定义词典。这个问题不只 Go 有Python 的 jieba 同样需要领域词典。批量索引的性能差距则是在对比中才发现的。给一万份文档生成 Embedding 时Go 的并发模型比 Python 的 asyncio 更直观——直接用 goroutine errgroup 控制并发不需要学习 async/await、事件循环和协程调度这些概念。十万文档量级下Go 管道的构建速度比同机的 Python 实现快了约 3 倍。起步时的推荐路径pgvector 做向量存储、OpenAI Embedding API 做向量化、Go 标准库 http 做 LLM 调用。这三样都是团队如果已经用 Go 就已有的基础设施。等检索质量或延迟成为瓶颈后再逐步换入本地模型、独立向量库或缓存层。如果你们团队正在评估 RAG 技术栈给 Go 一个机会——它在管道编排和并发控制上有天然优势而这些恰恰是 RAG 系统最核心的非功能性需求。