DEV Community

JH5
JH5

Posted on

實測 NVIDIA Switchyard:RTX 3050 邊緣與雲端混合路由

每次看到月底 API 帳單上那些忽多忽少的美元的扣款,心裡總是噴血,點開日誌一看裡面有八成竟然都只是在叫 Claude 讀個檔案、跑個 Linter、處理 Git...
如果在每個打到雲端 API 前的任務再加個 Routing,地端可以接球再決定要不要轉傳,是不是就可以幫忙處理很多種任務前置分流與動態執行任務的日常運用?


釐清架構盲點:為什麼單輪任務與 Agent 升級不能混為一談?

很多團隊在評估動態模型路由(Model Routing)時,最常犯的直覺錯誤就是把所有請求當成同一種東西在分流。

而 NVIDIA NeMo Switchyard 是基於 Rust 開發的開源動態模型路由層,特點是能將 Agent 在多輪對話中的每一步請求依據即時狀態分流到不同成本與能力的 LLM。

但仔細看它的核心設計,其實切成了兩套運作邏輯截然不同的路由範式:

                              ┌── 範例 A:任務型前置分流 (Task Routing)
                              │   單輪進門 ──► 裁判模型預判難度 ──► 派發 (8B vs 旗艦)
  ─────    應用範例  ──────────┤
                              └── 範例 B:執行期動態升級 (Stage Routing)
                                  多輪 Agent ──► 預設本地 8B 跑雜工 ──► 連續撞牆 ──► 雲端旗艦模型接棒
Enter fullscreen mode Exit fullscreen mode

如果你拿單輪的 URL 參數解析、SQL 分頁或常規文本摘要去跑多輪專用的 stage_router,Switchyard 根本沒有進行任何語意推理,它只是因為你設了 picker = "efficient_first" 而無腦套用預設值,那只能稱作靜態 fallback。

真正的分流架構,必須把這兩件事拆開:

  • 任務前置分流(Task Routing):由輕量 Judge 或意圖分類器在入口把關,依據單輪任務複雜度決定走便宜通道還是旗艦通道。
  • 執行期動態升級(Stage Routing):專門為 Tool-Using Coding Agent 或 SRE 診斷腳本打造。前幾輪看檔案、跑 linter、查 status 全丟給本地端 RTX 3050(成本為 0 元);只有在工具回傳連續報錯、卡死時,才強行 hot-swap 到雲端旗艦模型(例如 Claude-Sonnet-5.5 或 GPT-6)來收尾。

不過這裡也有一個可能會發生的架構卡點:Switchyard 本質上是一個「語意決策大腦」,預設是將本地任務指派給單一端點,當前端 Multi-Agent 同時併發多個任務時,單張 3050 顯卡依然可能被排隊隊列塞爆。如果要為本地端引入跨卡或跨設備的硬體推論網格,可以參考第一篇:《實測 NVIDIA PAIR:RTX 3050 串接 Colab A100 混合叢集》;而將兩者融合成「大腦+肌肉」的終極雙層架構,則記錄在第三篇:《實測 NVIDIA Switchyard 疊加 PAIR:雙層混合推論架構》。


格式避坑:Switchyard 雙路由 TOML

網路上包括 KDnuggets 在內的多數教學文章都停留在 0.2.0 版本,好多範例都是過時的 routes.random.yaml,在 0.3.0 版中,底層原生 Rust 伺服器已全面改為 TOML 結構。

在 Python 環境安裝套件非常直接,直接安裝預編譯 wheel 檔即可:

pip install nemo-switchyard
Enter fullscreen mode Exit fullscreen mode

在這次的邊緣與雲端混合架構中,我們在同一份 routes.toml 裡同時定義兩個不同用途的路由:task_route 負責單輪前置判定,agent_stage_route 負責多輪 Agent 工具監控。

schema_version = 1

[llm_clients.ollama_local]
format = "openai_chat"
base_url = "http://127.0.0.1:11434/v1"

