跳至主要內容
蒼時弦也
蒼時弦也
資深軟體工程師
發表於

查詢商品 - Clean Architecture in TypeScript

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

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

定義工具

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

 1// ...
 2getProducts: tool({
 3  description: 'Get a list of products available in the store',
 4  parameters: z.object({}),
 5  execute: async () => {
 6    // This is a placeholder for the actual implementation
 7    // You would typically fetch products from a database or an API
 8    return [
 9      { id: '1', name: 'Product 1', price: 10.0 },
10      { id: '2', name: 'Product 2', price: 20.0 }
11    ];
12  }
13}),
14// ...

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

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

定義介面

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

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

1export class Product {
2	constructor(
3		public readonly id: string,
4		public readonly name: string,
5		public readonly price: number
6	) {}
7}

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

1// ...
2export interface ProductRepository {
3	ListAll(): Promise<Product[]>;
4}
5
6export interface ProductPresenter {
7	addProduct(product: Product): void;
8}

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

 1import { ProductPresenter, ProductRepository } from "./interface";
 2
 3export class GetProducts {
 4	constructor(
 5		private readonly productRepository: ProductRepository,
 6		private readonly productPresenter: ProductPresenter
 7	) {}
 8
 9	async execute(): Promise<void> {
10		const products = await this.productRepository.ListAll();
11		products.forEach((product) => {
12			this.productPresenter.addProduct(product);
13		});
14	}
15}

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

進行重構

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

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

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

 1Refactor `LlmAssistantService` to use `GetProducts` usecase to get real products.
 2
 3## MockProductRepository
 4
 5- Add `MockProductRepository` that implements `ProductRepository`.
 6- You can reference `KvCartRepository` for implementation details.
 7- Define 10 mock products in `MockProductRepository`.
 8- You must define a schema for the mock products, including fields like `id`, `name`, `price`, etc.
 9
10## ToolProductPresenter
11
12- Add `ToolProductPresenter` that implements `ProductPresenter`.
13- You can reference `ToolCartPresenter` for implementation details.
14- Must have `toTool()` method that converts products to a format suitable for tools.
15
16## Chat Controller
17
18- Update controller to prepare dependencies for `LlmAssistantService`.
19
20## LlmAssistantService
21
22- Update `getProducts` tool to use `GetProducts` usecase.
23- Inject `MockProductRepository` to `LlmAssistantService`.
24- Inject `ToolProductPresenterer` to `LlmAssistantService`.
25
26Make sure all repository and presenter dependencies are injected through the constructor.

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

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

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

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