用架構(gòu):大模型API統(tǒng)一接入與治理實踐)
這兩年做了幾個AI應(yīng)用后我最深的體會是真正拉開項目復(fù)雜度的不是某個模型效果好不好而是能不能把一個多模型混合調(diào)用架構(gòu)搭好把多個大模型API統(tǒng)一管理起來。模型越來越多各家能力差異又大如果業(yè)務(wù)代碼直接散裝接入前期確實“爽”等要切換、降級、算成本的時候整個團隊都會被拖進(jìn)泥潭。這篇文章就把我在某個內(nèi)部AI應(yīng)用項目里沉淀下來的統(tǒng)一接入方案寫出來包括API抽象層、路由策略、容錯機制、成本觀測和幾個踩過的坑適合正在做Agent平臺、智能客服、內(nèi)容生成服務(wù)的開發(fā)者參考。1. 當(dāng)項目里同時接入三五個大模型API之后混亂是怎么滋生出來的1.1 每個模型一套接入方式代碼里到處是分支我最早接手的那套系統(tǒng)邏輯非常簡單業(yè)務(wù)方指定一個模型我們調(diào)一個接口把結(jié)果返回給前端。后來需求多了有的場景要便宜快速有的場景要強推理有的場景要視覺理解于是陸陸續(xù)續(xù)接了好幾個不同廠商的API。最開始大家沒當(dāng)回事每個新模型都按SDK文檔在業(yè)務(wù)代碼里單獨寫一個方法沒多久代碼里就出現(xiàn)了大量if provider xxx的分支。更難受的是各家API差異比想象中大得多。請求參數(shù)有的叫temperature有的叫temp最大值還不一樣流式返回有的用SSE有的用自研事件格式錯誤碼更是五花八門有的返回HTTP 429表示限流有的返回200但里層error_code是限流。每次聯(lián)調(diào)一個新模型光參數(shù)轉(zhuǎn)換和錯誤解析就要折騰兩三天等接入的模型數(shù)量超過三個這套代碼基本沒人敢動了。1.2 統(tǒng)一管理不只是“開發(fā)方便”更是線上穩(wěn)定性問題當(dāng)時很多人覺得多接幾個模型不就是為了買到更便宜的服務(wù)嗎開發(fā)麻煩一點忍忍就過去了。真正讓我下決心做統(tǒng)一架構(gòu)的是一次線上事故某個主力模型服務(wù)商發(fā)公告說當(dāng)天要升級我們想切到備用的另一個模型結(jié)果發(fā)現(xiàn)備用模型雖然早早就接好了但SDK版本不一致、請求參數(shù)沒適配生產(chǎn)環(huán)境一壓測就報錯。最后只能頂著風(fēng)險繼續(xù)用老服務(wù)整晚沒睡踏實。從那以后我想明白了分管多個大模型API的核心價值不在于“集成了多少家”而在于三個能力透明替換切換模型不應(yīng)該是改代碼、重新上線而是配置層面的動作。策略路由不同請求能自動選擇合適模型比如簡單的摘要走低價模型復(fù)雜的推理走強模型。穩(wěn)定兜底某個模型故障時流量能自動轉(zhuǎn)移到健康模型而不是讓用戶看到超時。1.3 我最終確定的架構(gòu)目標(biāo)統(tǒng)一管理多個大模型API本質(zhì)上是給業(yè)務(wù)層和模型服務(wù)商之間加一層“中間人”。這層中間人要做的事情不復(fù)雜但邊界要清晰對業(yè)務(wù)層暴露一套穩(wěn)定、中立的統(tǒng)一接口業(yè)務(wù)方不需要關(guān)心后端到底是哪家模型。對模型服務(wù)商通過適配器做隔離新增一個模型只需要實現(xiàn)一個適配器不改業(yè)務(wù)代碼。對運維側(cè)提供路由、熔斷、降級、計費的統(tǒng)一配置入口。架構(gòu)上就分成了四塊統(tǒng)一抽象層、適配器層、路由調(diào)度層、可觀測與成本層。下面我逐個講清楚設(shè)計和實現(xiàn)思路。2. 先把API抽象拉齊一個Request/Response模型吃下所有差異2.1 為什么必須先統(tǒng)一協(xié)議而不是直接做網(wǎng)關(guān)很多人一聽“統(tǒng)一管理多個大模型API”第一反應(yīng)是搭一個API網(wǎng)關(guān)把各家請求轉(zhuǎn)發(fā)一下。我覺得這是本末倒置。如果網(wǎng)關(guān)的轉(zhuǎn)發(fā)邏輯還是基于各家私有協(xié)議的每接一個模型就要在網(wǎng)關(guān)里寫一層透傳參數(shù)那網(wǎng)關(guān)反而成了又一個維護地獄。正確的做法是先定義一套中立的請求/響應(yīng)模型把所有模型的差異全部收斂在適配器里。業(yè)務(wù)方只認(rèn)這一套模型網(wǎng)關(guān)和路由也只認(rèn)這一套模型。只要協(xié)議層統(tǒng)一了后面做路由、做緩存、做成本統(tǒng)計才有共同的抓手。我定義統(tǒng)一請求時堅持幾個原則字段用中性語義不偏向任何一家。比如對話歷史統(tǒng)一叫messages模型名統(tǒng)一叫model隨機性統(tǒng)一叫temperature。能力按最小可用集設(shè)計先覆蓋對話、流式、工具調(diào)用、JSON模式這四個高頻能力其他特殊能力走擴展字段。參數(shù)范圍做歸一比如有的廠商溫度支持0到2有的只支持0到1統(tǒng)一層直接約束為0到1適配器內(nèi)部做縮放。這樣看起來“犧牲”了一些廠家特性但換來的是業(yè)務(wù)邏輯的高度一致。我在實踐里最大的體會是協(xié)議統(tǒng)一得越徹底后面的路由和成本代碼就越省力。2.2 供應(yīng)商適配器的接口拆分統(tǒng)一協(xié)議定義好之后每個模型服務(wù)商對應(yīng)一個適配器。適配器的核心接口非常收斂class ModelAdapter(Protocol): def get_model_info(self) - UnifiedModelInfo: ... def chat(self, request: UnifiedRequest) - UnifiedResponse: ... def stream_chat(self, request: UnifiedRequest) - Iterator[UnifiedEvent]: ...get_model_info返回模型元數(shù)據(jù)包括模型ID、支持哪些能力、上下文長度、計費單價等。chat走非流式stream_chat走流式。業(yè)務(wù)方永遠(yuǎn)只面向這兩個方法編程具體HTTP調(diào)用、鑒權(quán)、SDK封裝全部在適配器內(nèi)部。每個模型的元信息長這樣class UnifiedModelInfo(BaseModel): model_id: str supports_tools: bool False supports_json_mode: bool False supports_vision: bool False max_context_tokens: int 0 prompt_price_per_1k: float 0.0 completion_price_per_1k: float 0.0這里有個很容易忽略的細(xì)節(jié)模型元信息不是寫死的常量而是每個適配器必須返回的真實值。因為后面路由要根據(jù)supports_tools、max_context_tokens、prompt_price_per_1k做篩選和排序。如果元信息不準(zhǔn)確路由算出來的結(jié)果全是錯的。2.3 模型能力差異的歸一化處理統(tǒng)一協(xié)議不難真正花時間的是處理“大家都說自己支持工具調(diào)用但格式完全不一樣”這類問題。比如工具調(diào)用某個服務(wù)商返回的是結(jié)構(gòu)化的tool_calls數(shù)組另一個服務(wù)商要求把工具定義寫成一段特殊格式的字符串還有一個服務(wù)商把工具調(diào)用結(jié)果嵌在Markdown代碼塊里。統(tǒng)一層的做法是定義自己的UnifiedToolCall結(jié)構(gòu)class UnifiedToolCall(BaseModel): call_id: str function_name: str arguments: dict適配器負(fù)責(zé)把各家返回格式轉(zhuǎn)換成這個結(jié)構(gòu)反向時再把業(yè)務(wù)方的工具結(jié)果轉(zhuǎn)換成廠商要求的格式。這樣業(yè)務(wù)層永遠(yuǎn)不會看到廠商相關(guān)的字段。JSON mode也是重災(zāi)區(qū)。有的用response_format{type: json_object}有的叫structured_output有的干脆不支持只能靠提示詞約束。歸一化之后統(tǒng)一層只認(rèn)一個response_formatjson的開關(guān)適配器內(nèi)部實現(xiàn)“真支持就用參數(shù)不支持就用提示詞拼一段強約束文本”。我當(dāng)時的一個判斷是能靠適配器抹平的差異就不要讓業(yè)務(wù)方感知只有“支持還是不支持”這種硬能力差異才需要放到元信息里交給路由層決策。比如視覺請求只能路由到支持視覺的模型工具調(diào)用請求只能路由到支持工具的模型。3. 路由與調(diào)度按成本、延遲、質(zhì)量、配額做動態(tài)決策3.1 四種路由策略的適用場景模型多了以后真正讓業(yè)務(wù)跑得又穩(wěn)又省的是路由策略。我沉淀下來最常用的四種如下表策略核心邏輯典型場景需要注意的問題固定主備優(yōu)先打主模型失敗才切備模型對穩(wěn)定性要求極高的核心鏈路主模型故障切換時可能有秒級抖動權(quán)重比例按配置比例分配流量同時用多個模型分?jǐn)偝杀緹o法感知實時質(zhì)量和可用性延遲優(yōu)先從健康節(jié)點里選延遲最低的模型客服、實時助手等對時延敏感場景低成本慢模型容易被餓死成本優(yōu)先估算成本后選最便宜的合格模型批處理、摘要、分類等海量低頻請求需要先定義“合格”的底線這里最關(guān)鍵的是明確一點任何單一策略都不可能通吃。更合理的做法是“先過濾后打分”。過濾階段用硬性條件把不滿足的模型剔除比如上下文不夠、不支持視覺、配額已耗盡打分階段再用業(yè)務(wù)關(guān)心的維度排序比如成本權(quán)重0.4、延遲權(quán)重0.3、質(zhì)量權(quán)重0.3。3.2 路由權(quán)重與自動降級的聯(lián)動只做靜態(tài)權(quán)重是不夠的因為真實線上環(huán)境隨時在變化。某個模型突然限流了如果還按原比例把流量打進(jìn)去那批請求就會大量失敗。我后來做了一個聯(lián)動邏輯路由系統(tǒng)實時統(tǒng)計每個模型的滑動窗口錯誤率當(dāng)錯誤率超過閾值時自動把該模型標(biāo)記為“不健康”并把原本分配給它的流量按剩余模型的比例重新分配。這就是路由和容錯的結(jié)合點。一個看起來簡單的改動但效果非常明顯。之前某個模型故障時業(yè)務(wù)方經(jīng)常反饋“部分請求超時”做了動態(tài)降級之后故障期間的成功率能穩(wěn)定維持在99%以上只是響應(yīng)速度略有波動。聯(lián)動邏輯還要考慮“恢復(fù)”不能一健康就立刻把流量全部切回來那會造成抖動。我采取的做法是“緩慢回升”恢復(fù)后的模型先接收一小部分流量觀察錯誤率穩(wěn)定后才逐步恢復(fù)原權(quán)重。3.3 落到代碼的簡易路由引擎我把路由引擎的骨架簡化后基本長這樣class ModelRouter: def __init__(self, registry: ModelRegistry, metrics: MetricsCollector): self.registry registry self.metrics metrics def route(self, req: UnifiedRequest) - ModelAdapter: candidates self.registry.filter_by_capabilities(req) candidates self.filter_healthy(candidates) scored [self.score_model(m, req) for m in candidates] scored.sort(keylambda x: x.score, reverseTrue) return scored[0].adapter打分函數(shù)里會把元信息和實時指標(biāo)結(jié)合起來比如成本分?jǐn)?shù)來自元信息里的價格字段延遲分?jǐn)?shù)來自最近5分鐘的P50延遲健康分來自滑動窗口錯誤率。分?jǐn)?shù)權(quán)重通過配置文件調(diào)整改配置就能調(diào)整路由傾向不用動代碼。這樣我后面做實驗就非常方便今天想讓請求多走便宜模型改一下權(quán)重重啟即可。4. 容錯與降級重試、熔斷、超時一個都不能少4.1 不同失敗類型的處理優(yōu)先級很多團隊的容錯就是“失敗就重試三次”這在大模型API場景下會出大問題。大模型請求不像普通RPC那樣無狀態(tài)重試可能產(chǎn)生重復(fù)計費而且很多生成任務(wù)不是冪等的。我按錯誤類型把處理策略分成了幾檔4xx參數(shù)錯誤不重試。這是代碼或配置問題重試只會浪費額度應(yīng)該直接報錯并告警。429限流可以先小退避重試一次但不宜反復(fù)重試。如果連續(xù)多次遇到429說明路由層應(yīng)該把流量切走了。5xx服務(wù)端錯誤可以重試但前提是確認(rèn)請求沒有副作用。如果業(yè)務(wù)側(cè)已經(jīng)接收了用戶輸入重試前最好跟業(yè)務(wù)約定是否允許重復(fù)生成。連接超時和讀超時必須設(shè)置閾值。我見過有請求掛在某個模型上整整90秒沒返回用戶早就跑了。4.2 實現(xiàn)了熔斷器之后的真實效果統(tǒng)一接入層最適合做的事就是熔斷。我給每個模型節(jié)點都配了一個熔斷器狀態(tài)機遵循經(jīng)典的關(guān)閉、打開、半開三態(tài)。當(dāng)某個模型的連續(xù)失敗率超過閾值熔斷器打開后續(xù)請求直接拒絕進(jìn)入該模型快速失敗經(jīng)過冷卻時間后進(jìn)入半開狀態(tài)放少量試探請求成功率達(dá)到要求再關(guān)閉。我最初犯過一個錯只按服務(wù)商維度做熔斷沒有把模型節(jié)點拆開。結(jié)果有一次某個具體模型因為配額問題連續(xù)報錯同廠商的其他模型是好的但整個服務(wù)商的流量都被切走了白白損失了可用的算力。后來改成“按模型節(jié)點熔斷”每個模型的熔斷狀態(tài)獨立維護問題就解決了。熔斷器對用戶體感的提升也很明顯。以前某個模型掛了用戶會一直轉(zhuǎn)圈等超時現(xiàn)在熔斷后立刻返回錯誤前端可以馬上提示重新選擇服務(wù)或者走兜底方案用戶端的感知變成“稍后再試”而不是“服務(wù)卡死”。4.3 兜底方案設(shè)計兜底方案不是隨便寫一段“服務(wù)繁忙”就完事了。我在實踐中把兜底分成三級模型級降級優(yōu)先路由到同能力的備選模型比如強推理掛了切到中檔模型。結(jié)果級緩存如果是同質(zhì)化請求之前有高質(zhì)量結(jié)果可以直接把緩存結(jié)果返回。這個對摘要、分類這類任務(wù)尤其有效做法是按文本向量相似度或哈希做緩存命中率能做到20%到30%。提示級兜底以上都不可用時返回預(yù)設(shè)的降級文案并記錄一條告警。這樣設(shè)計之后線上很少有用戶真正看到“AI服務(wù)不可用”的裸奔狀態(tài)絕大部分異常都在前兩級被消化掉了。5. 上線后必須盯的指標(biāo)與成本分?jǐn)?.1 Token計價口徑天生不統(tǒng)一怎么拉齊說到成本第一道坎就是“各家對token的統(tǒng)計口徑不一樣”。有的按token計費有的按字符計費有的把緩存命中按折扣計費有的直接不區(qū)分有的token數(shù)在返回體里明明白白有的要自己去估算。如果按廠商返回的數(shù)字直接加總算成本最后對賬一定對不上。我的做法是在統(tǒng)一層自己維護一套成本口徑適配器返回統(tǒng)一響應(yīng)時除了業(yè)務(wù)字段必須附帶input_tokens和output_tokens。這個數(shù)字不一定要跟廠商完全一致但要保證系統(tǒng)內(nèi)部所有模型用的是同一套統(tǒng)計規(guī)則。比如可以用一個通用分詞器統(tǒng)一估算也可以直接采信廠商但必須明確記下“本數(shù)字來自廠商估算可能存在10%以內(nèi)偏差”。然后成本就很簡單了cost req_input_tokens / 1000 * model_info.prompt_price_per_1k resp_output_tokens / 1000 * model_info.completion_price_per_1k統(tǒng)一層會在每個請求日志里寫清楚用了哪個模型、消耗了多少token、估算成本是多少。這樣財務(wù)同事要對賬時能直接把日志拉出來不用再問研發(fā)“這兩個模型價格是怎么算的”。5.2 觀測面板上的關(guān)鍵指標(biāo)模型多了以后只看“整體成功率”完全不夠。我上線后長期盯的是四個維度維度關(guān)鍵指標(biāo)踩坑提示模型狀態(tài)調(diào)用量、成功率、錯誤碼分布錯誤碼分布要區(qū)分服務(wù)端錯誤和限流延遲P50、P95、流式首Token延遲流式請求必須單獨統(tǒng)計首Token耗時路由各策略命中比例、降級次數(shù)降級次數(shù)突增說明上游模型有波動成本每請求成本、日成本、場景成本按場景分?jǐn)倓e只看總體大盤這里最容易被忽視的是“流式首Token延遲”。非流式接口的延遲很好理解但流式場景下用戶感知到的等待時間其實是首Token之前的時間。如果不單獨統(tǒng)計你會發(fā)現(xiàn)延遲指標(biāo)很漂亮但用戶還是覺得慢因為你把整個流式拉滿的時間也算進(jìn)去了。5.3 成本預(yù)算控制在什么粒度比較合理預(yù)算控制我建議按“場景”切而不是按模型切。比如客服問答、內(nèi)容生成、文檔總結(jié)各給各的預(yù)算每個場景有獨立的日預(yù)算和月預(yù)算。這樣做有兩個好處一是某個場景的流量突然爆炸時能快速定位到是哪個功能在燒錢二是預(yù)算限制可以精細(xì)控制該場景的請求如果預(yù)估成本超標(biāo)直接降級到更便宜的模型。我在統(tǒng)一層實現(xiàn)了一個簡單的預(yù)算預(yù)檢每次路由前根據(jù)請求類型和模型價格估算成本如果該場景當(dāng)日預(yù)算剩余不足就強制走低成本模型甚至走緩存。這套機制上線后整體模型成本下降了大約30%因為沒有錢花在“不需要高質(zhì)量模型”的請求上。6. 從這套架構(gòu)里沉淀出的幾條實戰(zhàn)經(jīng)驗6.1 先跑通最小閉環(huán)再考慮完美抽象我見過不少團隊一上來就畫很大的架構(gòu)圖要做模型市場、要做自動調(diào)優(yōu)、要做全鏈路追蹤結(jié)果做了三個月還沒接完第一個模型。我的建議是反著來先選兩個差異最大的模型一個閉源強模型、一個開源低成本模型做一個最簡單的適配器加網(wǎng)關(guān)把“代碼不感知廠商”這個核心鏈路跑通。第一版哪怕只支持chat和stream_chat都行工具調(diào)用后面再補。因為協(xié)議統(tǒng)一這件事不做永遠(yuǎn)不知道自己的接口設(shè)計哪里有問題只有真實接了兩個模型才會發(fā)現(xiàn)溫度范圍、錯誤碼、流式格式這些差異比你想象得要多。最小閉環(huán)跑通后再逐步補充能力元信息、路由策略、預(yù)算控制。6.2 別忽略流式接口對統(tǒng)一層的沖擊如果一開始只支持非流式后面補流式支持會是一場災(zāi)難。因為流式不是“多返回幾個chunk”那么簡單它牽扯到SSE連接管理、心跳、客戶端斷連取消、事件解析、工具調(diào)用增量拼接、最終計費統(tǒng)計等一大堆邏輯。等業(yè)務(wù)量大了再改兼容成本極高。我最早設(shè)計Protocol時就同時要求實現(xiàn)chat和stream_chat哪怕第一版只有少數(shù)模型支持流式。后來新接入的廠商幾乎沒有額外改動因為流式差異全部在適配器里被吸收掉了。這個決定后來被證明是最省時間的一個選擇。6.3 模型版本切換要留審計記錄模型API不只是模型本身在變提示詞、參數(shù)、路由策略也經(jīng)常調(diào)。以前排查問題時常碰到這種對話業(yè)務(wù)方說“我讓模型做了個總結(jié)結(jié)果輸出格式不對”研發(fā)說“是不是提示詞改了”兩邊扯皮半天。后來我在統(tǒng)一層里給每個請求都加了版本號和切換記錄寫到日志里。這里說的版本號不是一個數(shù)字而是一套“鏈路快照”請求命中了哪條路由策略、最終選了哪個模型、用什么版本的提示詞模板、適配器做了什么參數(shù)轉(zhuǎn)換。只要有了這套審計數(shù)據(jù)線上任何一次異常輸出都能回溯到具體的推理鏈路而不是猜。實際排查問題時這個能力比多寫十個監(jiān)控指標(biāo)都管用。這套多模型混合調(diào)用架構(gòu)做下來我最大的體會就是“統(tǒng)一層不是中轉(zhuǎn)站而是業(yè)務(wù)和模型之間的契約”。契約定得清晰后面加模型、換模型、控成本都只是適配器層面的工作量契約定得含糊所有復(fù)雜性都會反向滲透到業(yè)務(wù)代碼里越往后越難收拾。如果你正在處理多個大模型API的接入問題可以從一個統(tǒng)一協(xié)議和兩個適配器開始先把鏈路跑通再一步步把路由、熔斷、預(yù)算這些能力填進(jìn)去。