
AIエージェントに「記憶」を持たせる方法として、RAG(Retrieval-Augmented Generation)が広く使われています。しかし、RAGには「毎回検索してコンテキストを再構築する」という根本的な制約があります。
ここでのRAGは、外部のデータ(文書・ログなど)を検索して、LLM(Large Language Model: 大規模言語モデル)に渡す仕組みです。 一方で「状態の蓄積(前回の決定が、今回どう更新されたか)」は苦手になりやすいです。
Rowboat
は、この問題を「継続的にナレッジグラフ(知識をノードとリンクで表す構造)を更新し、状態を蓄積する」アプローチで解決しようとするオープンソース(Apache-2.0)のローカルファーストAIコワーカーです。
Y Combinator
のS24出身のチームが開発しており、メール(Gmail
)や会議メモ(Granola
、Fireflies
)から情報を抽出します。
抽出した情報は、Obsidian
互換(ObsidianはMarkdownベースのノートアプリ)のMarkdownファイルとして保存され、リンク(バックリンク: [[...]] でノート同士を参照する仕組み)でつながります。
ローカルファースト(データをクラウドではなく端末内に置く設計)なので、内容を自分で確認・編集できるのが売りです。
本記事では、Rowboatの設計思想、特にエンティティ解決(Entity Resolution: 表記ゆれや文脈をまたいで同一の人物/組織を統合する処理)とバッチ処理の実装を掘り下げます。
RAGの限界:「状態の蓄積」ができない
RAGは、ベクトルデータベース(文章を埋め込みベクトルにして検索する仕組み)に文書を保存します。 そしてクエリに応じて関連文書を検索し、LLMに渡します。 この方法は以下の問題を抱えています。
- 毎回検索が必要: 同じ人物やプロジェクトについて何度も質問しても、毎回ゼロから検索し直す
- 状態の更新が困難: 「前回の会議で決まったこと」を「今回の会議で変更された」という更新を追跡できない
- 関係性の喪失: 人物間の関係、プロジェクト間の依存関係などのグラフ構造を保持できない
Rowboatの開発者はHacker News で次のように説明しています。
Passing gigabytes of email, docs, and calls directly to an AI agent is slow and lossy. And search only answers the questions you think to ask. A system that accumulates context over time can track decisions, commitments, and relationships across conversations, and surface patterns you didn’t know to look for.
(意訳:メール、ドキュメント、会議記録をそのままAIエージェントに渡すのは遅く、情報が失われます。検索は「あなたが思いついた質問」にしか答えられません。時間とともにコンテキストを蓄積するシステムは、会話を横断して決定、約束、関係性を追跡し、あなたが探そうとも思わなかったパターンを浮かび上がらせることができます)
Rowboatのアーキテクチャ:二層構造
Rowboatは、データ取得層とナレッジグラフ構築層を分離した二層アーキテクチャを採用しています。
第一層:Raw Sync Layer(生データ同期層)
Gmail、Google Calendar 、会議メモ(Granola、Fireflies)などのデータソースから、生データをローカルに保存します。 Rowboatは「ファイルベースで冪等(同じ入力は同じ結果になり、重複取り込みしない)」な同期層を持つ、と説明されています。
- 冪等性(Idempotency): 同じメールスレッドや会議記録は、ソースIDをキーとして一度だけ保存される
- 追記専用(Append-only): 既存ファイルを上書きせず、新しいデータのみ追加
- 重複排除なし: この層では「Sarah」と「Sarah Chen」を区別しない
例えば、Gmailのスレッドを「ソースIDでキーされたファイル」として保存するイメージです(実際のパスや形式はバージョンで変わり得ます)。
| |
第二層:Graph Building Layer(グラフ構築層)
生データから、人物(People)、組織(Organizations)、プロジェクト(Projects)、トピック(Topics)を抽出し、Obsidian互換のバックリンク付きMarkdownファイルとして保存します。
これもあくまでイメージですが、人物やプロジェクトごとにMarkdownファイルが作られます。
| |
この層で重要なのがエンティティ解決(Entity Resolution)です。
エンティティ解決:「Sarah」と「Sarah Chen」を同一人物と判断する
エンティティ解決とは、異なる表記や文脈で登場する同一エンティティ(人物、組織など)を統合する処理です。
従来の方法:文字列マッチング
単純な文字列マッチングでは、以下のような問題が発生します。
- 「Sarah」と「Sarah Chen」は別人として扱われる
- 「Sarah Chen」と「S. Chen」も別人になる
- 「Horizon Ventures」と「Horizon VC」も別組織になる
RowboatのLLMベースエンティティ解決
Rowboatは、LLMを使ってエンティティ解決を行います。開発者はHacker Newsのコメント で次のように説明しています。
An LLM processes batches of those raw files along with an index of existing entities (people, orgs, projects and their aliases). Instead of relying on string matching, the model decides whether a mention like “Sarah” maps to an existing “Sarah Chen” node or represents a new entity, and then either updates the existing note or creates a new one.
(意訳:LLMは、生ファイルのバッチと既存エンティティのインデックス(人物、組織、プロジェクト、およびそれらの別名)を処理します。文字列マッチングに頼るのではなく、モデルが「Sarah」という言及が既存の「Sarah Chen」ノードにマッピングされるか、新しいエンティティを表すかを判断し、既存のノートを更新するか新しいノートを作成します)
具体的な処理フロー
インデックス構築: 既存のすべてのエンティティ(人物、組織、プロジェクト、トピック)のインデックスを作成
- エンティティ名
- 別名(Aliases)
- 主要なメタデータ(役職、所属組織など)
バッチ処理: 生ファイルを一定数ごとにバッチ化し、LLMに渡す
- バッチサイズは処理効率とコンテキストウィンドウのバランスで決定
- 各バッチには、インデックス全体と処理対象の生ファイルが含まれる
エンティティ判定: LLMが以下を判断
- 「Sarah」は既存の「Sarah Chen」か、新しい人物か
- 「Horizon VC」は既存の「Horizon Ventures」か、別組織か
ノート更新または作成:
- 既存エンティティの場合:該当するMarkdownファイルを更新
- 新規エンティティの場合:新しいMarkdownファイルを作成
インデックス再構築: 次のバッチ処理前に、新しく作成されたエンティティを含むインデックスを再構築
この「バッチ処理 → インデックス再構築」のサイクルにより、後のバッチは前のバッチで作成されたエンティティを認識できます。
マルチパス処理:グラフの収束
Rowboatのグラフ構築は、実質的にマルチパス処理です。
開発者はHacker Newsのコメント で次のように説明しています。
It’s effectively multi-pass: we process in batches and rebuild the index between batches, so later batches see entities created earlier. That keeps context manageable while still letting the graph converge over time.
(意訳:実質的にマルチパスです。バッチで処理し、バッチ間でインデックスを再構築するため、後のバッチは前に作成されたエンティティを認識します。これにより、コンテキストを管理可能に保ちながら、時間とともにグラフを収束させることができます)
収束の例
第1バッチ: メール10件を処理
- 「Sarah」という人物を新規作成 →
sarah.md
- 「Sarah」という人物を新規作成 →
インデックス再構築:
sarah.mdがインデックスに追加第2バッチ: メール10件を処理
- 「Sarah Chen」という言及を発見
- LLMが既存の「Sarah」と同一人物と判断
sarah.mdをsarah-chen.mdにリネームし、内容を更新
第3バッチ: 会議メモ5件を処理
- 「S. Chen」という言及を発見
- LLMが既存の「Sarah Chen」と同一人物と判断
sarah-chen.mdに別名「S. Chen」を追加
このように、バッチを重ねるごとにエンティティの情報が充実し、グラフが収束していきます。
ツールアクセス:必要に応じて詳細情報を取得
LLMは、インデックスだけでは判断できない場合、ツールを使って既存ノートの詳細情報を取得できます。
開発者はHacker Newsのコメント で次のように説明しています。
The agent also has tool access to read full notes or search for entity mentions in existing knowledge if it needs more detail than what’s in the index.
(意訳:エージェントは、インデックスにある情報以上の詳細が必要な場合、完全なノートを読み取ったり、既存の知識内でエンティティの言及を検索したりするツールアクセスを持っています)
例えば、「Sarah」という言及があった場合、LLMは以下のように判断します。
- インデックスに「Sarah Chen(Partner at Horizon Ventures)」が存在
- しかし、メール本文に「Sarah from TechFlow」という記述がある
- LLMがツールを使って
sarah-chen.mdの詳細を読み取る - 「Sarah Chen」の所属が「Horizon Ventures」であることを確認
- 「Sarah from TechFlow」は別人と判断し、新しいノート
sarah-techflow.mdを作成
コンテキストサイズの管理:グラフ全体を渡さない
Rowboatは、グラフ全体をLLMに渡すのではなく、軽量なインデックスのみを渡します。
ここでいうコンテキストウィンドウ(Context Window)は、LLMが一度に入力として扱えるトークン量のことです。
開発者はHacker Newsのコメント で次のように説明しています。
We don’t pass the entire graph into the model. The graph acts as an index over structured notes. The assistant retrieves only the relevant notes by following the graph. That keeps context size bounded and avoids dumping raw history into the model.
(意訳:グラフ全体をモデルに渡すことはしません。グラフは構造化されたノートのインデックスとして機能します。アシスタントはグラフをたどって関連するノートのみを取得します。これにより、コンテキストサイズが制限され、生の履歴をモデルに投げ込むことを避けられます)
インデックスの構造例
| |
このインデックスは、グラフ全体(数百〜数千のノート)に比べて圧倒的に小さく、LLMのコンテキストウィンドウに収まります。
実例:Obsidian/Rowboatのノートから「軽量インデックス」を作る
Rowboatの実装をそのまま再現するのは大変です。 ただ、「軽量インデックスがあると、エンティティ解決や検索がやりやすい」という発想は、自分のVault(ノートを格納するフォルダ)でも試せます。
ここでは、Markdownを走査して簡易インデックス(タイトル、別名、バックリンク)を生成するスクリプトを紹介します。
前提
- Python 3.10+ が入っていること
- Markdownのフォルダ(Obsidian vaultや、Rowboatのknowledgeフォルダ相当)を用意すること
サンプル(最小)
例えば ./vault/people/sarah-chen.md を用意します。
| |
次に build_index.py を作ります。
| |
実行します。
| |
index.json に notes 配列が出力されていれば成功です。
この例は単純ですが、次の拡張ができます。
people/やprojects/などのディレクトリを重み付けして「重要度の代理指標」にするaliasesの一致とバックリンクを使って、同一人物候補を列挙する- 更新日時(タイムスタンプ)を入れて「最新状態」を優先する
ノイズ問題への対処:note-strictness設定
実運用では、「スパムメールの送信者がナレッジグラフに入ってしまう」という問題が発生します。
Rowboatは、note-strictnessという設定で、どの程度の情報をエンティティとして抽出するかを制御します。
開発者はHacker Newsのコメント で次のように説明しています。
We currently have different note-strictness levels that auto-inferred based on the inbox volume (configurable in ~/.rowboat/config/note-creation.json) that control what qualifies as a new node. Higher strictness prevents most emails from creating new entities and instead only updates existing ones.
(意訳:現在、受信箱のボリュームに基づいて自動推論される異なるnote-strictnessレベルがあります(~/.rowboat/config/note-creation.jsonで設定可能)。これは、何が新しいノードとして適格かを制御します。より高い厳格さは、ほとんどのメールが新しいエンティティを作成するのを防ぎ、代わりに既存のエンティティのみを更新します)
重要度の代理指標
開発者は、「送信先を重要度の代理指標にする」というアイデアを検討しています。
Using “people I send emails to” as a proxy for importance is a really good idea.
(意訳:「私がメールを送信する人」を重要度の代理指標として使用するのは本当に良いアイデアです)
つまり、以下のようなルールが考えられます。
- 受信のみ: note-strictness = High(スパムの可能性が高い)
- 送信あり: note-strictness = Low(重要な人物の可能性が高い)
GraphitiやMem.aiとの違い
Rowboatと似たアプローチを取るプロダクトとして、Graphiti (Zep)やMem.ai があります。
Graphiti vs Rowboat
開発者はHacker Newsのコメント で次のように説明しています。
Graphiti is primarily focused on extracting and organizing structured facts into a knowledge graph. Rowboat is more focused on day-to-day work. We organize the graph around people, projects, organizations, and topics.
(意訳:Graphitiは主に構造化された事実を抽出してナレッジグラフに整理することに焦点を当てています。Rowboatは日々の仕事により焦点を当てています。私たちは人物、プロジェクト、組織、トピックを中心にグラフを整理します)
- Graphiti: 汎用的な時系列ナレッジグラフ、構造化事実の抽出が中心
- Rowboat: 仕事の文脈(人物、プロジェクト、組織、トピック)に特化
Mem.ai vs Rowboat
- Mem.ai: クラウドベース、ナレッジグラフの可視化機能なし
- Rowboat: ローカルファースト、Obsidian互換のMarkdownファイル、グラフ構造を直接編集可能
まとめ
Rowboatは、RAGの「毎回検索」アプローチではなく、「継続的にグラフを更新」するアプローチで、AIエージェントに長期記憶を持たせようとしています。
その核心は、以下の設計にあります。
- 二層アーキテクチャ: 生データ同期層とグラフ構築層を分離
- LLMベースエンティティ解決: 文字列マッチングではなく、LLMが文脈を考慮して同一エンティティを判断
- バッチ処理とマルチパス: インデックスを再構築しながらバッチ処理を繰り返し、グラフを収束させる
- 軽量インデックス: グラフ全体ではなく、インデックスのみをLLMに渡してコンテキストサイズを管理
- note-strictness: ノイズを抑制し、重要なエンティティのみを抽出
この設計により、Rowboatは「前回の会議で決まったことが今回の会議で変更された」といった状態の更新を追跡できます。
ローカルファーストでObsidian互換という選択も、透明性と可搬性を重視した結果です。すべてのデータはMarkdownファイルとして保存され、ユーザーはいつでも読み取り、編集、削除できます。
Rowboatはまだ発展途上ですが、AIエージェントの「記憶」設計における一つの方向性を示しています。