8

让 OpenAI 更 Open,在 ChatGPT 里自由接入数据源

 1 year ago
source link: https://soulteary.com/2023/05/19/make-openai-more-open-and-freely-access-data-sources-in-chatgpt.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

让 OpenAI 更 Open,在 ChatGPT 里自由接入数据源

2023年05月19日阅读Markdown格式14213字29分钟阅读

本篇文章中,我们简单聊聊如何在 OpenAI 的 ChatGPT Web 客户端中,自由的接入和使用各种数据源。

三月以来,我在 ChatGPT 官方客户端上做了不少实践,也做过一些技术分享。也在网上晒过一些折腾的有趣的事情:

  • 例子 1,直接通过 ChatGPT 来搜索最新发行的游戏和游戏攻略,以及某些平台上的商品价格信息,并根据销量给出简单的购买建议。
  •  例子 2,通过 ChatGPT 来阅读超长的内容,你可以自由组合信息来源,或者使用开源的支持长 Token 生成的模型。
  •  例子 3,直接在 ChatGPT 里,调用 Mid Journey 来绘制图片。

有不少朋友好奇其中的实现,也有一些朋友觉得 ChatGPT Web 客户端是一个很棒的载体,拥有着不错的交互形式,希望能够使用这种方式来玩,节约大量不必要的系统开发成本,以及期待能够一起进行开源共建。

最近,在一位合作伙伴的推动下,从五一假期开始,我们陆陆续续进行了“ChatGPT”后端服务的代码重写,以及部分敏感信息的剥离:把 ChatGPT 的前端封装成了独立的 Docker 容器,并重写了一套兼容 ChatGPT 客户端的后端服务。

让任何人都可以在本地启动一套和官方交互体验一致的 ChatGPT 客户端,并能够根据自己需求接入合适的信息源来玩:

  • 可以是借助 API 调用的模型接口,不仅限于 OpenAI 3.5 或 4,你也可以接入 HuggingFace 或者国内的大模型,甚至是托管在你的私有环境的服务。比如,在 ChatGPT 里甚至能够调用 Claude、国内的通义千问、图片生成模型。
  • 可以是一个搜索引擎,用聊天的方式,实现信息的搜索,顺带再使用模型的生成能力来调整和润色返回的结果。
  • 可以是固定的数据源或数据库,比如指定的内容、博客长文,甚至是你预设的一个固定答案,哪怕存在文本文件或者 Excel 里的数据。
  • 也可以是 RSS 信息源,或者任意你希望对接的 “API”、“网站”等等,不论目前 ChatGPT 官方是否支持,你是否排队排到了的各种功能使用权限。

目前,这两个项目分别开放在了 GitHub 上:

或许等待后端相对完善之后,我会重写一套完全开源的前端客户端,让整个项目变的真的完整起来。

基础使用:OpenAI API

在项目的示例目录中,我们能够找到一些开箱即用的使用 Demo,先来看看最简单的接入 OpenAI API 的配置示例

version: '3'

services:

  # 能够私有化部署的 ChatGPT Web 客户端
  chatgpt-client:
    image: soulteary/chatgpt
    restart: always
    ports:
      - 8090:8090
    environment:
      # 容器中的服务使用的端口
      APP_PORT: 8090
      # 前端使用的 ChatGPT 客户端域名,需要和 `sparrow` 中的 `WEB_CLIENT_HOSTNAME` 中的设置保持一致
      APP_HOSTNAME: "http://localhost:8090"
      # 客户端使用的服务端地址,如果你使用这个配置文件,可以保持下面的数值,否则需要调整为 `sparrow` 部署的实际地址
      APP_UPSTREAM: "http://sparrow:8091"

  # 开源实现的后端服务
  sparrow:
    image: soulteary/sparrow
    restart: always
    environment:
      # [基础设置]
      # => ChatGPT Web 客户端使用的域名,需要和 `chatgpt-client` 的 `APP_HOSTNAME` 保持一致
      WEB_CLIENT_HOSTNAME: "http://localhost:8090"
      # => 服务端口,默认端口: 8091
      # APP_PORT: 8091

      # [私有使用 OpenAI API 服务设置] *可选配置
      # => 启用 OpenAI API
      ENABLE_OPENAI_API: "on"
      # => OpenAI API Key,填写你自己的 KEY
      OPENAI_API_KEY: "sk-123456789012345678901234567890123456789012345678"
      # => 启用访问 API 的代理,如果你不是在海外服务器使用
      # OPENAI_API_PROXY_ENABLE: "on"
      # => 设置 API 代理地址, eg: `"http://127.0.0.1:1234"` or ""
      # OPENAI_API_PROXY_ADDR: "http://127.0.0.1:1234"
    logging:
        driver: "json-file"
        options:
            max-size: "10m"

