Ctrl K
ComunidadGuías…

docs/xkb-memory-service.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# 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 <token>" \
  -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 <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。