CtxFST CH17 - Relation-Aware Routing:讓 Selector 分得出因果邊和相似邊
CtxFST CH17:Relation-Aware Routing,讓 Selector 分得出因果邊和相似邊
如果說:
CH15解的是「selector 終於知道 goal 在哪裡」CH16解的是「這整個 runtime 現在有測試保護了」
那下一步最自然的升級就是:
selector 不只要知道 goal 在哪裡,還要知道哪些邊比較像真正的規劃路徑,哪些邊只是語意上的鄰居。
這個差別非常重要。
因為在 world model 裡,不是所有 edge 都應該被當成一樣。
REQUIRES比較像真的依賴關係LEADS_TO比較像真的後繼路徑SIMILAR只是語意相近COMPLETED則只是歷史紀錄
如果你把這些邊全部當作「距離 = 1」,agent 很容易就會被語意相近的路徑帶偏。
所以 CH17 要補的就是這件事:
讓
skill_selector.py開始具備 relation-aware routing。
先講結論:Uniform BFS 已經不夠了
在 CH15,我們做的事情是把 current_subgraph 與 goal_proximity 接進 selector。
那個版本已經很有價值,因為它讓 selector 不再只看:
costpostcondition_count
而開始看:
postconditions到 goal 的 hop distance
但那個版本還有一個很大的限制:
它把所有邊都當成同樣的 hop。
這在 graph 很單純時還可以,但一旦 relation type 開始變多,就會出問題。
因為下面兩條路徑,語意上完全不是同一回事:
goal <-REQUIRES- entity:docker
goal <-SIMILAR-- entity:containerization
兩者都可能只差 1 hop,但規劃意義完全不同。
REQUIRES表示你大概率真的得先經過它SIMILAR則只是它和 goal 在語意上有點近
如果 selector 把這兩條路都當一樣短,最後就會出現一種錯誤:
agent 會把語意漂移當成規劃路徑。
這正是 relation-aware routing 要避免的事。
問題的本質:不是「有沒有 path」,而是「這條 path 是什麼性質」
這一章最重要的觀念,我想用一句話講清楚:
在 world model 裡,最短路徑不只要看長度,還要看邊的語意。
也就是說,規劃時不能只問:
從某個 postcondition 到 goal 幾步?
還得問:
這幾步是沿著
REQUIRES、LEADS_TO走的,還是只是沿著SIMILAR亂飄過去的?
這就是為什麼單純 BFS 不夠,而需要 weighted shortest-path。
這次實作的核心:從 uniform BFS 換成 weighted Dijkstra
這次 skill_selector.py 的核心變化可以濃縮成一句:
把
_goal_hop_distances()從 uniform BFS 升級成依 relation type 加權的 Dijkstra。
這不是只是換個演算法名稱而已。
它真正代表的意思是:
- path 不再只看 hop 數
- 不同 relation 會有不同 traversal cost
- 某些 relation 甚至直接不該進規劃圖
也就是說,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 |
預設中等成本 |
如果把它翻成白話,就是:
- 真正的 dependency / transition edges 最便宜
- 軟因果 edges 可以參考,但次一級
- semantic similarity 只能當輔助,不能主導規劃
- execution trace 和阻擋邊不該被拿來當「靠近目標」的捷徑
這個權重設計很合理,因為它終於把 graph relation 的語意帶進路徑計算裡了。
為什麼 SIMILAR 應該比 REQUIRES 更貴?
這點值得單獨講。
在 GraphRAG 的世界裡,SIMILAR 很有價值,因為它能幫你:
- 找相近概念
- 擴展搜尋範圍
- 發現 unknown unknowns
但到了 planning 的世界裡,SIMILAR 的角色就不一樣了。
它可以告訴你:
這個東西看起來像下一步
但它不能保證:
這個東西真的是你要先完成的條件
例如:
Docker和Kubernetes可能有REQUIRESKubernetes和Nomad可能只是SIMILAR
如果目標是學會 Kubernetes,那 agent 應該先走 Docker -> Kubernetes 這種因果路,而不是被 Kubernetes -> Nomad 這種語意相似路徑吸走。
所以把 SIMILAR 設成較高 cost,本質上是在告訴 planner:
你可以參考語意鄰居,但不要把它們誤當成主幹路徑。
為什麼 COMPLETED 必須被排除?
這也是這次設計很關鍵的一點。
COMPLETED edge 的用途是:
- 記錄 agent 執行過什麼
- 留下 runtime trace
- 支援後續查詢與回放
它不是拿來表示:
- 哪個狀態是某個 goal 的必要前置
- 哪條路徑應該在未來被再次走
如果你讓 COMPLETED 也參與 proximity 計算,就會出現一個很奇怪的污染:
因為某個 skill 以前剛好完成過,所以它在圖上看起來突然變得離 goal 很近。
這是不對的。
因為 execution history 不應該反過來改寫規劃語意。
所以這次把 COMPLETED 明確 skip 掉,我覺得非常正確。
同理,BLOCKED_BY 也不應該被拿來當正向 proximity path。
這次升級後,selector 的行為具體有什麼變化?
最具體的效果可以用這個例子來看。
假設有兩個 candidate skills,兩個都是 cost = low:
Skill A
- postcondition 離 goal 是
1個REQUIREShop
Skill B
- postcondition 離 goal 是
1個SIMILARhop
在舊的 uniform BFS 裡,兩者都會被視為距離 1。
這表示 selector 可能看不出差別。
但在 relation-aware routing 之後:
REQUIREShop cost =1SIMILARhop cost =3
所以:
- Skill A 的
goal_proximity = 1 - Skill B 的
goal_proximity = 3
最後 Skill A 會明確排在前面。
這就是我們真正想要的 planner 行為:
優先沿因果鏈前進,而不是沿語意漂移前進。
這次不是在做更複雜的 graph,而是在做更正確的 graph usage
我覺得這點很重要。
很多人看到 weighted Dijkstra,會以為是在把系統搞複雜。
但其實這次真正做的不是「更複雜」,而是:
更尊重 graph 裡每種 relation 的本來語意。
如果你的 graph 明明已經分出了:
REQUIRESLEADS_TOSIMILARCOMPLETED
但 planner 還把它們全都當作同樣的 1-hop,那其實是在浪費 schema 的表達力。
這次升級,正是把 schema 的語意真的接進 runtime。
所以它不是 feature embellishment,而是語意對齊。
測試也一起升級了:從 20 個變成 28 個
這次很棒的一點是,relation-aware routing 不是裸改。
你有同步幫 tests/test_agent_loop.py 補上新的測試,整體從:
20tests
升級成:
28tests
全部綠燈通過。
新增的測試重點包括:
REQUIRES成本為1LEADS_TO成本為1SIMILAR成本為3COMPLETED會被跳過- Dijkstra 會選較便宜的混合路徑
COMPLETEDedge 不會污染 proximity
這很重要,因為 relation-aware routing 一旦進 selector 核心,它就會直接影響整個 agent_loop.py 的決策方向。
如果沒有測試保護,後面非常容易出現「看起來更聰明,其實只是更不穩」的情況。
現在有了這 8 個新測試,這一層就比較安全了。
這對 agent_loop.py 的意義是什麼?
雖然這次主要改的是 skill_selector.py,但真正受益的是整個 loop。
因為 agent_loop.py 本身並不負責思考哪個 skill 比較合理,它只是:
- 讀 candidates
- 選 best candidate
- 執行
- 寫回結果
所以 selector 一旦升級,整個 loop 的方向感就會一起升級。
也就是說:
CH13讓 loop 能跑CH14讓 loop 會寫回CH15讓 loop 看見 goalCH17則讓 loop 分得出「哪條邊才是規劃邏輯上的主幹」
這一步很關鍵,因為它讓 planning 開始真正依賴 graph semantics,而不是只依賴 graph topology。
這是不是已經等於完整 planner 了?
還沒有,但已經更接近了。
目前這版 relation-aware routing 仍然是:
- single-step
- greedy
- deterministic
它還沒有做到:
- multi-step lookahead
- branch simulation
- expected future gain estimation
- relation-specific path explanation
但它已經完成一個非常關鍵的升級:
selector 不再把 graph 當成無差別連線圖,而是開始把它當成有語意層級的規劃空間。
這其實是完整 planner 之前必經的一步。
下一步最自然會去哪?
有了 relation-aware routing 之後,後面最自然的兩個方向會更清楚。
1. Multi-Step Planning
現在 selector 還是只看單步 postconditions。
下一步可以讓它模擬:
- 如果先做 skill A
- 會解鎖哪些下一步 skill
- 哪一條 2 到 3 步 skill chain 最接近 goal
這會讓它從 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 開始分得出:哪些邊是在帶你前進,哪些邊只是看起來很像前進。
- ← Previous
CtxFST CH16 - 先別急著變聰明:為 Agent Loop 補上 End-to-End Test Suite - Next →
CtxFST CH18 - Multi-Step Planning:從 Greedy Selector 升級成 Lookahead Planner