AIニュース最前線
最新ニュースAI日報Hacker日報週報動画AIツールトレンド企業

AIニュース最前線

世界中のAI最新情報を日本語で毎時更新

最新ニュース日報トレンド企業プレミアムRSS
© 2026 ainew.jp特定商取引法に基づく表記
ニュース一覧元記事を開く
Mercari Engineering·2026年6月10日 12:00·約16分で読める

AI と作る LP エディタ EGP Code を支える 4 つの仕組み

#LLM Agent#Reinforcement Learning from Human Feedback#Tool Use#Context Window Management#Merpay
TL;DR

メルペイが公開した AI エージェントによる LP 自動生成エディタ「EGP Code」の内部アーキテクチャと、再帰的推論・リアルタイム反映・ローカルテストなどの 4 つの核心技術について詳述するエンジニアリング記事。

AI深層分析2026年6月10日 12:01
4
重要/ 5段階
深度40%
5
関連度30%
4
実用性20%
5
革新性10%
3

キーポイント

1

エージェントの再帰ループによる複雑タスク処理

単純な修正だけでなく、推論→ツール実行→結果確認を繰り返す再帰ループにより、複数要素への指示やテスト修正など複雑な要件も自律的に解決する仕組み。

2

ZDR 対応のステートレス履歴管理

データ保持ポリシー(ZDR)遵守のため、プロバイダ側での会話保存を廃止し、自前で履歴を管理・要約してコンテキストを圧縮する方式へ移行。

3

ブラウザ完結型のテストランナー

サーバーを経由せずブラウザ内で完結するテスト実行環境を提供し、AI が生成したコードの即時検証とデバッグを可能にしている。

4

プレビューとソースマップの双方向連携

視覚的なプレビューと HTML ソースコードを対応表で紐付け、ユーザーの操作と AI の編集内容をリアルタイムに同期させる仕組み。

影響分析・編集コメントを表示

影響分析

本記事は、単なる AI ツールの紹介に留まらず、セキュリティ要件(ZDR)を遵守しつつ高度な自律推論を実現するための具体的なアーキテクチャ設計を示しています。特に、履歴管理の自前化やブラウザ内テストの実装など、実運用レベルで AI エージェントを安定稼働させるためのエンジニアリング知見は、業界全体にとって非常に参考になる実践的な内容です。

編集コメント

セキュリティ要件である ZDR の遵守と、高度な AI エージェントの自律性を両立させるための具体的な実装手法が詳述されており、実務で AI エージェントを構築するエンジニアにとって非常に示唆に富む記事です。

こんにちは。メルペイのフロントエンドエンジニアの @mattsuu です。この記事は「Merpay & Mercoin Tech Openness Month 2026」の7日目の記事です。

EGP Code は、ランディングページ(LP)を AI と作る社内向けのエディタです。作成背景については AI と作る HTML ベースの LP エディタ EGP Code を内製した理由 という記事で紹介しました。本記事では、その内部で動いている 4 つの仕組みを紹介します。

EGP Code が扱うのは、HTML と状態や動きを担う少数の ``(独自の Web Components)を混ぜた 1 枚のページです。

code

  春のキャンペーン
  応募受付中
  応募する

この HTML を AI エージェントとの対話やエディタで編集し、プレビューで確認して公開します。紹介する 4 つの仕組みは、(1) エージェントの再帰ループ、(2) Firestore を介したリアルタイム反映、(3) ブラウザだけで完結するテストランナー、(4) プレビューと HTML を結ぶ対応表です。

1 つの指示がユーザーから届いてプレビューに反映されるまでの流れと、それぞれの仕組みが効く場所は次のとおりです。

1 つの指示がブラウザからサーバの AI エージェントへ渡り、その編集が Firestore を介してブラウザへリアルタイムに反映され、プレビューに届くまでの全体フロー図。ブラウザ側にプレビュー・HTML ソースマップ・ブラウザ内テスト、サーバ側にエージェントが置かれている。
1 つの指示がブラウザからサーバの AI エージェントへ渡り、その編集が Firestore を介してブラウザへリアルタイムに反映され、プレビューに届くまでの全体フロー図。ブラウザ側にプレビュー・HTML ソースマップ・ブラウザ内テスト、サーバ側にエージェントが置かれている。

仕組み 1: 文脈とツールを束ねるエージェントの再帰ループ

