---
title: "查詢商品 - Clean Architecture in TypeScript"
date: 2025-10-10T00:00:00+08:00
publishDate: 2025-10-10T00: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/10/10/clean-architecture-in-ts-query-products/"
language: "zh-tw"
---



處理完讀取購物車資訊後，我們還需要讓使用者可以取得能購買的商品。因為是對話式的介面，所以需要將查詢商品的功能變成 AI 助手工具的一部分，雖然會因為語言模型的能力影響好用程度，不過以實驗性專案的情境還是蠻有趣的。

<!--more-->

## 定義工具{#define-tool}

因為直接被 AI 使用，所以我們可以省去前端、API 的實作，只需要在 `src/service/LlmAssistantService.ts` 中加入新的工具定義。

```ts
// ...
getProducts: tool({
  description: 'Get a list of products available in the store',
  parameters: z.object({}),
  execute: async () => {
    // This is a placeholder for the actual implementation
    // You would typically fetch products from a database or an API
    return [
      { id: '1', name: 'Product 1', price: 10.0 },
      { id: '2', name: 'Product 2', price: 20.0 }
    ];
  }
}),
// ...
```

第一階段先以寫死的商品資料為主，同時我們可以直接進行對話測試看看，如果能順利的獲取商品的資訊，那麼就表示工具的定義沒有問題，而語言模型也沒有理解能力上的問題。

有了取得購物車資訊工具的實作經驗後，要再處理新的工具基本上不會太困難。在這次的實作中，我們會全程使用假的商品資料，如果有興趣的話可以嘗試自己擴充跟真實資料整合。
## 定義介面{#define-interface}

跟所有製作 Use Case 的處理相同，我們先將需要實作的介面定義好，後續再轉接給 AI 幫忙轉換成可以使用的實作。

首先，加入 `src/entity/Product.ts` 定義商品的基本資訊。

```ts
export class Product {
	constructor(
		public readonly id: string,
		public readonly name: string,
		public readonly price: number
	) {}
}
```

接下來更新 `src/usecase/interface.ts` 將讀取、顯示商品的介面定義下來。

```ts
// ...
export interface ProductRepository {
	ListAll(): Promise<Product[]>;
}

export interface ProductPresenter {
	addProduct(product: Product): void;
}
```

接著再實作 `src/usecase/getProducts.ts` 這個 Use Case 描述實際的行為。

```ts
import { ProductPresenter, ProductRepository } from "./interface";

export class GetProducts {
	constructor(
		private readonly productRepository: ProductRepository,
		private readonly productPresenter: ProductPresenter
	) {}

	async execute(): Promise<void> {
		const products = await this.productRepository.ListAll();
		products.forEach((product) => {
			this.productPresenter.addProduct(product);
		});
	}
}
```

接下來我們就可以交棒給 AI 幫我們處理後續的實作。

## 進行重構{#refactoring}

原本的 `LlmAssistantService` 並不會實際使用我們新定義的 `getProducst` Use Case，因此需要調整原本的實作加入對應的行為。

以 Aider 為例子，加入 `src/controller/chat.ts` 和 `src/service/LlmAssistantService.ts` 作為可以編輯的檔案，並且以 Read-only 模式加入 `src/usecase/interface.ts`、`src/usecase/getProducts.ts`、`src/entity/Product.ts`、`src/repository/kvCartRepository.ts`、`src/entity/ToolCartPresenter.ts` 等檔案當作參考。

使用類似下方的提示，要求進行對應的修改。

```markdown
Refactor `LlmAssistantService` to use `GetProducts` usecase to get real products.

## MockProductRepository

- Add `MockProductRepository` that implements `ProductRepository`.
- You can reference `KvCartRepository` for implementation details.
- Define 10 mock products in `MockProductRepository`.
- You must define a schema for the mock products, including fields like `id`, `name`, `price`, etc.

## ToolProductPresenter

- Add `ToolProductPresenter` that implements `ProductPresenter`.
- You can reference `ToolCartPresenter` for implementation details.
- Must have `toTool()` method that converts products to a format suitable for tools.

## Chat Controller

- Update controller to prepare dependencies for `LlmAssistantService`.

## LlmAssistantService

- Update `getProducts` tool to use `GetProducts` usecase.
- Inject `MockProductRepository` to `LlmAssistantService`.
- Inject `ToolProductPresenterer` to `LlmAssistantService`.

Make sure all repository and presenter dependencies are injected through the constructor.
```

跟以往的提示一樣，我們預先定義好會產生的檔案以及需要出現的內容，接下來只要等待 AI 將對應的實作完成即可。

到這個階段，大家可能會注意到一個問題，當 AI 的功能越來越多時，我們依靠依賴注入從 Controller 提供的物件（如：Repository）也會不斷增加，要怎麼管理比較好？

這個問題還可以延伸思考，假設 AI 使用的工具可以不受限制存取所有資料，要怎麼避免 AI 存取到其他使用者的資料回答，單靠 System Prompt 肯定是不夠的，我們是否有方法處理？

以 TypeScript 的情境來說還算容易解決，大家可以思考看看。
