Go 博客
使用 Go 构建 LLM 驱动的应用程序
随着在过去一年中,LLM(大型语言模型)及其相关工具(如嵌入模型)的功能显著增强,越来越多的开发人员正在考虑将 LLM 集成到他们的应用程序中。
由于 LLM 通常需要专用硬件和大量的计算资源,因此它们最常被打包成提供 API 访问的网络服务。领先的 LLM(如 OpenAI 或 Google Gemini)就是这样工作的;即使是像 Ollama 这样的本地 LLM 工具,也会将 LLM 包装在 REST API 中以供本地使用。此外,在应用程序中利用 LLM 的开发人员通常还需要诸如向量数据库之类的辅助工具,而这些工具也最常作为网络服务进行部署。
换句话说,LLM 驱动的应用程序与其他现代云原生应用程序非常相似:它们需要对 REST 和 RPC 协议、并发性和性能有出色的支持。而这些恰恰是 Go 擅长的领域,这使其成为编写 LLM 驱动应用程序的绝佳语言。
这篇博文将通过一个使用 Go 构建简单 LLM 驱动应用程序的示例。它首先描述了演示应用程序正在解决的问题,然后展示了应用程序的几个变体,它们都完成了相同的任务,但使用了不同的软件包来实现。本文演示的所有代码均在线提供。
一个用于问答的 RAG 服务器
一种常见的 LLM 驱动应用程序技术是 RAG(检索增强生成)。RAG 是定制 LLM 知识库以进行领域特定交互的最具可扩展性的方法之一。
我们将用 Go 构建一个RAG 服务器。这是一个提供两个操作给用户的 HTTP 服务器:
- 将文档添加到知识库
- 就知识库向 LLM 提问
在典型的实际场景中,用户会将大量文档添加到服务器,然后开始向其提问。例如,一家公司可以将 RAG 服务器的知识库填充内部文档,并利用它为内部用户提供 LLM 驱动的问答功能。
这是展示我们的服务器与外部世界交互的图示:

