本文使用「署名 4.0 国际 (CC BY 4.0)」许可协议,欢迎转载、或重新修改使用,但需要注明来源。 [署名 4.0 国际 (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/deed.zh) 本文作者: 苏洋 创建时间: 2023年05月19日 统计字数: 14213字 阅读时间: 29分钟阅读 本文链接: https://soulteary.com/2023/05/19/make-openai-more-open-and-freely-access-data-sources-in-chatgpt.html ----- # 让 OpenAI 更 Open,在 ChatGPT 里自由接入数据源 本篇文章中,我们简单聊聊如何在 OpenAI 的 ChatGPT Web 客户端中,自由的接入和使用各种数据源。 ## 写在前面 三月以来,我在 ChatGPT 官方客户端上做了不少实践,也做过一些技术分享。也在网上晒过一些折腾的有趣的事情: - [例子 1](https://www.zhihu.com/zvideo/1615679760738250752),直接通过 ChatGPT 来搜索最新发行的游戏和游戏攻略,以及某些平台上的商品价格信息,并根据销量给出简单的购买建议。 - [ 例子 2](https://www.zhihu.com/pin/1636158221214887936),通过 ChatGPT 来阅读超长的内容,你可以自由组合信息来源,或者使用开源的支持长 Token 生成的模型。 - [ 例子 3](https://www.zhihu.com/pin/1637642465724325890),直接在 ChatGPT 里,调用 Mid Journey 来绘制图片。 有不少朋友好奇其中的实现,也有一些朋友觉得 ChatGPT Web 客户端是一个很棒的载体,拥有着不错的交互形式,希望能够使用这种方式来玩,节约大量不必要的系统开发成本,以及期待能够一起进行开源共建。 最近,在一位合作伙伴的推动下,从五一假期开始,我们陆陆续续进行了“ChatGPT”后端服务的代码重写,以及部分敏感信息的剥离:把 ChatGPT 的前端封装成了独立的 Docker 容器,并重写了一套兼容 ChatGPT 客户端的后端服务。 **让任何人都可以在本地启动一套和官方交互体验一致的 ChatGPT 客户端,并能够根据自己需求接入合适的信息源来玩:** - 可以是借助 API 调用的模型接口,不仅限于 OpenAI 3.5 或 4,你也可以接入 HuggingFace 或者国内的大模型,甚至是托管在你的私有环境的服务。比如,在 ChatGPT 里甚至能够调用 Claude、国内的通义千问、图片生成模型。 - 可以是一个搜索引擎,用聊天的方式,实现信息的搜索,顺带再使用模型的生成能力来调整和润色返回的结果。 - 可以是固定的数据源或数据库,比如指定的内容、博客长文,甚至是你预设的一个固定答案,哪怕存在文本文件或者 Excel 里的数据。 - **也可以是 RSS 信息源,或者任意你希望对接的 “API”、“网站”等等,不论目前 ChatGPT 官方是否支持,你是否排队排到了的各种功能使用权限。** 目前,这两个项目分别开放在了 GitHub 上: - [https://github.com/soulteary/docker-chatgpt](https://github.com/soulteary/docker-chatgpt) - [https://github.com/soulteary/sparrow](https://github.com/soulteary/sparrow) 或许等待后端相对完善之后,我会重写一套完全开源的前端客户端,让整个项目变的真的完整起来。 ## 基础使用:OpenAI API 在项目的[示例目录](https://github.com/soulteary/docker-chatgpt/blob/main/examples)中,我们能够找到一些开箱即用的使用 Demo,先来看看最简单的[接入 OpenAI API 的配置示例](https://github.com/soulteary/docker-chatgpt/tree/main/examples/01.use-OpenAI-API): ```yaml 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 管理页面](https://platform.openai.com/account/api-keys),将自己的 API 更新到配置中。接着,使用 `docker compose up` 启动程序,将看到类似下面的日志输出: ```bash # 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”](https://attachment.soulteary.com/2023/05/19/first-look.jpg) 如果你希望将服务搭建在其他的机器上,只需要调整上面配置中的两个环境变量即可(比如 `http://10.11.12.240:8090`): ```yaml 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` 启动服务,能够看到日志输出的内容中包含了我们新的配置地址: ```bash ... 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](https://attachment.soulteary.com/2023/05/19/openai-api.jpg) 是不是非常简单?当然,这个仅仅是个 Demo,“[OpenAI API 数据源](https://github.com/soulteary/sparrow/blob/main/connectors/openai-api/openai.go)”在开源的后端代码项目里是这样的,只有不到 40 行: ```go 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 差不多: ```yaml 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 账号,访问[官方文档页面](https://flagstudio.baai.ac.cn/document)获取你自己的 API Key,并将它更新到上面配置中的 `FLAGSTUDIO_API_KEY`。 ![获取 FlagStudio API Key](https://attachment.soulteary.com/2023/05/19/flagstudio-api.jpg) 每个 API 每天能够调用生成 500 张图,如果生成效果不好,使用 ChatGPT 自带的“Prompt”问题重写、补充连续对话、重新生成按钮都可以重新生成图片。 ![在 ChatGPT 中使用 FlagStudio](https://attachment.soulteary.com/2023/05/19/flagstudio-in-chat.jpg) 下面我们聊聊,如何封装这样一个简单的数据源,让 ChatGPT 能够输出一些不一样的东西。 ## 封装自定义数据源:Flag Studio Flag Studio 的数据源封装实现,存放在后端项目 sparrow 的 [connectors/flag-studio](https://github.com/soulteary/sparrow/tree/main/connectors/flag-studio) 里,关键实现代码行数不到 200 行。 参考官方文档,一个完整的 Flag Studio 图片生成流程中,需要根据我们申请的 API Key 去换服务调用所需要的 Token,最后携带 Token 去调用图片生成接口即可。 我们先来实现根据 API Key 换 Token 的逻辑: ```go 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。 接下来,我们来实现主要逻辑,图片生成接口调用: ```go 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 内容,自动选择合适的模型风格,如果你感兴趣,可以在[项目中提交你的代码实现](https://github.com/soulteary/sparrow),让更多的人受惠于此。 好了,上面的代码就是核心实现。但是,为了让实现生效,我们还需要完成一些边边角角的调整。 我们需要先在流式响应组件中[components/stream-responser/stream\_builder.go](https://github.com/soulteary/sparrow/blob/main/components/stream-responser/stream_builder.go),添加一段调用,让服务端在响应请求的时候,能够将用户提交的 Prompt 交给我们刚刚封装好的程序。 ```go 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 } ... } ... } ``` 接着,是在[程序功能开关中](https://github.com/soulteary/sparrow/blob/main/internal/define/define.go)添加一些定义。如果你不需要按需启用,可以不进行实现: ```go 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](https://github.com/soulteary/sparrow/blob/main/internal/datatypes/models.go),在其中添加我们自定义的新数据源: ```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](https://github.com/soulteary/sparrow/blob/main/internal/api/models/flagstudio.go),定义一个获取我们定义好的模型类型的功能: ```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](https://github.com/soulteary/sparrow/blob/main/internal/api/models/models.go) 中,当 ChatGPT 调用模型列表的时候,就能够访问到我们的新增的模型或者数据源了。 ```go 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 --EOF