iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0
AI Engineering

AI Agent 系統開發 30 天系列 第 12

讓 Agent 在迴圈中自主決策

  • 分享至 

  • xImage
  •  

在常見的工具呼叫模式中,任務往往是一問一答:使用者提出明確問題,模型呼叫一次工具取得資料,接著直接回答結束流程。如果任務需要的資料一開始就能確定,系統的控制流就是一條直線,寫成固定的工作流(Workflow)就能穩定運作。

但真實世界中的探索型任務——例如線上系統事故排查,往往只有模糊的初始症狀,無法在出發時就預先決定完整的執行步驟。

假設工程師收到一則事故通報:「10:00 起 checkout API 的 5xx 比例升高,顧客結帳時會在付款步驟逾時。」通報中只有症狀,沒有指出具體是哪一個下游依賴故障。在開始調查時,程式無法預先寫死排查步驟:

  • 如果日誌搜尋查到 connection refused to redis-primary:6379,下一步就必須深入檢查 Redis 的健康狀態。
  • 如果日誌查到的是 database pool utilization 100%,下一步則應該改查資料庫連線池與慢查詢。
  • 如果日誌查無錯誤,下一步可能需要擴大時間窗。

第二步要查 Redis 還是 Database,完全取決於第一步工具執行的真實環境資料。在 Agent 架構中,將工具執行結果送回給模型感知的資訊稱為 Observation(觀察,在程式中即 ToolMessage

因為程式無法預知未來的 Observation,排查過程不能是一條直線走到底,而必須形成閉環:讓模型每執行一次工具,就停下來讀取環境回饋,再決定下一步。這種「執行動作 -> 觀察結果 -> 重新決策」的循環機制,就是 Agent Loop(自主決策迴圈)

https://ithelp.ithome.com.tw/upload/images/20260923/20111896SUrJukLdkA.png

在 Agent Loop 中,排查路徑由模型與 LangGraph 分工推進:

  1. 模型負責解讀與決策:模型每一輪讀取 State 中累積的所有訊息。它觀察上一輪工具回傳的 Observation,判斷目前的假設還缺什麼證據,進而決定下一個動作(是發起另一次查詢,還是證據充足可以輸出結論)。
  2. 應用程式負責執行與閉環:LangGraph 負責執行模型指定的 Python 工具函式,把執行結果作為 Observation 包裝為 ToolMessage 寫回 State,再將控制權交回給模型節點。

只要模型持續要求呼叫工具,流程就會在模型與工具節點之間反覆循環;直到模型判定證據齊全、不再產生工具呼叫時,條件邊才會結束流程。

本篇對應的範例程式在 ai-agent-sample/langgraph/langgraph-incident-investigation。我們以「線上系統事故排查」為例,通報輸入固定為:

「10:00 起 checkout API 的 5xx 比例升高,顧客結帳時會在付款步驟逾時。」

範例在 tools.py 定義兩個唯讀查詢工具,使用內建資料模擬服務日誌與監控指標:

  • 跨服務日誌查詢:搜尋時間窗內的系統日誌,找出異常訊息與可疑的相依服務。
  • 相依服務健康檢查:查詢特定相依服務(例如快取或資料庫)的即時健康狀態與指標數據,驗證日誌發現的疑點。

模型無法單靠通報判斷根因,必須先透過日誌取得線索,再依據 Observation 決定是否深入檢查特定服務,直到掌握充分證據才收斂出報告。

用 System Prompt 定義調查規則與收斂條件

Agent Loop 需要清楚的系統提示詞規範其行為邊界。Prompt 確立分析目標與原則,要求模型根據真實證據推論,並在掌握充分因果後收斂報告:

SYSTEM_PROMPT = """你是負責排查系統線上事故的分析助理。

目前時間:2026-07-30T10:05:00+08:00

排查原則:
- 根據通報症狀追查故障根因,區分表面症狀與下游相依服務問題。
- 只能依據工具回傳的真實數據進行推論,不得假設未經證實的事實。
- 當從日誌發現可疑的相依服務時,應進一步驗證該服務的即時狀態。
- 掌握充分證據後停止呼叫工具,輸出結構化根因分析與處置建議。
- 不得要求執行部署、重啟、封鎖帳號、刪除資料或修改正式環境。
"""

Prompt 著重於原則性的指引(區分症狀與下游問題、發現可疑依賴時深入驗證),具體的工具參數與用法則留在工具本身的 Docstring 中。這讓模型在第一輪取得日誌線索後,能自主推導出下一步需要檢查相依服務的健康度。

定義供模型呼叫的排查工具

tools.py 中,我們使用 @tool 裝飾器定義兩項查詢工具。模型只會讀取函式簽名、型別標註與 Docstring 來決定是否呼叫工具與如何填入參數,不會看到函式內部的實作細節:

from typing import Literal
from langchain_core.tools import tool

DependencyService = Literal["database", "redis"]
LogLevel = Literal["ERROR", "WARN", "INFO"]


@tool
def search_logs(level: LogLevel | None = None, minutes: int = 15) -> str:
    """搜尋時間窗內所有服務的日誌,不需要指定服務,用 trace_id 串接跨服務的紀錄。

    Args:
        level: 只回傳這個等級的日誌(ERROR、WARN 或 INFO)。不指定則回傳所有等級。
        minutes: 往回查詢的分鐘數(相對於目前時間)。
    """
    ...


@tool
def check_service_health(service: DependencyService) -> str:
    """查詢下游依賴目前的健康狀態,用來驗證日誌裡追到的服務是否真的異常。

    Args:
        service: 要查詢的下游依賴服務名稱。
    """
    ...

這兩個工具有兩項設計特點:

  1. search_logs 允許跨服務探索:工具不需要指定服務名稱,模型只要根據通報時間帶入 levelminutes,就能一次取得時間窗內所有服務的日誌,從中找出帶有相同 trace_id 的關聯錯誤。
  2. check_service_health 限定依賴型別service 參數使用 Literal["database", "redis"] 型別限制,模型在被日誌引導時,只能驗證這兩個下游相依服務,避免模型隨意填入不存在的服務名稱。

用 LangGraph 組裝決策迴圈

定義好工具後,我們在 workflow.py 將模型與工具組裝成迴圈。

在 LangGraph 中,讓流程形成迴圈的核心在於兩個連線設定:一個條件分支決定是否需要呼叫工具,以及一條把工具結果送回模型的連線。只要模型持續發出 Tool Call,流程就會不斷把最新的 Observation 帶回模型手中,驅動下一輪決策:

# 1. 綁定排查工具清單至模型
tools = [search_logs, check_service_health]
model_with_tools = model.bind_tools(tools)


# 2. 模型決策節點:讀取累積的訊息歷史,決定下一步行動
def investigate(state: MessagesState):
    messages = [
        SystemMessage(content=SYSTEM_PROMPT),
        *state["messages"],
    ]
    response = model_with_tools.invoke(messages)
    return {"messages": [response]}


# 3. 建立 Graph 並註冊節點
builder = StateGraph(MessagesState)
builder.add_node("investigator_agent", investigate)
builder.add_node("tools", ToolNode(tools))

# 4. 設定流程連線
builder.add_edge(START, "investigator_agent")
# 條件分支:模型產生 Tool Call 則前往 tools;無 Tool Call 則前往 END
builder.add_conditional_edges("investigator_agent", tools_condition)
# 工具執行完畢後連回模型,帶入 Observation 進行下一輪決策
builder.add_edge("tools", "investigator_agent")

graph = builder.compile()

這段程式碼由三個部分組成:

  1. *state["messages"] 展開完整歷史:每次進入 investigate 節點時,模型都能讀到最初的使用者輸入、前面幾輪模型提出的 Tool Call,以及工具回傳的 ToolMessage
  2. tools_condition 路由分流:檢查最後一則 AIMessage 是否包含 tool_calls。有呼叫就導向 tools 節點;沒有呼叫就代表模型判定調查完成,導向 END
  3. builder.add_edge("tools", "investigator_agent") 連回模型ToolNode 執行完工具並產出 ToolMessage 後,藉由這條連線再次觸發 investigate,把 Observation 送回模型手中驅動下一輪決策。

決策迴圈的實際執行過程

初始輸入為事故通報:

「10:00 起 checkout API 的 5xx 比例升高,顧客結帳時會在付款步驟逾時。」

接下來看模型如何在一輪又一輪的動作與 Observation 推進下收斂出根因。

第 1 輪:初始通報 -> 搜尋錯誤日誌

通報中只有「結帳逾時」的表面症狀,模型決定先查詢跨服務日誌以尋找錯誤線索:

  • 模型動作search_logs(level="ERROR", minutes=15)
  • 工具回傳(Observation 1)
    [
      {
        "timestamp": "2026-07-30T10:03:06+08:00",
        "level": "ERROR",
        "message": "connection refused to redis-primary:6379; client fallback triggered",
        "trace_id": "trace-checkout-7f3a"
      },
      {
        "timestamp": "2026-07-30T10:03:08+08:00",
        "level": "ERROR",
        "message": "redis GET session-cache miss on checkout:session:7f3a timed out after 200 ms",
        "trace_id": "trace-checkout-7f3a"
      },
      {
        "timestamp": "2026-07-30T10:03:12+08:00",
        "level": "ERROR",
        "message": "payment request timed out after 5000 ms",
        "trace_id": "trace-checkout-7f3a"
      }
    ]
    

第 2 輪:解讀日誌 -> 深入檢查 Redis 健康度

模型讀取第一筆 Observation。雖然表面症狀是付款逾時(10:03:12),但同一筆請求早在 10:03:06 就出現了 connection refused to redis-primary:6379。為了確認 Redis 是短暫網路抖動還是服務徹底斷線,模型發起第二個查詢:

  • 模型動作check_service_health(service="redis")
  • 工具回傳(Observation 2)
    {
      "service": "redis",
      "status": "down",
      "signals": {
        "connection_errors": {"value": 512, "baseline": 3},
        "timeout_percent": {"value": 100, "baseline": 0}
      },
      "detail": "所有連線嘗試皆失敗,redis-primary 目前完全無法連線,判定為 down。"
    }
    

第 3 輪:證據齊全 -> 產出根因報告並終止

此時日誌與健康度證據互相印證,形成完整的因果鏈條(Redis 當機 -> 快取讀取逾時 -> 結帳逾時)。模型判定證據充足,不再呼叫工具,直接輸出結構化根因報告:

根因分析:
1. 10:03:06 起 checkout 服務嘗試連線 redis-primary:6379 失敗。
2. 健康度檢查確認 redis 服務狀態為 down,連線錯誤數達 512 次(基準值為 3)。
3. 顧客結帳時因 session cache 讀取逾時(200 ms)與付款請求等待逾時(5000 ms)而中斷。

處置建議:
優先重啟或切換 redis-primary 節點至備援實例,並檢查 session cache 連線池設定。

模型沒有發出工具請求,tools_condition 路由至 END,整個 Agent Loop 結束。

基礎 Loop 的侷限與下一步

這篇我們建立了最純粹的自主決策迴圈:由模型根據 Observation 自主決定是否繼續查詢。然而,預設的 tools_condition 將終止決策完全交由模型掌控,在正式環境中存在明顯風險:

  1. 死循環風險:當日誌中查無明確根因時,模型可能反覆變換參數查詢。tools_condition 每輪都判定有 Tool Call,導致流程持續循環,直到觸發 LangGraph 的 recursion limit 拋錯崩潰。
  2. 過早結束:當模型發生幻覺或偷懶時,可能僅查了一次日誌就直接輸出報告。tools_condition 判定無 Tool Call 即導向 END,導致系統接受了未經健康度驗證的草率結論。
  3. 工具例外崩潰:工具若拋出未處理的例外,整個 Graph 會中斷退出;若只回傳非結構化錯誤字串,模型也無從理解如何修正參數重試。

要解決這三類問題,不能單靠調整 Prompt,必須在 State 中記錄工具呼叫計數與任務狀態,將工具失敗轉成可自我修正的結構化 Observation,並改用自訂 Router 與結果驗證節點進行剛性控制。

下一篇,我們會接著這個事故調查流程,在 State 記錄工具呼叫次數,執行前檢查是否超過,並在模型停手後確認日誌與健康度結果是否都已取得。


上一篇
用 LangGraph 與 Guardrails 建立安全邊界
系列文
AI Agent 系統開發 30 天12
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言