所有文章 > API开发 > 使用 Golang 构建你的 LLM API

使用 Golang 构建你的 LLM API

大语言模型,像 ChatGPT, Llama 等已经席卷全球,从上图的数据可以看出,ChatGPT 花了 5 天时间就达到了 100 万用户。而 Netflix 则花了近 4 年的时间。本文将使用 Gin 和 Langchain 教你快速构建一套 LLM API。

Gin

Gin[1] 是一个用于使用GoLang构建API的现代、快速的Web框架。它被设计为易于使用、高效且性能出色,利用了GoLang并发的强大能力,以实现极高的吞吐量。

LangChain

LangChain[2] 是一个用于开发由语言模型驱动的应用程序的框架。它旨在让开发者轻松地连接到一个 LLM,并使用户能够为 LLM 提供额外的上下文。简单来说,Langchain使LLM模型能够基于在线、文档或其他数据源中最新的信息生成响应。

准备工作

首先确保已经安装了 Golang 的开发环境,其次需要下面几个依赖包:

  • Gin
  • LangChainGo
  • UUID
  • Exp
$ go get github.com/gin-gonic/gin
$ go get github.com/tmc/langchaingo
$ go get github.com/google/uuid
$ go get golang.org/x/exp@v0.0.0-20230713183714-613f0c0eb8a1

构建 API

Request 和 Response (routes/structs.go)

GenerateVacationIdeaRequest 是用户将提供给我们的内容,以便我们为他们创建 vacation idea。我们希望用户告诉我们他们喜欢的季节、他们可能有的任何爱好,以及他们的度假预算是多少。我们可以在后面将这些输入提供给 LLM。

GenerateVacationIdeaResponse 是我们将返回给用户的内容,表示想法正在生成中。Langchain 可能需要一些时间来生成响应,我们不希望用户永远等待他们的HTTP调用返回。因此,我们将使用 goroutines(稍后会详细介绍!),用户可以在几秒钟后检查想法是否已完成。

GenerateVacationIdeaResponse 反映了这一点,包含两个字段:

  • 一个 ID 字段,允许他们查询我们的 API 以获取项目的 UUID
  • 一个 completed 字段,告诉用户结果是否已经生成

GetVacationIdeaResponse 是当用户查询想法或其状态时我们将返回给用户的内容。几秒钟后,用户会说 “嗯,想法已经完成了吗?”然后可以查询我们的API。GetVacationIdeaResponse 具有与 GenerateVacationIdeaResponse 相同的字段,但添加了一个想法字段,当生成完成时 LLM 将填写该字段。

type GenerateVacationIdeaRequest struct {
FavoriteSeason string `json:"favorite_season"`
Hobbies []string `json:"hobbies"`
Budget int `json:"budget"`
}

type GenerateVacationIdeaResponse struct {
Id uuid.UUID `json:"id"`
Completed bool `json:"completed"`
}

type GetVacationIdeaResponse struct {
Id uuid.UUID `json:"id"`
Completed bool `json:"completed"`
Idea string `json:"idea"`
}

API Routing (routes/vacation.go

现在我们的请求和响应模式已经确定,我们可以写路由了。

GetVacationRouter 函数接受一个gin路由器作为输入,并为其添加一个新的路由器组,路径前缀为 /vacation。因此,我们添加到路由器的任何端点都将具有 /vacation前缀。然后我们添加两个端点:

  • POST /create 用于为我们创建一个用户的想法
  • GET /:id 根据ID获取一个想法

/create 端点将启动一个goroutine,调用 langchain 和 openAI。它将返回一个 GenerateVacationIdeaResponse 给调用者,以便他们稍后可以检查其状态。他们可以通过 /:id 端点来检查该想法的状态。这将返回一个 GetVacationIdeaResponse。如果想法已经完成生成,它将包含一个id、一个想法,并且 completed 标志将设置为true。否则,它将包含一个id、一个空想法,并且 completed 标志将设置为 false。

package routes

import (
"net/http"

"github.com/afoley587/52-weeks-of-projects/07-golang-gin-langchain/chains"
"github.com/google/uuid"

"github.com/gin-gonic/gin"
)

func generateVacation(r GenerateVacationIdeaRequest) GenerateVacationIdeaResponse {
// First, generate a new UUID for the idea
id := uuid.New()

// Then invoke the GeneateVacationIdeaChange method of the chains package
// passing through all of the parameters from the user
go chains.GeneateVacationIdeaChange(id, r.Budget, r.FavoriteSeason, r.Hobbies)
return GenerateVacationIdeaResponse{Id: id, Completed: false}
}

func getVacation(id uuid.UUID) (GetVacationIdeaResponse, error) {
// Search the chains database for the ID requested by the user
v, err := chains.GetVacationFromDb(id)

// If the ID didn't exist, handle the error
if err != nil {
return GetVacationIdeaResponse{}, err
}

// Otherwise, return the vacation idea to the caller
return GetVacationIdeaResponse{Id: v.Id, Completed: v.Completed, Idea: v.Idea}, nil
}

func GetVacationRouter(router *gin.Engine) *gin.Engine {

// Add a new router group to the gin router
registrationRoutes := router.Group("/vacation")

// Handle the POST to /create
registrationRoutes.POST("/create", func(c *gin.Context) {
var req GenerateVacationIdeaRequest
err := c.BindJSON(&req)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"message": "Bad Request",
})
} else {
c.JSON(http.StatusOK, generateVacation(req))
}
})

