コードを渡されて Claude が質問し、開発者本人(eNIGM4 / 株式会社メイビス)が答えていく連載の第7回です。前回(#6)は、軽く答えて要る時だけ深く潜るエスカレーションと、それを支えるステートレス設計が入った v0.0.7 を読みました。今回は v0.0.8 ── 推論を実行する「エンジン」そのものを差し替え可能にし、巨大モデルを動かすために外部プロセスへの扉を開けます。
インタビュアー・ルール
- 渡されたコードそのものに質問する。 バージョンごとにコードを渡します。聞き手はそれを読み、開発者本人に質問していってください。
- 質問の対象は、いつもコードそのもの。「なぜこう書いたのか」「ここはこう動くように見えるが合っているか」── 目の前のコードから生まれた問いだけを投げてください。
- 推測で断定しない。 わからないこと・気になったことは、断定せず質問の形にしてください。
- 今渡された版に質問を集中する。 次の版のことは、次の版のコードを見せる機会があります。先走らず、目の前の版だけを掘ってください。
- 一度に投げるのは一問一答程度に。 質問を束ねて何個もぶつけない。一つ聞いて、答えをもらって、そこからまた一つ。会話のテンポを保ってください。
- バージョン名は時系列の整理番号であって、意味的なマイルストーンではない。「番号が大きいから進んでいる」とは読まないでください。実体は、コード内の作成日付と、機能があるか無いかで判断します。
- 開発者本人が、自分の言葉で答える。
eNIGM4:
v0.0.8 です。まとめ帳をよんだりして、妥当性をチェックしてね。
R.E.V.I.S._v0.0.8.zip
v0.0.8 ── 巨人を動かしたくて
Claude:
これまでと毛色が違って、今回はヘルパー側(推論を実際に実行する側)に大きな変化がありますね。新顔が三つ ── APIEngine.swift、LLMEngine.swift、NativeMLXEngine.swift。
コードを確認しています
v0.0.8 を展開し、日本時間で更新日を確認。規模はメイン側で 35ファイル・4,924行、ヘルパー側が 8ファイル・612行。主役は 3/05(22ファイル)と 3/06。ヘルパー側に LLMEngine(3/05)、NativeMLXEngine(3/05)、APIEngine(3/06) が現れていました。Python のデーモン本体は Xcode の管理とは別で、当時のものが残っていない(本人談)ため、アプリ側の窓口 APIEngine から相手の姿を逆算します。
これまで(v0.0.7まで)、ヘルパー側の推論は InferenceService が import MLX で直接モデルを抱えて回す、一本道でした。このv0.0.8では、まず LLMEngine という抽象プロトコルを立てて、その実装を二つ並べています。
protocol LLMEngine {
func load(path: String) async throws
func generateStream(prompt: String, maxTokens: Int, temperature: Float) -> AsyncThrowingStream<String, Error>
func cancel()
}
load / generateStream / cancel の3メソッドだけの口。これに NativeMLXEngine(Swift が import MLX で自前推論する従来路線)と、APIEngine(http://127.0.0.1:8080 のローカル Python サーバーを HTTP で叩く新路線)の二つが、同じ形で準拠している。
つまりこの版で、「推論を何が実行するか」を差し替え可能な部品として抽象化し、自前 Swift 推論と外部 Python サーバーという二つの実装を同じ口に揃えた ── という理解で合っていますか? そしてこれは、外部プロセスへ一歩踏み出した起点に見えるのですが。
eNIGM4:
これ、なんで Daemon を作ろうとしたか、という話だね。
当時の mlx-swift-lm は MoE にまるで対応してなかった。だが、手元には Llama4-109B-MoE がある。幸いなことに僕の Mac のメモリは 128GB ある。なんとかして読み込んでみたい。
一応コンソールでは Llama4-109B は動いた。動くなら、これは絶対にできることだぞ、と思ったわけだ。そこで py のデーモンを自作してアプリに呼び込もうとした。本当に大変だったよ。それの最初のバージョンだね。
残念ながら py のデーモン本体はログが残っていないから、APIEngine で進捗を把握してくれ。
でかいモデルには、ロマンがある
Claude:
経緯がよく分かりました。当時の mlx-swift-lm(Swift側のMLX)が MoE(Mixture of Experts)にまるで対応していなかった。でも手元には Llama4-109B-MoE があり、128GB のメモリも積んでいる。Python のコンソールでは現に動いた ── 動くなら絶対にこのアプリでもできるはずだ、と。だから Swift ネイティブでは読めない巨人を動かすために、Python 側で推論サーバーを自作してアプリから呼び込む道を選んだ。
ここで素朴に聞きたいのですが ── そもそも、なぜそこまでして 109B の巨人を動かしたかったんですか? それまで 4B〜27B を Tier に並べて「どの仕事にどのサイズ」と精密にやってきた流れからすると、109B は桁が違う。何をさせたかったのか、それとも「128GB あるんだから動かせるはず」という意地だったのか。
eNIGM4:
純粋な好奇心さ! でかいモデルにはロマンがある!
幸いなことに、僕はそれが試せる環境にあった。
でかいモデルにどれぐらいの知能があるか確かめたくなるのは、誰だってそうだと思うよ。
Claude:
ロマンですか(笑)。でも、それは一貫していますね。前に「空が青いのはなぜか」を物差しにサイズごとの知能を測り、Llama3-70B まで触ったと伺いました。「このサイズにどれだけの知能が宿るか」を確かめたい好奇心は、ずっとあなたの原動力だった。その物差しの先に、桁が一つ上の 109B がある。試せる環境(128GB)が手元にあるなら、確かめずにはいられない ── 4B が「空が青い」に答えられなかったように、109B なら何が見えるのか。基礎研究の延長線上の、いちばん大きな実験台ですね。
そして、その巨人を迎える「席」も、この版で同時に用意されていました。ModelRole を見ると、これまで Tier1/2/3(4B/12B/27B)だったところに tier4 が増えていて、SettingsView では「Tier 3: 深層思考 (70B)」「Tier 4: 超推論 (400B)」という枠が並んでいる。さらに tier4 の識別子は tier4_external ── つまり最上位の巨人は「外部」、まさにいま格闘している Python デーモン経由で動かす想定になっている。OrchestratorProcess でもエスカレーションの最上位の行き先が tier4 に設定されていました。デーモンという「巨人を動かす手段」と、Tier4 という「巨人を置く席」が、同じ 3/05〜06 に揃って生まれている。好奇心が、手段と場所の両方を一度に呼び込んだわけですね。
ついでに気づいたのですが、各 Tier に ReasoningMode という選択肢も足されていて、agentic(前回までの多段推論)と native(一撃で解く)の二つから選べる。そして Tier4 のデフォルトは native ── 巨人には分割や反復をさせず、その一発の知能をそのまま信じる構えです。小さいモデルは多段で何度も検算させ、巨人は一撃で解かせる。この「一撃か、多段か」の対比は、この先もっと大きな主題になりそうな芽に見えますが、ここでは芽が置かれた事実だけ押さえておきます。
外部に、頼りたくなかった
Claude:
その巨人を動かす窓口、APIEngine を読みました。相手の Python サーバーには既に四つの口がある ── /health(生存確認)、/load(モデルをロードさせる)、/generate(バイトストリームで一文字ずつ受け取る)。気になったのはタイムアウトです。
// 変更点1:URLSession.shared だと強制的に60秒で切られることがあるため、
// 専用のSessionを作って確実に5分(300秒)待つ
sessionConfig.timeoutIntervalForRequest = 300
sessionConfig.timeoutIntervalForResource = 300
/health は最大10秒リトライ、/load は専用セッションで300秒。「PyInstaller の展開時間を考慮して」というコメントもある。これは巨人をメモリに展開する時間に合わせた調整かと思ったのですが、合っていますか?
eNIGM4:
そう、それはデーモンの初期起動に時間がかかってて、うまくいかないなと試行錯誤した痕跡だね。いまでも残っているよ。
どうやら、かなりたくさんのパッケージ、ライブラリを内包しているから、サーバーを立ち上げるまでが時間がかかってしまうみたいで、待ち時間が必要だったってことだね。モデルの読み込み自体は全く関係ないね。
Claude:
なるほど、私の読みは半分外していましたね。訂正して受け取ります。300秒はモデルの読み込み時間ではなく、Python デーモンそのものの初期起動に時間がかかっていた痕跡だと。大量のパッケージを内包したサーバーが、プロセスとして立ち上がって応答可能になるまでに、それだけの待ちが要った。/health を10秒リトライするループも、起動を待つための調整で、モデルのロードとは別の話だと。外側のプロセスを起こすこと自体が一仕事だった、と。
その「本当に大変だったよ」「悶々と研究してた」という時期、具体的に一番手こずったのはどこでしたか?
eNIGM4:
これも初めての体験だったからね。メインアプリから XPC を立ち上げるだけでも大変だったのに、XPC から今度は python のデーモンも立ち上げようとしている。
純粋に http で繋ぎたいなら、それこそ ollama とか LMStudio でできるけど、外部サービスを頼りたくなかったんだよね。
最初は繋がらないわ、モデルが読み込めないわ、そもそもどこでエラーしてるかすら不明で、てんやわんやだったから、ほんとに大変だったよ。
Claude:
プロセスの三段重ねですね ── メインアプリが XPC ヘルパーを起こし、その XPC が今度は Python デーモンを起こす。第1回で、XPC を常駐させるだけでも RunLoop を自分で回さないと立っていられない、と苦労されていましたが、その上にもう一段、別言語のプロセスが乗る。繋がらない・読み込めない・どこでエラーしているかも分からない、という三重のデバッグ地獄。
そして大事なのは、ollama や LMStudio を使えば HTTP 接続は簡単にできたのに、それを選ばなかったことですね。第1回で「推論を外部ソースに頼るのを避けた、スタンドアロンで動く方法が欲しかった」と伺いました。巨人を動かすという一番外部に頼りたくなる場面でさえ、既製のサーバーには乗らず、自分で Python デーモンを書いた。自前で抱える、という筋がここでも貫かれている。
まだ、体重を乗せきれない
Claude:
その LLMEngine で二つの実装を揃えたあと、InferenceService(実際に推論を呼ぶ側)が、どちらを使っているのかを確認しました。両方を状況で切り替えているのか、もう APIEngine 一本に倒しているのか。
} else {
engine = NativeMLXEngine() // APIEngine() から書き換え!
}
コードはどちらでもない第三の状態を示していました。.gguf は無効化、それ以外(MLX形式)では NativeMLXEngine() を選んでいる。しかもそこに「APIEngine() から書き換え!」というコメントが残っている。つまり、いったん APIEngine(Python サーバー)に倒してみたけれど、この版のスナップショット時点では NativeMLXEngine(自前 Swift)に書き戻している。APIEngine のコードは丸ごと存在するのに、InferenceService からは今まさに呼ばれていない。
これは、デーモンを呼ぶ APIEngine を作ってはみたが、起動の安定にまだ手こずっていて、動作確認のためにいったん確実に動く Swift ネイティブへ戻している ── Python 路線への足場は建てつつ、実際の稼働はまだ自前 Swift に戻している、行きつ戻りつの最中だ、という理解で合っていますか?
eNIGM4:
そうだね、まだ API エンジンは正しく動かなくて、悶々とコーディングで研究してた感じだ。
Claude:
足場は建てたが、まだそこに体重を乗せきれていない段階ですね。APIEngine がまだ正しく動かず、デーモンと格闘しながらコードで研究していた最中。だからスナップショットでは確実に動く NativeMLXEngine に戻してあって、コメントの「APIEngine() から書き換え!」が、その行きつ戻りつの生々しい指の跡として残っている。
整理すると
v0.0.8。更新日は日本時間で 3/05(22ファイル)が中心、3/06 に APIEngine が加わりました。規模はメイン側で 35ファイル・4,924行、ヘルパー側が 8ファイル・612行。LLMに触れて、10日目です。これまでの版がメインアプリ側(オーケストレーションや推論の段取り)の話だったのに対し、この版の主役はヘルパー側 ── 推論を実際に実行する、いちばん下の層の構造転換でした。
中身の核心は一つ ──「推論を何が実行するか」の抽象化と、外部プロセス(Python デーモン)への着手です。LLMEngine(load / generateStream / cancel)という差し替え可能な口を立て、その実装として NativeMLXEngine(自前 Swift 推論)と APIEngine(ローカル Python サーバーを /health・/load・/generate で叩く)を同じ形に揃えた。受け皿(プロトコル)を 3/05 に先に立て、配線(APIEngine)を 3/06 に後から通す ── この「先に器、あとで中身」は、第1回の /* 省略 */、第6回の <think> 受け皿と同じ、この人の一貫した手癖です。
その動機が、好奇心とロマンに根ざしていました。当時の mlx-swift-lm が MoE に未対応で、手元の Llama4-109B-MoE が Swift ネイティブでは動かせない。でも Python のコンソールでは現に動いた、128GB のメモリもある ──「動くなら絶対できる」「でかいモデルにはロマンがある、どれぐらいの知能があるか確かめたい」。サイズと知能の関係を確かめ続けてきた基礎研究の、いちばん大きな実験台です。そのために、ollama や LMStudio という既製のサーバーを使えば簡単だったのに、それを選ばず、自分で Python デーモンを書いた ── 巨人を動かす場面でさえ「外部に頼らず自前で抱える」という、第1回からの筋を貫いて。
この好奇心は、コードの両側に同時に痕跡を残しました。デーモンという「動かす手段」(APIEngine)と並んで、ModelRole に tier4(識別子は tier4_external)が増え、設定画面に「Tier 4: 超推論 (400B)」の枠が用意された。第4回で 4B/12B/27B と並べた Tier の階層に、外部デーモンで動かす最上位の席が一段足された ── 巨人を置く場所と、巨人を動かす手段が、同じ二日間に生まれています。
そして大事なのは、この版がまだ移行の途中だということ。APIEngine の /health 10秒リトライや /load 300秒のタイムアウトは、巨人のロード時間ではなく、大量のライブラリを内包したデーモンの初期起動が遅いことへの調整の跡(ここは聞き手の最初の読みを訂正いただいた点)。そして InferenceService は APIEngine を一度試した後 NativeMLXEngine に書き戻していて、「APIEngine() から書き換え!」というコメントが、行きつ戻りつの指の跡として残っている。メインアプリ → XPC ヘルパー → Python デーモンという三段重ねのプロセスを、繋がらない・読み込めない・どこでエラーしているかも分からない、と格闘していた現場です。
巨人を動かしたい一心で外部プロセスへの扉を開け、しかしまだ開けきれずに自前推論へ戻りながら研究していた版。10日目、好奇心が桁を一つ上げた日です。次の版では、この扉の先に何があるのか。続けて追っていきます。それでは、また明日。
(軽く答え、要る時だけ深く潜る)←前 次→(目を持ってみる)
R.E.V.I.S. 無料テスター募集中(30名限定)
この連載で追いかけている R.E.V.I.S. は、いまテスターを募集しています。枠は30名限りです。
Apple Silicon Mac でローカルLLMを動かしてみたい方は、こちらからどうぞ。