自戒、点検、内省

終わらない反省会をしよう

自分のClaudeに提供する"知恵袋" ブログやSNSの全ログを詰めました

chiebukuro-mcp シリーズの続きです。

retrospective.hatenadiary.com

今回は自分の過去のブログ記事と SNS 投稿をぜんぶ SQLite に詰めて、chiebukuro-mcp から Claude に見せられるようにしました、という話。

なんで入れたのか

Claude に「あの頃なに考えてたっけ」を聞きたいときが、わりとあります。

個人的な記憶は頭の中にありますが、解像度が粗いです。手元に過去の自分の発言ログがあれば、Claude に日付範囲とキーワードを渡すだけで拾い上げてくれる。そう思って詰め始めました。

長期記憶の機構は別で動いていますが、あれは Claude Code のセッションを後追いで保存するもので、2023年以前にわたしが考えていたことは当然入っていません。過去のわたしを呼び出すなら、過去のわたしが書いた文章から引き出すしかないわけです。

何を詰めたか

はてなブログや、Xなど合計 60,194 件、2006年5月から2026年4月までの20年分、という感じになりました。 それぞれのエクスポートデータから取り込んでいます。

テーブル設計

ドカンとシンプルです。

CREATE TABLE blog_entries (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  source TEXT NOT NULL,   -- 'twitter:bash0C7' など
  date TEXT NOT NULL,     -- ISO8601文字列
  title TEXT,             -- ブログのみ、SNSはNULL
  body TEXT NOT NULL
);
CREATE INDEX idx_blog_entries_source_date ON blog_entries(source, date);

date を ISO8601 文字列にしているのは、Claude が書く SQL が WHERE date >= '2020-06' AND date < '2020-07' のようなシンプルな文字列比較で書けるからです。このほうが複合インデックスが効きやすくて、substr()date() を挟まれるよりも速い、という理由です。

title は SNS で常に NULL にしてあるので「ブログ記事だけほしい」というときは WHERE title IS NOT NULL で分けられます。

embedding は chunk 刻みで

semantic search 用の embedding は、chiebukuro-mcp gem 側の Embedder クラスを使って一括生成しました。

60,000件一気に回すとメモリが不安だったので、chunk_size=100 で細かく分けて、LEFT JOIN で未処理のエントリだけを拾う形にしています。

loop do
  rows = db.execute(<<~SQL, chunk_size)
    SELECT e.id, e.title, e.body
    FROM blog_entries e
    LEFT JOIN blog_entries_vec v ON v.entry_id = e.id
    WHERE v.entry_id IS NULL
    ORDER BY e.id
    LIMIT ?
  SQL
  break if rows.empty?
  rows.each do |id, title, body|
    text = [title, body].compact.join("\n")
    blob = embedder.embed(text).pack("f*")
    db.execute("INSERT INTO blog_entries_vec(entry_id, embedding) VALUES (?, ?)", [id, blob])
  end
  GC.start
end

LEFT JOIN 方式なので、途中で止めてもリランすれば続きから再開されます。

Claude にヒントを渡す

chiebukuro-mcp は chiebukuro.json の description を Claude にそのまま見せる作りになっています。なので description に単なる説明ではなく、データの特性とクエリの書き方のヒントを詰め込んでいます。

いまの blog DB の description にはこういうことを書いています。

  • 合計件数と期間
  • ソース別の件数・期間・平均文字数・テーマ
  • 高密度期 / 低密度期
  • 日付フィルタの書き方(文字列比較でOK)
  • 複合インデックスがあるので substr() / date() は避けて、という注意
  • 時期ごとのデータの偏り(発信の多い時期、そうじゃない時期)

最後のは地味に大事です。「その時期のわたしの投稿を見せて」と頼んだときに「データがありません」とだけ返ってくると、データが消えたのか当時黙っていたのかがわかりません。description にあらかじめそういう期間と書いておくと、Claude がその旨を添えて返してくれます。

今後のためにスキルを切った

この更新作業、これからも定期的にやることになります。手動ではありますが追加取り込みが発生します。

そのたびに「どの順番で何を実行するんだっけ」を思い出すのは面倒なので、Claude Code 用のスキルとして固定化しました。blog-db-update という名前で、現状把握 → ソース別インポート → embedding 更新 → 検証、という手順を書いただけのものです。

どう使っているか

Claude に自然言語で聞くだけです。

2020年の夏のわたしのツイートを20件ほど見せて。技術系のものがあれば優先で。

すると chiebukuro_query_blogchiebukuro_semantic_search_blog のどちらかが呼ばれて、結果が返ってきます。

SQL で日付レンジを絞りたいだけなら query 側、テーマで探したいなら semantic search 側、という使い分けになっています。Claude が適当に選んでくれるので、こちらは「何を知りたいか」だけ伝えればよい、というところです。

日誌系のスキルと組み合わせると面白いです。chiebukuro-daily-journal スキルが location_history と things3 と health DB を横断して日誌を作ってくれるのですが、そこに blog DB を加えると「その日に何をして、何を考えて、何を書いたか」まで一枚の日誌になります。

これから

次は「書いた内容の濃さ」をどう扱うかです。

ツイートの1件とブログ記事の1件を同じ重みで semantic search するのは、さすがに粗いです。retrospective の記事は平均1,247文字あり、twitter の1件は平均40文字なので、検索結果に大量のツイートが並ぶと、わたしが腰を据えて書いた記事が埋もれます。

source を重みに入れる、長さでフィルタする、長文は chunk 化する、あたりをぼんやり考えていますが、まずはドッグフーディングしてみてから決めようと思います。

あとは、この chiebukuro-mcp のシリーズ自体を、もう少しまとめて書き起こす時期かもしれません。いろいろ DB を並べてきて、個別に紹介してきましたが、全体像を改めて並べたほうが自分でも整理できそうです。そのうち。