iT邦幫忙

2026 iThome 鐵人賽

DAY 7
0

Day 6 的 recursion_limit 解決了一件事:graph 不該無限繞圈。但我很快發現,step 數根本不能回答「這次到底花了多少」。一次 step 可能只是選擇下一步,也可能包含昂貴的模型回覆、外部工具請求,或一段很長的輸出。

所以今天我不再把「最多六步」當成成本控制,而是把不同花費拆開算。我用 mock workflow 和固定 usage data 驗證停止條件,避免為了做 demo 真的把 API key 跑到超額。

一個上限不夠,我現在有八個

邊界 目前設定 它限制的是什麼
LangGraph recursion_limit 6 graph 可執行的 super-step,Day 6 的最外層煞車。
模型呼叫數 3 一次 run 可以要求模型幾次。
工具呼叫數 4 一次 run 可以執行幾次工具。
單次模型 HTTP timeout 20 一個模型請求不能一直卡住。
單次輸出 600 tokens 每次模型回覆的輸出長度。
累積 Token 3,000 已回傳的 input + output usage。
執行時間 45 到下一個模型或工具動作前,這個 run 已跑多久。
估算成本 部署設定 依 input/output token 價格估算的金額。

前三項的單位都不同。recursion_limit 是 graph 的 super-step;模型呼叫與工具呼叫是可觀察的外部動作。Day 6 已經說過,它們不能互相代替。

LangChain 有 ModelCallLimitMiddlewareToolCallLimitMiddleware 可限制單次 run 的次數;官方文件也把它們列為控制模型成本與工具執行的 middleware。Prebuilt middleware 文件

我把它們和自訂預算 middleware 一起掛進 Agent:

middleware=[
    ExecutionBudgetMiddleware(
        limits=self.budget_limits,
        pricing=self.token_price,
    ),
    ModelCallLimitMiddleware(run_limit=MAX_MODEL_CALLS, exit_behavior="end"),
    ToolCallLimitMiddleware(run_limit=MAX_TOOL_CALLS, exit_behavior="end"),
]

exit_behavior="end" 的意思是限額到達後正常結束這次 run,不是丟出一串框架例外給使用者。這不代表回覆一定完整,所以前端和 trace 都要把「停止」當成一種結果,而不是成功。

3 次模型呼叫,到底最多是多少 Token?

以目前設定看,最多三次模型呼叫、每次最多 600 output tokens。只算輸出,理論上是 1,800 tokens;但這不是一次 run 的總 token 上限。

每次模型呼叫都還會帶入 system prompt、使用者訊息、工具結果與前幾輪對話,這些都是 input tokens。模型 provider 有回傳 usage 時,LangChain 會把 token 數放在 AIMessage.usage_metadata;沒有回傳時,程式不能憑空知道用了多少。LangChain models 文件

我目前只累積確實拿到的 input_tokensoutput_tokens

@dataclass(frozen=True)
class BudgetLimits:
    max_elapsed_seconds: float = 45.0
    max_total_tokens: int = 3_000
    max_estimated_cost_usd: Decimal | None = None

這個 3,000 是停止下一個動作前的判斷門檻,不是 provider 層的硬性 token cap。假設第三次模型回覆才讓累積值超過 3,000,那次回覆已經發生;程式會阻止後面的模型或工具動作,不能把已送出去的請求倒帶。

這也是我不想把「設定了 Token budget」寫成「永遠不會超過 3,000 tokens」的原因。上限的精度取決於可以在哪個時點量到 usage,也取決於 provider 有沒有提供它。

我把成本留給部署設定

Token 本身不是價格。input、output、cache 和不同模型的計價方式都可能不同,直接把某個模型價格寫死在程式裡,很容易過期。

我把價格放在 .env

MODEL_INPUT_PER_MILLION_USD=
MODEL_OUTPUT_PER_MILLION_USD=
RUN_COST_BUDGET_USD=

三個都填好才啟用成本上限。價格沒填時,模型呼叫數、工具呼叫數、時間和總 Token 上限仍會生效;程式不會用猜的成本把 run 停掉。

本機成本 demo 使用的是刻意簡化的測試價格:input 每百萬 tokens $1、output 每百萬 tokens $2。第一筆 usage 是 800 input 加 300 output,估算 $0.0014;第二筆加上 1,200 input 與 600 output 後,累積是 2,900 tokens、$0.0038。因為 demo 的成本預算是 $0.0030,它在下一個動作前停止。

這些數字只用來驗證計算與停止順序,不是任何模型的報價。

預算用完後,工具到底有沒有被叫出去?

這是我今天補上的一層。原本的 before_model 只能在下一次模型呼叫前檢查;如果上一輪模型剛超標又回傳一個 tool call,單靠它不足以保證工具不會先被執行。

現在我在 wrap_tool_call 做同一份檢查。超標時不呼叫 handler,而是回傳一個帶原 tool call ID 的 ToolMessage(status="error")

def wrap_tool_call(self, request: ToolCallRequest, handler: Callable[[ToolCallRequest], Any]) -> Any:
    exceeded = self._exceeded_limit(request.state)
    if exceeded is None:
        return handler(request)
    return ToolMessage(
        content=_budget_stop_message(exceeded),
        tool_call_id=request.tool_call["id"],
        status="error",
    )

LangChain 的 middleware 可以包住每一次模型或工具呼叫,正適合放這種執行前的規則。Custom middleware 文件

工具還是可能因為網路、timeout 或資料格式失敗,那些是 Day 8 的 retry 與 circuit breaker 要處理的事。這一層只回答一件事:已知預算用完時,還要不要讓 Agent 做下一個副作用?答案是不要。

image

我實際驗證了什麼

我新增了兩層測試。

  • Python unit test 讓 usage 超過 1,000 tokens,再要求呼叫工具。測試確認 handler 沒有被執行,回傳的 tool result 是 error
  • Promptfoo suite 新增「Token 預算用完後不能執行工具」。它檢查 tool_handler_calledfalsetool_statuserror,以及回覆明確說出 Token 上限。

本次結果是 37 個 Python 測試通過;Promptfoo 5 passed、0 failed、0 errors。Promptfoo 在這裡仍然是本機 deterministic eval,不是用真實模型反覆燒 Token 的壓力測試。

45 秒不是完整的強制中斷

時間上限也有同樣的邊界。ChatOpenAI(timeout=20) 限制一個模型 HTTP 請求;累積 45 秒的預算則在模型呼叫前與工具執行前檢查。若目前有一個已經送出的請求卡住,後面的檢查無法把它從網路上抓回來。

所以現在的設計是:限制單次模型請求、限制整個 run 的下一步,並留下停止訊息。之後接上外部 API 或 MCP 工具時,每一個 client 仍要有自己的 timeout;不能以為 Agent 外層的 45 秒會自動取消所有事情。

Day 8 會處理另一種更危險的情況:工具失敗後,Agent 一直重試。上限能讓它停下來,但不會告訴它該等多久、何時改走 fallback,或何時應該直接升級給人處理。

本日程式碼

day-07-execution-budgets


上一篇
Day 6|AI Agent 不是在思考,它其實是在不斷試錯
下一篇
Day 8|重試不是保險:Agent 如何把一個錯誤變成事故
系列文
從 LLM、Agent 到 Guardrails:30 天打造可控、安全、可驗證的 AI Agent8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言