// Handle the GET to /:id
registrationRoutes.GET("/:id", func(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))

if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"message": "Bad Request",
})
} else {
resp, err := getVacation(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"message": "Id Not Found",
})
} else {
c.JSON(http.StatusOK, resp)
}
}
})

// Return the updated router
return router
}

现在我们可以将路由添加到我们的 API 中了。我们只需要实例化一个 Gin engine,将我们的路由添加到其中,然后运行即可。

import (
"github.com/afoley587/52-weeks-of-projects/07-golang-gin-langchain/routes"
"github.com/gin-gonic/gin"
)

func main() {
r := gin.Default()
routes.GetVacationRouter(r)
r.Run()
}

构建 Chain

现在我们已经为 API 设置了场景。现在,我们需要一种与我们的 LLM 进行交流的方法(或者至少向它提问)。

让我们定义一个“数据库”来存储所有生成的想法。Vacations 是我们的度假“数据库”。我在数据库中加了引号,因为这只是一个在整个包中共享的切片。理想情况下,这应该是一种更持久、更稳定、更可扩展的存储形式,但是本文仅做演示,切片就够用了。Vacations 是一个Vacation 结构体的切片。我们的 Vacation 结构体只是一个数据持有者。它保存了正在进行和最终的度假对象。它具有我们之前讨论过的GetVacationIdeaResponse相同的字段,但我更喜欢将它们分开,这样可以更容易地解耦这些代码片段。

我们需要向想要使用这个包的人提供两种方法:

  1. 提供一种方式,让调用者从我们的“数据库”中检索度假信息
  2. 提供一种方式,让调用者请求生成新的度假想法 为了解决第一点,我们将编写GetVacationFromDb(id uuid.UUID)函数。该函数将获取度假的ID。然后它尝试在地图中找到度假,并且如果存在,它将返回度假对象。否则,如果ID不存在于数据库中,则返回错误。

接下来,我们需要一些实际创建想法并将它们存储到我们的数据库中的东西。

GeneateVacationIdeaChange是我们最终开始调用langchain的地方。它接受几个参数:

  • 从我们的路由器传入的 UUID。我们将使用这个 UUID 来保存我们的度假数据库中的结果。
  • 用户首选的季节。我们将将其作为参数传递给 langchain 链。
  • 用户喜欢的爱好。我们将将其作为参数传递给 langchain 链。
  • 用户的财务预算。我们将将其作为参数传递给 langchain 链。

首先,我们需要实例化我们的 LLM 模型(这里我们使用 openai )。然后我们需要创建一些 prompts。我们创建一个系统提示以传递给 LLM。系统提示是应用程序或系统提供的指令或信息,用于指导对话。系统提示有助于设置上下文和指导 LLM 如何响应人类提示。

一个人类消息和模板遵循着相同的思路。我们可以把它想象成一个聊天应用程序。系统提示有助于设置聊天机器人。人类提示是用户会问它的内容。

