FDE core topic - Context Management:Token 預算管理與上下文修剪策略

Context Management 的本質是:在有限的 Token 預算內,決定哪些資訊值得保留、哪些必須壓縮或捨棄——這是所有長對話 AI 系統的核心資源排程問題。


一、為什麼面試官問這個

面試官測試的核心能力:

  • 系統資源意識:你是否理解 LLM 的上下文視窗是有限的硬性限制,而非軟性建議?能否量化每個元件的 Token 消耗?Token waterfall 問題若不主動管理,100 輪對話後必然觸發截斷或 OOM。
  • Trade-off 判斷力:截斷策略的選擇(FIFO vs. 重要性加權 vs. 摘要壓縮)直接影響對話品質,面試官想看你能否說清楚「何時選哪種、代價是什麼」,而不是背誦一個萬用答案。
  • 生產可操作性:理論上知道「要做 context trimming」很容易,但能否描述 LangGraph MemorySaver 的週期摘要節點、如何做 tenant-level 成本歸因,才是資深工程師的標誌。

弱答案長這樣:「我們用滑動視窗,把最舊的訊息刪掉就好。」——沒有量化 Token 預算、沒有解釋為什麼 FIFO 在長對話中會丟失關鍵系統指令。

強答案長這樣:「128K context 視窗中,system prompt 佔 8K、tools schema 佔 12K,剩餘 108K 給 history + answer。我們設 history ceiling 為 80K、answer reserve 為 28K。超過 history ceiling 時觸發階層式摘要:先壓縮最舊的 20 輪,~6K tokens 壓成 ~500 tokens,然後再做 FIFO。這樣能維持對話連貫性同時控制成本。壓縮比約 13:1,每次摘要呼叫用 Gemini Flash,成本不到主對話的 5%。」


二、核心原理與技術深度

Token 預算分配公式

LLM 的上下文視窗 $C$ 是固定上限(以 Claude Sonnet 4 為例:200K tokens;Gemini 1.5 Pro:1M tokens;GPT-4o:128K tokens)。每次推理呼叫的 Token 組成:

C = P_system + P_tools + P_history + P_answer_reserve

其中:
  P_system        = system prompt tokens(通常 4K–16K)
  P_tools         = tools/function schema tokens(每個工具約 200–800 tokens)
  P_history       = 對話歷史 tokens(動態增長,主要管理目標)
  P_answer_reserve= 輸出保留空間(通常 4K–32K)

以具體數字為例(128K context 視窗):

C          = 128,000 tokens
P_system   =   8,000 tokens  (system prompt)
P_tools    =  12,000 tokens  (15 個工具 × 平均 800 tokens)
P_reserve  =  16,000 tokens  (answer reserve)
─────────────────────────────
P_history  ≤  92,000 tokens  (history ceiling)

Token Waterfall 問題:若不主動管理,每輪對話累積 ~800 tokens(user + assistant 各 ~400),成長曲線:

Turn 10  →   8,000 tokens   (9% history ceiling)   安全
Turn 50  →  40,000 tokens   (43% history ceiling)  警戒
Turn 100 →  80,000 tokens   (87% history ceiling)  危險
Turn 115 →  92,000 tokens   觸發 OOM 或強制截斷    ← 問題發生點

上下文視窗的記憶體布局

┌───────────────────────────────────────────────────────────────┐
│  CONTEXT WINDOW  (128K tokens = 100%)                         │
├──────────────┬─────────────────┬─────────────────────────────┤
│ System Prompt│  Tools Schema   │  History Budget             │
│   8K ( 6%)   │   12K ( 9%)     │   92K (72%)                 │
│   [靜態]     │   [半靜態]      │   [動態,需主動管理]        │
│              │  啟動時一次計算  │  每輪 +800 tokens,需修剪  │
└──────────────┴─────────────────┴──────────────┬──────────────┘
                                                │ Answer Reserve
                                                │ 16K (13%)
                                     ┌──────────▼──────────────┐
                                     │  [輸出空間,不可壓縮]   │
                                     │  設太小 → 輸出被截斷    │
                                     │  設太大 → history 空間少│
                                     └─────────────────────────┘

三種截斷策略的機制圖

策略一:FIFO(先進先出截斷)

