跳至主要内容
搜索Ctrl+K
博客
LeoChat:あるエージェントクライアントの過去と現在

| 19 分钟阅读 |

正直なところ、LeoChat と呼ぶのが正確でなくなって久しいです。LeoMCP?LeoAgent?LeoHarness?そのどれの機能にも関わっているのですが、どの名前もしっくりこないので、ずっと変えていません。

オープンソース:cybermanhao/LeoChat(MIT ライセンス)

前身のプロジェクトを始めたころは、コーディングエージェントが登場したばかりでした(しかも中国からは購入しづらく、私はまだ試していませんでした)。今では、その機能はとっくにコーディングエージェントにカバーされています(エージェントクライアントを作っていて、カバーされない人なんているでしょうか?╮(╯▽╰)╭)。役に立たないわけではありません。ただ、一人の力では、精鋭が集まったチームより良いものを作るのがとても難しいのです。

DeepSeek Harness がオープンソースになったとき、このプロジェクトの更新をやめることにしました。

完成度でいえば、LeoChat は dsh には遠く及びません。今後同じようなものを作るなら、dsh のような成熟したプロジェクトから始めて改造すると思います。それでも、これは私がいちばん誇りに思っている作品です。

この記事では、それが何なのか、そして私に何をもたらしてくれたのかを書きます。

LeoChat を試したい方は、このサイトのダウンロードページ leo-releases から最新版をダウンロードしてください(Windows、解凍してすぐ使えます)。テスト用の API キーが必要な場合は、[email protected] までメールをください。


LeoChat を作る前

2025年5月、私は Bilibili で RAG 関連の動画を探していました。すると突然、ホームに1本の動画が表示されたのです。投稿者は、半年ほどフォローしていた知乎の回答者、锦恢(ジンフイ)でした。

動画はこちらです:OpenMCP の紹介。

ちなみに、私はその動画の最初の再生を獲得しました。当時の AI への不安がどれほど深かったかが分かります(笑)。

OpenMCP 紹介動画の最初の再生を獲得

動画は OpenMCP を紹介するもので、MCP Inspector よりずっと使いやすい MCP デバッガです。見終わって、とても興奮しました。MCP という概念を知ったのはこれが初めてで、本当に夢中になってしまいました。アプリケーションにこれほど簡単に AI をつなげられることを知ったのも、これが初めてでした。私はすぐに遊戯王の RAG ナレッジベースを作り、MCP 経由で OpenMCP につなぎました。

遊戯王 RAG ナレッジベースを MCP 経由で OpenMCP に接続(1)遊戯王 RAG ナレッジベースを MCP 経由で OpenMCP に接続(2)

当時勤めていた会社はちょうど AI カスタマーサポートを作っていて、私は MCP + RAG で接続する案を提案しました。独立した Node.js サービスを作り、ほかの同僚がフロントエンドと旧システムとの連携を担当し、データやテキストの検索用に MCP Server を少しずつ追加していくものです(試す機会をくれた会社に感謝しています)。

プロジェクトは順調に進みました。その業務の合間にも、私は OpenMCP にどんどん夢中になっていきました。コードを読んで、似たものを自分でも作ってみたいと思ったのです。

OpenMCP がデバッガだとは分かっていましたが、「MCP につないでツールを呼び出す」というだけで、すでに十分衝撃的でした。

その意味を冷静に考えられるようになったのは、ずっと後のことです(エージェントの軌跡?データの還流?データのフライホイール?2025年3月の锦恢が何を考えていたのか、私たちはまだ知りません)。

その衝撃に突き動かされ、私はすぐに自分のクライアントを作らなければと思いました。そこで、昔ながらの手書きのコーディングで、LeoChat の前身である zz-ai-chat を作りました。

LeoChat の前身、zz-ai-chat

しかし2025年の後半になると、私は Cursor、Copilot、Claude Code を使った自動的なイテレーションを少しずつ始めました。さらに、zz-ai-chat は見せびらかしたい一心で、異なる出力向けに4種類のビルド構成を持っていて、プロジェクトは保守しづらくなっていきました。短い中断(これは別の記事で書きます)を経て、書き直しを始めました。

やり直すとき、私は何が欲しかったのか

書き直しにあたって、私はいくつかのとても絞った目標を立てました。

  1. 滑らかで、信頼できる:マルチラウンドのツール呼び出しもストリーミング出力も滑らかで、反応が速く、エラー処理が行き届いていること。
  2. LLM がインタラクションをどう変えるかを探る:LLM にはテキストを出力してツールを呼ぶだけでなく、UI の操作も引き受けさせたいと考えました。
  3. まず Electron の完成度を優先し、ほかのモードは二の次:Web 優先か Electron 優先かでは、ずっと迷いました。SaaS が生きるのは Web ですが、Electron が与えてくれるローカルの能力は手放せませんでした(Web で同じことをするなら、GCP のようなクラウドサービスとサンドボックスの2つの道があります。いつか書きます)。そこで最終的に Electron を主目標にしました。

