故障切換實(shí)戰(zhàn))
給 OpenAI SDK 配好base_url和api_key就能發(fā)請求這是最快的方式。可一旦上游返回 502或者限流策略收緊單個(gè)端點(diǎn)就會(huì)拖住整條鏈路。openai sdk 多端點(diǎn)故障切換要解決的就是這類問題主端點(diǎn)不可用時(shí)請求自動(dòng)落到備用端點(diǎn)而不是在同一個(gè)地址上反復(fù)重試。多端點(diǎn)切換不是負(fù)載均衡。它只處理失敗轉(zhuǎn)移不負(fù)責(zé)流量分配。落點(diǎn)由一個(gè)端點(diǎn)池維護(hù)每個(gè)端點(diǎn)都有獨(dú)立的 base_url、api_key 和 model。端點(diǎn)池的實(shí)現(xiàn)其實(shí)不復(fù)雜先看下核心結(jié)構(gòu)。端點(diǎn)池的最小實(shí)現(xiàn)importosimporttimeimportrandomfromdataclassesimportdataclassfromopenaiimportOpenAIdataclassclassEndpoint:name:strbase_url:strapi_key:strmodel:strdef_default_endpoints():keyos.environ[SILVAMUX_API_KEY]basehttps://www.silvamux.com/api/v1modelos.environ.get(MODEL_NAME,minimax-m2.5)return[Endpoint(primary,base,key,model),Endpoint(backup,base,key,model),]classMultiEndpointChat:def__init__(self,endpointsNone):self.endpointsendpointsor_default_endpoints()self.cursor0def_next_endpoint(self):epself.endpoints[self.cursor%len(self.endpoints)]self.cursor1returnepdef_should_failover(self,exc):statusgetattr(exc,status_code,None)ifstatusisNone:returnTrueifstatusin(400,401,402,403):returnFalseifstatus429:time.sleep(random.uniform(1,3))returnTrueifstatusin(500,502):time.sleep(1)returnTruereturnTruedefchat(self,messages,**kwargs):last_excNonefor_inself.endpoints:epself._next_endpoint()clientOpenAI(api_keyep.api_key,base_urlep.base_url)try:returnclient.chat.completions.create(modelkwargs.get(model,ep.model),messagesmessages,**{k:vfork,vinkwargs.items()ifk!model})exceptExceptionasexc:last_excexcifnotself._should_failover(exc):raiseraiseRuntimeError(全部端點(diǎn)都不可用)fromlast_exc代碼里base固定為 SilvaMux 的 OpenAI 兼容端點(diǎn)SILVAMUX_API_KEY從環(huán)境讀取。默認(rèn)兩個(gè)端點(diǎn)指向同一個(gè)地址是為了把故障切換邏輯獨(dú)立出來讓示例可以直接跑。要接第二個(gè)服務(wù)把backup的base_url換掉就行。錯(cuò)誤怎么分類切換前先把失敗類型分清楚。4xx 是調(diào)用方的問題模型名拼錯(cuò)、key 失效、項(xiàng)目沒綁對(duì)切到下一個(gè)端點(diǎn)只會(huì)得到相同錯(cuò)誤。5xx、網(wǎng)絡(luò)抖動(dòng)和 429 限流才是切換的主要對(duì)象。這個(gè)邊界要在代碼里寫死否則 401 會(huì)被當(dāng)成瞬時(shí)故障反復(fù)切換最后日志里全是同一種配置錯(cuò)誤。400/401/402/403不重試、不切換直接拋回給調(diào)用方。429先退避再切換到下一個(gè)端點(diǎn)。指數(shù)退避比固定間隔更穩(wěn)妥。500/502間隔 1 到 5 秒重試并切換到備用端點(diǎn)。網(wǎng)絡(luò)層異常沒有 HTTP 狀態(tài)碼直接切換。有些網(wǎng)關(guān)會(huì)在響應(yīng)頭里返回X-Request-Id排查問題時(shí)把這個(gè) header 帶回來。錯(cuò)誤體如果是直接調(diào) REST 拿到的通常會(huì)包含error.typegateway_error和error.code。把error.code打點(diǎn)聚合能很快看出是不是同一類故障。這也是 openai sdk 多端點(diǎn)故障切換和普通重試最大的區(qū)別。流式請求怎么處理流式請求里談 openai sdk 多端點(diǎn)故障切換要先接受一個(gè)現(xiàn)實(shí)切換只能發(fā)生在建流之前。OpenAI SDK 在streamTrue時(shí)返回 SSE如果流已經(jīng)輸出了一半再斷開只能由業(yè)務(wù)決定重試還是丟棄。所以流式方法里我會(huì)先嘗試建立流再返回生成器給調(diào)用方。defchat_stream(self,messages,**kwargs):last_excNonefor_inself.endpoints:epself._next_endpoint()clientOpenAI(api_keyep.api_key,base_urlep.base_url)try:streamclient.chat.completions.create(modelkwargs.get(model,ep.model),messagesmessages,streamTrue,**{k:vfork,vinkwargs.items()ifknotin(model,stream)})returnself._iter_stream(stream)exceptExceptionasexc:last_excexcifnotself._should_failover(exc):raiseraiseRuntimeError(全部端點(diǎn)都不可用)fromlast_excdef_iter_stream(self,stream):forchunkinstream:# 別只盯著 data: [DONE]# 末尾可能出現(xiàn) choices 為空但攜帶 usage 的 chunkifnotgetattr(chunk,choices,None):continueyieldchunk_iter_stream不依賴[DONE]文本而是等迭代結(jié)束。有些網(wǎng)關(guān)會(huì)先結(jié)束事件流再補(bǔ)一個(gè)choices為空的用量 chunk過早返回會(huì)漏掉 token 統(tǒng)計(jì)。這個(gè)點(diǎn)雖然小但在計(jì)費(fèi)相關(guān)邏輯里容易造成誤差。常見的坑把 401 當(dāng)作瞬時(shí)故障切換會(huì)掩蓋配置錯(cuò)誤。切端點(diǎn)的前提是請求本身沒問題。只依賴data: [DONE]判斷完成可能提前退出流。在生成器中間切換端點(diǎn)輸出會(huì)重復(fù)或截?cái)?。切換盡量放在建流前。openai sdk 多端點(diǎn)故障切換的代碼里最容易忽略的其實(shí)是錯(cuò)誤分類。端點(diǎn)池本身沒有多復(fù)雜難的是把 4xx、限流和 5xx 分開對(duì)待。分對(duì)了切換才有意義。常見問題問OpenAI SDK 自帶重試還需要端點(diǎn)池嗎要。SDK 的重試只針對(duì)同一個(gè) base_url。如果那個(gè)地址已經(jīng)不可用重試多少次都一樣。openai sdk 多端點(diǎn)故障切換不是替代 SDK 重試而是補(bǔ)上跨地址的短板。問429 該等待還是該換端點(diǎn)先退避再換。限流有時(shí)是局部策略換個(gè)端點(diǎn)可能直接規(guī)避。但退避不能省否則切換過去也可能立刻被壓垮。問流式?jīng)]有收到 data: [DONE] 是不是失敗不一定??蛻舳藨?yīng)該以流結(jié)束為準(zhǔn)。末尾可能出現(xiàn) choices 為空但帶 usage 的 chunk過早返回會(huì)漏掉用量信息。環(huán)境說明以上示例在 千木 的 OpenAI 兼容端點(diǎn)上驗(yàn)證模型調(diào)用名為 minimax-m2.5。模型調(diào)用名以文檔為準(zhǔn)見 千木大模型聚合平臺(tái)接入文檔