前兩天我們完成了 ReAct Agent,昨天再用 LangGraph 把原本的迴圈拆成比較清楚的節點與狀態。
但不管有沒有框架,ReAct 的核心都一樣:
做一步
↓
看結果
↓
再決定下一步
這很適合「一開始不知道答案在哪裡」的任務。
例如:
主力模型設定在哪裡?
可能讀一個設定檔就找到答案了。
但如果問題變成:
模型端點在哪裡設定?Client 又是怎麼使用這個設定的?
我們大概已經知道至少要看:
設定檔
Client 實作
這種任務如果每讀完一個檔案,都重新問模型「下一步要幹嘛」,不一定最划算。
另一種做法是:
先把需要做的事情列出來,再逐步執行。
這就是今天要看的 Plan-and-Execute。
今天先不讓模型產生 Plan。
先把「一份可執行的計畫到底應該長什麼樣」定義清楚,明天再把 Planner 接上去。
假設模型給你這份計畫:
1. 研究專案
2. 分析設定
3. 整理答案
人看得懂。
但 Executor 拿到之後還是不知道:
要讀哪個檔案?
每一步到底要做什麼?
總共有幾步?
所以今天不接受自由文字 Plan,而是直接用 Pydantic 定義結構。
class PlanStep(BaseModel):
model_config = ConfigDict(extra="forbid")
purpose: str = Field(
min_length=1,
max_length=300,
)
path: str = Field(
min_length=1,
max_length=200,
)
class Plan(BaseModel):
model_config = ConfigDict(extra="forbid")
steps: list[PlanStep] = Field(
min_length=1,
max_length=4,
)
這次刻意把每一步限制成:
讀一個檔案
例如:
{
"steps": [
{
"purpose": "確認模型端點從哪裡設定",
"path": "src/ironman/config.py"
},
{
"purpose": "確認 Client 如何使用這份設定",
"path": "src/ironman/llm.py"
}
]
}
這不是說 Plan-and-Execute 只能讀檔。
只是目前的 Agent 還是唯讀工具,先把操作面縮小,會比較容易看清楚 Planner 和 Executor 的責任。
Plan 如果要真的交給程式執行,就不能只靠 Prompt 說:
請產生合理的計畫。
我們可以直接限制:
至少要有 1 步
最多 4 步
purpose 不能是空字串
path 不能是空字串
不接受未知欄位
這些交給 Pydantic。
但 Schema 驗證通過,還不代表這份 Plan 可以執行。
例如:
{
"purpose": "讀取環境設定",
"path": ".env.local"
}
格式完全合法。
但這個檔案不在我們允許模型讀取的範圍內。
所以還要再做應用層檢查:
路徑是否允許
是否有路徑穿越
是否重複讀取同一個檔案
步驟是否符合目前 Executor 的能力
也就是:
Schema 正確
≠
Plan 可以執行
跟前面 Tool 的安全邊界其實是同一個概念。
最直觀的差別在「什麼時候決定下一步」。
ReAct 是:
執行一步
↓
取得 Observation
↓
再決定下一步
Plan-and-Execute 則是:
先產生 Plan
↓
確認步驟
↓
逐步 Execute
↓
必要時重新規劃
可以先這樣判斷:
| 情境 | 較適合的方式 | 原因 |
|---|---|---|
| 一開始不知道資料在哪 | ReAct | 下一步高度依賴剛取得的 Observation |
| 已經知道大概要查哪些資料 | Plan-and-Execute | 可以先列出工作順序,方便追蹤進度 |
| 任務有多個固定階段 | Plan-and-Execute | 比較容易檢查遺漏與完成狀態 |
| 操作具有寫入或執行風險 | Plan + 額外核准 | 可以先看計畫,但 Plan 本身不代表授權 |
兩種方式也不是互斥。
例如:
Planner
↓
產生 3 個大步驟
↓
Executor
↓
每一步內部再用 ReAct
完全可以。
今天只是故意把 Executor 做得很單純,方便先看懂兩個角色怎麼分工。
今天先不用模型產生 Plan。
直接手動建立一份合法的兩步計畫:
1. 讀 config.py,確認設定來源
2. 讀 llm.py,確認 Client 如何使用
接著再故意塞入:
.env.local
看驗證層會不會擋下來。
執行:
uv run python days/day15_plan_concept/main.py
uv run pytest -q tests/test_day14_16_frameworks.py
🧪 隔離環境實測
如果要求:
讀取 .env.local
正確結果不是幫 Planner 猜一條別的路。
而是直接拒絕。
測試也會確認:
空 Plan → 拒絕
重複 path → 拒絕
路徑穿越 → 拒絕
不允許的檔案 → 拒絕
今天這份 Plan 是 我們手動建立的。
模型還沒有參與規劃。
這件事要講清楚,不然看到漂亮的 Plan 輸出,很容易誤以為 Planner 已經做成功了。
今天驗證的是:
我們已經有一份模型未來可以遵守,而且程式能驗證的 Plan 格式。
假設 Plan 已經驗證完成:
Step 1 → src/ironman/config.py
Step 2 → src/ironman/llm.py
真正執行時,每一步還是要重新經過 Tool 原本的限制。
不能因為:
Plan 已經審過
就讓 Executor 跳過:
路徑驗證
Tool allowlist
參數驗證
權限檢查
因為 Plan 和實際執行之間可能隔了一段時間。
這段時間裡:
檔案可能被移動
權限可能改變
程式可能更新
環境可能不同
所以 Plan 比較像:
預計要做什麼
而不是:
永久通行證
真正動手時,還是要重新驗證。
假設原本有三步:
Step 1 → 成功
Step 2 → 失敗
Step 3 → 尚未執行
這時如果重新規劃,新的 Plan 要不要再跑一次 Step 1?
今天只是讀檔,重複執行頂多浪費一點時間。
但如果以後換成:
寄信
建立 Issue
修改檔案
付款
就不能隨便重做。
所以 Plan-and-Execute 除了 Plan 之外,還需要保存:
哪些 Step 已完成
哪些 Step 失敗
每一步的 Observation
目前剩哪些工作
Re-plan 時才能知道:
哪些事情已經做過,不要再來一次。
這也是明天實作時會補上的狀態。
前幾天的 ReAct 是:
先走一步
↓
看結果
↓
再想下一步
今天換一個角度:
先把路線列出來
↓
驗證這條路能不能走
↓
再開始執行
但目前 Planner 還是我們自己。
今天只完成:
Structured Plan
+
Pydantic Validation
+
Plan Policy
明天才會把規劃交給本機模型:
使用者任務
↓
Planner
↓
Structured Plan
↓
Validator
↓
Executor
↓
Observation
並處理執行失敗後的第一次 Re-plan。
Day 16:
用 Python 實作 Plan-and-Execute:讓 Agent 完成多步任務。