「フォントサイズを 24px にして」「文字を太くして」のように特定の要素への簡単な指示なら、その要素を特定して CSS を更新したり文言を調整したりするだけなので、ほぼ 1 回の操作で終わります。一方で複数の要素にまとめて指示したり、API を使った画面やテストを作ったり、Lint・テストのエラーを直したりする場合は、推論とツール実行を何度か往復することになります。

エージェントは「推論 → ツール実行 → 結果を会話履歴に追加 → 再び推論」という再帰ループで動きます。ここでいうツールとは、AI が必要と判断したときに呼べる関数の定義と説明のことです。HTML を書き換える、ファイルを読む、Lint で検証する、テストを走らせる、といった操作をツールとして用意しておき、モデルがその中から必要なものを選んで呼び、戻り値を見て次の手を考えます。

サーバがシステムプロンプト・ユーザーの指示と現在の HTML・会話履歴・ツール一覧の 4 種類の入力を束ねて LLM へ送り、LLM がツールを選んで実行し、結果を会話履歴に積んで次のラウンドへ再帰する 1 ラウンドの流れ図。ツールが不要になれば回答して完了する。
サーバがシステムプロンプト・ユーザーの指示と現在の HTML・会話履歴・ツール一覧の 4 種類の入力を束ねて LLM へ送り、LLM がツールを選んで実行し、結果を会話履歴に積んで次のラウンドへ再帰する 1 ラウンドの流れ図。ツールが不要になれば回答して完了する。

1 回の入力に対して 4 つの情報をまとめて渡します。システムプロンプトで AI エージェントに役割やコード生成のルールといった前提を与え、それにユーザーの指示と選択要素と現在の HTML、これまでの会話履歴、そして使えるツールの一覧を教えます。このうち現在の HTML・仕様・テストといったページの状態は、種類ごとに XML タグで区切ってまとめます。

会話履歴の持ち方は、開発当初 OpenAI API 側に任せていました。しかし全社的に ZDR(Zero Data Retention) を適用することになり、プロバイダ側に会話を残せなくなったため、いまは履歴をすべて自前で記録し、毎回のリクエストに載せて送るステートレス方式にしています。履歴の肥大化を防ぐため、一定量を超えたら要約させてコンテキストを圧縮しています。

code
// 1 ラウンドの推論
const stream = await client.responses.stream({
  ...args,
  input: sessionBuffer.getItems(), // 自前で管理している履歴全体を送る
  previous_response_id: undefined,
  store: false, // プロバイダ側に会話を保存させない
});

ループの工夫をいくつか紹介します。

1 つ目は、ツールの失敗の扱いです。ここでいう失敗とは、apply_patch">apply_patch の差分が当たらない、Lint がエラーを返す、テストが落ちるなど、ツールが期待どおりに完了しなかった状態を指します。こうした失敗ではループを止めず、エラーの内容をそのまま結果としてエージェントに返すことで自己修正させています。

2 つ目は、find_skill ツールによる情報の出し分けです。社内 API の使い方などのドキュメントは、最初は ID と一行説明の一覧だけを見せておき、本文は必要になったタイミングで読み込みます。たとえば「商品一覧を表示したい」という指示が来ると、エージェントはまず一覧から関連しそうなものを探し、find_skill でそのドキュメント本文を取得します。エージェントは取得したドキュメントを読み、正しい引数で API を呼びます。

「商品一覧を表示したい」という指示に対し、エージェントが API の一覧から候補を探し、find_skill でドキュメント本文を取得し、正しい引数で API を呼ぶまでの流れ図。
「商品一覧を表示したい」という指示に対し、エージェントが API の一覧から候補を探し、find_skill でドキュメント本文を取得し、正しい引数で API を呼ぶまでの流れ図。

この推論とツール実行の往復を繰り返し、最後にエージェントが apply_patch で HTML を差分更新すると 1 つの指示が編集として完成します。

ループの途中で方向を変える Real-time steering

ここまでは、1 つの指示を最後まで処理してから次を受け取る前提でした。ですが実際には、処理の途中で「やっぱり色は青にして」と方針を変えたり、「ついでにフッターも直して」と指示を足したくなることがあります。完了を待たずに割り込みで指示を足し、走っているループに後から反映する仕組みを用意しています。こうした仕組みは Real-time steering とも呼ばれます。

ユーザーが処理中に指示を足したときの流れは、次のようになります。

