有名テック企業の技術ブログを、ひとつのフィードで。
フィード
38件
宣伝 LayerXでは2026/04/11~26開催の技術書典20でLayerX TeckBook 2を発売します。そちらにも記事を寄稿していますので、もし良ければご一読いただけると幸いです。 techbookfest.org はじめに LayerX Ai Workforce事業部R&Dチームマネージャーの澁井(しぶい)と申します。 本記事はAIエージェントのHuman-in-the-loopを定量評価するための手法やビジネス価値を検討します。 AIエージェントによる業務効率化やソフトウェア開発自動化が進むに従って、AIエージェントのアウトプットを人間が確認してアクションすることが増えていると思います。こうしたAIエージェントに対する人間の確認や行動、承認をHuman-in-the-loopと言います。 Human-in-the-loop(以下HITL)はAIエージェントを安定稼働させるために人間が介在してAIエージェントにフィードバックを送る仕組みです。HITLは安全性や説明責任を担保する重要な仕組みである一方、人間の確認コストも伴います。したがって「減らすこと」自体を目的にするのではなく、リスクに応じて介入の強さとタイミングを設計することが重要です。その評価指標として以下のようなものが考えられます。 HITL回数:AIエージェントの1セッションで発生したHITLの回数。 HITL必要率:HITLを実施した回数のうち、本当にHITLが必要だった率。 HITLの待機時間:人間がHITLにレスポンスするまでの時間。 HITLの承認後エラー率:人間が承認したにもかかわらず、エラーになった処理の率。 HITL見逃し修復コスト:AIエージェントがHITLを起動すべきだったのにしなかった場合の修復コスト。 その他・・・ これらメトリクスについては先日、筆者個人でテックブログで整理して公開しました。 AIエージェントのHuman-in-the-Loopを定量評価する しかし、こうしたメトリクスを正確に計測できるようになっても、それだけでは答えられない問いがあります。 HITLが1件少なかった場合と1件多かった場合、どちらがより深刻な問題か? 個々のHITLは適切でも、全体として人間の判断品質を劣化させていないか? 本当にこのHITLは必要か? これらの問いに答えるには、メトリクスの「計測」を超えた「分析フレームワーク」が必要です。本記事では、評価の非対称性と評価の総体性という2つの分析軸を導入し、HITLメトリクスを実践的な意思決定ツールに昇華させる方法を解説します。 この記事を読むと、以下のことが得られます。 「見逃し」と「過検出」を非対称に評価する損失関数の設計方法 「理想のHITL回数」をどうラベリングするかの実務手順 HITL発生パターンの類型化とHITL間の相互作用の分析手法 前提知識: HITLを運用し評価方法を理解していることを想定しています。 1. 評価の非対称性:「1件足りない」と「1件多い」は同じ重さではない 1-1. RMSEが見落とすもの HITLの回数を評価するとき、最も素朴なアプローチは「理想のHITL回数」と「実際のHITL回数」の誤差を測ることです。RMSE(二乗平均平方根誤差)やMAE(平均絶対誤差)がその候補になるでしょう。または2値分類と捉えて、Precision/Recallで評価するかもしれません。 しかし、これらの指標には本質的な盲点があります。理想から「1件少ない」ことと「1件多い」ことを、同じ大きさの誤差として扱ってしまう点です。 RMSEの式を見ればこれは明らかです。 二乗によって誤差の「方向」が失われるため、+1の誤差と−1の誤差は同じ重みになります。 実際にはこの2つは質的にまったく異なる問題を引き起こします。 HITLが少なすぎる場合(下振れ) は、人間が確認すべきだったリスクの見逃しです。見逃されたリスクは、障害、データ損失、セキュリティインシデント、法的問題など、損害が非線形に拡大する可能性を持ちます。本番DBへの破壊的マイグレーションが人間のレビューなしに実行されれば、その1件の見逃しの損害は計り知れません。 HITLが多すぎる場合(上振れ) は、人間の工数浪費とエージェントの待機時間増大です。これは確かにコストですが、その損害は比較的線形で予測可能です。不要なHITLが10件あれば、おおよそ10件分の人件費と待機コストが失われます。 つまりHITLの誤差は、方向によって損害のスケールが根本的に異なるのです。下振れは「爆発的なリスク」、上振れは「じわじわとしたコスト」。この非対称性を無視した評価指標は、最も重要な情報を捨てています。 1-2. 非対称な損失関数で評価する 誤差の方向に応じて異なるペナルティを与える「非対称損失関数」を導入します。 まず誤差 を以下のように定義します。 なら下振れ(HITLが足りない)、 なら上振れ(HITLが多すぎる)です。この に対して、方向別にペナルティを変える損失関数を設計します。 ここで3つのパラメータが登場します。 :下振れペナルティ係数(リスク見逃しの重み) :上振れペナルティ係数(工数浪費の重み) :下振れの非線形指数 設計の要点は以下の3つです。 第一に、 とする。 リスク見逃し(下振れ)の損害は、工数浪費(上振れ)の損害よりも一般に大きいためです。本番環境での障害1件の損害は、不要なHITL10件分の人件費を容易に超えます。 第二に、 とする。 下振れが連続すると、リスクが単純に積み上がるのではなく複合的に拡大するためです。見逃しが1件なら軽微な修正で済むかもしれませんが、5件連続で見逃すとシステム全体が不整合な状態に陥り、修復コストが加速度的に増大します。 であれば、見逃し5件の損害は見逃し1件の25倍として評価されます( )。一方、上振れ側は指数が1(線形)です。不要なHITLが3件でも5件でも、損害は件数に比例した工数浪費であり、相互に増幅し合うことは少ないからです。ただし、Fatigue効果による判断品質の劣化という間接的な増幅はありえます。これについては第2章で後述します。 第三に、パラメータはHITLカテゴリごとに変える。 セキュリティ関連のHITLであれば として見逃しを極端に厳しく評価し、ドキュメントの体裁確認であれば として上振れとの差を小さくする、という具合です。 なお、これらの数値はあくまで初期値です。実際の運用では、過去のインシデントデータや事後レビューの結果をもとに校正していく必要があります。たとえば「セキュリティHITLの見逃し1件あたりの平均インシデントコストが500万円、不要なHITL1件あたりの人件費が5,000円」というデータがあれば、 の比率に現実的な根拠を与えられます。 この非対称損失関数の形を視覚的にイメージすると、原点(、理想通り)の左側(下振れ)は急峻な曲線が立ち上がり、右側(上振れ)は緩やかな直線が伸びる、左右非対称なV字型のグラフになります。 1-3. 見逃しと過検出の非対称な二値分類 HITLの回数だけでなく、HITLのトリガー判定そのものにも非対称性があります。 エージェントがHITLを起動するかどうかの判定は、本質的に二値分類問題です。そしてこの分類には4つの結果があります。 実際にHITLが必要だった 実際にHITLは不要だった エージェントがHITLを起動した 適切な介入(True Positive) 人間の工数浪費(False Positive) エージェントがHITLを起動しなかった リスク見逃し(False Negative) 効率的な自律実行(True Negative) 機械学習の評価でおなじみのF1スコアは、PrecisionとRecallを等しく重み付けします。しかしHITLの文脈では、False Negative(必要なHITLを起動しなかった=リスク見逃し)の損害が、False Positive(不要なHITLを起動した=工数浪費)の損害を上回ることが多いと言えます。 ここではFβスコアが有効でしょう。 スコアは以下の式で定義されます。ここでの は前節の損失関数の上振れ係数とは別の変数で、FβスコアにおけるRecallの重み付けパラメータです(混同を避けるため以降は「Fβの 」と明記します)。 ここで Precision と Recall はそれぞれ以下です。 Fβの <img src="https://chart.apis.go
こんにちは。LayerX Ai Workforce事業部でSWEとしてインターンをしているYuです。 本記事では、AIの提案をそのまま実装してうまくいかなかった経験や、フレームワークのソースコードを読んで解決に至ったプロセス、そしてその過程で感じたことについてお話しします。 はじめに みなさんは、普段開発をするときに、Coding Agentを使っていますか? Claude Codeがリリースされてから、Coding Agentの流れが加速したように感じており、最近では自分自身でコードを書くこともかなり減ってきています。自分はソフトウェア開発を始めて2年ほどなのですが、自分の成長スピードを遥かに超える速度で進化するCoding Agentを見ていると、自分が学ぶ意味とは?と思ってしまうことが時々あります。 そんな中、自分なりに学ぶことの楽しさや重要性を感じたタスクがあるので、そのお話について書きたいと思います。 発生していた問題 Ai Workforce事業部では、バックエンドとしてPythonのFastAPIを使用しています。 ある時から、ローカルでの開発中にホットリロードやサーバーシャットダウンが確率的にうまくいかないという現象が生じました。同じ操作でも成功したり失敗したりするため、再現性がなく原因の特定が難しい状態でした。 本番環境での影響は特になかったのですが、開発時にホットリロードがうまくいかないというのは地味に不便で、開発生産性に影響が出ていたため、改善しようということになり自分が取り組むことになりました。 まずAIに聞いてみた タスクに着手した直後は、SSE(Server-Sent Events)周りが原因になっていそうだなと、なんとなく予想していました。ただ、「SSEが怪しい」以上の仮説を立てられる状態ではなく、ましてやホットリロードやシャットダウンの仕組み自体を正確には理解していませんでした。 そこで、普段から使っていたコーディングエージェントに聞いてみたところ、on_event("shutdown") でSSE接続を止めるという提案をしてもらいました。 on_event("shutdown") について簡単に説明すると、FastAPIが提供する「アプリ終了時に呼ばれるフック」です。 つまり、提案された方針は、シャットダウン時にこのフックの中でフラグを立て、SSEのループを終了させるという方針です。 それっぽい方針だったので実装してみたのですが、、、うまくいきませんでした。 なぜうまくいかなかったのか。この時点の自分にはわかりませんでした。AIの提案が「もっともらしかった」からこそ、何が間違っているのかの見当がつかなかったのです。 FastAPIのシャットダウンの仕組みを理解する AIの提案で解決できなかったことで、そもそも自分がシャットダウンの仕組みを理解していないことが問題なのではないかと思いました。 そこでまずは、ソースコードを読んで、仕組みを理解することにしました。 FastAPIは内部的には uvicorn というASGIサーバーの上で動いています。 今回の問題はシャットダウン処理に関わるものだったため、uvicorn のソースコードを読むことにしました。 コードを追っていくと、シャットダウンはおおよそ次の順番で行われていることがわかりました。 SIGTERM / SIGINTの受信 新規リクエストの受付停止 アクティブな接続が閉じるのを待つ(ここにSSEのような長寿命接続も含まれる) lifespanのshutdown(on_event("shutdown") など) 実際の処理も、ざっくりいうとこの順番で実行されています。 # uvicorn/server.py(簡略化・抜粋) async def shutdown(...): # 新規接続の受付を停止 server.close() # 既存の接続が終わるのを待つ await self._wait_tasks_to_complete() # アプリケーションのshutdown処理 await self.lifespan.shutdown() ※より詳細な実装はこちらをご覧ください ポイントは、lifespanのshutdownが呼ばれるのは一番最後という点です。 また、ホットリロード時の挙動についても確認しました。 uvicornのリロード機能では、サーバーはサブプロセスとして起動されており、ファイル変更が検知されるとそのプロセスに対して SIGTERM が送られます。 つまり、ホットリロードの実体は「プロセスの再起動」です。 原因の特定と、なぜAIの提案ではうまくいかなかったのか シャットダウンの仕組みを理解した上で改めて問題を整理すると、原因は明確でした。 SIGTERM / SIGINTの受信 新規リクエストの受付停止 アクティブな接続が閉じるのを待つ ← SSE接続が無限ループで生き続けているため、ここで止まる lifespanのshutdown ← ここに到達しない AIが提案した on_event("shutdown") は、ステップ4で実行される処理です。しかし、問題はステップ3で発生していました。SSE接続が閉じないからステップ4に進めない。ステップ4でSSEを止めようとしても、そもそもそこに辿り着けないのです。 また確率的にシャットダウンがうまくいかない原因もここで判明しました。SSE接続が必要な画面を開いていない場合には、SSE接続を閉じる必要がないため、問題なくステップ4に進むことができる。 つまり、問題なくシャットダウンできるかどうかは、SSE接続の有無次第なため、確率的な挙動に見えていました。 この「順番」がわかった瞬間、解決策も見えました。 ステップ4の on_event("shutdown") で対応するのではなく、もっと手前、シグナルを受け取った時点でSSE接続に終了を通知する必要があります。 具体的には、SIGTERM / SIGINT を受けたタイミングで、SSEのループが自発的に終了できるようにフラグを立てる方針にしました。ここまで解像度が上がっていると、AIに対して具体的な指示を出すことができ、一発で修正をすることができました。 実装としては、uvicornが内部で登録しているシグナルハンドラをそのまま置き換えるのではなく、その前段で少しだけ処理を挟む形にしています。 シグナルを受けたらまずSSE側に終了通知を送り、その後で元のハンドラに処理を委譲する、という流れです。 これによって、アクティブなSSE接続が終了待ちで詰まる前にループを抜けるようになり、その後の通常のシャットダウン処理や lifespan によるクリーンアップも、これまで通り正しく実行されるようになりました。 修正自体は数十行程度で、仕組みさえ理解してしまえばシンプルなものでした。 まとめ Coding Agentはすごいスピードで進化していて、開発スタイルを大きく変えつつあります。かく言う自分自身も日々助けられており、すでにCoding Agentなしでの開発は想像ができません。 ただ今回、AIの提案をそのまま実装してうまくいかず、ソースコードを読んで解決したという経験を通じて、「原理を理解すること」の大切さを改めて実感することができました。シャットダウンの順番という、たった一つの知識があるかないかで、AIの提案が正しいかどうかの判断や、正しい解決策を導くためのAIへの指示の出し方も、全てが変わってきます。 コードを自分で書く機会は減っていくかもしれませんが、技術を理解しようとする姿勢はこれからも持ち続けたいと思います。そして幸いなことに、理解するためのハードルもAIによって下がっています。今回のuvicornのソースコードも、基本的にはAIと並走しながら理解を進めました。 また、シンプルに自分の知らないことを理解するというのは非常に楽しいことだという、初心を思い出すこともできました。 AIなどのツールをうまく活用して、自分自身の成長に繋げていきたいです。 おわりに LayerXでは「Bet AI」を掲げ、全社でAIの活用と新しい開発スタイルの模索を推進しています。圧倒的なスピードで進化するAIを楽しみながら、自らの技術力もアップデートし続けたいエンジニアインターンを絶賛募集中です! ▼ 技術を楽しみながら確かな成長を目指すエンジニアインターンに興味のある方はこちら open.talentio.com ▼ 「Bet AI」を掲げるエンジニア組織の裏側をもっと知りたい方はこちら speakerdeck.com 技術への知的好奇心を絶やさず、AIと共に新しい開発の形を模索したい方、まずはカジュアルにお話しできるのを楽しみにしています!