西川和久の不定期コラム
OpenClawなどに飽き足らず、エージェントアプリを自作してみた!
2026年7月28日 06:07
きっかけはds4-agent
筆者は日頃、AIエージェントとしてOpenClawを使っているのだが、先日、久々にローカル推論エンジンの「DwarfStar 4(ds4)」を触った。
ds4は、インメモリデータベース「Redis」の作者antirez氏が作ったもの。当初はDeepSeek V4 Flash専用のMetal推論エンジンとして構築され、その後、CUDAやROCm対応、ほかのLLM対応、そしてエージェント機能にも対応……など、いろいろ精力的に開発しているプロダクトだ。DGX Sparkにも対応していることもあり、筆者は時間があるとき、M4 Max 128GBとともに最新版をgit pull/buildして遊んでいた。
ds4の中の1つとして「ds4-agent」というのがあり、TUI(Text User Interface)で
- read / more — ファイル読み込み(行範囲指定・続き読み)
- write — ファイル作成・上書き
- edit — 文字列の一意マッチによる編集(CRC32ではなく、対象ブロックが1箇所だけに絞れることを要求する方式)
- search — 正規表現/リテラル検索
- list — ディレクトリ一覧
- bash / bash_status / bash_stop — 非同期シェルジョブの実行・監視・停止
- google_search / visit_page — 可視Chromeでのブラウザ操作(API不使用)
と、エージェントとしては必要最低限と思われる機能を持ち、問いに対して結構いい感じに答えが返ってくる。もちろんコードもコンパクト。いろいろ機能山盛りで肥大化しているOpenClawとは真逆の性質を持つ。
ただ実際にds4-agentを使ってみると、
- DeepSeek V4 FlashはVision未対応(次バージョンでは対応との噂)で画像解析できない
- 対応しているLLMしか使えない
- skillがない
- メディア(画像/動画/音)を再生できない
など、大なり小なりいろいろ不満点があったりする。もともとコードを公開しているのでそれに手を入れても良かったが、外部のLLM(OpenAI APIエンドポイント)に対応すると、(処理速度的に)C言語で書く必要もなく、では筆者がフルスクラッチで組んでみるか!というのが今回のお題となる。
機能の洗い出し+α
新しく作るエージェントアプリは、Sonnet 5を使ってds4-agentの機能を洗い出し(上記一覧)、これに仕様変更/追加することに。紆余曲折あったのだが、最終的には、
- OpenAI API対応 / メインLLM
- OpenAI API対応 / Vision対応LLM。ツール呼び出しで呼び出す
- skill対応(特定の手順に従って作業する仕組み)
- cron対応(指定したスケジュールで自動的にタスクを実行させる仕組み)
- メディア(画像/動画/音)再生対応
と、このように変更/追加した。中でもVision対応LLMをツール呼び出しで呼び出す実装はあまり見かけないので珍しいかも知れない。
最近は推論性能が高い代わりにVision非対応というモデルも少なくない。そこでメインLLMには推論特化モデルを使い、画像が必要になったときだけVision対応LLMをツール呼び出しで呼び出す構成にした。これならメインモデルをVision対応にするかどうかで選ぶ必要がなくなる。
今回はVision対応LLMのみだが、この考え方を拡張すれば、コーディング・日本語文書・数理推論など得意分野ごとにLLMをツールとして呼び分ける、オーケストレーション型エージェントへ発展させることもできる。
skillやcronはある意味当たり前、メディア再生対応はTUIだとどうしてもセッション中に表示できないため、諦めてGUIにするか外部ウィンドウでもいいから出せるようにするかの2択。今回は後者を選択した次第だ。
使用言語はPythonかNode/TypeScriptか、どちらを使うか悩んだものの、後にGUI対応するとき、Pythonだと厄介なのでNode/TypeScriptとした。
手間取った部分は!?
実装を始めると、LLMをOpenAI API互換のエンドポイント式(Anthropicの/v1/messages形式にも対応)とし、さらにツール呼び出しでVision対応LLM呼び出しはあっさり実現でき、拍子抜けするほど簡単に終わった。
skill対応も既にClaude Codeなど前例があるのでサクッと完了。標準でGmailのSMTP送信(SMTP AUTH/SMTP認証+アプリパスワード)と、マークダウン書式からPDF出力はテストも兼ね作り、一般的にあれば便利なので、ということで標準搭載とした。メディア再生は別ウィンドウで開くが、これはこれでないよりずっといい。
手間取ったのはbash関連でWindows対応と、Chromeを使うgoogle_searchとvisit_pageだ。前者はUnix系のmacOSに対し、Windowsでは何もかもまったく違うPowerShell/CMD。意識して実装しないとまず動かない。
具体的には、bashコマンド実行の裏側で「どのシェルを使うか」、「バックグラウンドで動かしたコマンドを途中で止める方法」の2点がmacOS/Linuxのやり方に決め打ちされていたのを、Windows標準のコマンドプロンプトでも動くよう分岐を追加した。
Chrome自動操作の方も、Windowsだとインストール場所がmacOSと全然違う(しかも管理者権限の有無でさらに変わる)ので、その候補地を追加した。作業用に使う/tmpもWindowsでは場所が違う。
別件のプロダクトで後からWindows対応したときも大騒動。AI関連でWindows対応していないものが多いのも納得という感じだ。
そして次はgoogle_searchとvisit_page。そのままエージェントが自動でChromeを開き検索しようとすると、プロファイルが新しい状態(Cookie/閲覧履歴がない状態)なので確実にロボットと判断され弾かれるのだ。これはPlaywrightを使っても同様。
これを何とかするため、初めてgoogle_searchを使うとき、前段階としてウォームアップ機能を作った。内容的には自動でChromeが開き、以降、ユーザーが意図的にウィンドウを閉じるまで、Cookieなど人間が操作した内容を保存していく。ウィンドウを閉じ、google_searchが動くとき、この情報を抱えて動作するため、ロボットと認識されにくくなる。ただし、この方法はグレーなので自己責任で。
ほかの方法としてSearXNGというのがあり、これをDockerで起動、検索APIサーバーとする方法がここにあるので、興味のある人は参考にして欲しい。実際組み込んだところうまく動いている。とはいえグレーなのには変わりなく、またgoogle_searchはこれで置き換え可能だが、visit_pageは普通にサイトを開くためウォームアップが不要になるわけではない。
cronに関しては最後に実装。システムのcronを設定しても良かったが、ここでもWindows対応が……となり、結局、このエージェントアプリが起動中のみcronが動作するようにした。
筆者の使い方だとアプリは起動しっぱなしなので、特にこの仕様で問題ないものの、一般的運用も考え、ヘッドレスモードを追加。バックグラウンドで起動しておけば、cronが動作するようにした。
動かしてみる……その前に
さてこれで.envにLLM関連を書いて、
npm install
npm run build
npm start
で動くのだが、これだとソースコードが必要な上に、それらが入ったフォルダからのみの起動となる。何処からでも起動したいので、バイナリ化することにした。方法は、
esbuild src/cli.ts --bundle --platform=node --format=cjs --outfile=bundle/cli.cjs
pkg bundle/cli.cjs --targets node20-macos-arm64 --output dist-bin/macos-arm64/core-agent
pkg bundle/cli.cjs --targets node22-win-x64 --output dist-bin/windows-x64/core-agent.exe
--targetsでOS/CPUアーキテクチャごとに指定すれば、クロスコンパイル(Mac上でWindows向けも作る、など)も可能だ。
ハマった点だけ軽く触れると、Windows向けをMac上でビルドしたバイナリが、実機で「V8のbytecodeキャッシュが不一致」というエラーで即クラッシュしたこと。ビルド元とビルド先でV8のバージョンが微妙に噛み合わないのが原因で、pkgに--publicオプション(bytecode生成自体を無効化するフラグ)を足す必要があった。
これで準備OKなのだが、考えてみるとカレントにある.envを読み込むいつものパターンだと、結局.envがあるフォルダからしか起動できない。そこで~/(Windowsだとユーザー¥<ユーザー名>¥直下)にグローバル用設定を持ち、そこから読み込む仕様に変更した。
以降、.env、skillsは、まずカレントフォルダを調べ、あればそれを、なければグローバル設定を読み込む形へ。こうしておけばプロジェクトで設定を変えたい場合、有効な手段となる。
なお、skillsに関してはグローバル設定とローカル設定の合計。つまりセットしてあるskillはすべて使用可能となる。
動かしてみる
試しにやったことは、
- PC Watch今日の記事一覧をPDFへ
- 2026年7月のドル円推移グラフ
- ComfyUIで画像生成するskillを作ったので、2026年夏、日本でのコーデトレンドをリサーチ、1つ選んで画像生成
この3つ。初めて検索するときは、上記したようにウォームアップが動作、Google画面で私はロボットではないにチェックし、パズルをすることになる。その後、いつも見ているサイトを徘徊。1分ほど操作してウィンドウを閉じた。
なお、セッションに関してはカレントフォルダにsessions/フォルダが作られデフォルトはdefault.jsonで保存されている。これはコマンド起動時、--session <name>とすれば、そのセッションを読み込み、保存される。
ファイル書き込みなど危ない操作はy/N/a(uto)で切り替え可能。起動時のオプションで--yes(もしくは-y)とすると、初めからautoで起動もできる。
長時間使っていると、当然会話履歴はどんどん伸びていく。放置するとモデルのコンテキスト上限を超えてエラーになるため、コンテキスト圧縮も用意した。
仕組みは単純で、会話履歴のトークン数を(厳密なトークナイザではなく文字数÷4の概算で)監視し、既定6万トークンを超えたら発火。system prompt(先頭)と直近のやり取りは残しつつ、それより古い部分をLLM自身に要約させて、1つのメッセージに置き換える形とした。
と、前置きはいいとして、3つの実行結果は以下の通り。
いかがだろうか?見事にお題をこなしているのが分かる。自分で作ったエージェントアプリでこれが動いたときはちょっと感動だったりする(笑)。
なおセッションの出力はマークダウン形式になることが多いので簡易ビューアーも搭載している。
ここまで書いて、ふと気になったことがある。Claude Codeも、OpenCodeも、広い意味では同じ「ハーネス」。tool-use loopでLLMにファイル操作やbashを実行させる、という構造は変わらない。なのに規模は桁違いに大きい。
そこで、Claude Codeに違いを尋ねると(笑)、コアループ(メッセージ配列+tool_calls+tool-use loop)自体は、今回作ったものとほぼ同じらしい。
差は、そこに何年もかけて積み上げた周辺機能の量から来ている。具体的には、MCPやサブエージェントといった拡張性のインフラ、数百のツールを必要なときだけ読み込むcontext管理の精緻さ、許可パターンのallow/denyリストのような安全機構の粒度、そして編集後に本物のコンパイラ/linterの診断が返ってくるLSP連携――このあたりが規模の差の正体だという。
この違いを考慮すると「不特定多数のユーザー・環境で使われることへの投資」と「個人利用でも普通に効く投資」に分かれている。前者(コンテキスト管理の精緻さ、安全機構の粒度、IDE拡張などのエコシステム対応)は、今回のような1人用/1構成専用のツールには基本的にオーバースペックだ。
一方、LSP連携だけは、「多数のユーザーのため」ではなく「編集の正しさを機械的に検証できるか」という、規模に関係なく効くが、実装が大変らしく見送った(保留)。MCPについては、あれば便利だが、skillでいいのでは?と思う部分があり、実装も大変らしく非対応に。
改めて数えてみると、今回の実装はsrc/配下約2,300行(テスト込みで約3,050行)。Claude CodeやOpenCodeの内部実装がどれくらいの規模かは分からないが、少なくとも桁は違うはずだ。それでも、バックエンド差し替え、Vision、tool-use loop、コンテキスト圧縮、確認ゲート、skill機構など、「個人の日常使いに要るもの」だけに絞れば、この程度のコード量で十分に動くところまで持っていけたのが今回一番の収穫かもしれない。
要望があればGitHubに公開するが、はたしてこんなのに需要があるかどうか……(笑)。























