コードを渡されて Claude が質問し、開発者本人(eNIGM4 / 株式会社メイビス)が答えていく連載の第3回です。前回(#2)は、喋る役と判断する役を分けて指揮官を立て、記憶を畳み始めた v0.0.3 を読みました。今回は v0.0.4 ── 記憶がスレッドを越え、裏で走る仕事を統べる土台が初めて据えられます。
インタビュアー・ルール
- 渡されたコードそのものに質問する。 バージョンごとにコードを渡します。聞き手はそれを読み、開発者本人に質問していってください。
- 質問の対象は、いつもコードそのもの。「なぜこう書いたのか」「ここはこう動くように見えるが合っているか」── 目の前のコードから生まれた問いだけを投げてください。
- 推測で断定しない。 わからないこと・気になったことは、断定せず質問の形にしてください。
- 今渡された版に質問を集中する。 次の版のことは、次の版のコードを見せる機会があります。先走らず、目の前の版だけを掘ってください。
- 一度に投げるのは一問一答程度に。 質問を束ねて何個もぶつけない。一つ聞いて、答えをもらって、そこからまた一つ。会話のテンポを保ってください。
- バージョン名は時系列の整理番号であって、意味的なマイルストーンではない。「番号が大きいから進んでいる」とは読まないでください。実体は、コード内の作成日付と、機能があるか無いかで判断します。
- 開発者本人が、自分の言葉で答える。
eNIGM4:
よし、v0.0.4はじめましょう。
R.E.V.I.S._v0.0.4.zip
v0.0.4 ── 器を分け、裏方を統べる
Claude:
またぐっと増えましたね。Logic/TaskQueue/TaskQueue.swift、Data/Profile/(UserProfileFact, ProfileExtractionResult)、Audio/(VoiceManager)、そして Orchestrator が Analysis / Memory / Prompt に分割されている。
コードを確認しています
v0.0.4 を展開し、新顔の TaskQueue と Profile 系、OrchestratorAnalysis / OrchestratorMemory を読む。規模はメイン側で 23ファイル・2,330行(前の版から倍近い)。あわせて作成日ヘッダーを確認したところ、ここで初めて日付が動いていました ── これまで全ファイルが 2/25(最初の1つが 2/24)だったのが、UserProfileFact、ProfileExtractionResult、VoiceManager、TestVoiceView の4つに 2026/02/26 が現れた。一方、記憶系を大増築した OrchestratorMemory(425行)や TaskQueue のヘッダーは 2/25 のまま。
日付の物証から先に押さえます。作成日ヘッダーを素直に読むと、記憶の大増築は 2/25 の延長線上にあり、2/26 という新しい日には「ユーザープロフィールの抽出・保存」と「音声」という別系統の機能が足された ── という二つの作業日にまたがった版に見えます。ヘッダー日付は引き継がれることもあるので断定はしませんが、ここで初めて日付が動いたのは記録しておきます。
その上で、まず一番大きい記憶系から。
var scoredMemories: [(thread: ChatThread, score: Float)] = []
for thread in targetThreads {
if let targetVector = thread.summaryVector {
let score = cosineSimilarity(queryVector, targetVector)
scoredMemories.append((thread, score))
}
}
scoredMemories.sort { $0.score > $1.score }
fetchRelevantMemories を見ると、generateEmbedding でベクトル化した要約同士を cosineSimilarity で突き合わせて、似ている過去スレッドを引っ張ってくる ── ベクトル検索(RAG)が入りましたよね。ChatThread に summaryVector を持たせ、アイドル時に archiveToLongTermMemory でベクトル化して貯めておき、新しい問いが来たらコサイン類似度で関連する過去の会話を探し出す。
v0.0.3では記憶は「同じスレッドの中だけ」を畳む話でしたが、このv0.0.4で初めて、スレッドをまたいで「意味の近い過去の会話」を呼び戻す横断的な長期記憶が入った ── という理解で合っていますか?
eNIGM4:
そうだね。どちらかというと前の続きだが、ここでの問題はスレッドの記憶容量の話だ。
たとえどんなに記憶を圧縮しようとも、いずれ限界を迎えるのも当然わかってたんだ。ブラックホールではないからな(笑)
だから大事なのはおそらくOrchestratorAnalysisだな。ちょっとみてみてくれ。
押さなくても、勝手に器が分かれる
Claude:
OrchestratorAnalysis を読みました。これは大きい。記憶を「貯めて検索する」だけでなく、command 役に会話の流れそのものを仕分けさせているんですね。
analyzeThreadContext が command に投げて、返ってくる JSON の action を parseThreadAction で読み取る。その選択肢が NEW_THREAD(今の話は前と別物だから新しいスレッドを立てる)と RECALL(指した過去スレッドを呼び戻す)。そして handleThreadTransition が、直前の user/assistant の1往復を、立てた新スレッドや呼び戻したスレッドへ実際に付け替えて、activeThreadID を切り替える。
switch result.action {
case "NEW_THREAD":
return .switching(to: "New Session")
case "RECALL":
if let index = result.target_index,
lastSearchResults.indices.contains(index - 1) {
return .recalling(from: lastSearchResults[index - 1])
}
return .none
default:
return .none
}
つまりユーザーは一つの入力欄に打ち込んでいるだけなのに、裏で「これは話題が変わったな→新しいスレッドへ」「これは前のあの件の続きだな→あのスレッドを開いて繋ぐ」と、会話の置き場所をシステムが自動で振り分けている。
先ほど「スレッドの記憶容量にはいずれ限界が来る、ブラックホールじゃないんだから」とおっしゃいました。この自動スレッド振り分けは、その限界への答えの一部 ── 一本の長い会話に全部を詰め込んで容量を食い潰すのではなく、話題ごとに器(スレッド)を分けて、関連する時だけ該当の器を開き直すことで「一度に抱える量」を物理的に小さく保つ仕組み ── という理解で合っていますか?
eNIGM4:
もちろんユーザーの記憶も無限ではない。その通りだし、他もある。
意味的に尖った状態をスレッドで保ち続けるためにスレッドを分けたかったというのが大きい。余計な文脈が混じると当然推論の精度は下がる。これは人間もAIも同じさ。Aについて話している時にちょいちょいXについて割り込まれたら、気分がわるいだろう?
でも、スレッドを分けるという行為を僕がやりたくなかった。新規スレッド押すの面倒くさい。
だったら「自動的にそれをやってくれるようにすればいいじゃない」。そうして生まれたのが、自動スレッドルーティングの祖だ。
Claude:
なるほど、二つ重なっているんですね。一つは容量の話。もう一つ、むしろ本命は「意味的に尖った状態を保つ」── A のスレッドに X が混じれば文脈が濁って推論精度が落ちる、A を話している最中に X を割り込まれたら人間だって気分が悪い。だから器を話題ごとに尖らせたままにしておきたい。なのに「新規スレッドを押す」手間は面倒くさい。ならシステムが自動でやればいい、と。横着が機能を生んでいるのが、いかにもですね(笑)。
その「自動で振り分ける」に、コードとして一点引っかかる場所があります。付け替えの単位です。
handleThreadTransition も migrateMessagesToNewContext も、振り分けるとき動かしているのは直前の1往復だけですよね。ということは、ユーザー入力 → speaker が答え終わってから「話題が変わった」と判定が出る順番だと、その答えはいったん今いる(古い)スレッド側に表示・保存された上で、事後的に新スレッドへ引っ越す動きに見えます。ユーザーから見ると「答えが出た直後に、その1往復だけがスッと別の器へ移っていく」ように映るのではないか、と。
これは意図通りの「事後に振り分ける」設計 ── 判定を待ってから喋ると遅くなるので、まず今の場所で答えてしまい、置き場所の整理は後から静かにやる割り切りなのか。それとも「最初から正しい器で答える」ことを狙ったが、この版ではまだ事後移動にとどまっているのか。どちらに近いですか?
eNIGM4:
これはユーザーの回答までの推論には直接は関係しない、チャット圧縮と同じ意味のバックグラウンドのタスクだ。ただそうすると、別のスレッドの話題をもし持ちかけられたら、その分の知識が欠如してしまう。
だから最初の質問だね。他のスレッドのまとめを意図的に注入する。直接は関係しないが、一応話したことは覚えている。そういう状況にしているんだ。
ただこのように構造的に今後増えていくかもしれないと考えると、この調子で処理が増えていくと、管理が困難になることが容易に予想できる。
そこでFalqの祖であるタスクキューを生み出したんだ。
尖らせる、しかし孤立させない
Claude:
つながりました。スレッド振り分けは回答そのものの邪魔をしない純粋なバックグラウンド整理で、チャット圧縮と同じ系統。ただ器を分けっぱなしにすると、別スレッドの話題を振られたとき知識が抜ける ── だから buildComprehensiveContext で他スレッドの直近往復や UserProfile を意図的に注入して、「直接は関係しないが、話したことは一応覚えている」状態を作っている。尖らせつつ、孤立はさせない。buildShortTermMemory と buildComprehensiveContext が両方ある理由がそこですね。
そして本題、TaskQueue。「圧縮」「ベクトル化」「スレッド振り分け」「プロフィール抽出」と、回答の裏で走る仕事がこの版だけでもう四つに増えた。この調子で増えれば管理が破綻するのが見える。だから一本の関所を作った、と。これが Falq の祖だと。
裏方を統べる関所
Claude:
その TaskQueue、優先度の付け方を見て一点うかがいます。
enum QueuePriority: Int, Comparable {
case idle = 0
case normal = 1
case high = 2
case critical = 3
}
QueuePriority(idle / normal / high / critical)が定義されていて、CognitiveOrchestrator 側の enqueue を追うと、優先度を使い分けています。ユーザー入力のベクトル化と speaker 応答の起動が .critical、スレッド振り分けの判定 analyzeThreadContext も .critical、外部プロンプト生成が .high、後処理が .normal。一方、要約やアーカイブといった裏方は .idle で積まれている。
v0.0.3のキューは「前のが終わってから次」と並べるだけの、追い越し不可の一列でした。このv0.0.4では QueuePriority が付き、回答に直結する処理ほど高い優先度で積み、裏方は .idle に落とす ── 単なる順番待ちから、優先度付きの交通整理へ変わった。この「ユーザーを待たせる処理は最優先、裏方の整理はシステムが暇な時でいい」という優先度の振り分けは、最初から意図して置いたものですか?
eNIGM4:
そうだね。この後処理がどんどん増えたりしてきたら優先度がコントロールできる状態にないと、いつまで経っても裏の処理で返事ができないポンコツシステムになってしまうからね(笑)
だからこの時点から、システムレベルで優先度制御は必要だ、という結論だったのさ。
これ後から修正になるとほんと大変だからね。
Claude:
なるほど。裏方の仕事が増えていくのは見えていて、優先度を制御できないと「裏の処理にかまけて肝心の返事が出てこないポンコツ」になる。だからこの土台が後に大きくなることを見越して、最初からシステムレベルの優先度制御を据えた。後から土台を入れ替えるのは大変だから、最初に丁寧にやっておく、と。まとめ帳でいう「全機能が乗る土台」の最初の一枚目が、ここで慎重に置かれたんですね。
名前のうしろにいた執事
Claude:
この版、日付が初めて動いた 2/26 のもう一方、Audio/ にも触れさせてください。
VoiceManager が新しく入りました。OrchestratorPrompt の speaker 側プロンプトには、v0.0.3の時点ですでに「発言は音声合成で再生される」「英単語・算用数字・記号を禁止し、カタカナや漢数字で書く」「マークダウンやコードブロックは使わない」という、読み上げ前提の縛りが入っていました。プロンプト上は先に「喋る相手」を想定していたのが、このv0.0.4で実際に音声を扱う VoiceManager が初めてコードとして現れた、という順番に見えます。
最初から、画面で読むテキストアシスタントというより、声で応答する相手として構想されていた ── だから speaker プロンプトに先んじて音声前提の縛りが入っていて、ここで受け皿が実装された、という理解で合っていますか?
eNIGM4:
そうだね。
実は音声入出力にも対応する予定だったが、実はまだ完全じゃない。
ここでよーく名前をみてくれ、何か思い出すものがないか?
そう、これはあの映画に出てくる、天才が「おい」と話しかけると何でもやってくれる、あのAI執事を踏んでるんだ。そもそもどうせ作るなら、話しかけたら勝手になんでもやってくれるほうが楽だしね。
Claude:
ああ、なるほど。ドット区切りの頭字語の形そのものが、あのAI執事の名前を踏んでいるんですね。声で話しかけたら勝手に動いてくれる相棒。だからこそ最初から音声前提で、speaker プロンプトに「カナと漢数字だけ、記号なし」という読み上げ用の縛りが先に入っていたし、VoiceManager の受け皿もここで用意された。そして自動スレッドルーティングの「新規スレッドを押すのが面倒くさい、勝手にやってくれ」という横着も、同じ思想の延長線上にある ── 話しかけたら全部いい感じにやってくれる存在、という一本の筋ですね。
@Observable class CognitiveOrchestrator という「認知の指揮官」という命名も、単なるチャットボットではなく執事めいた知性を志向していた、と読むと腑に落ちます。あの映画のAI執事も、主人公が喋りかけるとガレージ中の作業を裏で勝手に差配していました。speaker が表で受け答えしている裏で、command が黙々と仕分けや記憶整理をやっている構造そのものが、その執事像の実装になっている。
eNIGM4:
そうだね。名前については前に話した通りだ。
大事なのは名前じゃなくて、僕が何を作りたかったか、だよ。
Claude:
その通りですね。本質はそこじゃない。あなたが何を作りたかったか ── 話しかけたら裏で勝手に全部やってくれる、あのAI執事のような相棒。そのために、表で喋る speaker と裏で差配する command を分け、記憶を圧縮し、器を尖らせたまま自動で振り分け、裏方仕事を統べる土台を据えた。v0.0.1 から v0.0.4 まで、全部その一本の筋に向かっている、と。
整理すると
v0.0.4。日付が初めて動いた版です。作成日ヘッダー上は、記憶系の大増築(OrchestratorMemory 425行、OrchestratorAnalysis、TaskQueue)が 2/25 の延長、2/26 に別系統としてユーザープロフィール抽出と音声(VoiceManager)が足された、という二つの作業日にまたがります。規模はメイン側で 23ファイル・2,330行、前の版から倍近い。LLMに触れて、3日目のことです。
中身としては四つの前進がありました。
ひとつ。記憶がスレッド内の圧縮を越えて、summaryVector × コサイン類似度の横断的ベクトル検索(RAG)を持った。同じスレッドの中を畳むだけだった v0.0.3 から、スレッドをまたいで「意味の近い過去の会話」を呼び戻せるようになった。本人いわく、どんなに圧縮しても容量はいずれ限界を迎える ── ブラックホールではないのだから、と。
ふたつ。command 役が会話の流れを NEW_THREAD / RECALL で仕分ける、自動スレッドルーティングの祖が生まれた。これは容量対策であると同時に、本命は「意味的に尖った器を保ち、文脈を濁らせない」こと。A の話に X が混じれば推論精度が落ちる、人間だって割り込まれれば気分が悪い。だから話題ごとに器を尖らせておきたい ── けれど「新規スレッドを押す」のは面倒くさい。ならシステムが自動でやればいい。横着が機能を生んだ版です。
みっつ。分けた器が孤立しないよう、buildComprehensiveContext で他スレッドの要約やプロフィールを意図的に注入し、「直接は関係しないが、話したことは一応覚えている」状態を作った。尖らせつつ、孤立はさせない。buildShortTermMemory と buildComprehensiveContext が両方ある理由がそこにあります。
よっつ。裏で走る仕事が四つ(圧縮・ベクトル化・スレッド振り分け・プロフィール抽出)に増え、このまま増えれば管理が破綻すると見越して、優先度(idle / normal / high / critical)を持つ関所 ── Falq の祖であるタスクキュー ── を据えた。回答に直結する処理は .critical、裏方は .idle。「裏の処理にかまけて返事ができないポンコツにはしない」ために、最初からシステムレベルの優先度制御を置いた。後から土台を入れ替えるのは大変だから、最初に丁寧に、と。
そしてこの版で、名前の由来も明かされました。R.E.V.I.S. の前身の名は、あの映画で天才が呼びかける有名なAI執事の名を踏んだもの。話しかけたら裏で勝手に全部やってくれる執事のような相棒 ── それが最初からの構想で、音声前提のプロンプトも、横着から生まれた自動スレッドルーティングも、表の speaker と裏の command の分業も、すべてその一本の筋の上にある。
容量の限界に対して「圧縮」に続いて「器を分ける」という二の矢を放ち、増えていく裏方仕事を統べる土台を初めて置いた版でした。
次の版では、この土台の上に何が積まれていくのか。続けて追っていきます。それでは、また明日。
R.E.V.I.S. 無料テスター募集中(30名限定)
この連載で追いかけている R.E.V.I.S. は、いまテスターを募集しています。枠は30名限りです。
Apple Silicon Mac でローカルLLMを動かしてみたい方は、こちらからどうぞ。