ユーザー・Firestore・エージェントを結ぶフロー図。ユーザーの指示でエージェントが推論とツール実行を繰り返し、追加の指示は Firestore に保存され、エージェントがループの途中でそれを読み取って、ツール実行を止めて割り込みを処理する。
ユーザー・Firestore・エージェントを結ぶフロー図。ユーザーの指示でエージェントが推論とツール実行を繰り返し、追加の指示は Firestore に保存され、エージェントがループの途中でそれを読み取って、ツール実行を止めて割り込みを処理する。

エージェントが処理中(画面がローディング中)にユーザーがメッセージを送ると、クライアントはそれを通常の指示ではなく、割り込みメッセージとして送信します。サーバは受け取った割り込みメッセージを、 Firestore の通常の会話履歴とは別のサブコレクションに、いま走っているリクエストの ID を添えて書き込みます。エージェントは、自分が処理しているリクエストの ID に一致する割り込みだけを読み取ります。

code
// 自分のリクエスト宛の割り込みメッセージだけを読み、読んだら消す
const consumeSteeringMessages = async (conversationId, requestId) => {
  const snapshot = await steeringRef
    .where('requestId', '==', requestId) // このリクエスト宛だけを対象にする
    .orderBy('timestamp', 'asc')
    .get();
  const messages = snapshot.docs.map((doc) => doc.data());
  await deleteDocs(snapshot.docs); // 読んだら消す
  return messages;
};

ID で絞るので別のリクエスト宛の割り込みを拾うことはなく、読んだら消すので同じ指示が二重に効いたり取りこぼしたりすることもありません。処理のループに差し込むため、割り込みが来たときにエージェントが何をしようとしていたかで、対応が 2 つに分かれます。

1 つ目は、ツールを呼ぼうとしていた場合です。そのツールを実行せず、戻り値の代わりに「ツールは実行していません。処理中に新しい指示が届きました」という内容を返します。

code
// ツール実行の直前に割り込みを確認する
const steering = await consumeSteeringMessages(conversationId, requestId);
if (steering.length > 0 && toolCalls.length > 0) {
  for (const toolCall of toolCalls) {
    // ツールは実行せず、戻り値の代わりに割り込みを差し込む
    buffer.pushMessage({
      role: 'tool',
      tool_call_id: toolCall.id,
      content: '[CONVERSATION_STEERING] 処理中に新しい指示が届きました ...',
    });
  }
  continue; // 計画を立て直すため、もう一度推論へ
}

2 つ目は、ツールを呼ばずに、ユーザーへの返信メッセージを作り終えていた場合です。本来ならこれを見せてループが終わるところですが、割り込みが届いたので止めるべきツールがありません。この返信はまだ画面に出していないので破棄し、直前のユーザーの指示に割り込みメッセージを足して送り直すことで、続きの作業を依頼します。

code
// ツールがない場合は、画面に出していない返信を捨てて指示を足す
if (steering.length > 0 && toolCalls.length === 0) {
  buffer.pop(); // まだ画面に出していない返信を捨てる
  const priorUserTurn = buffer.pop(); // 直前のユーザーの指示を取り出す
  buffer.pushMessage({
    role: 'user',
    content: `${priorUserTurn.content}\\n[CONVERSATION_STEERING] 処理中に新しい指示が届きました ...`,
  });
  continue;
}

いずれの場合も、すでに実行した副作用を巻き戻すわけではなく、これから実行するはずだったツールを止めたり、まだ画面に出していない返信を捨てたりして、計画を組み直しています。

仕組み 2: Firestore を指示の受け渡し場所にしたリアルタイム反映

仕組み 1 で見たように、エージェントは複数のツールを往復させて指示に応えるため、編集には時間がかかることがあります。処理が終わるまで画面が何も更新されないと、利用者は反映されたかどうか分からないまま待つことになります。

そこで Firestore SDK を利用して、変更をリアルタイムに受け取れるようにしています。サーバ側は 1 つの操作が終わるたびにその内容を Firestore へ書き込み、ブラウザ側はそれをサブスクライブして即座に検知・反映します。これで自前で WebSocket を張らずに、編集の途中経過をそのままプレビューへ反映できます。

エージェントが何かを書き換えると、サーバは会話ごとのコレクションに、次のようなドキュメントを 1 件追加します。

code
{
  "status": "PENDING",
  "requests": [
    {
      "action": "setHtmlSchema",
      "payload": " ...更新後の HTML... ",
      "reason": "ユーザーの依頼を反映"
    }
  ]
}

