CtxFST CH18 - Multi-Step Planning:從 Greedy Selector 升級成 Lookahead Planner
CtxFST CH18:Multi-Step Planning,從 Greedy Selector 升級成 Lookahead Planner
如果回頭看前面幾章,CtxFST 的 runtime 能力其實是一步一步長出來的:
CH13:讓最小 agent loop 跑起來CH14:讓 execution 結果寫回 world state 與 graphCH15:讓 selector 開始知道 goal 在哪裡CH17:讓 selector 開始分得出因果邊和相似邊
走到這裡,你其實已經有一個很像 planner 的東西了。
但還差最後一個很關鍵的能力:
它雖然知道哪個 skill 現在比較好,卻還不會先看 2 到 3 步之後會發生什麼。
這就是 greedy selector 的極限。
也就是說,到 CH17 為止,系統回答的還主要是:
現在這一步,哪個 skill 最合理?
而 CH18 要回答的問題則是:
如果我往前看幾步,哪一條 skill chain 最可能真的走到 goal?
這就是 multi-step planning 的核心。
先講一句最短的結論
這次升級最值得記住的一句話是:
CtxFST的 planner 不再只會挑「下一步最像對的 skill」,而是開始會找「最短能走到 goal 的 skill sequence」。
這是一個質變,不只是排序規則又多一條而已。
因為從這一步開始,系統的思考方式從:
- single-step ranking
變成:
- short-horizon search
也就是從 router 更進一步長成真正的 lookahead planner。
為什麼 single-step greedy 仍然不夠?
先回顧一下 CH15 和 CH17 做了什麼。
在那兩章裡,skill_selector.py 已經開始看:
preconditionscostgoal_proximity- relation-aware path cost
這讓它已經不再是盲選了。
但問題是,它仍然有一個結構性限制:
它每次只看一個 skill 執行完之後,會不會更接近 goal。
這種做法在很多情況下很好用,但遇到下面這種問題就會卡住:
情況 A:短期沒有明顯收益,但能解鎖後面整條路
某個 skill 的 postcondition 可能離 goal 看起來不近,但它會解鎖下一個 skill,而下一個 skill 再下一步就能到 goal。
如果你只看單步 proximity,很可能會錯過這種技能。
情況 B:眼前很近,但其實是死巷
另一個 skill 也許在 graph 上看起來離 goal 很近,但它不會解鎖真正的後續技能鏈。
結果 selector 可能一直選它,卻始終無法真正抵達 goal。
情況 C:多條候選 path 同時存在
這時候真正重要的不是:
這一步哪個 skill 看起來最好
而是:
哪一條 skill chain 最短、最穩、最有機會到 goal
這就已經不是單步排序能完全回答的問題了。
所以這次真正做的是什麼?
核心其實很清楚:
- 在
skill_selector.py新增find_plan(state, skills, max_depth) - 在
agent_loop.py新增lookahead參數 - 讓
run_loop()在每一次 iteration 都可以:- 先找一條最短 skill plan
- 執行第一步
- 再根據新 state 重新規劃
也就是說,這不是一次把整條 plan 算完然後硬跑到底,而是:
每一步都短距離 lookahead,但每一步都重新規劃。
這個設計我覺得很對,因為它同時保留了:
- search 的前瞻性
- agent runtime 的彈性
find_plan() 是怎麼想的?
如果用白話講,find_plan() 做的事就是:
從現在這個 state 出發,模擬 skill 一步一步被套用,看看在
max_depth以內能不能找到一條通往 goal 的最短 skill sequence。
它不是直接在 entity graph 上跑最短路,而是在一個更像「skill application graph」的空間上做 BFS。
這裡的節點不是單一 entity,而是整個規劃狀態:
(active_states, completed_non_idempotent_skills)
這個設計很重要,因為對 planner 來說,真正的世界狀態不只是:
- 現在哪些 state 是 active
還包括:
- 哪些 non-idempotent skills 已經做過,不能再重跑
所以 find_plan() 其實是在 search 一個更完整的 runtime state space,而不只是 graph node space。
為什麼 state key 要包含 completed non-idempotent skills?
這是一個很關鍵但很容易被忽略的細節。
如果 planner 只把 active_states 當作搜尋狀態,那會有一個問題:
某些 non-idempotent skills 即使 state 沒變,也不應該再被視為可用。
例如:
generate-plan可能只能做一次one-shot-onboarding做完之後不該再重複執行
如果 search state 裡沒有把「已完成的 non-idempotent skills」記進去,那 planner 很容易在 search tree 裡反覆踩同樣的 skill,產生假性路徑。
所以把 state key 設成:
frozenset(active_states), frozenset(completed_non_idempotent)
本質上是在讓 planner 尊重 skill execution 的真實語意。
這點做得很好。
為什麼用 BFS,而不是更複雜的演算法?
這次 find_plan() 用 BFS,我覺得是很好的第一步。
原因很簡單:
- 目標是先找到「最短 skill sequence」
- 每一步 skill application 都可以視為等成本 transition
- 目前先不做複雜 utility estimation
所以 BFS 在這個階段其實非常合理:
- 簡單
- deterministic
- 容易 debug
- 很容易跟現有 runtime 接合
你現在要的不是一個「宇宙最強 planner」,而是一個:
能穩定從 greedy 升級成 short-horizon search 的第一版 lookahead planner。
而 BFS 很適合當這個版本。
最重要的設計決策:不是先算完再跑,而是每一步都 replan
這一點我想單獨拉出來講,因為它是這次設計裡最漂亮的地方。
很多人一看到 multi-step planning,直覺會想:
那就先算一整條 plan,然後照表執行到底。
但這在 agent runtime 裡其實不夠穩。
因為真實執行時,可能發生這些事:
- skill 回傳了意料外的 state
- skill 只 partial success
- 某個 skill failed
- world state 因外部事件改變了
如果你一開始算完就死跑到底,那整條 plan 很快就可能過時。
所以這次 run_loop(lookahead=N) 的做法是:
- 每一步先呼叫
find_plan() - 只執行 plan 的第一步
- 寫回新 state
- 下一輪重新找 plan
也就是:
plan 是短期導航,不是長期僵化腳本。
這樣的設計比較像真正的 agent。
一個很清楚的例子:plan 會越跑越短
這次 CLI smoke test 的輸出其實很有說服力:
[Step 1] Plan (3 steps): analyze-resume → match-skills → generate-plan
[Step 2] Plan (2 steps): match-skills → generate-plan
[Step 3] Plan (1 step): generate-plan
這三行代表的不是只是 log 比較好看,而是:
planner 每做完一步,都重新根據新的世界狀態縮短路徑。
這很關鍵,因為它證明:
- planner 不是先算死
- planner 真的是在用新的 state 重新推理
這正是 world model runtime 應該有的樣子。
lookahead=0 保留舊行為,這點非常重要
另一個很好的設計,是這次沒有把舊行為硬砍掉。
目前的約定是:
lookahead=0:保留原本 greedy 行為lookahead=N:啟用 multi-step planning
這種切法很好,因為它讓系統有很清楚的升級邏輯:
基線模式
greedy single-step
升級模式
lookahead search + replanning
這樣你就可以很乾淨地比較:
- greedy 和 lookahead 的差別
- 某些 case 下誰表現更好
- 某些 regression 是 search 引入的,還是原本就有的
對測試和 debug 都很有幫助。
這次新增的 10 個測試,保護了什麼?
這次很棒的一點是,multi-step planning 也不是裸上。
你有同步幫 tests/test_agent_loop.py 補上 10 個新測試,把整體從:
- 28 tests
提升到:
- 38 tests
全部綠燈。
新增測試大致保護了這些行為:
find_plan()
- 能找出 3-step chain
- goal 已存在時回傳空 plan
- 沒路時回傳
None - depth 不夠時回傳
None - 會選較短路徑
- 會跳過已完成的 non-idempotent skills
run_loop(lookahead=...)
- 開啟 lookahead 可以成功到達 goal
- 找不到 plan 時會 fallback 到 greedy
lookahead=0和舊 greedy 行為一致
這些測試很重要,因為 multi-step planning 一旦進入 runtime,最怕的就是:
- search 卡住
- cycle 沒被剪掉
- non-idempotent skill 被錯誤重跑
- planner 找不到路時整個 loop 崩掉
現在這些都有 baseline 保護了。
這次升級後,planner 的性質怎麼變了?
這是這章最核心的問題。
我會這樣總結:
在 CH17 以前
skill_selector.py 比較像:
- relation-aware greedy router
到了 CH18
整個 runtime 開始更像:
- replanning short-horizon planner
這代表它的思考方式從:
哪一步現在看起來最好?
變成:
如果往前看幾步,哪條 sequence 最可能真的通往 goal?而且每走一步後要重新判斷。
這就是一個非常典型的 planner 升級。
這還不是最終形態,但它已經越過一條重要分界線
現在這版 multi-step planning 還不是最終型。
它目前還是:
- BFS
- fixed depth
- shortest sequence 優先
- 沒有更細的 utility scoring
它還沒有做到:
- weighted multi-step utility
- expected outcome ranking
- uncertainty-aware planning
- branch evaluation with probabilistic futures
但我覺得這些都不是現在最重要的。
現在最重要的是,你已經跨過一條很關鍵的線:
planner 不再只會在當前時刻排序候選 skill,而是開始在 skill chain 空間裡搜尋。
這一點非常重要,因為它從根本上改變了系統的性質。
這對 CtxFST 的 world model 意味著什麼?
如果把整個系列拉高來看,這次升級其實意義很大。
因為 world model 真正的價值,不只是:
- 有 states
- 有 actions
- 有 graph
- 有 writeback
而是:
系統是否真的能利用這些東西,推演出一條可行的未來行動鏈。
到了 CH18,答案開始變成:
可以,而且是以 deterministic、可測、可 replan 的方式。
這點我覺得很強。
因為很多 agent 系統號稱會 planning,但其實只是:
- 用 prompt 假裝規劃
- 或用 LLM 一次吐一長串步驟
而這裡做的,是一個更乾淨的版本:
- search state space
- 遵守 idempotency
- 遵守完成紀錄
- 每一步重新規劃
- 有 tests 保護
這就不只是「像 planner」,而是真的開始有 planner 的骨架了。
下一步最自然會去哪?
有了 CH18 之後,後面最自然的方向其實變得很清楚。
1. Relation-Specific Explanations
既然 planner 現在已經會找 skill chain,那下一步很適合讓它能說清楚:
Why this plan?
- analyze-resume unlocks has-skill-inventory
- match-skills produces has-skill-gap-analysis
- generate-plan reaches goal directly
這會讓 planner 更容易 debug,也更適合拿來展示。
2. Weighted Multi-Step Planning
現在 find_plan() 還是 shortest sequence 優先。
未來可以把:
cost- relation weights
- expected gain
- risk of failure
都帶進 multi-step search 裡。
這樣 planner 就會從「找最短 plan」進一步變成「找最划算的 plan」。
3. Tool-Backed Real Execution
現在 lookahead planner 已經很完整了,但 executor 端還可以再升級成真正跑工具、讀輸出、回寫結果的版本。
這樣整個 planning stack 就更完整了。
結語:從這一章開始,CtxFST 不再只是會挑下一步,而是開始會推演下一段路
如果 CH17 的重點是:
selector 開始懂得沿對的 relation 類型去走
那 CH18 的重點就是:
planner 現在開始不只看下一步,而是會先往前看幾步,再決定現在要走哪一步。
這是一個很關鍵的躍遷。
因為從這裡開始,CtxFST 的 runtime 不再只是:
- 會選 skill
- 會寫回 state
- 會沿 graph 靠近 goal
它現在還開始具備另一個更接近真正 agent planning 的能力:
在可行 skill chain 的空間裡,搜尋一條到 goal 的短路徑,並且每一步都重新規劃。
這已經不是單純的 selector 了。
這是一個真正開始長出規劃能力的 planner。
參考實作
📌
CH17讓 planner 分得出該走哪種邊,而CH18則讓 planner 開始真的往前看,從挑單步技能升級成搜尋 skill chain。
- ← Previous
CtxFST CH17 - Relation-Aware Routing:讓 Selector 分得出因果邊和相似邊 - Next →
CtxFST CH19 - Relation-Specific Explanations:讓 Planner 不只會找路,還會說明為什麼