[llm_clients.cloud_upstream]
format = "openai_chat"
base_url = "http://127.0.0.1:19800/v1"

[targets.local_edge]
id = "llama3.1:latest"
llm_client = "ollama_local"

[targets.cloud_flagship]
id = "claude-sonnet-5-5"
llm_client = "cloud_upstream"

[targets.local_judge]
id = "llama3.1-judge"
llm_client = "ollama_local"

# 範式一:單輪任務前置意圖分流 (Task-level Pre-Routing)
[routes.task_route]
id = "task_route"
type = "llm_classifier"
classifier_target = "local_judge"
strong_target = "cloud_flagship"
weak_target = "local_edge"
base_threshold = 0.5
threshold_step = 0.0
classify_trigger = "new_session"

# 範式二:多輪自主 Agent 執行期動態升級 (Stage-level Execution Routing)
[routes.agent_stage_route]
id = "agent_stage_route"
type = "stage_router"
picker = "efficient_first"
capable_target = "cloud_flagship"
efficient_target = "local_edge"
confidence_threshold = 0.5
Enter fullscreen mode Exit fullscreen mode

這裡有一個很大的設定陷阱:Switchyard 的 Rust 引擎在同一條 route 內是拿 model id 當 key,如果你把 classifier_target 跟 weak_target 都指向同一個字串 llama3.1:latest,伺服器在初始化時會直接報錯,目前暫時的解決辦法是在 Ollama 裡用 ollama cp llama3.1:latest llama3.1-judge 複製一個別名來暫時通過 Init 的驗證。

在 Python 裡拉起原生伺服器只要兩行:

from switchyard_rust.server import Server

# 啟動 Rust 監聽伺服器
sy_server = Server("routes.toml", port=19888)
print(f"Switchyard 路由伺服器已上線:{sy_server.base_url}")
Enter fullscreen mode Exit fullscreen mode

拆解任務前置分流:llm_classifier 在本地端到底靈不靈?

第一個測試,我們先測試了全端開發常見的錯誤的分流能力

當請求打到 task_route 時,Switchyard 會先掛起請求,把任務送進 classifier_target(我們部署在 RTX 3050 上的 llama3.1-judge),接著,Switchyard 會透過 TaskClassifierVerdict,要求裁判模型輸出 crux、primary_rule、capability_boundary 以及解題機率 p_solve。

在全端常見錯誤(例如前端與 API Gateway 最頻繁遇到的 URL Query 參數解析與缺失防禦)中,裁判模型判斷小模型具備充足能力,信心值直接給出 1.0,請求順利被分發給本地 3050 上的 8B 模型:

INFO switchyard.request: libsy.run: switchyard_libsy::algorithms::util::llm_judge: consulting llm judge target="llama3.1-judge"
INFO switchyard.request: libsy.run: Model selected algorithm="llm_task_classifier" target=llama3.1:latest confidence=1.0
INFO switchyard.request: switchyard_server::request: LLM request handled wire_format=openai_chat status=200 requested_model="task_route" selected_model="llama3.1:latest"
Enter fullscreen mode Exit fullscreen mode

而因為我的前端選的是 Next.js,過程中好幾次遇到 Server Actions 引發的 JWT Session Cookie 覆蓋跟 WebSocket 斷線的錯誤,裁判模型判定超出能力範圍,Switchyard 果斷將請求直送雲端 Claude-Sonnet-5.5。

另一個發現是,Switchyard 內建的 capability classifier 規範了 SUP-1 到 SUP-5 的規則列舉,如果本地 8B 裁判對提示詞的理解稍微有一點格式偏差,Switchyard 會直接將判定標記為無效,並觸發 fall_through 安全轉換到比較強模型,這種保守的設計保證了生產環境不會因為判斷出錯而GG,但也代表在日常的業務情境下,如果要精準發揮前置分流的威力,還是必須要微調小模型或改寫專屬 prompt 。


實測執行期動態升級: 捕捉 repeated_failure