ブラウザ側の JS は、Firestore の SDK(onSnapshot)でこのコレクションをサブスクライブしており、PENDING のドキュメントが届くと requests の各操作をエディタの状態へ反映します。たとえば setHtmlSchema なら、エディタが表示している HTML を新しいものに置き換えて、プレビューを再描画します。

サーバとブラウザが直接つながらず Firestore 経由でやり取りする図。サーバが PENDING のアクションを書き込み、ブラウザが onSnapshot で受け取って反映し、COMPLETED と結果を書き戻し、結果が必要なアクションだけサーバがポーリングして読む。
サーバとブラウザが直接つながらず Firestore 経由でやり取りする図。サーバが PENDING のアクションを書き込み、ブラウザが onSnapshot で受け取って反映し、COMPLETED と結果を書き戻し、結果が必要なアクションだけサーバがポーリングして読む。

ブラウザが requests の操作を反映し終えると、その JS が status を COMPLETED に書き戻します。HTML 差し替えなどの「反映だけ」のアクションは、投げたら終わりで結果を待ちません。一方でテスト実行のように結果が必要なアクションでは、ドキュメントが COMPLETED になるまで一定間隔で読み直して、書き込まれた結果を取り出します。取り出した結果はツールの戻り値としてエージェントに返り、それを見て次のツール呼び出しを決めます。

仕組み 3: ブラウザだけで完結するテストランナー

応募ボタンを押したときの API 呼び出しやその結果に応じた表示の切り替え、リンクによる画面遷移といった LP の動作を手動で確認するのは手間がかかります。そこで EGP Code では、こうした動作をテストで確かめられるようにしています。

テストはブラウザ上のエディタで直接書いたり、AI に書かせたりできます。これらのテストは、サーバや CI ではなく、プレビューと同じブラウザの中で実行します。ただし Jest や Vitest は Node.js 上で動くツールなので、そのままブラウザには読み込めません。そこで test / it / describe / expect を提供する小さなランナーを自作しました。アサーションは単体で使える @vitest/expect、DOM 操作は Testing Library をそのまま利用しています。

code
// 自作の test 関数でテストを定義する
test('エントリーボタンで API が呼ばれる', async () => {
  // Testing Library でユーザーのクリックを再現する
  await userEvent.click(screen.getByText('エントリー'));
  // @vitest/expect で結果を検証する
  expect(mockEntry).toHaveBeenCalledWith({ campaign: 'X' });
});

テストが社内 API を呼ぶこともありますが、本番に飛ばすわけにはいきません。そこで iframe 内で window.fetch を差し替え、リクエストはすべてモック関数に通します。モックしていない呼び出しはエラーになるので、本番へ漏れることはありません。

テストの実行は、この iframe を作るエディタのページ(ホスト)と iframe の postMessage のやり取りで進みます。

ホストとテスト専用 iframe の postMessage によるやり取りのシーケンス図。ホストが iframe を生成して srcdoc にテスト用 HTML を流し込み、iframe が自己初期化して準備完了を返し、ホストが実行を指示し、iframe が各テストの開始・終了と全体結果を返す。
ホストとテスト専用 iframe の postMessage によるやり取りのシーケンス図。ホストが iframe を生成して srcdoc にテスト用 HTML を流し込み、iframe が自己初期化して準備完了を返し、ホストが実行を指示し、iframe が各テストの開始・終了と全体結果を返す。

ホストは iframe を作ってテスト用 HTML を流し込み、iframe 側の初期化(window.fetch の差し替えなど)が済むのを待ってから実行を指示します。先に指示が届くと取りこぼすため、必ず「準備完了」を待つようにします。結果は仕組み 2 のアクションでエージェントへ戻ります。失敗していれば、仕組み 1 で触れた自己修正がここで働き、内容を読んで実装を直してもう一度走らせます。

仕組み 4: プレビューの要素と HTML の位置を結ぶ対応表

ここまではエージェント主導の編集を見てきましたが、人が直接手を入れる場面もあります。Code タブを開くと Monaco エディタで HTML を直接編集でき、プレビューでリアルタイムに確認できます。プレビューの要素をクリックしてエージェントへ指示を出したり、Monaco の対応行へジャンプするには、「プレビューの要素と HTML 上の位置」を対応づける仕組みが必要です。

EGP Code のエディタ。左に Monaco エディタの HTML、右にライブプレビューが並び、プレビューで選んだ見出しに対応する HTML の行(h1)へジャンプしている画面
EGP Code のエディタ。左に Monaco エディタの HTML、右にライブプレビューが並び、プレビューで選んだ見出しに対応する HTML の行(h1)へジャンプしている画面

