Ian Chou's Blog

CtxFST CH15 - Goal-Aware Skill Routing:讓 skill_selector.py 真的朝目標前進

CtxFST CH15:Goal-Aware Skill Routing,讓 skill_selector.py 真的朝目標前進

如果說:

那下一個很自然的問題就是:

現在 agent 會挑 skill 了,但它挑的是「最便宜的 skill」,還是「最接近目標的 skill」?

這兩者差很多。

因為如果 selector 只看:

那它其實還不知道 goal 在哪裡。

也就是說,它雖然會做 deterministic sorting,但還不算真正的 goal-aware planning

這就是 CH15 要補的那一塊:

skill_selector.py 結合 current_subgraph 與 goal proximity,開始真的朝目標前進。


先講一句最短的定義

這次升級可以濃縮成一句話:

以前 selector 只知道哪個 skill 比較便宜;現在 selector 開始知道哪個 skill 離 goal 比較近。

這個變化看起來像只是排序 key 多一個欄位,但其實代表 planner 的性質變了。

以前它比較像:

rule-based skill picker

現在它開始更像:

graph-aware goal router


問題出在哪裡?舊排序其實不懂 goal

在加入 goal-aware routing 之前,skill_selector.py 的核心排序邏輯很單純:

candidates.sort(key=lambda c: (
    COST_ORDER.get(c["cost"], 1),
    -c["postcondition_count"],
    c["name"],
))

這個排序其實很合理。

它偏好的候選 skill 是:

  1. 成本低
  2. 能產生更多 postconditions
  3. 名字排前面

但問題就在這裡:

它完全不知道 goal 在哪裡。

所以只要兩個 skill 都是 low cost,系統就會傾向挑 postconditions 比較多的那個,即使那些 postconditions 根本不在通往 goal 的路徑上。

這就是舊 selector 最大的盲點。


一個很白話的例子

假設現在 goal 是:

entity:goal

current_subgraph 長這樣:

entity:goal <-1 hop-> entity:mid <-1 hop-> entity:far

現在有兩個 skill:

如果只看舊排序:

最後只能靠名字決定。

這 obviously 不合理。

因為 entity:mid 離 goal 只有 1 hop,entity:far 卻有 2 hops。

如果 agent 真的是在朝目標前進,那應該優先選 skill-close

這就是這次 goal-aware routing 想解決的問題。


這次到底加了什麼?

最核心的改動只有一個:

把 goal proximity 納入 select_candidates() 的排序依據。

也就是新的排序邏輯變成:

cost
-> goal_proximity
-> postcondition_count
-> name

這裡的 goal_proximity 指的是:

某個 candidate skill 的 postconditions,離 state["goal"] 最近的 hop 距離。

hop 越小,代表越接近 goal,排序就越前面。

這也表示:

這比以前更像真正的 planner。


current_subgraph 在這裡終於真的派上用場

在前幾章裡,我們其實已經有 current_subgraph 這個概念了。

但老實說,在更早的版本裡,它比較像:

這次改動之後,情況就不一樣了。

因為 skill_selector.py 會直接讀:

state["current_subgraph"]["edges"]

然後做兩件事:

  1. 建 adjacency map
  2. state["goal"] 做 BFS,算出每個 entity 到 goal 的 hop 距離

也就是說,current_subgraph 現在終於不再只是 runtime 附件,而是 selector 真正會用來決策的 graph context。

這一點非常重要。

因為從這裡開始,selector 不再只是讀 YAML,而是真的開始「看圖選路」。


BFS 怎麼算出 proximity?

這次實作的核心其實很樸素,但很有效。

做法大致是:

  1. goal 節點出發
  2. 沿著 current_subgraph.edges 做 BFS
  3. 算出每個 entity 的最短 hop distance
  4. 對每個 candidate skill,取其 postconditions 中最小的 hop distance
  5. 這個值就是該 skill 的 goal_proximity

所以如果:

那麼:

這種做法的好處是:

而且已經足夠讓 selector 開始出現「朝 goal 前進」的味道。


為什麼這件事比「postconditions 多寡」更重要?

因為 postconditions 多,不等於有用。

舉個簡單例子:

Skill A

Skill B

舊排序很可能會選 A,因為它 postconditions 比較多。

但真正有目標感的 planner,應該會選 B。

因為 world model 裡最重要的不是:

我一次產生多少 state

而是:

我有沒有把世界往 goal 推近

這就是 goal-aware routing 的本質。


這次實作有一個很好的細節:fallback 完全保留舊行為

我很喜歡這次設計的一點,是它沒有粗暴地破壞舊排序。

current_subgraph 是空的,或者某個 postcondition 根本沒有通往 goal 的路徑時,實作上會用一個 sentinel:

_UNKNOWN_PROXIMITY = sys.maxsize

