iT邦幫忙

0

完全離線:使用 LiteRT 與 Gemma 4 建置隱私優先的個人理財助理

  • 分享至 

  • xImage
  •  

想像一下,您擁有一位個人財務規劃師,能夠分析您敏感的交易歷史記錄、標記異常支出,並提供預算最佳化建議,而這一切都 100% 離線運行,直接在您的網頁瀏覽器中進行。

在本指南中,我們將建置一個離線檢索增學生成 (RAG) 助理,由 LiteRT LM Web API 與高效能的裝置端 Gemma 4 E2B 小型語言模型驅動。透過將模型權重直接快取到瀏覽器的快取儲存空間 (Cache Storage) 中,您的應用程式將實現完全的隱私保護、零 Token 成本以及真正的離線運作能力。

1. 先決條件

  • LiteRT LM Web API
  • Gemma 4 E2B 模型
  • Angular 22
  • TailwindCSS
  • Node 24

雖然 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)、強制執行程式碼品質,並提供生產環境套件的部署服務。

2. 架構設計

我們將此應用程式設計為完全在裝置端處理交易,以確保瀏覽器內財務資料的隱私。該流程主要分為三個關鍵階段:

  • 模型與資料儲存: 首先,應用程式會下載 Gemma 4 E2B 模型權重,並直接快取在瀏覽器的快取儲存空間中,以便離線使用。當使用者記錄支出時,這些資料會本機儲存於 IndexedDB。

  • 算術接地(Arithmetic Grounding): 小型模型在處理算術查詢時容易產生幻覺,因此我們將支出加總為每月總額、每日總額,以及按類別分類的支出明細。我們將這些預先計算好的數值與使用者查詢一起提供給 Gemma WebGPU 引擎,作為地面真理(Ground truth)。

  • 對話管理: 為了避免超出瀏覽器的 Token 限制,應用程式會在進行三次對話輪次後重設對話,但會保留用戶端最後兩次查詢的記憶。重設時,應用程式會在重新執行這些查詢之前,先用不可變的財務接地資料重新引導模型。這能在將記憶體使用量控制在裝置端限制內的同時,維持對話的連貫性。

使用 LiteRT LM Web SDK 與 Gemma 4 建置的本機財務助理架構圖

接下來,讓我們看看如何實例化這個 LiteRT LM 引擎、定義深入分析服務,並設計 Angular 使用者介面來產生這些深入分析。

3. 原始碼

雖然完整的程式碼可以在 NG 個人理財助理 儲存庫中找到,但我們的應用程式依賴 IndexedDB 資料庫來離線追蹤支出。您可以在服務檔案中找到資料庫的實作,該服務是透過 Injection Token 注入,並在啟動時使用 provideAppInitializer 進行初始化。

配置好這個本機資料庫來記錄支出後,我們現在可以專注於 AI 整合。以下章節將說明如何快取 Gemma 4 權重,並利用 Angular 22 的響應式(Reactive)特性,直接從交易資料產生安全的本機深入分析。

4. Gemma 模型快取與引擎初始化

在瀏覽器中本機運行 LLM 需要兩個步驟:

  1. 快取權重:將模型權重(~2GB)儲存在瀏覽器的快取儲存空間中,使應用程式能 100% 離線運行。

  2. 引擎引導:建立一個單一且可重複使用的 LiteRT LM 引擎執行個體,以便我們能利用 WebGPU 的速度來串流回應,並避免昂貴的重複初始化延遲。

4.1 將 Gemma 4 快取至瀏覽器的快取儲存空間 (Cache Storage)

在討論裝置端 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 引擎,從而實現完全的離線運作能力。

4.2. 延遲初始化 LiteRT LM 引擎

本應用程式僅維護單一的 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 引擎並傳回。

5. Angular 中的深入分析服務定義

我們定義了系統提示、引導提示和使用者提示,以分析並深入探討個人理財和支出狀況。您可以在此處查看我們的確切提示內容

為了確保 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 更新。

6. Angular 中的響應式 UI 建置

6.1 歷史深入分析外觀(Facade)

HistoryInsightsService 在我們的 UI 元件與核心 InsightService 之間扮演著乾淨的外觀(Facade)角色,使我們能夠輕鬆查詢支出並根據使用者的提問串流深入分析。

我們的元件會注入此服務,以非同步方式觸發並呈現 JSON 回應。

HistoryInsightsService 原始碼

6.2 歷史深入分析元件

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 事件處理常式會呼叫 HistoryInsightsServicestreamInsights 方法來獲取串流回應,並逐步更新 streamingResponse 的 Signal。衍生的 aiState Signal 則會計算當前狀態、捕獲 any 錯誤,並將這些值直接綁定到子元件,以即時呈現串流回應。

HistoryInsightsComponent 完整列表

7. 結論

使用 LiteRT LM Web SDK、Gemma 4 E2B 小型模型和 Angular 建置裝置端個人理財助理的旅程至此結束。本篇部落格文章展示了如何透過預先在快取儲存空間中快取模型,來建置離線 RAG 應用程式。即使關閉 Wi-Fi 連線,個人理財助理依然能夠針對使用者的查詢提供深入分析。

我們透過提供預先計算好的聚合數據作為 LLM 的上下文,解決了 API 缺乏工具調用(Tool-calling)能力以及小型模型容易產生數學幻覺的問題。這能防止最終結果中出現諸如 "60 + 40.5 = 200.2" 這類錯誤算式。

工程師現在可以利用本機模型建置網頁應用程式來進行財務分析,而無需擔心將敏感的個人資料洩露給 AI 廠商的訓練資料集。

相關資源


圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言