最簡單,直接丟棄最舊的訊息。問題:若第一輪定義了「角色設定」或「任務目標」,FIFO 會把最重要的上下文刪掉。

FIFO 截斷前:                    FIFO 截斷後(超出 ceiling):
┌─────────────────────┐          ┌─────────────────────┐
│ Turn  1: [角色定義] │ ← 刪除   │ Turn 21: 普通問答   │
│ Turn  2: [任務目標] │ ← 刪除   │ Turn 22: 普通問答   │
│ Turn  3: [重要約束] │ ← 刪除   │ ...                 │
│ ...                 │          │ Turn 49: 當前話題   │
│ Turn 20: 普通問答   │ ← 刪除   │ Turn 50: 最新輸入   │
│ Turn 21: 普通問答   │ ← 保留   └─────────────────────┘
│ ...                 │          問題:角色設定消失,
│ Turn 50: 最新輸入   │ ← 保留   模型行為改變
└─────────────────────┘

策略二:重要性加權截斷(Importance-Weighted)

為每個 turn 計算重要性分數,優先保留高分 turn。

重要性分數計算:
  score(turn_i) = α × recency_score(i)
                + β × semantic_relevance(turn_i, current_query)
                + γ × explicit_marker_score(turn_i)

其中:
  recency_score  = exp(-λ × age)          (λ ≈ 0.05,衰減因子)
  semantic_score = cosine_sim(embed(turn), embed(query))
  marker_score   = 1.0 if contains("記住", "重要", "必須") else 0.0
  α=0.3, β=0.5, γ=0.2

典型分數範例:
  Turn  1 [角色定義]   → recency=0.08, semantic=0.9, marker=1.0 → score=0.75  ← 高,保留
  Turn 20 [普通問答]   → recency=0.37, semantic=0.2, marker=0.0 → score=0.21  ← 低,可刪
  Turn 49 [相關討論]   → recency=0.90, semantic=0.8, marker=0.0 → score=0.67  ← 高,保留

代價:每次截斷需要做 embedding 計算,約 +5–10ms latency(可用 cache 優化到 <1ms)。

策略三:階層式摘要壓縮(Hierarchical Summarization)

觸發前(history > 80% ceiling):
┌────────────────────────────────────────────┐
│  Turn 1–20  ≈ 16,000 tokens  [待壓縮]      │
│  Turn 21–50 ≈ 24,000 tokens  [保留完整]    │
│  Total      ≈ 40,000 tokens                │
└───────────────────┬────────────────────────┘
                    │  呼叫輕量摘要模型
                    ▼
┌────────────────────────────────────────────┐
│  摘要節點(Gemini Flash / Claude Haiku)   │
│  Prompt: "壓縮為 500 tokens,保留:        │
│    - 使用者目標與角色設定                  │
│    - 已確認的事實與決策                    │
│    - 尚未解決的問題                        │
│  省略:重複問答、格式說明、中間推理"       │
└───────────────────┬────────────────────────┘
                    │  輸出 ≈ 500 tokens
                    ▼
觸發後:
┌────────────────────────────────────────────┐
│  [SUMMARY] Turn 1–20 摘要  ≈    500 tokens │
│  [LIVE]    Turn 21–50      ≈ 24,000 tokens │
│  Total                     ≈ 24,500 tokens │
│  節省:40,000 → 24,500(-38.75%)          │
└────────────────────────────────────────────┘

壓縮效果量化

策略50 輪後 tokens100 輪後 tokens對話品質延遲開銷
無管理40K80K(接近 OOM)高(直到爆掉)0ms
FIFO40K(固定上限)40K中(失憶風險)<1ms
重要性加權40K(固定上限)40K+5–10ms
階層式摘要24.5K24.5K(持續壓縮)最高+500–1000ms(每 20 輪一次)

工具輸出修剪(Tool Output Trimming)

工具呼叫回傳的 JSON 往往是 token 最大的浪費來源。一次搜尋工具可能回傳 20 筆結果,每筆含長文本與大量 metadata,輕易超過 2,000 tokens。

工具呼叫前後的 Context 占用:

呼叫前  ──────── 工具呼叫(搜尋 20 筆結果)──────── 呼叫後(未修剪)
history=30K                                           history=30K + 工具輸出
                                                      = 30K + 2,400 = 32,400K

