Skip to content

turn_context 與提示 (prompt) 組裝

源码版本v2026.7.20

職責

在每一輪 API 呼叫前,把「歷史訊息 + 本輪使用者輸入 + 網關 (gateway) 側附加上下文 (context) + 壓縮 (compression) 標記」裝配成發給 provider 的 api_messagesTurnContext 是這輪產物的容器,build_turn_context 是裝配入口。它還負責把網關收集的筆記(notes)注入到多模態內容裡。

關鍵檔案

資料流

  1. 主迴圈每輪呼叫 build_turn_context:268,得到一個 TurnContext
  2. compose_user_api_content(agent/turn_context.py:44)把使用者原始輸入拼成 API 內容。
  3. 若網關側累積了 notes(來自附件、上下文提示),consume_gateway_turn_context_notes(agent/turn_context.py:124)取出並經 append_notes_to_multimodal_content 注入。
  4. substitute_api_content(agent/turn_context.py:79)替換占位符;drop_stale_api_content 清理過期內容。
  5. 若上一輪發生過壓縮,reanchor_current_turn_user_idx(agent/turn_context.py:164)修正本輪使用者訊息在歷史中的位置。
  6. 裝配好的 api_messages 進入 API 呼叫重試子迴圈:1227

TurnContext 是普通 dataclass,把 prologue 算出來的值捆成一份(關鍵欄位見 agent/turn_context.py:242-268):user_message 是消毒後的本輪訊息;original_user_message 保留原始文字用於記憶與日誌查詢(nudge 不會注入進去);conversation_history 可能被 preflight 壓縮置 None——壓縮會開新會話,舊 history 不能再用;active_system_prompt 是本輪快取的系統提示,也可能被壓縮重建。

網關 notes 的消費邏輯見 agent/turn_context.py:124-145:

python
def consume_gateway_turn_context_notes(agent: Any) -> str:
    """Pop the gateway's per-turn must-deliver notes off the agent (one-shot).
    ... so the composed system prompt stays byte-stable turn-over-turn.
    ... this consumes them so a cached agent can never replay a stale note."""
    notes = getattr(agent, "_gateway_turn_context_notes", "") or ""
    if hasattr(agent, "_gateway_turn_context_notes"):
        try:
            agent._gateway_turn_context_notes = ""
        except Exception:
            pass
    return notes if isinstance(notes, str) else ""

取完立即清零——這是個一次性 pop,不是 peek。即便 agent 被快取複用,上一輪沒消費完的 notes 也不會洩漏到下一輪。

設計動機

為什麼把 prologue 抽成 build_turn_context?原來所有「每輪一次」的準備(stdio 守衛、retry 計數器重置、user message 消毒、todo/nudge 注入、系統提示建構、preflight 壓縮、pre_llm_call 鉤子、外部記憶預取、崩潰復原持久化)都內嵌在 run_conversation 開頭,接近 130 行。抽成函式後,主迴圈只剩「迴圈 + 重試」,讀主迴圈時不必被準備邏輯打斷;prologue 還可以單獨測試,subagent 路徑也能複用。

為什麼 notes 走使用者訊息旁路而不是塞進 system prompt?系統提示在多輪之間需要 byte 穩定——一旦改變,前面快取的 prefix 就失效,provider 側的 prompt caching 直接報銷。網關把易變內容(首次自我介紹、語音通道切換、自動 reset 的臨時提示)從 system prompt 裡挪出來,透過使用者訊息的 api_content sidecar 投遞,既讓每輪都能帶上新事實,又保住了快取命中率。

_compression_made_progress 用 5% 作為 material 閾值,是為了防多輪壓縮空轉:某輪把 220 條訊息壓成 220 條但 token 從 288k 降到 183k,按行數判斷會誤報「壓不動了」然後觸發 auto-reset。

邊界與失敗

  • 多模態訊息遺失 notes:compose_user_api_content 對非字串內容回傳 None,使用者這輪發圖片時 sidecar 形式的 notes 會靜默遺失。append_notes_to_multimodal_content 兜底把 notes 作為 text part 直接 append,代價是這部分內容會被持久化進 transcript——網關側必須只放真正需要落盤的內容。
  • 壓縮後使用者訊息索引漂移:壓縮把舊訊息合成 summary,本輪使用者訊息在 messages 裡的位置會變。reanchor_current_turn_user_idx 重新掃一遍 list 找到下標;不重定位會讓工具呼叫 (tool dispatch) 結果回填找錯位置。
  • 少量但超大訊息:_should_run_preflight_estimate 舊版只看訊息條數,幾條巨型 base64 圖片永遠不會觸發壓縮,直接撞 hard overflow(#27405)。新版加了 char-based 估算分支,大訊息也能命中閾值。
  • stale notes 殘留:consume_gateway_turn_context_notes 用 try/except 兜住寫入失敗——即便 agent 屬性是唯讀 property,也不會因清零失敗而中斷裝配。

小結

turn_context 是「每輪的快照工廠」:把動態上下文固化為一個可發送的結構。它和上下文壓縮協同——壓縮改寫歷史後,turn_context 負責把指標重新對齊。

非官方社群學習站,內容以 MIT 授權的 NousResearch/hermes-agent 原始碼為依據。