想像一下,您擁有一位個人財務規劃師,能夠分析您敏感的交易歷史記錄、標記異常支出,並提供預算最佳化建議,而這一切都 100% 離線運行,直接在您的網頁瀏覽器中進行。
在本指南中,我們將建置一個離線檢索增學生成 (RAG) 助理,由 LiteRT LM Web API 與高效能的裝置端 Gemma 4 E2B 小型語言模型驅動。透過將模型權重直接快取到瀏覽器的快取儲存空間 (Cache Storage) 中,您的應用程式將實現完全的隱私保護、零 Token 成本以及真正的離線運作能力。
雖然 Angular 與 TailwindCSS 提供了使用者介面與樣式,但 LiteRT LM Web API 與裝置端小型模型才是驅動此展示專案 AI 能力的核心。
npm i --save-exact @litert-lm/core tailwindcss postcss @tailwindcss/postcss jsonrepair dexie
npm i --save-exact --save-dev angular-eslint husky lint-staged serve @commitlint/cli
我們安裝了額外的相依套件,用於裝置端 AI、CSS 樣式設計、JSON 回應串流以及 IndexedDB 儲存。開發相依套件(Dev dependencies)則有助於自動偵測程式碼異味(Code smells)、強制執行程式碼品質,並提供生產環境套件的部署服務。
我們將此應用程式設計為完全在裝置端處理交易,以確保瀏覽器內財務資料的隱私。該流程主要分為三個關鍵階段:
模型與資料儲存: 首先,應用程式會下載 Gemma 4 E2B 模型權重,並直接快取在瀏覽器的快取儲存空間中,以便離線使用。當使用者記錄支出時,這些資料會本機儲存於 IndexedDB。
算術接地(Arithmetic Grounding): 小型模型在處理算術查詢時容易產生幻覺,因此我們將支出加總為每月總額、每日總額,以及按類別分類的支出明細。我們將這些預先計算好的數值與使用者查詢一起提供給 Gemma WebGPU 引擎,作為地面真理(Ground truth)。
對話管理: 為了避免超出瀏覽器的 Token 限制,應用程式會在進行三次對話輪次後重設對話,但會保留用戶端最後兩次查詢的記憶。重設時,應用程式會在重新執行這些查詢之前,先用不可變的財務接地資料重新引導模型。這能在將記憶體使用量控制在裝置端限制內的同時,維持對話的連貫性。

接下來,讓我們看看如何實例化這個 LiteRT LM 引擎、定義深入分析服務,並設計 Angular 使用者介面來產生這些深入分析。
雖然完整的程式碼可以在 NG 個人理財助理 儲存庫中找到,但我們的應用程式依賴 IndexedDB 資料庫來離線追蹤支出。您可以在服務檔案中找到資料庫的實作,該服務是透過 Injection Token 注入,並在啟動時使用 provideAppInitializer 進行初始化。
配置好這個本機資料庫來記錄支出後,我們現在可以專注於 AI 整合。以下章節將說明如何快取 Gemma 4 權重,並利用 Angular 22 的響應式(Reactive)特性,直接從交易資料產生安全的本機深入分析。
在瀏覽器中本機運行 LLM 需要兩個步驟:
快取權重:將模型權重(~2GB)儲存在瀏覽器的快取儲存空間中,使應用程式能 100% 離線運行。
引擎引導:建立一個單一且可重複使用的 LiteRT LM 引擎執行個體,以便我們能利用 WebGPU 的速度來串流回應,並避免昂貴的重複初始化延遲。
在討論裝置端 AI 時,人們通常會假設其具有離線運作、零伺服器依賴以及完全隱私等優點。然而,對於 Web AI 來說,這僅實現了一部分。大多數教學都使用 fetch 請求從 HuggingFace 擷取 Gemma 模型,這在連線狀態下運作良好,但如果使用者的網路連線不穩定或無法使用,則會失敗。
這說明了將小型 Gemma 模型快取在快取儲存空間中的好處。裝置只需要連線一次下載模型,之後應用程式就能 100% 離線運行。
此展示專案提供了一個下載按鈕,供使用者下載大小約為 2GB 的 Gemma 4 E2B 模型。