我们将上面的文件保存为 docker-compose.yml ,访问 OpenAI 的 API Key 管理页面,将自己的 API 更新到配置中。接着,使用 docker compose up 启动程序,将看到类似下面的日志输出:

# docker compose down && docker compose up

[+] Running 9/9
 ✔ sparrow 2 layers [⣿⣿]      0B/0B      Pulled                                                                                                                                                 41.5s 
   ✔ 178ce6ca3c2d Pull complete                                                                                                                                                                  2.6s 
   ✔ 6e49bc84596f Pull complete                                                                                                                                                                  6.1s 
 ✔ chatgpt-client 5 layers [⣿⣿⣿⣿⣿]      0B/0B      Pulled                                                                                                                                       25.7s 
   ✔ 2408cc74d12b Already exists                                                                                                                                                                 0.0s 
   ✔ 53e036a1e5c8 Pull complete                                                                                                                                                                  2.9s 
   ✔ b6a24d60453c Pull complete                                                                                                                                                                  3.7s 
   ✔ a5072006fa7c Pull complete                                                                                                                                                                  6.3s 
   ✔ 8a30712078cf Pull complete                                                                                                                                                                  6.3s 
[+] Running 3/1
 ✔ Network chatgpt_default             Created                                                                                                                                                   0.1s 
 ✔ Container chatgpt-sparrow-1         Created                                                                                                                                                   0.0s 
 ✔ Container chatgpt-chatgpt-client-1  Created                                                                                                                                                   0.0s 
Attaching to chatgpt-chatgpt-client-1, chatgpt-sparrow-1
chatgpt-sparrow-1         | Sparrow vv0.10.1
chatgpt-sparrow-1         | Sparrow Service has been launched 🚀
chatgpt-chatgpt-client-1  | [OpenAI Chat Client] http://localhost:8090
chatgpt-chatgpt-client-1  | - Project: https://github.com/soulteary/docker-chatgpt
chatgpt-chatgpt-client-1  | - Release: 2023.05.19 v1

等待服务启动完毕,我们在浏览器中打开 http://localhost:8090 (或你自定义的地址) 就能够使用自己搭建的 ChatGPT 服务了。

崭新的 “ChatGPT”

崭新的 “ChatGPT”