HTML をパースして各要素へ data-egp-src-id(公開ページには残らない内部 id)を注入してプレビューを描画することで、これを実現しています。

code

  春のキャンペーン
  応募する

プレビュー上で「応募する」ボタンをクリックすると src-42 が取れます。クリック先が Web Components の内部要素などで id を持たない場合もあるため、closest">closest で祖先方向に遡って最寄りの data-egp-src-id を探します。

取得した id はエディタが各要素に振った内部 id で、対応表を引くキーになります。対応表はパース時に作る「id → その要素の HTML 上の位置」の Map で、id を引くと文字列オフセットやタグ名が取れます。

code
// 対応表(id → HTML 上の位置とタグ)
Map {
  'src-10' => { range: { startOffset: 0,  endOffset: 130 }, tagName: 'section' },
  'src-42' => { range: { startOffset: 64, endOffset: 110 }, tagName: 'egp-button' },
  // ...
}

クリックされるのは描画後の DOM 要素で位置を持つのはこの対応表です。要素に振った data-egp-src-id をキーにすることで、両者を突き合わせています。この対応表には 2 つの用途があります。1 つ目が Monaco エディタへのジャンプです。

code
// 対応表から位置を取得
const range = await mappingApi.findRangeById('src-42');
// オフセットを Monaco の行・列に変換
const pos = model.getPositionAt(range.startOffset);
// その行をエディタの中央に表示してカーソルを移動
editor.revealPositionInCenter(pos);

取得した id を元に対応表から文字列オフセットを取り出して Monaco の行・列に変換します。その行をエディタ中央にスクロールしてカーソルを移動することでジャンプさせています。

もう 1 つが AI への指示です。data-egp-src-id が付いているのはプレビュー用に描画した HTML だけで、エージェントが読み書きする公開用の HTML には付いていないため、id をそのまま渡しても対応する要素は見つかりません。そこで id は対応表を引くキーとしてのみ使い、エージェントには、そこから取り出した要素の HTML・タグ名・行番号をユーザーの指示文と一緒に XML へまとめて渡します。

code

  
    egp-button
    
    10
    色を赤にして
  

エージェントはこれらを手がかりに HTML の中から対象要素を見つけて指示に対応します。このように指示を XML タグで構造化する書き方は、Anthropic のプロンプトのベストプラクティスの一つとしても紹介されています。

おわりに

一見シンプルに見える AI エディタですが、実際に体験を作ろうとすると、待ち時間が長い、途中経過が見えない、どこを編集したか分からないといった小さなつまずきが積み重なります。EGP Code では、そのつまずきを 1 つずつ潰すために、ここまで紹介した仕組みを裏側で積み上げてきました。AI を組み込むときほど、モデルの手前にある体験と安全性を設計することが重要だと感じています。本記事が、AI を組み込んだプロダクトづくりの参考になれば幸いです。

次の記事は@mikupoさんです。引き続きお楽しみください。

この記事をシェア

関連記事

MarkTechPost★42026年6月11日 07:07

Microsoft SkillOptを用いたインストルメント付きプロンプト最適化の実装、スキル進化分析およびベースライン比較に関するコーディングチュートリアル

MarkTechPost は、Microsoft の SkillOpt リポジトリを設定し、OpenAI 互換モデルに接続してコストを管理しながら SearchQA 最適化パイプラインを実行するチュートリアルを提供している。この手法では、元のシードスキルをベースラインとして評価した後、ロールアウトや反射などのプロセスを通じてスキルを改善する最適化ループを実行する。

Claude Blog★42026年6月3日 09:00

Claude Code の構築から学んだこと:スキル活用方法について

Anthropic は、Claude Code の開発過程で得た教訓を共有し、同ツールがどのように「スキル」機能を活用しているかを解説した。

TLDR AI★42026年5月29日 09:00

Agent Judge:生産環境向けエージェントの長期コンテキスト評価を解決(10 分読了)

TLDR AI が紹介する「Agent Judge」は、検索・検証・適応に焦点を当て、従来の LLM 判定器が苦手とする長期コンテキストや状態保持アクションの評価精度と一貫性を向上させる手法です。

今日のまとめ

AI日報で今日の重要ニュースをまとめ読み

ニュース一覧に戻る元記事を読む