若連續 10 次工具呼叫,未修剪:
  history = 30K + (10 × 2,400) = 54,000 tokens  ← 工具輸出佔 55%
  
若每次工具輸出修剪至 180 tokens:
  history = 30K + (10 × 180)   = 31,800 tokens  ← 工具輸出只佔 6%

修剪後的 JSON 結構(以搜尋工具為例):

 1def trim_tool_output(raw: dict, tool_name: str) -> dict:
 2    """標準化工具輸出修剪"""
 3    config = TRIM_CONFIG[tool_name]  # per-tool 設定
 4    result = {}
 5
 6    # 1. 陣列截斷:只保留 top-N
 7    if "results" in raw:
 8        result["results"] = [
 9            _trim_item(item, config.keep_fields, config.max_content_chars)
10            for item in raw["results"][:config.top_n]  # top_n = 3–5
11        ]
12
13    # 2. 保留關鍵計數欄位(用於後續推理)
14    for key in config.keep_scalar_keys:   # ["total_count", "status"]
15        if key in raw and raw[key] is not None:
16            result[key] = raw[key]
17
18    # 3. 隱式:忽略 debug_info, trace_id, null 欄位
19    return result
20
21def _trim_item(item: dict, keep_fields: list, max_chars: int) -> dict:
22    trimmed = {k: v for k, v in item.items() if k in keep_fields and v is not None}
23    if "content" in trimmed and len(trimmed["content"]) > max_chars:
24        trimmed["content"] = trimmed["content"][:max_chars] + "...[truncated]"
25    return trimmed