前面有提到另一個常見場景就是 stage_router的應用

在 Coding Agent 與 DevOps 維運場景中,我們實測了四個常見的日常:

第一組是規範檢查:Coding Agent 讀取 src/auth_service.py 裡的 JWT 驗證邏輯,接著調用 run_linter 工具,工具回傳 SUCCESS: 0 warnings, 0 errors,確認了在 Switchyard 的歷史紀錄中看見全數正常,維持在本地 3050 處理,端到端耗時僅 438 ms。

第二組是環境維護:DevOps Agent 執行 git status 確認工作目錄乾淨,再執行 hadolint 驗證多階段 Dockerfile ,沒有任何異常訊號,全程在本地 8B 解決。

第三組是deadlock除錯:Agent 嘗試修復並呼叫 pytest 工具,結果第一次GG:

Traceback (most recent call last):
  PostgreSQLError: 40001 could not serialize access due to concurrent update (deadlock detected)
FAILED tests/test_concurrency.py::test_deduct_balance
Enter fullscreen mode Exit fullscreen mode

Agent 嘗試修改程式碼後再次執行測試,又噴出了一模一樣的 40001 錯誤,不過就在連續失敗兩次後,Switchyard 發揮了它的價值~

INFO switchyard.request: switchyard_libsy::algorithms::util::stage: stage router override decision_source="override" override_reason="repeated_failure" target=claude-sonnet-5-5
INFO switchyard.request: libsy.run: Model selected algorithm="stage_router" target=claude-sonnet-5-5 confidence=1.0
INFO switchyard.request: switchyard_server::request: LLM request handled wire_format=openai_chat status=200 requested_model="agent_stage_route" selected_model="claude-sonnet-5-5" handling_duration_ms=300.96
Enter fullscreen mode Exit fullscreen mode

從判斷出 repeated_failure 到切換,只花了 11 微秒(0.011 ms),信心度也被拉到 1.0,直接將下一輪提示詞切換至雲端 Claude-Sonnet-5.5,讓雲端模型分析與接手處理。

第四組是 Kubernetes 故障排查:DaemonSet Pod 連續兩次噴出 ExitCode 137 (OOMKilled),Switchyard 也順利的觸發了 critical_error ,同樣的順利傳球給雲端模型接手處理。


實際運用的架構取捨

雖然 Switchyard 的反應很快,但在生產環境部署與雲端混合架構時,實測完後還是有一些需要考慮的架構取捨問題。

第一個是雲端 Prompt Caching 的失效問題,在一般的雲端架構裡,如果多輪對話全走同一家雲端 API,重複的前文享有高達 50% 到 90% 的快取折扣,但是在混合架構下,前三輪在本地 3050 跑 8B 模型,第四輪才因為 repeated_failure 突然切換到雲端 Claude-Sonnet-5.5 時,上下文已經累積了一定數量的 tokens,這一次跳轉產生的首次 pre-fill 費用,可能還是會吃掉前幾輪省下來的差價。

第二個是小模型的 Tool Use 穩定度,要讓本地 8B 模型跑前面幾輪的前提,是該模型必須具備穩定的 Function Calling 能力,如果 8B 模型在第二輪就產生了無效的 JSON 參數,導致工具根本無法執行,那它連觸發 repeated_failure 的機會都沒有,反而會直接把 Agent 卡死在語法錯誤上。

Switchyard 實測了幾天,的確是可以幫忙解決在 routine 工作的不少小雜事, 下一步要解決的,可能還有兩件事,第一個是前面有提到的,要怎麼從拿到適當的訓練資料來做微調,讓他分流可以更為精準,第二個則是目前另一個大花費,讓 Agent 在控制瀏覽器時如果多次遇到 repeated_failure ,可以從local的 Gemma4 自動換到雲端的模型,並順利接手前面試過的這些 screen context ,如果有測試到不錯的結果再來跟大家分享。


延伸閱讀:NVIDIA 地雲混合推論實測三部曲

Top comments (0)