LeoChat にできること

まず、MCP 対応のチャットボックスです(これはプロジェクトの最初の位置づけで、今では格好よさが足りなくなりましたが)。さまざまな汎用 MCP ツールに柔軟に接続できます。

組み込みの MCP Server には、ファイルシステム(Filesystem)、ナレッジグラフ型メモリ(Memory)、Web ページ取得(Fetch)、Excel の読み書き、Word の読み書き、ブラウザ自動化(Playwright)、そしてテスト用の Everything があります。

ただ、私はそこに特別なものを加えました。それこそが、LeoChat をほかのクライアントと違うものにしている点です。

AI で UI を操作する

テーマカラーの切り替え、さらにはウィンドウサイズの変更まで。目の覚めるような(文字どおりの意味で)効果です。

AI にカードを描画させることもできます。たとえば名刺、商品カード、ボタン付きのインタラクティブなコンポーネントまで作れます。

AI が描画したカードの例(1)AI が描画したカードの例(2)AI が描画したカードの例(3)AI が描画したカードの例(4)

ここでは、Vercel がオープンソースで公開している stream-ui は使わず、自分で実装しました(いつか詳しく書きます)。

中身を見てみる:LeoChat だけの2つの要素

チャット、ストリーミング出力、ツール呼び出しは、ほかのクライアントにもあります。私が本当に力を入れたのは、上で見せた2つ、UI コマンドとカードシステムです。

UI コマンド:モデルに手を伸ばして画面のボタンを「押させる」

テーマの切り替えやウィンドウのリサイズといった効果の裏には、とても素朴な約束事があります。

組み込みの leochat-mcp は、普通の MCP Server と変わりません。モデルは標準的なツール呼び出しで使います。違うのは、そのツールがファイルを読んだりデータを検索したりせず、小さな JSON を返すことです。

{
"__ui_command__": true,
"command": "update_theme",
"payload": { "themeId": "dark-purple" }
}

フロントエンドは、結果に __ui_command__ が付いているのを見つけると、コマンドレジストリから同名のハンドラを探して実行します。

現在登録されているコマンドは11個です。

  • update_theme、resize_window:テーマカラーの切り替え、Electron ウィンドウのサイズ変更;
  • show_notification、show_confirm:通知や確認ダイアログの表示;
  • open_panel、close_panel:サイドパネルの開閉;
  • set_input、scroll_to_message、copy_to_clipboard、open_url:入力欄の書き換え、特定のメッセージへの移動、コピー、リンクを開く;
  • send_message:ユーザーに代わってメッセージを送る(カードの話でもう一度出てきます)。

この設計の良いところは、双方が互いに依存しないことです。MCP Server はフロントエンドがどんな見た目かを知る必要がなく、フロントエンドもどのモデルがコマンドを出したかを気にしません。新しい機能を足すには、型の表に名前を1つ追加して、ハンドラを1つ書くだけです。

ただし、私は1つルールを決めました。leochat-mcp は、フロントエンド全体を遠隔操作する万能インターフェースに膨らませてはいけないということです。

その道の末路は想像がつきます。境界はどんどん曖昧になり、プロトコルは積み上がり、どのコマンドも壊れやすい約束事になって、最後には誰も保守できなくなります。だから、残すのは「ホストレベル」の小さな操作だけにしました。内容を見せる必要があるもの、ユーザーに選ばせる必要があるものは、すべてカードに任せます。

カードシステム:LeoCard

最初、LeoChat のカードは、モデルの出力からテキストのタグ(<card />)を解析して作っていました。

すぐに、この道は行き止まりだと気づきました。モデルがかっこを1つ間違えただけでカードが壊れます。ボタンを押しても、取り決めたテキスト形式から意図を推測するしかありません。

そこで、構造化されたカードプロトコルとして作り直しました。原則はただ1つ、コマンドはコンテンツではないということです。

  • ui-command は「動作」を担当します:テーマの切り替え、パネルを開くなど、一度きりの副作用;
  • card は「内容」を担当します:商品の概要、レンダリングの進捗、ユーザーに二択を選ばせる画面など、チャット履歴に描かれるもの。

カード1枚の構造は小さく、組み合わせやすくなっています。