如果你希望将服务搭建在其他的机器上,只需要调整上面配置中的两个环境变量即可(比如 http://10.11.12.240:8090):

version: '3'
services:

  chatgpt-client:
...
    environment:
      APP_HOSTNAME: "http://10.11.12.240:8090"
...
  sparrow:
...
    environment:
      WEB_CLIENT_HOSTNAME: "http://10.11.12.240:8090"
...

当完成配置的调整后,我们重新使用 docker compose up 启动服务,能够看到日志输出的内容中包含了我们新的配置地址:

...
chatgpt-sparrow-1         | Sparrow vv0.10.1
chatgpt-sparrow-1         | Sparrow Service has been launched 🚀
chatgpt-chatgpt-client-1  | [OpenAI Chat Client] http://10.11.12.240:8090
chatgpt-chatgpt-client-1  | - Project: https://github.com/soulteary/docker-chatgpt
chatgpt-chatgpt-client-1  | - Release: 2023.05.19 v1
在 ChatGPT 中调用 OpenAI API
在 ChatGPT 中调用 OpenAI API

是不是非常简单?当然,这个仅仅是个 Demo,“OpenAI API 数据源”在开源的后端代码项目里是这样的,只有不到 40 行:

package OpenaiAPI

import (
	"context"
	"fmt"
	"net/http"
	"net/url"

	openai "github.com/sashabaranov/go-openai"
	"github.com/soulteary/sparrow/internal/define"
)

func GetClient() *openai.Client {
	config := openai.DefaultConfig(define.OPENAI_API_KEY)
	if define.ENABLE_OPENAI_API_PROXY {
		proxyUrl, err := url.Parse(define.OPENAI_API_PROXY_ADDR)
		if err != nil {
			panic(err)
		}
		transport := &http.Transport{Proxy: http.ProxyURL(proxyUrl)}
		config.HTTPClient = &http.Client{Transport: transport}
	}
	return openai.NewClientWithConfig(config)
}

func Get(prompt string) string {
	c := GetClient()
	resp, err := c.CreateChatCompletion(
		context.Background(),
		openai.ChatCompletionRequest{
			Model:    openai.GPT3Dot5Turbo,
			Messages: []openai.ChatCompletionMessage{{Role: openai.ChatMessageRoleUser, Content: prompt}},
		},
	)
	if err != nil {
		return fmt.Sprintf("OpenAI API, Chat Completion error: %v\n", err)
	}
	return resp.Choices[0].Message.Content
}

如果你想有更好的体验,比如完整的会话记录管理、多轮会话上下文保持,欢迎来开源项目中提交你的改进代码。

基础使用:官方不支持的图文模型

接下来,我们来看看如何在 ChatGPT 中使用官方原本不支持的数据源或模型。比如我们先来折腾一个接入获取难度非常低、支持在线申请免费 API 使用的,智源研究院推出的 Flag Studio 图文大模型。

使用 FlagStudio 的配置文件和使用 OpenAI API 差不多:

version: '3'

services:

  # 能够私有化部署的 ChatGPT Web 客户端
  chatgpt-client:
    image: soulteary/chatgpt
    restart: always
    ports:
      - 8090:8090
    environment:
      # 容器中的服务使用的端口
      APP_PORT: 8090
      # 前端使用的 ChatGPT 客户端域名,需要和 `sparrow` 中的 `WEB_CLIENT_HOSTNAME` 中的设置保持一致
      APP_HOSTNAME: "http://localhost:8090"
      # 客户端使用的服务端地址,如果你使用这个配置文件,可以保持下面的数值,否则需要调整为 `sparrow` 部署的实际地址
      APP_UPSTREAM: "http://sparrow:8091"

  # 开源实现的后端服务
  sparrow:
    image: soulteary/sparrow
    restart: always
    environment:
      # [基础设置]
      # => ChatGPT Web 客户端使用的域名,需要和 `chatgpt-client` 的 `APP_HOSTNAME` 保持一致
      WEB_CLIENT_HOSTNAME: "http://localhost:8090"
      # => 服务端口,默认端口: 8091
      # APP_PORT: 8091

      # [私有实现的 FlagStudio 服务] *可选
      # => 启用 FlagStudio
      ENABLE_FLAGSTUDIO: "on"
      # => 只启用 FlagStudio 数据源
      ENABLE_FLAGSTUDIO_ONLY: "off"
      # => FlagStudio API Key
      # FLAGSTUDIO_API_KEY: "your-flagstudio-api-key", like: `238dc972f6a2ebf15d787aef659cc4d1` (页面上获取)
      FLAGSTUDIO_API_KEY: "填写你自己的 API KEY"

    logging:
        driver: "json-file"
        options:
            max-size: "10m"

先将上面的内容保存为 docker-compose.yml,接着注册一个 FlagStudio 账号,访问官方文档页面获取你自己的 API Key,并将它更新到上面配置中的 FLAGSTUDIO_API_KEY

获取 FlagStudio API Key

获取 FlagStudio API Key

每个 API 每天能够调用生成 500 张图,如果生成效果不好,使用 ChatGPT 自带的“Prompt”问题重写、补充连续对话、重新生成按钮都可以重新生成图片。

在 ChatGPT 中使用 FlagStudio
在 ChatGPT 中使用 FlagStudio

下面我们聊聊,如何封装这样一个简单的数据源,让 ChatGPT 能够输出一些不一样的东西。

封装自定义数据源:Flag Studio

Flag Studio 的数据源封装实现,存放在后端项目 sparrow 的 connectors/flag-studio 里,关键实现代码行数不到 200 行。

参考官方文档,一个完整的 Flag Studio 图片生成流程中,需要根据我们申请的 API Key 去换服务调用所需要的 Token,最后携带 Token 去调用图片生成接口即可。

我们先来实现根据 API Key 换 Token 的逻辑:

package FlagStudio

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
)

