iT邦幫忙

2025 iThome 鐵人賽

DAY 22
0
生成式 AI

AI 產品與架構設計之旅:從 0 到 1,再到 Day 2系列 第 22

Day 22: 當重構遇上「相容」的真相 - 一個關於思考過程消失的故事

  • 分享至 

  • xImage
  •  

嗨大家,我是 Debuguy。

還記得 Day 16 我們為了安全性和成本控制,把 GenKit 從直接連 Google AI 改成透過 LiteLLM 代理嗎?當時測試都正常,Code Review 也過了,就開心地 merge 了。

結果過了幾天,在測試其他功能時突然發現:

「欸...思考過程的串流怎麼不見了?Day 9 不是做好了嗎?」

一開始以為是網路問題,重開了幾次還是不行。突然靈光一閃:

「該不會...是 Day 16 的重構造成的?」

回顧架構演進

Day 9 的架構(原始版本)

那時候我們直接用 Google AI Plugin:

// GenKit/src/index.ts (Day 9)
import { googleAI } from '@genkit-ai/google-genai';

const ai = genkit({
  plugins: [
    googleAI(),
  ],
});

Prompt 設定長這樣:

# prompts/chatbot.prompt (Day 9)
---
model: googleai/gemini-2.5-flash-lite
config:
  temperature: 0.2
  topP: 0.95
  topK: 30
  thinkingConfig:
    thinkingBudget: -1      # 啟用動態思考
    includeThoughts: true   # 包含思考過程
---

串流處理的邏輯:

const { stream, response } = ai.prompt('chatbot').stream({
  // ...
});

for await (const chunk of stream) {
  for (const content of chunk.content) {
    if (content.reasoning) {
      sendChunk(chunk.reasoning);  // ✅ 可以收到思考過程!
    }
  }
}

當時測試完全正常,Bot 會即時顯示 AI 的思考過程,體驗很棒。

Day 16 的架構(重構後)

為了安全性和成本控制,我們引入了 LiteLLM:

// GenKit/src/index.ts (Day 16)
import { liteLlm } from './plugin/litellm.js';

const ai = genkit({
  plugins: [
    liteLlm(),  // 改用自己寫的 plugin
  ],
});

LiteLLM Plugin 的核心實作:

// GenKit/src/plugin/litellm.ts
import openAICompatible from '@genkit-ai/compat-oai';

export const liteLlm = (): GenkitPluginV2 => {
  return openAICompatible({  // 基於 compat-oai
    name: 'litellm',
    apiKey: process.env['LITELLM_API_KEY'],
    baseURL: process.env['LITELLM_API_URL'],
    // ...
  });
};

「架構看起來更優雅了,測試也都過了...但為什麼思考過程不見了?」

Debug 之旅:兩條線索

線索一:參數名稱長得不太對

我先檢查了最可疑的地方 — Prompt 設定:

# prompts/chatbot.prompt (Day 16 還是用舊的)
---
model: litellm/gemini-2.5-flash-lite
config:
  temperature: 0.2
  topP: 0.95       
  topK: 30
  thinkingConfig:
    thinkingBudget: -1
    includeThoughts: true
---

「等等...model 換了,參數不是也要跟著改嗎?」

因為我們用的是 gemini-2.5-flash-lite,根據 Google 文件,是用 thinkingBudget: -1 來啟動 dynamic thinking(因為 flash-lite 預設是 disable thinking)。

但現在我們改用 LiteLLM 轉接,config 要改用 OpenAI 的格式。趕緊翻了 LiteLLM 的文件,發現一個關鍵事實:

LiteLLM 支援 reasoning models,但參數格式遵循 OpenAI 的規範!

OpenAI 的 reasoning model (o1 系列) 的參數是這樣的:

config:
  reasoning_effort: "low" | "medium" | "high"

而且:

  • ❌ 不支援 topP(OpenAI 的 o1 不支援這參數)
  • ❌ 不支援 thinkingConfig(這是 Google 專屬的)

「好吧,參數名稱錯了...改一下應該就好了...」

改完參數重跑後:

「還是沒有!這...不科學啊?」

線索二:更深層的問題

還好在 Day 17 我們架設了 Langfuse,可以攔截到從 LiteLLM 輸出的內容。打開 trace 一看:

Langfuse 截圖顯示有 reasoning_content

「咦?LiteLLM 明明有回傳 reasoning 啊!而且欄位名稱是 reasoning_content...」

突然想到一件事:

「該不會是 plugin 本身的問題?」

仔細看了 Day 16 寫的 LiteLLM plugin,它是基於 @genkit-ai/compat-oai 這個套件:

import openAICompatible from '@genkit-ai/compat-oai';

export const liteLlm = (): GenkitPluginV2 => {
  return openAICompatible({  // 這裡!
    // ...
  });
};

openAICompatible...這不就是 OpenAI API 的抽象層嗎?」

趕緊去翻 OpenAI 的 Chat Completion API 文件

OpenAI 的標準 API 根本不包含 reasoning_content!

真相大白了!

  1. ✅ LiteLLM 確實有回傳 reasoning_content
  2. ✅ 但 @genkit-ai/compat-oai 只處理標準 OpenAI 格式
  3. ❌ 所以 reasoning_content 被直接忽略了!

「原來如此...我被『OpenAI-compatible』這四個字騙了」

技術深入:OpenAI API 的「方言」問題

標準 vs. 擴充