{
"id": "card_1",
"kind": "summary",
"title": "ワイヤレスイヤホン",
"tone": "info",
"body": [
{ "type": "image", "image": { "url": "https://…/earbuds.jpg" } },
{ "type": "fields", "fields": [{ "label": "価格", "value": "¥3,980" }] },
{ "type": "badge", "badge": { "text": "在庫あり", "tone": "success" } }
],
"actions": [
{
"id": "buy",
"label": "カートに入れる",
"kind": "primary",
"action": { "type": "ui-command", "command": "send_message",
"payload": { "text": "これをカートに入れて" } }
}
]
}
  • kind は全体のスタイルを決めます:summary、media、list、status、review、error。このフィールドは開かれていて、外部のツールが独自の名前を付けることができ、未知の kind は通常の概要スタイルにフォールバックします;
  • body はブロックの集まりです:テキスト(Markdown 対応)、キーと値のペア、画像1枚、複数画像、プログレスバー、バッジ、区切り線;
  • tone は5種類あります:default、info、success、warning、error;
  • actions はボタンです。クリックは3つの宛先のいずれかに振り分けられます:ui-command(ホストのコマンドを実行)、agent-event(ホストアプリにイベントを送出)、link(リンクを開く)。危険な操作には confirm を付けられ、クリックが有効になる前に確認を出します。

データの流れもシンプルです。ツールが結果を返し、フロントエンドが JSON を解析し、MCP の結果エンベロープを取り出し、LeoCard に正規化して、メッセージ内のコンテンツ項目としてレンダラーに渡します。

新しい伝送経路は要りません。組み込みでもサードパーティでも、MCP Server がこの形式を返しさえすれば、チャットの中にカードを描画できます。

カードと会話をループさせる

カードがおもしろいのは、ボタンが send_message という UI コマンドを使って、ユーザーに代わってモデルにメッセージを送れることです。

こうして、閉じたループができます。

ユーザーが質問 → モデルがツールを呼ぶ → ツールがカードを返す
→ カードにボタンが表示される → ユーザーがクリック
→ send_message が意図をモデルに送る → モデルが次のツールを呼ぶ → …

これを使って、独立したデモ用の Server を2つ作り、リポジトリにも入れてあります。

  • pokemon-quiz:ポケモン当てのミニゲーム。各問題は、画像と4つの選択肢ボタン付きのカードです。選択肢を押すと回答が送信され、「次へ」で先に進みます。
  • ecommerce:購入までの一連の流れ。商品検索、詳細表示、カートに追加、注文、支払い、配送追跡まで、すべてのステップがカードで、ボタンが流れを前に進めます。

pokemon-quiz を作っているとき、おもしろい落とし穴にはまりました。カードが表示されたあと、モデルがどうしても「自問自答」してしまうのです。選択肢を分析したり、答えまで言ってしまったりします。

解決策は、ツールの戻り値に、モデルだけが読む指示(llmInstruction)を加えることでした。「上のカードをそのまま見せて。選択肢は分析せず、ユーザーにボタンをクリックして答えさせて」という指示です。

これで、生成 UI ではUI は人間のためのもので、指示はモデルのためのものであり、両者は別々に設計しなければならないと実感しました。

組み込みの generate_waifu は、いちばん単純なカードの例です。ランダムに生成した画像を数枚含む media カードを返します。

組み込み MCP:すぐ使える道具箱

MCP クライアントでいちばん心が折れるのは、ユーザー自身に Node や Python をインストールさせ、JSON を設定させる段階です。

そのため LeoChat は起動すると、組み込みの MCP Server 群に自動で接続します。パッケージ版ではランタイムのパスまで処理してあるので、Windows なら解凍してすぐ使えます。

Serverできること
LeoChat(自作)UI の操作:テーマ切り替え、ウィンドウのサイズ変更、通知・確認の表示、パネルの開閉、入力欄の書き換え、コピー、リンクを開く。さらに、コマンドを実行する bash ツール(rm、format、shutdown などの危険なコマンドは即拒否)と、アニメ画像を生成してカードで返す generate_waifu
Filesystemローカルファイルの読み書き、ディレクトリ一覧、ファイル検索
Memoryナレッジグラフ型の長期記憶。会話をまたいで、エンティティと関係を覚えます
FetchWeb ページの内容を取得し、モデルが読みやすいテキストに変換
ExcelExcel の読み書き(Windows では実行ファイルとしてパッケージ化されていて、Python は不要)
WordWord 文書の読み書き
Playwrightブラウザ自動化:ページを開く、クリック、フォーム入力、スクリーンショット
EverythingMCP 公式のリファレンス実装。プロトコルの各機能のテストに使います