const API_GET_TOKEN = "https://flagopen.baai.ac.cn/flagStudio/auth/getToken"

type ResponseToken struct {
	Code int `json:"code"`
	Data struct {
		Token string `json:"token"`
	} `json:"data"`
}

// parseToken parses the token from the response body
func parseToken(buf []byte) (string, error) {
	var data ResponseToken
	err := json.Unmarshal(buf, &data)
	if err != nil {
		return "", err
	}
	if data.Code != 200 || data.Data.Token == "" {
		return "", fmt.Errorf("FlagStudio API, Get Token error, Code %d\n, Token: %s", data.Code, data.Data.Token)
	}
	return data.Data.Token, nil
}

// get token from the API
func GetToken(apikey string) (string, error) {
	req, err := http.NewRequest("GET", API_GET_TOKEN, nil)
	if err != nil {
		return "", fmt.Errorf("FlagStudio API, Error initializing network components, err: %v", err)
	}

	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")
	q := req.URL.Query()
	q.Add("apikey", apikey)
	req.URL.RawQuery = q.Encode()

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		fmt.Println(err)
		return "", fmt.Errorf("FlagStudio API, Error sending request, err: %v", err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		fmt.Println(err)
		return "", fmt.Errorf("FlagStudio API, Error reading response, err: %v", err)
	}

	token, err := parseToken(body)
	if err != nil {
		fmt.Println(err)
		return "", fmt.Errorf("FlagStudio API, Error parsing response, err: %v", err)
	}
	return token, nil
}

上面的代码中,我们实现了一个非常基础的 HTTP 调用,以及对服务端返回的 JSON 内容的解析,如果 API Key 正确、网络没有异常的情况下,函数运行结束,我们将得到生成图片所需要的 Token。

接下来,我们来实现主要逻辑,图片生成接口调用:

package FlagStudio

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strings"

	"github.com/soulteary/sparrow/internal/define"
)

type TextToImage struct {
	Prompt          string  `json:"prompt"`
	GuidanceScale   float64 `json:"guidance_scale"`
	Height          int     `json:"height"`
	NegativePrompts string  `json:"negative_prompts"`
	Sampler         string  `json:"sampler"`
	Seed            int     `json:"seed"`
	Steps           int     `json:"steps"`
	Style           string  `json:"style"`
	Upsample        int     `json:"upsample"`
	Width           int     `json:"width"`
}

const API_TEXT_TO_IMAGE = "https://flagopen.baai.ac.cn/flagStudio/v1/text2img"

