蒼時弦也
蒼時弦也
資深軟體工程師
發表於

模擬語言模型呼叫 - Clean Architecture in TypeScript

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

因為 LLM 的不確定性,再加上大多數服務都會採取呼叫 API 的方式來使用,我們很難在測試中直接呼叫,比較好的做法是將 LLM 呼叫使用 Mock 方式處理。

如果要測試 LLM 的表現,則會使用 Evaluation(評估)的方式,並不在這系列討論的範圍中

Cloudflare Worker 的限制

雖然 AI SDK 在 TypeScript 生態系能讓我們非常容易的整合 LLM 來使用,但 Cloudflare Worker 在測試環境時有一些限制,反而讓 AI SDK 內建的測試工具無法使用。

原因在於 Cloudflare Worker 是使用自己的 Runtime 而非 Node.js 因此有一部分的 Node.js API 是無法使用的,這也是我們需要在特定應用啟用 nodejs_compact 這個選項。

很剛好的 AI SDK 的 MockLanguageModel 會實際使用 Node.js API 中的一部分功能,用來模擬一個真實的 LLM API 來提供測試環境使用,這剛好是 Cloudflare Worker 環境不支援的部分,截至 2025 年 9 月為止,雖然加入了一部分的相容,但並沒有完整的實作所有 API。

也因此,想要在 Cloudflare Worker 環境測試 AI SDK 為基礎的 AI 功能,就需要自己實作 LanguageModel 這個介面,用來替代我們原本使用 AI SDK 產生的模型(如:GPT-4.1)

產生 MockLanguageModel

AI SDK 本身已經是相當複雜、龐大的生態系,光是 LanguageModel 這個介面就有非常多方法需要實現,在過去我們透過閱讀原始碼、理解意圖再實作的方式會花費不少力氣,但這就很適合使用 Coding Agent 來協助我們

以下是一個 Prompt 的範例,用於讓 Claude Code 尋找 LanguageModelV2 介面,並且實作測試所需的必要實作

We should add `POST /chat/:id` tests to `chat.spec.ts`

## MockLanguageModel

Implement `MockLanguageModel` which implements `LanguageModel` interface from AI SDK and have `doGenerate()` to return mock action

```
import { LanguageModelV2 } from '@ai-sdk/provider'

export class MockLanguageModel implements LanguageModelV2 {
}
```

Keep `MockLanguageModel` simple with minimal implementation, use `npm exec tsc -- -p test/tsconfig.json` to verify

**IMPORTANT**: Ensure you have read the interface contract from AI SDK before implement the class, the `LanguageModelV2` from `@ai-sdk/provider` is correct do not change it if you cannot pass `tsc` check

## Remove MockAssistantService

Remove `MockAssistantService` use `MockLanguageModel` to verify assistant service is covered

## Add test case for POST /chat/:id

Use `describe('GET /chat/:id')` and `describe('POST /chat/:id')` to group tests in `chat.spec.ts`

- Test add new chat message
- Test tool use works correctly
- Use API response to verify it

After everything is ready, use `npm run test` to ensure all test is passed

實際運行後,會發現很不容易成功,即使使用了 Opus 4.1 已經相對頂尖的模型,還是有可能實作失敗,上述的 Prompt 已經是盡可能完整描述任務的版本。

這就要深入一些比較精細的 Prompt 撰寫技巧,像是我們單一任務的規模、現有實作有多少參考價值這類問題,像是我們可以考慮先區分為實作 MockLanguageModel 再補上測試跟對應的測試步驟,會讓成功率提高一些⋯⋯

平衡

這一系列嘗試使用 AI 來輔助開發,但實際的過程中並不一定會跟撰寫文章時示範的一樣順利,或者在製作範例時就已經多次嘗試調整,提供比較容易的 Prompt 版本。

正因為 AI 的不確定性,我們需要更仔細看待「手寫」跟「生成」兩者之間的平衡,以 LanguageModel 作為例子,假設我們連這個介面的存在都不清楚,那麼連生成的方法都不會出現,基本閱讀原始碼尋找關鍵知識的能力還是要有。

除此之外,到這個階段的實作,會發現 AI 生成的非常多功能都是「不太一致」的狀況,可能是風格、處理的習慣,即使都可以順利運作起來,也會發現每一次 Claude Code 在尋找參考的程式碼時,似乎仍然無法使用相同的方式實現。

這也是我們需要在 AI 輔助開發時取捨的地方,一些關鍵的行為、介面設計採用手寫的方式更容易讓 AI 理解到專案採用的風格跟規範,進而產生更穩定的實作。