蒼時弦也
蒼時弦也
資深軟體工程師
發表於

整合大型語言模型 - Clean Architecture in TypeScript

這篇文章是 Clean Architecture in TypeScript 系列的一部分,你可以透過 Leanpub 提前閱讀內容。

我們現在已經可以將假的對話資料傳到前端呈現出來,接下來我們繼續讓前端的對話可以實際使用 LLM(大型語言模型)產生的訊息,因此我們需要先加入一個新的 API 端點以及更新介面將對話發送到後端。

整合後端

要讓對話由後端回傳的話,我們需要更新 Hono JSX 的介面、Hono RPC 客戶端以及我們的後端實作,初期可以先寫死假資料,而不需要實際串接語言模型,先以驗證能夠正常運作為初期目標。

以 Aider 為例,先將 src/api/chat.tssrc/controller/chat.tssrc/view/Chat.tsx 三個檔案設定為可編輯的狀態。

接下來使用下面的 Prompt 進行修改。

 1Refactor `handleSendMessage` in `Chat.tsx` to call the backend API to get mock data from backend.
 2
 3## Chat View
 4
 5Call `sendChatMessage(id)` api to send the message.
 6
 7## Chat API
 8
 9Add a new API client `sendChatMessage` in `api/chat.ts` to call the backend API.
10
11## Chat Controller
12
13Add a new endpoint `post('/:id')` in `ChatController.ts` to handle the request, returning mock data.
14
15The response message should be
16
17```json
18{
19  "id": 1,
20  "text": "Assist reply message"
21}
22```

開啟專案在本機測試,確認後端可以正常回覆。

如果發生問題,比較容易出錯的是 src/controller/chat.ts 這個檔案,在生成時有機會產生如下的版本。

1// ...
2const routes = app.get('/:id', async (c) => {
3  // ...
4});
5app.post('/:id', async (c) => {
6 // ...
7});

在使用 Hono RPC 時預期是 Chain Method 的形式,要將 .post() 連接在 .get() 之後,Hono RPC 才能正確的偵測到對的行為,這邊可能會需要手動稍微修正。

另一個是缺少 @hono/zod-validator 的問題,因為我們在之前的範例給過這樣的用法,直接用 npm install @hono/zod-validator 加入到專案即可。

AI SDK

使用 TypeScript 要整合 LLM 最簡單的方式是使用 Vercel 的 AI SDK 來進行整合,我們會使用 OpenAI 相容的 API 來進行開發,可以根據需求自行調整使用的語言模型,開發階段先使用 Ollama 在本機部署會是不錯的做法。

1npm install ai @ai-sdk/openai

安裝完畢後,我們就可以直接使用 LLM 來產生回答,AI SDK 的使用方式非常簡單。

1import { generateText } from 'ai';
2import { openai } from '@ai-sdk/openai';
3
4const { text } = await generateText({
5  model: openai('gpt-4.1-mini'),
6  prompt: 'Explain the concept of quantum entanglement.',
7});

上面這段是官方的範例,我們只需要將 openai 載入進來即可,然而實際使用時我們會希望透過 Cloudflare Worker 的環境變數設定,因此可以改為使用自訂的方式。

 1import { generateText } from 'ai';
 2import { createOpenAI } from '@ai-sdk/openai';
 3
 4const openai = createOpenAI({
 5		apiKey: 'YOUR_OPENAI',
 6		baseURL: 'https://api.openai.com/v1',
 7});
 8
 9const { text } = await generateText({
10  model: openai('gpt-4.1-mini'),
11  prompt: 'Explain the concept of quantum entanglement.',
12});

這樣我們在開發時也可以根據 baseURL 來設定要使用本機的模型,還是 OpenAI 的模型進行開發跟測試。

整合模型

接下來我們稍微手動處理 src/controller/chat.ts 來使用 LLM 回覆,這部分使用 AI 來產生出錯的機率比較大,先手動寫好範例,後續再讓 AI 幫忙重構會相對穩定不少。

我們先在 .dev.vars 設定環境變數,這會讓 Cloudflare Workers 知道該產生哪些型別定義資訊給 TypeScript 參考。

OPENAI_BASE_URL=http://localhost:11434/v1
OPENAI_API_KEY=ollama
OPENAI_MODEL=gemma3:27b-it-qat

運行 npm run cf-typegen 更新定義,這樣我們就可以在 Hono 的 Context 中看到這個變數。

更新 src/controller/chat.ts 的內容,將原本的 app.post('/:id', ...) 改寫成如下的版本。

 1// ...
 2import { createOpenAI } from '@ai-sdk/openai'
 3import { generateText } from 'ai'
 4
 5// ...
 6.post("/:id", zValidator("json", messageSchema), async (c) => {
 7  const chatId = c.req.param('id');
 8  const { text } = await c.req.json();
 9
10  const openai = createOpenAI({
11		apiKey: c.env.OPENAI_MODEL,
12		baseURL: c.env.OPENAI_BASE_URL ? c.env.OPENAI_BASE_URL : "https://api.openai.com/v1",
13	});
14
15	const { text: replyText } = await generateText({
16		model: openai(c.env.OPENAI_MODEL),
17		prompt: text,
18	})
19
20  // In a real implementation, we would save the message
21  // For now, return mock data
22  const message = {
23    id: Date.now().toString(),
24    text: replyText,
25  };
26
27  return c.json({ message });
28});

我們將原本 AI 放置的假資料移除,改為使用 generateText() 從 LLM 產生內容,這樣一來我們的對話功能就能實際使用 LLM 來產生,接下來要先重構成 UseCase 來將商業邏輯跟底層的機制分離開來。