← 回所有專案

後端開發與架構設計 ‧ 房仲業 · 2023

NexusGPT — 企業內部 ChatGPT 平台

公司內部使用的 ChatGPT 平台,前端做成和 ChatGPT 幾乎一樣的深色對話介面,後端以 SignalR(WebSocket)把 GPT 回覆逐字即時推送到畫面上,並串接公司的 OpenID Connect 登入,讓每個人的聊天室跟著帳號走、換電腦也找得回來;具備聊天室、圖片生成與 AI 資料分析功能,使外部 GPT 費用節省 3 倍以上。開源版本以 Hexagonal Architecture 實作,儲存後端可在本機檔案與 AWS S3 間直接抽換。

.NET 8ASP.NET CoreSignalRWebSocketOpenID ConnectDDDEvent SourcingHexagonal ArchitectureEF CoreMSSQLOpenAI APIAWS S3Docker
NexusGPT — 企業內部 ChatGPT 平台

專案概述

ChatGPT 推出之後,公司裡很多同仁開始自己拿個人帳號用 GPT 寫程式、整理資料、寫 文案。問題也跟著來:每個人各自付費,費用分散又難管理;更麻煩的是,開發人員會直 接把內部程式碼、客戶資料貼到外部工具上,資安上完全無法掌握。

於是我設計並開發了 NexusGPT:一個只在公司內部使用的 ChatGPT 平台。同仁用 公司帳號登入,就能使用 GPT-4 與 DALL-E 3,所有對話都留在公司自己的系統裡,費用 也由公司統一向 OpenAI 採購 API 用量。

核心程式碼後來以 Hexagonal Architecture 整理成獨立的開源專案,拿掉了內部串接的 部分。

介面:做得跟 ChatGPT 一樣

一個內部工具要讓大家願意用,最快的方式就是讓大家不用重新學。當時同仁已經習 慣 ChatGPT 的操作方式,所以 NexusGPT 的前端刻意做得和 ChatGPT 幾乎一模一樣:

  • 深色主題:整個介面採用黑底配色,和當時的 ChatGPT 一致
  • 左側聊天室列表:依「今天、過去 7 天」分組,點一下就切換對話;上方是「新對 話」按鈕,下方顯示目前登入的帳號
  • 右側對話區:使用者的訊息靠右、以圓角氣泡呈現,AI 的回覆靠左,支援 Markdown 表格、程式碼區塊與檔案下載
  • 底部輸入框:圓角輸入列與送出按鈕,送出後回覆會一個字一個字出現在畫面上

因為介面和操作方式幾乎沒有落差,導入時不需要額外辦教育訓練,同仁打開就會用。

系統架構

NexusGPT 架構圖:前端先透過 OpenID Connect 以公司帳號登入取得 Token,再經由 SignalR WebSocket 送出訊息;後端驗證 Token 取出使用者 ID,向 OpenAI API 以串流方式取得回覆並逐字推送回前端,聊天室與訊息依使用者 ID 存入 MSSQL,DALL-E 生成的圖片存到 AWS S3

整個流程分成四步:

  1. 登入:前端導向公司的 OpenID Connect 服務,以公司帳號登入後取得 Token
  2. 送出訊息:前端透過 SignalR 建立的 WebSocket 連線,把訊息送到後端
  3. 串流回覆:後端以串流模式呼叫 OpenAI API,GPT 每產生一小段文字就收到一段
  4. 即時推送:每收到一段,就立刻透過同一條 WebSocket 推回前端,畫面上逐字長出 回覆

即時對話:SignalR + 串流回覆

GPT-4 產生一段完整的回答常常要十幾秒到幾十秒。如果用一般的 HTTP 請求,使用者要 盯著空白畫面等到整段生成完才看得到結果,體驗很差。ChatGPT 之所以感覺「很快」, 是因為它邊生成邊顯示。

要做到這點,後端需要能「主動、連續地」把資料推給前端,所以選用 SignalR—— ASP.NET Core 的即時通訊框架,底層優先使用 WebSocket,連線建立後雙方都能隨時 傳送訊息。

後端的做法是把 OpenAI 的串流回覆,原封不動地接到 SignalR 的串流上:

C#
// 示意:OpenAI 每回傳一小段文字,就立刻推給前端
public async IAsyncEnumerable<string> SendMessage(
    Guid chatRoomId,
    string message,
    [EnumeratorCancellation] CancellationToken cancellationToken)
{
    var userId = Context.UserIdentifier!;
    var answer = new StringBuilder();

    await foreach (var chunk in _chatService.StreamAsync(userId, chatRoomId, message, cancellationToken))
    {
        answer.Append(chunk);
        yield return chunk;
    }

    // 串流結束後,把完整的回答與 Token 用量寫回該使用者的聊天室
    await _chatService.SaveAnswerAsync(userId, chatRoomId, answer.ToString());
}

幾個實作上的細節:

  • 串流中不寫資料庫:每一小段文字只推給前端,等整段回覆結束後才一次把完整訊 息寫回聊天室,避免每個字都打一次資料庫
  • 支援中途停止:使用者按下停止或關掉頁面時,CancellationToken 會一路傳到 OpenAI 的請求,立刻中斷生成,不會在背景繼續消耗 Token
  • 前端只負責拼接:前端收到一段就接在目前回覆的後面,並重新渲染 Markdown, 表格和程式碼區塊在生成過程中就會逐步成形

