直接在 VisionService 與 TextToSpeechService 內部呼叫 getGenerativeModel(),會重複撰寫 firebase/ai 匯入、安全設定(Safety Settings)與思考設定(Thinking Configurations),同時在每次請求時於用戶端重新建立 GenerativeModel 物件,進而觸發不必要的垃圾回收(Garbage Collection)。在本篇部落格文章中,我們將抽離出 AiModelFactory 來集中管理並快取模型執行個體以支援多模型路由,同時解決兩個隱藏的快取陷阱:請求層級的 generationConfig 覆寫問題,以及 Remote Config 更新後導致的過期 AI 後端問題。
VisionService 與 TextToSpeechService 將模型的建立與生命週期管理委派給 AiModelFactory:
VisionService 會傳入 AiModelOptions 呼叫 getModel(options),而 TextToSpeechService 則呼叫 getTtsModel()。getValidBackend()):在回傳模型之前,getValidBackend() 會將 ConfigService.getAiBackend() 與 #cachedBackend 進行比對。如果 Remote Config 重新整理了 AI 後端,它會清空 #models 並將 #ttsModel 重設為 null。#models):getModel(options) 會透過 generateKey() 建立決定性的快取鍵(Deterministic Key),並將執行個體快取於 #models(Map<string, GenerativeModel>)中。#ttsModel):getTtsModel() 會快取單一 #ttsModel 執行個體,並將其設定為 responseModalities: [ResponseModality.AUDIO]。flowchart TD
subgraph Callers["Domain Services"]
VS["VisionService (1 model now -> 3 models with MediaPipe routing)"]
TTS["TextToSpeechService (per-request voice speechConfig)"]
end
subgraph Factory["AiModelFactory (@/core/firebase)"]
GVB["getValidBackend(): compare with #cachedBackend"]
EVICT["Backend changed? #models.clear() & #ttsModel = null"]
GM["getModel(options?: AiModelOptions)"]
KEY["generateKey(): model_thinking_instruction_schema_tools"]
MAP[("#models: Map<string, GenerativeModel>")]
GTTS["getTtsModel()"]
TTS_CACHE[("#ttsModel: GenerativeModel | null")]
end
VS -- "getModel(options)" --> GVB
TTS -- "getTtsModel()" --> GVB
GVB -- "New AI instance" --> EVICT --> GM & GTTS
GVB -- "Same AI instance" --> GM & GTTS
GM --> KEY --> MAP
GTTS --> TTS_CACHE
AiModelOptions 與決定性快取鍵AiModelOptions 介面AiModelOptions 介面定義了建立視覺模型以及計算其快取鍵所需的設定資訊:
systemInstruction、schema(SchemaRequest | TypedSchema)與 tools 陣列(Tool[])可讓呼叫端設定結構化 JSON 輸出、純文字回應或接地(Grounding)工具。// src/app/core/firebase/ai-model-options.interface.ts
import type { SchemaRequest, Tool, TypedSchema } from 'firebase/ai';
export interface AiModelOptions {
systemInstruction?: string;
schema?: SchemaRequest | TypedSchema;
tools?: Tool[];
}
AiModelFactory 中的私有狀態與 generateKey()#configService):AiModelFactory 將 ConfigService 注入至 #configService,以讀取 Remote Config 數值並取得 Firebase AI Logic 後端。#cachedBackend):#cachedBackend 儲存用於建立生成式模型的有效 AI 後端參照。#ttsModel):#ttsModel 保存單一執行個體的 TTS 模型,用於將英文文字合成為語音。#models 與 generateKey):#models 是一個 Map<string, GenerativeModel>,會以 generateKey() 回傳的索引鍵來儲存視覺模型。該索引鍵會以底線(_)字元串接模型名稱(geminiModelName)、thinkingLevel、systemInstruction、結構化輸出的 schema 以及 tools。// src/app/core/firebase/ai-model-factory.service.ts (fields & key generation)
import type { AI, GenerationConfig, GenerativeModel, ModelParams, SafetySetting } from 'firebase/ai';
@Service()
export class AiModelFactory {
readonly #configService = inject(ConfigService);
readonly #models = new Map<string, GenerativeModel>();
#ttsModel: GenerativeModel | null = null;
#cachedBackend: AI | null = null;
private generateKey(options?: AiModelOptions): string {
const { geminiModelName, thinkingLevel } = this.#configService.appConfig;
const instruction = options?.systemInstruction ?? '';
const schemaKey = options?.schema ? JSON.stringify(options.schema) : '';
const toolsKey = options?.tools?.length ? JSON.stringify(options.tools) : '';
return `${geminiModelName}_${thinkingLevel}_${instruction}_${schemaKey}_${toolsKey}`;
}
AiModelFactory 中依需求建立模型與使快取失效createModel、createModelParams、createGenerationConfig)我們很容易想直接在檔案頂端匯入 HarmBlockThreshold 與 HarmCategory 列舉(Enum),但這麼做會導致 firebase/ai 的執行階段程式碼洩漏至 main.js 中,進而增加初始打包檔案的大小:
createModel):createModel() 會透過動態匯入從 firebase/ai 載入 getGenerativeModel、HarmBlockThreshold 與 HarmCategory。createModel):createModel() 會建立 SafetySetting[] 陣列,並呼叫 getGenerativeModel(backend, modelParams)。createGenerationConfig):createGenerationConfig() 會設定 thinkingLevel 並將 includeThoughts 設為 true。createGenerationConfig):當呼叫端提供 options?.schema 時,createGenerationConfig() 會附加 responseSchema 與 responseMimeType: 'application/json'。createModelParams):createModelParams() 會結合模型名稱、generationConfig 與 safetySettings,並依條件加入 tools 與 systemInstruction。 // src/app/core/firebase/ai-model-factory.service.ts (model creation helpers)
private async createModel(backend: AI, options?: AiModelOptions): Promise<GenerativeModel> {
const { getGenerativeModel, HarmBlockThreshold, HarmCategory } = await import('firebase/ai');
const safetySettings: SafetySetting[] = [
{ category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, threshold: HarmBlockThreshold.BLOCK_ONLY_HIGH },
{ category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, threshold: HarmBlockThreshold.BLOCK_ONLY_HIGH },
{ category: HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: HarmBlockThreshold.BLOCK_ONLY_HIGH },
{ category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold: HarmBlockThreshold.BLOCK_ONLY_HIGH },
];
const modelParams = this.createModelParams(safetySettings, options);
return getGenerativeModel(backend, modelParams);
}
private createModelParams(safetySettings: SafetySetting[], options: AiModelOptions | undefined): ModelParams {
const generationConfig = this.createGenerationConfig(options);
const modelParams: ModelParams = {
model: this.#configService.appConfig.geminiModelName,
generationConfig,
safetySettings,
};
if (options?.tools?.length) {
modelParams.tools = options.tools;
}
if (options?.systemInstruction) {
modelParams.systemInstruction = options.systemInstruction;
}
return modelParams;
}
private createGenerationConfig(options: AiModelOptions | undefined): GenerationConfig {
const generationConfig: GenerationConfig = {
thinkingConfig: {
thinkingLevel: this.#configService.appConfig.thinkingLevel,
includeThoughts: true,
},
};
if (options?.schema) {
generationConfig.responseSchema = options.schema;
generationConfig.responseMimeType = 'application/json';
}
return generationConfig;
}
getModel 與 getTtsModel)在 AiModelFactory 中,取得視覺模型與取得 TTS 模型遵循兩種不同的快取流程:
getModel):getModel() 會透過 generateKey(options) 從 AiModelOptions 產生決定性快取鍵,並檢查 this.#models.get(key)。#models 時,getModel() 會呼叫 createModel() 來實例化模型,並將其存入映射表中。getTtsModel):getTtsModel() 會呼叫一次 createTtsModel(),以實例化設定為 responseModalities: [ResponseModality.AUDIO] 的單例 TTS 模型,並將其指派給 #ttsModel。getGenerativeModel() 以及不必要的垃圾回收。 // src/app/core/firebase/ai-model-factory.service.ts (on-demand getters)
async getModel(options?: AiModelOptions): Promise<GenerativeModel> {
const backend = await this.getValidBackend();
const key = this.generateKey(options);
let model = this.#models.get(key);
if (!model) {
model = await this.createModel(backend, options);
this.#models.set(key, model);
}
return model;
}
private async createTtsModel(ai: AI): Promise<GenerativeModel> {
const { getGenerativeModel, ResponseModality } = await import('firebase/ai');
return getGenerativeModel(ai, {
model: this.#configService.appConfig.geminiTTSModelName,
generationConfig: {
responseModalities: [ResponseModality.AUDIO],
},
});
}
async getTtsModel(): Promise<GenerativeModel> {
const backend = await this.getValidBackend();
if (!this.#ttsModel) {
this.#ttsModel = await this.createTtsModel(backend);
}
return this.#ttsModel;
}
getValidBackend)GenerativeModel 需要有效的 Firebase AI Logic AI 後端;當相依的 Firebase Remote Config 參數(例如 useLimitedUseAppCheckTokens 與 vertexAILocation)收到更新值且 ConfigService 重設其 AI 執行個體時,原先的後端就會過期。getValidBackend):當 getValidBackend() 偵測到 this.#configService.getAiBackend() 與 #cachedBackend 不再相符時,它會更新 #cachedBackend、清空 #models 映射表,並將 #ttsModel 重設為 null。 // src/app/core/firebase/ai-model-factory.service.ts (cache invalidation)
private async getValidBackend(): Promise<AI> {
const backend = await this.#configService.getAiBackend();
if (backend !== this.#cachedBackend) {
this.#cachedBackend = backend;
this.#models.clear();
this.#ttsModel = null;
}
return this.#cachedBackend;
}
}
VisionService 與 TextToSpeechService(以及淺層展開陷阱)VisionService 以支援未來的多模型路由我們透過將模型建立邏輯委派給 AiModelFactory,重構了 VisionService 以提升其擴充性與擴展性:
AiModelFactory 後,即可從 VisionService 移除 SAFETY_SETTINGS、ConfigService 與私有的 getGenerativeAIModel() 輔助方法。altTextPrompt:將 altTextPrompt 移至 vision.prompt.ts,可讓 VisionService 專注於協調影像分析流程——產生標籤、撰寫無障礙替代文字(Alt Text),以及發掘冷知識。VisionService 將 AiModelFactory 注入至 #aiModelFactory,並呼叫 this.#aiModelFactory.getModel({ schema: ImageAnalysisSchema, tools: [{ googleSearch: {} }] })。雖然目前的模型使用 Google Search 接地工具,但 VisionService 也可以透過 AiModelOptions 傳入 Google Maps 工具或自訂函式呼叫(Function Calling)工具來設定其他模型。getModel() 會依設定鍵快取執行個體,我們可以透過裝置端(On-Device)的 MediaPipe 分類器擴充 VisionService,依需求請求 3 個專用模型(地點、人物與臉孔,以及一般物件)。// src/app/core/vision/vision.service.ts (refactored excerpt)
@Service()
export class VisionService {
readonly #window = inject(WINDOW);
readonly #aiModelFactory = inject(AiModelFactory);
async analyzeImage(image: File): Promise<ImageAnalysisResponse> {
const { data, mimeType, optimizationMetrics } = await preprocessImageForVision(image, this.#window);
const imagePart: InlineDataPart = {
type: 'inlineData',
inlineData: { data, mimeType },
};
const aiModel = await this.#aiModelFactory.getModel({
schema: ImageAnalysisSchema,
tools: [{ googleSearch: {} }],
});
const result = await aiModel.generateContent([altTextPrompt, imagePart]);
// ... parse response, tokenUsage, and grounding citations
}
}
TextToSpeechService 中重用 #ttsModel 而不覆蓋 responseModalities同樣地,TextToSpeechService 將 TTS 模型的建立委派給 AiModelFactory,並在每次請求時傳入所選的語音:
TextToSpeechService 將 AiModelFactory 注入至 #aiModelFactory,並由 createAIModelGenerationRequest() 透過 this.#aiModelFactory.getTtsModel() 取得快取的 TTS 模型,而不再於每次合成呼叫時實例化新模型。speechConfig:createAIModelGenerationRequest(text, voiceName) 輔助方法會建立 GenerateContentRequest(由 synthesizeStream() 與 synthesize() 共用),並以請求的 voiceName 與 languageCode: 'en-US' 設定 speechConfig。ResponseModality.AUDIO:在 @firebase/ai 中,傳入請求層級的 generationConfig 會覆寫模型預設的 aiModel.generationConfig。將 ...aiModel.generationConfig 展開(Spread)至請求的 generationConfig 中,即可在加入 speechConfig 的同時保留 responseModalities: [ResponseModality.AUDIO]。// src/app/core/speech/text-to-speech.service.ts (refactored excerpt)
@Service()
export class TextToSpeechService {
readonly #aiModelFactory = inject(AiModelFactory);
private async createAIModelGenerationRequest(text: string, voiceName: string): Promise<AIModelGenerativeRequest> {
const aiModel = await this.#aiModelFactory.getTtsModel();
const contents: Content[] = [
{
role: 'user',
parts: [{ type: 'text', text }],
},
];
const generationConfig: GenerationConfig = {
...aiModel.generationConfig,
speechConfig: {
voiceConfig: {
prebuiltVoiceConfig: { voiceName },
},
languageCode: 'en-US',
},
};
return {
aiModel,
generationRequest: { contents, generationConfig },
};
}
async *synthesizeStream({ text, voice }: SpeechPrompt): AsyncGenerator<AudioStreamChunk> {
const { aiModel, generationRequest } = await this.createAIModelGenerationRequest(text, voice);
const responseStream = await aiModel.generateContentStream(generationRequest);
for await (const chunk of responseStream.stream) {
const chunkData = this.extractValidChunkData(chunk);
if (chunkData) {
yield decodeAudioChunk(chunkData.data, chunkData.mimeType);
}
}
}
async synthesize({ text, voice }: SpeechPrompt): Promise<Blob> {
const { aiModel, generationRequest } = await this.createAIModelGenerationRequest(text, voice);
const result = await aiModel.generateContent(generationRequest);
const chunk = this.extractValidChunkData(result.response);
if (!chunk) {
throw new Error('No audio data received in response.');
}
return toWavBlob(chunk.data, chunk.mimeType);
}
}
AiModelFactory 會動態匯入 firebase/ai 來建構文字轉語音(TTS)與影像分析模型。它集中管理影像分析模型的安全設定與 thinkingLevel,同時將 TTS 模型的 responseModalities 鎖定為 [ResponseModality.AUDIO]。AiModelFactory 會將模型快取於 #models 映射表或 #ttsModel 欄位中並跨請求重用,消除每次請求不必要的 GenerativeModel 實例化與垃圾回收。VisionService 具備可擴展的多模型路由:#models 映射表中的鍵值快取機制讓 VisionService 具備擴展性,使未來整合的 MediaPipe 分類器能將影像路由至專用模型(地點、人物與臉孔,以及一般物件)進行分析。ResponseModality.AUDIO:由於文字轉語音合成會在每次請求時接收語音參數,將 ...aiModel.generationConfig 展開至請求的 generationConfig 中,即可在傳入所選 voiceName 的同時保留 ResponseModality.AUDIO。ConfigService 在背景擷取 Remote Config 數值並使快取的 AI 後端失效時,AiModelFactory 會清除所有快取模型,並在下一次呼叫時重新建立新的執行個體。