雖然大家都說自己是「OpenAI-compatible」,但現實很複雜。

各家的擴充:

提供商 擴充欄位 說明
xAI (Grok) reasoning_content 文件
DeepSeek reasoning_content 文件
LiteLLM reasoning_content 文件

問題的核心是:

@genkit-ai/compat-oai 是基於「標準 OpenAI API」設計的,不認識這些擴充欄位。

為什麼 Google AI Plugin 沒問題?

// @genkit-ai/google-genai 的實作
// 它直接對接 Gemini API
// 完全了解 Gemini 的 thinking 機制
// 知道怎麼處理 reasoning 內容

當我們從 Google AI Plugin 切換到 OpenAI-compatible Plugin 時:

獲得:

  • ✅ 統一的抽象層
  • ✅ 可以輕鬆切換模型
  • ✅ LiteLLM 帶來的安全性和成本控制

失去:

  • ❌ 對 Gemini 特有功能的原生支援
  • ❌ reasoning_content 的自動處理

這就是架構決策的取捨。

解決方案:三條路,我該選哪一條?

發現問題後,我開始思考怎麼解決。擺在眼前的有三條路:

選項 A:改回 Google AI Plugin

優點:

  • ✅ 原生支援 Gemini 的所有功能
  • ✅ 思考過程串流立刻恢復
  • ✅ 不用改 code,只要改回 import

缺點:

  • ❌ 失去 LiteLLM 的安全性保護
  • ❌ 失去成本控制機制
  • ❌ 失去 Virtual Key 管理
  • Day 14-16 的努力全白費了

「這個...好像不太行。Day 14-16 可是為了生產環境的安全性才做的重構啊」

選項 B:修改 @genkit-ai/compat-oai

優點:

  • ✅ 一勞永逸,之後其他 reasoning model 也受益
  • ✅ 保持 LiteLLM 架構
  • ✅ 對社群有貢獻

缺點:

  • ❌ 需要時間研究 GenKit 的內部實作
  • ❌ 需要提 PR 等 review
  • ❌ 不知道什麼時候會 merge

選項 C:Workaround + 長期方案

短期:

  1. 修正參數名稱(reasoning_effort: "medium"
  2. 暫時接受沒有即時思考過程串流
  3. 在 Slack 回覆中加註「完整思考過程請點 Langfuse 連結查看」

長期:

  1. 研究 @genkit-ai/compat-oai 的實作
  2. 提 PR 加入 reasoning_content 支援
  3. 等 merge 後升級

「這個...好像比較務實?」

最終決定:務實主義萬歲

經過一番掙扎,我選擇了選項 C

為什麼?

1. 使用者體驗不能打折

雖然暫時看不到即時的思考過程,但 Langfuse trace link 還在,點開就能看完整的。比起退回去用沒有安全保護的架構,這個折衷可以接受。

2. 技術債要還,但不急於一時

這個 PR 應該要做,但可以慢慢研究、好好寫。不要為了趕進度寫出爛 code。

3. 保持架構的正確性

LiteLLM 架構是對的,這只是實作細節的問題。

實際行動

短期修復(Day 22):

# prompts/chatbot.prompt
---
model: litellm/gemini-2.5-flash-lite
config:
  temperature: 0.2
  topK: 30
  reasoning_effort: "medium"  # ✅ 改用 OpenAI 格式
---

修改 @genkit-ai/compat-oai(進行中):

提交 PR 給 GenKit

「雖然 PR 還沒 merge,但至少踏出第一步了」

小結:重構的代價與收穫

今天這個 bug 給我很多反思:

關於架構重構

重構不是免費的午餐,總會有些東西在轉換過程中掉在地上。

從 Google AI Plugin → LiteLLM + OpenAI Compatible Plugin:

  • ✅ 獲得:安全性、成本控制、統一抽象層
  • ❌ 失去:原生功能的完整支援

這就是架構決策的本質:取捨。

沒有完美的架構,只有在特定情境下最適合的架構。

關於「相容」的真相

當你看到「OpenAI-compatible」時,要知道:

  • ✅ 它確實能用 OpenAI 的標準 API
  • ⚠️ 但各家的擴充功能可能不相容
  • ⚠️ 「80% 相容」和「100% 相容」差很多

Devil is in the details.

就像「方言」一樣,雖然都是中文,但各地區還是有差異彼此還是有差異的。OpenAI Compatible API 也是如此。

關於 Open Source 貢獻

這次的經歷讓我決定給 GenKit 提 PR。

當你遇到 Open Source 工具的限制時,你有兩個選擇:

  1. 抱怨它不夠完美
  2. 動手讓它變得更好

我選擇後者。這也是為什麼我們一開始選擇 Open Source 工具的原因 — 當它不符合需求時,我們有能力去改進它。

關於妥協

有時候,完美主義是工程師的敵人。

「沒有即時串流就先沒有吧,至少 trace 還能看到完整的思考過程」

要在「理想」和「現實」之間找到平衡點。


AI 的發展變化很快,目前這個想法以及專案也還在實驗中。但也許透過這個過程大家可以有一些經驗和想法互相交流,歡迎大家追蹤這個系列。

也歡迎追蹤我的 Threads @debuguy.dev


上一篇
Day 21: 當警報響起時 - 從 Grafana Alert 到自動化問題追蹤
系列文
AI 產品與架構設計之旅:從 0 到 1,再到 Day 222
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言