帳號整合:OpenID Connect

一開始的版本,聊天紀錄是跟著「瀏覽器」走的:換一台電腦、清掉瀏覽器資料,之前的 對話就找不回來,也沒辦法知道某段對話是誰發起的。

後來把 NexusGPT 串上公司的 OpenID Connect 服務:

  • 公司帳號登入:同仁直接用既有的公司帳號登入,不需要另外註冊;離職的帳號停 用後,自然也就無法再使用
  • 對話跟著使用者走:後端驗證 Token 後,從中取出使用者的唯一識別碼,所有聊天 室都以這個 ID 存放與查詢。不論在公司電腦、筆電還是換了瀏覽器,登入後看到的都 是自己的聊天室列表與完整對話紀錄
  • 只看得到自己的資料:查詢、改名、刪除聊天室時,一律以 Token 中的使用者 ID 過 濾,不能靠改網址或參數看到別人的對話
  • 用量可以歸戶:每段對話的 GPT Token 費用都記在對應的使用者底下,各部門的實 際用量一目了然

SignalR 的連線同樣需要帶上 Token,Hub 透過 Context.UserIdentifier 就能知道目前 是哪位使用者在說話,即時對話與一般 API 共用同一套身分驗證。

功能

聊天室:建立、改名、刪除、列表、取得內容、上傳、分享、搜尋聊天室

聊天室訊息:支援一般文字對話(串流回覆),以及 DALL-E 3 圖片訊息

資料分析:上傳資料讓 GPT-4 分析,並可把 AI 回覆內容匯出成 Excel

架構設計

同樣採用 Hexagonal Architecture,並把「圖片儲存」做成可抽換的 Port/Adapter:

  • NexusGPT.Entities:聚合根與領域事件(Events.cs),以 Aggregate Root、 Code First 方式建模
  • NexusGPT.UseCase:應用層服務(建立/變更聊天室、新增文字訊息、新增圖片訊 息、以 Stream 方式新增訊息)
  • NexusGPT.Adapter.Out:以 EF Core 存取 SQL Server 的資料庫存取層
  • NexusGPT.Adapter.Out.ImageStorage.Local / NexusGPT.Adapter.Out.ImageStorage.S3: 同一個 Port 的兩種 Adapter 實作——本機測試用本地檔案系統,正式環境換成 AWS S3,上層程式完全不需要改動
  • NexusGPT.EventBus:領域事件匯流排
  • NexusGPT.WebApplication:Web API 進入點,整合 SignalR(SignalRSwaggerGen 讓 SignalR Hub 也能出現在 Swagger 文件中)與 API 版本控制 (Asp.Versioning.Mvc.ApiExplorer)

部署方式

Shell
# 建立 Migration
dotnet ef migrations add InitialCreate \
  --project NexusGPT.Adapter.Out --startup-project NexusGPT.WebApplication

# 打包 Docker image
docker build -f NexusGPT.WebApplication/Dockerfile -t nexusgpt:0.0.1 .

# 使用 AWS S3 儲存圖片
docker run -d -p 8080:80 --name nexusgpt \
  -e ConnectionString="Your ConnectionString" \
  -e AWS_ACCESSKEY="Your AWSAccessKey" \
  -e AWS_SECRET="Your AWSSecretKey" \
  -e AWS_REGION="Your AWSRegion" \
  -e AWS_BUCKET="Your AWSBucketName" \
  -e OPENAI_API_KEY="Your OpenAI API Key" \
  nexusgpt:0.0.1

換成本地檔案儲存則只需要把 S3 相關環境變數換成一個掛載路徑(-v /path/to/local/file:/app/Images),不需要改任何程式碼——這就是 Port/Adapter 模式的意義所在。

實際導入內容(公司內部版本)

  • 仿 ChatGPT 的深色對話介面,同仁無需教育訓練即可上手
  • 串接公司 OpenID Connect 登入,聊天室與對話紀錄依使用者保存,跨裝置都找得回來
  • 建立個人聊天室,儲存對話紀錄、計算每段對話的 GPT Token 費用,並支援聊天室分享
  • 串接 GPT-4,支援上傳資料進行分析,並可將 AI 回覆內容匯出成 Excel
  • 串接 DALL-E 3 圖片對話,並將生成圖片儲存至 AWS S3
  • 搭配 SignalR(WebSocket)與 OpenAI 串流回覆,實現逐字顯示的即時對話

成果

  • 公司採購 GPT-4 的費用減省 3 倍以上
  • 有效過濾資安風險,防止開發人員將內部程式碼提供給外部 GPT 工具
  • 提升企劃、行政等單位的作業效率,如圖片生成、報表分析與資料整理
  • 所有對話與用量都能對應到個人帳號,管理與費用歸屬一目了然

尚未完成/規劃中

開源版本 README 上列出的後續項目:支援 AWS DynamoDB、支援 MongoDB、加上使用 者驗證、加上 OpenTelemetry 可觀測性。(使用者驗證在公司內部版本是以 OpenID Connect 完成,開源版本因為拿掉了內部串接,尚未補上。)