前兩天看 anthropics/oncall-kit,比較像是在 Claude Code 這個既有 Runtime 上,加入 Skill、SOP 與 Human Gate。
今天換一個完全不同的例子:HolmesGPT。
HolmesGPT 是一個開源的 SRE Agent,目前也是 CNCF Sandbox Project。它主要拿來調查 production incident,例如 Kubernetes Pod 異常、Prometheus 指標、Log、Database、Cloud resource 等問題。
例如可以直接問:
payment-api 為什麼一直 CrashLoopBackOff?
HolmesGPT 不只是把問題丟給 LLM。
它會讓模型自己決定要不要查:
Kubernetes
Prometheus
Grafana
Datadog
Database
Bash
Internet
MCP
...
查到結果後,再把結果送回模型,繼續下一步 investigation。
官方 README 把這件事直接稱為:
agentic loop
這也是今天我想看的主題:
HolmesGPT 不只定義 Agent 要做什麼,它連 Model → Tool → Result → Model 這條執行迴圈都自己掌握。這會帶來什麼差別?
如果先不碰細節,HolmesGPT 可以簡化成:
User
│
▼
Prompt / Skills
│
▼
ToolCallingLLM
│
▼
LLM
│
├── 不需要 Tool ──────────→ Answer
│
└── 需要 Tool
│
▼
Tool Executor
│
▼
Tool
│
▼
Tool Result
│
└──────────────→ LLM
這裡最重要的元件是:
ToolCallingLLM
它在:
holmes/core/tool_calling_llm.py
HolmesGPT 並不是用 LangGraph 畫一張固定 Workflow:
查 Pod
→ 查 Log
→ 查 Prometheus
→ 回答
而是每一輪讓模型看目前資訊,再決定下一步。
例如:
User:
payment-api 為什麼 CrashLoopBackOff?
第一輪模型可能決定:
先查 Pod status
Tool Result 回來後:
OOMKilled
restartCount = 12
模型看到結果,再決定:
查 Pod memory limit
接著可能再查:
Prometheus memory usage
最後資料夠了,才停止 Tool Calling 並回答。
所以它真正的流程是:
LLM
↓
決定下一個 Tool
↓
Runtime 執行
↓
Observation
↓
LLM
↓
再決定下一步
如果熟悉 ReAct,HolmesGPT 的核心流程其實很接近:
Reason
→ Action
→ Observation
→ 再決定下一步
HolmesGPT 同樣是讓模型根據目前資訊決定下一個 Tool,拿到 Tool Result 後,再繼續下一輪判斷。
差別在於,經典 ReAct 比較著重「模型如何交錯 Reasoning、Action、Observation」;HolmesGPT 則把中間的 Action 做成更完整的工程流程:
LLM
↓
Structured Tool Call
↓
Tool.invoke()
↓
Approval / Validation
↓
_invoke()
↓
StructuredToolResult
↓
LLM
也就是說,HolmesGPT 並不是另一種完全不同於 ReAct 的 Agent 思路。
更準確地說,它是:
ReAct-like 的決策循環,加上一套 structured tool calling 與可控制的 Tool Execution Pipeline。
後面真正值得看的,也正是 ReAct 圖裡常被簡化成一個箭頭的:
Action → Observation
HolmesGPT 到底在這個箭頭中間,放了哪些控制機制。
HolmesGPT 的確讓 LLM 決定:
下一步要呼叫哪個 Tool?
參數要填什麼?
拿到結果後還要不要繼續?
但模型並沒有直接執行 Tool。
真正的關係比較像:
LLM
= 提出 Action
Runtime
= 決定怎麼執行這個 Action
例如模型回傳:
bash(
command="docker ps"
)
它只是產生一個 Tool Call。
真正執行:
docker ps
的是 HolmesGPT 的程式。
這個差異看起來很小,卻是整個 Repo 最值得看的地方。
因為既然 HolmesGPT 自己握有執行權,它就可以在:
Model Decision
和:
Real Side Effect
中間插入控制。
在 Model Call 前,HolmesGPT 先準備本輪可以使用的 Tools。
它把不同 Data Source 包成 Toolset,例如:
Kubernetes Toolset
Prometheus Toolset
Grafana Toolset
Bash Toolset
Database Toolset
MCP Toolset
最後由 Runtime 決定哪些 Tool 真正提供給模型。
也就是:
Runtime
↓
準備 Available Tools
↓
送給 LLM
↓
LLM 從這批能力裡選
第一層控制因此已經出現:
Code 先決定 Model 擁有哪些 executable capabilities。
Skill 則是另外一回事。
HolmesGPT 也有 Skill,它比較像 SOP:
遇到 Pod CrashLoopBackOff:
1. 先看 Pod status
2. 看 current / previous logs
3. 看 events
4. 再比對 rollout
Skill 告訴模型:
怎麼調查
Tool 則回答:
你真的能做什麼
這兩件事要分開。
接下來才進到今天真正想看的 Code。
所有 Tool 都繼承 HolmesGPT 的 Tool。
核心位置:
holmes/core/tools.py
其中真正重要的是:
Tool.invoke()
簡化實際程式後,大概長這樣:
def invoke(params, context):
if not context.user_approved:
approval = self._get_approval_requirement(
params,
context
)
if approval and approval.needs_approval:
return StructuredToolResult(
status=APPROVAL_REQUIRED
)
params = self._coerce_params(params)
result = self._invoke(
params=params,
context=context
)
return result
這十幾行,其實已經說明 HolmesGPT 的控制邊界。
模型產生:
Tool Call
之後,不是直接進:
_invoke()
而是:
Tool Call
↓
Tool.invoke()
↓
Approval Check
↓
Parameter Processing
↓
_invoke()
真正的 Tool implementation 在:
_invoke()
因此:
只要程式停在
Tool.invoke(),真正的 Side Effect 就還沒有發生。
HolmesGPT 的 Bash Tool 是一個很好的實際例子。
相關程式在:
holmes/plugins/toolsets/bash/bash_toolset.py
Bash Tool 在真正執行 command 以前,會先跑:
requires_approval()
裡面會把 command 拿去做 validation。
結果大致分三種:
ALLOWED
DENIED
APPROVAL_REQUIRED
概念上:
LLM 想執行 Bash command
│
▼
validate_command()
│
┌────┼────────────┐
│ │ │
ALLOWED DENIED APPROVAL_REQUIRED
│ │ │
▼ ▼ ▼
執行 拒絕 等 User Approval
實際 requires_approval() 裡可以看到:
if validation_result.status == ValidationStatus.APPROVAL_REQUIRED:
return ApprovalRequirement(
needs_approval=True,
reason="Command requires approval...",
)
接著回到共用的:
Tool.invoke()
發現:
approval.needs_approval == True
就直接回:
APPROVAL_REQUIRED
此時 _invoke() 還沒有執行。
這裡有一個很有意思的細節。
真正執行 Bash command 的 _invoke() 裡,還是會重新 Validate。
如果理論上需要 Approval 的 command 居然跑進 _invoke(),程式不是默默執行,而是:
if validation_result.status == ValidationStatus.APPROVAL_REQUIRED:
return StructuredToolResult(
status=ERROR,
error="Command requires approval but was not approved. This may be a bug."
)
Code 裡甚至直接留了一句 Comment:
This indicates requires_approval() was bypassed.
意思很清楚:
第一道:
Tool.invoke() 應該先擋
第二道:
就算前面的 approval 流程被繞過,
_invoke() 仍不要直接執行
這就不是:
Prompt 裡拜託 Model 不要做
而是:
Code 明確定義:
這個狀態不能執行
假設 SOP 寫:
執行高風險操作以前,
一定要先取得使用者確認。
正常情況:
Skill
↓
LLM 理解
↓
先問 User
↓
User Confirm
↓
Tool
但 LLM 可能犯錯。
它可能直接產生 Tool Call。
如果架構是:
LLM
↓
Tool Call
↓
直接執行
那 SOP 就只是 Instruction。
HolmesGPT 則多了一層:
Skill
↓
LLM
↓
Tool Call
↓
Tool.invoke()
↓
Approval Check
↓
真正執行
所以 Skill 和 Runtime Control 是兩件不同的事。
可以很簡單地分:
Skill
= 告訴 Model 正確行為
Runtime
= 限制實際可發生的行為
看 Agent Repo 時,很容易先問:
用了 LangGraph 嗎?
用了 ReAct 嗎?
用了 Skill 嗎?
支援 MCP 嗎?
但 HolmesGPT 讓我開始多問一個問題:
從模型產生 Tool Call,到外部世界真的被修改,中間經過誰?
例如:
LLM
↓
Tool Schema
↓
Tool.invoke()
↓
Approval
↓
Validation
↓
Credential
↓
External System
如果把所有控制都寫進 Prompt:
請不要做危險的事。
請務必先確認。
請遵守 SOP。
等於把安全性建立在:
Model 永遠判斷正確
這個假設上。
HolmesGPT 的做法則接受另一個現實:
Model 可以犯錯。
但錯誤的 Tool Call
不應該自動等於
錯誤的 Side Effect。
第一次看 HolmesGPT,可以先不用記它所有 Toolset、Memory、Skill 或 Evaluation。
先記住這張圖就好:
User
↓
LLM
↓
Tool Call
↓
Tool.invoke()
↓
Approval / Validation
↓
_invoke()
↓
External System
↓
Tool Result
↓
LLM
HolmesGPT 的 Model 仍然有很大的自主性。
它決定:
下一步查什麼
使用哪個 Tool
Tool Parameter 怎麼填
什麼時候停止 investigation
但真正執行 Tool 的權力仍留在 Runtime。
所以今天看完這個 Repo,我留下的不是:
HolmesGPT 有 Runtime Approval。
而是更一般化的一條 Agent 設計原則:
Model 可以決定 Action,但不要讓 Model 同時擁有 Action 的最終執行權。
前面用 oncall-kit 看到了 SOP 與 Human Gate;再用 Claude Code 看 Host Runtime 如何把 Gate 做成強制阻擋。
HolmesGPT 則讓這件事更清楚:
如果 Runtime 就掌握在自己手上,Gate 最自然的位置,就是 Model Decision 與 Real Execution 之間。