「AI で業務効率化」が当たり前になった一方で、こんな経験はないでしょうか。
- 思ったような出力をしてくれない…
- 出力に、いらないデータが混ざっている…
- 事実と異なる結果を、それっぽく返してくる…
さらに、チームで使い始めると別の不安も出てきます。
- このエージェント、社内のコードやデータをどこに送っているんだろう
- API キーを渡しているけど、これは大丈夫なんだろうか
筆者自身、「AI 万能論、実はそうでもないのでは」と感じる場面を何度も経験しました。そして原因を追っていくと、モデルの性能そのものより 「何を文脈に入れたか」と「どこに繋がっているか」が見えないこと に行き着くことが多かった。
そこで、マルチエージェントのタスク管理から実行・監査・文脈の最適化までを 1 つのデスクトップアプリで完結させる ことを目指して、moeca を個人開発しています。この記事では、その全体像と、特に力を入れている 2 つの設計(セキュリティとコンテキスト最適化)を紹介します。
該当資材が入ったレポジトリはこちらで公開しています。
本アプリは個人開発中です。主要な機能は一通り動く状態になりましたが、細部は継続的に手を入れています。
1. moeca とは
1.1 概要
moeca は、複数の AI エージェントをセキュアに「管理して・走らせて・振り返る」ための現在進行形で個人開発中のデスクトップアプリです。
| 形態 | デスクトップアプリ(Tauri + React + TypeScript) |
| バックエンド | Go サイドカー(gateway / hostagent / sandbox / registry proxy / RAG indexer) |
| エージェント実行環境 | Docker サンドボックス(実行ごとに使い捨て) |
| 画面 |
Delivery / Daily / Terminal / Knowledge / Audit / Settings
|
コンセプトを一言でいうとこうなります。
エージェントは信頼しない。そのかわり、エージェントが動ける場所と、見える文脈を、こちらが完全に決める。
言葉より先に、6 画面を 27 秒で通したところを置いておきます。
Delivery(レビュー)→ Daily(定期実行)→ Audit(ログ)→ Knowledge(ナレッジのグラフ)→ Settings の順に通しています。映っているのは実データで、架空 4 サービスの資料 90 件を索引し、エージェントにタスクを 1 本走らせた後の状態です。
1.2 主な機能
(1) タスク管理から実行までを一括で管理できる
タスクは 2 系統あります。
Delivery — コードの成果物を出す系のタスク。inbox(起票) → working(判断待ち) → done(完了) のボードで、リポジトリごとに並びます。
- タスクごとに git worktree が切られ、エージェントはその中だけで作業する
- レビュードロワーで 差分 / 原本(編集可)/ 成果物 / エビデンス を確認できる
- CI ゲート:CI が通るまでセルフレビューの承認が解錠されない
- 承認するとホスト側でマージ(マージはエージェントには絶対にできないホスト権限の操作)
カード 1 枚がタスク 1 件。担当エージェントと差分行数・CI 状態がカード上に出ます。
上段が CI ゲート。通るまで承認が解錠されません。最下部の A2A ログで、この差分がどのエージェント間のやりとりから出たかが同時に見えます。
Daily — 定期実行系のタスク。カレンダー/ギャラリーで、生成された成果物(テキスト・画像・動画・音声)を一覧します。
- ローカル PC 上でアプリが起動しているときだけスケジュールが発火する
- スケジュールの予定をカレンダーで管理することができる
成果物ギャラリー。テキスト・画像・音声・動画が種別ごとに色分けされ、その場で再生・表示できます。
(2) 実行用のエージェントは柔軟に設定できる
Settings → Agents で エージェントテンプレートを作ります。3 形態あり、いずれも保存され、そのまま実行できます。
| テンプレート | 実行のされ方 |
|---|---|
| Solo | 単体。provider + model を選び、任意で HTTP ツール / RAG を有効化 |
| Static Multi-Agent | supervisor + graph。Graph は Stage DAG として、Supervisor は plan → workers → integrate にコンパイルされて走る |
| Dynamic Orchestration | 実行時にテンプレートへルーティングする |
Stage DAG の各ステージはそれぞれ独立したサンドボックスで走り、共有 worktree を通して仕事を受け渡します。dependsOn が満たされたステージから、maxParallel値 の範囲で並列に起動します。
ステージが指定するのはイメージ参照ではなく「ポリシー名」(base / poly / media)である点がポイントで、実際のイメージ・ネットワーク構成・リソース上限はコントローラ側が与えます。つまりハードニングのフラグはどのイメージでも同一になります。
| ポリシー | 中身 | ネットワーク |
|---|---|---|
base |
distroless、エージェントバイナリのみ(シェルもツールチェインも無い) | egress |
poly |
Node 22 / Python 3 / Go 1.25 + シェル(ビルド・テスト用のコマンドステージ) | egress |
media |
ffmpeg / ImageMagick / libvips(Daily の画像・動画生成用) | 無し |
media を共通イメージに入れていないのは意図的です。メディアパーサは信頼できないバイナリ入力を食うので、全エージェントの攻撃面を広げたくない。独立ステージにすればネットワークすら不要になり、むしろ普通のサンドボックスより閉じています。
テンプレートが決めるのは「どのエージェントを、どの順で走らせるか」だけです。ノード 1 つがステージ 1 つになります。
何を渡すかを決めるのは、そのノードの中身にあたる Solo エージェントの編集画面です。
RAG・Web 検索・画像生成・音声生成・動画生成を、エージェントごとに個別に許可します。3 つのメディア生成を 1 つのフラグにまとめなかったのは、コストが 1 桁違うからです。そして許可していない種類はツールが存在しません —— 無効化ではなく不在なので、モデルからは見えず、要求すらできません。
media ポリシーと「動画生成」は別物です。
前者は どの箱で動かすか(ffmpeg などを積んだサンドボックスのイメージ。種別「コマンド」のステージで選ぶ)。後者は どのツールを渡すか(生成 API を gateway 経由で叩く権限)。名前は似ていますが、片方はエンコードする側、もう片方は作らせる側です。
API キーは書き込み専用で OS キーチェーンに入り、gateway だけが注入します。エージェントにも localStorage にも出てきません。
(3) エージェント実行時のログを、各粒度ごとに取得できる
Audit タブでは、A2A 形式のログを階層のまま辿れます。
Context
└ Task
├ Artifact
└ Message
└ Part
Extensions / Metadata
粒度・種別・時間でフィルタでき、トークン数やセッション数のメトリクスも同じ画面にあります。
重要なのは、このログはエージェントが迂回できないことです。エージェントの通信は物理的に gateway しか通れないので(後述)、「ログに残さずに外へ出る」経路が存在しません。さらに記録は append-only な SQLite にハッシュ連鎖(hash = SHA256(prevHash + record))で保存され、/_gateway/audit/verify が最初に壊れた行を報告します。集約されたセキュリティ境界による配置が、単純な途中改変を検出できる、tamper-evidentな監査ログの役割も担っています。
左のツリーが Context → Task → Message → Part、右にその本文。ラン単位の行から、次の章のトレースへ飛べます。
(4) ログからトレースしながら、コンテキストを最適化できる
ここが moeca の本命です。Audit のランから トレースボタンで Knowledge タブへ遷移すると、そのランが実際にどのナレッジへ到達したかがグラフ上に重なって表示されます。
- ステージを選ぶと、そのステージが到達したノードだけがフォーカスされる
- ステージを送っていくと、参照が移り変わっていくのが見える
- 一度も参照されなかったノードが分かる → 「このタスクにこの group は要らなかった」という判断に直結する
- 右ドロワーで、そのステージのシステムプロンプトをその場で編集・保存できる
Audit のラン → Knowledge のトレース → その場でプロンプト修正、までが 1 本の導線になっています。
実際に走らせた例を 1 つ。「src/retry.ts の再試行が 4xx にも効いている。根拠を調べてから直せ」というタスクに対して、エージェントは 7 回検索し、90 ノード中 26 に到達しました。
到達(上位):
×3 docs/payments/adr-001.md 台帳は追記のみ
×3 docs/payments/incident-2026-05-20.md 再送集中の障害
×2 media/payments/walkthrough.ja.vtt ← 動画の字幕がヒットしている
…(計 26 ノード。残り 64 ノードには一度も到達していない)
64 ノードが沈んだままであることがこの画面の価値です。到達しなかった group は、そのタスクに要らなかったと証拠付きで言えます。
光っているノードがこのランの到達先。沈んでいるノードは一度も返っていません。
トレースを見たまま、右ドロワーでシステムプロンプトを直せます。保存時に出ている「影響範囲」が次の段落の話です。
この画面で気をつけたのは、UI が正直であることです。
「参照した」ではなく「到達した」しか分からない。
記録できるのは、検索結果としてエージェントに渡ったところまで。エージェントがその中の何を実際に読んで判断したかは分かりません。だから表示は常に上限値であり、UI 文言も 参照 ではなく 到達 返却 に統一しています。ログ量の上限で末尾が欠けることもあるので、「一部記録なし」を明示します。
1.3 いまの開発状況
現状はこうです。
| 領域 | 状態 |
|---|---|
| セキュリティ境界(L3/L4/L7・鍵注入・監査ログ) | 実装済み(サンドボックス→エージェント実行の end-to-end で検証済み) |
| Delivery(worktree / 実差分 / CI ゲート / マージ) | 実装済み |
| Daily(スケジュール・成果物・外部チケットのプル) | 実装済み |
| エージェントテンプレート(Solo / Graph / Supervisor / Dynamic) | 実装済み・実行可能 |
| 成果物の 4 種出力(テキスト / 画像 / 音声 / 動画) | 実装済み |
| Knowledge タブ(グラフ / 領域図 / スコープ割り当て) | 実装済み |
| ログからのトレース → RAG 最適化ループ | 実装済み(実タスクのランで検証済み) |
「タスク管理から実行まで」に加えて、振り返って直すループもひととおり通るようになりました。ここからは、実際に使いながら細部を詰めていく段階です。
2. フォーカスした課題と、その設計
筆者が課題として捉えたのは、大きく分けて次の 2 つです。
- セキュリティの担保
- コンテキスト最適化の透明性
2.1 セキュリティ — 「拒否する」のではなく「経路を無くす」
MCP をはじめ、あらゆるサービスを繋いで使うようになった結果、情報漏洩が起きうる断面も増えました。そこで本アプリの方針をこう置きました。
外部サービスの利用に必要な秘密鍵は、セキュリティ境界で一括担保できる場所にだけ置く。エージェントには一度も渡さない。
そして実現方法として、ファイアウォールのルールで「拒否」するのではなく、そもそも「到達する経路を無くす」 ことを選びました。
3 つのネットワークと、そこをまたぐ唯一の存在としての gateway。以下はこの図の解説です。
L3(ネットワーク層) — トポロジ自体が主防御
ネットワークを 3 つに分けています。
| ネットワーク | 中身 | 性質 |
|---|---|---|
| host | 見せたくない環境変数・資材があるローカル PC | ホスト |
| orchestra-egress | エージェントが発火・作業する Sandbox 用 | --internal(完全プライベート) |
| orchestra-upstream | 公開ネットワーク(インターネットへ NAT) | 公開 |
肝は Docker の --internal です。--internal なネットワークは、ホストにもインターネットにも一切ルートが張られません。そこに繋がれたコンテナは「島」になり、同じ島の仲間以外、パケットの送り先がそもそも存在しない。
エージェントのサンドボックスは、この egress 島にだけ繋ぎます。すると:
- ホスト(
:8788の管理プロセス等)に届かない — ルートが無いから - 他のサンドボックスに届かない
- インターネットに届かない
ファイアウォールが無いので、ファイアウォールの設定ミスも無い。
唯一の例外が gateway で、これだけが 2 つのネットワークに所属します(= NIC を 2 枚持つ dual-homed)。
gateway のインターフェース(RFC1918 は Docker 自動割当)
eth0 172.20.0.2 <- orchestra-egress (サンドボックスはここで gateway に届く)
eth1 172.21.0.2 <- orchestra-upstream (NAT でインターネットへ)
「あるネットワークで喋るには、そのネットワーク上に足(NIC)を持っていること」が必要——これがそのまま隔離になります。サンドボックスは egress の足しか持たないので、gateway の egress 側 IP には届くが、upstream 側にもその先のインターネットにも足が無い。古典的な multi-homing(ルータ/踏み台がやること)です。
実装者向けの罠:published port は「非 internal なネットワークを primary にした状態」でしか有効化されません。gateway を internal ネットワーク primary で起動すると、-p 127.0.0.1:8787 は記録されるのに実際にはバインドされません(docker port が空になる)。正解は upstream を primary で起動 → -p で公開 → その後 docker network connect egress。
L4(トランスポート層) — 「生 TCP で L7 を迂回できるのでは?」→ できない
よくある反論に、こういうものがあります。
HTTP の上の検査(L7)は、下の TCP(L4)で殴れば抜けられるのでは?
答えは No です。
- TCP は土管(あるポートへのバイトストリーム)で、HTTP はその土管に流す言語の一つ。確かに TCP は HTTP を強制しない
- しかし
:8787で待ち受けているのは Go の HTTP サーバ。そこへ非 HTTP のバイトを流すと400 Bad Requestか即切断。生 TCP を素通し転送する SOCKS / トンネルのモードは持たせていない - つまり「その土管の先で口を利けるのは HTTP だけ」
さらに本質的なのは、L3 の --internal 隔離はプロトコル非依存だということです。TCP でも UDP でも ICMP でも、宛先サブネットへの経路が無ければ 1 バイトも出ません。
| サンドボックスが生 TCP で狙う先 | 結果 |
|---|---|
| gateway(同じ egress サブネット) | TCP は繋がる。ただし HTTP 以外は弾かれる |
| host / インターネット | 経路が無い → 宛先が存在しない |
| upstream の別コンテナ | そのサブネットの NIC を持たない → 指定すらできない |
なお、ホストの UI が gateway を読む経路は 127.0.0.1:8787 の loopback バインドだけです。サンドボックス内の 127.0.0.1 は「サンドボックス自身の loopback」であって、ホストのものではありません。
L7(アプリケーション層) — gateway が「実際にどこへ転送するか」を統治する
L3 が全リクエストを gateway に集約し、gateway がアプリ層で行き先を決めます。ネットワーク間の横断は透過的な IP 転送ではなく L7 プロキシ・ホップです。サンドボックスは gateway に明示的な HTTP リクエストを送り、gateway が upstream 側 NIC から新しいリクエストを張り直す。この間接性が検査を可能にします。
各リクエストが通る関門:
- セッション認証(実行ごとの短命セッション)
-
ルーティング(
/anthropic/*,/github/*,/rag/*…) - ボディサイズ制限 / レート制限 / トークン・コスト予算
- allowlist(正のリスト) — 設定した upstream のみ
-
SSRF / host-reach deny(負のリスト) —
host.docker.internal等の docker ホスト別名、loopback / RFC1918 / link-local の IP リテラルを常にブロック(DNS リバインディング対策で、動的ターゲットは解決してからチェック) - 書き込み認可(deny-by-default) — GitHub は既定で変更を拒否。branch 作成 / PR 作成 / コメントだけ通し、PR マージ・保護ブランチへの push・直コミットは、有効なトークンがあっても拒否
-
鍵注入 — エージェントは
Content-Typeしか付けない。x-api-keyや bearer はgateway が自分のメモリから注入する
依存の取得(npm install / pip install / go mod download)についても、同じ考え方で registry proxy を別に立てています。こちらは GET/HEAD のみ(エージェントはパッケージを取得できてもpublish は絶対にできない)、固定 upstream、認証情報なし。これで「インターネットへの経路が無いサンドボックス」でも依存解決ができます。
なぜ鍵をサンドボックスに置かないのか — L3 では守れないもの
ここが一番書きたかったところです。ありがちな誤解として、
- 「L3 で隔離しているんだから、鍵を env に入れても大丈夫では?」
- 「鍵注入は host 到達を防ぐためのもの?」
どちらも違います。 守っている性質を分けると、この 2 つは直交しています。
| 守る性質 | 担当 | 一言 |
|---|---|---|
| 間違った宛先に到達できない | L3 + SSRF-deny | 「変なところに繋げない」 |
| 秘密そのものが正規の出口から漏れない | gateway の鍵注入 | 「正しい出口からも秘密が出ない」 |
核心はこうです。L3 は任意ホストへの到達を止めるが、gateway が許可した上流には届く。 もし鍵がサンドボックスの環境変数にあったら、侵害された(あるいはプロンプトインジェクションされた)エージェントは、その許可された断面を通じて鍵を持ち出せます。
- worktree のファイルに鍵を書く → レビュー/マージでリポジトリに混入
- 許可された PR 本文やコメントに鍵を埋める
-
モデル API のリクエストボディに鍵を混ぜて送る(
api.anthropic.comは許可済み)
つまり 「鍵をサンドボックスに置く + L3」は「鍵を最初から入れない」と等価ではない。 秘密の値が信頼境界に一度も入らないからこそ、持ち出す対象がそもそも存在しません。
そして「鍵を渡さない」は必要条件であって十分条件ではありません。エージェントは鍵を持たなくても、gateway 越しにその鍵の能力(capability)を行使できる代理人だからです。
実効的な権限 = min(注入される鍵そのもののスコープ, gateway の認可ポリシー)
なので設計原則は least-privilege + attribution + revocability に置いています。源で最小化(細粒度トークン・予算上限)し、実行単位で短命セッションを発行し、その run が必要な上流だけを解錠する。
境界も明示しておきます。gateway 注入がカバーできるのは 「gateway を通る HTTP トラフィック」だけです。AWS_ACCESS_KEY_ID を読んで SigV4 署名する SDK や SSH のようなネイティブ認証・非 HTTP には、ヘッダ注入は届きません。ここは (a) その通信も gateway を署名プロキシとして通す、(b) どうしても env に置くなら短命・最小権限を前提にする、のどちらかになります。
セキュリティ設計は 1 本の記事にまとめてあります(層ごとの根拠と反論への回答をもっと丁寧に書いています)。
まとめ(多層防御)
| 層 | 手段 | 保証 |
|---|---|---|
| L3 ネットワーク |
--internal egress 島 + dual-homed gateway |
サンドボックスはホスト/インターネット/他コンテナへ経路が無い |
| L4 ポート | gateway は 127.0.0.1:8787 のみ公開 |
ホスト↔gateway は loopback の一ドアだけ |
| L7 アプリ | 認証 → allowlist → SSRF-deny → 書込認可 | 行き先を統治。gateway を踏み台にした host 到達も塞ぐ |
| 鍵注入 | 秘密は gateway のメモリだけ、出口でだけ注入 | 秘密の値が境界に入らない=正規の出口からも流出不能 |
| 監査 | 単一 chokepoint + ハッシュ連鎖 | 迂回不能・改ざん検知可能な全 I/O 記録 |
覚えて帰ってほしいのは、2 つの直交する性質です。
- 到達不能(L3) — エージェントは間違った宛先に届かない。ルールで拒否ではなく、経路が無いから。
- 非流出(鍵注入) — 秘密は正しい宛先を通じても出ていかない。値が境界に一度も入らないから。
2.2 コンテキスト最適化 — 「何が文脈に入ったか」を可視化して直す
前提:コンテキストには粒度がある
- システムプロンプト(エージェントテンプレートが持つ)
-
RAG(リポジトリ内のドキュメント) —
docs/配下など、フォルダを指定 - RAG(RAG サーバ経由で取得するもの) — ベクトルインデックス + 人間が引いた関係グラフ
このうち、事故が起きるのはたいてい 3 つ目です。「なぜこれが文脈に入ったのか」が分からないと、直しようがない。
インデックス / エンティティ / スコープの整理
設計にあたって、次のように抽象化しました。
| 概念 | 役割 |
|---|---|
| reference | 参照データそのもの(local ファイル / external な HTTPS ドキュメント)。ベクトル化される実体 |
| index | reference への引き方。1 つの reference に複数付く |
| ├ group | ユーザーが明示する「範囲」。relation の端点にもなる |
| └ relation | 参照先どうしの関係性。ユーザーが明示的に引く線 |
| scope | RAG を引く前に、タスクごとの取得可能範囲を制限する。global / project / organization
|
ここが設計上の肝でした。
ベクトルを張っても、最終的に読まれるのは元ファイルそのもの。インデックスは「たどり着く手段」であって、データの実体ではない。
そして group はタグではなく領域として扱います。多重に重なるし、入れ子にもなるし、実体のないただの概念も group になれる。だから UI でも円(領域)として描くことにしました。
階層は organization ──1:N──▶ project ──1:N──▶ group。タスクは project に属し、参照可能な group タグの配列を持ちます。
relation は検索にも効く(が、権限は絶対に越えさせない)
relation はグラフに線が描かれるだけのものではなく、検索したときに「繋がっている範囲」も引っ張ってくるために使います。
1. 通常どおりベクトル検索して上位を取る
2. ヒットしたチャンクが属する group を集める
3. relation で繋がっている group を集める
4. 許可されている group だけに絞る ← この順序が絶対
5. 絞った範囲で「同じクエリで」再検索し、上位 m 件を足す
4 の順序が設計上いちばん重要です。relation を辿った結果として許可外の group が返ってしまうと、線を 1 本引くだけで権限を迂回できてしまう。relation は「何を取ってくるべきか」のヒントであって、「何を見てよいか」ではない。
5 で再検索するのは、繋がっている group から適当に詰めるとただのノイズになるからです。relation は候補を広げるだけで、順位付けは最後まで類似度に任せます。
なお実装上は、gateway がセッションに紐づく group をヘッダで宣言し、ragindex がそれで絞り込みます。エージェントは自分の権限を偽装できません(申告するのはエージェントではなく gateway なので)。
さらに UI 上で徹底したのが、「未指定」と「空」は正反対という表示です。
| 設定 | 意味 |
|---|---|
| 未指定 | ポリシー無し = 全件参照できる |
| 空 | 何も参照できない |
この 2 つを取り違えると権限が反転するので、明確に区別しています。
「どこまで広げるか」はタスクではなくテンプレートに置く
| 設定 | 置き場所 | 理由 |
|---|---|---|
| group タグ(何を見てよいか=権限) | タスク | タスクごとに変わる |
| 広げる量(どう取ってくるか=戦略) | エージェントテンプレート | 「広く見て推論するタイプか」は働き方の設定だから |
そして設定値は「深さ(何ホップ)」ではなく、relation 経由で追加するチャンクの上限数にしました。深さはグラフの形に依存しすぎて、密なグラフの 1 ホップが疎なグラフの 3 ホップより多くなってしまうためです。上限数なら形が変わっても挙動が読める。深さは暴走防止の上限としてだけ残しています。
RAG 設定の UI は 3 つのモードに分けた
同じデータの別の見え方であって、別のデータではありません。
| モード | 見せるもの | 主な操作 |
|---|---|---|
| ノード設定 | 点 = reference、線 = ベクトル的な近さ(機械が計算した近さ) | 探索・確認 |
| 意味設定 | group を円で描き、その中にノードが入る。relation は矢印(人間が宣言した意味) | group / relation の作成・編集 |
| 所属割り振り | organization / project / group への割り当て、scope の操作 | 権限の割り振り |
ノード設定 が「機械が計算した近さ」、意味設定 が「人間が宣言した意味」。この対比が視覚的に伝わることを狙っています。
点が参照先、線がベクトル的な近さ。しきい値と本数はスライダーで変えられます。
同じノードの上に、人間が宣言した group(領域)と relation(矢印)を重ねたところ。横断的な group がサービス別の group を跨いでいるのが、タグではなく領域にした理由です。
ここで選んだノードが、そのまま検索の参照範囲になります。
ベクトルの運用コスト
実運用の見積もりも置いておきます。
-
データ量:ベクトル本文 : 元テキスト ≒ 1.7 : 1
(text-embedding-3-small= 1536 次元、チャンクは既定 1200 rune 想定) - 読み取り時間:5 万チャンクで数十 ms 程度(Go 実装での想定)
再埋め込みの暴発を防ぐため、キーをこう置いています。
key = sha256(チャンク本文 + embedModel + maxChunkRune)
こうすると、埋め込みモデルやチャンクサイズを変えたときだけ再埋め込みが走ります。
最適化ループ
最終的にやりたいのはこれです。
- エージェントを実行する
- 成果物と、そこに至るまでの A2A セッションログが残る
-
Audit→Knowledgeへトレースし、どのナレッジに到達したかを見る - その場で直す
- システムプロンプトを、テンプレートの粒度ごとに修正する
- RAG を修正する(元のマークダウンを直す / ベクトルインデックスに載せる / group・relation を引き直す)
- 次の実行が良くなる
「ログを見る画面」と「直す画面」が別々だと、見ていたものを忘れてしまう。だから同じ画面から直せることにこだわっています。
3. 感想
作ってみて一番強く感じたのは、AI エージェントの品質問題の多くは、モデルではなく“境界の設計”の問題だったということです。
- 変な出力をする → 文脈に何が入ったのかが見えていない
- 怖くて使えない → どこに繋がっているのかが見えていない
どちらも「見えない」が根本にあって、しかも見えるようにするにはアプリ側の構造を変えるしかない。プロンプトを工夫しても、そこは変わりませんでした。
技術的に面白かったのは、セキュリティと監査とコンテキスト最適化が、同じ 1 つの chokepoint に収束したことです。全ての出入りが gateway を通る、という設計を選んだ時点で、
- 鍵を注入する場所も、
- 全 I/O を記録する場所も、
- 「どのナレッジが返ったか」を記録する場所も、
全部そこになりました。セキュリティのために作った構造が、そのまま最適化のためのデータ源になっている。これは設計を始めた時点では予想していなくて、素直に嬉しい発見でした。
一方で、正直に難しかったのは UI で嘘をつかないことです。「参照した」と書きたいところを「到達した」に直す、「未指定」と「空」を明確に描き分ける、保存時に影響範囲を出す——どれも地味ですが、ここを曖昧にするとユーザーが権限や影響を取り違えるので、機能そのものより時間を使いました。
「ログからのトレース → 最適化」のループまでひととおり通ったので、ここからは実際に使いながら細部を詰めていきます。進捗が出たらまた書きます。
4. 最後に、記事を面白いと思ってくれた方へ
今回紹介したmoecaは個人開発ですが、「AIを安心して使える仕組みをつくる」という考え方は、普段Sapeetで取り組んでいる開発にも通じています。
Sapeetでは、開発の取り組みや、働く人・会社の雰囲気を知っていただくための交流イベント「Open Sapeet」を開催します。
当日は、Sapeetのエンジニアをはじめとする社員が、AIやプロダクト開発、これからの働き方についてお話しします。軽食とドリンクを片手に、社員と気軽に交流できる時間も用意しています。
技術の話をもう少し聞いてみたい方や、Sapeetのメンバーと話してみたいと思っていただけた方は、ぜひお気軽にご参加ください。
イベント詳細・お申し込みはこちら
(本記事の設計は、個人開発中のセキュア・マルチエージェント基盤 moeca のものです。)

















