---
title: "對話 UseCase - Clean Architecture in TypeScript"
date: 2025-09-19T00:00:00+08:00
publishDate: 2025-09-19T00: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/19/clean-architecture-in-ts-conversation-usecase/"
language: "zh-tw"
---



驗證大型語言模型可以順利整合後，我們需要繼續依照 Clean Architecture 的處理方式將物件拆分開來，在對話紀錄的處理上我們已經完成過一次，基本上只要依照相同的方式處理即可。

<!--more-->

## 定義介面{#define-interface}

這一次我們需要一個叫做 `ChatWithAssistant` 的 Use Case 來幫我們處理對話的商業邏輯，除此之外還可以順便將原本假的對話紀錄重構成支援以記憶體方式保存的版本，開始進入使用真實對話紀錄的範圍。

首先，更新 `src/usecase/interface.ts` 更新定義

```ts
import { ChatMessage } from '@/entity/ChatMessage'

export interface ChatMessageRepository {
	byId(id: string): Promise<ChatMessage[]>;
	save(id: string, messages: ChatMessage[]): Promise<void>;
}

// ...

export interface AssistantService {
	ask(messages: ChatMessage[]): Promise<string>;
}
```

接下來實作 `src/usecase/chatWithAssistant.ts` 的內容，因為我們還沒有設計 `Chat` 用於聚合物件，所以需要先手動處理 `ChatMessage` 列表的組合。

```ts
import { ChatMessage } from '@/entity/ChatMessage';
import { ChatMessageRepository, ChatMessagePresenter, AssistantService } from './interface';

export class ChatWithAssistant {
	constructor(
		private readonly chatMessageRepository: ChatMessageRepository,
		private readonly assistantService: AssistantService,
		private readonly chatMessagePresenter: ChatMessagePresenter,
	) {}

	async execute(id: string, message: string): Promise<void> {
		const messages = await this.chatMessageRepository.byId(id);

		let newMessages: ChatMessage[] = [
			...messages,
			new ChatMessage(
				(messages.length + 1).toString(),
				id,
				'user',
				message
			),
		]

		const response = await this.assistantService.ask(newMessages);

		newMessages = [
			...newMessages,
			new ChatMessage(
				(messages.length + 2).toString(),
				id,
				'assistant',
				response
			),
		]

		await this.chatMessageRepository.save(id, newMessages);

		newMessages.forEach((message) => {
			this.chatMessagePresenter.addMessage(message);
		});
	}
}
```

設計上就是透過 Repository 將訊息取出後，加入使用者的訊息再由語言模型產生新的訊息，並且保存起來用於下一次的對話，為了初期保持行為單純，我們直接將所有訊息回傳。

## 進行重構{#refactoring}

接下來的部分交給 AI 處理即可，大部分關鍵的資訊我們已經在 Use Case 實作中處理完畢，剩下的細節繼續交給 AI 來調整，只需要在最後進行檢查即可。

以 Aider 為例，以下檔案設定為 Read-only 用於參考

* `src/presenter/JsonChatMessagePresenter.ts`
* `src/usecase/chatWithAssistant.ts`
* `src/usecase/interface.ts`

接下來將我們想要編輯的檔案加入到可編輯的檔案中

* `src/api/chat.ts`
* `src/controller/chat.ts`
* `src/repository/KvChatMessageRepository.ts`
* `src/view/Chat.tsx`

完成設定後，我們可以用以下的提示來進行修改。

````markdown
Refactor `src/controller/chat.ts` to use `ChatWithAssistant` usecase when user sends a message.

## ChatMessageRepository

* Update the existing repository to add `save` method for saving chat messages.
* Make existing mock data to use real data, returning empty array when no messages are found.

## AssistantService

* Move existing LLM implementation to `src/service/llmAssistantService.ts`.
* Make AI SDK `generateText` to use multiple messages mode to support contextual conversations.

```typescript
const { text } = await generateText({
    // ...
    messages: [
        {
            role: 'user',
            content: userMessage,
        },
        {
            role: 'assistant',
            content: assistantMessage,
        },
    ],
})
```

## Chat API

Update chat API client `sendChatMessage` to match new API interface.

## Chat View

The new API returns full chat history, so the chat view should be updated to display all messages in the conversation.
````

這一次調整的範圍又在更大一些，目前大部分語言模型都還在提升 Context 的範圍，即使大範圍修改通常也不太會失敗，不過要注意讓 AI 自己搜尋檔案時，指示不夠明確可能會參考錯誤檔案的問題，這部分可以根據正在進行的工作取捨。

## 修正細節{#fixes}

到目前為止，每一次使用 AI 後都會遺留一些小問題，我們盡可能的將這些問題修正。一定程度上可以確保後續實作在參考時，不會使用到有問題當作基準，另一部分則是 AI 的隨機性有可能造成一些小問題。

這一次的案例中，我們在 `src/repository/KvChatMessageRepository.ts` 中發現之前製作的假資料一部分被留下。

```ts
export class KvChatMessageRepository implements ChatMessageRepository {
  // Mock data storage - would be replaced with KV in production
  private static mockMessages: Record<string, MessageSchema[]> = {
    'default': [
      {
        id: '0',
        chatId: 'default',
        role: 'assistant',
        content: 'Hello! How can I help you today?'
      }
    ]
  };

  // ...
}
```

推測原因可能是因為被判斷成要先主動發出訊息呈現在畫面上，因此這樣設定，在我們會希望預設是沒有任何訊息的，因此修改為下面的版本。

```ts
export class KvChatMessageRepository implements ChatMessageRepository {
  // Mock data storage - would be replaced with KV in production
  private static mockMessages: Record<string, MessageSchema[]> = {
    'default': [
      {
        id: '0',
        chatId: 'default',
        role: 'assistant',
        content: 'Hello! How can I help you today?'
      }
    ]
  };

  // ...
}
```

這樣一來就完成將對話功能重構為 Clean Architecture 的狀態，透過幾次的實踐我們已經開始能透過 AI 來處理細碎的修改任務，同時又保持容易修改的狀態，開始跟以往開發方式有一些差異。
