連想記憶 — 設計¶
状態: 2.6 系。宣言 (§1–2)、§3 の再構成の段、traverse (§4) は実装済みです。
実装が決める必要のあった点は §9 に記録します。このページは宣言されたグラフ層 —
別名を持つ登録語と、エージェントが主張する関係 — と、それを最初に読む 1 箇所
(再構成想起) を固定します。
recall が返すものは変わらず、何も宣言されていなければ何も変わりません。
0. 何を足し、どこで働くか¶
recall には検索経路が 2 本あります。確率的なベクトルのアームと、表層の字面を照合する 字面のアームです。どちらも同じ種類の問いを取りこぼします — 答えが、問いの語や意味が 直接届くどの記録からも 2 つ先にある問い、あるいは記録とは別の名前で物を呼んでいる問い。 対話ベンチマークで測ると、multi-hop と open-domain の質問型が融合順位の損失が最も大きい 座標です。
連想記憶は構成上決定論的な第 3 の経路です。エージェントが entity・その別名・ 主語–述語–目的語の関係を宣言し、サーバは宣言を verbatim に保存して SQL と純関数で辿ります。 サーバ境界のどちら側でもモデルは呼びません: 抽出はエージェントの仕事、保存と走査はサーバの 仕事です。このストアで試した fuzzy な拡張はすべて contamination ベンチマークで退行したので、 他の 2 アームが近似であるところで連想だけは厳密です。
最初の段階は意図して狭くしています:
| この設計 | 別途決めること | |
|---|---|---|
| entity・別名・関係を保存し、辿る | する | |
| 再構成想起がそれを読む (cue・束ね・根拠・roles) | する | |
| 呼び出し側が entity の近傍を直接引ける | する (traverse) |
|
recall が自分の行をグラフで広げる |
しない | する — §7 |
| entity が自前の安定した埋め込みを持つ | しない | 2.6.1 — §7 |
| サーバが教えられていない関係を推論する | しない | 後の別層 — §7 |
最初の読み手に再構成想起を選んだのは、その契約に置き場所が既にあるからです: 宣言された関係で 結ばれた行の束ねキー、恒等だった bounded relation walk、そして 6 語のうち 4 語が宣言された 関係を待っている role 語彙。契約は 1 つも変わらず、恒等写像だった段が仕事を始めます。
1. スキーマ¶
新しいテーブル 4 つ。memories と episodes は変わりません。すべての行は memory と同じ
3 本の分離軸 (agent_id, project_id, channel) を持ち、読み取りは recall と同じ規則に
従います: エージェントは他のエージェントのグラフを見ず、project bucket は global pool と
一緒に読まれます。
CREATE TABLE entities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
agent_id TEXT NOT NULL,
project_id TEXT NOT NULL DEFAULT '',
channel TEXT NOT NULL DEFAULT '',
name TEXT NOT NULL, -- 正規名、宣言されたまま
normalized TEXT NOT NULL, -- 正規化後の名前 (§2)
declared_by TEXT NOT NULL, -- 'agent' | 'operator'
created_at TEXT NOT NULL,
UNIQUE (agent_id, project_id, channel, normalized)
);
CREATE TABLE entity_aliases (
entity_id INTEGER NOT NULL,
alias TEXT NOT NULL,
normalized TEXT NOT NULL,
PRIMARY KEY (entity_id, normalized)
);
-- 正規化した別名 1 つは、1 スコープ内で高々 1 つの entity に解決する。この表は
-- スコープ列を持たないので、その規則は制約でなく宣言ハンドラが強制する。
-- 索引は照合のためだけにある。
CREATE INDEX idx_entity_aliases_normalized ON entity_aliases (normalized);
CREATE TABLE entity_mentions (
entity_id INTEGER NOT NULL,
ref TEXT NOT NULL, -- 'mem:<id>' または 'ep:<id>'
declared_by TEXT NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY (entity_id, ref)
);
CREATE TABLE relations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
agent_id TEXT NOT NULL,
project_id TEXT NOT NULL DEFAULT '',
channel TEXT NOT NULL DEFAULT '',
subject_kind TEXT NOT NULL, -- 'entity' | 'mem' | 'ep'
subject_id INTEGER NOT NULL,
predicate TEXT NOT NULL, -- 正規化済み (§2)
object_kind TEXT NOT NULL,
object_id INTEGER NOT NULL,
anchor_ref TEXT NOT NULL DEFAULT '', -- 根拠となる記録、無ければ ''
declared_by TEXT NOT NULL,
declared_at TEXT NOT NULL,
UNIQUE (agent_id, project_id, channel,
subject_kind, subject_id, predicate, object_kind, object_id, anchor_ref)
);
2 種類の関係が 1 つのテーブルを共有し、端点の種類で区別されます:
- entity → entity (
Kirarimaintainsmizeye): walk が辿るもの。 - record → record (
mem:<newer>correctsmem:<older>): 2 つの候補を 1 つの item に束ね、 role 語彙が読むもの。
混合の端点 (entity と record の関係) は受理して保存しますが、この段階では読みません。
このデータベースは外部キーを使わないので、溢れ分の tree と同じく整合性はトリガーで 保ちます: entity の削除はその別名・言及・関係を削除し、memory / episode の削除はその言及と、 それを端点または anchor に持つすべての関係を削除します。根拠が消えた宣言は一緒に消えます — 関係が、もう存在しない行を指したまま残ることはありません。
2. 宣言¶
宣言の出どころは 2 つで、サーバはどちらかを記録します: 下のツールを通じたエージェントか、 dashboard を通じた運用者か。サーバが第 3 の出どころになることはありません — 保存された テキストから entity を抽出せず、既存の 2 つから関係を推論しません。したがってカバレッジは 宣言されたものそのものであり、それがこの層の取引です: 他の 2 アームが広く近似であるところで、 狭く確実。
正規化は宣言が受ける唯一の処理で、保存形に対しては無損失です: 宣言された name /
alias / predicate は書かれたまま保持し、normalized の双子 — Unicode NFKC、
case-fold、空白を 1 つに畳んで trim — を照合に使います。正規化して一致する 2 つの宣言は
1 つの entity です。トークナイザも固有表現認識も使いません: 日本語の固有名詞を決定論的に
切る方法は存在しないので、サーバはそれを試みません。
store への相乗り。 memory を保存する瞬間が、エージェントがそれの言及するものを
知っている瞬間です。store は任意の associations オブジェクトを受け取ります:
"associations": {
"entities": [{ "name": "MizEye", "aliases": ["mizeye", "ミズアイ"] }],
"relations": [{ "subject": "Kirari", "predicate": "maintains", "object": "MizEye" }]
}
ここで名指された entity は未登録なら登録され、保存された memory がそれぞれを言及するものと
して記録されます。関係は端点を entity 名か記録の ref (mem:<id>) で名指します; 未知の entity を
名指す端点も登録されます。保存された memory がその関係の anchor_ref になります。associations が
妥当かどうかにかかわらず memory は保存されます: 不正な宣言は応答で報告して落とすだけで、
memory を失う理由にはなりません。
単独。 declare_associations は同じオブジェクトと任意の anchor_ref を受け取り、
後からの宣言や dashboard からの宣言に使います。加えて retract — 取り消す関係 id と言及の
対のリスト。取り消しは宣言がストアを離れる唯一の経路で、自動失効はなく、誤った宣言は
取り消されるまで誤ったままです。
3. グラフが読まれる場所: 再構成想起¶
再構成想起には 4 段があります。グラフはそのうち 3 段に入り、4 段目の構造化は変わりません。
段 1 — 候補。 検索の前に、問いをスコープ内の別名と照合します (正規化した部分文字列一致、
長い別名が先)。問いが言及する entity ごとに、その正規名と他の別名を字面のアームにだけ
追加語として渡します。ベクトルのアームは問いをそのまま受け取ります: 別名を付け足すと問いの
埋め込みが動き、展開の目的は別の名前を使った記録を見つけることであって、問いの意味を変える
ことではないからです。候補の深さ (top_k) は変わりません; 一致する別名が無ければ、検索は
今日とまったく同じです。
段 2 — 束ね。 新しいキー cluster:relation が、宣言された record → record 関係で結ばれた
2 つの候補を結びます。キー順では cluster:adjacent の後です。言及する entity を共有することは
束ねません: そうするとプロジェクト名を言及するすべての memory が 1 つの item に畳まれ、
この層が再導入してはならない contamination そのものになります。
段 3 — bounded relation walk。 検索が直接浮かせた候補ごとに、
walk は次を辿ります: 候補が言及する entity; その上に宣言された entity → entity の関係を
max_hops まで; 到達した entity を言及する記録。walk が到達した記録は、そこへ導いた直接
候補の item の内側の根拠になります — 新しい item には決してなりません。各記録は
why: "relation:<predicate>" とホップ数を持ちます。walk は max_hops で止まり、落としたものは
根拠上限と同じく bounds.omitted で報告します。到達した記録が max_evidence の許す数を超える
時にどれを残すかは、書かれた 1 つの順序で決まります: ホップ数が少ない順、次に最近宣言された
関係、次に記録 id の小さい方。
Roles。 述語が role 語彙 (supports, supersedes, corrects, qualifies, contradicts,
temporal_predecessor) の語である record → record の宣言関係は、語彙が既に固定している向き
(参照される行が主語) でその role として出力されます。それ以外の述語の関係も束ね、why で
自分を説明します; ただ role ではないだけです。
walk は 1 つの関数 — 候補プールとスコープ内の関係が入り、出自付きの拡張プールが出る — で、 1 回の再構成につき正確に 1 回呼ばれます。この形が、後で §1 の想起プロセスが同じ関数を 反復ごとに 1 回呼ぶことを可能にします (§6)。
4. traverse¶
記録ではなく entity が欲しい問いもあります: MizEye に関係すると分かっているものは何で、
どの記録を通じてか。traverse(entity, max_hops, limit) は entity の近傍 — 別名、その上に
宣言された関係をホップ上限まで、それが到達する entity、各々を言及する記録の ref — を、
順序が決定論的で limit に有界なグラフとして返します。グラフそのものの照会ツールです。
記録のテキストは返しません; ref は他と同じく get_contents で展開します。
実装では: entity は名前か別名で、呼び出しが読める entity を複数名指す時 (プロジェクトのものと
グローバルプールのもの) は、そのすべてが起点です。entity は再構成の walk が切り詰めに使う順序 —
ホップ数、次にそこへ到達した最も新しく宣言された関係、次に id — で並び、limit は返す entity の数と
entity ごとに挙げる ref の数の両方を抑えます; 切ったものは数で示します。関係は両端が返される時に
挙げます。記録は、recall と同じプロジェクト・チャンネル・source の絞り込みの下で呼び出しが読める時
だけ挙げます。max_hops の既定は 1、上限は 5; limit の既定は 20、上限は 100 です。
5. 不変条件¶
recallは変わらない。 この設計のいかなるものもrecallの経路では読まれない。 テスト: recall のテストスイートが、新テーブルにデータの入ったデータベースに対して無改変で通る。- 空のグラフは no-op。 entity・言及・関係が無ければ、
reconstructはテーブルの無い同じ 呼び出しと byte-identical な出力を返す。テスト: fixture コーパスを schema v15 あり/なしで 再構成して等しいこと; グラフを無条件に参照する変異がこれを赤くしなければならない。 - 連想は item を作らず、並べ替えない。 グラフありの item の head と順序は、なしの時と 等しい; グラフは item の内側に根拠を足し、宣言された record 関係で束ね、roles を付けるだけ。 テスト: head の ref と順序を assert する; 到達した記録が item になる変異がこれを赤くしなければ ならない。
- モデルを呼ばない。 宣言は書かれたまま保存され、walk は SQL と純関数。
- 有界で決定論的。
max_hopsを超えて辿らず、max_evidenceを超えて残さない; 切り詰めは §3 の書かれた順序に従い、同じデータベース・問い・bounds は同じ出力を与える。 - すべての要素が出自を言う。 関係は誰が宣言したかと、あれば根拠の記録を記録する; walk が 足した記録はどの述語で何ホップかを言う。
- 分離は保たれる。 walk は
agent_idを越えない;project_idとchannelは呼び出しの 読み取り意味論に従い、呼び出しが直接読めないスコープへ関係を辿ることはない。 - 宣言は端点より長生きしない。 entity・memory・episode の削除は、それに依存するすべての 別名・言及・関係を取り除く。
6. 想起プロセスとの関係、recall との関係¶
§1 の想起プロセスは後の ラインです。それが入ると、宣言された関係は cue になります: 登録 entity へのヒットがその上に 宣言された関係を名指し、次の反復は索引から二度目の取得をせずにそこを見ます。それは §3 の walk を 呼び出しごとに 1 回でなく反復ごとに 1 回走らせることで、§3 が walk をプールとスコープ内の 関係の純関数に保っている理由です。この設計はループを作りません; ループが辿るエッジを作ります。
recall は平らなままです。以前の決定では walk は gate の後ろで recall の中にあり、直接
ヒットの下に関連行を足していました。これは保留にします (§7)。理由は 3 つ: recall は gate も
A/B も無しに byte 安定な契約を保つ; 順位リストに足された関連行はまさに以前 fuzzy 拡張が退行した
場所であり、item 内の根拠は有界で、ラベル付きで、決して並べ替えない; walk の呼び出し点が 1 つなら、
recall 側の拡張と reconstruct 側の拡張の間でホップ予算が複利になりえない。
7. 別途決めること¶
recallの中の連想 — グラフ経由で到達した行を直接ヒットの下に並べ、default-off の gate の後ろに置き、contamination ベンチマークの A/B で admit する。想起プロセスが計測される まで保留; その時にはそちらに吸収されるかもしれない。- 結果としての entity — 引用ではなく entity である reconstruct item。この段階では
入れない;
traverseが entity 中心の照会。 - entity の埋め込み (2.6.1) — 登録語ごとの安定ベクトル。名前から言い回しが drift した 記録でも届くように。同じ機能で、新しいツールは無し。
- 推論された関係 — この宣言集合を grounding に、confidence 付きで関係を提案する層。 別のテーブル、別の信頼水準; ここに混ぜない。
- anchor が削除された関係を残す (anchor を空にして)。この段階は削除する — すべての anchor が解決するように; 代替案は主張を残して根拠を失う。
8. この段階の判定¶
この段階が名乗る価値は能力です: 無かった決定論的経路が存在するようになる。ベンチマークの 利得は名乗らず、既定では与えられもしません — ベンチマークのコーパスには宣言が無く、無ければ 不変条件 2 により出力は byte-identical です。利得は何かが宣言して初めて測れ、その計測は 走らせる前に登録します:
- oracle 腕: ベンチマーク自身の注釈 (evidence session、entity ラベル) から機械的に導いた 関係。サーバ側の walk が寄与しうる上界を与える。
- 抽出器の腕: サーバの外のハーネスで抽出段が宣言した関係。実配備が得るもの — 抽出の質と walk の積 — を測る。
- 対照: 宣言なしの同じ質問。変更前の出力と等しくなければならない。
前もって述べる予想の形: 利得があるなら multi-hop と entity 中心の質問型に集中する; single-hop の 質問は不変で、そこの変化は利得ではなく束ねの退行と数える。上がった item は 1 つずつ、到達した 根拠が問いに答えたのか、ただ取り囲んだだけかを検分します。
その計測より前に、この段階は不変条件 1–8 で出荷します。各々にテストと、それを赤くする名指しの 変異が付きます。
9. 最初の実装が確定させた読み方¶
§3 は 6 点を実装に委ねていました。それぞれ次のように決め、各々にテストと、それを赤くする 変異が付いています。
- 向き。 walk は entity → entity の関係をどちらの端からも辿ります。連想は到達です: Kirari maintains MizEye は、問いがどちらを名指しても 2 つを結びます。述語は宣言された とおりに報告します。
- ホップは 1 から。 記録が根拠になるのは、少なくとも 1 本の宣言関係を通った時だけです。 候補と同じ entity を言及する記録は根拠になりません: entity の共有は関係ではなく、束ねキーでも ないのと同じです。
- 1 行は 1 箇所。 既に候補プールにある記録は、検索が置いた場所に留まります。2 つの item が
到達する記録は前の item に属します。walk は件数の窓に入った item に対し item 順で走るので、
ある item に足されるものは
countに依存しません。 - 読み取りも有界。 到達した entity ごとに、読む記録は最大
max_evidence件、id の小さい順 — 1 本の関係の内側で切り詰めが既に使っている順序 — です。それより多い entity があれば、 応答はbounds.reachedにmax_evidenceを挙げます: 上限に達しており、その先に何があるかは 分かりません。 whyと独立性。 宣言関係が束ねた行はwhy: "relation:<predicate>"を持ち、hopsを 持ちません; walk が到達した行は同じものにhopsを持ちます。関係が作ったクラスタのindependence_reasonは"cluster:relation"です。- 段 1 が足すのは票であって、通過ではない。 追加語は、問いの語では得られない字面の一致を 記録に与えます。融合された行はそれでも recall の品質ゲートを通る必要があり、ゲートは 変わらない問いに対して採点するので、字面のアームだけが見つけた記録は、別名が無い時と まったく同じくゲートの下に留まります。ゲートは recall のもので、この段階は変えません。 宣言された名前での一致をそれだけで通すかどうかは、別の判断です。