使用 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

可以看到接口输出:

使用 Golang 构建你的 LLM API
参考资料
[1]

Gin: https://github.com/gin-gonic/gin

[2]

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


原文始发于微信公众号(Go Official Blog):使用 Golang 构建你的 LLM API

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/268339.html

(0)
Java朝阳的头像Java朝阳

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!