下載成功後,可以前往 Application > Storage > Usage 驗證該模型是否佔用了 2GB 的儲存空間。
讓我們在 AiModelCacheService 中實作此邏輯:
export const AI_CACHE_NAME = 'JMWebAIModels';
export const DEFAULT_MODEL_FILENAME = 'gemma-4-E2B-it-web.litertlm';
export const GEMMA_MODEL_URL = `https://huggingface.co/litert-community/gemma-4-E2B-it-litert-lm/resolve/main/${DEFAULT_MODEL_FILENAME}`;
@Service()
export class AiModelCacheService {
readonly #modelUrl = GEMMA_MODEL_URL;
readonly #state = signal({ status: 'not-downloaded' });
async downloadModel(): Promise<string> {
if (this.#state().status === 'cached') {
return await this.getModelUrl();
}
this.#state.set({ status: 'downloading' });
const localBlobUrl = await FileProxyCache.loadFromURL(this.#modelUrl);
this.#state.set({ status: 'cached' });
return localBlobUrl;
}
async getModelUrl(): Promise<string | null> {
return await FileProxyCache.loadFromURL(this.#modelUrl);
}
}
AiModelCacheService 類別利用了 FileProxyCache(由 Jason Mayes 開發的開源工具,專為快取大型 Web AI 權重而設計)。與使用標準的瀏覽器 fetch(這在下載 ~2GB 的大型資產時可能會崩潰或失敗)不同,FileProxyCache 處理分塊下載並將 Gemma 模型直接儲存到瀏覽器的快取儲存空間中。在隨後的執行中,它會立即載入快取的本機檔案,並將其 Blob URL 傳遞給 LiteRT 引擎,從而實現完全的離線運作能力。
本應用程式僅維護單一的 LiteRT LM 引擎執行個體。為單個查詢初始化和銷毀引擎是一項非常昂貴的操作,這會在等待引擎就緒、啟動新對話和接收訊息時引入延遲。
我們使用 GemmaEngineService 延遲初始化引擎,並在下一節中將其注入到我們的深入分析服務中。
@Service()
export class GemmaEngineService {
readonly #cacheService = inject(AiModelCacheService);
#engine: Engine | null = null;
private async initializeEngine(): Promise<Engine> {
const localBlobUrl = await this.#cacheService.getModelUrl();
const instance = await Engine.create({
model: localBlobUrl,
mainExecutorSettings: {
maxNumTokens: 4096,
},
});
this.#engine = instance;
return instance;
}
getEngine(): Promise<Engine> {
if (this.#engine) {
return Promise.resolve(this.#engine);
}
return this.initializeEngine();
}
... other methods and lifecycle methods ...
}
如果已初始化,該方法會立即傳回引擎。否則,initializeEngine 輔助方法會從快取儲存空間中載入模型,建置 LiteRT LM 引擎並傳回。
我們定義了系統提示、引導提示和使用者提示,以分析並深入探討個人理財和支出狀況。您可以在此處查看我們的確切提示內容。
為了確保 Gemma 4 輸出高品質、結構化的資料,模型必須傳回符合 InsightsResponse 介面的 JSON 回應。您可以在此處查看回應格式與 Insight 介面。
讓我們實作我們的 InsightService 來協調串流過程:
@Service()
export class InsightService {
readonly #engineService = inject(GemmaEngineService);
#conversation: Conversation | null = null;
#lastPrimedExpenses: Expense[] | null = null;
#turnsCount = 0;
#previousQueries: string[] = [];
async *streamInsights(userQuery: string, expenses: Expense[]): AsyncGenerator<InsightsResponse> {
const isContextDifferent = this.#lastPrimedExpenses !== expenses;
const isContextExhausted = this.#turnsCount >= 3;
// Reset & prime context when threshold is met
if (!this.#conversation || isContextDifferent || isContextExhausted) {
await this.primeContext(expenses, isContextExhausted ? this.#previousQueries : undefined);
}
const userPrompt = INSIGHTS_USER_PROMPT(userQuery);
const stream = await this.#conversation.sendMessageStreaming(userPrompt);
this.#previousQueries.push(userQuery);
this.#turnsCount = this.#turnsCount + 1;
// Yield repaired, streamed JSON chunks (using jsonrepair)
yield* this.processStream(stream);
}
/* processStream, lifecycle, and other helper methods */
}
我們的 primeContext 方法會將交易資料格式化、計算高層級的支出統計數據,並將它們附加到查詢內容中,從而保證 Gemma 4 產生可靠、有依據(Grounded)的 JSON 回應。
processStream 方法會傳回一個非同步產生器(Asynchronous generator),用於反覆迭代串流以擷取每個分塊的內容。由於 LLM 的回應串流是以片段(Fragment-by-fragment)形式傳遞,原始緩衝區通常是不完整的(例如:未閉合的大括號或中括號)。嘗試對這些部分分塊執行標準的 JSON.parse() 會立即拋出語法錯誤。為了解決這個問題,jsonrepair 函式庫會在執行過程中動態修補並閉合不完整的 JSON 字串,使應用程式能夠成功解析並以漸進方式呈現 UI 更新。
HistoryInsightsService 在我們的 UI 元件與核心 InsightService 之間扮演著乾淨的外觀(Facade)角色,使我們能夠輕鬆查詢支出並根據使用者的提問串流深入分析。
我們的元件會注入此服務,以非同步方式觸發並呈現 JSON 回應。
HistoryInsightsComponent 由子元件和一個 Pipe 組成。然而,與 Gemma 4 通訊以產生深入分析清單的主要方法是 onAskGemma。
@Component({
selector: 'app-history-insights',
...
})
export default class HistoryInsightsComponent {
protected readonly vm = inject(HistoryInsightsService);
readonly expenses = signal<Expense[]>([]);
readonly streamingResponse = signal<InsightsResponse>({ insights: [] });
async onAskGemma(query: string): Promise<void> {
const trimmed = query.trim();
if (!trimmed) {
return;
}
this.streamingResponse.set({ insights: [] });
// The expenses are populated after a search query
// Consume the stream generator and update the UI signal incrementally
const generator = this.vm.streamInsights(trimmed, this.expenses());
for await (const response of generator) {
this.streamingResponse.set(response);
}
}
}
onAskGemma 事件處理常式會呼叫 HistoryInsightsService 的 streamInsights 方法來獲取串流回應,並逐步更新 streamingResponse 的 Signal。衍生的 aiState Signal 則會計算當前狀態、捕獲 any 錯誤,並將這些值直接綁定到子元件,以即時呈現串流回應。
使用 LiteRT LM Web SDK、Gemma 4 E2B 小型模型和 Angular 建置裝置端個人理財助理的旅程至此結束。本篇部落格文章展示了如何透過預先在快取儲存空間中快取模型,來建置離線 RAG 應用程式。即使關閉 Wi-Fi 連線,個人理財助理依然能夠針對使用者的查詢提供深入分析。
我們透過提供預先計算好的聚合數據作為 LLM 的上下文,解決了 API 缺乏工具調用(Tool-calling)能力以及小型模型容易產生數學幻覺的問題。這能防止最終結果中出現諸如 "60 + 40.5 = 200.2" 這類錯誤算式。
工程師現在可以利用本機模型建置網頁應用程式來進行財務分析,而無需擔心將敏感的個人資料洩露給 AI 廠商的訓練資料集。