CtxFST CH13 - 第一個可跑的 Agent Loop:用 world_state.py 與 skill_selector.py 驅動 World Model
CtxFST CH13:第一個可跑的 Agent Loop,用 world_state.py 與 skill_selector.py 驅動 World Model
如果說:
CH11講的是v2.0的方向CH12講的是World Model First的定位重構
那這一章要做的事情就很直接:
證明這套東西不是只停在 spec 和 README,而是真的已經能跑。
而且不是「概念上能跑」,而是:
- world state 真的能初始化
- state 真的能被新增與移除
- skill 真的能依 preconditions 被篩選
- skill 執行完之後,postconditions 真的會改變下一輪決策
這代表什麼?
代表 CtxFST v2.0 已經不只是把資料切乾淨、圖建乾淨而已,而是已經有了第一個最小可用的 agent loop。
這章要回答的問題
很多人在看到 World Model First 之後,直覺都會問:
好,那到底哪裡開始算「真的能動」?
答案就是這兩個 script:
它們的重要性很高,因為這兩個檔案剛好對應了 world model loop 裡最核心的兩件事:
- 世界現在處於什麼狀態?
- 在這個狀態下,下一個最適合執行哪個 skill?
一旦這兩件事能穩定跑起來,後面接 LLM、接工具、接 graph update 都只是往上疊而已。
先講結論:這兩個 script 已經是最小可跑版本
我先把結論講在前面:
world_state.py和skill_selector.py已經構成了一個真正可執行的最小 agent loop。
不是半成品,也不是純草稿。
它們目前已經能做到:
world_state.py
init:建立新的 session world stateadd-state:新增 active stateremove-state:移除 active statecomplete-skill:記錄 skill 執行完成check-preconditions:檢查某個SKILL.md是否符合當前狀態show:輸出目前世界狀態摘要update-subgraph:從 graph 中抽出 goal-relevant subgraph
skill_selector.py
- 掃描
SKILL.md - 解析 YAML header
- 根據
preconditions過濾候選 skills - 根據
cost與postconditions數量排序 - 在
--auto模式下直接選出最佳 skill
也就是說,最基本的 selector loop 已經完整了。
這就是 CH12 的真正落地
回頭看 CH12,我那時候講的是:
CtxFST不再只是資料格式,而是 agent 可操作的 semantic world model substrate。
這句話如果要落地,至少要有三樣東西:
- 一個 runtime state 表示層
- 一個 deterministic 的 skill selector
- 一個能讓 state 在 action 後改變的 loop
而現在這三件事其實都已經出現了:
world-state.jsonworld_state.pyskill_selector.py
這也是為什麼我覺得 CH13 很重要。
因為從這一章開始,你不再是在想像一個 world model,而是在操作一個 world model。
這兩個 script 在整個系統裡各自扮演什麼角色?
先用一張最簡單的圖來看:
flowchart LR
A[world-state.json
current goal + active states] --> B[skill_selector.py]
C[SKILL.md files
preconditions + postconditions + cost] --> B
B --> D[selected skill]
D --> E[skill execution]
E --> F[world_state.py complete-skill / add-state]
F --> A
這張圖其實已經很接近最小 agent runtime。
用白話講就是:
- 先看世界現在是什麼狀態
- 再看有哪些 skills 符合現在的狀態
- 挑一個最適合的
- 執行它
- 把結果寫回世界
- 再進入下一輪
這就是 loop。
Step 1:建立 world state
先從最基本的開始。
world_state.py 的第一步,就是初始化一個 session:
python3 scripts/world_state.py init \
--goal "entity:learn-kubernetes-path" \
--output /tmp/test-state.json
這一步會產生一份乾淨的 world state,大致長這樣:
{
"session_id": "33018093-f029-47b2-8c01-f55928952cc1",
"goal": "entity:learn-kubernetes-path",
"active_states": [],
"completed_skills": [],
"current_subgraph": {
"nodes": [],
"edges": []
},
"created_at": "2026-03-13T15:21:48.851380+00:00",
"updated_at": "2026-03-13T15:21:48.851407+00:00"
}
這個格式很重要,因為它把 agent loop 最需要的幾個 runtime 資訊正式獨立出來了:
- session id
- 當前 goal
- active states
- completed skills
- 目前關注的 subgraph
也就是說,從這一步開始,世界已經不是只有靜態 graph,而是有了「這次任務的當前存檔」。
Step 2:啟動第一個 state
只有 goal 還不夠,agent 還需要知道目前有哪些條件成立。
例如先加入:
python3 scripts/world_state.py add-state \
/tmp/test-state.json \
"entity:has-raw-resume"
然後用:
python3 scripts/world_state.py show /tmp/test-state.json
你會看到像這樣的摘要:
Session: 33018093-f029-47b2-8c01-f55928952cc1
Goal: entity:learn-kubernetes-path
Active states (1):
✓ entity:has-raw-resume
Completed skills (0):
(none)
Subgraph: 0 nodes, 0 edges
這裡雖然還很簡單,但概念上已經非常關鍵了。
因為 agent 現在的判斷依據,不再只是「query 是什麼」,而是:
當前世界已經有哪些 state 被激活。
這正是 world model 和普通 prompt routing 的差別。
Step 3:準備兩個最小的 SKILL.md
接著準備兩個最小 skill:
skill A:analyze-resume
---
name: analyze-resume
description: "Parse raw resume and extract skill evidence"
preconditions:
- "entity:has-raw-resume"
- "NOT entity:has-parsed-resume"
postconditions:
- "entity:has-parsed-resume"
- "entity:has-skill-evidence"
cost: low
idempotent: true
---
skill B:docker-overview
---
name: docker-overview
description: "Docker fundamentals overview"
preconditions: []
postconditions:
- "entity:has-docker-knowledge"
cost: medium
idempotent: false
---
這兩個例子很小,但已經能展現完整邏輯:
analyze-resume需要特定狀態才可執行docker-overview沒有前置條件,所以永遠可候選- 兩者的
cost和postconditions又會影響排序
這表示 SKILL.md 已經不是純說明文件,而是真的在提供 selector 可計算的 planning signal。
Step 4:跑 skill_selector.py
現在把 world state 和 skill directory 一起丟進 selector:
python3 scripts/skill_selector.py \
/tmp/test-state.json \
--skill-dir /tmp/test-skills/
這時 selector 會掃描所有 SKILL.md,檢查誰符合 preconditions,並依規則排序。
在這個例子裡,輸出結果的重點是:
- 一共掃到 2 個 skills
- 目前 2 個都 eligible
- 最佳候選是
analyze-resume
為什麼?
因為它:
cost = lowpostconditions = 2- 而且當前狀態剛好滿足
entity:has-raw-resume - 同時也滿足
NOT entity:has-parsed-resume
這裡最值得注意的,不是排序本身,而是:
這個決策完全不需要 LLM。
也就是說,CtxFST 現在已經有一個 deterministic、可測、可解釋的 planning baseline。
這非常重要,因為你不會想在最底層就把所有邏輯都做成黑盒。
Step 5:完成 skill,讓世界狀態改變
接下來才是這章最關鍵的一步。
如果 analyze-resume 執行成功,那世界應該要改變。
例如:
python3 scripts/world_state.py complete-skill \
/tmp/test-state.json \
--skill analyze-resume \
--result success \
--summary "Parsed 3 skills"
python3 scripts/world_state.py add-state \
/tmp/test-state.json \
"entity:has-parsed-resume"
python3 scripts/world_state.py add-state \
/tmp/test-state.json \
"entity:has-skill-evidence"
做完之後再 show 一次,就會看到世界狀態已經改變:
Active states (3):
✓ entity:has-raw-resume
✓ entity:has-parsed-resume
✓ entity:has-skill-evidence
Completed skills (1):
✅ analyze-resume [success] Parsed 3 skills
這個結果看起來很普通,但其實它代表了一件大事:
執行 skill 之後,世界不是只有多一段 log,而是真的多了新的可計算 state。
這就是 world model loop 成立的關鍵。
Step 6:重新選 skill,觀察 eligibility 變化
現在再次執行:
python3 scripts/skill_selector.py \
/tmp/test-state.json \
--skill-dir /tmp/test-skills/ \
--auto
這時輸出的重點會變成:
- 掃描到 2 個 skills
- 現在只剩 1 個 eligible
- 最佳候選變成
docker-overview
為什麼 analyze-resume 被排除了?
因為它有一條條件:
NOT entity:has-parsed-resume
但前一步我們已經把 entity:has-parsed-resume 加進 active states 裡了。
所以這條 precondition 現在失敗,analyze-resume 就不再 eligible。
這件事非常關鍵,因為它證明:
postconditions 不是裝飾欄位,而是真的會改變下一輪 action selection。
這一步一成立,整個系統就從「規格上支援 world model」變成「行為上已經是一個 world model」。
這個最小 loop 為什麼意義重大?
因為它證明了三件事。
1. CtxFST 已經有 runtime memory
以前的 pipeline 比較像:
document -> chunk -> entity -> graph
現在多了:
world state -> skill selection -> execution -> state update
這代表系統第一次真的擁有:
- session memory
- completed action memory
- active condition memory
這已經不是靜態資料工程了。
2. SKILL.md 已經是 machine-usable policy contract
以前講 SKILL.md,很多人會覺得它比較像人類可讀指南。
但在這個 loop 裡,它已經可以被機器直接拿來做:
- eligibility filtering
- deterministic ranking
- postcondition-driven transition
所以它已經從文件變成一種 action schema。
3. agent loop 已經不必靠 LLM 才成立
這點我特別想強調。
現在很多 agent 系統最大問題是:
- 一切都靠 prompt
- 一切都靠 LLM 猜
- 一切都缺乏 deterministic baseline
而這個最小 loop 告訴你:
即使完全不接 LLM,
CtxFST也已經能先跑出一個可測試、可除錯、可解釋的 planning loop。
這對真正要做工程的人,價值非常大。
這跟 GraphRAG 的關係是什麼?
這章也順便把一件事情釐清了:
GraphRAG 沒有消失,只是它現在變成 world model 的觀察層。
也就是說:
entity-graph.json還是很重要SIMILAR邊還是很重要- graph expansion 還是很重要
只是到了 v2.0 之後,graph 不再只負責「找回更多相關 chunk」,還可以進一步支援:
update-subgraph- goal-relevant node extraction
- skill routing hints
- state-conditioned planning
所以不是從 GraphRAG 跳到另一個完全不同的世界,而是:
把原本的 semantic graph,往可執行的 world state 系統再推進一步。
現在這個 loop 還缺什麼?
這章也要誠實一點。
雖然最小 loop 已經成立,但它還不是完整 agent。
目前還缺幾層更高階的能力:
1. skill 真正的執行器
現在的範例裡,我們是手動模擬 skill 成功後的狀態更新。
未來更完整的版本,應該要讓:
- 執行
SKILL.md - 解析結果
- 自動套用 postconditions
變成一條連續流程。
2. graph mutation 自動化
目前 world state 已經能更新,但 graph edge 更新還可以更進一步自動化,例如:
agent:ian -> COMPLETED -> action:analyze-resumeagent:ian -> EVIDENCE -> entity:docker
這樣 runtime state 和 graph 會更緊密連動。
3. goal-aware ranking
現在 selector 的排序規則主要是:
- cost
- postcondition count
- name
這已經很好,但未來還可以加入:
- 距離 goal 的 graph proximity
- skill 對 current_subgraph 的覆蓋率
- 歷史成功率
4. LLM 作為上層 planner
這是最後才要加的,不是最先加的。
因為比較好的切法是:
- 底層 selector 保持 deterministic
- 上層 planner 再利用 LLM 做更高階策略
這樣系統才穩。
我會怎麼定義這一章的真正成果?
不是「多了兩個 script」。
而是:
CtxFST已經第一次具備了從 state 出發、選 skill、執行、再回寫 state 的最小閉環能力。
這句話很重要,因為它說明了:
- world model 不再只是 spec 名詞
- runtime layer 不再只是設計草圖
SKILL.md不再只是靜態文件
這些東西現在已經接成一個真正會動的 loop。
結語:從這一章開始,CtxFST 才真正像一個 agent substrate
如果回頭看整個系列的節奏:
- 前面幾章比較像在建立
chunk -> entity -> graph CH11開始把問題提升到 world modelCH12則把 repo 定位徹底改成 World Model First- 到了
CH13,我們終於看到第一個真正能跑的 agent loop
所以這章最重要的意義不是「實作了一個 CLI」。
真正的意義是:
從這一章開始,
CtxFST不再只是能描述語意世界,而是已經能讓 agent 在這個世界裡做出下一步選擇。
而這,正是 semantic world model 和普通 GraphRAG 格式之間最本質的差別。
參考實作
📌
CH11講 world model 的方向,CH12講 World Model First 的重構,而CH13終於讓這個世界真的動了起來。