LiteLLMをやめて自作Goバイナリに置き換えたら一気に軽くなりました - 「実践 AI エージェント開発」を実装してみた
できあがったもの
Go 1.25で書いたgo-llm-agentというシングルバイナリのAIエージェントを公開しました。CGO_ENABLED=0でビルドした15MB級の単一バイナリで、対応プロバイダーはOpenAI Chat Completions、Anthropic Messages、Google Gemini、Ollamaの4つです。
CLI、ワンショット実行、OpenAI互換のHTTPサーバを1つのバイナリで切り替えられます。
$ ./bin/agent chat --model openai/gpt-5.4-mini
$ ./bin/agent run --model anthropic/claude-sonnet-4-6 -p "Goのcontextを教えて"
$ ./bin/agent serve --addr 127.0.0.1:14000
race detector付きで全テストが通り、各層は概ね80%前後のカバレッジを保っています。ローカルpre-commitとCIはまったく同じscripts/quality-gate.shを呼び、gitleaksやgovulncheckを含む複数段の品質ゲートを1つのエントリで通します。
この記事では、なぜわざわざLiteLLMを使わず自作したのか、最初に書いた最小構成がどのようなものか、そして書籍を読んで本番運用に向けて何をどう機能強化したのかを順番に説明します。
なぜLiteLLMで満足しなかったのか
LiteLLMは100以上のプロバイダーをまとめて叩けるPython製の優れたゲートウェイで、プロダクションAPIゲートウェイとしての完成度は本家のままで十分です。それでもあえて自作した理由は3つあります。
1つ目は配布形態です。LiteLLMを使うにはpip install litellmでPythonと依存ツリーを揃える必要があります。わたしが欲しかったのは、開発機にバイナリ1つを置いて即時起動できるエージェントでした。Pythonランタイムを管理対象に追加したくありません。
2つ目はエージェントループの不在です。LiteLLMが扱うのはあくまでLLM呼び出しの抽象層で、tool callingの往復ループや会話履歴の保持は利用者の責務です。わたしはLLMの抽象化とエージェントループを同じプロセスで一気通貫に書きたかったので、別々に組み合わせるよりも自分で書いた方が早いと判断しました。
3つ目は守備範囲の調整です。LiteLLMは多機能で、コールバックやスペンドトラッキング、フォールバックルーティングなど、通常のコーディングでは必ずしも常用しない機能まで含まれています。最初に欲しかったのはローカル開発用のシングルバイナリエージェントだけだったので、機能を絞ることで保守する責任を最小にしたかったのです(書籍を読んだ後の追加実装で、課金や信頼性まわりも自前で持つことになりました)。
AIエージェントの軽量化とシングルバイナリ化に対する立ち位置
ここ数年、AI関連に限らず、開発者ツールをシングルバイナリで配るアプローチは広く普及しています。AI関連では、LLMをローカルで走らせるOllama、推論エンジンを単体実行できるllama.cpp、AI関連以外では、Goで書かれた静的サイトジェネレータのHugo、Go製TUIライブラリ群を整えるCharmなど、ランタイム不要で動く配布形態が支持を多く集めています。フロントエンド資産をバイナリ内に同梱するembedパッケージの存在もこの流れを後押ししているでしょう。
AIエージェント領域でも、軽量化やローカル実行を訴求するプロジェクトはじわじわ増えています。Web UI、ブラウザ自動化、オフィスファイル読み書き、RAG、ワークフローオーケストレーションまで詰め込む「全部入り型」と、「目的を絞って小さく作る軽量型」の両方がよく出てきます。わたしは後者を選びました。
理由は2つあります。
1つ目は機能過多のリスクです。Web UIやRAG、ブラウザ自動化を内蔵すると、フロントエンドのバージョン追従、ベクトルDBの運用、ヘッドレスChromeの維持、ファイル形式パーサのCVE対応など、本体機能の多寡に応じて保守すべき項目がどんどん増えます。わたしの用途はCLIとHTTP APIだけで足りるので、それ以外を持つと攻撃への耐性考慮と心理的負担が増えるだけだと判断しました。
2つ目は受け入れ口の少なさです。全部入り型は機能数を最優先するため、エンドユーザーへの提供口がWeb UIに集約されがちで、シェルやエディタや別アプリから取り込む経路がどうしても少なくなりがちです。わたしが欲しかったのはターミナルからshellのpipeで投げて、エディタ拡張機能からはHTTPで叩ける、といった、利用方法を柔軟に切り替えられる道具でした。
go-llm-agentは入口・コア・出口の3層をinterfaceで切った縦に薄い構造になっています。図にすると次の通りです。
各層は他層の具体実装に依存せず、cmd/agent/main.goのDIコンテナだけが組み立てを担います。Web UI、ベクトルDB、ヘッドレスChromeなどの重い同梱を持たないため、保守すべき内容が少なくなります。
このアーキテクチャがもたらす利用者側のメリット
go-llm-agentは入口、コア、出口を分けて設計したことで、同じ1つのバイナリから次の使い方が無設定で全てできます。
1つ目は4プロバイダーの即時切り替えです。--model openai/gpt-5.4-miniをanthropic/claude-sonnet-4-6やgemini/gemini-3.5-flashやollama/llama3.3に書き換えるだけで、同じプロンプトを別モデルへそのまま投げ直せます。プロバイダー固有のSDKや別ツールを行き来する必要はありません。
2つ目はOpenAI互換のHTTPサーバを内蔵していることです。Continue、Cline、Aider、OpenAI SDK、curlなど、OpenAI互換エンドポイントを叩ける既存ツールはそのまま接続できます。LANにagent serveを上げておけば、社内全員のエディタ拡張が同じプロキシを共有でき、APIキーも一箇所で管理できます。OllamaのOpenAI互換エンドポイントと組み合わせれば、エディタ拡張からローカルLLMにtool calling込みで投げられます。
3つ目はCLIワンショットでshellやCIから直接呼びだせることです。
# PRタイトル候補を生成
$ ./bin/agent run -p "次のdiffから3行以内のPRタイトルを出して" --model anthropic/claude-sonnet-4-6 < diff.patch
# MarkdownをJSONに整形
$ cat input.md | ./bin/agent run -p "見出しを階層JSONで返して" --model openai/gpt-5.4-mini | jq .
./bin/agent runは標準入力からプロンプトを受け、標準出力に結果を吐くだけのフィルタコマンドとして振る舞います。
4つ目は内蔵ツールでローカルファイル操作系のエージェント挙動がそのまま動くことです。config.yamlのenabled_toolsに列挙したツールだけが有効になります。既定はreadonlyなfs_read / search_files / http_fetchの3つのみで、fs_writeやshellを有効にしたい場合は意図して明示列挙するdeny-by-default構成です。
5つ目はローカル完結による情報統制です。ollama/llama3.3をdefault_modelにしておけば、社内コードや顧客情報を含むプロンプトをクラウドに送らずに済みます。
6つ目は1ファイル配布の取り回しです。scp ./bin/agent remote:~/bin/で済むので、go installもDockerもpipも一切不要です。
7つ目はJSONL履歴のローカルでの後加工です。storage.sessions_dir配下のJSONLは1行1メッセージなので、jqでフィルタしたりfzfで履歴検索したりがUNIXの思想通りに標準ツールの組み合わせだけで可能です。
8つ目はCLIとHTTP APIの同居です。同じバイナリでagent chatとagent serveを切り替えられるので、ローカルではREPLを回し、リモートに置いた同じバイナリはLANプロキシとして共有する、といった二重運用ができます。
第1段階:自分の必要な機能をまず形にした話
ここからは、書籍を読む前の段階で、まず手元の困りごとを解消するために書き上げた最小構成を紹介します。
ストリーミングとtool callingは最初から両方サポート
各プロバイダーのProvider実装には、必ずChat(ctx, req) (*ChatResponse, error)とStream(ctx, req) (ChatStream, error)の両方があります。SSEの差異はプロバイダーごとに思ったよりはかなり大きいので、内部でだけStreamEventという統一型に正規化します。
OpenAIはdata:形式のSSEでchoices[].delta.contentを逐次返します。Anthropicはevent: content_block_deltaとevent: message_deltaをやり取りします。Geminiは?alt=sseで同様にSSEを返し、Ollamaは改行区切りのNDJSONを返します。これらをすべてStreamEvent{DeltaText, ToolCall, Usage, Finish, Err}に変換します。
tool callingも同様に正規化します。Anthropicはtool_useブロックで、GeminiはfunctionCallオブジェクトで、Ollamaはtool_calls配列で、それぞれ別の形式を取りますが、内部表現はllm.ToolCall{ID, Name, Arguments}に揃えます。エージェントループから見ると、どのプロバイダーでも同じ型で扱えます。
エージェントループは次のような流れです。
for hop := 0; hop <= in.MaxToolHops; hop++ {
stream, _ := prov.Stream(ctx, req)
for {
ev, ok := stream.Recv()
if !ok { break }
// DeltaText, ToolCall, Finish, Errを順次処理
}
if pendingCall == nil { /* 終了 */ }
res, _ := tools.Lookup(pendingCall.Name).Execute(ctx, args)
msgs = append(msgs, /* tool 結果 */)
}
MaxToolHopsを超えたら明示的にエラーで打ち切ります。無限ループ対策です。
内蔵ツールはサンドボックスで制限する
エージェントが手元のファイルやコマンドを叩けるのは便利ですが、危険でもあります。go-llm-agentは内蔵ツールにそれぞれ多層のガードを入れています。
-
fs_readとfs_writeは許可ルート配下のみアクセス可能で、MaxReadBytesを超えた読み取りはTruncated=trueを立てます。さらに.git.env.ssh.awsid_rsa*等のセンシティブパターンは設定で外せない強制denyで常に弾かれ、終端パスがsymlinkの場合も拒否されます -
shellはallow_binariesのコマンド名照合に加えて、引数文字列に対するdeny正規表現を持ちます。既定でgit config --global/bash -c <code>/core.sshCommand=.../go env -w/--exec等の典型的なプロンプトインジェクション経路を遮断します -
http_fetchはnet.IP.IsPrivateとIsLoopbackでプライベートCIDRとリンクローカル接続を拒否し、allow_domainsを設定すればFQDNサフィックス一致で公開先も絞れます -
search_filesはio/fs.WalkDirで再帰探索しますが、サンドボックス外のrootは事前に拒否します - すべてのsensitiveツールはlog/slog経由で監査ログを残します
サンドボックスはfilepath.EvalSymlinksでcanonical化したパスに対して許可ルート配下判定とdenyパターン判定を行い、..を含むtraversalは正規化前後の二段でブロックします。
横断関心はlog/slogベースの薄いレイヤで揃える
ログはlog/slogを使い、JSONHandlerとTextHandlerを設定で切り替えます。シークレットマスクは小さなヘルパで、_API_KEY _TOKEN _SECRET _PASSWORDで終わるキー名のみ値を***に置換します。
セッション履歴はJSONLで1行1メッセージを追記します。SQLiteを使わない理由は、CGO不要のシングルバイナリを維持したかったからです。シークレット解決は環境変数を一次、.envを二次として扱います。
ここまでが「ローカルの手元用に動けば十分」だった段階の到達点です。コードは小さく、配布も軽く、自分の用途にはこれで十分でした。
第2段階:書籍に従って本番運用機能を実装した話
ここからが書籍読了後の取り組みです。"Building Applications with AI Agents"(Michael Albada著、O'Reilly、2025年)は、エージェントを「動くもの」から「本番に置けるもの」へ持ち上げるための観点を体系的に説いています。
書籍の主張を自分なりに要約すると、運用するエージェントは大きく次の4層を順に積み上げると無理がない、という流れになります。
- 観測可能性を最初に確保します
- コストと信頼性を見える化してから外部に公開します
- ツール契約と入出力フィルタで挙動を制御します
- 評価、人手での承認、高度な戦略、段階デプロイで運用品質を上げます
この4層はdocs/design/00-overview.mdに16項目のロードマップとして書き下しました。各設計書は書籍の章番号と照合済みで、docs/design/01から16までを番号順に実装すると、自然に下層から積み上がるようにしてあります。番号順に意味があるのは、後の項目が前の項目で得られる「観測情報」や「契約強制」を前提にしているからです。
以下、書籍の章対応と実装の対応を、番号順に簡潔にたどります。
01 まず観測可能性から入れる
書籍のCh10「Monitoring Stacks(「10.2 監視スタック 」)」では観測の重要性を強調しています。観測がないままで他の機能を足すと、リトライしているのか、コストが伸びているのか、評価でどこが落ちているのかが見えません。
go-llm-agentではOpenTelemetryでOTLP HTTP exporterを有効にし、agent loopのhopごとにspanを張り、token使用量とレイテンシをattributeに乗せます。
詳しくは01-otel-instrumentation.mdに書いています。
02 次にコストを見える化して上限を設ける
Ch10の表10-1ではWorkflow levelのメトリクスとしてToken usageが挙げられ、急増が異常検知の入口になると述べられています。観測が入ったらすぐに、コスト集計とセッション単位の上限設定を入れます。
実装ではプロバイダーごとに円単価をconfig.yamlのproviders.<name>.pricingに書き、セッションIDと日次の双方で集計し、上限を超えたらErrBudgetExceededでループを止めます。集計値はstorage.sessions_dir/billing.jsonlにも記録するので、後でjqでも参照できます。
詳細は02-token-cost-tracking.mdを参照してください。
03 外部呼び出しの信頼性を整える
Ch10とCh11がretry logicとfallback frequencyを観測指標として推奨しています。観測とコストの上限が乗ったら、次にプロバイダー側の一時障害に耐えるためのリトライとフォールバックを入れます。
実装は指数バックオフ+ジッタで、429と5xxはRetryable=trueとしてmax_attemptsまで自動再試行します。fallback_toを書いておけば、OpenAI障害時にAnthropicに自動フェイルオーバーします。リトライ回数はOTel attributeに載るので、観測側で頻度を可視化できます。
03-llm-retry-backoff.mdに判定ロジックを書いています。
04 外部に公開する前にHTTP境界を固める
Ch12「Protections from External Threats(「12.5.2 外部の脅威からの保護」)」がrate limitingとAPI keysを最低限の外部脅威対策として挙げています。リトライまで動いた段階で初めて、外部に向けたHTTPエンドポイントの認証とレート制限を整えます。
実装ではcrypto/subtle.ConstantTimeCompareでSHA-256ダイジェストを比較するBearer認証、トークンバケットのrate limiter、CIDRベースのIP allowlist、CORS、429のRetry-Afterヘッダ付与までを実装しました。トークン値はconfig.yamlに直書きさせず、secret_env参照のみを許容するconfig loaderで強制しています。
04-http-auth-ratelimit.mdに詳細があります。
05 ツール契約とJSONスキーマでハルシネーションを弾く
Ch4「Tool Use Configuration(「4.3 ツールの設定」)」がauto / required / none / 特定ツール指定の4モードを提示し、「Validate first using jsonschema」と「Retry intelligently」を出力検証のガイドラインに据えています。HTTP境界が固まったら、ツール呼び出し側の契約を強制します。
実装ではsanthosh-tekuri/jsonschema/v5で各ツールのInputSchemaを検証し、無効な引数はLLMにエラー文として返し、max_retries回まで自動修正させます。各プロバイダーのtool_choice方言(OpenAIのrequired、Anthropicの{"type":"any"}、GeminiのMode:"ANY")を共通インターフェースのModeに正規化して同じ意味で扱えるようにしています。
05-tool-choice-schema-validation.mdに書いた通り、ここで「LLMのハルシネーション引数がツールに到達しない」境界線が決められます。
06 入力プロンプト検知と出力フィルタで安全弁を増やす
Ch12「Securing Foundation Models(「12.3 ベースモデルのセキュリティ確保」)」が入力プロンプトインジェクション検知と出力フィルタを基本対策として扱っています。BIPIAやLakera PINTがベンチマークとして言及されています。
実装ではユーザー入力に対する正規表現ベースの簡易スキャナを置き、ignore previous instructionsや[system]偽装といったよくあるパターンに当たればslogで警告を残し、設定でblock_on_match: trueにすれば即拒否します。出力側はOpenAIキー、JWT、PEM秘密鍵、クレジットカードなど機密パターンをマスキングするOutputRedactorを通します。
06-input-output-filter.mdに詳しく記載しています。
07 評価フレームワークで質を測れるようにする
Ch9「Integrating Evaluation into the Development Lifecycle(「9.3 全体的な評価」)」が、評価を開発ループに組み込むための具体的なメトリクスとしきい値を提示しています。安全弁まで揃ったら、初めて「変更が品質を上げているか」を測る土台が作れます。
実装ではtestdata/eval/*.jsonl形式のスイートに対して、agent evalサブコマンドで「exact match」「JSON shape match」「tool use 正しさ」などをスコアリングし、reports/eval-<timestamp>.jsonを書き出します。CIで閾値を下回ったら落とせる構造です。
07-eval-framework.mdに評価指標の定義があります。
08 危険操作には人間の承認をはさむ
Ch11「Human-in-the-Loop Review(「11.1.2 ヒューマンインザループレビュー」)」とCh13「Escalation Design and Oversight(「13.3.3 エスカレーションの設計と監視」)」が、信頼度・リスク・影響範囲のいずれかで人間に介入させる閾値を設けるべきだと述べています。
実装ではツール名のリストagent.approval.required_toolsに登録したツールについては、agent loopがpending状態で人間の決裁を待ち、POST /v1/runs/<id>/approveにDecisionが来るまで実行を保留します。timeout時は常にdeny扱いとするfail-closed設計で、default_decision: allowはそもそも起動時にconfig errorで弾くようにしました。
08-hitl-approval.mdに承認フローの図があります。
09 単発ReActから戦略を選べるようにする
Ch5「Planner-Executor Agents(「5.1.3 プランナーエグゼキューターエージェント」)」「Reflection Agents(「5.1.5 リフレクションエージェント」)」が戦略パターンを論じています。HITLまで揃った段階で、用途に応じて戦略を切り替えられる構造を入れます。
実装ではReActに加えて、計画モデルが[Planner]タグ付きで分解計画を出すplanner_executor、自己批評で再試行するreflectionを選べるようにしました。config.yamlのagent.strategyで切り替えるだけで挙動が変わります。
09-planner-executor.mdで戦略選択の判断指針を書いています。
10 並列ツール実行で遅延を縮める
Ch5「Parallel Tool Execution(「5.4.2 並列ツール実行」)」によると、OpenAI、Anthropic、Geminiは並列tool callsをネイティブにサポートしており、活用すると応答時間が大幅に縮みます。
実装では1hop中に複数のToolCallが返ってきた場合、errgroupで順序保証付きの並列実行をかけます。require_approvalのツールが混ざっている場合は自動で直列にフォールバックする安全装置も入れています。
10-parallel-tool-calls.mdに並列化のロックフリー実装が書いてあります。
11 メモリとノート全文検索でRAG MVP
Ch6「Foundational Approaches to Memory(「6.1 記憶への基礎的アプローチ」)」「Note-Taking(「6.3.4 ノートテイキング」)」「Retrieval-Augmented Generation(「6.2.3 RAG」)」が、全文検索とベクター検索を併用するMVP像を示しています。
実装ではnote_addとnote_searchのローカルツールを提供し、JSONLファイルへの追記と簡易スコアリング検索(title=3、tags=2、body=1)を入れました。クエリのトークナイズは日本語など分かち書きしない言語に対して2-gramで展開する処理を入れ、英語混在クエリでも自然にヒットするようにしています。SQLite + FTS5への差し替えはinterfaceで差し込めるよう抽象化済みです。
11-rag-mvp.mdが詳細です。
12 MCPクライアントで外部ツールを動的取り込み
Ch4「Model Context Protocol(「4.1.4 MCP」)」がMCPの位置づけを述べています。go-llm-agent側からMCPサーバを起動して、tools/listとtools/callをJSON-RPCで叩く最小実装を入れました。子プロセスのClose()はcmd.Wait()に5秒タイムアウトを設けてProcess.Kill()にエスカレーションするので、相手が応答停止してもagent本体がハングしません。PATH解決を許さず絶対パスのみを受け付けるvalidateMCPCommandもここに入っています。
12-mcp-client.mdに通信図があります。
13 プロンプトテンプレートをバージョン管理する
Ch10の表10-1がTask success rate by workflow or prompt template versionを観測指標として推奨し、Ch11「Prompt and Tool Refinement(「11.1.3 プロンプトとツールの改良」)」がA/B比較の重要性を強調しています。name@version.tmplという命名のテンプレート群をinternal/promptのローダから読み出します。
実装ではテンプレートディレクトリ外への参照をfilepath.EvalSymlinksで拒否し、text/templateのvarsに関数値が混入していたらreflectで即エラーにしてSSTI経路を塞ぎます。
13-prompt-template-versioning.mdを参照してください。
14 PII自動マスキングを最後の出口で重ねる
Ch12「Handling Sensitive Data(「12.4.3 機密データの取り扱い」)」では、ログ・キャッシュ・中間出力すべてに匿名化を適用するべきと述べています。
実装ではメール、日本国内の電話番号、マイナンバー、IPv4を厳密な範囲で検出するPIIRedactorを作り、OutputRedactorとChainRedactorで連結しました。OpenAI互換HTTPレスポンスの非ストリーム経路ではchunk境界を跨いだPIIを取りこぼさないよう、syncChatで集約後の最終content文字列にもう一度Redactorを適用しています。ストリーミング経路ではchunk単位redactのみとなる制約があるため、機密性最優先のユースケースではstream: falseを選ぶ運用を推奨と明記しました。
14-pii-redaction.mdに正規表現と注意点が書いてあります。
15 mTLSとOAuth2 JWTで本番強化
Ch12「Protections from External Threats(「12.5.2 外部の脅威からの保護」)」がmTLSとOAuth 2.0を本番向けに推奨しています。Bearer認証だけで足りる現場はそのままで構いませんが、社外向けやBYOK相当の運用が必要な現場のために、crypto/tlsのClientCAsとClientAuth=RequireAndVerifyClientCertによるmTLSと、HS256以上のシークレットで検証するgolang-jwt/jwt/v5を入れました。tls.min_versionは1.2と1.3のみを受け付け、その他は起動時にconfig errorで弾きます。
複数のAuth方式を同時に有効化した場合の判定は明示的にAND(すべて成功必須)と定めました。設計書にも明記し、優先度ベースのORは今回サポートしません。
15-mtls-oauth.mdを参照してください。
16 カナリアとシャドウで安全に切り替える
Ch11「Shadow Deployments(「11.2.1 シャドウデプロイ」)」がcanaryとshadowの併用を推奨しています。agent.canary.ratioで新モデルへの段階ロールアウト、agent.shadow.ratioで並走比較を行えるルーター実装を最後に乗せました。shadow ratioは0.5を上限としてクランプし、shadow側の応答がユーザーに返らないことを実装側で保証します。
16-canary-shadow.mdに切り替え図があります。
なぜこの番号順なのかをもう一度
書籍を読みながら設計書を起こす過程で、章ごとに「これは何に依存しているか」を意識し、依存の少ない側から積むよう並べ替えました。
- 観測(01)が再下位層です。観測なしには他の機能のうまく動いているかが分かりません
- コスト(02)と信頼性(03)はそのすぐ上に置きます。これらは観測の指標に直結します
- 外部公開のための認証・レート(04)はコストと信頼性のすぐ上です。安く動くことと止まらないことが先にないと、公開しても運用できません
- ツール契約(05)と入出力フィルタ(06)はその次です。HTTPで叩かれる前提が整って初めて、ツール契約とプロンプト/出力の安全弁が意味を持ちます
- 評価(07)と人手承認(08)はここに来ます。安全弁が揃って初めて、品質を測ったり危険操作を人に振ったりできます
- 戦略(09)、並列(10)、RAG(11)、MCP(12)、テンプレ(13)の機能拡張はこの土台の上です
- PII(14)は最終出口で、全機能の出力をまとめてラップする位置に来ます
- mTLS / OAuth2(15)、canary / shadow(16)はproduction向けのデプロイ強化で、最後に載せます
この順序を守ったおかげで、各実装ステップでまず観測上の異常をすぐ捉えられ、上限と再試行のおかげで暴走せず、ツール契約があるのでハルシネーションによる引数はツールに届かず、評価で品質回帰を見つけられる、という流れが自然に得られました。
実装は1ステップごとにユニットテストとtests/e2e/01-*.shから16-*.shまでのE2Eを揃えており、ローカルpre-commitとGitHub Actionsの両方で同じscripts/quality-gate.shを呼んで、gofmt / go vet / staticcheck / golangci-lint / govulncheck / go test -race -cover / gitleaksまでを一括で通します。
LiteLLMとの使い分けと利用者メリット
LiteLLMとgo-llm-agentを、利用者がどんなときにどちらを選ぶと得かという観点で並べてみましょう。
| 観点 | LiteLLM | go-llm-agent |
|---|---|---|
| インストール |
pip install litellmでPython 3.8以上が必要 |
バイナリ1本をscpかcpで置くだけ |
| 起動 | Pythonプロセス起動の数百msオーバーヘッド | ネイティブで即時起動するのでCIの1行コマンドにも向く |
| 用途の中心 | プロダクションのAPIゲートウェイ、ルーティング、課金計測 | 開発機やCI上の手元エージェントとして使い倒す |
| 対応プロバイダー | 100以上の主要モデル | 初期4 (OpenAI / Anthropic / Gemini / Ollama)。設定だけで切替可能 |
| エージェント挙動 | LLM呼び出し抽象。エージェントループは利用者が書く | tool callingループを内蔵。agent runだけで完結 |
| 既存ツールとの接続 | OpenAI互換プロキシで接続可能 | OpenAI互換プロキシで接続可能 (ContinueやClineなどが直結) |
| ローカルLLMでのtool calling | LiteLLM経由で可能 |
ollama/<model>指定だけでローカルLLMにtool callingが乗る |
| 課金計測 | Spend tracking / Budgets が成熟 | セッション/日次のJPY集計と上限超過時のErrBudgetExceededを内蔵 |
| PII対応 | プラグイン方式 | OutputRedactor + PIIRedactor を内蔵し、HTTPレスポンスでchunk跨ぎも補正 |
| ライセンス | MIT | MIT |
要点は次の通りです。
- 数百人の開発者が叩く社内LLMゲートウェイを構築するなら、運用実績の長いLiteLLMが圧倒的に有利です
- 「自分の開発機に置いてシェルやエディタから日常的に使うAIエージェント」が欲しいなら、Pythonを介さないgo-llm-agentが軽くて速いです
- 両方を併用するパターンも有効で、社内中央プロキシをLiteLLMで運用し、開発者ローカルだけgo-llm-agentから直接プロバイダーへ抜ける、という構成も問題なく機能します
全部入り型シングルバイナリAIエージェントとの使い分け
シングルバイナリでAIエージェントを配るアプローチは、Web UIやRAGや多数のツールを同梱する全部入り型と、機能を絞った軽量型に大別できます。go-llm-agentは後者の立場で、利用者が得る違いは次の通りです。
| 観点 | 全部入り型 | go-llm-agent |
|---|---|---|
| 起動と利用 | バイナリ実行後にブラウザでWeb UIを開く | ターミナル1枚で完結、エディタ拡張やCIにも直接組み込める |
| シェルやスクリプトとの統合 | UIから出力をコピーするのが基本 | 標準入出力で繋げる (agent run -p ... | jqのような構成が可能) |
| エディタ拡張との連携 | UI側のAPIに合わせる必要があり、対応拡張は限定的 | OpenAI互換のためContinueやClineなど既存拡張がそのまま接続できる |
| 内蔵機能 | Web UIに加えてブラウザ自動化やOffice読み書き、RAG、ワークフローなどフル装備 | fs / shell / http_fetch / search_files / note_add / note_search の小さなセットに限定 |
| 起動オーバーヘッド | フロントエンド資産展開とブラウザ起動分の時間 | ネイティブで即時起動、1ショット呼び出しに向く |
| メモリ占有 | ブラウザ込みで数百MBに達することがある | 数十MB程度で済む |
| サーバ運用 | サービス化するなら追加設定が要る |
agent serveを起動するだけでLAN内のOpenAI互換ゲートウェイになる |
| ローカルLLM対応 | プロダクト次第 |
ollama/<model>だけでローカルLLMに切替可能 |
試してみる
リポジトリはokamyuji/go-llm-agentで、READMEに使い方をまとめてあります。
git clone https://github.com/okamyuji/go-llm-agent
cd go-llm-agent
cp .env.example .env
$EDITOR .env # OPENAI_API_KEYなどを埋める
make build
./bin/agent chat --model openai/gpt-5.4-mini
OpenAI互換クライアントから叩きたい場合は次のように起動します。
./bin/agent serve --addr 127.0.0.1:14000
curl http://127.0.0.1:14000/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{"model":"gemini/gemini-3.5-flash","messages":[{"role":"user","content":"hi"}]}'
ストリーミングは"stream": trueを足すだけです。書籍の章対応で実装した機能群はdocs/design配下に番号順で並んでいるので、興味のあるところから読んでみてください。
機能拡張として考えられる内容
ここからは確定した予定ではなく、現状の構造のままで素直に乗せられそうな拡張アイデアを並べておきます。
Provider側はMistral、xAI、Bedrock経由のClaude、Azure OpenAIなどを足す余地があります。仮に追加する場合は、config.yamlのproviders:にエントリを1つ書くだけで切替対象に入る設計を踏襲できます。
シークレット解決は、現状の環境変数と.envに加えて、macOS Keychain、Windows DPAPI、Linux Secret Serviceへの保存も拡張先として考えられます。実装する場合は同じagentバイナリのまま、agent secrets set openaiのようなサブコマンドでOSの安全領域に保存できる形が自然です。
Episodic MemoryとProcedural Memoryは今回のロードマップでは見送りました。ベクターDBに依存する段階で、軽量バイナリの方針との折り合いを別途検討します。
書籍の章を一通り写経した今、自分の手元から本番運用を見据えた小さなエージェントまで一気通貫で動かせるようになりました。書籍をお読みになった方は読了の余韻が冷めないうちに、ぜひお手元で試してみてください。scp ./bin/agentですぐに配布もでき軽量で動作します。
Discussion