开发个人Go-ChatGPT--5 模型管理 (一)

开发个人Go-ChatGPT--5 模型管理 (一)

背景

开发一个chatGPT的网站,后端服务如何实现与大模型的对话?是整个项目中开发困难较大的点。

如何实现上图的聊天对话功能?在开发后端的时候,如何实现stream的响应呢?本文就先介绍后端的原理,逐步攻克这个课题。

环境部署

  • 启动ollamadocker run -d -p 3000:8080 -p 11434:11434 -v ollama:/root/.ollama -v open-webui:/app/backend/data --name open-webui --restart always ollama/ollama

  • ollama 下载对话模型: docker exec -it open-webui ollama run gemma:2b

    bash 复制代码
    pulling manifest 
    pulling c1864a5eb193... 100% ▕████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏ 1.7 GB                         
    pulling 097a36493f71... 100% ▕████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏ 8.4 KB                         
    pulling 109037bec39c... 100% ▕████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏  136 B                         
    pulling 22a838ceb7fb... 100% ▕████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏   84 B                         
    pulling 887433b89a90... 100% ▕████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏  483 B                         
    verifying sha256 digest 
    writing manifest 
    removing any unused layers 
    success

Stream reponse

前端

svelte 复制代码
        ....
        const [res, controller] = await generateChatCompletion(localStorage.token, {
            model: model,
            messages: messagesBody,
            options: {
                ...($settings.options ?? {})
            },
            format: $settings.requestFormat ?? undefined,
            keep_alive: $settings.keepAlive ?? undefined,
            docs: docs.length > 0 ? docs : undefined
        });

        if (res && res.ok) {
            console.log('controller', controller);

            const reader = res.body
                .pipeThrough(new TextDecoderStream())
                .pipeThrough(splitStream('\n'))
                .getReader();
        ...

ollamaopen-webui 前端项目实现和人类一样沟通的方法,使用的是stream监听 messages事件收到的响应,保持长连接的状态,逐渐将收到的消息显示到前端,直到后端响应结束。

后端

  • gin.Stream
go 复制代码
...
    c.Stream(func(w io.Writer) bool {
        select {
        case msg, ok := <-msgChan:
            if !ok {
                // 如果msgChan被关闭,则结束流式传输
                return false
            }
            fmt.Print(msg)
            // 流式响应,发送给 messages 事件,和前端进行交互
            c.SSEvent("messages", msg)
            return true
        case <-c.Done():
            // 如果客户端连接关闭,则结束流式传输
            return false
        }
    })
...
  • ollama 响应
go 复制代码
...
    // llms.WithStreamingFunc 将ollama api 的响应内容逐渐返回,而不是一次性全部返回
    callOp := llms.WithStreamingFunc(func(ctx context.Context, chunk []byte) error {
        select {
        case msgChan <- string(chunk):
        case <-ctx.Done():
            return ctx.Err() // 返回上下文的错误
        }
        return nil
    })

    _, err := llaClient.Call(context.Background(), prompt, callOp)
    if err != nil {
        log.Fatalf("Call failed: %v", err) // 处理错误,而不是 panic
    }
...
  • 完整代码
go 复制代码
package main

import (
    "context"
    "fmt"
    "io"
    "log"
    "net/http"

    "github.com/gin-gonic/gin"
    "github.com/tmc/langchaingo/llms"
    "github.com/tmc/langchaingo/llms/ollama"
)

func main() {
    router := gin.Default()

    router.GET("/ping", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{
            "message": "OK",
        })
    })

    router.POST("/chat", chat)

    router.Run(":8083")
}

type Prompt struct {
    Text string `json:"text"`
}

func chat(c *gin.Context) {
    var prompt Prompt
    if err := c.BindJSON(&prompt); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return


    }

    var msgChan = make(chan string)
    // 通过chan 将ollama 响应返回给前端
    go Generate(prompt.Text, msgChan)

    c.Stream(func(w io.Writer) bool {
        select {
        case msg, ok := <-msgChan:
            if !ok {
                // 如果msgChan被关闭,则结束流式传输
                return false
            }
            // fmt.Print(msg)
            c.SSEvent("messages", msg)
            return true
        case <-c.Done():
            // 如果客户端连接关闭,则结束流式传输
            return false
        }
    })
}

var llaClient *ollama.LLM

func init() {
    // Create a new Ollama instance
    // The model is set to "gemma:2b"
    // remote url is set to "http://ollama-ip:11434"
    url := ollama.WithServerURL("http://ollama-ip:11434")
    lla, err := ollama.New(ollama.WithModel("gemma:2b"), url)
    if err != nil {
        panic(err)
    }

    llaClient = lla

    fmt.Println("connect to ollama server successfully")
}

func Generate(prompt string, msgChan chan string) {
    // ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) // 设置超时
    // defer cancel()                                                          // 确保在函数结束时取消上下文

    callOp := llms.WithStreamingFunc(func(ctx context.Context, chunk []byte) error {
        select {
        case msgChan <- string(chunk):
        case <-ctx.Done():
            return ctx.Err() // 返回上下文的错误
        }
        return nil
    })

    _, err := llaClient.Call(context.Background(), prompt, callOp)
    if err != nil {
        log.Fatalf("Call failed: %v", err) // 处理错误,而不是 panic
    }

    // 确保在所有数据处理完毕后关闭 msgChan
    close(msgChan)
}

项目地址

jackwillsmith/openui-svelte-build (github.com)

GitHub - jackwillsmith/openui-backend-go: openui-backend-go

相关推荐
岁忧5 小时前
(LeetCode 面试经典 150 题 ) 209. 长度最小的子数组(双指针)
java·c++·算法·leetcode·面试·go
熬了夜的程序员6 小时前
【华为机试】HJ30 字符串合并处理
算法·华为·面试·go
一条GO7 小时前
易犯的五个Go编码错误
go
Code季风7 小时前
Go并发详解
go·编程语言·设计
Swift社区9 小时前
日志不再孤立!用 Jaeger + TraceId 实现链路级定位
人工智能·chatgpt
程序员爱钓鱼11 小时前
Go语言实战案例-字符串反转
后端·google·go
王中阳Go1 天前
面试完第一反应是想笑
后端·go
Code季风1 天前
gRPC与Protobuf集成详解—从服务定义到跨语言通信(含Go和Java示例)
go·grpc·protobuf
Code季风1 天前
Protobuf 高级特性详解
go·protobuf
_代号0071 天前
Go 编译报错排查:vendor/golang.org/x/crypto/cryptobyte/asn1 no Go source files
后端·go