Ian Chou's Blog

CtxFST CH17 - Relation-Aware Routing:讓 Selector 分得出因果邊和相似邊

CtxFST CH17:Relation-Aware Routing,讓 Selector 分得出因果邊和相似邊

如果說:

那下一步最自然的升級就是:

selector 不只要知道 goal 在哪裡,還要知道哪些邊比較像真正的規劃路徑,哪些邊只是語意上的鄰居。

這個差別非常重要。

因為在 world model 裡,不是所有 edge 都應該被當成一樣。

如果你把這些邊全部當作「距離 = 1」,agent 很容易就會被語意相近的路徑帶偏。

所以 CH17 要補的就是這件事:

skill_selector.py 開始具備 relation-aware routing。


先講結論:Uniform BFS 已經不夠了

CH15,我們做的事情是把 current_subgraphgoal_proximity 接進 selector。

那個版本已經很有價值,因為它讓 selector 不再只看:

而開始看:

但那個版本還有一個很大的限制:

它把所有邊都當成同樣的 hop。

這在 graph 很單純時還可以,但一旦 relation type 開始變多,就會出問題。

因為下面兩條路徑,語意上完全不是同一回事:

goal <-REQUIRES- entity:docker
goal <-SIMILAR-- entity:containerization

兩者都可能只差 1 hop,但規劃意義完全不同。

如果 selector 把這兩條路都當一樣短,最後就會出現一種錯誤:

agent 會把語意漂移當成規劃路徑。

這正是 relation-aware routing 要避免的事。


問題的本質:不是「有沒有 path」,而是「這條 path 是什麼性質」

這一章最重要的觀念,我想用一句話講清楚:

在 world model 裡,最短路徑不只要看長度,還要看邊的語意。

也就是說,規劃時不能只問:

從某個 postcondition 到 goal 幾步?

還得問:

這幾步是沿著 REQUIRESLEADS_TO 走的,還是只是沿著 SIMILAR 亂飄過去的?

這就是為什麼單純 BFS 不夠,而需要 weighted shortest-path。


這次實作的核心:從 uniform BFS 換成 weighted Dijkstra

這次 skill_selector.py 的核心變化可以濃縮成一句:

_goal_hop_distances() 從 uniform BFS 升級成依 relation type 加權的 Dijkstra。

這不是只是換個演算法名稱而已。

它真正代表的意思是:

也就是說,planner 現在不只是「會找近路」,而是「會偏好對的路」。


新的 relation weights 怎麼設?

這次的設計很清楚:

Relation Cost 意義
REQUIRES 1 直接依賴,最該走的規劃邊
LEADS_TO 1 直接後繼,最該走的規劃邊
EVIDENCE 2 軟因果,代表支持訊號
IMPLIES 2 軟因果,代表推論上的接近
SIMILAR 3 只是語意鄰近,不應該壓過因果鏈
COMPLETED skip 歷史紀錄,不是規劃邊
BLOCKED_BY skip 阻擋訊號,不是正向 proximity path
unknown 2 預設中等成本

如果把它翻成白話,就是:

這個權重設計很合理,因為它終於把 graph relation 的語意帶進路徑計算裡了。


為什麼 SIMILAR 應該比 REQUIRES 更貴?

這點值得單獨講。

在 GraphRAG 的世界裡,SIMILAR 很有價值,因為它能幫你:

但到了 planning 的世界裡,SIMILAR 的角色就不一樣了。

它可以告訴你:

這個東西看起來像下一步

但它不能保證:

這個東西真的是你要先完成的條件

例如:

如果目標是學會 Kubernetes,那 agent 應該先走 Docker -> Kubernetes 這種因果路,而不是被 Kubernetes -> Nomad 這種語意相似路徑吸走。

所以把 SIMILAR 設成較高 cost,本質上是在告訴 planner:

你可以參考語意鄰居,但不要把它們誤當成主幹路徑。


為什麼 COMPLETED 必須被排除?

這也是這次設計很關鍵的一點。

COMPLETED edge 的用途是:

它不是拿來表示:

如果你讓 COMPLETED 也參與 proximity 計算,就會出現一個很奇怪的污染:

因為某個 skill 以前剛好完成過,所以它在圖上看起來突然變得離 goal 很近。

這是不對的。

