串接多家模型服務:抽象層該做到什麼程度
過度抽象會綁死自己,完全不抽象又難以換供應商。這是實用的中間路線。
想保留換供應商的彈性,但也不想寫一個支援所有功能的巨大抽象層。中間路線是:只抽象你真的會用到的部分。
建議抽象的部分
- 基本補全與對話呼叫。
- 串流。
- 工具呼叫的請求與回應格式。
- 用量統計(輸入輸出 token)。
- 錯誤分類:限流、逾時、內容過濾、其他。
不建議抽象的部分
- 各家特有的參數——用
extra字典直接透傳。 - 結構化輸出的實作細節,差異太大。
- 快取機制,各家計費與行為不同。
@dataclass
class Reply:
text: str
tool_calls: list
usage: dict # {'in': int, 'out': int}
raw: dict # 保留原始回應,需要時可取用特有欄位
class Provider(Protocol):
def chat(self, messages, tools=None, stream=False, **extra) -> Reply: ...錯誤要統一分類
class RateLimited(Exception): pass
class Overloaded(Exception): pass
class ContentFiltered(Exception): pass
# 各家的錯誤碼在 provider 內部轉換成上面這幾種
# 上層邏輯只需要處理統一的類別錯誤分類是抽象層最有價值的部分。有了它,重試與降級的邏輯才能寫一次就適用所有供應商。
保留 raw 欄位
抽象層一定會漏掉某些欄位。保留原始回應讓上層在需要時能取用,比為了每個新欄位改抽象層實際得多。
別在第一天就寫抽象層。先直接串一家,第二家要接的時候再重構——那時你才知道真正需要抽象什麼。