Ian Chou's Blog

CtxFST CH18 - Multi-Step Planning:從 Greedy Selector 升級成 Lookahead Planner

CtxFST CH18:Multi-Step Planning,從 Greedy Selector 升級成 Lookahead Planner

如果回頭看前面幾章,CtxFST 的 runtime 能力其實是一步一步長出來的:

走到這裡,你其實已經有一個很像 planner 的東西了。

但還差最後一個很關鍵的能力:

它雖然知道哪個 skill 現在比較好,卻還不會先看 2 到 3 步之後會發生什麼。

這就是 greedy selector 的極限。

也就是說,到 CH17 為止,系統回答的還主要是:

現在這一步,哪個 skill 最合理?

CH18 要回答的問題則是:

如果我往前看幾步,哪一條 skill chain 最可能真的走到 goal?

這就是 multi-step planning 的核心。


先講一句最短的結論

這次升級最值得記住的一句話是:

CtxFST 的 planner 不再只會挑「下一步最像對的 skill」,而是開始會找「最短能走到 goal 的 skill sequence」。

這是一個質變,不只是排序規則又多一條而已。

因為從這一步開始,系統的思考方式從:

變成:

也就是從 router 更進一步長成真正的 lookahead planner。


為什麼 single-step greedy 仍然不夠?

先回顧一下 CH15CH17 做了什麼。

在那兩章裡,skill_selector.py 已經開始看:

這讓它已經不再是盲選了。

但問題是,它仍然有一個結構性限制:

它每次只看一個 skill 執行完之後,會不會更接近 goal。

這種做法在很多情況下很好用,但遇到下面這種問題就會卡住:

情況 A:短期沒有明顯收益,但能解鎖後面整條路

某個 skill 的 postcondition 可能離 goal 看起來不近,但它會解鎖下一個 skill,而下一個 skill 再下一步就能到 goal。

如果你只看單步 proximity,很可能會錯過這種技能。

情況 B:眼前很近,但其實是死巷

另一個 skill 也許在 graph 上看起來離 goal 很近,但它不會解鎖真正的後續技能鏈。

結果 selector 可能一直選它,卻始終無法真正抵達 goal。

情況 C:多條候選 path 同時存在

這時候真正重要的不是:

這一步哪個 skill 看起來最好

而是:

哪一條 skill chain 最短、最穩、最有機會到 goal

這就已經不是單步排序能完全回答的問題了。


所以這次真正做的是什麼?

核心其實很清楚:

  1. skill_selector.py 新增 find_plan(state, skills, max_depth)
  2. agent_loop.py 新增 lookahead 參數
  3. run_loop() 在每一次 iteration 都可以:
    • 先找一條最短 skill plan
    • 執行第一步
    • 再根據新 state 重新規劃

也就是說,這不是一次把整條 plan 算完然後硬跑到底,而是:

每一步都短距離 lookahead,但每一步都重新規劃。

這個設計我覺得很對,因為它同時保留了:


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 來說,真正的世界狀態不只是:

還包括:

所以 find_plan() 其實是在 search 一個更完整的 runtime state space,而不只是 graph node space。


為什麼 state key 要包含 completed non-idempotent skills?

這是一個很關鍵但很容易被忽略的細節。

如果 planner 只把 active_states 當作搜尋狀態,那會有一個問題:

某些 non-idempotent skills 即使 state 沒變,也不應該再被視為可用。

例如:

如果 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,我覺得是很好的第一步。

原因很簡單:

所以 BFS 在這個階段其實非常合理:

你現在要的不是一個「宇宙最強 planner」,而是一個:

能穩定從 greedy 升級成 short-horizon search 的第一版 lookahead planner。

而 BFS 很適合當這個版本。


最重要的設計決策:不是先算完再跑,而是每一步都 replan

這一點我想單獨拉出來講,因為它是這次設計裡最漂亮的地方。

很多人一看到 multi-step planning,直覺會想:

那就先算一整條 plan,然後照表執行到底。