因為 execution history 不應該反過來改寫規劃語意。

所以這次把 COMPLETED 明確 skip 掉,我覺得非常正確。

同理,BLOCKED_BY 也不應該被拿來當正向 proximity path。


這次升級後,selector 的行為具體有什麼變化?

最具體的效果可以用這個例子來看。

假設有兩個 candidate skills,兩個都是 cost = low

Skill A

Skill B

在舊的 uniform BFS 裡,兩者都會被視為距離 1

這表示 selector 可能看不出差別。

但在 relation-aware routing 之後:

所以:

最後 Skill A 會明確排在前面。

這就是我們真正想要的 planner 行為:

優先沿因果鏈前進,而不是沿語意漂移前進。


這次不是在做更複雜的 graph,而是在做更正確的 graph usage

我覺得這點很重要。

很多人看到 weighted Dijkstra,會以為是在把系統搞複雜。

但其實這次真正做的不是「更複雜」,而是:

更尊重 graph 裡每種 relation 的本來語意。

如果你的 graph 明明已經分出了:

但 planner 還把它們全都當作同樣的 1-hop,那其實是在浪費 schema 的表達力。

這次升級,正是把 schema 的語意真的接進 runtime。

所以它不是 feature embellishment,而是語意對齊。


測試也一起升級了:從 20 個變成 28 個

這次很棒的一點是,relation-aware routing 不是裸改。

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

升級成:

全部綠燈通過。

新增的測試重點包括:

這很重要,因為 relation-aware routing 一旦進 selector 核心,它就會直接影響整個 agent_loop.py 的決策方向。

如果沒有測試保護,後面非常容易出現「看起來更聰明,其實只是更不穩」的情況。

現在有了這 8 個新測試,這一層就比較安全了。


這對 agent_loop.py 的意義是什麼?

雖然這次主要改的是 skill_selector.py,但真正受益的是整個 loop。

因為 agent_loop.py 本身並不負責思考哪個 skill 比較合理,它只是:

所以 selector 一旦升級,整個 loop 的方向感就會一起升級。

也就是說:

這一步很關鍵,因為它讓 planning 開始真正依賴 graph semantics,而不是只依賴 graph topology。


這是不是已經等於完整 planner 了?

還沒有,但已經更接近了。

目前這版 relation-aware routing 仍然是:

它還沒有做到:

但它已經完成一個非常關鍵的升級:

selector 不再把 graph 當成無差別連線圖,而是開始把它當成有語意層級的規劃空間。

這其實是完整 planner 之前必經的一步。


下一步最自然會去哪?

有了 relation-aware routing 之後,後面最自然的兩個方向會更清楚。

1. Multi-Step Planning

現在 selector 還是只看單步 postconditions

下一步可以讓它模擬:

這會讓它從 greedy router 升級成 lookahead planner。

2. Relation-Specific Explanations

既然現在路由已經分 relation type,那 future 很適合補一層可解釋輸出,例如:

Selected: match-skills
Reason:
- produces entity:has-skill-gap-analysis
- reaches goal via LEADS_TO -> REQUIRES path with cost 2
- alternative path through SIMILAR edges costs 6

這會讓整個 planner 更可 debug,也更適合對外展示。


結語:從這一章開始,selector 不只知道 goal 在哪裡,還知道該走哪種邊

如果 CH15 的核心是:

selector 開始知道 goal 在哪裡

CH17 的核心就是:

selector 現在開始知道,通往 goal 的不同邊,語意上並不等價。

這是非常大的進步。

因為 world model 真正困難的地方,從來不只是「有沒有圖」,也不只是「能不能走到某個節點」。

真正困難的是:

你能不能沿著對的關係類型去走。

而 relation-aware routing 正是在補這一塊。

所以這章真正完成的事情,不是把 BFS 換成 Dijkstra 這麼簡單。

它真正做的是:

CtxFST 的 selector 第一次開始尊重 graph relation 的語意階層。

這一步一成立,CtxFST 的 world model runtime 就更像一個真正的 planner,而不只是 graph 上的排序器。


參考實作


📌 CH15 讓 selector 看見 goal,CH16 讓 runtime 可驗證,而 CH17 則讓 planner 開始分得出:哪些邊是在帶你前進,哪些邊只是看起來很像前進。