现在模板已经建立,我们可以通过首先创建聊天提示模板来创建聊天提示。为此,我们使用 FormatMessages 方法将用户提供的值插入到我们的模板中。现在所有内容都以字符串格式进行了模板化。我们将创建LLM消息内容,这是我们的LLM将期望作为输入的内容。最后,我们可以使用 GenerateContent 调用我们的 LLM。GenerateContent 的输出将是从 OpenAI API 返回的结果,但我们只关心 LLM 生成的内容。内容是 LLM 生成的字符串响应,类似于 ChatGPT 窗口中返回的响应。

package chains

import (
"context"
"errors"
"log"
"strings"

"github.com/google/uuid"
"github.com/tmc/langchaingo/llms"
"github.com/tmc/langchaingo/llms/openai"
"github.com/tmc/langchaingo/prompts"
"golang.org/x/exp/slices"
)

type Vacation struct {
Id uuid.UUID `json:"id"`
Completed bool `json:"completed"`
Idea string `json:"idea"`
}

var Vacations []*Vacation

func GetVacationFromDb(id uuid.UUID) (Vacation, error) {
// Use the slices package to find the index of the object with
// matching ID in the database. If it does not exist, this will return
// -1
idx := slices.IndexFunc(Vacations, func(v *Vacation) bool { return v.Id == id })

// If the ID didn't exist, return an error and let the caller
// handle it
if idx < 0 {
return Vacation{}, errors.New("ID Not Found")
}

// Otherwise, return the Vacation object
return *Vacations[idx], nil
}

func GeneateVacationIdeaChange(id uuid.UUID, budget int, season string, hobbies []string) {
log.Printf("Generating new vacation with ID: %s", id)

// Create a new vacation object and add it to our database. Initially,
// the idea field will be empty and the completed flag will be false
v := &Vacation{Id: id, Completed: false, Idea: ""}
Vacations = append(Vacations, v)

// Create a new OpenAI LLM Object
ctx := context.Background()
llm, err := openai.New()
if err != nil {
log.Printf("Error: %v", err)
return
}

// Create a system prompt with the season, hobbies, and budget parameters
// Helps tell the LLM how to act / respond to queries
system_message_prompt_string := "You are an AI travel agent that will help me create a vacation idea.\n" +
"My favorite season is {{.season}}.\n" +
"My hobbies include {{.hobbies}}.\n" +
"My budget is {{.budget}} dollars.\n"
system_message_prompt := prompts.NewSystemMessagePromptTemplate(system_message_prompt_string, []string{"season", "hobbies", "dollars"})

// Create a human prompt with the request that a human would have
human_message_prompt_string := "write a travel itinerary for me"
human_message_prompt := prompts.NewHumanMessagePromptTemplate(human_message_prompt_string, []string{})

// Create a chat prompt consisting of the system messages and human messages
// At this point, we will also inject the values into the prompts
// and turn them into message content objects which we can feed through
// to our LLM
chat_prompt := prompts.NewChatPromptTemplate([]prompts.MessageFormatter{system_message_prompt, human_message_prompt})

vals := map[string]any{
"season": season,
"budget": budget,
"hobbies": strings.Join(hobbies, ","),
}
msgs, err := chat_prompt.FormatMessages(vals)

if err != nil {
log.Printf("Error: %v", err)
return
}

content := []llms.MessageContent{
llms.TextParts(msgs[0].GetType(), msgs[0].GetContent()),
llms.TextParts(msgs[1].GetType(), msgs[1].GetContent()),
}

// Invoke the LLM with the messages which
completion, err := llm.GenerateContent(ctx, content)

if err != nil {
log.Printf("Error: %v", err)
return
}
v.Idea = completion.Choices[0].Content
v.Completed = true

log.Printf("Generation for %s is done!", v.Id)
}

Running And Testing

所有的组件都已经构建好了,让我们运行它吧!让我们打开两个终端:

  • 一个用来运行 API
  • 一个用来对 API 进行 cURL 调用在第一个终端中,让我们运行我们的 API:
# 导入你的 openAI API Key
$ export OPENAI_API_KEY=sk...
$ go run main.go

然后测试:

$ curl -X POST -H"Content-type: application/json" \
-d'{"favorite_season": "summer", "hobbies": ["surfing","running"], "budget":1000}' \
http://localhost:8080/vacation/create

可以看到接口输出:

参考资料[1]

Gin: https://github.com/gin-gonic/gin[2]

LangChain: https://www.langchain.com/

文章转自微信公众号@Go Official Blog

#你可能也喜欢这些API文章!