組み合わせれば、1文で小さなワークフローを実行できます。たとえば「このページを取得して、表を Excel にまとめ、デスクトップに保存して」といった具合です。

組み込み以外にも、MCP の設定画面で任意のサードパーティ Server を追加できます。Stdio と Streamable HTTP のどちらにも対応しています。

法律版は、この土台の上に、法律ナレッジベースの MCP を1つ追加したものです。

そのほかの基盤、かんたんに

どれも、信頼できるエージェントクライアントを作るなら避けて通れない部分で、私が最も時間をかけたところでもありますが、LeoChat だけのものではないので、簡単に触れるだけにします。

  • TaskLoop:システムで最大のファイルです。モデルをラウンドごとに呼び出し、ツールを実行し、結果を返します。自分で処理すべき細部がいくつかあります。ストリーミングされるツール呼び出しの引数は、index ごとに組み立てて完全な JSON に戻す必要があります。1つのタスクは最大50ラウンドで、無限ループを防ぎます。1つのツールの結果が3000文字を超えたら切り詰めます。ラウンドの終了時、一時停止時、エラー時にはチェックポイントを保存し、再開できるようにします。
  • モデルアダプタ層:OpenAI、OpenRouter、Anthropic、DeepSeek、Kimi、Gemini、および任意の互換エンドポイントに対応しています。デフォルトの URL、認証ヘッダー、ストリーミング形式はそれぞれ違いますが、アダプタ層が違いを隠し、統一されたインターフェースだけを公開します。この層には、今では成熟したコミュニティのソリューションがあります。ただ、自分で書いたことで、OpenAI API の各パラメータをずっとよく理解でき、デバッグのときにはどんなドキュメントよりも役立ちました。
  • 信頼性層:指数バックオフと full jitter によるリトライ、試行ごとに独立したタイムアウト、連続して失敗すると「開く」サーキットブレーカー。のちに neo-studio で外部プラットフォームと連携したときの考え方と同じものです。外部システムは必ず壊れる。だから、失敗を前提に設計する。

プロジェクトの運命は、個人の努力だけでなく、歴史の流れにも左右される

最初に私を迷わせたのは OpenClaw でした。非技術系のユーザーの注目を最速で集め、重心は最も煩雑な仕事、つまりゲートウェイに置かれていました。

正直なところ、独立したチャットアプリとしての LeoChat には、今日ではもう商業的な価値はありません。Claude Code や Codex のような汎用エージェントはすでに非常に成熟していて、完成度は一人で作れるものをはるかに超えています。

それでも、いくつかのものを残してくれました。

  1. このブログの AI アシスタント:ここをクリックして話しかけてみてください。または上の「About」ページへどうぞ(モバイルでは三本線のボタンでメニューを開きます)。あのアシスタントは、3日間の空き時間で書いたもので、私の分身のようなものです。3日で書けたのは、その前に LeoChat に2か月を費やして、エージェントループ、ツール呼び出し、カード型 UI といった問題を考え抜いていたからです。 ps:実は、ブログの AI アシスタントにも「目が覚める」ような演出があります。試してみてください。
  2. プロトコル層への理解:MCP のトランスポート、ツール呼び出しのストリーミング組み立て、チェックポイントと復旧。これらは実際に一度作ってみて初めて身についたもので、ドキュメントを読むだけでは代わりになりません。
  3. 信頼できない外部システムを扱う習慣:アダプタ、リトライ、サーキットブレーカー、失敗を前提とした設計。のちに neo-studio の連携層を作ったときも、ほとんど同じ考え方でした。
  4. 法律版:LeoChat の派生ブランチで、RAG を使って中国法の知識ベースを作ったものです。現在は法律の実務家が文書作成に使っています。試したい方は、メールで連絡してください。

おわりに

これは、現時点(2026年9月)で私が作った中で最も複雑で、最も完成度の高い個人プロジェクトで、「AI をアプリケーションに落とし込みたい」という情熱だけで作りました。作る過程で、私は OpenMCP の意義を少しずつ理解し、AI インフラの勉強まで始めました。オープンソースコミュニティの助けもあり、技術の視野は飛躍的に広がりました。

LeoChat がぶつかった「エージェントクライアントの意味とは何か」という問いは、エージェント開発者なら誰もが直面するものだと思います。特に、自分の作ったエージェントを日常で使おうとすると、どうしても自問したくなります。どうして Claude Code を使わないんだろう、と。それでも、エージェントクライアント、エージェントランタイムを作ったという経験は、これまでの私の技術の歩みの中で欠かせない部分です。

LeoChat は終点ではなく、出発点です。