但這在 agent runtime 裡其實不夠穩。

因為真實執行時,可能發生這些事:

如果你一開始算完就死跑到底,那整條 plan 很快就可能過時。

所以這次 run_loop(lookahead=N) 的做法是:

  1. 每一步先呼叫 find_plan()
  2. 只執行 plan 的第一步
  3. 寫回新 state
  4. 下一輪重新找 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 每做完一步,都重新根據新的世界狀態縮短路徑。

這很關鍵,因為它證明:

這正是 world model runtime 應該有的樣子。


lookahead=0 保留舊行為,這點非常重要

另一個很好的設計,是這次沒有把舊行為硬砍掉。

目前的約定是:

這種切法很好,因為它讓系統有很清楚的升級邏輯:

基線模式

greedy single-step

升級模式

lookahead search + replanning

這樣你就可以很乾淨地比較:

對測試和 debug 都很有幫助。


這次新增的 10 個測試,保護了什麼?

這次很棒的一點是,multi-step planning 也不是裸上。

你有同步幫 tests/test_agent_loop.py 補上 10 個新測試,把整體從:

提升到:

全部綠燈。

新增測試大致保護了這些行為:

find_plan()

run_loop(lookahead=...)

這些測試很重要,因為 multi-step planning 一旦進入 runtime,最怕的就是:

現在這些都有 baseline 保護了。


這次升級後,planner 的性質怎麼變了?

這是這章最核心的問題。

我會這樣總結:

CH17 以前

skill_selector.py 比較像:

到了 CH18

整個 runtime 開始更像:

這代表它的思考方式從:

哪一步現在看起來最好?

變成:

如果往前看幾步,哪條 sequence 最可能真的通往 goal?而且每走一步後要重新判斷。

這就是一個非常典型的 planner 升級。


這還不是最終形態,但它已經越過一條重要分界線

現在這版 multi-step planning 還不是最終型。

它目前還是:

它還沒有做到:

但我覺得這些都不是現在最重要的。

現在最重要的是,你已經跨過一條很關鍵的線:

planner 不再只會在當前時刻排序候選 skill,而是開始在 skill chain 空間裡搜尋。

這一點非常重要,因為它從根本上改變了系統的性質。


這對 CtxFST 的 world model 意味著什麼?

如果把整個系列拉高來看,這次升級其實意義很大。

因為 world model 真正的價值,不只是:

而是:

系統是否真的能利用這些東西,推演出一條可行的未來行動鏈。

到了 CH18,答案開始變成:

可以,而且是以 deterministic、可測、可 replan 的方式。

這點我覺得很強。

因為很多 agent 系統號稱會 planning,但其實只是:

而這裡做的,是一個更乾淨的版本:

這就不只是「像 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 優先。

未來可以把:

都帶進 multi-step search 裡。

這樣 planner 就會從「找最短 plan」進一步變成「找最划算的 plan」。

3. Tool-Backed Real Execution

現在 lookahead planner 已經很完整了,但 executor 端還可以再升級成真正跑工具、讀輸出、回寫結果的版本。

這樣整個 planning stack 就更完整了。


結語:從這一章開始,CtxFST 不再只是會挑下一步,而是開始會推演下一段路

如果 CH17 的重點是:

selector 開始懂得沿對的 relation 類型去走

CH18 的重點就是:

planner 現在開始不只看下一步,而是會先往前看幾步,再決定現在要走哪一步。

這是一個很關鍵的躍遷。

因為從這裡開始,CtxFST 的 runtime 不再只是:

它現在還開始具備另一個更接近真正 agent planning 的能力:

在可行 skill chain 的空間裡,搜尋一條到 goal 的短路徑,並且每一步都重新規劃。

這已經不是單純的 selector 了。

這是一個真正開始長出規劃能力的 planner。


參考實作


📌 CH17 讓 planner 分得出該走哪種邊,而 CH18 則讓 planner 開始真的往前看,從挑單步技能升級成搜尋 skill chain。