Go 博客
使用 Go 构建基于大型语言模型的应用
随着大型语言模型 (LLM) 及其相关工具(如嵌入模型)的功能在过去一年中显著增强,越来越多的开发人员开始考虑将其集成到他们的应用程序中。
由于 LLM 通常需要专门的硬件和大量的计算资源,因此它们最常被打包成网络服务,提供 API 用于访问。这就是 OpenAI 或 Google Gemini 等领先 LLM 的 API 的工作方式;即使是像 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,并将答案返回给用户。
我们的演示使用的服务是
- Google Gemini API 用于 LLM 和嵌入模型。
- Weaviate 用于本地托管的向量数据库;Weaviate 是一个开源向量数据库,用 Go 实现。
替换这些服务为其他等效的服务应该非常简单。事实上,服务器的第二个和第三个变体就是关于这一点的!我们将从第一个变体开始,它直接使用这些工具。
直接使用 Gemini API 和 Weaviate
Gemini API 和 Weaviate 都有方便的 Go SDK(客户端库),我们的第一个服务器变体直接使用这些库。此变体的完整代码位于 此目录。
我们不会在这篇博文中复制整个代码,但以下是一些在阅读代码时需要注意的事项
结构:代码结构对于任何编写过 Go HTTP 服务器的人来说都很熟悉。Gemini 和 Weaviate 的客户端库被初始化,客户端存储在一个状态值中,该值被传递给 HTTP 处理程序。
路由注册:我们服务器的 HTTP 路由使用 Go 1.22 中引入的 路由增强功能 非常容易设置
mux := http.NewServeMux()
mux.HandleFunc("POST /add/", server.addDocumentsHandler)
mux.HandleFunc("POST /query/", server.queryHandler)
并发:我们服务器的 HTTP 处理程序通过网络访问其他服务并等待响应。这对 Go 来说不是问题,因为每个 HTTP 处理程序都在其自己的 goroutine 中并发运行。此 RAG 服务器可以处理大量并发请求,并且每个处理程序的代码都是线性和同步的。
批处理 API:由于 /add/
请求可能会提供大量要添加到知识库中的文档,因此服务器利用 批处理 API 进行嵌入 (embModel.BatchEmbedContents
) 和 Weaviate 数据库 (rs.wvClient.Batch
) 以提高效率。
使用 LangChain for Go
我们的第二个 RAG 服务器变体使用 LangChainGo 来完成相同的任务。
LangChain 是一个流行的 Python 框架,用于构建基于 LLM 的应用程序。LangChainGo 是其 Go 等价物。该框架提供了一些工具来使用模块化组件构建应用程序,并以通用 API 支持许多 LLM 提供商和向量数据库。这允许开发人员编写可能与任何提供商一起工作的代码,并且可以非常轻松地更改提供商。
此变体的完整代码位于 此目录。阅读代码时,您会注意到两件事
首先,它比之前的变体短一些。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 等项目。
下一篇文章:名称中的 (别名) 内容是什么?
上一篇文章:分享您对 Go 开发的反馈
博客索引