var FS_STYLES = []string{"国画", "写实主义", "虚幻引擎", "黑白插画", "版绘", "低聚", "工业霓虹", "电影艺术", "史诗大片", "暗黑", "涂鸦", "漫画场景", "特写", "儿童画", "油画", "水彩画", "素描", "卡通画", "浮世绘", "赛博朋克", "吉卜力", "哑光", "现代中式", "相机", "CG渲染", "动漫", "霓虹游戏", "蒸汽波", "宝可梦", "火影忍者", "圣诞老人", "个人特效", "通用漫画", "Momoko", "MJ风格", "剪纸", "齐白石", "张大千", "丰子恺", "毕加索", "梵高", "塞尚", "莫奈", "马克·夏加尔", "丢勒", "米开朗基罗", "高更", "爱德华·蒙克", "托马斯·科尔", "安迪·霍尔", "新海诚", "倪传婧", "村上隆", "黄光剑", "吴冠中", "林风眠", "木内达朗", "萨雷尔", "杜拉克", "比利宾", "布拉德利", "普罗旺森", "莫比乌斯", "格里斯利", "比普", "卡尔·西松", "玛丽·布莱尔", "埃里克·卡尔", "扎哈·哈迪德", "包豪斯", "英格尔斯", "RHADS", "阿泰·盖兰", "俊西", "坎皮恩", "德尚鲍尔", "库沙特", "雷诺阿"}

func GetRandomStyle() string {
	return FS_STYLES[define.GetRandomNumber(0, len(FS_STYLES)-1)]
}

func GenerateImageByText(s string) string {
	data := TextToImage{
		Prompt:          s,
		GuidanceScale:   7.5,
		Width:           512,
		Height:          512,
		NegativePrompts: "",
		Sampler:         "ddim",
		Seed:            1024,
		Steps:           50,
		Style:           GetRandomStyle(),
		Upsample:        1,
	}

	payload, err := define.MakeJSON(data)
	if err != nil {
		return fmt.Sprintf("FlagStudio API, An error occurred while preparing to enter data: %v", err)
	}

	token, err := GetToken(define.FLAGSTUDIO_API_KEY)
	if err != nil {
		return fmt.Sprintf("FlagStudio API, An error occurred while getting the token: %v", err)
	}

	req, err := http.NewRequest("POST", API_TEXT_TO_IMAGE, strings.NewReader(payload))
	if err != nil {
		return fmt.Sprintf("FlagStudio API, An error occurred while initializing network components: %v", err)
	}

	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")
	req.Header.Add("token", token)
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return fmt.Sprintf("FlagStudio API, An error occurred while sending request: %v", err)
	}

	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return fmt.Sprintf("FlagStudio API, An error occurred while reading response: %v", err)
	}

	base64Image, err := parseTextToImage(body)
	if err != nil {
		return fmt.Sprintf("FlagStudio API, An error occurred while parsing response: %v", err)
	}

	return `![](data:image/png;base64,` + base64Image + `)`
}

type ResponseTextToImage struct {
	Code int    `json:"code"`
	Data string `json:"data"`
	Nsfw int    `json:"nsfw"`
}

// parseToken parses the token from the response body
func parseTextToImage(buf []byte) (string, error) {
	var data ResponseTextToImage
	err := json.Unmarshal(buf, &data)
	if err != nil {
		return "", err
	}
	if data.Code != 200 || data.Data == "" {
		return "", fmt.Errorf("FlagStudio API, Get Result error, Code %d", data.Code)
	}

	if data.Nsfw != 0 {
		return "", fmt.Errorf("FlagStudio API, Get Token error, Code %d\n, NSFW: %d", data.Code, data.Nsfw)
	}

	return data.Data, nil
}

和上面调用 Token 的逻辑类似,不过这里我们需要使用 POST 来发送请求,并携带合适的请求参数。

关于图片风格的定义,这里简单实现了一个随机选取风格,更好的实现是根据用户的 Prompt 内容,自动选择合适的模型风格,如果你感兴趣,可以在项目中提交你的代码实现,让更多的人受惠于此。

好了,上面的代码就是核心实现。但是,为了让实现生效,我们还需要完成一些边边角角的调整。

