仕組みは直った。それでも賢くならなかった
自宅のマシンで OpenClaw を6台動かしていて、それぞれ Discord の Bot として常駐しています。全台に**意味検索(ベクトル検索)**を入れていて、過去の記録を聞けば思い出してくれる、はずでした。
ある1台(以下 K)でそれを復旧させたあと、動作確認をして気づきました。仕組みは直ったのに、答えの質が上がらない。
検索スコアを比べると差が明らかでした。
| 検索スコア | |
|---|---|
| 別の1台(以下 A) | 0.80 |
| K | 0.40〜0.48 |
しかも K は、無関係な USER.md の見出し行が上位に来ることがありました。ひどいときは「よ。」という2文字の断片がスコア 0.404 でヒットします。
原因は仕組みではなく素材でした。この記事は、素材をどう増やすか考えて、最終的に「Discord の生ログを毎晩ファイルに落として索引に食わせる」というところに着地するまでの話です。
索引の中身を数えてみる
openclaw memory status で索引の状態が見られます。3台を並べるとこうなりました。
日記(memory) |
会話ログ(sessions) |
合計 chunks | 日次日記の実ファイル | |
|---|---|---|---|---|
| A | 130 files · 2,823 | 246 files · 2,520 | 5,343 | 29件(1件76KB) |
| B | 35 files · 87 | 450 files · 4,023 | 4,110 | 1件 |
| K | 22 files · 86 | 36 files · 180 | 266 | 0件 |
桁が違います。 K は A の20分の1しか材料を持っていませんでした。スコアが伸びないのは当然です。
ここで1つ罠がありました。K の memory 配下 22ファイルは、全部 dreaming/ の自動生成物でした。手書き(というかエージェントが書いた)日次日記は1件もありません。memory status の "memory · 22 files" だけ見て「日記はある」と誤解しかけました。
索引の材料は2種類ある
OpenClaw の memory search は sources で対象を指定します1。
{
"memory": {
"search": {
"provider": "local",
"sources": ["memory", "sessions"]
}
}
}
-
memory— ワークスペースのmemory/**/*.md。日記。自分で書くもの -
sessions— OpenClaw が管理するセッション記録。やり取りの生ログ。呼ばれたり cron で動いたときに自動で貯まる
3台はこの2つの入り口の太さが違っていました。
- A = 日記型 — 毎日76KB 書く。索引の53%が日記
- B = セッション型 — 日記は書かない(1件だけ)。動いた記録が勝手に貯まる
- K = どちらも細い
誤診その1: 「B は全会話に参加しているから貯まっている」
B の索引が厚い理由を最初はこう分析しました。「B は requireMention の設定が無いので、全部の会話に参加して返事している。だから貯まる」。
これは間違いでした。
「返事してる? メンションしないと応答しないよ?」と指摘されて、ログを見直しました。B のログにも no-mention の skip が出ています(前日56件、当日32件)。**B もメンション時しか応答していません。**設定に requireMention の記述が無かったので「全会話に参加している」と読んでしまったのですが、ギルドでは既定でメンションが要ります。
実データで差の正体を調べ直すと、こうでした。
| 全セッション | cron 由来 | Discord 由来 | |
|---|---|---|---|
| B | 946 | 595(63%) | 340 |
| K | 40 | 7 | 32 |
B はセッションの6割以上が定期ジョブでした。 毎朝のメールチェック、毎晩の定型処理、1時間ごとの heartbeat、週次のレビュー。A も15分ごとのループが回っています。
つまり**素材の差は「会話量」ではなく「活動量」**でした。日記が厚くなるのは、書くネタになる仕事を毎日しているからです。
最大の発見: メンションされていない会話は記録が残らない
これが今回いちばん効いた発見です。
requireMention: true の場合、呼ばれていない発言は gateway が reason: "no-mention" で捨てます。捨てられた発言は transcript_events に1件も入りません。
つまり、
- 日記に書かれないどころか、そもそも記録が存在しない
- 索引に入りようがない
- 「あのとき誰かがこう言ってたよね」は原理的に答えられない
実測すると、K ではその日12件が no-mention で skip され、transcript_events の最新は「呼ばれて返事したやり取り」だけでした。
エージェントは、自分がメンションされていない会話については、見ていないのではなく記憶していない。 ここが穴でした。
検討して、採らなかった案
素材を増やす方向で、3つの案を検討して却下しています。
❌ 案1: ローカル LLM で会話を要約して日記にする
これは私の提案でした。Claude の消費(トークン)を抑えたいので、眠っている Gemma 4 E4B(4.6GB、K のみ所持)に要約させたらどうか、という発想です。
方向性は正しいが、要約はやらないという結論になりました。理由が4つ。
- 要約は情報を失う。生ログは失わない。 後から要約はできますが、逆はできません。まず溜めるのが正しい順序です
- 4B モデルの日本語要約品質が未検証。 不正確な要約が索引に入ると、エージェントが間違った事実を自信を持って答えます。生ログなら少なくとも事実です
-
実測 5.6 トークン/秒(CPU 推論・GPU 無し)。しかも Gemma 4 は thinking モデルで、
max_tokens: 200では英語の思考だけで打ち切られて出力が空になりました - そもそも要約しなければトークンは1つも要らない
ちなみに検証の途中で1つ学びがありました。openclaw agent 経由でローカル LLM を叩くと、巨大なシステムプロンプトとツール定義が乗って、CPU 推論の4Bモデルには重すぎて10分でも終わりません。スクリプトから直接 HTTP を叩けば動きます。「モデルが遅い」ではなく「呼び方が重い」でした。
このモデル自体、対話用に入れたつもりが /model ピッカーで何も選ばなかったため使用経路が塞がれていて、入れてから6日間、一度もロードされていませんでした(最終アクセス時刻とログで確認)。
❌ 案2: 生ログを Zip にして Git へ
サイズが気になったので聞いてみた案です。これも却下でした。
-
索引が効かない。 索引は
memory/**/*.mdを読みます。Zip の中身は読めないので、目的そのものを達成できません - Git がむしろ肥大する。 テキストは差分圧縮が効きますが、Zip はバイナリ扱いなので毎日全体が新しいオブジェクトになります
- 秘密情報の検査ができない。 gitleaks が中身を見られません
- そもそも年間4〜8MB で、圧縮する動機がありません(A の日記29日分が1.65MB)
❌ 案3: 日記をやめて生ログだけにする
「日記は Claude を使うから高いのでは」と思ったのですが、実測を見たら違いました。
A の14日間の消費 63.2億トークンの内訳:
| 用途 | 割合 |
|---|---|
| 15分ごとのループ | 77% |
| 対話 | 22% |
| その他 cron | 0.7% |
しかも総トークンの 95.8% はキャッシュ読み込みで、出力は 0.7% です。日記は 57KB/日 ≒ 出力3〜4万トークン/日、14日で約50万トークン。総消費の 0.01% 未満でした。
重いのは日記ではなくループです。 日記をやめても何も浮きません。
そして日記には生ログに無い価値があります。「その日何が重要だったか」という判断が入っている点です。生ログは平坦で、全部が同じ重みで並びます。
着地: 日記 + 生ログの二本立て
結論はこうなりました。
memory/
├── YYYY-MM-DD.md ← 日記。エージェント自身(Claude)が書く
└── discord-logs/
└── YYYY-MM-DD.md ← Discord 生ログ。スクリプトが保存(LLM 不使用)
↓
OpenClaw が自動で索引化(embeddinggemma / 768次元)
↓
意味検索で応答に使われる
役割分担はこうです。
| 誰が | LLM | 頻度 | |
|---|---|---|---|
| 日記 | エージェント自身 | 使う | cron 毎日22:00 |
| Discord 生ログ | スクリプト | 使わない | cron 毎晩23:30 |
| 索引化 | OpenClaw | 埋め込みのみ | 自動。cron 不要 |
索引化に cron は要らない
ここは実測で確認しました。cron が書いた日記が、openclaw memory index を一度も走らせていないのに Indexed 59/59 · Dirty: no として取り込まれていました。ファイルを memory/ に置けば OpenClaw が追随します。
書いたのが誰か(Claude かスクリプトか)も関係ありません。ここが設計上ありがたい点で、スクリプトが直接 memory/ にファイルを置けば、それがそのまま索引に入ります。
なお、埋め込みモデルはローカルの GGUF です2。
openclaw plugins install @openclaw/llama-cpp-provider
既定で embeddinggemma-300m-qat-Q8_0.gguf(約0.3GB)が自動ダウンロードされ、768次元のベクトルを作ります。API キー不要・課金ゼロなので、毎晩ファイルが増える運用と相性がいいです。
日記の cron
agentTurn 型で、isolated セッション、Discord には投稿しない設定にしています。
日記(毎日22:00) cron 0 22 * * * (exact) isolated 投稿なし
-
isolated— main セッションの記録を汚しません - 投稿なし — 日記は本人の記録なので Discord には出しません
-
--exact— 時刻をずらさず固定します
AGENTS.md 側には書式のルールだけを残しました。「何もなかった日も1行書く」「上書きせず追記」「名前やパスは具体的に」。起動は cron、heartbeat は「その日の分がまだ無ければ書く」保険、という分担です。
実際に書かれた最初の日記がこれです。
## 2026-09-08
- メンションなし。静かな一日。
その日は本当にメンションが無かったので1行です。「何もなかった日も書く」を守ると、空白の日が無い記録になります。
生ログ収集スクリプト
memory/discord-logs/YYYY-MM-DD.md に平文の Markdown で落とします。設計方針をスクリプトの docstring にそのまま書きました。
"""Discord の「その Bot に見えている範囲」の会話を生ログとして workspace に保存する。
意味検索(ベクトル検索)の素材を増やすのが目的。メンションされていない発言は
gateway が `reason: no-mention` で捨てるため transcript に一切残らず、
日記にも索引にも入らない。その穴を埋める。
設計方針:
- **LLM を使わない。** 要約せず生ログのまま置く。要約は情報を失い、後から復元できない
- **取得範囲は実行時に動的判定。** 見えるチャンネルは Bot ごとに意図的に絞られている
(アクセス制御の境界)。固定リストを持たず、権限が変われば範囲も自動で追随する
- **公開スレッドは参加不要で読める。** 親チャンネルの閲覧権限があれば足りる
- **上限で切らない。** 多すぎる場合も捨てずに保存し、警告だけ出す
- **1発言1チャンクにしない。** チャンネル単位でまとめて書く。細切れだと
「よ。」のような無意味な断片が検索上位に来る
"""
最後の項目は実害から来ています。冒頭に書いた「よ。」がスコア 0.404 でヒットした件です。1発言ずつ切ると、意味を持たない断片が独立したチャンクとして索引に入ります。 時系列でまとめて書けば、前後の文脈ごと1つのチャンクになります。
保存先を memory/ 配下にしているので、workspace の Git 管理(30分ごとの autocommit)に自動的に乗ります。秘密情報は autocommit スクリプトの gitleaks が push 直前に検査して、検出したら git reset して中断します。
個人情報や他人の発言が入る点は、全部自分が管理しているリポジトリなので許容という判断にしました。
取得範囲の実測
Bot は閲覧権限のあるチャンネルしか読めません。ただし公開スレッドは参加不要で読めます。
| ギルド | テキストch | 読める | 読めない |
|---|---|---|---|
| ギルド1 | 47 | 10 | 37 |
| ギルド2 | 9 | 9 | 0 |
| ギルド | アクティブなスレッド | 参加中 | 未参加でも読める | 読めない |
|---|---|---|---|---|
| ギルド1 | 110 | 1 | 28 | 81 |
| ギルド2 | 35 | 23 | 12 | 0 |
読めないスレッド81件は、スレッド側の問題ではなく親チャンネルの権限不足でした。チャンネル単位で権限を開ければ、配下のスレッドは自動的について来ます。
スクリプトが固定のチャンネルリストを持たないのはここが理由です。見える範囲は Bot ごとに意図的に絞ってあり、それがアクセス制御の境界そのものなので、権限を変えたら取得範囲も自動で追随してほしい。
開発ログが索引を埋める問題
ここからが今回いちばん揉めたところです。
私のホストには Claude Code のセッションを Discord スレッドで回す仕組み(C-lord)があって、#c-lord チャンネルの配下に開発スレッドが大量に立ちます。「これ、索引に入れたら多すぎない?」と何度か聞きました。
最初の回答は「#c-lord は 5KB/日で少ないです」でした。これも誤りでした。 チャンネル本体しか測っていなかったのです。
実体を測り直すと、こうでした。
| 1日の取得量(除外なし) | 154KB / 297件 |
うち #c-lord 配下スレッド
|
105KB(68%) |
#c-lord チャンネル本体
|
6KB |
7割が開発スレッドでした。 索引は1日150〜300チャンク増える計算で、K 自身の記憶(267チャンク)が1週間で埋もれます。
除外の設計: チャンネル本体は残す
最初の提案は「#c-lord チャンネルごと除外」でした。ここは反対しました。
チャンネル内部でどういう開発が行われているかすら知らないと、必要に応じて取得にも行けないのでは?
#c-lord のチャンネル本体には、各スレッドの1行要約が流れます。そこを残しておけば「何の開発が動いているか」は把握できて、詳細が要るときだけエージェントが Bash で取りに行けます。丸ごと除外すると「そもそも何が行われているか知らない」状態になり、取りに行くこともできません。
着地した設計がこれです。
{
"_default": {
"exclude_threads_under": {
"<#c-lord のチャンネルID>": "Claude 同士の開発スレッド群。チャンネル本体は残す"
}
}
}
指定したチャンネルの配下スレッドだけを除外し、チャンネル本体のログは残す。 スレッドかどうかは parent_id で判定します。
心配だったのは「開発以外のチャンネルの会話まで消えないか」でしたが、実データで確認して問題ありませんでした。除外されるのは #c-lord 配下だけで、事業まわりのチャンネル配下のスレッド12本(品質の相談、SNS運用、価格と決済…)はそのまま残ります。
結果
| 除外なし | 除外後 | |
|---|---|---|
| 1日の取得量 | 154KB / 297件 | 59KB / 71件 |
| 主成分 |
#c-lord の開発ログ |
「iPhoneから睡眠データの抜き出し」34KB など本来欲しい相談 |
#c-lord 本体 |
— | 6.8KB(11%) |
取得量が減ったこと自体より、中身が入れ替わったことが重要でした。索引の主成分が「自分に関係のある相談」になりました。
上限で切らない
「1チャンネル1日あたり500件 / 200KB を超えたら切り捨てる」というガードを提案されましたが、これは断りました。切ってしまうとそれはそれで不便だからです。
代わりに同じ閾値を警告だけにしています。想定外に膨らんだチャンネルがあれば気づけますが、データは捨てません。
# 想定を超えた量を検知するための「警告のみ」の目安(切り捨てはしない)
WARN_MSGS_PER_CHANNEL = 500
WARN_BYTES_PER_CHANNEL = 200 * 1024
記録は消えない、という設計判断
1つ判断を求められたことがあります。
あるチャンネルを一度記録した後で権限を絞っても、
memory/discord-logs/に残った過去ログは索引に残り続けます。「Bot に見えない=その Bot 経由では見えない」という設計の趣旨からすると、ここが穴になり得ます。
何もしないを選びました。一度見たものは記憶として持ち続ける、という考え方です。人間と同じだと認識しておいて、今後不都合が出たら削除機能を入れます。
事故: 手動起動のインスタンスに設定変更を打った
作業中に1台を約20分停止させました。
config set の反映で、そのインスタンスが自分を再起動しました。
restart mode: full process restart (supervisor restart)
ところがこの台は手動起動で、supervisor がいませんでした。落ちたまま戻ってきません。手動起動スクリプトで復旧して、agent -m "1+1は?" で応答も確認しました。
同じ理由で他の台も再起動していましたが、そちらは systemd 管理下だったので自動復帰していました。
手動起動のインスタンスに
config setを打つと、自己再起動して落ちたまま戻らないことがある。 systemd 管理下ならRestart=alwaysで戻るが、手動起動には supervisor がいない。設定変更のあとはss -ltnで listen を確認する。
最終状態
全6台で意味検索が稼働しています(768次元)。
台1 ✅ 768次元 4,185 chunks
台2 ✅ 768次元 684 chunks ← 今回新規に有効化
台3 ✅ 768次元 267 chunks
台4 ✅ 768次元 39 chunks
台5 ✅ 768次元 1,353 chunks
台6 ✅ 768次元 5,343 chunks
日記 cron を全6台に(22:00〜22:24、台ごとにずらす)、生ログ cron も全6台に(23:30〜23:55)入れました。時刻をずらしているのは、6台が同時に動いてホストが詰まるのを避けるためです。
なお、意味検索を今回新規に有効化した台は、もともと memory.search.enabled: false にしていました。OpenAI 埋め込みの課金を避けるためです。ローカル埋め込みなら課金ゼロなので、有効化しない理由がなくなりました。
まだやっていないこと
-
未参加スレッドの要約。 「過去N日に更新のあったスレッド」は
last_message_idから正確に絞れます(実測: 過去1日=9件、過去7日=16件)。ただしローカル LLM では非現実的(105KB を CPU 推論で毎晩は数十分)で、Claude Sonnet 5 なら1〜2分・日次消費の0.01%未満です。生ログ運用の効果を見てから着手にしました。原文は Discord に残るので後から遡れます - 効果測定。 初回実行は導入当夜なので、検索スコアが実際に上がるかはまだ見ていません
- 権限の拡張。 読めないチャンネル37件は、必要になったら都度開ける方針です
この作業で学んだこと
「仕組みを直す」と「賢くする」は別の作業でした。 ベクトル検索を復旧させたときは、それで終わったつもりでいました。実際には索引に入る材料が20分の1しかなくて、仕組みが正しく動いた結果として貧しい答えが返っていただけです。
そして測り方を間違えると、間違った設計に着地します。 今回、「開発ログが多すぎないか」という懸念に対して最初「5KB/日で少ない」という回答が返ってきました。チャンネル本体しか測っていなかったからです。実体は7割が開発スレッドでした。同じ質問を繰り返し投げたことで、除外設計まで辿り着けました。
エージェントは自信を持って数字を出してきます。その数字が何を測ったものかは、聞かないと出てきません。
参考リンク
- OpenClaw 公式サイト / ドキュメント
- Memory search(ハイブリッド検索とプロバイダ)
- Memory configuration reference
- Memory CLI(
memory status/memory index) - Dreaming(夜間の記憶の統合)
- Scheduled tasks (cron)
- Agent workspace
- gitleaks
- C-lord(Discord のスレッドごとに Claude Code セッションを立てる拙作 OSS)
- 拙記事: 「ドメイン移管が失敗した理由を調べて」をどこに投げるか — ChatGPT・Claude Code と比べて OpenClaw を選んでいる理由
-
出典: Memory search · OpenClaw および Memory configuration reference。
memory.search.sourcesに["memory", "sessions"]を指定すると、ワークスペースのmemory/配下と OpenClaw のセッション記録の両方が検索対象になります。なおsessionsを含むセッション記録の検索はtools.sessions.visibilityの制約に従います。 ↩ -
出典: Memory configuration reference の Local (managed llama.cpp server) の項。「Install the official llama.cpp provider, then choose llama.cpp once in interactive setup. OpenClaw installs a pinned, verified
llama-serverand writes its loopbacklocalServiceconfiguration. Default model:embeddinggemma-300m-qat-Q8_0.gguf(~0.3 GB, auto-downloaded).」検証にはopenclaw memory status --deep --agent main/openclaw memory index --force --agent mainを使います。OpenClaw 2026.8.1 同梱ドキュメントで確認。 ↩