除了用户发送 HTTP 请求(上述两个操作)之外,服务器还与以下组件交互:
- 嵌入模型,用于计算提交的文档和用户问题的向量嵌入。
- 向量数据库,用于高效存储和检索嵌入。
- LLM,用于根据从知识库收集的上下文提问。
具体来说,服务器向用户公开两个 HTTP 端点:
/add/: POST {"documents": [{"text": "..."}, {"text": "..."}, ...]}
:提交一系列文本文档给服务器,以添加到其知识库。对于此请求,服务器将:
- 使用嵌入模型计算每个文档的向量嵌入。
- 将文档及其向量嵌入存储在向量数据库中。
/query/: POST {"content": "..."}
:提交一个问题给服务器。对于此请求,服务器将:
- 使用嵌入模型计算问题的向量嵌入。
- 使用向量数据库的相似性搜索来查找知识数据库中最相关的文档。
- 使用简单的提示工程,将第 (2) 步找到的最相关的文档作为上下文重新表述问题,然后将其发送给 LLM,并将 LLM 的答案返回给用户。
我们的演示使用的服务是:
- Google Gemini API,用于 LLM 和嵌入模型。
- Weaviate,一个本地托管的向量数据库;Weaviate 是一个开源向量数据库,用 Go 实现。
替换为其他等效服务应该非常简单。事实上,这正是服务器的第二种和第三种变体的目的!我们将从直接使用这些工具的第一种变体开始。
直接使用 Gemini API 和 Weaviate
Gemini API 和 Weaviate 都提供了方便的 Go SDK(客户端库),我们的第一个服务器变体直接使用了它们。此变体的完整代码在此目录中。
我们不会在本文档中复制全部代码,但请注意以下几点:
结构:代码结构对于任何用 Go 编写过 HTTP 服务器的人来说都会很熟悉。Gemini 和 Weaviate 的客户端库会得到初始化,并将客户端存储在一个传递给 HTTP 处理程序的 state 值中。
路由注册:使用 Go 1.22 中引入的路由增强功能,可以轻松设置服务器的 HTTP 路由。
mux := http.NewServeMux()
mux.HandleFunc("POST /add/", server.addDocumentsHandler)
mux.HandleFunc("POST /query/", server.queryHandler)
并发:我们的服务器的 HTTP 处理程序通过网络与其他服务进行通信并等待响应。这对于 Go 来说不是问题,因为每个 HTTP 处理程序都在自己的 goroutine 中并发运行。这个 RAG 服务器可以处理大量并发请求,每个处理程序的代码都是线性和同步的。
批量 API:由于 /add/
请求可能提供大量要添加到知识库的文档,因此服务器利用了嵌入(embModel.BatchEmbedContents
)和 Weaviate DB(rs.wvClient.Batch
)的批量 API 以提高效率。
使用 LangChain for Go
我们的第二个 RAG 服务器变体使用 LangChainGo 来完成相同的任务。
LangChain 是一个流行的 Python 框架,用于构建 LLM 驱动的应用程序。LangChainGo 是其 Go 版本。该框架提供了一些工具来构建由模块化组件组成的应用程序,并支持多种 LLM 提供商和向量数据库,具有通用 API。这使得开发人员可以编写与任何提供商兼容的代码,并轻松切换提供商。
此变体的完整代码在此目录中。阅读代码时,您会注意到两点:
首先,它比前一个变体要短一些。LangChainGo 负责将向量数据库的完整 API 包装成通用接口,因此初始化和处理 Weaviate 所需的代码更少。
其次,LangChainGo API 使切换提供商变得相当容易。假设我们想用另一个向量数据库替换 Weaviate;在之前的变体中,我们必须重写所有与向量数据库交互的代码以使用新 API。有了 LangChainGo 这样的框架,我们就不再需要这样做了。只要 LangChainGo 支持我们感兴趣的新向量数据库,我们应该只需要更改服务器中几行代码,因为所有数据库都实现了通用接口。
type VectorStore interface {
AddDocuments(ctx context.Context, docs []schema.Document, options ...Option) ([]string, error)
SimilaritySearch(ctx context.Context, query string, numDocuments int, options ...Option) ([]schema.Document, error)
}
使用 Genkit for Go
今年早些时候,Google 推出了Genkit for Go - 一个用于构建 LLM 驱动应用程序的新开源框架。Genkit 和 LangChain 有一些共同之处,但在其他方面有所不同。
与 LangChain 一样,它提供了可以由不同提供商(作为插件)实现的通用接口,因此更容易从一个提供商切换到另一个。然而,它并不试图规定不同的 LLM 组件如何交互;相反,它专注于生产特性,如提示管理和工程,以及带有集成开发工具的部署。
我们的第三个 RAG 服务器变体使用 Genkit for Go 来完成相同的任务。其完整代码在此目录中。
此变体与 LangChainGo 变体非常相似——它使用 LLM、嵌入器和向量数据库的通用接口,而不是直接的提供商 API,这使得切换更加容易。此外,使用 Genkit 将 LLM 驱动的应用程序部署到生产环境要容易得多;我们在变体中没有实现这一点,但如果您有兴趣,可以随时阅读文档。
总结 - Go 用于 LLM 驱动的应用程序
本文中的示例只是展示了使用 Go 构建 LLM 驱动的应用程序的可能性。它演示了如何用相对较少的代码构建一个强大的 RAG 服务器;最重要的是,这些示例由于 Go 的一些基本特性,具备了相当程度的生产就绪性。
处理 LLM 服务通常意味着向网络服务发送 REST 或 RPC 请求,等待响应,然后根据该响应向其他服务发送新请求,依此类推。Go 在所有这些方面都表现出色,提供了强大的工具来管理并发和协调网络服务的复杂性。
此外,Go 作为云原生语言的出色性能和可靠性使其成为实现 LLM 生态系统更基础构建块的自然选择。例如,请参阅 Ollama、LocalAI、Weaviate 或 Milvus 等项目。
下一篇文章:别名(Alias)的含义?
上一篇文章:分享您对 Go 开发的反馈
博客索引