我们需要先在流式响应组件中components/stream-responser/stream_builder.go,添加一段调用,让服务端在响应请求的时候,能够将用户提交的 Prompt 交给我们刚刚封装好的程序。

package StreamResponser

...

func StreamBuilder(parentMessageID string, conversationID string, modelSlug string, broker *eb.Broker, input string, mode StreamMessageMode) bool {
	...
	switch modelSlug {
	...
	case datatypes.MODEL_FLAGSTUDIO.Slug:
		if define.ENABLE_FLAGSTUDIO {
			sequences = MakeStreamingMessage(FlagStudio.GenerateImageByText(input), modelSlug, conversationID, messageID, mode)
			quickMode = true
		}
	...
	}
...
}

接着,是在程序功能开关中添加一些定义。如果你不需要按需启用,可以不进行实现:

var (
	ENABLE_FLAGSTUDIO      = GetBool("ENABLE_FLAGSTUDIO", false)                       // Enable Flagstudio
	ENABLE_FLAGSTUDIO_ONLY = GetBool("ENABLE_FLAGSTUDIO_ONLY", false)                  // Enable Flagstudio only
	FLAGSTUDIO_API_KEY     = GetSecret("FLAGSTUDIO_API_KEY", "YOUR_FLAGSTUDIO_SECRET") // Flagstudio API Token
)

为了实现多种模型、数据源的切换,我们还需要为每一种数据源进行一些数据预定义。在模型列表目录中创建一个新程序文件internal/datatypes/models.go,在其中添加我们自定义的新数据源:

var MODEL_FLAGSTUDIO = ModelListItem{
	Slug:        "flag-studio",
	MaxTokens:   1000,
	Title:       "FlagStudio",
	Description: "FlagStudio is a text-to-image platform developed by BAAI's z-lab and FlagAI team.\n\nIt supports 18-language text-to-image generation including Chinese and English, and aims to provide advanced AI art creation experience.",
	Tags:        []string{},
	QualitativeProperties: ModelListQualitativeProperties{
		Reasoning:   []int{4, 5},
		Speed:       []int{4, 5},
		Conciseness: []int{3, 5},
	},
}

为了让模型能够被 ChatGPT 正常调用,我们还需要实现模型获取 API 中的一些实现,依旧是创建一个新的程序 internal/api/models/flagstudio.go,定义一个获取我们定义好的模型类型的功能:

package models

import (
	"github.com/soulteary/sparrow/internal/datatypes"
	"github.com/soulteary/sparrow/internal/define"
)

func GetFlagStudioModel() (result []datatypes.ModelListItem) {
	model := datatypes.MODEL_FLAGSTUDIO

	if define.ENABLE_I18N {
		model.Description = "FlagStudio 是由 BAAI 旗下的创新应用实验室和 FlagAI 团队开发的文图生成工具。\n\n支持中英等18语的文图生成,旨在为大家提供先进的AI艺术创作体验。"
	}

	result = append(result, model)
	return result
}

最后,实现完调用函数,我们将调用函数添加到internal/api/models/models.go 中,当 ChatGPT 调用模型列表的时候,就能够访问到我们的新增的模型或者数据源了。

package models

import (
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/soulteary/sparrow/internal/datatypes"
	"github.com/soulteary/sparrow/internal/define"
)

func GetModels(c *gin.Context) {
...
	if define.ENABLE_FLAGSTUDIO {
		model := GetFlagStudioModel()
		if define.ENABLE_FLAGSTUDIO_ONLY {
			c.JSON(http.StatusOK, datatypes.Models{Models: model})
		}
		modelList = append(modelList, model...)
	}

...
}

目前添加新数据源的体验还不是很好,后续我考虑进行一些优化调整,让添加数据源能够更简单明了一些。当然,后端服务是开源实现,如果你有好的想法,也可以进行开源共建。

关于 “ChatGPT” 还有很多其他的有趣的实现,接下来相关的文章里,我们慢慢展开 :D


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK