Valkeyを中枢神経に据えた3エージェントAIスワーム「NeuroValkey Agents」の実装
目次
DEV Communityに上がっていた Building a Multi-Agent AI Swarm with Valkey as the Nervous System(Harish Kotra)の実装が面白かったので、設計を読み解く。
タイトルどおりValkeyを中枢神経(nervous system)に据えた3エージェント構成の実験で、GitHubに neurovalkey-agents として全コードが公開されている。
先に位置付けを整理しておく。
エージェントのメモリ問題はここ数カ月で Cloudflare Agent Memoryのマネージド化、Chroma Context-1の検索エージェント、Compresr Context Gatewayのプロキシ型 と方向を変えつつ攻められてきた。
NeuroValkey Agentsはこの流れのうち「プラットフォームに頼らず、Valkey一本で記憶・協調・状態遷移を全部引き受ける」セルフホスト側の実装サンプルだ。
Cloudflare Agent Memoryが外に置かれた共有ストアをマネージドで提供するのに対して、こちらは自分のDocker Compose上にValkeyを立てて、そこに全部預ける構成になっている。
Valkeyとは何か
まずValkeyの位置付けから。
Valkeyは2024年3月、Redis社が Redis 7.4でライセンスをRSALv2/SSPLv1のデュアルライセンスに変更 したことを受けて、Linux Foundation配下でフォークされたBSDライセンスのKey-Valueストアだ。
コマンド体系・プロトコルはRedis互換で、既存のRedisクライアントがそのまま動く。
この記事の文脈で重要なのは2点ある。
ひとつは公式DockerイメージにValkey Search(Redis Stack系の FT.* コマンド)やValkeyJSON(JSON.* コマンド)などのモジュールが同梱されていること。
もうひとつはPub/Sub・Stream・Hash・Vector検索がワンプロセスで揃っていて、別々のミドルウェアを重ねる必要がないこと。
つまりRedis Stackで書いていたものをライセンスの縛りなしでそのまま動かせる、というのが2026年時点のValkeyだ。
NeuroValkey Agentsはこの「全部入り」を1つの実行基盤として使い倒す例になっている。
キャッシュではなく実行基盤として使う
著者(Harish Kotra)が強調しているのは、ValkeyをLLMレスポンスのキャッシュとして使うのではなく、エージェント群のランタイム実行基盤(operational substrate)として使うという発想だ。
LLMs are reasoning engines, but Valkey is the operational substrate that turns them into coordinated systems.
(LLMは推論エンジンだが、Valkeyはそれらを協調システムに変えるための実行基盤だ)
エージェントの状態をプロセスメモリに持たず、全部Valkeyのキースペースに書き出す。
すると次のような副作用が得られる。
- 実行中の状態を外から覗ける(キースペースを
KEYS/JSON.GETで可視化できる) - 再起動してもセッションが落ちない
- 別プロセス・別言語のエージェントを後から足しても同じストアを見られる
- デバッグ時に「いま何が起きたか」をイベントとストアの両側で追える
この設計自体は新しくなく、actor model や event sourcing の古い知見の焼き直しに近い。
目新しいのは、マルチエージェントLLMシステムでそれを実演して見せた点と、Valkeyという具体的な基盤を選んだ点だ。
3エージェントの構成
NeuroValkey Agentsは次の3エージェントを直列に並べる。
| 順番 | エージェント | 役割 |
|---|---|---|
| 1 | Researcher | 与えられたトピックについて事実を生成し、埋め込みと一緒にValkeyに保存する |
| 2 | Writer | Valkeyに溜まった事実をKNN検索で取り出し、ドラフトを書く |
| 3 | Editor | ドラフトを採点し、リファインして最終版を書き出す |
処理は Researcher → Writer → Editor の順に流れるが、呼び出し関係は直接ではなくValkeyのPub/Subチャネル経由だ。
Researcherが終わるとチャネルにイベントを投げ、Writerがそれを購読していて起動する。
同じ構造をEditorにも使う。
flowchart LR
U[ユーザー入力<br/>トピック] --> R[Researcher Agent]
R -->|fact:uuid ハッシュに保存<br/>text / topic / agent / embedding| V[(Valkey)]
R -->|Pub/Sub: researcher_done| W[Writer Agent]
W -->|FT.SEARCH KNN @embedding| V
W -->|JSON.SET draft| V
W -->|Pub/Sub: writer_done| E[Editor Agent]
E -->|JSON.GET draft / SET final| V
E -->|Pub/Sub: editor_done| D[Dashboard<br/>/api/state /api/logs]
ここでValkeyは4つの役割を同時にこなしている。
| 役割 | 使いどころ |
|---|---|
| Pub/Sub | エージェント間のイベント駆動なオーケストレーション |
| Hash | ベクトル付き事実ストア(fact:<uuid>) |
| JSON | ワークフロー全体のマニフェスト・ドラフト・最終出力 |
| Search Index | 事実に対する意味的な検索 |
別々のミドルウェアを組み合わせてもできるが、全部1プロセスで完結するのが肝だ。
Researcherが事実を書き込む
Researcherの仕事は単純で、トピックを受け取ってLLMに「このトピックに関する事実を列挙して」と投げ、返ってきたそれぞれのテキストをOpenAI APIで埋め込みベクトルにしてValkeyに書き込む。
埋め込みはFloat32のバイナリBufferに変換してからHSETする。
キー形式は fact:<uuid> で、フィールドは text / topic / agent / embedding の4つ。
embedding だけがバイナリで、残りは普通の文字列タグだ。
ここで効いてくるのが Valkey Search(FT.* コマンド)のインデックス定義。
FT.CREATE facts_idx ON HASH
PREFIX 1 "fact:"
SCHEMA
topic TAG
agent TAG
embedding VECTOR FLAT 6
TYPE FLOAT32
DIM <embeddingDim>
DISTANCE_METRIC COSINE
PREFIX 1 "fact:" で「fact: で始まるハッシュを自動的にインデックス対象にする」と宣言している。
以降はHSETするだけでインデックスにも載る。
VECTOR FLAT 6 の 6 はオプションパラメータが6個続くことを示す単なる長さ指定で、アルゴリズムは FLAT(総当たり)を選んでいる。
サンプルなので正確性優先、という判断だ。本番でコレクションが大きくなったら HNSW に切り替えるのが定石。
topic と agent を TAG として持たせているのは、「特定トピックの中だけでKNN検索する」のような絞り込みを効かせるため。
ここを後述のWriterがプレフィルタに使う。
WriterがKNN検索でドラフトを書く
Writerは与えられたお題についてクエリ文を作り、それを埋め込んでValkey SearchにKNNクエリを投げる。
FT.SEARCH facts_idx
"*=>[KNN 8 @embedding $vec AS score]"
PARAMS 2 vec <float32_bytes>
DIALECT 2
*=>[KNN 8 ...] という独特の書き方は、「全件の中から @embedding に対するKNNを8件」を意味する。
* の部分を @topic:{ai-agents} のように差し替えると、TAGフィルタで絞ってからKNNを回せる。
DIALECT 2 は比較的新しいクエリ構文を有効にするおまじない。
KNNは返却されるのが主にスコアとキー名なので、続けて HMGET fact:<id> text topic agent で本文を取りにいく、という2段構えになる。
これは Valkey Search / RediSearch の典型的な使い方で、ベクトル検索はインデックス上で、本体データは元のハッシュから、という分業だ。
取ってきた事実をプロンプトに差し込んでLLMに要約を書かせ、出てきたドラフトは JSON.SET swarm:<runId>:draft $ "<json>" で書き込む。
これで Editor 側からは JSON.GET でそのまま取れる。
著者は「ベクトル検索に関しては高レベルSDKのヘルパーを使わず、生の commandClient.call() で FT.* を投げている」と書いている。
表面のラッパーが変わっても動き続ける、互換性重視の選択だ。
Editorが採点して最終化する
Editorはドラフトを JSON.GET swarm:<runId>:draft で読み出し、自前の採点プロンプトに渡す。
返ってくるのはスコアとリファイン案で、必要なら追加で書き直す。
最後に JSON.SET swarm:<runId>:final で最終出力を書き込み、チャネルに editor_done を投げてワークフローを閉じる。
マニフェスト自体も別のJSONキー(swarm:<runId>:manifest)に保持されていて、どのトピックで、どの事実を使って、どういうスコアで、最終何が出たか、を1カ所で追える。
Dashboardはこのマニフェストをポーリングしているだけで、特別な中継サーバーは介在しない。
キースペースをそのままダッシュボードに見せる
NeuroValkey Agentsには http://localhost:3055 のダッシュボードが同梱されている。
ここが面白いのは、実装が基本的に /api/state と /api/logs を定期ポーリングするだけという点だ。
- キースペースマップ(
swarm:*,fact:*)を列挙 - 各キーの生JSONをインスペクト
- ベクトル事実の埋め込みバイトをそのまま表示
FT.INFOでインデックスのテレメトリを見る- イベントタイムラインとログを横並びに相関させる
SSEやWebSocketではなくHTTPポーリングにしているのはあえての選択で、ローカルデモの信頼性を優先した、とある。
遅延は上がるが、インフラ前提をゼロに抑えられる。
小さなトレードオフだが、「観測可能であること自体」が最優先というスタンスが読み取れる。
状態をプロセスに持たない設計のメリットは、ここが一番わかりやすく効く。
エージェントがいま何を読んで、何を書いて、どんなイベントを投げたかを、そのままキースペースの形で見せられる。
LLMアプリケーションはブラックボックス化しやすいので、この「生データをUIにそのまま垂れ流す」姿勢はデバッグビリティに直結する。
設計トレードオフ
実装の要所を表にまとめると次のようになる。
| 観点 | 選択 | 得たもの | 払ったコスト |
|---|---|---|---|
| 更新方式 | HTTPポーリング | ゼロインフラ・再現性 | 更新レイテンシが上がる |
| データモデル | Hash + JSON の併用 | アクセスパターンごとに最適化 | キーの二重管理 |
| ベクトル検索 | 生の FT.* コマンド | 明示性・互換性 | ボイラープレートが増える |
| ベクトルアルゴリズム | FLAT(総当たり) | 正確・実装単純 | 件数が増えたら遅くなる |
| エージェント協調 | Pub/Sub | 明示的で追跡しやすい | リトライやDLQは別途必要 |
「全部Valkeyで賄う」と決めてしまうと、データ構造の選び方だけ考えればよくなる。
Redis Stack系に慣れていれば学習コストも低い。
マネージド型との比較
冒頭で触れた Cloudflare Agent Memory と比べると立ち位置の違いがはっきりする。
| 観点 | Cloudflare Agent Memory | NeuroValkey Agents |
|---|---|---|
| ホスティング | マネージド(Durable Objects + Vectorize + Workers AI) | セルフホスト(Docker上のValkey 1プロセス) |
| ライセンス | Cloudflareプラットフォーム専用 | Valkey(BSD)+アプリコード |
| 記憶モデル | Facts / Events / Instructions / Tasks に分類 | 事実(fact)ハッシュ1種類 |
| 検索 | 5チャネル並列+RRF(HyDE込み) | 単一KNN+TAGフィルタ |
| 状態管理 | Durable Objectsのトランザクション | JSONキーのスナップショット |
| 目的 | 本番で動くエージェント記憶層 | 設計パターンを見せる実装サンプル |
NeuroValkey Agentsは「マネージド記憶層を使わずにどこまでやれるか」の参照実装で、Cloudflare Agent Memoryのような成熟した機能セットを目指したものではない。
逆に言えば、Agent Memoryの内部で起きていることを小さく切り出したミニチュア版としても読める。
Cloudflareのパイプラインの中にある「SHA-256で冪等に重複排除」や「宣言文と疑問文の埋め込みギャップをクエリ変換で埋める」のような最適化が、NeuroValkeyにはまだ無い。
そこを自分で書くかどうかが、セルフホストに振ったときの分水嶺になる。
気になるところ
記事とリポジトリを読んで引っかかった点をいくつか。
| 気になる点 | 中身 |
|---|---|
| ベンチマーク数値がない | スループット・レイテンシ・KNN精度いずれも公開ベンチは無い。著者自身もfuture workとして「throughput/latency benchmarking mode」を挙げている |
| 単一Valkey前提 | クラスタリングやレプリカ切り替えには踏み込んでおらず、単一ノードがSPOFになる |
| リトライ・DLQが未実装 | Pub/Subで発火したあと失敗したエージェントを拾い直す仕組みが無い。Streamsに切り替えてコンシューマグループにすれば対応できるが、現状は素のチャネル |
| 事実分類が粗い | Cloudflareで言うところのFacts/Events/Instructions/Tasksに相当する区別が無く、すべて同じ fact:* ハッシュに入る |
| 埋め込みモデル固定 | OpenAI APIで決め打ちされていて、ローカルモデル(bge-m3等)への差し替えは自分で書く必要がある |
これらは著者がREADMEでも素直に認めていて、「観測可能性とシンプルさを優先した参照実装」という割り切りで通している。