這代表:

這樣帶來一個很好的效果:

當 graph context 不足時,selector 會自然退回舊的 cost -> postcondition_count -> name 行為。

也就是說,新功能不是硬覆蓋舊功能,而是:

這是很好的升級方式。


一個最小測試,剛好把這件事講清楚

這次驗證用的最小案例其實非常漂亮。

狀態

state = {
    "goal": "entity:goal",
    "active_states": ["entity:start"],
    "completed_skills": [],
    "current_subgraph": {
        "nodes": ["entity:goal", "entity:mid", "entity:far"],
        "edges": [
            {"source": "entity:goal", "target": "entity:mid", "relation": "REQUIRES"},
            {"source": "entity:mid",  "target": "entity:far", "relation": "REQUIRES"},
        ]
    }
}

候選 skills

skills = [
    {"name": "skill-far",   "preconditions": [], "postconditions": ["entity:far"], "cost": "low"},
    {"name": "skill-close", "preconditions": [], "postconditions": ["entity:mid"], "cost": "low"},
]

算出的 hop distance

entity:goal -> 0
entity:mid  -> 1
entity:far  -> 2

最後排序結果是:

skill-close  proximity=1
skill-far    proximity=2

這正是我們要的行為。

因為它證明:

selector 已經會因為「更靠近 goal」而改變排序。


這代表 planner 變成什麼了?

我覺得這一步之後,skill_selector.py 的性質有了很明顯的提升。

以前它比較像:

現在它開始更接近:

雖然它還不是完整的 search-based planner,也還沒做到多步 lookahead,但它已經具備了一個很重要的特質:

它的排序邏輯開始對 world model 的拓樸結構敏感。

這就是從「會挑 skill」到「會挑方向正確的 skill」的差別。


這對 agent_loop.py 有什麼影響?

很直接。

因為 agent_loop.py 本來就是靠 select_candidates()select_best() 來決定下一步。

現在每個 candidate 多了一個:

goal_proximity

所以 loop 可以自然得到幾個新好處:

  1. 選 skill 時更像在走向 goal,不只是貪便宜
  2. log 可以開始顯示 proximity,方便 debug
  3. 之後如果要做更強的 planner,也有一個很好的 baseline feature

換句話說,這次雖然只改了 selector,但受益的是整個 agent loop。


這是不是就等於「goal-aware skill routing」?

是,這兩個說法其實描述的是同一件事。

下面兩句話完全等價:

  1. skill_selector.py 結合 current_subgraph 與 goal proximity,做更強的 routing
  2. 做 goal-aware skill routing

因為它們都在講同一個核心能力:

selector 排序時,不只看技能本身,還看這個技能會不會把系統更接近 goal。

這就是 routing。


現在這個版本還有哪些限制?

雖然這次升級已經很關鍵,但它還不是終點。

至少還有幾個可以繼續往前走的方向。

1. 目前只看最短 hop,不看 edge type

現在 BFS 主要是在 graph 上算距離,但還沒分辨:

未來更強的版本可以做 relation-aware routing。

例如:

2. 目前只看單步 postconditions,不看多步收益

現在 selector 看的還是:

但未來可以更進一步評估:

3. 目前 proximity 只來自 current_subgraph

這是好事,因為它很乾淨。

但也代表 graph 如果抽取得不夠完整,proximity 就不夠準。

未來可以結合:

讓 proximity 更穩。

4. 目前仍是 deterministic baseline,不是 learned policy

這點我反而覺得是優點。

因為比較好的演化順序本來就應該是:

  1. 先有 deterministic planner
  2. 再在上層疊 LLM 或 learned policy

而不是一開始就全靠模型猜。


為什麼我覺得這一章很關鍵?

因為 CH13CH14 證明的是:

但直到 CH15,我們才真正開始回答這個問題:

如果同時有多個可執行 skill,系統會不會優先走向正確的目標?

這個問題一旦成立,planner 就真的開始有方向感了。

也就是說:

這三章合起來,才真正把 world model runtime 的核心骨架補完整。


結語:從這一章開始,selector 不再只是排序器,而是目標導向的路由器

如果只從程式碼 diff 看,這次改動也許只是:

但從系統設計角度看,真正發生的事情其實是:

skill_selector.py 從單純的 rule-based sorter,進化成會參考 graph 結構與目標距離的 goal-aware router。

這個差別很大。

因為這代表 CtxFST 的 world model runtime 現在不只會:

它還開始會:

優先挑選那些真正把世界推向 goal 的技能。

而這一步,正是從「有閉環」走向「有方向感」的關鍵升級。


參考實作


📌 CH13 讓 loop 跑起來,CH14 讓 loop 寫回世界,而 CH15 則讓 loop 開始真的朝 goal 前進。