# XKB Knowledge Service(Stage 3B) XKB 的跨 Agent 共享入口是 local-first HTTP service。它不是只有對話記憶服務,而是完整 XKB knowledge backend 的 API facade:OpenClaw、Claude Code、Codex 等 adapter 不直接讀 workspace 檔案,而是透過同一套 API 讀取對話、外部來源、evidence、knowledge cards、wiki 與 pipeline 狀態。 ## 啟動 ```bash python3 scripts/xkb_memory_service.py ``` 預設值: - bind:`127.0.0.1` - port:`18972` - database:`${HOME}/.xkb-runtime/memory.sqlite` 可用環境變數覆寫: - `XKB_SERVICE_HOST` - `XKB_SERVICE_PORT` - `XKB_SERVICE_DB` 服務拒絕非 loopback bind;只有在已經配置 VPN、TLS、authentication 與 network policy 後,才可明確設定 `XKB_ALLOW_NON_LOOPBACK=1`。 ## API ```text GET /v1/health POST /v1/sessions/open POST /v1/turns/start POST /v1/turns/{turn_id}/complete POST /v1/recall POST /v1/context GET /v1/sources/{source_id} GET /v1/evidence/{evidence_id} GET /v1/cards/{card_id} GET /v1/cards/{card_id}/relations GET /v1/wiki/topics/{topic_id} GET /v1/ingest/status GET /v1/knowledge/cold?min_considered=5 GET /v1/pipeline/snapshot?days=7 GET /v1/pipeline/jobs?stage=index&status=failed&limit=50 POST /v1/pipeline/jobs/events POST /v1/candidates/query GET /v1/artifacts/{trace_id} ``` 最小流程: ```text sessions/open ↓ turns/start(同時執行 semantic retrieval,回傳 retrieval packet) ↓ 直接使用 start 回傳的 context;不可對同一 turn 重複搜尋 ↓ Agent 執行 ↓ turns/{id}/complete ``` ## 每個 turn 的雙重處理 任何接入的 Agent turn 都必須同時完成: 1. **Capture / persistence**:保存 query、answer、tool data、session、episode、agent、namespace、時間與 provenance,先成為 L1 observed evidence。 2. **Semantic retrieval**:解析當前 query,優先透過既有 XBrain/Gemini hybrid vector search,必要時降級 keyword search,從整個 XKB knowledge plane 召回 context。 ```text user turn ├── capture → L1 trace / replayable evidence └── semantic query → XBrain hybrid vector search → context packet ``` `/v1/recall` 與 `/v1/context` 的回應會包含 `retrieval_mode`: - `xbrain_hybrid`:已使用既有 XBrain/Gemini hybrid search - `keyword_fallback`:semantic backend 不可用或無結果,已明確降級 不能在 semantic backend 不可用時假稱完成向量搜尋。 ### 相關度門檻與意圖閘門 召回不會把前十名照單全收。backend 回的分數是 **RRF 排名分數**—— 它說的是「這筆排第幾」,不是「這筆多相關」,所以第一名永遠在 0.88 附近, 即使知識庫裡根本沒有相關內容(實測「今天天氣如何」0.863、「碳盤查的計算方式」0.862, 分不出來)。因此 service 會重新計算**真實的餘弦相似度**, 低於 `XKB_SERVICE_MIN_RELEVANCE`(預設 0.55)的直接丟掉。 回應中的 `score` 是實測相似度,原本的排名分數保留在 `rank_score`。 被丟掉幾筆記在 `dropped_as_irrelevant`。 另外,純粹的招呼與確認(「好」「謝謝」「早安」)會**完全跳過召回** (`retrieval_mode: "skipped"`),連 embedding 都不呼叫。 實測效果:每則訊息平均注入從 2,067 字元降到 394 字元。 ### 知識退場(唯讀報告) `GET /v1/knowledge/cold?min_considered=5` 列出**被召回多次、卻從來沒有一次 相關到值得注入**的紀錄。 XKB 沒有「任務成功與否」的訊號,所以不照抄 Memmy 的獎勵模型—— 一張講運鏡的卡片沒有成功或失敗可言,硬給分數會產生假的依據。 可以誠實測量的是:這筆知識被撈出來之後,有沒有任何一次夠格被使用。 **只報告,不自動封存或刪除。** XKB 的價值是來源可追溯, 為了整潔而丟掉證據是本末倒置。 ### 為什麼召回不到:ACL 過濾統計 回應中的 `filtered_counts` 會說明**哪一層**被 ACL 擋掉,而不是只給一個總數: ```json { "filtered_counts": { "total": 3, "by_layer": { "card": 1, "wiki": 2, "semantic": 0, "conversation": 0 }, "semantic": 0, "keyword": 3 } } ``` - `by_layer.card` / `by_layer.wiki`:keyword 路徑可以判斷來源層 - `by_layer.semantic`:semantic backend 不回報層級,只能歸到通道 - `by_layer.conversation`:對話召回在 SQL 階段就依 namespace 過濾,結構上恆為 0 - `semantic` / `keyword`:舊有的通道統計,保留給既有呼叫端 有東西被擋掉時,`warnings` 會出現 `records_filtered_by_acl`。 這一項的用意是排除歧義:「沒有資料」與「有資料但被 ACL 擋掉」 在沒有分層統計時看起來完全一樣,而這正是 XKB 過去讓故障靜默數週的模式。 ## Adapter 設定 OpenClaw plugin 與 Claude Code hook 都支援: ```text XKB_MEMORY_SERVICE_URL=http://127.0.0.1:18972 ``` 若 service 不可用,adapter 會 fail-open 並回退到本機 L1 artifact / configured recall command;不應因此阻斷 Agent 對話。 ## 從另一台機器連進來 service 只綁 loopback,**不要為了讓其他機器連線而改成對外綁定**。 正確做法是用 SSH 通道:對 service 而言連進來的仍是 `127.0.0.1`, 但流量走的是 SSH 已經加密、已經驗證身分的通道。 ```bash # 在本機執行(保持這個行程開著) ssh -N -L 18972:127.0.0.1:18972 <你的伺服器> ``` 之後本機的 `http://127.0.0.1:18972` 就是伺服器上的 service: ```bash curl -s http://127.0.0.1:18972/v1/health ``` 為什麼是 SSH 通道而不是直接把 port 開到公網: 即使已經有 token 驗證(見下節),把 port 曝露在公網仍會多出一整層風險面 (掃描、暴力嘗試、TLS 憑證管理)。SSH 通道讓流量走已加密、已驗證的通道, service 本身維持只綁 loopback,兩層防護互相獨立。 ## 身分驗證 預設不需要 token:沒有設定任何 token 時,service 維持匿名(單人、只綁 loopback 的既有行為)。 **一旦設定了 token,匿名就會被拒絕**,而且 namespace 改由 token 決定, request body 不能再自行宣告。 設定檔預設在 `~/.xkb-runtime/auth.json`(可用 `XKB_SERVICE_AUTH` 覆寫): ```json { "tokens": { "至少十六字元的隨機字串": { "namespace": "private", "scopes": ["memory:read", "memory:write"], "label": "windows-laptop" } } } ``` 呼叫時放在 header(不接受放在網址查詢字串,因為網址會進日誌): ```bash curl -s http://127.0.0.1:18972/v1/recall \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" --data-binary @query.json ``` 規則: - `namespace` 由 token 決定;body 宣告不同的 namespace 會被拒絕(403),而不是默默改用別的 - `scopes` 分讀寫:`memory:read` 可召回與查詢,`memory:write` 才能開 session/寫入 turn/觸發 job - `/v1/health` 永遠免驗證,並回報 `auth_required`,方便探活 - auth 設定檔壞掉時 service **拒絕啟動**,不會退回成「誰都能讀」 - 非 loopback 綁定且未設定 token 時,service 拒絕啟動 ## 讓 Agent 自動接上 `xkb_agent_hook.py` 掛在 Agent 的事件上,不必 Agent 自己記得呼叫: ```text UserPromptSubmit → turns/start → 把召回的知識注入上下文 Stop → turns/complete → 對話成為 L1 證據 ``` `turns/start` 除了 `query`,hook 還會帶兩個選填欄位: - `conversation`:最近的對話(最多保留四則),讓「那個」「再一段」這類接續的話有前文可解。 - `context_since`:Claude Code 上次壓縮對話的時間(從對話紀錄讀出)。在這之後已經主動給過的建議、以及這段對話自己的紀錄,回答者都還看得到,所以不會再送;更早的可以再送、也可以被召回。沒帶時視為整段對話都還看得到。 注入的只有回傳結果裡的 `delivery`:最多三條判定相關的知識,不呼叫生成模型,由回答的 agent 自己判斷是否適用。`claude -p` 這類排程(Claude Code 標記為無人值守)不會觸發召回,也不寫入對話紀錄。 安裝(目前支援 Claude Code): ```bash python3 scripts/xkb_install_agent_hook.py --status python3 scripts/xkb_install_agent_hook.py --install python3 scripts/xkb_install_agent_hook.py --install --token python3 scripts/xkb_install_agent_hook.py --uninstall ``` 安裝是冪等的:重跑會取代舊的設定而不是疊加,也不會動到你其他的 hook; 設定檔以原子方式寫入,中斷不會留下壞掉的 settings.json。 **讀取一律 fail-open**:service 連不上時 hook 直接放行,絕不擋住對話。 這與寫入端相反——吸收閘門故障時必須擋住,因為寫進錯的知識比沒寫更糟。 送出含中文的 request 時,注意 shell 的編碼;較穩的做法是把 JSON 寫成 UTF-8 檔案再送: ```bash curl -s -X POST http://127.0.0.1:18972/v1/recall \ -H "Content-Type: application/json" --data-binary @query.json ``` ## Stage 3B 邊界 ### Control plane 第一個切片:pipeline snapshot `GET /v1/pipeline/snapshot?days=7` 是只讀總覽,將既有 pipeline 的階段、負責腳本、目前可觀測的 status 檔與摘要統一放在一個回應中。它只描述已存在的 filesystem/status evidence,不代表 worker 正在執行,也不會啟動、重試或修改任何 worker。 ### Worker job 履歷 `GET /v1/pipeline/jobs` 提供可篩選的 job 履歷;目前支援 `stage`、`status` 與 `limit`。`POST /v1/pipeline/jobs/events` 是給未來 worker adapter 回報 `queued`、`running`、`succeeded`、`failed`、`cancelled` 事件的受控入口。它只保存觀測事件,不會依 worker 名稱執行 shell command,也不會自動重試或 promotion。 每筆 job 會保存: ```text job_id / stage / worker / status started_at / finished_at input_ref / output_ref error / retryable metadata ``` 目前這個 job model 是履歷層,不是 scheduler;既有 worker 尚未自動接入。 目前列出的階段是: ```text ingest → card_generation → index → distill → promotion → publish ``` 這是 control plane 的觀測層,不是執行層;下一步才是為 worker job 建立可持久化的事件與狀態模型。 目前 service 的 data plane 是: - session identity、turn query / answer、tool/content payload、L1 trace - 既有 XKB search index 的 sources / cards metadata - 既有 cards 的完整 markdown 內容 - wiki topics 的完整內容 - read-only evidence/card/source facade - 對話 + 外部知識的 unified recall/context 目前 control plane 先採 observed-status-only: - `GET /v1/ingest/status` 只讀取既有 pipeline status,不啟動 ingest - `/v1/candidates/query` 為唯讀查詢:只回報候選目前的狀態與分析結果,不做 promotion - 不直接改寫 `MEMORY.md`、wiki topics、cards、indexes 或 production knowledge - 不自動 promotion、不自動建立 knowledge card、不綁定新的 embedding provider - 不對外開放服務 下一步才是把既有 ingest、index、distillation、relation、candidate、promotion、 absorb、publish worker 逐步納入 service control plane;目前先保留既有 worker 與檔案作為 source of truth。