---
title: "整合大型語言模型 - Clean Architecture in TypeScript"
date: 2025-09-12T00:00:00+08:00
publishDate: 2025-09-12T00:00:00+08:00
lastmod: 2025-11-21T10:38:12+08:00
tags: ["TypeScript","Clean Architecture","架構","經驗","AI"]
series: "clean-architecture-in-ts"
toc: true
permalink: "https://blog.aotoki.me/posts/2025/09/12/clean-architecture-in-ts-integrate-llm/"
language: "zh-tw"
---



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

<!--more-->

## 整合後端{#integrate-backend}

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

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

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

````markdown
Refactor `handleSendMessage` in `Chat.tsx` to call the backend API to get mock data from backend.

## Chat View

Call `sendChatMessage(id)` api to send the message.

## Chat API

Add a new API client `sendChatMessage` in `api/chat.ts` to call the backend API.

## Chat Controller

Add a new endpoint `post('/:id')` in `ChatController.ts` to handle the request, returning mock data.

The response message should be

```json
{
  "id": 1,
  "text": "Assist reply message"
}
```
````

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

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

```typescript
// ...
const routes = app.get('/:id', async (c) => {
  // ...
});
app.post('/:id', async (c) => {
 // ...
});
```

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

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

使用 TypeScript 要整合 LLM 最簡單的方式是使用 Vercel 的 [AI SDK](https://ai-sdk.dev) 來進行整合，我們會使用 OpenAI 相容的 API 來進行開發，可以根據需求自行調整使用的語言模型，開發階段先使用 Ollama 在本機部署會是不錯的做法。

```bash
npm install ai @ai-sdk/openai
```

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

```typescript
import { generateText } from 'ai';
import { openai } from '@ai-sdk/openai';

const { text } = await generateText({
  model: openai('gpt-4.1-mini'),
  prompt: 'Explain the concept of quantum entanglement.',
});
```

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

```typescript
import { generateText } from 'ai';
import { createOpenAI } from '@ai-sdk/openai';

const openai = createOpenAI({
		apiKey: 'YOUR_OPENAI',
		baseURL: 'https://api.openai.com/v1',
});

const { text } = await generateText({
  model: openai('gpt-4.1-mini'),
  prompt: 'Explain the concept of quantum entanglement.',
});
```

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

## 整合模型{#integrate-model}

接下來我們稍微手動處理 `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', ...)` 改寫成如下的版本。

```typescript
// ...
import { createOpenAI } from '@ai-sdk/openai'
import { generateText } from 'ai'

// ...
.post("/:id", zValidator("json", messageSchema), async (c) => {
  const chatId = c.req.param('id');
  const { text } = await c.req.json();

  const openai = createOpenAI({
		apiKey: c.env.OPENAI_MODEL,
		baseURL: c.env.OPENAI_BASE_URL ? c.env.OPENAI_BASE_URL : "https://api.openai.com/v1",
	});

	const { text: replyText } = await generateText({
		model: openai(c.env.OPENAI_MODEL),
		prompt: text,
	})

  // In a real implementation, we would save the message
  // For now, return mock data
  const message = {
    id: Date.now().toString(),
    text: replyText,
  };

  return c.json({ message });
});
```

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