查詢商品 - Clean Architecture in TypeScript
處理完讀取購物車資訊後,我們還需要讓使用者可以取得能購買的商品。因為是對話式的介面,所以需要將查詢商品的功能變成 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.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 等檔案當作參考。
使用類似下方的提示,要求進行對應的修改。
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 的情境來說還算容易解決,大家可以思考看看。
喜歡這篇文章?請我喝杯奶茶 🧋