修剪規則優先順序:

  1. 陣列截斷:只保留 top-N(通常 N=3–5,視 LLM 推理需求而定)
  2. 移除 null 值欄位(直接節省 JSON key 空間)
  3. 長文字截斷:content > 200 字元時截斷並加 ...[truncated]
  4. 移除純除錯欄位(debug_info, trace_id, checksum, query_time_ms
  5. 只保留推理所需欄位(whitelist 模式優於 blacklist)

三、三個實作層次

Layer 1 — 最小可行(Minimal)

實作方式:純 FIFO 截斷 + 固定 Token 計數 + 靜態工具輸出截斷

 1def trim_history_fifo(messages: list, max_tokens: int) -> list:
 2    """從最新訊息往前保留,直到 token 預算用完"""
 3    total = 0
 4    result = []
 5    for msg in reversed(messages):  # 從最新開始保留
 6        t = count_tokens(msg)
 7        if total + t > max_tokens:
 8            break
 9        result.insert(0, msg)
10        total += t
11    return result
12
13# 呼叫前:計算 static overhead
14STATIC_TOKENS = count_tokens(SYSTEM_PROMPT) + count_tokens(TOOLS_SCHEMA)
15HISTORY_CEILING = CONTEXT_WINDOW - STATIC_TOKENS - ANSWER_RESERVE
16# 128K - 8K - 12K - 16K = 92K
17
18# 每輪呼叫前執行
19messages = trim_history_fifo(messages, HISTORY_CEILING)

解決的問題:防止 context OOM,成本可控,對話不會因 API 報錯而中斷。 剩下的問題:FIFO 可能丟失關鍵早期上下文(角色設定、任務目標),對話可能突然「失憶」。工具輸出若未 trim,幾輪後仍快速耗盡 budget。 複雜度:1 天實作,token counting 用 tiktoken 或模型 API 的 count_tokens 端點,零額外 API 成本。 適用場景:POC 驗證、單輪 Q&A bot、< 20 輪的短對話場景、對話連貫性要求不高的場景。


Layer 2 — 生產就緒(Production-Ready)

實作方式:LangGraph MemorySaver + 週期摘要節點 + Pin 機制保護早期 turns

架構:每 20 輪觸發一次摘要節點,使用輕量模型(Gemini Flash 或 Claude Haiku)壓縮最舊的 N 輪。前 3 輪(通常含角色設定)標記為 pinned,不參與任何截斷。

LangGraph 圖結構:

┌──────────┐    ┌──────────────┐    ┌───────────────────┐
│  User    │───▶│  Agent Node  │───▶│  Tool Exec Node   │
│  Input   │    │  (主推理)    │    │  + Output Trimmer │
└──────────┘    └──────┬───────┘    └─────────┬─────────┘
                       │                       │
                       ▼                       │
              ┌────────────────┐               │
              │ should_        │◀──────────────┘
              │ summarize()?   │  每輪結束後檢查
              └───────┬────────┘
                  YES │     NO
                      ▼      ▼
              ┌──────────┐  繼續
              │ Summarize│
              │  Node    │  (Gemini Flash)
              └──────────┘
 1from langgraph.checkpoint.memory import MemorySaver
 2from langchain_core.messages import SystemMessage
 3
 4PINNED_TURNS = 3          # 前 N 輪不參與截斷
 5SUMMARY_EVERY_N = 20      # 每 N 輪觸發摘要
 6SUMMARY_TARGET_TOKENS = 500
 7
 8def should_summarize(state: AgentState) -> bool:
 9    """每 20 輪觸發,或 history token > 70% ceiling"""
10    live_messages = state["messages"][PINNED_TURNS * 2:]  # 跳過 pinned turns
11    turn_count = len(live_messages) // 2
12    history_tokens = count_tokens(live_messages)
13    return (turn_count > 0 and turn_count % SUMMARY_EVERY_N == 0) \
14        or history_tokens > int(HISTORY_CEILING * 0.70)
15
16async def summarize_node(state: AgentState) -> AgentState:
17    pinned = state["messages"][:PINNED_TURNS * 2]      # 永不壓縮
18    live = state["messages"][PINNED_TURNS * 2:]         # 可壓縮部分
19    oldest = live[:SUMMARY_EVERY_N * 2]                 # 最舊 N 輪
20    remaining = live[SUMMARY_EVERY_N * 2:]              # 保留的 live 部分
21
22    summary_prompt = f"""將以下對話壓縮為繁體中文摘要,不超過 {SUMMARY_TARGET_TOKENS} tokens。
23保留:使用者目標、角色設定、重要決策、已確認事實、未解決問題。
24省略:重複問答、格式說明、中間推理過程、過時的臨時資料。
25
26{format_messages(oldest)}"""
27
28    summary = await gemini_flash.ainvoke(summary_prompt)
29
30    # 驗證摘要 token 數(LLM 輸出長度不穩定)
31    summary_tokens = count_tokens(summary.content)
32    if summary_tokens > SUMMARY_TARGET_TOKENS * 1.5:
33        # 遞迴再壓縮(通常不需要,但要有防護)
34        summary = await compress_further(summary.content, SUMMARY_TARGET_TOKENS)
35
36    summary_msg = SystemMessage(
37        content=f"[對話摘要 Turn {PINNED_TURNS+1}{PINNED_TURNS+SUMMARY_EVERY_N}]\n{summary.content}"
38    )
39    return {"messages": pinned + [summary_msg] + remaining}

新增元件:Pin 機制、摘要觸發邏輯、輕量摘要模型呼叫、摘要 token 驗證。 解決的問題:大幅延長可用對話輪數(理論上無限輪)、保護早期關鍵上下文、維持對話語意連貫性。 代價:每次摘要 ~0.5–1 秒額外 latency(異步不阻塞主流程),摘要模型 API 成本(Gemini Flash 輸入 $0.075/1M tokens,20 輪對話 ~16K tokens ≈ $0.0012,成本極低)。 適用場景:生產 chatbot、客服系統、法律/醫療助理(需要長期記憶)、10K–200K DAU。


Layer 3 — 企業級(Enterprise-Grade)

實作方式:分級 Token 預算 + 成本歸因 + 即時監控 + 配額執法

Tenant 分級預算配置(範例):

┌────────────────┬──────────────────┬──────────────┬────────────┬───────────────┐
│  Tenant Class  │  Context Budget  │ Summary Freq │ Tool Top-N │ 月 Token 配額 │
├────────────────┼──────────────────┼──────────────┼────────────┼───────────────┤
│  Free          │   32K tokens     │  每 10 輪    │    2 筆    │  10M tokens   │
│  Pro           │   92K tokens     │  每 20 輪    │    5 筆    │ 100M tokens   │
│  Enterprise    │  180K tokens     │  每 40 輪    │   10 筆    │   unlimited   │
└────────────────┴──────────────────┴──────────────┴────────────┴───────────────┘

成本歸因追蹤

 1@dataclass
 2class ContextCostAttribution:
 3    tenant_id: str
 4    session_id: str
 5    turn_id: int
 6    system_tokens: int          # 靜態,通常可用 prompt cache 降低成本
 7    tools_tokens: int           # schema 靜態 + 輸出動態
 8    history_tokens: int         # 主要管理目標
 9    answer_tokens: int          # 輸出 tokens(成本通常高 3–5 倍)
10    total_tokens: int
11    summary_invocations: int    # 本次 session 觸發摘要次數
12    estimated_cost_usd: float   # (input_tokens × $3/1M) + (output_tokens × $15/1M)
13    # Claude Sonnet 4 定價,Enterprise tier 可能有折扣
14
15# 每輪寫入 BigQuery / Cloud Spanner 供帳單稽核
16# 可觸發配額告警:月用量 > 80% → 降速;> 100% → 拒絕服務

可觀測性指標(Prometheus / Cloud Monitoring):

# 即時監控
context_utilization_ratio{tenant_id, session_id}
  = history_tokens / history_ceiling
  # 告警閾值:> 0.80

# 壓縮效率
summary_compression_ratio{tenant_class}
  = tokens_before_summary / tokens_after_summary
  # 健康值:> 10x;< 5x 代表摘要 prompt 需要優化

# 工具成本節省
tool_trim_savings_tokens_total{tool_name}
  = raw_output_tokens - trimmed_output_tokens
  # 追蹤哪些工具最「浪費」context

# 異常事件
context_oom_events_total{tenant_class}
  # 應接近 0;若不為 0,代表 ceiling 設定或監控有問題
  
# 摘要延遲
summary_latency_seconds{p50, p95, p99}
  # p95 應 < 2s;超過代表摘要模型過載

新增元件:Tenant 配置服務(動態載入,無需重部署)、成本歸因 pipeline(寫入資料倉儲)、即時預算監控 dashboard(Grafana / Looker Studio)、配額執法中間件(超限降速或拒絕)。 解決的問題:多租戶公平性(免費用戶不影響付費用戶)、成本可見性(知道每個 session 花多少錢)、合規審計(誰在什麼時間用了多少 token)。 代價:額外工程複雜度約 2–3 週,監控基建成本 ~$50–200/月(相對 API 成本微不足道)。 適用場景:SaaS 平台、企業內部 AI gateway、200K+ MAU、需要成本分攤的多部門部署。


四、為什麼選 X 不選 Y

決策選擇 X選 X 的理由不選 Y 的理由翻轉條件(何時選 Y)
截斷策略階層式摘要保留語意,對話可無限延伸,13:1 壓縮比FIFO:刪掉早期關鍵 turns,對話失憶對話 < 20 輪、角色設定不重要時選 FIFO
摘要模型Gemini Flash成本 $0.075/1M tokens,延遲 ~300ms主模型(Claude Sonnet 4 $3/1M):摘要成本佔主對話 40 倍摘要品質極為關鍵(法律/醫療)時才考慮主模型
觸發機制每 N 輪 + Token 閾值 雙觸發兼顧固定節奏與動態保護僅每 N 輪:工具呼叫密集時可能在 N 輪內就 OOM對話輪次與 token 消耗高度相關時單觸發即可
工具輸出Whitelist 欄位保留只留推理所需欄位,安全可控Blacklist 欄位刪除:新工具欄位容易遺漏,token 膨脹難發現工具輸出結構極不穩定時用 blacklist 作為補充
Pinning 策略前 N 輪固定保護角色設定/任務目標通常在開頭,永不丟失依重要性分數決定:計算複雜,且初始 turns 在早期可能分數不高對話中途才有關鍵設定時改用重要性加權
Budget 配置分級 tenant class資源隔離,付費用戶不受免費用戶影響全局統一 budget:免費用戶長對話會耗盡資源池,拖累付費用戶單一租戶系統(內部工具)時無需分級

五、系統效應:導入 Context Management 前後

指標導入前(無管理)導入後(Layer 2)改善幅度
最大可用對話輪數~115 輪(128K 窗格爆滿)理論無限(週期摘要)
100 輪對話 token 消耗80,000 tokens~28,000 tokens(摘要+trim)-65%
100 輪對話估算成本$0.24(輸入)$0.084(輸入)-65%
工具輸出 token 占比~55%(10 次工具呼叫)~6%(修剪後)-90%
對話 OOM 發生率~100 輪後必發~0(有監控+提前觸發)-100%
摘要觸發延遲(p95)N/A~800ms(Gemini Flash)每 20 輪一次,可接受
對話品質(長對話)100 輪後急降(上下文丟失)穩定(摘要保留關鍵語意)主觀 +40%

六、常見錯誤與陷阱

錯誤模式後果正確做法
把完整工具 JSON 回應放進 context單次工具呼叫消耗 2K–10K tokens,10 次工具後 budget 耗盡呼叫後立即 trim:whitelist 欄位、截斷陣列至 top-N、移除 null
純 FIFO 不保護早期 turns角色設定、使用者目標被刪除,對話「失憶」,品質崩潰前 3 輪標記為 pinned,永不參與 FIFO 截斷
沒有 budget 監控,等 API 報錯才知道超限線上 OOM 導致對話中斷,用戶看到錯誤訊息設 80% ceiling 告警,提前觸發摘要,勿等到 100%
摘要節點用跟主模型相同的大模型摘要成本 ≈ 主對話成本,吃掉所有成本節省(40 倍差距)摘要用 Gemini Flash / Claude Haiku,成本低 10–40 倍
忽略 tools schema 的靜態 Token 消耗誤以為 history budget 有 108K,實際被 tools 佔走 12K啟動時計算 static overhead,動態調整 history ceiling
對所有 tenant 用相同 context 策略免費用戶消耗企業級 Token 預算,SaaS 平台成本失控依 tenant class 設定差異化 budget、summary frequency、trim 策略
摘要後不驗證壓縮後 token 數LLM 輸出長度不穩定,摘要可能超過目標 500 tokens 的 2–3 倍對摘要結果做 token 計數斷言,超上限時遞迴再壓縮一次

七、與其他核心主題的關聯

  • RAG 架構設計(本系列 Part 2):RAG 的 retrieved chunks 是 context 最大的動態消耗來源之一;chunk size 策略(256 vs 512 tokens)與 top-K 設定直接影響 context budget 分配。Context management 的 budget 計算必須為 RAG chunks 預留空間。
  • 工具呼叫(Tool Use / Function Calling)(fde-interview-guide Part 31,ADK 深度解析):每個工具的 schema 定義消耗靜態 Token(12K for 15 tools),工具回傳結果消耗動態 Token;Tool Output Trimming 是 context management 的必要子策略,兩者緊密耦合。
  • Agent 記憶體架構(Memory Architecture)(本系列後續 Part):長期記憶(episodic memory store / vector DB)與短期 context window 的分工——什麼存 Vertex AI Vector Search、什麼留在 context,是 context management 的上游架構決策。若記憶體分層設計正確,context 壓力可大幅降低。
  • 成本最佳化(Cost Optimization)(fde-interview-guide Part 35–38):Token 預算管理直接等於成本控制。Prompt caching(系統提示快取,Claude / Gemini 均支援)可讓靜態 system prompt 的 Token 成本降低 90%,是 context management 的重要補充策略。

八、面試一句話(Killer Phrase)

「Context management 是 LLM 系統的記憶體排程問題:在固定的 Token 預算內(例如 128K 視窗中,system 8K + tools 12K + reserve 16K,history ceiling 約 92K),決定哪些資訊值得保留。純 FIFO 最簡單但會刪掉關鍵早期上下文,所以生產系統應採用 Pin 機制保護前 3 輪 + 階層式摘要壓縮——每 20 輪用 Gemini Flash 把最舊的 20 輪(約 16K tokens)壓縮到 500 tokens,實現 32:1 壓縮比,讓對話無限延伸而不失憶。工具輸出修剪同樣關鍵:用 whitelist 欄位保留策略把完整 JSON 回應從 2,400 tokens 降至 180 tokens,節省 92%,十次工具呼叫就能省下 22K tokens 的 budget。企業級再加上 tenant 分級預算與 Token 成本歸因,才能在 SaaS 場景下做到公平的多租戶資源管理,避免免費用戶壓垮付費用戶的 context 配額。」


系列導航

← 前一篇(本系列第一篇) | 後一篇:RAG 架構設計與向量檢索策略

Yen

Yen

Yen