有名テック企業の技術ブログを、ひとつのフィードで。
フィード
38件
こんにちは。LayerXで「Ai Workforce」というプロダクトのプロダクトマネージャーをしているinaoです。 Ai Workforceは、組織の中でAI Agentを活用するためのプラットフォームです。 getaiworkforce.com おかげさまで、我々の組織もお客様もユースケースも拡大を続けていて、AI Agentとその周辺機能を日々企画・開発しています。そのなかで、最近とくに議論が長引くテーマがあります。 権限管理です。 社内Slackで権限の話を始めると、毎回スレッドが伸びます。私もさまざまな方と意見交換するたびに新しい視点が得られ、とても面白い領域だと感じています。(そういう意味で、社内的にいまアツい、です) 視点が増え、論点が分岐し、さらにスレッドが伸びる。なぜこんなに難しいのか。そして、なぜ「いま」これを考える意味があるのか。この記事では、権限管理を 過去(これまでの前提)/現在(各社の動向と現場の論点)/未来(向かう先) の時間軸で整理してみたいと思います。 最初にお断りしておくと、Ai Workforceは「さまざまな業務を支援・遂行するためのAI Agentプラットフォーム」という位置づけです。そのため本稿も、特定の理想形に絞らず、「さまざま」を扱うための権限管理について、雑多に考えを広げています。その前提で読んでいただけると幸いです。 なぜ権限管理がエージェントの肝になるのか 個人のAIと組織のAI 組織の中にAIを配置し、安全かつ効率的に運用する。人とAIがコラボレーションする。そう考えたとき、設計の肝になるのは「どの権限を、誰に、どこまで、どうやって渡すか」です。 ここが決まらないと、エージェントは安心して仕事を任せられる同僚にはなれません。逆に縛りすぎれば、できることが少なく、柔軟性の低いものになってしまいます。 面白いのは、議論すると人によって意見が様々であることです。理由はおそらく2つあります。 想定しているユースケースは人によって違う。 個人の生産性ツールとしてAIを使う文脈と、組織の共通リソースとして業務を自動化する文脈とでは、最適な権限のかたちはまるで変わります。 個人利用のClaudeやChatGPTに求められることと、組織の中のAgentに求められることも違ってきます。 権限への立ち位置が違う。 業務を設計する人と、業務で扱うリソースを設計する人とでは、「⚪︎⚪︎権限」に対するスタンスが異なります。 しかも、いまだ世の中に存在しないもので、人間にはとてもわかりづらい権限について想像しながら話しているため、人によって思い描く理想像もずれていきます。この課題を完全に解消できるかは怪しいですが、整理することで、議論の足場くらいは作れるはずです。 過去:主に人間を中心に設計されてきた権限管理 これまでのソフトウェアの権限管理は、システム間の連携のようなアクセス制御やルールもありますが、概ね一貫して「人間」を中心に設計されてきました。代表的なものを並べてみます。 RBAC(Role-Based Access Control):アクセスの主体は人間のユーザーで、権限をロールやグループに束ねる。 ABAC(Attribute-Based Access Control):主体(ユーザー/システム)に加えて、対象リソースの属性とポリシーでアクセス可否を決める。 ACL(アクセスコントロールリスト):「誰が見られる/編集できる」をフォルダ・ファイル単位で管理する(Google Drive、SharePoint、Boxなど)。 認可(OAuthなど):アプリがユーザーの代わりに何かするとき、フローを通じて「ユーザーの許可」をもらいアクセストークンを取得する。リソース側からは、ユーザー本人の行動かアプリの行動かをほぼ区別できない。 これらの世界には、共通する暗黙の前提がありました。 行為者は人間か、または人間の意図を忠実に転送するだけのプログラムである。 ボタンを押したのは人であり、アプリはその意図を運んでいるに過ぎない。だから「誰がやったか」は実質的にユーザー本人と等しく、監査ログにユーザー情報が紐づけば十分。 つまり、意図の出どころは常に人間という点です。 現在:エージェントは「自律的な行為者」になった エージェントによって、何が変わったのでしょうか。 一言でいえば、意図を「転送」するのではなく、文脈から意図やアクションを「生成」するようになったことだと考えています。 人間が「この資料をまとめて」と頼むと、エージェントはどの情報を参照し、どう判断し、どんな成果物を作り、どのアクションを実行するかを、自分で決めていきます。たった一回のクリックの後ろで、数十の判断とアクションが連鎖する。ここで、従来の前提が静かに崩れ始めます。 課題1:主体をユーザーのままにすると、「誰がやったか」区別がつかない リソース側(内部データや外部システム)は、「ユーザー本人が操作したのか」「エージェントが自律的に動いたのか」を見分けられません。監査ログ上は同じに見えるため、事故が起きたときに「誰が」やったのかを後から辿れなくなります。 また、ユーザーの権限を継承してエージェントが情報にアクセスしてしまうと、情報セキュリティ上に外部に送ってはいけない情報が外部システムとしてのエージェントに渡されてしまったり、エージェントを通してさらに外に送信されてしまうおそれがあります。 課題2:エージェント固有の権限で動かすと、「できること」がブラックボックス化する エージェントにサービスアカウントを与えてその権限で動かすと、ユーザーの権限とエージェントの権限が乖離します。それで良い場面もありますが、ユーザー本人には許されないことを、エージェント経由で実行できてしまう、ということも起こりえます。 エージェントからアクセスできる範囲やできることがユーザーから見て分かりづらくなってしまったり、ガバナンスが適切に効かせられているかどうか分かりづらくなってしまうかもしれません。 課題3:人への依頼と、エージェントへの依頼は「責任の所在」が違う 「AさんがBさんに作業を依頼する」のと、「AさんがエージェントA′に依頼する」のは違います。Bさんには本人の判断と責任がありますが、エージェントA′が何をどこまでできるのかを、Aさんが正確に把握しているとは限りません。把握できていないことが、トラブルや業務の停滞につながりかねません。 各社のプロダクトはどう設計しているか エンタープライズ向けにAIエージェントを提供する各社も、同じ課題に向き合っているはずです。公開情報をもとにざっくり調べたところ(※すぐ触れる環境がなかったため厳密さは欠きます)、大きく2つに分かれそうでした。 実行ユーザーの権限を継承するタイプ 例:SalesforceのAgentforceのEmployee Agent。 エージェント自体に独立した権限モデルを持たず、実行ユーザーの権限をベースに動く。基本的にはユーザーに許可を求める仕組み。 エージェントを第一級のアイデンティティとして扱うタイプ 例:Google Cloudの「Agent Identity」、Microsoftの「Entra Agent ID」「Agent 365」。 エージェントそのものに身元と権限を持たせる。 どちらが良いではなく、それぞれの体験、汎用性を踏まえて設計されているように見えます。 また、各社のプロダクト以外にも、OAuthやMCPを中心に標準化の動向や議論もあるようです。(ここは話が広がりすぎるので、また別の機会に) いま現場で起きている論点 世の中の動きと同じことが、社内でも論点として立ち上がってきます。 (以下はあくまで社内で出ている論点の一例で、権限管理の一般的な論点を網羅したものではないのでご注意ください) 1. 代行か、依頼か 1つ目は、エージェントが「誰の権限で」「どこまで自分で判断して」動くかという軸です。業務の任せ方には2種類あります。 業務の代行:自分の仕事をそのまま肩代わりしてもらう。エージェントは裁量を持たず、ユーザーの権限の範囲で動く。例:「自分の提案書を仕上げてほしい」 業務の依頼:任された側にも裁量がある。エージェント自身の権限と判断で動く。例:「組織内の情報整理を定期的に行ってほしい」 例えば、「エージェントは自分の権限で動いてくれた方がアクセス範囲やできることが明確」である、という意見もありますし、一方で「誰が実行しても同じ結果が得られるようにエージェントごとに権限が管理が理想」という意見もあります。 ユースケースによってどちらも妥当そうに見えます。どちらをやるにしても一つのプロダクトの中でどう表現し、どう実現できるのか、はまだぼんやりしています。 なお、代行だからといって「ユーザーの全権限をそのままエージェントに渡してよい」わけではありません。本人なら許される操作でも、エージェント経由だと意図しない結果になることがあります(機密情報や受領資料を外部に送信してしまったら、目も当てられません)。代行であっても「どの権限を、どの操作に限って貸すか」の絞り込みは必要です。 2. そのエージェントは「誰のもの」か 2つ目は、論点1とは別の軸で、**そのエージェントが「誰に紐づくアイデンティティなのか」**という話です。 個人に紐づくエージェント:自分の業務のために使うエージェント。誰に許可を取ればよいかが明確で、説明責任もその人に集約されるため、シンプルで安心しやすい。一方で、異動や退職のたびに引き継ぎが発生します。 組織に紐づくエージェント(脱属人化):チーム全体に向けて働く、共通リソースとしてのエージェント。特定個人がいなくなっても残せますが、「誰が責任を持ち、誰が許可を出すのか」を明確にする必要がある。 1つ目の代行・依頼と、この2つ目の誰のものか、は2軸として独立しているかもしれないと考えています。「代行か依頼か(権限の出どころ)」と「個人か組織か(アイデンティティの所在)」は、同じ話に見えますが、たとえば組織に属するエージェントが、実行時には指示者の権限を借りて代行するといった組み合わせも成り立ちます。混同せず別々の話として設計したほうが、選べる形が広がりそうです。 3. リソースをどう安全に分けて扱うか 3つ目は、エージェントが触れるリソースの範囲をどう引くかです。 エージェントを特定のプロジェクト資料やアクションに限定すると、安全性は高まりますが、その範囲でしか動けず、汎用性や、応用力は下がります。 一方で、プロジェクトを横断・俯瞰したい場面も当然あります。 目的によってエージェントが触れる範囲を限定したくなります。 リソース範囲ごとにエージェントをそれぞれ用意する形になるかもしれませんし、用途に応じたスコープを柔軟に構成できると良いのかもしれません。また、用途に応じたエージェントを実行できる主体も異なる形にする必要があるかもしれません。 これらはどれも「最終的には全部必要」という話に着地するかもしれません。 さまざまなユースケースをカバーしたい我々のようなプロダクトでは、汎用性とカスタマイズ性を両立しながら、作り手もユーザーも安心・安全に使いこなせる権限モデルを確立する必要があると考えています。 未来?:権限管理はどこへ向かうのか 足元の課題を解くのはもちろんですが、もう少し先を妄想してみたいと思います。 権限は「リソースの列挙」から「意図・条件の記述」へ 「どのファイル」「どのフォルダ」と一つずつ指定するのではなく、「機密区分が高くないもの」「この案件に関するもの」のように、条件や意味で権限を切り分ける 方向に進むかもしれません。エージェントは大量かつ多様なリソースを必要とするため、人間が一つずつACLを設定する運用はいずれ破綻してしまいそうです。クエリのようなスコープや、属性ベースのポリシーが中心になっていくのではないでしょうか。 権限は連鎖し、その連鎖は検証可能になる 人→エージェント→別のエージェント→外部API、というように多段の依頼や権限委譲が当たり前になりつつあります。各ホップでトークンが渡され、最終的に「誰の意図で、どのエージェントが、何をしたか」を正確に辿れる状態が求められます。複雑な流れであってもアカウンタビリティが保てること自体が、権限設計の要件になっていくはずです。 エージェントは「管理されるアイデンティティ」になる 人が会社に入社・退社する際、アカウント発行、PCセットアップ、アカウント削除、PCリセットといった手続きがあります。同じように、エージェントにも作成・更新・廃棄のライフサイクルが必要になりそうです。「いま組織にどんなエージェントがいて、何ができ、最後にいつ何をしたか」を把握できる状態が、ガバナンスの前提になっていくでしょう。 組織の中で働くエージェントが本当の意味で安心・安全になるには、さまざまな工夫が必要であると思います。 さいごに ここまで、権限管理を「過去/現在/未来」の時間軸で整理してきました。 いまこの論点を考える意味は、エージェントが便利な新機能ではなく、組織の中で意思決定とアクションを連続的に生み出す「新しい行為者」になりつつあるからだと思っています。だからこそ、単に強い/弱い権限を付けるのではなく、次の3つをプロダクトと運用の両面で設計していく必要があります。 誰のために動いたのか どの範囲まで委ねたのか 何をしたのかを、後から説明できるのか 完璧な解はまだ見えていません。それでも、Ai Workforceなりの、「さまざま」な業務に耐える権限モデルを探っていきたいと思います。 さいごのさいごに Ai Workforceでは、プロダクトマネージャーやエンジニアを募集しています。 ご興味のある方は、ぜひカジュアル面談からお気軽にご応募ください! jobs.layerx.co.jp
※本記事は、2026年5月までLayerXのAi Workforce事業部でSWEインターンとして活躍してくれたkawachan(かわちゃん)さんによる執筆です。本人のインターン期間終了に伴い、LayerX の koichi(こいち)が代理で投稿いたします。最先端のAI協業の現場で得られたリアルな学びを、ぜひご覧ください! はじめに こんにちは、kawachan(かわちゃん)です。昨年の12月から、LayerXのAi Workforce事業部SWE(Software Engineer)チームでインターンをしています。 この記事では、Ai Workforce事業部への応募を検討されている方や、当事業部に興味をお持ちの方に向けて、私たちがどのようにAIを活用し、協業しながら開発を進めているのか、インターン生としての実体験を交えてご紹介します。 私の業務内容 当事業部では、FDE(Forward Deployment Engineer)やプロダクトチームのメンバーから寄せられた意見や要望が、チケットとして起票されます。 インターン生もそのチケットの中から主体的に案件を担当し、設計、実装、テストまで一気通貫で行います。私は主に、Ai Workforceの既存機能の改善や、新バージョンに向けた新規機能の開発を担当させていただきました。 AIとの協業体制 Ai Workforce事業部SWEチームでは、AIを単なる補助ツールとしてではなく、強力な開発パートナーとして位置づけています。ここでは、具体的な取り組みをいくつか紹介し、チームの魅力をお伝えします。 1. 圧倒的なAIコーディングツールへの投資 LayerXでは「Bet AI」の行動指針の通り、CursorやClaude Codeといった最新のAIコーディングツールへの投資を惜しみません。私自身、週2日の勤務であるにもかかわらず、潤沢なAI利用環境を提供していただき、その投資の大きさに驚きました。 開発現場では、AIと壁打ちをしながら案件のドメイン知識を高速にキャッチアップし、まずはプロトタイプ(叩き台)を作成してチームへ共有します。認識のズレがあればその場で即座に修正していく、という高速なサイクルが確立されています。 これによりリリース速度が飛躍的に向上し、FDEを介してお客様のフィードバックが迅速に開発チームに届くため、さらなる改善へとつながる非常に良いサイクルが生まれています。AIを用いた高速な実装力は、これからの時代の必須スキルだと身をもって実感しました。 2. AIの活用を前提としたコーディング環境(rulesの運用) AIにただコードを書かせると、チームのコーディング規約や設計方針を無視した実装になりがちで、結果としてレビュワーの負担が増えてしまいます。 この課題に対し、当事業部ではコードのドメイン知識や共通ルールをrulesというディレクトリに集約しています。AIの活用をはじめから前提として開発環境を整備している点が非常に合理的です。 AIは常にこのrulesを参照するため、チームの共通認識に沿った納得感のある実装方針を出力してくれます。また、開発メンバーは誰でも自由にrulesへ知見を追加・更新できるため、AIは常に最新のコンテキストに基づいた精度の高いコーディングを行ってくれます。 3. プルリクエスト(PR)の自動レビュー 開発の中で特に興味深かったのが、GitHubとGreptileやDevinなどのAIツールを連携させた、プルリクエストの自動レビューの仕組みです。 タスクによっては修正が500行を超える大規模なものもあり、人間によるレビュー負担が大きい場合がありました。そのような際、まずはAIが網羅的にレビューを行って細かなバグや規約の課題を潰します。これにより、人間のレビュワーはドメインロジックの妥当性やアーキテクチャの議論など、より本質的なレビューに集中できるようになりました。 4. QAガイドおよびStory Testの自動生成 開発フローは、チケット起票 → 実装 → QA(品質保証)メンバーによる検証 → 本番リリース、という順で進みます。 私が参画して1ヶ月ほど経った頃、QAメンバーに提出するための「QAガイド」をAIが自動生成する機能が導入されました。 これにより、開発者のドキュメント作成負担やQAメンバーの確認コストが軽減されただけでなく、開発者が「QAメンバーがどのような観点で検証を行っているのか」をより深く理解するきっかけになりました。結果として開発とQAの距離が縮まり、気軽に相談し合える文化がさらに強固になっています。AIには、部署間のギャップを埋め、チームの心理的距離を近づける効果もあるのだと実感しました。 インターンを通じて得た2つの大きな学び 1. 「どう作るか」より「何を作るか」の重要性 インターンを通じて最も強く感じたのは、開発現場において「これは本当に必要な機能なのか?」という本質的な議論が、技術的な実装方針の相談よりも圧倒的に多いことです。 AIの進化により、機能を作るコストは劇的に下がりました。だからこそ、「なぜ作るのか」「誰が求めているのか」「今本当に作るべきか」を深く熟考する重要性が増しています。 最近担当したタスクでも、まさにこのことを実感する出来事がありました。あるワークフロー作成機能において、「DSL(ドメイン固有言語)で定義するオブジェクト型のデフォルト値に対し、バリデーション(入力チェック)を強化する」という要件がありました。 しかし、SWEチームの社員の方々と相談し、実際の顧客のユースケースを深く検討する中で、「エラーを出してユーザーに修正を促す(バリデーションの強化)」よりも、「システム側でデフォルト値を自動補完する」方が、顧客の真の課題解決に繋がるのではないか、という議論に発展しました。 このように、言われたものをそのまま作るのではなく、「何を作り、何を解決したいのか」をチーム全体で常に問い直しています。ただがむしゃらにコードを量産するだけでは、使われない機能が増え、プロダクトの認知負荷やメンテナンスコストを高めるだけになってしまいます。FDEやビジネスサイドのメンバーと密に連携し、顧客の真の課題を捉え続けることが、今後のSWEにとって最も重要な役割になると確信しました。 2. 「AI前提の開発」における設計の難しさと、エンジニアの新しい役割 LayerXは日本でもトップクラスにAIを活用している企業ですが、それでも「AI前提の開発」は発展途上であり、新たな課題も見えてきました。 実装スピードが加速した分、設計が疎かになると技術負債が蓄積する速度も比例して早くなります。私自身、数週間前に実装した機能が開発のスピード感に追いつけず、すぐに負債化してしまう経験をしました。 AIが負債を残さず、いつでも切り離しや変更が容易なコードを書くためには、どのような設計やアーキテクチャが必要になるのでしょうか。従来のDDD(ドメイン駆動設計)などに代わる、AI時代に適した新しいアーキテクチャパターンが、今後このチームの試行錯誤の中から生まれていくのではないかとワクワクしています! おわりに LayerXのAi Workforce事業部は、AIの可能性を最大限に引き出し、これまでにないスピード感で社会に価値を届けている非常にエキサイティングな組織です。 「AIが普及していく社会で、エンジニアとしてどう生き残るべきか」という不安を抱えている学生の皆さんにこそ、ぜひこの最先端の開発環境を体験していただきたいです。LayerXでのSWEインターンへの挑戦を、心からおすすめします。
※本記事は、2026年5月までLayerXのAi Workforce事業部でR&Dインターンとして活躍してくれた kemotoさんによる執筆です。本人のインターン期間終了に伴い、LayerX の koichi(こいち)が代理で投稿いたします。最先端のAI協業の現場で得られたリアルな学びを、ぜひご覧ください! -- こんにちは。LayerX AiWorkforce 事業部 R&D チームでインターンしております竹本(@kemotohuman)です。 当事業部では、エンタープライズのお客さま向けに『Ai Workforce』というプロダクトを提供しています。Ai Workforce では、エンタープライズの業務に用いる複雑なドキュメントを「AIの力でいかに正確に処理・分析するか」というテーマに日々向き合っています。 しかし、実務で扱われるドキュメントの解析は一筋縄ではいきません。複雑なグラフや図表、緻密なレイアウトが施されたPDF資料を最新のAIに読み込ませ、「この内容を正確に分析してほしい」と依頼したとき、期待外れの回答にガッカリした経験はないでしょうか。現在のLLM(大規模言語モデル)技術は飛躍的に進化しましたが、依然として「実世界のドキュメント」を真に理解しているとは言い難いのが現状です。 今回は、このような課題を解決するために提案されたフレームワーク「RAG-Anything」について、そのアプローチと仕組み、そして実務への応用可能性を解説します。 元論文:https://arxiv.org/abs/2510.12323 GitHub:https://github.com/hkuds/rag-anything 一般的な情報抽出が抱える課題 複雑なビジネスドキュメントからAIを用いて情報を抽出する際、従来の一般的なシステム(テキストベースのRAGなど)では、主に3つの構造的な壁に直面します。 1. 視覚的・マルチモーダルな情報の欠落 一般的にドキュメントをAIに処理させる場合、まずは一度「テキストデータに変換(文字起こし)」してからLLMに入力するアプローチが広く使われています。しかし、この方法ではグラフの推移やフロー図のように、「視覚的な理解が必要な要素」から情報を正確に抽出することが極めて困難になります。 テキストデータへの変換が情報抽出に悪影響を及ぼす例 ページ全体をそのまま画像としてマルチモーダルLLMに入力するという手段もありますが、レイアウトが緻密で複雑なドキュメントほどノイズが増えてしまい、ピンポイントでの情報抽出において精度が出ないというジレンマを抱えています。 2. 散在する情報や多段階推論への弱さ 回答の根拠となる記述がドキュメント内の1箇所にまとまっていれば良いのですが、根拠が複数かつ離れたページに散在している場合、一般的なアプローチでは抽出の難易度が急激に上がります。 ドキュメント全体の内容を俯瞰し、離れた位置にある複数の要素を整理しながら、多段階で答えを導き出すような高度なタスクにおいて、ナイーブな処理の場合LLMの純粋な処理能力(モデルの賢さ)まかせになってしまい、システム側での制御が難しいという弱点があります。 3. 回答根拠の不透明さとハルシネーション 実務でAIを活用する上で、「AIの回答が、ドキュメントのどの部分に基づいているか」をユーザーに明示することは、信頼性の観点から不可欠です。一般的な情報抽出では「LLMに回答と同時に参照元のテキストも出力させる」という手法がよく取られます。 この場合、LLMがプロンプトの指示を途中で省略してしまったり、実際にはドキュメントにないテキストを出力してしまうハルシネーションのリスクが常に伴います。また、「根拠を正確に出力する」という余分なタスクをモデルに強いることで、本来の情報抽出性能そのものが低下してしまう懸念も存在します。 このように、従来のやり方では「視覚情報の欠落」「散在する情報の整理」「根拠の不透明さ」という3つの壁にぶつかってしまいます。 これらの課題、特に「散在する情報の整理」や「複雑な関係性の欠落」を根本から解決するためのアプローチとして、近年大きな注目を集めているのが「知識グラフ(Knowledge Graph)」の活用です。 次のセクションでは、まずこの知識グラフとLLMを融合させることで何が可能になるのかを詳しく説明します。 知識グラフとLLMの融合 前セクションで触れた「情報の散在」や「複雑な関係性の欠落」といった従来のRAGの限界を突破するための鍵となるのが、「知識グラフ(Knowledge Graph)」とLLMの融合です 。 1. 知識グラフとは? 知識グラフとは、実世界に存在する人・場所・組織・概念などの「エンティティ」と、それらの間にある「エッジ(関係性)」を用いて、情報をネットワーク構造で記述したものです 。 知識グラフの例(はじめての知識グラフ構築ガイド より抜粋して作成) 例えば上の例だと、「KarlとFredは友達(FRIEND)同士で、2人とも東京(Tokyo)に住んでいる(LIVES_IN)」という情報を、それぞれの人物や場所をノード(丸)で表し、それらの関係を矢印(エッジ)で繋いで構造化して表現しています。 こうしたデータはNeo4jなどの専用のグラフデータベースで管理され、Cypherなどのクエリ言語を使って操作されます。 知識グラフは情報同士の複雑なつながりや暗黙的な関係性を明示的かつ正確に表現できる一方で、データの構築や運用のハードルが高い点が長年の課題でした。 2. LLMとの統合 近年のLLM(大規模言語モデル)の発展は、知識グラフの構築や活用のあり方にも大きな変化をもたらしています。 まず大きな変化として挙げられるのが、LLMによって知識グラフの構築が圧倒的に容易になった点です。従来、テキストからエンティティやエッジを抽出してグラフデータを構築するには、固有表現認識(NER)をはじめとする複雑な自然言語処理の技術をいくつも組み合わせる必要があり、開発や運用のハードルが非常に高いものでした。しかし現在では、LLMに対してプロンプトで「この文章から要素と関係性を抽出して」と指示するだけで、柔軟かつ制御しやすい形でデータをパースできるようになり、構築のコストが大幅に下がっています 。 一方で、こうして構築した知識グラフを、今度はLLMのコンテキストとして与えて活用するアプローチも進化しています。これが、近年検索拡張生成の発展形として注目されている「GraphRAG」です 。LLMに構造化された知識グラフを組み合わせることで、従来のテキストを細切れにして検索するだけの方法では解けなかった、ドキュメント内の離れた情報を跨いで整理するような、高度で複雑な多段階推論が可能になります 。 3. 従来のRAGとGraphRAGの違い Naive RAGとGraphRAGの比較(「**When to use Graphs in RAG: A Comprehensive Analysis for Graph Retrieval-Augmented Generation**」 より抜粋して作成) ドキュメントの検索において、一般的なテキストベースの従来のRAG(Naive RAG)と、知識グラフを活用したGraphRAGには決定的なアプローチの差があります 。 Naive RAGの限界 :ドキュメントを一定の長さでチャンク化し、質問文とのベクトルの類似度(セマンティック検索)で関連箇所を探します 。しかし、この方法では情報同士の暗黙的な関係性が削ぎ落とされてしまい、適切な回答を導き出せないケースが多発します。 GraphRAGの強み:ドキュメント内の要素をインデックス化する際、あらかじめ要素間の関係性をネットワーク構造として保持します 。そのため、質問に対して関連するグラフ構造を辿る検索を行うことができ、離れたページに散在している情報や暗黙的な関係性も取りこぼさずに網羅して抽出することができます 。 このように、LLMにドキュメントの構造や関係性を教え込むアプローチとして、GraphRAGは極めて強力なソリューションです 。 しかし、一般的なGraphRAGは依然としてテキストデータをベースにグラフを構築するものが大半です。PDF資料にあるような「グラフや図表、複雑なレイアウトそのものが持つ視覚的な意味」までを知識グラフに融合させることは、容易ではありません。 RAG-Anything RAG-Anythingは、テキスト、画像、表、数式といった異なるモダリティ(データの種類)の情報を、すべて「相互に繋がりを持つ知識エンティティ」として再定義し、包括的に検索・生成を行うオールインワンのマルチモーダルRAGフレームワークです。 RAG-Anythingの概要 1. インデックスの作成 RAG-Anythingにおけるマルチモーダルなインデックス作成(元論文より抜粋) まず、入力されたPDF資料を高度なレイアウト解析パーサー(オープンソースの「MinerU」など)を用いて、テキスト、画像、表、数式などのコンポーネントに分離・抽出します。 このとき、単にデータを細切れ(チャンク化)にするのではなく、「図とそのキャプション」「数式とその定義文」「表とその説明テキスト」といった、ドキュメント内で密接に関わっている周囲の文脈を保持したまま、知識単位に分解するのが大きな特徴です。 次に、分解された知識単位から、役割の異なる2つの独立した知識グラフを並行して構築します。 クロスモーダル知識グラフ(Cross-Modal Knowledge Graph)画像や表、数式などの非テキスト情報をVLM(視覚言語モデル)に入力し、検索用の「詳細な説明文」と、グラフ用の「エンティティ要約」の2つのテキスト表現を出力させます。これらを起点(アンカー)として、周囲のテキストチャンクと「belongs_to(〜に属する)」などのエッジ(矢印)で結び、ビジュアル要素をドキュメントの文脈構造の中に正しく位置づけます。 テキストベース知識グラフ(Text-based Knowledge Graph)ドキュメント内の純粋なテキスト部分に対しては、従来のGraphRAGの手法を用い、固有表現抽出や関係性抽出を行って文字ベースの知識ネットワークを構築します。 こうしてできた2つのグラフを、同一の概念や単語のマッチングによって1つの包括的な知識グラフへと融合させます。同時に、高速なセマンティック検索を可能にするため、すべてのノードや関係性を高次元のベクトルに変換したベクトルデータベースも裏側で構築されます。 2. クロスモーダル検索と回答生成 RAG-Anythingにおける情報検索と回答生成の流れ ユーザーから質問(クエリ)が投げかけられると、RAG-Anythingはまずクエリを分析し、文中に「図」「グラフ」「表」といった、特定のモダリティを指し示す語彙が含まれているかを識別します。 その上で、以下の2つの経路を組み合わせたハイブリッド検索を実行します。 構造的ナビゲーション(Structural Knowledge Navigation):融合された知識グラフを探索し、キーワードが直接ヒットした要素だけでなく、グラフの繋がりを辿ることで、離れたページに散在している「暗黙的な関係性」を持つ関連要素まで取りこぼさずに抽出します。 意味的類似度マッチング(Semantic Similarity Matching):クエリのベクトルを用いて、ベクトルデータベースから意味の近いチャンクやエンティティをピンポイントで検索します。 こうして両方の経路から集められた候補は、グラフ構造上の重要度、ベクトルの類似度、クエリから推測されたモダリティの優先度などを統合したスコアリングによって再ランキングされ、本当に必要なコンテキストだけが厳選されます。 従来の検索システムであれば、検索にヒットした「テキストによる説明」だけをLLMに渡して終わりでした。しかしRAG-Anythingは、検索結果に画像や図表由来のノードが含まれていた場合、元のドキュメントから該当する「生の画像データ」をピンポイントで取得します。 そして、厳選された構造化されたテキストコンテキストと、取得された生の画像の両方をVLMに同時に入力し、最終的な回答を生成します。これにより、テキストの文脈理解と、画像そのものが持つ生の情報を掛け合わせて回答を生成できます。回答の根拠となる図表が元データに直接ひも付くため、「どの視覚情報に基づいた回答なのか」を追跡しやすくなり、根拠なき出力(ハルシネーション)の混入を検知・抑制しやすいという利点があります。 3. 精度評価 RAG-Anythingの有効性を検証するため、論文ではマルチモーダルなドキュメントQAのベンチマークである「DocBench」および「MMLongBench」を用いた性能評価が行われています 。比較対象として、ネイティブマルチモーダルLLMへの直接入力(GPT-4o-mini)や、テキスト中心のGraphRAG手法(LightRAG)、画像要素のみをグラフ化する手法(MMGraphRAG)などが選定されました 。 (DocBenchデータセット による精度評価結果(元論文より抜粋) 実験の結果、RAG-Anything は各種ベースラインと比較して、特にドキュメントのページ数が多くなる長文コンテキストにおいて良好なパフォーマンスを示す傾向が確認されています 。例えば DocBenchデータセットにおける100ページを超える長大なドキュメントの検証では、DocBench の正答率〔Accuracy, %〕で MMGraphRAG に対して13ポイント以上の精度向上が見られました。また、MMLongBenchにおいても同様の傾向が見られ、情報が複数ページに分散している状況での優位性が示されています 。 さらに、どのコンポーネントが精度向上に寄与しているかを分析するアブレーション研究も行われており、RAG-Anything における性能のコアは、単なるリランカー等の工夫ではなく、デュアルグラフの構築によってドキュメント内の構造や要素間の関係性を保持できている点にあることが示唆されています 。 実際に使
こんにちは!バクラク申請・経費精算のエンジニアリングマネージャーをやっています、@ar_tamaです。 今回は、私たちのプロダクトで最近行ったバックエンドのパフォーマンスチューニング(スロークエリの改善)について書いてみたいと思います。比較的地味なトピックではありますが、Coding Agentをフル活用したエピソードとして、主にMySQLをバックエンドに持つアプリケーションの性能問題に直面している方の助けになれば幸いです。 発端:データ量や分布の変化で実行計画が変わった 私たちは、プロダクトごとに内部SLO(サービスレベル目標)を定めてモニタリングし、基準値を割ったらアクションすることを習慣づけています。 SLOというと大きな障害や可用性の話を想像されるかもしれませんが、特にバクラクのような業務システムでは、毎日繰り返し使う操作の遅さがそのまま利用者(お客様)の生産性低下に直結してしまうため、観測対象に主要エンドポイントの応答時間も追加しています。 今回その基準に違反したのは、複数のテーブルをJOINしてSELECTする処理のあるエンドポイントでした。これまでも必要に応じて、クエリをチューニングしたり、インデックスやインデックスヒントを追加したり、ミドルウェアを立てたり……と対策をさまざま取ってきた箇所です。今度は一体何が……?と調べたところ、たくさんのお客様にお使いいただきデータ量や分布(偏り)に変化が生じたことで、実行計画が意図どおり選ばれなくなったことが分かりました。 やったこと:AIと実行計画を眺める 対象のクエリが見えてきたので、実際に発行されているSQLとパラメータを取得し、Coding Agentと一緒にEXPLAINとEXPLAIN ANALYZEを確認しました。世は大AI時代ですが、主なやることは昔と変わりませんね。 EXPLAINは、MySQLがそのSQLをどう実行しようとしているかを見るためのものです。JOINの順序、使われるインデックス、推定行数などを確認できます。MySQLの公式ドキュメントでも、インデックスを追加すべき箇所やJOINの処理順を確認するための手段として説明されています。 一方で、EXPLAINはあくまで推定です。実際にどのくらい時間がかかったのか、どのステップでどれくらい行を読んだのかを見るには、EXPLAIN ANALYZEが役に立ちます。実際にステートメントを実行するので実行環境には注意が必要ですが、推定と実測のズレによってより精緻に改善することができます。 実際のSELECT文で双方を確認してみたところ、先述のデータ量・分布の変化によって、効かせたいはずのインデックスが選択されていなかったり、頻出のソート条件に合う複合インデックスがなくテンポラリソートが起きていたりと、大小様々な問題が発見されました。その中でも今回特に取り上げたいのは以下です。 JOINの起点が期待と違い、サブテーブルに持たせている種別の絞り込みから始まっていた その後、メインテーブル側で数万件規模の候補行を読み、除外条件を1行ずつ評価していた → クエリの責務を分け、サブテーブルの絞り込み→結果を使ってメインテーブルの絞り込み、と2回クエリを発行 以前のパフォーマンス改善対応(NOT IN+サブクエリでフィルタ)が、むしろ大きな集合を作るような実行計画に変化していた → サブクエリではなく、 カラム NOT IN (...) とクエリ自体を書き換えることで対応 1についてはEXPLAINの目視でもすぐに分かるところでしたが、2についてはEXPLAIN結果を分析したCoding Agentからの指摘で初めて気づきました。 以前の対応時は、たしかに除外対象をサブクエリとして扱うほうが効率的だったはずなのですが、現時点でのデータ分布ではそのサブクエリを作るために大きなスキャンが発生しており、インデックスを辿りながら直接条件を評価する方が速くできそう、という見立てが出されたのです。 同僚からその部分の実装意図を聞いていたこともあり、はじめは「いやいやそんな〜まさか〜」と半信半疑でしたが、修正版を試してみたところ劇的に計画が改善されました。余計なコンテキストを持たないほうが強いこともある……🥲 なお、今回はGORMを使ってクエリを発行している部分が修正の対象だったため、実際のクエリの書き換えにあたり、同僚であるponさんのgormgoldenが大変頼りになりました。アプリケーションコード上では良さそうに見えても、出力されたクエリが大事故…ということもときには生じ得るため、このようにBefore/Afterの確認を行いやすい状態にしておくことも大事ですね。 github.com 結果として、対象クエリ群のほぼ全てで改善が叶い、ベストケースでは50倍以上もの高速化が叶いました。やったぜ! デプロイ後にアラート対象のクエリが激減した様子 Coding Agentと人間との役割分担 今回のようなクエリチューニング作業においては、Coding Agentの網羅性と手の速さがとても頼りになります。クエリ改善で振る舞いが変わってしまうことのリスクとテストの重要性は前述の通りですが、分析・複数の改善案出し・検証・実装にいたるまで、積極的にAgentに任せながら改善を進めたおかげで、従来の改善活動よりもずっと早く修正が叶いました。 振る舞いの担保についても、以下のようなソースを参照して破綻しないことを確認してもらいました。 ヘルプサイトや仕様書 対応するクライアント(フロントエンド)の実装 これにより人の目だけでは拾いにくい検証観点も補え、動作確認も楽に行えました。 ただし、Agentは放っておくと限界までチューニングしようとしてしまいます。改善提案の中には、修正後のクエリが複雑になりすぎてメンテナンス性を損ねたり、修正量の割に改善効果が限定的だったりするものもありました。そのため計画時、または修正を走らせた後でも、「どの程度改善される見込みか」を確認しながら改善を採用する・しないの判断を行いました。 ほかにも、過去経緯などのコードや仕様に立ち現れないコンテキストを補完したり、ハルシネーションにツッコミを入れたり、計測と修正のサイクルを回すためにステップを分けたりなど、人間の仕事も思いの外残りました。とはいえ、今回の改善を回す過程で補完した文脈はドキュメントに残しましたし、モデルの進化も著しいため、次回以降はどんどん手がかからなくなっていくことが予想されます。 おわりに 今後の展望としては、今回の一連の流れを元に、SLO違反を検知→スロークエリをEXPLAINして原因を特定→修正・検証までを実行するSkillなどを作れると、改善のサイクルがより速く・多く回せそうです。 人間が本当に必要な判断に集中できるよう、またAI Agentたちにより多くの業務を任せられるよう、引き続き事業成長を支えるための改善活動を(も)進めていきます。 LayerXでは、AIと共に爆速で価値を生む・守る仲間を大大大募集しています!ご興味を持っていただけたらぜひこちらからご応募ください。 open.talentio.com 本職はマネージャーなので、AI時代のマネジメントどうなる〜?みたいな話も大好物です。カジュアル面談のお誘いもお気軽にどうぞ! jobs.layerx.co.jp
こんにちは、バクラク事業部で機械学習エンジニアをしている伊藤です。 LayerXは、JSAI2026(第40回人工知能学会全国大会)にプラチナスポンサーとして協賛します。LayerXがJSAIに参加するのは昨年に引き続き4回目となり、本大会でも企業ブース展示、インダストリアルセッションでの発表を予定しております。 イベント概要 当社の参加内容 インダストリアルセッション 企業展示 学生向けランチ懇親会 協賛の背景 さいごに LayerXにおけるAI・機械学習関連の記事 イベント概要 JSAI(人工知能学会全国大会)は、人工知能学会 が年に1度開催する、人工知能および関連分野の研究発表や交流の場です。全国のアカデミアや企業からAIの研究・開発に従事している人々が集い、研究発表と活発な議論がなされる場です。LayerXからはバクラク事業部・Ai Workforce事業部を含む数名のメンバーで現地会場に参加させていただきます。 項目 内容 開催期間 2026年6月8日(月)〜6月12日(金) 開催場所 Gメッセ群馬+オンライン(ハイブリッド開催) 主催 一般社団法人 人工知能学会 公式サイト JSAI2026 当社の参加内容 インダストリアルセッション 大会初日である6月8日のインダストリアルセッション1にて、Ai Workforce事業部の平澤より「LayerXにおけるセキュリティ管理の現在地と次の一手」というタイトルで発表させていただきます。LayerXが展開するAi Workforceの事例を通じて、セキュリティ管理の現在地と、開発スピードとの両立を目指す今後のセキュリティ戦略についてお話しします。是非ともご聴講ください。 タイトル: LayerXにおけるセキュリティ管理の現在地と次の一手 発表者: 平澤 寅庄(株式会社LayerX) セッション: [1B4-IND-1] インダストリアルセッション1 日時: 2026年6月8日(月)16:40〜16:55 会場: B会場(展示ホール仮設1) プログラムリンク: 1B4-IND-1-05 | JSAI2026 登壇情報 企業展示 JSAI2026の企業展示では、ブースE77にて展示を行います。LayerXが目指す「業務の完全自動運転」や、日頃の開発・研究開発の取り組み、インターンシップなどについて紹介させていただきます。少しでも興味を持っていただけた方はぜひ気軽にお立ち寄りください! JSAI2026 企業展示配置図 学生向けランチ懇親会 JSAI2026に現地参加されている学生の方限定で、ランチ懇親会を開催予定です。現役のLayerXメンバーと、実際のAI Agent開発やサマーインターンなど、さまざまなトピックについてカジュアルに話せる場にできればと考えております。 ご希望の方は、以下の参加フォームより申し込みをお願いします! 参加対象: JSAI2026に参加している学生の方 開催日時: 2026年6月9日(火)12:15〜13:15 参加フォーム(connpass) 2026年6月10日(水)12:45〜13:45 参加フォーム(connpass) 2026年6月11日(木)12:15〜13:15 参加フォーム(connpass) 参加費: 無料 協賛の背景 LayerXでは、広くAI・機械学習技術を活用しつつ、"業務の完全自動運転"を実現すべく複数の事業を推進しています。バクラク事業部では、あらゆるバックオフィス業務が「気づいたら終わっている」体験を届けるため、「バクラク」で提供されるAIエージェント群を開発、運用しています。Ai Workforce事業部では、エンタープライズ企業が業務ごと・組織ごとに合わせた形で、AIエージェントを「毎日の仕事」に組み込むための汎用AIプラットフォーム「Ai Workforce」を開発、提供しています。 また、我々LayerXの掲げる行動指針である「Bet AI」や「徳」の観点から、アカデミアやOSSコントリビューターの成果から一方的に恩恵を受けるだけでなく、アカデミアや技術コミュニティへの貢献を継続して行っていきたいと考えています。 LayerX行動指針 その上で、国内でも最大級の規模でアカデミアや企業の学生、研究者、開発者など様々な背景の方が集まり、幅広いトピックについて研究発表、議論、交流がなされるJSAIへと貢献することは、同様に様々な背景のメンバーが集まって幅広い技術領域における開発を進めるLayerXにとっても、そして社会の発展にとっても大変有益なことであると考えており、今回プラチナスポンサーとしてJSAI2026に協賛させていただくこととしました。 さいごに LayerXとしてJSAIに参加するのは昨年に引き続き4度目となります。昨年のJSAI2025に参加した際にも、企業ブースや懇親会、セッション聴講を通じて非常に充実した時間を過ごさせていただきました。本大会でも、是非たくさんの方と交流できればと思っております。参加者の皆様と群馬・オンラインでお会いできるのを楽しみにしております。よろしくお願いいたします! LayerXにおけるAI・機械学習関連の記事 bakuraku.jp speakerdeck.com speakerdeck.com speakerdeck.com
0. はじめに バクラク事業部で債務管理の開発をしておりますshoyanです。同事業部では「業務の自動運転」を目指し、KPI の一つとして自動化率をトラッキングしています。その一環として、初期補完で生成される仕訳の手修正を減らし、自動化率を上げたいと考えています。 そこで、「どの項目を改善すれば仕訳の自動化率に効くのか」を試算するシミュレーターを作りました。きっかけは、各項目の修正率を見ても、その項目を改善したときに全体の自動化率がどれだけ上がるかは直感に反することがある、という気づきです。修正率が高い項目を直しても、伝票単位の手作業はほとんど減らない、そんなケースが起こり得ます。これから取り組む項目が全体の自動化率にどれだけ効くのかを事前に試算できれば、改善の優先順位づけの参考になると考えました。 この記事では、その試算を支える確率モデルと、Streamlit で組んだ画面の設計を共有します。紹介する数値はすべてモックです。 完成イメージはこちらです。本記事用に項目は一部省略しております。各項目の「目標自動化率」をスライダーで動かすと、伝票全体の自動化率がどれだけ変わるか(=その改善のインパクト)と、削減見込みの伝票数が試算されます。 自動化率シミュレーターの画面(数値はモックされています) 以降では、なぜ単純な項目別修正率では足りないのかという課題から順に、この画面の裏側にある考え方とロジックを説明していきます。 1. 課題: 修正率が高い項目を直しても、自動化率が上がるとは限らない 初期補完によって、請求書の内容から勘定科目・部門・税区分・摘要……といった仕訳項目を自動で埋めます。ユーザーはそれを確認し、必要なら手修正してから確定します。この「手修正」が利用者の手間であり、自動化でできるだけ減らしたいところです。 では、どの項目の自動化精度を上げれば、この手修正を効率よく減らせるでしょうか。その手がかりとして、まず思いつくのが 項目別の修正率 です。項目ごとに「どれくらいの割合で手修正されているか」を見るものです。 勘定科目 : 32% が修正される 部門 : 21% 摘要 : 45% 税区分 : 8% ... これを見ると「摘要の自動化精度を上げれば一番効きそう」に見えます。ところが、ここに落とし穴があります。 伝票(請求書)単位で見ると、複数の項目が同時に修正されているケースが大半なのです。 伝票A: 勘定科目 ✏️ 部門 ✏️ 摘要 ✏️ ← 3項目を修正 伝票B: 摘要 ✏️ ← 摘要だけ修正 伝票C: 勘定科目 ✏️ 税区分 ✏️ ← 2項目を修正 ここで「摘要の自動化を100%にできた」としましょう。自動化できるのは 伝票B だけ です。伝票A は勘定科目と部門の修正が残るので、結局ユーザーは伝票を開いて手を入れます。「摘要修正率45%」という数字は、伝票単位の手作業削減にそのまま直結するわけではありません。 つまり計測したいのは、項目別の修正率そのものではなく: ある項目に効く改善を行ったとき、伝票全体の修正率(=伝票単位で一度でも手修正が入る割合)が実際にどれだけ変わるか これを試算するのが今回のシミュレーターです。 2. 何を測るか: 指標の定義 シミュレーションの前に、比較する2つの状態と指標を定義しておきます。 用語 定義 初期仕訳 初期補完後の仕訳。ユーザーがまだ触っていない状態 修正後の仕訳 ユーザーが手修正したあとの最新状態 修正あり(項目) 項目ごとに「初期仕訳 ≠ 修正後の仕訳」 伝票の自動化率 どの項目も一度も修正されていない伝票の割合 3. 比較ロジック: 修正フラグの作り方 初期仕訳と修正後の仕訳を項目ごとに突き合わせ、0/1 の修正フラグにします。借方・貸方の各行と伝票(請求書)単位の全項目のフラグを立てたら、最後に フラグの組み合わせ × 件数 に畳み込みます。伝票1枚ずつではなく「同じ修正パターンの伝票は何件あるか」という形に集約するわけです。これがシミュレーターの入力になります。 集約後は次のような表になります。 勘定科目 部門 摘要 税区分 … 件数 1 1 1 0 … 1,200 0 0 1 0 … 3,400 1 0 0 1 … 800 0 0 0 0 … 9,500 (1 = その項目が修正された伝票群。最終行は「どこも修正なし」=既に自動化できている伝票です) 伝票単位の全データを持ち回るのではなく、この「修正パターン × 件数」に畳んでおくことでシミュレーションを軽くしています。 4. シミュレーションロジック: 確率モデルで試算する 基本原理 スライダーで各項目の「目標自動化率」を動かします。基本となる考え方はシンプルです。 その項目「だけ」が修正理由だった伝票は、その項目を自動化すれば自動化できる。他にも修正がある伝票は自動化できない。 先ほどのモック表でいえば、摘要だけ=1 の 3,400 件は摘要を自動化すれば丸ごと自動化できます。一方 勘定科目=1, 摘要=1 の 1,200 件は、摘要を自動化しても勘定科目の修正が残るので自動化できません。 スライダーは「目標自動化率」 ただし現実には「100%自動化」だけでなく「80%まで上げたら?」という中間も試したくなります。そこで各項目について、現在の自動化率 current から目標 target へ動かしたときに、以下を定義します。 改善方向(target > current): 修正されていた伝票が修正なしになる確率 p_zero = (target - current) / (1 - current) 悪化方向(target < current): 修正なしの伝票が修正ありになる確率 p_one = (current - target) / current p_zero = 1 - p_one p_zero は「その項目がこの伝票で修正なしになる確率」です。 直感的には、改善方向の式は「まだ自動化できていない残り (1 - current) のうち、どれだけを新たに自動化できたか」の比率になっています。 まず1つの修正パターンで考える 計算は伝票1枚ごとではなく、3章で畳み込んだ「修正パターン」の1行ごと(=同じ修正パターンの伝票群)に対して行います。1つの修正パターンが「完全に修正なし(=自動化できる)」になる確率は、各項目が独立に修正なしになると仮定し、各項目の p_zero を掛け合わせます1。 修正パターン: 勘定科目=1, 部門=1, 摘要=0(この修正パターンの伝票が N 件あるとする) スライダー : 勘定科目 70%→90%, 部門 80%→100% 勘定科目: p_zero = (0.90-0.70)/(1-0.70) = 0.667 部門 : p_zero = (1.00-0.80)/(1-0.80) = 1.000 摘要 : 修正なし(0) → 影響なし p_all_zero = 0.667 × 1.000 = 0.667 (= 各項目の p_zero の積) → この修正パターンの N 件のうち 66.7%(= N × 0.667 件)が自動化できる 全パターンを合成して全体の自動化率を出す あとは、これを全パターンに対して行うだけです。各修正パターンの件数に (1 - p_all_zero)(=自動化できずに修正が残る確率)を掛けて足し合わせれば、シミュレーション後の「修正あり件数」が求まります。そこから伝票単位の自動化率が出ます。 p_all_zero = Π p_zero (1つの修正パターン内の全項目について) 修正あり件数 = Σ 件数 × (1 - p_all_zero) (全修正パターンについて) 新しい自動化率 = (総伝票数 - 修正あり件数) / 総伝票数 5. 結果イメージ 冒頭で見せた画面の読み方を、改めて具体的に見ていきます。ここでは例として 勘定科目(93.1%→100%)と部門(97.0%→100%) を自動化できたと仮定しています。 現在の自動化率 : 45.23% シミュレーション後 : 49.85% (+4.63pt) 削減見込み伝票数 : 1,286 件 対象伝票数 : 27,783 件 ヘッドラインの「削減見込み 1,286 件」は、スライダー設定(勘定科目・部門の目標自動化率)に基づく確率モデルの期待値です。一方、以下は 勘定科目・部門を 100% 自動化したときに修正不要になる伝票(その2項目以外に修正がない伝票)の内訳(合計で最大 1,203 件)です。 修正理由(これだけ) 件数 全体に占める割合 勘定科目 839 3.02% 部門 344 1.24% 勘定科目 + 部門 20 0.07% この表から「勘定科目を自動化できれば単独で 839 件(全体の 3.02%)自動化できる」と即座に分かります。さらに「勘定科目と部門のどちらも修正する必要があった伝票」が 20 件あり、これは勘定科目だけ・部門だけを自動化しても残り、両方が揃って初めて修正不要になる伝票です。スライダーは複数項目を同時に動かせるので、こうした「両方を直さないと自動化できない伝票」の効果もまとめて試算に反映されます。 6. 終わりに 今回は、「どの項目を改善すれば伝票単位の自動化率が上がるか」を試算するシミュレーターを作りました。ポイントは、項目別の修正率を見るのではなく、「その項目だけが修正理由だった伝票」 を数え、伝票単位の自動化率への寄与から改善インパクトを捉えたことです。 確率モデルによる近似なので正確な予測値ではありませんが、改善の優先順位づけの方向感を掴む道具として活用しています。 最後にLayerXでは、業務の自動運転を共に目指す仲間を募集しています。興味のある方はぜひエントリーしてください。 LayerX 採用サイト 実際には項目間の修正には相関があるため、この掛け算(独立の仮定)はあくまで近似です。本ツールも正確な予測値ではなく、どの項目の改善が効きそうかを考えるための一つの参考資料として使っています。↩
はじめに LayerX Ai Workforce事業部でApplied R&D をしているtyoyoです。 AI Agentの長期記憶に関して様々な手法が提案されていますが、そのどれもが実際に長期間で運用されたことはほとんどないはずです。なぜなら、それらが台頭したのが最近だからです。 個人的に長期記憶についての肌感覚がなかったので、実験として「1年分のAIニュースの長期記憶」を作ってみることにしました。 最大6並列で約20時間、607回のセッション、4,552個のmemoryファイル ー 動かしてみないと分からない「長期記憶の課題」にぶつかるため、今回はこういった規模でシミュレーションを行いました。 Claude Codeを参考にした簡易的な長期記憶システムの作成 基本的にはClaude CodeのMemoryを参考に、以下のような frontmatter(ファイル先頭のメタデータ記述部分)つきのMarkdownファイルを1つの単位とします。これらが memory/ ディレクトリ以下に大量に配置されるシンプルな構成です。 memoryファイルの具体例 --- date: 2024-08-14 description: "GartnerによるPoC後に見送られる生成AIプロジェクトが30%に達するという2025年末予測、生成AIのROI・短期投資回収困難・データ品質・コスト問題、企業の生成AIPoC失敗率統計を調べている時。" sources: - https://atmarkit.itmedia.co.jp/ait/articles/2408/14/news067.html related: - 2024-08-07-gartner-ai-investment-business-fit-failure-4-reasons.md --- # Gartner:生成AIプロジェクトの30%が2025年末までにPoC後に見送りになると予測(2024年8月) Gartnerは2024年8月、生成AIプロジェクトの30%が2025年末までに概念実証(PoC)後に見送られると予測した。IT Mediaが同日付で報じた。 バイスプレジデントアナリストのリタ・サラム氏は、「生成AIの2023年の過熱ぶりを経て、経営幹部は投資に対するリターンを急いで求めているが、組織は価値を証明し実現することに苦戦している」と述べた。見送りの主な背景として、$500万〜$2000万規模のコスト、短期的なROIが出にくい構造、データ品質の問題が挙げられている。CFOは将来の間接的価値のために投資することに抵抗感を持ちやすいとされ、長期コミットより短期成果を優先する傾向がある。 一方で早期導入企業はビジネス改善を報告しており、Gartnerの2023年9〜11月に822人のビジネスリーダーを対象とした調査では、平均売上高15.8%増加・コスト15.2%削減・生産性22.6%向上という自己報告値が示されている。ただしサラム氏は「ビジネス価値の推定は容易でなく、企業・ユースケース・役割・人材によって大きく異なる」と留保している。 この記憶は、生成AIのPoC後廃棄率・ROI困難・コスト構造に関するGartnerの定量予測として位置づける。楽観的な効果数字と廃棄リスクの両方を持つ点が特徴。 生成AIプロジェクトの投資判断・経営説得・リスク評価の文脈でGartnerの見解を参照したい時に見るとよい。 記憶の追加については、qmem-add というSkillを作り、そこに保存すべきものや保存形式などの指示を記載しました。 記憶呼び出しは、qmem-search というSkillを作り、そこに4つのアクセス手段を提示しました。 tobi/qmd を用いた検索(BM25(キーワードベースのランキングアルゴリズム), ベクトル) description + ファイル名 の一覧を提示する qmem-ls コマンドによる、SKILL.md風遅延読み込み frontmatterのrelated によるグラフ辿り grep などを用いたキーワードマッチ またHook を用いたリマインダーによる自動的な記憶呼び出しも作成しました。各ユーザーからのpromptとツールの呼び出し結果をクエリとし、BM25検索で一定の閾値を超えた場合に <qmem> {filename}: {description} </qmem> のようにAgentに記憶が提示されます。 Dreaming(記憶のライフサイクル管理)としては qmem-maintain Skillを作り、20セッションに1回 Agent自身に記憶の重複削除・古い記憶の更新・related フィールドを用いた記憶の関連付けを行いました。 Agentに1日で1年分のニュースを読ませる データとしては LayerX AI・LLM Newsletter を60号分(2024年1月-2025年2月まで)使用しました。各Newsletterは平均2万文字、リンクとしては111個と大きなファイルです。Agentは10分割されたNewsletterファイルを読み込み、それぞれのニュースについて記憶を追加するように指示を受けます。 Claude Codeのデフォルト設定を使用し、モデルはSonnet 4.6 (200k) を利用しました。 Dreaming以外は6並列でまわし、およそ20時間程度かかりました。合計で607回のセッションが実行されました。まれにプロセスが固まってしまうため、Claude Codeの /loop を使い実験を監視・再開させていました。またそのClaude Code自体がレートリミットで落ちてしまうことがあったため、codexbar cli を用いてレートリミットがリセットされるタイミングで監視を蘇生させていました。実験はClaude Code Maxプランを用いて行いましたが、API換算だと合計で$407 かかっている計算です。 1年分の記憶をもつAgent 結果として4,552個のmemoryファイルが生成されました。これらの記憶をもつAI Agentに 「RAG の評価設計を相談したい。retriever / reranker / generator のどこから手を付けるべきか?」 と質問をしてみると、 ...記憶によると(2024-08-19-aws-ragchecker-claim-level-rag-evaluation-framework.md)、 Amazon/AWS が 2024年8月に公開した RAGChecker は 「Retriever モジュールと Generator モジュールのそれぞれに対して独立した診断メトリクスを持つ」設計で、 個々の claim レベルで評価を行うことで「どのモジュールに問題があるかを切り分けられる」とされています。 8 つの RAG システムを評価した論文で、GitHub(amazon-science/RAGChecker)でオープンソース公開されています。... のように、過去に読んだ記事をもとにした回答ができています。 Dreamingプロセスの分析 記憶のライフサイクル管理、Dreamingは計18回実行され、450件のファイルを更新・245件のファイルを削除しました。更新では主に記憶同士の関連付け(relatedフィールドの更新)、削除では主に重複した内容の削除(技術ブログとそれを題材にしたSNS投稿など)が行われていました。 またClaude自身が誤ったフォーマットでmemoryファイルを記述してしまい、それをDreamingプロセスで直しているケースも見受けられました。例えばファイル名だけを記述するように指示していたがファイルパスごと記述してしまったり、日付を書くように指定してたものの 2024-08 のように月までしか書いていなかったケースも有りました。これらを防ぐには、長期記憶のシステムにもハーネスのような可能な限りLLMではなくプログラムで守る仕組みが必要です。 個人的に残念だったのは、関連ノードのグラフがほとんど育たなかったことです。全ファイル中 related フィールドをもつのはわずか11.3%で、残りはほぼ孤立していました。もちろん全てのmemoryファイル自体に関連があるわけではありませんが、定性的に確認していても関連しているのにrelated ではないものが見受けられました。relatedを追加するかどうかのpromptは、もっと積極的にrelatedを追加せよ、と書くべきだったかもしれません。 またDreamingのプロセスは「新規追加されたmemoryファイルは全文読み込み、それ以外はfile名とdescriptionのカタログのみ読み込み」を行ってから関連記憶の紐づけを行っていたのですが、4,552ファイルの段階でこのカタログだけで200k contextの228% を占めてしまっていました。DreamingにおいてAgentに自身の記憶を修正させるのは強力ですが、その分扱えるmemoryファイルの数は有限で、「何を忘れるか」の設計も重要そうです。あるいはAgentを使わない埋め込みベースのライフサイクル管理や、階層的なデータ構造なども考えられます。 まとめ 実際に長期記憶の作成をシミュレーションしてみて感じたのは、動かしてみないと見えてこないことがたくさんあるということです。 構想段階では「どのベクトル検索ライブラリを使おうかな」「どんなデータ構造にしようかな」ということを考えていましたが、実際には「何を記憶するか」「いつ繋ぐか」などの細かいプロンプトの影響が大きかったです。またmemoryは容易に膨れ上がるので、「忘れさせる」という戦略をとるか、「スケールするようにする」という戦略の2軸があるように思いました。 この記事で紹介したR&Dチームの詳細はこちらです。 open.talentio.com
こんにちは。Ai Workforce事業部 開発部の id:ninjinkun です。 先日開催されたTSKaigi 2026において、LayerXはゴールドスポンサーおよび学生支援スポンサーとして協賛しました。 このエントリでは弊社社員の登壇資料の共有、1日目の最後に行われた基調講演の様子、そしてLayerXブースの様子をレポートします。今回はゴールドスポンサーとしての出展に加え、LayerXから総勢4名のエンジニアが登壇しました。まずはそれぞれの登壇資料からご紹介します。 登壇資料 API設計や型の推論にまつわる話まで、幅広いテーマでの登壇となりました。各発表の資料は以下の通りです。もしご興味のある方はぜひご覧ください。 基調講演 TS7: How We Got There 自身の中でもっとも印象に残ったセッションが、1日目最後の基調講演です。MicrosoftでTypeScriptコンパイラのGo移行をリードしているJake Bailey氏による基調講演が行われました。内容としては、セルフホストで書かれていたTSコンパイラが長年抱えていたパフォーマンスの問題の紹介から始まり、どのように移行プロジェクトが始まったのか、なぜGo言語を選んだのかという説明がわかりやすく展開されました。そして最後に劇的に改善されたパフォーマンスのデモで締め括られるというストレートかつ熱い構成で、会場も熱気に包まれていました。 印象的だったのは、VSCodeのコードが頻繁にベンチマークとして使われていたことです。VSCodeのコードに対して型検証を行う際の速度比較や、実際にリポジトリでtscを実行するデモ(氏曰く「呪われたコマンド」)が行われていました。VSCodeを引き合いに出しているのはおそらく社内に対する説明と社外に対するアピールの両方の目的があるのでしょうが、理にかなった戦略だと感じました。この例から伝わる通り、Microsoft自身がTypeScriptを大規模に利用しており、パフォーマンスを改善し続ける強い動機を持っている点は、いちTSユーザーとしても言語の将来に期待が持てる内容でした。 また私個人としては、2日目の昼食の時間に氏が近くに座っていたので、多少勇気を出して話しかけてお礼を言えたのがよかったです。非公開リポジトリでの数ヶ月の試行錯誤を経てリポジトリを公開した話や、なんでC#にしないんだと頻繁に言われる話などを冗談めかして語ってくれました。 すでにGo移行プロジェクトはtypescript-goリポジトリで公開されており、誰でも試すことが可能です。氏のおすすめはまずVSCode拡張のTypeScript (Native Preview)を利用することで、入れるだけでVSCodeでTSファイルを開いてから型解析が完了するまでの速度が大幅に向上するとのことでした。Go移行プロジェクトのリリース版であるTS7はComing Soonとのことなので、楽しみに待ちたいと思います。 LayerXブース ブース撤収完了しました!遊びに来てくださったみなさんありがとうございました、また来年お会いしましょう🙌#TSKaigi pic.twitter.com/J0TXcO25lG— LayerX Tech (@LayerX_tech) 2026年5月23日 今回LayerXとして2日間スポンサーブースを出展しました。ブースでは@syumai、@やた、@Yokan の3名が社内からヒアリングしてきたTypeScriptやフロントエンドに関連した事例をディスプレイで展示し、来場者の方の興味に合わせてその中からトピックを選び説明を行いました。内容は変数をGUIで選択しながら計算式を記述できる複雑なフォームを実装した話、ElectronでPC利用を記録するアプリを開発した話など多岐にわたり、ブースに訪れてくださった方にも好評でした。残念ながらコンテンツはその場限りの公開だったのですが、今後また紹介できればと思います。 私自身は今年4月に入社したばかりのため、前述のコンテンツや会社の事業であるバクラクやAi Workforceについて最初はたどたどしく説明していたのですが、2日間ブースに立ち続けてある程度スムーズに説明できるようになりました。ブースに立ちながら、LayerXの社名はご存知でも実際どんな事業をやっているのかご存知でない方もまだ多いことを実感したので、今後もこういった出展や社外へのアウトプットを継続していく必要性を強く感じました。 アフターイベントのご案内 6/3(水)にUbie様、ビットキー様との共催でアフターイベント「歴史あるプロダクトで、"AIに任せられる領域"をどう広げるか?TSKaigi アフターイベント」を開催します。 TSKaigi本編では話しきれなかったTypeScriptに関するトークに加え、懇親会でより学びを深められる機会です。ぜひ下記connpassリンクよりお申し込みください。 終わりに 私は今回初めてTSKaigiに参加しましたが、普段触っていないバックエンドTSの世界の話も聞くことができ、改めてTypeScriptの表現力を見直す機会になりました。 運営の皆様、スピーカーの皆様、そしてブースに立ち寄ってくださった皆様、ありがとうございました!
こんにちは。バクラク事業部 BizOps部 データグループの@civitaspoです。 この記事では、Snowflake と dbt を使ったデータ基盤で、意図しないデータ参照を防ぐために作った dbt package、dbt-authorized-models を紹介します。 github.com はじめに LayerX のデータ基盤では、dbt を使って、データの用途や加工段階に応じてモデルを階層化して管理しています。たとえば、生データに近い層、分析しやすい形に整えた層、業務やプロダクトで使いやすい形に集約した層、というように責務を分けています。 この構造は Medallion Architecture の亜種だと考えてもらえるとわかりやすいと思います。以降では説明のために Bronze / Silver / Gold という呼び方を使いますが、staging / intermediate / mart のような構成や、各社で使っている階層構造に読み替えながら読んでください。 docs.databricks.com このような階層構造では、依存関係の向きが重要です。たとえば Gold 層のモデルは Silver 層を参照して作られますが、Silver 層のモデルが Gold 層を参照してしまうと、データアーキテクチャ上の責務が崩れます。再利用してよいと思っていた中間モデルが、実は下流の業務ロジックに依存していた、という状態になると変更影響の見通しも悪くなります。 また、単一の dbt Project の中でも、すべてのモデルを同じ参照可能範囲で扱えるとは限りません。ディメンショナルモデリング等で適切にモデリングされた、広く参照されてよいデータモデルもあれば、データオーナーやモデルオーナーが許可した範囲でのみ参照できるようにしたいモデルもあります。 この記事では、このようなデータアーキテクチャにおいて、意図しないデータ参照をどう防ぐかを扱います。LayerX でも、これまではドキュメントやコードレビューで設計意図を共有しながら運用してきました。しかし、機械的にポリシーを強制できる仕組みがなければ、データ基盤の利用者や開発者が増えるにつれて、人の注意だけで参照ポリシーを守り続けるのは難しくなります。さらに、AI Agent による実装が当たり前になっていくことも踏まえると、ポリシーに準拠していることを機械的に検査できる仕組みがより重要になると考えています。 はじめに dbt-authorized-models を作った背景 Snowflake の機能では、データ間の参照境界を強制することはできない dbt 実行ロールが広い参照権限を持つと、本来許可してはいけない参照が成立してしまう dbt の model access は、今回必要な許可ルールとは粒度が異なる dbt-authorized-models を作った背景まとめ dbt-authorized-models を使って、参照ルールに違反した依存関係を検出する dbt-authorized-models の使い方 package を追加して authorization check を実行する モデルオーナーが参照範囲を管理できるようにする 許可ルールを誰が変更できるかまで考える 導入時の注意点 まとめ We are hiring 🔥 dbt-authorized-models を作った背景 ここから前半では、なぜ dbt-authorized-models が必要になったのかを説明します。使い方だけ先に確認したい方は、後半の「dbt-authorized-models の使い方」から読んでください。 Snowflake の機能では、データ間の参照境界を強制することはできない 意図しないデータ参照を防ぐ方法として、まず考えたのは、dbt の DAG とは別にインフラレイヤーで参照境界を強制することでした。この発想は、BigQuery の authorized view や authorized dataset に近いものです。データプラットフォーム側で「このデータは、許可された view や dataset 経由でのみ参照できる」という境界を表現できれば、dbt のコードやレビューだけに頼らずにガバナンスを効かせられます。 cloud.google.com cloud.google.com authorized view は、利用者に元テーブルへの直接権限を与えず、許可された view 経由でデータを参照させるための仕組みです。authorized dataset は、その考え方を dataset 単位に広げ、特定の dataset に含まれる view 群を、別 dataset のデータにアクセスできる主体としてまとめて許可するための仕組みです。 一方、Snowflake には、被参照データ側の ACL 等に特定の view 群や dataset を authorized resource として登録し、そこを経由した参照だけを許可する、という BigQuery の authorized dataset と同じ考え方の機能は存在しません。もちろん Snowflake にも、ロールベースの権限管理、Secure View、Row Access Policy、Masking Policy など、データアクセスを制御するための機能はあります。利用者にデータソースとなるテーブルへの直接権限を与えず、制御された view を公開する設計も可能です。 単一の権限主体によるアクセスであれば、Snowflake の built-in 機能だけでも多くのケースを制御できます。たとえば BI やアドホッククエリの利用者に対しては、RBAC や Secure View などを組み合わせることで、データソースとなるテーブルへの直接参照を制御できます。 問題になるのは、dbt Project の実行のように、複数の権限主体が持つ参照範囲を単一の実行ロールがまとめて持ちうるケースです。 dbt 実行ロールが広い参照権限を持つと、本来許可してはいけない参照が成立してしまう dbt Project の実行では、Snowflake に接続するときの role が profile の接続設定で決まります。dbt-snowflake で単一の Project / run を実行する場合、参照元モデルごとに Snowflake role が自動で切り替わるわけではありません。 一方、dbt のモデル依存関係は、SQL の中で ref() や source() を書くことで追加できます。つまり、DAG 上ではモデル単位で参照関係を作れますが、その参照関係に合わせて Snowflake の権限境界がモデル単位で切り替わるわけではありません。 複数の target や job に分ければ権限分離はできます。ただし、権限境界に合わせて実行単位を細かく分けるほど、開発、CI、リリース、依存関係の管理は複雑になります。モデルごとに Snowflake role を切り替えて参照範囲を制御する運用は、dbt の標準的な実行モデルとは相性がよくありません。 結果として、単一 Project のまま多くのデータモデルを管理する場合、dbt の実行ロールには広い参照権限が付与されがちです。本来であれば別々の権限主体が持つべき参照範囲を、dbt の実行ロールがまとめて持つことになります。このような実行ロールでモデルを構築すると、権限上は参照できても、データアーキテクチャ上は本来許可してはいけない参照が成立してしまいます。 dbt の model access は、今回必要な許可ルールとは粒度が異なる ここまでの課題に対して、dbt が持つガバナンス機能で解けないかも考えました。 まず、権限境界ごとに dbt Project を分け、dbt Mesh として運用する方法があります。複数のチームや Project がデータプロダクトを共有する組織では自然な選択肢です。ただし、Project を分けると、公開モデルの管理、利用側との調整、CI/CD やリリース単位の設計も必要になります。既存の単一 Project で許可していない参照を防ぐためだけに Project を分割すると、運用の負荷が大きくなりがちです。 docs.getdbt.com では、単一 Project のまま dbt の機能で参照境界を表現できないでしょうか。dbt のドキュメントでは model governance は dbt Mesh の文脈で説明されていますが、model access、model contracts、model versions などは単一の dbt Project でも利用できます。そのため、Project を分割しない場合でも、model access で今回の課題を解けないかは検討対象になります。 docs.getdbt.com docs.getdbt.com model access は、group と access を使い、モデルの所有者と公開レベルを表現するための機能です。モデルを private / protected / public として扱い、どの範囲から ref() できるかを制御できます。 これは、モデルを内部実装として閉じるのか、同じ Project 内で使えるようにするのか、他の Project からも参照できる安定したインターフェースとして公開するのかを表現するには有用です。一方で、特定のモデルや source に対して、どの参照主体からの利用を許可するかを細かく定義するための機能ではありません。また、model governance は model を対象にした機能であり、source への参照制御には使えません。 今回扱いたいのは、参照されるモデルや source ごとに「どの dbt node からの参照を許可するか」を明示・強制することです。たとえば、「このモデルはこの schema のモデルからだけ参照してよい」「この source はこの tag を持つモデルからだけ参照してよい」のように、参照元の database、schema、identifier、package_name、tags などのメタデータに基づいて許可範囲を定義したい場合があります。 つまり、model access はモデルの所有者と公開範囲を表現するには有用ですが、参照元メタデータに基づく細かな allow-list や、source() への参照制御を表現するためのものではありません。単一 Project のまま参照境界を守る
はじめに こんにちは、LayerX バクラク事業部 BizOps 部データグループの平田 (@TrsNium) です。 以前、Google Apps ScriptでSnowflakeとGoogle Sheetsを連携するアドオンを開発した話という記事で、Google Apps Scriptを使ってSnowflakeとGoogle Sheetsを連携するアドオンを開発した話をしました。このアドオンは、BigQueryのConnected Sheetsのような体験をSnowflakeでも実現するために作られたもので、UIからのオンデマンド実行やスケジュール実行、パラメータ設定など、データ分析に必要な機能を一通り実装していました。 このアドオンは、ビジネス職のメンバーを中心に社内で幅広く使われています。予実管理やKPIトラッキングはもちろん、SQLを書けないビジネス職のメンバーも、設定済みのクエリを選ぶだけで最新データを取得できます。手軽で共有しやすく、スケジュール機能で自動更新もできるため、データ分析と業務効率化に役立っていました。 しかし利用が広がるにつれ、GAS のタイムアウトや実行枠の制限が壁になり、ユーザー体験を損なう場面が増えました。そこで、GAS から脱却し Snowflake ネイティブな実行基盤に作り直すことにしました。本記事では、その設計・実装・成果を紹介します。 GAS制限とユーザー体験について GAS の制限について GASには利用形態ごとに異なるタイムアウトと実行枠の制限があります(Quotas for Google Services)。 UI実行(google.script.run 経由): 30秒でタイムアウト。ユーザーがボタンをクリックして実行するケースがこれに該当し、少しでも重いクエリは完了前に打ち切られる スケジュール実行(時間主導型トリガー): 30分でタイムアウト。重いクエリでも実行できるが、即時性がない 1日の総実行時間: ユーザーあたり6時間/日(Google Workspaceアカウントの場合)。ユーザーあたりのスケジュール数にも上限があり、KPIトラッキングなどで頻繁に利用するユーザーはすぐに上限に達してしまう。その回避策としてマシンアカウント1つにスケジュール実行を集約していたが、今度はそのアカウントの6時間枠を全ジョブで共有することになり、重いジョブが枠を消費すると他のスケジュールが実行できなくなる 同時実行数: スクリプトあたり30実行まで。利用者が増えるとピーク時にジョブが詰まる 最大の問題は、UI 実行の 30 秒制限です。ユーザーがクエリ実行ボタンを押すと、裏側では google.script.run 経由で GAS が呼ばれますが、この経路に 30 秒のタイムアウトがあります。少し重いクエリや集計テーブルを使う分析では、Snowflake 側で結果の準備が終わる前に処理が打ち切られ、Google Sheets には何も書き込まれません。利用者から「動かない」「同期できない」という問い合わせが多く寄せられました。 回避策としてスケジュール実行を案内していました。スケジュール経由なら重いクエリも完了しますが、これでは Google Sheets 本来の価値が損なわれます。データを見て疑問を持ち、条件を変えてすぐ再実行する — この高速なフィードバックループこそが、Google Sheets でデータ分析を行う中心的な価値だからです。スケジュール登録して次回実行を待つ運用ではサイクルが遅くなり、「ちょっと見てみよう」の気軽さが失われます。 この問題を解決するために、GASの制限から脱却した新しい実行基盤の検討を始めました。 アーキテクチャ選定の思考プロセス Option A: AWS構成 社内の既存インフラが AWS ベースのため、最初に AWS を使った基盤構築を検討しました。API Gateway、ECS、SQS、Step Functions、DynamoDB など、AWS のマネージドサービスを組み合わせてスケーラブルなジョブ実行基盤を作るというアプローチでした。 graph TB subgraph Client["クライアント層"] UI["Google Sheets Add-on UI"] GAS["Apps Script (Minimal Shim)"] end subgraph AWS["AWS Backend (8+ サービス)"] APIGW["API Gateway"] ECS1["ECS: Job API"] SQS["SQS (ジョブキュー)"] SFN["Step Functions"] ECS2["ECS Task (Runner + Writer)"] DDB["DynamoDB (状態管理)"] EVB["EventBridge (スケジューラ)"] end subgraph External["外部サービス"] SF["Snowflake"] SHEETS["Google Sheets API"] end style Client fill:#e3f2fd style AWS fill:#fff3e0 style External fill:#f3e5f5 UI --> GAS GAS -->|"POST /jobs"| APIGW APIGW --> ECS1 ECS1 -->|"enqueue"| SQS ECS1 -.->|"state"| DDB SQS --> SFN SFN -->|"RunTask"| ECS2 ECS2 --> SF ECS2 --> SHEETS ECS2 -.-> DDB EVB -.-> ECS1 Step Functions でワークフローを可視化でき、SQS で柔軟なキューイングを実現でき、ECS でスケーラブルな実行環境を構築できます。事業が成長し、コネクタの利用が増えても対応できる基盤です。しかし、実装期間、運用コスト、そしてアーキテクチャ上の課題があります。 まず、ユーザーが日常的に GAS の制限に直面している状況で、基盤構築に時間をかける余裕はありませんでした。AWS 構成ではインフラ構築、認証実装、ジョブ管理システムなど、実装に相応の時間がかかる見込みでした。 Option B: Snowflakeネイティブ構成 アーキテクチャの複雑さを減らすため、AWS を使わない構成を検討しました。データはすべて Snowflake 上にあり、処理も同じ基盤で完結できる可能性があります。調査を進めると、Snowflake には Tasks によるジョブ管理、Python Stored Procedure による外部 API 呼び出し、External Access Integration による外部連携といった機能が備わっていることが分かりました。これらを組み合わせれば、AWS を介さず Snowflake 単体で完結する構成を実現できます。 以下の図は、Snowflake ネイティブ構成の全体像を示したものです(実際の Task などの詳細は後述します)。 graph TB UI["Google Sheets"] subgraph Snowflake["Snowflake (単一プラットフォーム)"] Tasks["Tasks 層<br/>スケジューリング"] Catalog["Catalog 層<br/>メタデータ管理"] Orchestration["Orchestration 層<br/>ジョブ制御"] Core["Core 層<br/>実行エンジン"] end SHEETS["Google Sheets API"] style Snowflake fill:#e0f7ff UI -->|"手動実行"| Orchestration Tasks -->|"参照"| Catalog Tasks -->|"ジョブ投入"| Orchestration Orchestration --> Core Core -->|"パラメータ取得"| Catalog Core --> SHEETS Snowflake ネイティブ構成では、AWS 構成で必要だった 8 つ以上のサービスを Snowflake 内部の機能だけで実現します。レイヤー構成の詳細は後述します。 共通の懸念: Google Sheets API Quota どちらの構成を選ぶにしても、Google Sheets API の Quota 制限という問題がありました。 GAS 以外の基盤に移行する場合、Google Sheets API の write requests のデフォルト quota がボトルネックになる可能性があります。AWS 構成でも Snowflake 構成でも同じです。この制限が緩和されなければ、GAS のタイムアウト問題を解決しても、結局別の制約に引っかかってしまいます。 そこで事前に Google Cloud へ Quota 増加リクエストを提出したところ、承認されて Quota 制限が大幅に緩和されました。 意思決定のポイント 最終的に、以下の 2 つの観点で Snowflake 構成を選択しました。 1. 実装速度 Snowflake の組み込み機能で要件 (定期実行・パラメータ設定・オンデマンド実行) を満たせるため、AWS 構成のようにインフラやミドルウェアを個別構築する工数が不要になります。基盤構築に時間をかけず、ユーザー課題に集中できる点が決め手でした。 2. 運用・スキルセットとの相性 データチームのコアスキルセットは SQL、dbt、Snowflake です。SQL と Python で完結する Snowflake 構成なら、新メンバーのキャッチアップも容易です。また、QUERY_HISTORY と TASK_HISTORY でログを一元管理でき、デバッグも単一クエリログで完結します。 Snowflakeベースの新アーキテクチャ レイヤー構成 全体は4つのレイヤーで構成されています。 レイヤー 役割 主なコンポーネント Tasks スケジューリング scheduler_tick, ingest_job_requests_task, run_pending_jobs_task, check_failed_schedules_task Catalog メタデータ管理 QUERY_CATALOG, QUERY_SCHEDULES, SPREADSHEET_PARAMETERS Orchestration ジョブ制御 JOB_REQUESTS, JOB_STATE, JOB_REQUESTS_STREAM Core 実行エンジン EXECUTE_JOB, WRITE_QUERY_TO_SHEET Tasks層: scheduler_tick が5分間隔でスケジュール対象を取り込み、check_failed_schedules_task が1日1回連続失敗を検知してスケジュールを自動停止します。オンデマンド実行は専用の Task を持たず、UI から JOB_REQUESTS に直接 INSERT します。 Catalog層: QUERY_CATALOG にクエリ定義、QUERY_SCHEDULES にスケジュール設定、SPREADSHEET_PARAMETERS にパラメータ設定を格納します。 Orchestration層: JOB_REQUESTS にリクエストが投入されると JOB_REQUESTS_STREAM (CDC) が変更を検知し、JOB_STATE へジョブをエンキューします。 Core層: EXECUTE_JOB がジョブを受け取り、WRITE_QUERY_TO_SHEET (Python Stored Procedure) を介して Snowflake クエリの実行と Google Sheets への書き込みを行います。 ジョブ実行フロー 実際のジョブ実行フローは以下の通りです。スケジュール実行とオンデマンド実行のどちらも、同じ仕組みで動作します。 %%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#29b5e8'}}}%% sequenceDiagram autonumber participant User as 👤 ユーザー participant Tasks as ⏰ Snowflake Tasks participant Catalog as 📚 Catalog participant Queue as 📥 Job Queue participant Executor as ⚙️ Python Stored Procedure participant Sheets as 📊 Google Sheets alt スケジュール実行 Tasks->>Queue: 定期的にジョブを投入 else オンデマンド実行 User->>Queue: ボタンクリックでジョブを投入 end Queue->>Executor: ジョブを取得して実行開始 Executor->>Catalog: クエリ定義・パラメータを取得 Catalog-->>Executor: メタデータ返却 Executor->>Executor: Snowflakeクエリを実行 Executor->>Sheets: 結果を書き込み Sheets-->>User: ✅ データ更新完了 Note over Tasks,Sheets: GASが担っていた役割をすべてSnowflake内で実現 スケジュール実行もオンデマンド実行も、同じ Job Queue に投入される仕組みです。Executor がジョブを取り出すと、Snowflake でクエリを実行し、その結果を Google Sheets に書き込みます。 変化として大きかったのは、時間制限がなくなったことです。GAS では 30 秒でタイムアウトしていたクエリも、Snowflake 基盤なら数十分かかるクエリでも最後まで完了します。エラー時は自動リトライされ、複数ジョブの並列実行でスループットも向上しました。 リトライは exponential backoff で行われます。 詳細な図はこちら %%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#29b5e8'}}}%% sequenceDiagram autonumber participant Scheduler as ⏰ Scheduler Task<br/>(5分間隔) participant Catalog as 📚 Query Catalog participant Queue as 📥 Job Queue participant Stream as 🌊 CDC Stream participant Ingest as 📨 Ingest Task participant Executor as ⚙️ Executor Task participant Snowflake as ❄️ Snowflake participant Sheets as 📊 Google Sheets Scheduler->>Catalog: 実行すべきスケジュールを検索 Catalog-->>Scheduler: スケジュール一覧を返却 Scheduler->>Queue: JOB_REQUESTSにジョブを投入 Queue->>Stream: CDC検知 Stream->>Ingest: SYSTEM$STREAM_HAS_DATA() = TRUE Ingest->>Queue: JOB_REQUESTSを読み取り Ingest->>Queue: JOB_STATEにエンキュー (status=PENDING) Ingest->>Executor: AFTER句で次Taskをトリガー Executor->>Queue: PENDING状態のジョブを取得 Note over Executor: 状態: PENDING → RUNNING<br/>リース時間: 15分 Executor->>Catalog: クエリ定義とパラメータを取得 Catalog-->>Executor: メタデータ返却 Executor->>Snowflake: クエリ実行 ⏱️ 時間制限なし Note over Snowflake: 重いクエリでも確実に完了<br/>(GASなら30秒でタイムアウト) Snowflake-->>Executor: 結果セット (例: 5万行) Executor->>Sheets: データを書き込み Note over Executor: 指数バックオフでリトライ Sheets-->>Executor: ✅ 完了 Note over Executor: 状態: RUNNING → SUCCEEDED Snowflake Tasksによるワークフロー制御 Snowflake ネイティブなアーキテクチャの中心となっているのが、Tasks によるワークフロー制御です。GAS のトリガー機能に代わり、Snowflake Tasks でスケジューリングとジョブの依存関係を管理しています。なお、Python Stored Proced
こんにちは、LayerX Ai Workforce事業部でSWEをしているosukeです。 Webサービスの裏側など不特定多数のユーザーが操作する環境でAIエージェントを動作させるためにまず必要になるのがサンドボックス技術です。エージェントは自律的に柔軟な挙動をする反面、それらをセキュリティ的に閉じ込める必要が出てきます。 本記事では、Azure内で閉じる構成として、Claude CodeをSubprocessとして動かせるClaude Agent SDKとMicrosoft Foundry Hosted Agentというエージェントサンドボックス基盤を軸に構成の検討をしていきます。 AI エージェントにおけるサンドボックスの重要性 不特定多数のユーザーが触る環境で AI エージェントをサンドボックスで守るために、具体的には次のような要件が同時に求められます。 各ユーザーのチャット内容や添付ファイル・実行結果が、別のユーザーから見えないこと LLM が意図せず (あるいはプロンプトインジェクション的に悪意ある形で) 触ったコマンドが、ホストや他テナントのリソースに到達しないこと ファイル書き込み・ネットワーク通信・サブプロセス起動の副作用を、用途に応じて制限できること これらの個別の攻撃パターンを一つひとつ deny ルールで塞いでいくアプローチは、新しい variation が出てくるたびに穴が開きます。アドホックな個別対応は、時間とともに漏れが生じることを前提にすべきです。セキュリティは「包括的に、構造的に、多層に」組まないと持たない、というのが AI Agent の sandbox を設計するうえでの基本的な発想となります。 Hosted Agent の sandboxing Hosted Agent は、session 単位で microVM を立て、Azure VM 同士と同じ強度の境界で隔離する仕組みです。これを実機で確認するため、2 つの session で /proc/sys/kernel/random/boot_id を比較してみると、boot_id が完全に異なる、つまり session ごとに別の microVM を立てていることが実機で確認できます。 $ cat /proc/sys/kernel/random/boot_id # session A 8b86efce-0bb0-43af-b96b-72d78dfcad60 # session B bd14205a-5eb2-4145-8d0a-ac9ed6fa932d 同じ session で 2 回呼ぶと同じ boot_id が返るので、microVM の寿命が session 単位であることも確認できます。 Built-in ツール (Read / Write / Glob 等) はこの境界で防御 Claude Agent SDK の Read / Write / Edit / Glob / Grep は Agent プロセス内で完結する処理なので、microVM の filesystem に閉じています。cwd をプロジェクトディレクトリに制限し、Agent SDK の add_dirs=[] で cwd の外側を既定で読めないように制限すれば、別 session のファイルが見えてしまうことは構造的に発生しません。 問題は Bash です。Bash を allow にした瞬間、microVM 境界だけでは塞げない経路が多く出てきます。エージェントが Bash を手にすることで本来の自律的な動きが備わる一方でセキュリティ的な考慮点が大幅に広がります。 例えば、外部サービス(Model など)にアクセスするための Token など機微情報がファイルシステム上に存在しているとそれを守る必要があります。あるいは、あらゆる手段で外に抜けようとする内部通信や外部通信を防ぐ必要があります。 Hosted Agent においては Bash 実行における脅威を保護するためにもう一つレイヤを入れる必要があります。 Bash 経由の脅威を個別対応で塞ぐことはできる、しかし 同一 microVM 内で Bash が起こせる脅威に対しては、それぞれにアドホックな対応を積み重ねていけば、原理的には個別に塞ぐことができます。具体的なイメージは以下の通りです。 機微情報抽出 (Bash から token 等を読まれて外部クラウドの権限を奪取される) → 読み出しコマンドを deny rule で塞ぐ + OS の uid 分離で file permission を活用して読み取り権限自体を落とす sibling process の env 漏洩 (/proc/<別 pid>/environ 経由で同 container 内の他プロセスの secret を吸われる) → Bash subprocess の uid を分離する + container に渡す env を最小化する。同一 micro VM 内なので脅威は限定的ではある。 persistence pivot (.bashrc / CLAUDE.md を書き換えて次回起動時の hook に悪意あるコードを仕込まれる) → 設定ファイル群を read-only マウントにする + 起動時に読み込まれる設定ファイルの読み込み自体を無効化する。同一 micro VM 内なので脅威は限定的ではある。 TLS Inspection 回避経由の exfil (allowlist 許可ドメインへの SNI 偽装や DNS exfil で secret を外に持ち出される) → Firewall allowlist を最小化 + DNS 経由で wildcard 的に到達先を広げられないよう制御する アドホック対応は漏れやすい 冒頭の通り、これを「アドホックな個別対応の積み重ね」として運用しようとすると、いくつもの構造的問題が出てきます。 1: 個々の対応の境界がそもそも狭い。たとえば前述の permissions.deny の Read rule は cat / head / tail / sed のような「読み出し専用ツール」には適用されますが、ファイルを間接的に開ける任意のサブプロセス(スクリプト言語の標準ライブラリ、テキスト処理ツール、bash builtin のリダイレクト等)には適用されません。例えば Python が同梱されていれば、 python3 -c "print(open('/run/secrets/example-token').read())" のような形で deny rule を構造的に素通りできます。回避経路はこのカテゴリ全体にわたるため、コマンド単位の deny を積み上げても閉じません。 2: 新しい脅威カテゴリが見つかるたびに穴が開く。Python 経由の file read が回避経路だと分かった時に Python 対策を足す、というイタチごっこになる。 3: 監査説明性が低い。お客様のセキュリティレビューや社内監査で「どんな攻撃から守られているか」を説明するときに、個別ルールの組み合わせよりも、「OS 層の namespace で構造的に切ってあります」と言える方が説明性も再現性も高い。 要するに、AI Agent の sandbox を本気で組むなら、個別 deny rule の積み上げではなく、構造的・包括的・多層的な防御を OS / VM 境界で組むのが筋 です。 Bash sandbox のパターン比較 それを踏まえて、LLM に Bash を渡すための実装パターンを 4 つ並べました。ここでは、Hosted Agent を基盤として、Azure 内に閉じた構成にできる技術選定に絞っています。Hosted Agent は microVM 境界を提供する基盤として有用ですが、その上で Bash を sandbox 化するレイヤがもう一つ必要になります。 パターン 隔離レイヤ 実行体 (1) Hosted Agent 上で素の Bash + 個別アドホック対応 なし (個別 deny の積み上げ) container 内の実 bash (2) just-bash (Vercel) JS interpreter 層 (Node.js プロセス内) TS 製の bash 互換 interpreter (3) Azure Container Apps Dynamic Sessions で safe-bash MCP tool 別 microVM 層 (Cloud Hypervisor) 別 microVM 内の実 bash (4) Claude Code 公式 sandbox OS 層 (Linux: bubblewrap / macOS: Seatbelt) 同 container 内の実 bash を namespace で jail (1) については前節で議論した通り「アドホック対応の積み上げは漏れる前提」なので本命からは外しています。(2) と (3) も検討候補にはなりましたが、いずれも本検証では本命から外したので、まず先に短く触れます。 (2) just-bash Vercel の just-bash は TypeScript で書かれた bash 互換 interpreter + in-memory virtual filesystem で、child_process を import すらせず実 process を spawn しないというユニークな設計です。「実 bash を渡さないことで攻撃面そのものを狭める」という思想自体は綺麗ですが、ただ 2026 年 5 月時点では README にも明記の通り Beta Software で、リリース履歴もまだ若く、信頼境界に持ち込むには実績が十分とは言えない段階と考えます。 (3) Azure Container Apps Dynamic Sessions: 強い分離・重いコスト 外部の 隔離環境 (Azure Container Apps Dynamic Sessions) に bash 実行を外注する設計で、Microsoft 公式に "designed to run untrusted code" と明言されているだけあって sandbox 強度的には適した基盤として扱えます。ただ設計を進めてみると、軽い実行でもオーバーヘッドが重い、cold start考慮、並列実行のための特殊な考慮など構造的なコストが重く、Agent 体験を最優先するワークロードには合いませんでした。 (4) Claude Code Bash Sandbox: Hosted Agent と組み合わせて構造的二層防御に Claude Code (= Claude Agent SDK の CLI 部分) の Bash sandbox 機能 は、CLI binary 内に bubblewrap (Linux) を呼び出す実装が組み込まれていて、Bash tool subprocess を OS 層で jail できます。ClaudeAgentOptions.settings 経由で設定を渡すと、 mount namespace で cwd 外を ro,bind に net namespace で外向き通信を構造的に遮断 pid namespace で sibling process 不可視 user namespace で uid 分離 filesystem deny/allow rule で JWT 等の secret を物理マスク これらを OS の namespace 機構で一括して構造的に切ってくれます。 ただ、Bash sandbox を使うためには、上記のように非特権プロセスが syscall で namespace を作れる環境である必要があります。実は、Hosted Agent はマネージドサービスでありながらこの点で相性が良く、VM 境界に信頼をおいているゆえ、namespace を切ることができるようです。 実機で sandbox 有効状態にして、各種検証したログがこちらです。 # 1. federation token の読み出し — denyRead が OS 層で mask $ cat "$EXAMPLE_TOKEN_FILE" # 機微情報を保持する環境変数 (例: federation token のパス) cat: /run/secrets/example-token: No such file or directory # 2. Python 経由で同じファイルを開いてみる — Python でも同様に存在しないことになっている $ python3 -c "open('/run/secrets/example-token').read()" Traceback (most recent call last): File "<string>", line 1, in <module> FileNotFoundError: [Errno 2] No such file or directory: '/run/secrets/example-token' # 3. sibling process の environ を覗く — pid namespace 分離で jail 内の pid しか見えない $ ls /proc | grep -E '^[0-9]+$' 1 2 4 $ cat /proc/123/environ cat: /proc/<span class="s
こんにちは。LayerX の Ai Workforce 事業部 FDE部エンジニアのkoseiと申します。 当事業部では、エンタープライズのお客さま向けにAi Workforceというプロダクトを提供しています。 今回はAi Workforceそのものの話ではないですが、関連する一般的なユースケースについて少し試していることを共有します。 組織が大きくなると、多種多様で膨大なアセットをどう活かすか、どう管理し、守り、継続的に価値を出していくのかが重要になります。 ここでいうアセットは、単なるファイルや文書だけではありません。個別の案件やプロジェクト、ノウハウ、実施機関や取引先企業、契約や判断履歴、現時点でのリスク状態なども含まれます。 こうした対象は、一度評価して終わりではなく、外部で起きた出来事をきっかけに、評価や優先順位、活用可能性が変わっていきます。そうなると、人間がすべてを手作業で追い続けるのではなく、エージェントが 今どれがリスクが高まりそうか どれに人が入るべきか どこは一旦据え置いてよいか を常に監視・更新し、人間がすぐにアクションに活かせる状態を作りたくなります。 今回は、そのようなケースで何が重要になってくるのかや肌感を得るため、 「外部イベントをフックに、管理対象の評価を更新していく ambient agent 的なもの」を、小さく作って試してみました。 背景 このような「外部を起点に、管理対象の評価・判断を更新し続ける」ユースケースは、意外と幅があります。 例えば、公募案件の一覧に対して、自社アセットを活かして取りにいけそうな案件はどれかを継続的に監視・評価する、というケースがあります。 他にも、コンプライアンス運用において、法改正や当局ガイダンス、同業他社への行政処分のニュースを受けて、今どのルールは優先的に見直しをした方が良いかを考えるなどがあり得ます。 ただ、ここで難しいのは二次的な影響です。外部イベント(ニュース記事等)に案件名や実施機関名がそのまま出てくるなら話は簡単ですが、実際にはそうならないことのほうが普通です。たとえば、湾岸の情勢の悪化やタンカー被害のニュースは、個別の内陸インフラ案件名を含みません。航空便の大量欠航や移動制限のニュースも、どの島嶼国の空港案件や物流案件に影響するかまでは直接書いてくれません。商品価格や燃料価格の変動も、どの設備案件に効くかを明示してくれるわけではありません。 このあたりはサプライチェーンの文脈では以前からよく議論されているようです。特定の外部イベントがどの調達先や物流網、供給先に波及するのかを把握し、調達不能になるリスクを下げたり、代替調達を素早く打てるようにしたりする、という文脈です。 今回見たかったのも構造としてはかなり近く、違うのは対象が部材や供給網そのものではなく、案件やプロジェクト、その評価状態(リスクがどの程度か)である、という点です。 しかも、外部イベントとして得られる記事が、必ずしも管理対象の文書と意味的に近いとも限りません。たとえば「航空便の大量欠航」という記事に対して、本当に見たいのは航空会社そのものではなく、現地渡航や施工監理が止まりそうな案件、輸送制約で機材搬入が遅れそうな案件のような、もう一段波及した先です。 このとき、単純な対象名一致やベクトル検索だけだと、二次的な影響先までは届きにくいです。そこで今回は、この「二次的な波及」についても試すのにちょうどよい題材として、Knowledge Graph (以後KG)を使用しました。 題材とするユースケース 今回は題材として、World Bank の公開プロジェクト文書と外部ニュースを使いました。複数プロジェクトを横断的に見ているマネージャーや評価機関のような立場を想定して、「外部環境の変化によってどのプロジェクトが危なそうか」「どのプロジェクトに手を入れるべきか」を見たい、というダミーユースケースを置いています。 World Bank のdocumentsデータには、「プロジェクトの関連文書」として、プロジェクトの概要や追加融資の背景が書かれた資料、プロジェクトの基本情報や環境・社会配慮の論点がまとまった資料、環境影響評価のための文書といった資料が多く含まれており、実際の業務におけるユースケースに近い形で実験できるため、今回用いました。 データ出典: The World Bank, Projects & Operations / World Bank Projects API, 取得日:2026年5月20日。 The World Bank, Documents & Reports API, 取得日:2026年5月20日。 World Bank 資料の利用は、Terms of Use for Datasetsに従います。 これらの資料を用いて、KGを構築します。 イベント側のデータとしては、外部ニュースのようなものを想定しています。今回は具体的なソースとして GDELT を使いました。特に、15分ごとに更新される GDELT Event Database という、報道されている情報を元に「誰が、誰に対して、何をしたか」といったメタデータが付与されたイベントデータセットから抽出して利用しました。 データ出典:The GDELT Project(https://www.gdeltproject.org/) 本当は、一定期間で LLM が「リスクが高い」と判定した案件が、結果として本当に危なかったのか、あるいは安定してうまくいったのかまで検証できるとさらに面白かったと思っています。World Bank の案件には第三者機関による評価の仕組みもあるようで、そういったスコアと照らし合わせられると、どの程度当たり外れがあったのかをもう少し定量的に見られたはずです。今回は時間の都合でそこまではできていません。 画面イメージ 今回試作したものの画面は以下になります。 1枚目は各プロジェクトのリスクに関するスコアを集計した全体を俯瞰するダッシュボード的な画面と特にリスクが高く手当が必要なプロジェクトが並んでいる画面です。2枚目は、特定のプロジェクトをクリックして閲覧可能な詳細画面であり、各プロジェクトの具体的なリスク内容等を検討する画面です。 重要な点としては、各値は固定のレポートではなく、外部イベントをきっかけにエージェントが情報収集・判断した結果を集計・反映したものであり、外部イベントが発生するたびに更新されていくということです。 人間はこのモニターを起点に、スコアが大きく変化した案件を深掘りしたり、異常な変化だけを別途検知したりできます。 今回の画面では、実際に LLM が作ったリスク評価結果を使っています。つまり、最終的にやりたかったのは、 人間が一覧で全体感を掴む 変化が大きいものに気づく 必要なときだけ個別案件に入る という運用であり、モニターはそのための入口です。 今回の構成 今回の構成は、大きく分けると 3 段階です。 2と3を繰り返すことで、外部イベントを起点にした判断結果データ(エージェントによるリスク評価)を更新し続ける仕組みです。 まず、World Bank のプロジェクト文書から KG を構築し、各プロジェクトの初期リスク評価を作って DB に保存する その後、イベント(ニュース)を入力にして、KG 上で関連しておりリスク影響がありそうなプロジェクトを見つける プロジェクト文書等の詳細と照らし合わせて、リスク評価の更新を推論する flowchart TB %% ===== データソース ===== subgraph WB["世界銀行プロジェクト(一次データ)"] WB_DOC["プロジェクト文書<br>(PDF/HTML/JSON)"] WB_META["プロジェクトマスタ<br>(ID / 国 / セクター / 金額など)"] end subgraph EXT["外部イベント(ニュース等)"] EV["外部イベント<br>(ニュース等)"] end %% ===== 事前構築(KG + DBの初期評価) ===== subgraph PREBUILD["1. 事前構築(KG構築 + 初期DB構築)"] RELEX["LLMで関係抽出"] KG((("Neo4j ナレッジグラフ"))) PM[("プロジェクトDB<br>(project_id, title, links, ...)")] RISK[("リスクDB<br>(type, prob, impact, rationale, ...)")] WB_DOC --> EVAL_INIT["LLMで初期リスク評価"] --> RISK end %% ===== イベントフックでのKG探索 ===== subgraph EXPLORE["2. イベントフックでのKG探索"] ING["イベント取り込み"] QGEN["KG探索"] JOIN["関連プロジェクト候補の抽出"] end %% ===== リスク評価の更新 ===== subgraph UPDATE["3. リスク評価の更新"] EVAL["LLMでリスク評価<br>(記事 + 根拠パス + 既存状態)"] HIST[("イベント評価履歴DB<br>(event, path, diff, timestamps)")] end %% ===== PREBUILD ===== WB_DOC --> RELEX --> KG WB_META --> PM %% ===== EXPLORE ===== EV --> ING --> QGEN --> JOIN KG --> JOIN %% ===== UPDATE ===== JOIN --> EVAL PM --> EVAL RISK --> EVAL EVAL --> HIST %% ==== overall ==== 1. 事前構築 flowchart TB %% ===== データソース ===== subgraph WB["世界銀行プロジェクト(一次データ)"] WB_DOC["プロジェクト文書<br>(PDF/HTML/JSON)"] WB_META["プロジェクトマスタ<br>(ID / 国 / セクター / 金額など)"] end %% ===== 事前構築(KG + DBの初期評価) ===== subgraph PREBUILD["1. 事前構築(KG構築 + 初期DB構築)"] RELEX["LLMで関係抽出"] KG((("Neo4j ナレッジグラフ"))) PM[("プロジェクトDB<br>(project_id, title, links, ...)")] RISK[("リスクDB<br>(type, prob, impact, rationale, ...)")] EVAL_INIT["LLMで初期リスク評価"] end WB_DOC --> RELEX --> KG WB_DOC --> EVAL_INIT --> RISK WB_META --> PM KGの構築自体は、かなりシンプルに neo4j_graphrag (https://github.com/neo4j/neo4j-graphrag-python)の SimpleKGPipeline を使って構築を始めました。 llm = OpenAILLM(model_name=kg_model) embedder = OpenAIEmbeddings() pipeline = SimpleKGPipeline( llm=llm, driver=driver, embedder=embedder, schema=KG_SCHEMA, on_error="IGNORE", from_file=True, perform_entity_resolution=True, ) await pipeline.run_async( file_path=str(path), document_metadata=metadata, ) このくらいのコード量でスタートできるので、「まずナレッジグラフを構築して動かしてみる」用途ではかなり扱いやすいかと思います。 また、今回は どの組織が実施主体なのか どの施設や設備が関係するのか どの国や制度と結びついているのか などを ノードやリレーションとして設定しておくことで、二次的な波及もLLMが推論に活かすことを期待します。 最終的にはプロジェクトへの影響が見たいので、KG上には、管理対象である「プロジェクト」を表すノードも追加しておき、 そのノードとのリレーションも推論させます。(プロジェクトへの波及の推論に使うため) これにより以下のようなナレッジグラフが得られます(ピンク色の大きめの丸がプロジェクトに対応します) 2. ニュース評価 flowchart TB subgraph EXT["外部イベント(ニュース等)"] EV["外部イベント<br>(ニュース等)"] end subgraph EXPLORE["2. イベントフックでのKG探索"] ING["イベント取り込み"] QGEN["KG探索"] JOIN["関連プロジェクト候補の抽出"] ING --> QGEN --> JOIN end EV --> ING 外部イベントであるニュースが入力されると上記のフローで関連するプロジェクトのリスク評価を更新します。 今回は素朴に、探索の起点としてまず記事本文に近い Chunk を探し、その Chunk から関連エンティティや 管理対象プロジェクトのノードまでのパスを取っています。 3. リスク評価更新 flowchart TB subgraph UPDATE["3. リスク評価の更新"] EVAL["LLMでリスク評価"] HIST[("イベント評価履歴DB<br>(event, path, diff, timestamps)")] end JOIN["関連プロジェクト候補の抽出"] PM[("プロジェクトDB<br>(project_id, title, links, ...)")] RISK[("リスクDB<br>(type, prob, impact, rationale, ...)")] JOIN --> EVAL PM --> EVAL RISK --> EVAL EVAL --> HIST その後、 ニュース本文 KG 上のパス 既存リスク プロジェクト文書 を合わせて LLM に渡し、「このプロジェクトのリスク評価をどう更新すべきか」を判断させる流れにしました。 更新先のデータベースはかなりシンプルで、イメージとしては、 project_id リスク内容 リスクの影響度 リスク発生確度 判定理由 のような項目を持つものです。 実際に作ってみてどうだったか 結論から言うと、今回狙っていたような 外部イベントから KG を通じて 妥当な二次的波及を拾い そのままリスク評価の更新を適切に行う というところまでは、まだ十分に実証できたとは言いづらかったです。 12回分のイベントデータを入力してリスク評価の推移を人手で見た所感ですが、妥当な結果よりも「変化しない」「誤検知」の割合がか
バクラク事業部ソフトウェアエンジニアの矢田(@0e2b3c)です。 LayerXはゴールドスポンサー並びに学生支援スポンサーとしてTSKaigi 2026に協賛させていただきます。 加えて、弊社ソフトウェアエンジニアである泉(@izumin5210)、福岡(@syumai)、山本(@minako-ph)、田中(@ypresto)が登壇者として参加を予定しています。 スポンサーブースのご紹介 昨年度までは社内ADRを公開するというブース内容でしたが、今年度は新しい取り組みとして社内の面白かった・苦労した実装を紹介するブースを出展予定です。 弊社の事業の1つであるバクラク事業部の各チームにフロントエンドやTypeScriptに関するインタビューを行い、それをスライドとしてまとめたものをブースにて展示予定です。 LayerXの実際の開発を体感できるこれまでにない取り組みですので、是非当日は足をお運びください。 登壇のご紹介 開発体験を左右するライブラリの API 設計 ― GraphQL スキーマ構築ライブラリから考える@izumin5210 TypeScript で GraphQL を実装するためのライブラリには、さまざまな API 設計が存在します。Schema-first と Code-first といったスキーマ記述アプローチの違い、スキーマと TypeScript 型をどのように接続するか、resolver をどのような形で記述させるかといった設計上の選択は、単なる記法の違いではなく、開発者がどのようにスキーマと向き合い、どのように設計を組み立てるかという開発体験そのものを規定します。 本発表では、既存の GraphQL 実装ライブラリを比較しながら、スキーマ構築と resolver 実装の API 設計を整理します。特に、スキーマと型の結びつけ方や、開発者に与えるメンタルモデルの違いに注目し、それぞれのトレードオフを明らかにします。そのうえで、AI Coding が前提となりつつある現在、ライブラリに求められる API 形状や型情報の役割がどのように変化しうるのかを検討します。整理を踏まえ、新たに開発した GraphQL 実装ライブラリ「gqlkit」の設計を一例として紹介し、今後の GraphQL 実装基盤の方向性を提案します。 日時:Day1 / 11:00 ~ 11:40 場所:RightTouchトラック 2026.tskaigi.org Oxlintはいかにしてtsgolintのlint ruleを呼び出しているのか@syumai Oxlintは、VoidZeroによって開発されている、Rust製の高速なJavaScript / TypeScript Linterです。 既に安定版のv1がリリース済みですが、実は、そこにはTypeScriptの型情報に依存するルールの実装が含まれていません。 その役割を担うtype-aware lintingの実装は、typescript-goベースの別リポジトリである、oxc-project/tsgolintで行われています。 tsgolintを通じてlintを行うことで、Oxlintは no-floating-promises などのlint ruleをサポートすることができます。 つまり、Rust実装であるOxlintから、Go実装のtsgolintを呼び出していることになるのですが、一体これはどのように実装されているかご存知でしょうか? 本発表では、発表者がOxlintおよびtsgolintの実装を読んで知った、プロセス間通信に基づくOxlintとtsgolintの通信方法についての概要と、tsgolint側にルールを追加する度に必要となるOxlint側での実装について解説を行います。 また、発表者が、tsgolintにカスタムルールを実装しようとした過程で得た知見についてもお話しします。 日時:Day1 / 17:20 ~ 17:50 場所:Leveragesトラック 2026.tskaigi.org 柔軟なPDFレイアウトエディタを支える型システム設計 — Discriminated UnionとConditional Typeの実践@minako-ph バクラク請求書発行では、請求書・見積書・納品書などのPDFレイアウトをGUIで自由にデザインできるビジュアルエディタを提供しています。テキスト・画像・動的テーブルなど6種類のノードを自由に配置し、実際の書類データをバインドしてPDFを生成する仕組みです。 しかし自由度が高いほど、コードの複雑さも増します。テキストノードにはフォント設定があり、テーブルノードには列定義があり、画像ノードにはフィット方法がある——同じ「ノード」でも中身はまったく別物です。さらにノードの値は固定テキストかもしれないし、書類データから動的に取得するかもしれない。こうした組み合わせの中で「テキストノードなのにテーブルの列定義を参照してしまう」ような取り違えをどう防ぐか。人のレビューに頼るのではなく、型に任せられないか—— 本トークはその問いから始まります。 Protocol Buffersのoneof定義からZod経由でDiscriminated Unionを自動生成する仕組み、Conditional Typeでノード型名から値型を安全に取り出すパターン、DeepPartialでReducerの部分更新を型安全にする設計、そして同一の型定義でエディタのプレビューとPDF本番生成の両方を駆動するアーキテクチャを、具体的なコードとともに紹介します。 日時:Day2 / 15:50 ~ 16:20 場所:RightTouchトラック 2026.tskaigi.org TypeScriptはどのようにどこまで推論できるのか ─ とにかく as は禁止で@ypresto(スポンサーセッション) TypeScript の型チェックは、型推論、Control Flow 解析、そして各種ガードレールという強力な3つの仕組みに支えられています。そしてInferred Type Predicates (filter()の型推論) のように、以前は手でケアしていた場面でも自動で正しい型が付くようになるなど、これらの機構は年々強化されてきました。 一方これらの機構でカバーできるにも関わらず as (as constを除く) を使用してしまうことは、AI 時代に必要な安全性を自ら緩めることになり、それは "敗北" です。 本セッションは、Contextual Type をはじめとして、TypeScript の推論機構やガードレールの仕組みと制限を紹介し、その安全性を活かすことにより、「as全部禁止」に対して「主張が強い」と返されないように試みるものです。 日時:Day2 / 12:30 ~ 13:30(スポンサーセッション内) 場所:Leveragesトラック 2026.tskaigi.org TSKaigi 2026について 2024年に産声をあげ、2025年も大盛況のうちに幕を閉じた TSKaigi を今年も開催します! 私たちは、誰かの発表を聞くだけでなく、他の誰かに向けて発表することもまた学びの一つだと考えています。 参加者、登壇者、スタッフ、スポンサーをはじめ、TSKaigi に関わるすべての人たちが互いに学び合い、新たな繋がりを生み出し、型にとらわれないエンジニアとして生き生きと活躍できる世界を目指します。 TypeScript に関するあらゆるテーマを扱う国内最大級のカンファレンスとして、まさに「型破り」なイベントを目指し成長を続ける TSKaigi にご期待ください。 詳細やタイムテーブルなどは TSKaigi 2026公式サイト をご覧ください。 協賛の背景 わたしたちは技術コミュニティから日々たくさんの技術的な知見を頂いたり、実際にOSSとして活用させて頂いています。 LayerXの掲げる行動指針である「徳」の観点からも技術コミュニティから一方的に恩恵を受けるだけでなく、技術コミュニティへの貢献を継続して行っています。 アフターイベントのご案内 TSKaigi 2026終了後、6/3(水)にUbie様、ビットキー様との共催でアフターイベント「歴史あるプロダクトで、"AIに任せられる領域"をどう広げるか?TSKaigi アフターイベント」を開催します。 TSKaigi本編では話せなかったTypeScriptトークや懇親会でより学びを深められる機会となりますので、是非下記connpassリンクよりお申し込みください。 bitkey.connpass.com 最後に 過去最多の登壇人数、新たなスポンサーブースで、これまでで最大規模となるTSKaigiへ参加します。 TypeScriptやフロントエンド開発に関してディスカッション、交流できればと思いますので、是非お気軽にスポンサーブースやセッションへ足をお運びください!
はじめに バクラクのQAエンジニアをしているteyamaguです。 バクラクでは、カスタマーサポート担当者やカスタマーサクセス担当者が開発チームへ問い合わせをエスカレーションする際、QAエンジニアが一次対応を担当しています。一次対応の中では、調査開始時点では追加確認が必要になるケースがありました。このような場合、不足情報を確認するために担当者がお客様へ連絡し、回答が返ってくるまで調査が止まっていました。このタイムラグが積み重なると、対応リードタイムの長期化につながっていました。 そこで、LLMを使って不足情報を自動検出し、「お客様へ確認すべき質問」を生成するボットを社内に構築し、試行しています。担当者がエスカレーションとして問い合わせ入力をSlackに入力すると、その入力をボットが検知し、Slack上に確認すべき質問が出力される仕組みです。 このボットを運用し始めてすぐ、想定外の問題に直面しました。LLMが「お客様にこの時点では追加で確認しなくてよい情報」まで質問として出力してしまっていたのです。 本記事では、この問題に対して「LLM自身に分析・提案させる」アプローチを試し、改善した過程をまとめます。 何が起きていたか ボットが生成する質問を実際に確認したところ、調査に不要と判断できる出力が含まれていることがわかりました。 ケース1: 再現済みなのに継続確認を求める 問い合わせ内容(抜粋): - 再現環境: ステージング環境にて再現確認済み - 再現手順・スクリーンショット: 提供済み ボットの出力: 「現在も本番環境で同様の事象が発生しているか確認してください」 「事象発生日時(日時と可能であれば時刻)をご教示ください」 ステージング環境で再現済みで、手順もスクリーンショットも提供済みなのに、発生日時や本番環境でも継続して発生しているかの確認を求めています。再現確認と手順が揃っているため、調査の開始にあたってはこれらの情報は不要でした。 ケース2: 内部で確認できる情報を聞く 問い合わせ内容(抜粋): - テナントID: 提供済み - 問い合わせ種別: 仕様確認(ステータスを元に戻せるか) ボットの出力: 「ご利用中の連携先サービス名および連携設定状況をご教示ください」 「ご契約情報をご教示ください」 仕様確認の問い合わせに対して、テナントIDがあれば内部で確認できる情報を質問しています。弊社のシステムでは、お客様の環境ごとにテナントIDが割り当てられており、そのIDがあれば一定の情報を取得でき、お客様に聞く必要がありません。 データで見る問題の規模 実際の11件の出力に含まれる46個の質問項目に、OK/NGラベルを付けて分類しました。 評価 件数 割合 OK(適切な質問) 9個 20% NG(不要な質問) 37個 80% 出力された質問の8割がNGでした。 NGをパターン別に分類すると、以下のようになります。 パターン 件数 割合 再現状況から判断すると不要(再現済みなのに継続確認を求める等) 18個 49% 内部で確認できる情報を聞いている 13個 35% 問題の性質に合わない情報を聞いている(仕様確認に環境情報など) 4個 11% 問い合わせ文から自明な情報を再確認している 2個 5% 最多は「再現状況から判断すると不要」で、半数近くを占めていました。 なぜ起きていたのか プロンプトの初期設計を振り返ると、「何を聞くべきか」は詳細に書いていましたが、「何を聞いてはいけないか」は書いていませんでした。 LLMは「不足情報を見つけて質問を生成する」という指示に対して、忠実に、ときに過剰に最適化します。つまり、あらゆる不足情報を質問として出力しようとします。「内部で確認できるか」「再現済みで不要か」といった判断は、明示的に指示しないと、安定しにくい傾向があります。 「何を聞くべきか」を詳細に指示するだけでは足りませんでした。では、「何を聞いてはならないか」と書けばよいのでしょうか。それを人間が網羅的に考えるのは容易ではありません。 そこで、ラベル付きデータをLLM自身に渡して、プロンプトの改善を依頼するというアプローチをとりました。 解決策: LLM自身に出力を分析・提案させる プロセス ボットの実際の出力にOK/NGラベルを付けてデータ化する そのデータをLLMに渡し、プロンプトの改善案を提示するよう依頼する LLMが提案した改善内容をプロンプトに組み込む ポイントは、人間が「禁止ルール」を考えるのではなく、LLMにNGパターンを分析させ、LLM自身に「自分が聞くべきでないこと」を導かせた点です。LLMの提案内容は、人間がレビューした上でプロンプトに組み込みました。 LLMへの依頼はシンプルで、改善案の提示を求めるものでした。禁止ルールという形式はLLM自身が提案したものです。 LLMが導いた禁止ルール 元のプロンプトは、以下の5つのStepで処理を行うものでした。 問い合わせ内容の分類 提供済み情報の確認 プロダクト別不足情報の特定 事象固有の追加確認項目 優先順位・出力ルール これらのStepに、LLM自身がStep 2.5として追加ステップを提案しました。Step 3〜4で候補に挙がる質問のうち「聞くべきでないもの」をあらかじめ定義することで、過剰な質問生成を抑制します。 LLMが提案した内容を整理・確認し、プロンプトに追加しました。以下にそのステップを示します。 ### Step 2.5: 質問してはいけない情報の除外 Step 3〜4で候補に挙がった質問について、 以下に該当するものは必ず除外してください。 ■ 内部確認可能なため聞かない - 連携先サービス名および連携設定状況 - 契約情報 - 対象データの現在のステータスや操作日時 - ユーザーIDが提供済みの場合の関連データ(社内で確認できる情報) - 調査用IDが提供済みの場合の発生日時(IDをもとに社内で特定できる場合) ■ 再現状況に基づいて聞かない - 再現済みと明記されている場合 →「現在も継続して発生しているか」「本番でも発生するか」は不要 - 再現手順・スクリーンショットが提供済みの場合 → 発生日時は確認不要 ■ 問題の性質に基づいて聞かない - 仕様確認の場合 → 環境情報・継続確認は原則不要 - サーバー側処理が原因の事象 → ブラウザ・OS・アプリバージョンは不要 - データ処理・計算ロジックの問題 → ブラウザ・OS情報は不要 ■ 問い合わせ文から自明な場合は聞かない - 期待結果が問い合わせに明記されている場合 → 期待結果を再確認しない - 事象の対象が特定されている場合 → 同じ情報を別の呼び方で再度聞かない あわせて、除外後に質問が0件になった場合の出力も定義しました。 【追加確認は不要です】 調査に必要な情報は揃っています。 このまま開発チームへ共有・調査を進めてください。 結果 禁止ルール追加後の出力についても同様にOK/NGラベルを付けて集計したところ、NG率は改善前の80%から約61%に低下しました。ただし、今回の集計は改善前11件・改善後13件という限られたサンプルに基づいており、統計的な有意性は保証できません。傾向の把握を目的とした参考データとしてご覧ください。また、61%はまだ高い数値です。今回この記事で伝えたいのは最終的なNG率そのものではなく、「ラベル付きデータをLLMに渡してルールを導かせる」というアプローチ自体の再現性です。数値はあくまで手法の有効性を示す参考指標として読んでいただければと思います。 NGパターン別の内訳比較 以下の割合はNG全体に占めるパターン別の比率を示しています。 パターン 改善前(NG内訳) 改善後(NG内訳) 再現状況から判断すると不要 49% 40% 内部で確認できる情報を聞いている 35% 50% 問題の性質に合わない情報を聞いている 11% 0% 問い合わせ文から自明な情報を再確認している 5% 10% 「問題の性質に合わない情報を聞く」パターンが完全に消えました。 仕様確認なのに環境情報を聞く、計算ロジックの問題なのに連携先サービスを聞く、といった出力がゼロになりました。「再現状況から判断すると不要」なパターンも減少しました。 一方、「内部で確認できる情報を聞く」パターンは残存し、むしろ割合が増えました。 NG全体の数が減った結果として相対的に目立つようになった面もありますが、このカテゴリの禁止ルールがLLMにとって判断しにくいことも示しました。「内部で確認できるかどうか」はシステムの知識が必要な判断であり、プロンプトに列挙できる具体例には限界があります。この点は継続的な改善課題と認識しています。 振り返って気づいたこと データが先、ルールが後 最初からこのルールが書けたわけではありません。まずデータを集めてラベルを付け、それをLLMに渡して初めて「どういう条件のときに不要な質問が生まれるか」が言語化されました。 人間がルールを先に考えると「もれなく書かなければ」という意識が働き、かえって抽象的で使いにくいルールになりがちです。実際のNGパターンから帰納的にルールを作る順序が重要でした。今回は11件という少量のデータでも4つのパターンが出揃い、ルールの骨格を作ることができました。 ラベル付きデータをLLMに渡す LLMに「あなたの出力のどこが問題か」を分析させることができました。その際、単に問題のある出力を見せるより、OK/NGのラベルを付けて渡す方が、LLMはパターンを把握しやすくなると考えています。 この「ラベル付きデータを渡してLLMに改善を依頼する」というアプローチは、プロンプト改善の汎用的な手順として使えると考えています。 「質問しない」という出力を定義する 改善前のプロンプトは「質問を生成する」ことを前提に設計されていたため、情報が十分に揃っている場合でも、何かしら質問を出力しようとする傾向がありました。 LLMの提案により「質問が0件のときは追加確認不要と出力する」というルールが加わったことで、質問を生成しないことも正しい出力であると明示できました。当初は想定していませんでしたが、実運用上の価値が大きいことがわかりました。 おわりに 「LLMの出力精度を上げる」というと、モデルの選択やFew-shot(プロンプトに具体例を埋め込む手法)の工夫に目が向きがちです。しかし今回の改善の核は、実際の出力データをLLM自身に分析させるというアプローチでした。 QAエンジニアとして、この流れはテスト設計の改善サイクルと近いものがあります。不具合を収集し、パターンを分析し、設計にフィードバックします。LLMのプロンプト改善も、同じ考え方で進められました。 今回解消しきれなかった「内部で確認できる情報を聞く」パターンについては、Few-shotで具体例を増やす、あるいはラベリングを継続して次のルール改善サイクルに入るといったアプローチを検討しています。改善サイクルを回し続けることが、プロンプトの精度を高める近道だと考えています。 LayerX バクラク事業部QAでは、こうした試行錯誤を一緒に楽しみながら品質に向き合える仲間を募集しています。興味のある方はぜひエントリーください。 【バクラク】QAエンジニア_ポテンシャル枠 / 株式会社LayerX 【バクラク】QAエンジニア / 株式会社LayerX 【バクラク】シニアQAエンジニア / 株式会社LayerX また、カジュアル面談も行っていますので、興味のある方是非お話ししましょう! jobs.layerx.co.jp
こんにちは、Ai Workforce事業部でAI検索エンジニアをしている鷹取です。 AIエージェントにドキュメントを探させる方法として、最近は検索ツールを使わせるアプローチに関心があります。検索APIを直接呼ばせる方法もありますが、ディレクトリを見たり、候補文書を読んだり、検索語を変えたりできる探索的なインターフェースも相性が良さそうです。 その方法として面白いのが、Agent向けの仮想ファイルシステムです。Agentには ls, cat, grep のようなファイル操作を見せつつ、実体は検索エンジン上のデータにします。これなら、Agentはファイルを探索しているように振る舞えますが、システム側では検索エンジンのindexingや権限制御を使えます。 今回は、この仮想ファイルシステムをOpenSearch上に作り、さらに自然文で探せる semantic_search も同じ探索面に追加してみました。ファイルシステム風の使いやすさを保ちながら、Document Level Securityによる権限制御を cat, grep, semantic_search に同じように適用できるかを試します。 今回の実験で作成したコードは以下で公開しております。 github.com 背景:Agentに検索させるという考え方 Ai Workforceは、エンタープライズ向けのAIプラットフォームです。多種多様なドキュメントワークをAIエージェントを使って自動化できます。 AIエージェントが効率的に動作するには、社内に存在するさまざまなドキュメントを適切に検索できる必要があります。そのため検索エンジニアとしては、人間のユーザーだけではなく、AIエージェントにとっても使いやすい検索とは何かを日々考えています。 こうした背景から、最近 Agentic Search という考え方に注目しています。 Agentic Searchとは、LLMエージェントが検索ツールを使い、検索方針の立案、クエリの変更、結果の読解、再検索を自律的に繰り返す検索アプローチです。エージェントは人間よりも粘り強く、同じタスクに対して何度も検索語や探索対象を変えられます。そのため、単発の検索クエリで完結しない調査タスクとの相性が良いと考えられます。 Agentic Searchの一形態として、LLMエージェントにファイルシステムを ls や grep のようなツールで探索させる方法があります。 たとえば、エージェントがまず ls /docs で全体構造を見て、関係しそうなディレクトリに移動し、grep で候補を探し、cat で根拠文書を読む、という流れです。 この方向性は、最近よく見かけるようになっています。たとえばAmazon Scienceの Keyword search is all you need: Achieving RAG-Level Performance without vector databases using agentic tool use では、ベクトルDBに頼らず、エージェントがキーワード検索ツールを使うことでRAG相当の性能を目指す方向が示されています。 また、AnthropicのClaude Codeも、コードベースを理解するためにエージェントがファイルを読み、検索し、コマンドを実行する体験を前面に出しています。Claude Codeのドキュメント でも、利用可能なツールとして Bash, Grep, LS, Read などが説明されています。これはコード検索の文脈ですが、LLMに「検索API」ではなく「探索できる環境」を渡すという意味で、今回の話と近いものがあります。 これらに共通しているのは、LLMに大きなコンテキストを丸ごと渡すのではなく、ファイル、検索、読み取りといった操作を通じて必要な情報へ到達させるという考え方です。 一方で、エンタープライズ検索にそのまま適用しようとすると難しい点があります。 権限管理が難しい 大規模な文書集合をローカルファイルとして扱うのは現実的ではない Agentが読める結果量には限界があり、候補の順位付けが重要になる 特に権限管理は重要です。エンタープライズ検索では、ドキュメントごとに閲覧権限が異なります。単にローカルディレクトリをマウントしてLLMに読ませるような構成では、プロファイルごとに見える文書を厳密に制御するのが難しくなります。 仮想ファイルシステム 上記の課題を解決するアイデアとして、Agent向けの仮想ファイルシステムという方法があります。 ここでいう仮想ファイルシステムとは、Agentからは ls, cat, grep のようなファイル操作に見える一方で、実体はdatabaseや検索エンジン上のデータである、という仕組みです。Agentには探索しやすいファイルシステム風のインターフェースを渡し、裏側では検索エンジンがindexing、検索、権限制御を担当します。 この方法であれば、ローカルファイルを直接Agentに読ませる必要がありません。path treeを事前に作っておき、ls や cd はそのtreeを見て返し、cat や grep は検索エンジン上の文書を読みに行きます。さらに、ユーザーの権限に応じてpath treeや検索対象を制限すれば、Agentからは権限外のpath自体が見えなくなります。 このアイデアは、Mintlifyの How we built a virtual filesystem for our Assistant と、Leonie Monigatti氏の Implementing a virtual filesystem over Elasticsearch で紹介されています。Mintlifyの記事では既存のdatabase上に仮想ファイルシステムを作る考え方が紹介されており、Leonie Monigatti 氏の記事ではそれをElasticsearch上で実装し、Document Level Securityによる権限制御まで含めた構成が紹介されています。 図は Leonie Monigatti 氏の “Implementing a virtual filesystem over Elasticsearch” より引用 Agentから見るとファイルを読んでいるだけですが、裏側では検索エンジンに対するリクエストが走ります。これにより、検索エンジン側のインデックス、スケーラビリティ、権限制御を利用できます。 今回試したこと この記事では、OpenSearch上に仮想ファイルシステムを作り、通常の grep に加えて、ベクトル検索を使った semantic_search コマンドも同じ探索面で使えるようにしました。 この発想の背景には、階層的な探索インターフェースをAgentに渡す研究もあります。たとえば A-RAG: Scaling Agentic Retrieval-Augmented Generation via Hierarchical Retrieval Interfaces では、複数の検索方法や階層的なインターフェースをエージェントループと組み合わせることで、検索精度を高める方向が示されています。 検索エンジンを仮想ファイルシステムの裏側に置くなら、全文検索だけでなくベクトル検索も同じ場所に置けます。そこで今回は、ファイルシステム風の探索、grep によるキーワード検索、semantic_search による意味検索を、同じOpenSearch indexと同じ権限境界の中に入れることにしました。 実装上は、文書の読み取り、grep、semantic_search のすべてをOpenSearch経由にしました。runtimeでローカルファイルを直接読むのではなく、OpenSearchのDocument Level Securityを通した結果だけを返します。indexing側では文書本文とembeddingをOpenSearchに登録し、query側ではAgentからの操作をOpenSearchへの検索・取得に変換します。 Agentから見えるMCP toolは docs_bash だけです。docs_bash の中で使えるコマンドとして、以下を用意しました。 ls cat grep semantic_search ここで使っているのが just-bash です。just-bashは、AI Agent向けの仮想bash環境を提供するTypeScriptライブラリです。IFileSystem を実装したfilesystemを new Bash({ fs }) に渡すことで、実ファイルシステム以外のbackendも ls, cat などのコマンドで操作できます。また、defineCommand で独自コマンドを追加したり、grep のような既存コマンドの挙動を差し替えたりできます。Leonie Monigatti 氏の記事では、この仕組みでElasticsearchをbackendとして接続しています。 イメージとしては、以下のような形です。Agentには docs_bash という1つのtoolだけを渡し、その内側でOpenSearchをバックエンドにした仮想ファイルシステムと検索コマンドを実行します。 import { Bash, defineCommand } from "just-bash"; const fs = new OpenSearchFs({ client, files, dirs }); const bash = new Bash({ fs, cwd: "/", customCommands: [ defineCommand("grep", (args, ctx) => runOpenSearchGrep(args, ctx, fs), ), defineCommand("semantic_search", (args, ctx) => runSemanticSearch(args, ctx, fs), ), ], }); const server = createSdkMcpServer({ tools: [ tool("docs_bash", "Run read-only bash commands on docs", schema, async ({ command }) => { const { stdout, stderr } = await bash.exec(command); return { content: [{ type: "text", text: stdout + stderr }] }; }), ], }); semantic_search は次のように使えます。 semantic_search "自然文の検索クエリ" [path] path を指定した場合は、そのsubtreeに検索範囲を限定します。指定しない場合は現在のディレクトリ配下を検索します。出力は path:snippet 形式にしておき、Agentがそのまま cat <path> で根拠文書を読めるようにしました。 なお、今回OpenSearchを使っているのは、性能や機能比較の結果として最適だと主張したいからではなく、私自身がOpenSearchを学ぶ目的があったためです。 実験 実験では、2つの観点を確認しました。 1つ目は、小さな日本語の社内ドキュメント風データを使った定性的な確認です。ここでは、Agentが仮想ファイルシステムを自然に探索できるか、grep で見つからない文書に semantic_search で到達できるか、そして権限外の文書が検索結果に出ないかを確認しました。 2つ目は、EnterpriseRAG-Benchを使った定量評価です。ただし、ここでの目的は公式ベンチマークスコアを出すことではありません。同じ20,000文書のサブセッ
機械学習エンジニアの吉田です。バクラクヘルプデスクエージェントの開発を担当しています。この記事では、バクラクヘルプデスクエージェントにおけるナレッジ更新の同時実行制御を Temporal を活用してどのように実現したか紹介します。 背景 バクラクヘルプデスクエージェントとは バクラクヘルプデスクエージェントは、社内の問い合わせ対応を自動化するAIエージェントで、社内規程や業務マニュアルを元にSlackで多様な質問に自動で回答します。AIで解決できない場合は適切な担当者へシームレスに取り次ぎ、問い合わせをきっかけとしたマニュアルの更新も支援します。 bakuraku.jp AIが正確に回答するには、根拠となる社内ドキュメント群 (ナレッジ) の品質が重要です。ナレッジの元になるのは Notion や Google Drive 上の社内規程・業務マニュアルで、これらのソースと同期し、常に最新の状態に保つ必要があります。 ナレッジ更新の同時実行制御が必要になる理由 ナレッジの更新処理には大きく2つの処理があります。ソースから最新のコンテンツを取得して反映する「同期」と、不要なナレッジの「削除」です。なお、ナレッジは管理画面や Slack から直接作成することも可能ですが、これらはソースとの同期を伴わないため、今回の同時実行制御の対象外とします。 これらの更新処理は、管理画面・Slack・日次バッチなど複数の経路から実行されます。異なる経路からの同時更新はもちろん、同一経路であっても複数のユーザーが同時に更新するケースがあり得るため、あらゆる経路・タイミングで同時実行制御が必要になります。 また、ナレッジはフォルダ (親) ・ページ (子) という階層構造を持っています。フォルダ配下の全ページを一括更新するケースと、特定のページだけを個別に更新するケースがあり、フォルダ全体の更新中に同じフォルダ内の個別ページが更新されると、同じページに対して2つの処理が同時に走ることになります。 加えて、更新処理は外部APIからのコンテンツ取得、Markdown変換、ベクトル化、LLMによる要約生成といった複数ステップからなり、完了までに時間がかかります。その間にユーザーがナレッジを削除すると、削除と同期の順序次第では、削除したはずのデータが復活したり中途半端な状態のデータが残るといった不整合が生じる恐れがあります。 開発初期はナレッジのソースも更新経路も少なかったため、DBの排他ロックで同時実行を防いでいました。しかし、ソースが増えていくにつれてエントリポイントも増加し、すべての箇所でロックの取得・解放を正しく実装する負荷が大きくなり、実装漏れや不整合、デッドロックのリスクが高まりました。 こうした課題を踏まえ、既に採用していた Temporal の機能を活かして、同時実行制御の仕組みを見直すことにしました。 Temporalとは Temporal は、耐久性のあるワークフロー実行を提供するオープンソースのワークフローエンジンです。一度開始されたワークフローは、プロセスのクラッシュやネットワーク障害が発生しても、中断した箇所から自動的に再開されます。リトライ・タイムアウト・状態管理といった分散システム特有の課題をプラットフォーム側が吸収するため、アプリケーション層では業務ロジックに集中できます。 以下では、検討した2つの設計アプローチを順に紹介します。 設計案①:Entity Workflow と Signal キューによるシリアライズ DBロック方式の課題は、同時実行制御の責任が各エントリポイントに分散していることでした。そこで、同一ナレッジへの操作をひとつのワークフローに集約する Entity Workflow パターンを検討しました。Entity Workflow は、エンティティごとに長寿命のワークフローを常駐させ、そのライフサイクルを一元管理するパターンです。 Signal によるキューイング 今回のケースでは、すべての操作を Signal 経由でキューに追加し、順次処理することでシリアライズを実現する設計を考えました。 呼び出し側は signalWithStart を呼ぶだけです。ワークフローが未起動なら起動してから Signal を送り、起動済みなら Signal だけを送信する処理がアトミックに行われるため、DBロックが不要になります。 以下は TypeScript による簡略化した実装例です。 export async function entityWorkflow() { const queue: Operation[] = []; // シグナルハンドラ: キューに追加するだけ setHandler(syncSignal, () => { // 同期リクエスト queue.push({ type: "sync" }); }); setHandler(deleteSignal, () => { // 削除リクエスト queue.push({ type: "delete" }); }); // メインループ: Signal が来るまで待機し、順次処理 while (true) { await condition(() => queue.length > 0); const op = queue.shift()!; switch (op.type) { case "sync": await syncKnowledge(); // 同期処理 break; case "delete": await deleteKnowledge(); // 削除処理 return; // 削除後はワークフローを終了させる } } } 長寿命ワークフローの運用課題 しかし、この設計にはいくつかの懸念がありました。Entity Workflow は一度起動すると長期間にわたって稼働し続けます。ワークフロー内部の処理やインターフェースを変更する場合、すでに稼働中のワークフローとの互換性を維持する必要があり、新旧のコードを共存させるバージョニングが求められます。 また、長寿命のステートフルワークフローには運用上の課題もあります。Temporal はワークフローの全実行履歴を Event History として記録しますが、Event History にはサイズ上限があるため、上限に達する前に continueAsNew でワークフローを新しい実行として再起動し、履歴をリセットしなければなりません。しかし再起動の際にはキューなどの状態を引き継ぐ必要があり、実装が複雑化します。 また、削除時にはキューに溜まった未処理の同期リクエストをすべて消化してからでないと削除処理に到達しないため、ユーザーが削除を要求しても、先にキューイングされた同期処理がすべて完了するまで待たされることになります。 設計案②:Workflow ID の命名規則による同時実行制御 「何を防ぐべきか」を整理する Entity Workflow の課題を踏まえ、そもそもすべての操作を厳密にシリアライズする必要があるのか考え直しました。 同期 × 同期:重複しても問題ない 同期はソースの最新状態をそのまま反映する冪等な処理です。同じページの同期が2つ同時に走っても、最終的なデータは同じです。既に同期中であれば、後続のリクエストをスキップすれば十分です。 厳密には、同期の実行中にソース側が更新されると古い内容で上書きされる可能性があります。しかし、社内規程や業務マニュアルが短時間に何度も更新されることは稀ですし、仮に起きても日次バッチで最新状態に収束します。 同期 × 削除:データ復活はDB制約で防げるが、クリーンアップが必要 削除済みのナレッジに同期ワークフローがデータを書き戻そうとしても、DB制約で書き込みは失敗します。つまり、削除したナレッジが復活することはありません。 しかし、2つの問題が残ります。 DB制約違反のエラーが発生すること自体が不健全 ベクトルインデックスなどDB外には孤立したデータが作られてしまう したがって、削除時には進行中の同期をキャンセルする仕組みが必要です。 整理すると、必要な制御は以下の2つに絞られます。すべての操作をキューでシリアライズする必要はありません。 同期・削除の二重実行を防ぐ — Workflow ID の一意性制約 を活用し、WorkflowIdConflictPolicy.USE_EXISTING を指定することで、同じIDのワークフローが実行中なら新たに起動せず既存のワークフローを使う 削除時に進行中の同期を止める — Visibility API で実行中の関連ワークフローを検索し一括キャンセルすることで、孤立データやエラーを防ぐ Workflow IDに階層パス構造を持たせる これらを機能させるための鍵が、Workflow ID の命名規則の設計です。ナレッジはフォルダ (親) とページ (子) の階層構造を持っています。この親子関係を、ファイルシステムのパスのように Workflow ID に反映します。 操作 Workflow ID フォルダ同期 folder/{folderId}/sync/{uniqueId} ページ同期 folder/{folderId}/page/{pageId}/sync ページ削除 folder/{folderId}/page/{pageId}/delete フォルダ削除 folder/{folderId}/delete ポイントは、すべてのIDが共通のプレフィックスを持つことです。folder/{folderId}/ でプレフィックス検索すれば、そのフォルダに関連するすべてのワークフローを一括取得できます。folder/{folderId}/page/{pageId}/ まで指定すれば特定ページに絞り込めます。 同期:重複排除と並列実行を Workflow ID の命名で両立する フォルダ同期は配下の全ページを走査し、ページごとに同期ワークフローを起動します。ここでポイントになるのが、フォルダ同期とページ同期で Workflow ID の付け方を意図的に変えている点です。フォルダ同期は「毎回ユニークなID」で即座に開始できるようにし、ページ同期は「固定ID」で重複を自動排除する、という役割分担をしています。 ページ同期(固定ID → 重複排除) ページ同期の Workflow ID は folder/{folderId}/page/{pageId}/sync で、pageId から一意に決まります。同じページへの同期リクエストが複数来ても Workflow ID が同一なので、Temporal が2つ目以降の起動を自動的にスキップします。つまり、1ページにつき同時に走る同期は常に1つだけです。 フォルダ同期(毎回ユニークID → 並列実行) 一方、フォルダ同期の Workflow ID は folder/{folderId}/sync/{uniqueId} で、リクエストごとに異なるIDが振られます。こうすることで、前回のフォルダ同期が完了していなくても新しい同期を開始できます。ソース側の更新を取りこぼさないために、先行する同期の完了を待たない設計にしています。 フォルダ同期が複数走っても安全な理由 フォルダ同期が並列で走ると、内部では同じページに対する同期リクエストが重複して発行されます。しかし、ページ同期の Workflow ID は固定なので、Temporal が重複を排除してくれます。さらに、各ページの同期処理はソース側に変更がなければ更新をスキップする実装にしているため、無駄な処理も発生しません。 削除:プレフィックス検索で関連ワークフローを一括キャンセルする プレフィックスでキャンセル対象を特定する 削除ワークフローは、自身の Workflow ID から末尾の /delete を除いた部分をプレフィックスとして使います。Temporal の Visibility Query で STARTS_WITH 検索をかけると、そのプレフィックスに一致する実行中のワークフローを一覧取得できます。 Workflow ID がパス構造になっているため、ページ削除ならページまで、フォルダ削除ならフォルダまでをプレフィックスにすることで、キャンセル範囲が削除の粒度に応じて自然に決まります。 以下は TypeScript による簡略化した実装例です。 export async function deleteWorkflow() {</s
こんにちは。バクラク給与の開発を担当しているakahaneです。 新規プロダクトとしてバクラク給与を立ち上げ、2026年3月にリリースしました。その開発の進め方について振り返ります。 はじめに バクラク給与の立ち上げでは、Claude CodeやCodexなどのコーディングエージェントを積極的に活用しました。 ただし、これは「AIにコードを書かせたら速かった」という話ではありません。 給与計算は、間違いが許されない領域です。計算結果が1円ズレれば、それは従業員の生活に直結する。「それっぽく動くコード」が最も危険な領域とも言えます。 一方で、新規プロダクトの立ち上げは変化の連続です。仕様も設計もプロダクトの形も日々変わる。このフェーズでは、試作・修正・検証のサイクルをどれだけ速く回せるかが勝負になります。 「間違えてはいけない」と「速く試行錯誤したい」。この一見矛盾する要求の中で、コーディングエージェント時代の新規プロダクト立ち上げが実際どのように進んだのかを紹介します。 フィードバックループを「翌スプリント」から「翌日」に変えた 新規プロダクトの立ち上げでは、最初から正解の仕様・設計は存在しません。 バクラク給与でも、初期は「まず動くものを作る」「実際に触れる形にする」「チームやドメインエキスパートと議論できる材料を作る」ことが最優先でした。仮で作った画面を翌日に作り直す。一度決めたデータ構造を、ドメイン理解が進んだことでゼロから見直す。そうしたことが日常的に起きていました。 ここでコーディングエージェントが大きく効きました。 たとえば、ある日の仕様検討会で「この画面の構成を変えるべき」というフィードバックがありました。一見すると画面上の表示変更に見えますが、実際にはAPIのレスポンス構造やフロントの表示ロジックにも影響するほどの変更です。以前なら次のスプリントに回していたような規模感ですが、コーディングエージェントを使うことで、その日のうちに方針を整理し、翌日にはチームにデモできる状態にできました。 こうしたサイクルは、この一度きりではありませんでした。 立ち上げ期には、毎日のように仕様検討会やレビュー会を実施していました。顧客ヒアリングで得たフィードバックも、翌日の別の顧客のヒアリングで意見を伺う。そこでまた反応を見て改善する。そういう短いループを日々回していました。 実装速度が上がることで、チームや顧客との対話の頻度も上がる。仮説を早く形にし、早くぶつけ、早く直す。結果として、プロダクトの解像度を上げるスピードが変わりました。 ただし、速く作れることは、何でも作ってよいという意味ではありません。作るコストが下がったからこそ、「本当に作るべきか」「使われ続けるものか」「将来の開発速度を落とさないか」を、以前よりも強く意識する必要がありました。新規プロダクトにおいては、作らない判断やシンプルに保つ判断も、同じくらい重要です。 note.layerx.co.jp 実務の温度感は、コーディングエージェントには判断できない 給与計算領域では、AIにいきなり実装を依頼してもうまくいきませんでした。 コーディングエージェントに聞けば、給与ドメインに関する一般的な情報はある程度得られます。所得税の計算方法、社会保険料の算出ロジック、基本的な用語の説明。しかし、それだけでは足りません。 実際の日々の業務で、その作業がどのくらい大変なのか。労務担当者がどの部分を特に慎重に確認しているのか。間違えたときにどのくらい影響が大きいのか。なぜ一見面倒に見える確認ステップが必要なのか。こうした現場の温度感は、コーディングエージェントには持ちようがありません。 たとえば、社会保険料の控除ひとつとっても、法令上のルールだけを見れば「翌月徴収」で「端数処理は五捨五超入(50銭以下切り捨て、50銭超切り上げ)」とシンプルに見えます。しかし実務では、慣習的に当月徴収を採用している企業や、法令とは異なる端数処理を用いている企業が少なくありません。こうした現場ごとの運用実態は、法令の条文やインターネット上の情報だけでは把握できず、コーディングエージェントに聞いても出てきません。 また、給与計算そのものだけでなく、計算後のチェックをどう行うかも重要でした。労務担当者は計算結果をただ眺めるのではなく、前月との差分を見て異常値を探す、特定の項目を重点的に確認する、といった独自のチェックフローを持っています。どの画面で何を見れば安心して確定ボタンを押せるのか。こうした確認プロセスは法令に書かれているものではなく、ドメインエキスパートとの議論を通じて初めて見えてきた部分でした。 そのため、AIに実装を任せる前に、労務ドメインエキスパートと一緒に仕様を言語化するプロセスが不可欠でした。そして言語化した仕様をもとに議論を重ね、重要度の濃淡を設計判断に落とし込むこと。それがプロダクトの土台になりました。 note.layerx.co.jp 給与計算ロジックの中心は、あえて人間が書いた 給与計算ロジックの中心部分は、コーディングエージェントの利用をあえて控え、人間が中心となって実装しました。 理由は明確で、この領域ではAIが「それっぽいコード」を書けてしまうこと自体がリスクだからです。 給与計算では、単に数式を書けばよいわけではありません。支給、控除、勤怠、締め日、支給日、月途中入社・退職、休職、端数処理。さまざまな条件が絡む。さらに、「計算結果が合っている」だけでなく、「なぜその金額になったのか」を説明できることも求められます。 コーディングエージェントに正しく実装させようとすると、業務上の前提、計算の意図、例外条件、端数処理、保存すべき計算根拠、将来の拡張可能性を、すべて正確に言語化して渡す必要がある。そして、出てきた実装が正しいかをレビューするには、結局人間がロジックを同じレベルで理解していなければなりません。 であれば、そのコンテキストを作る時間とレビューの時間を考えると、人間が自分で書いたほうが早く、確実でした。 AIが生成するコードの怖さは、エラーになる、動作しないことではなく、それっぽく動作してしまうことにあります。画面上も問題なく見える。しかし、特定の条件で1円ズレている。給与計算では、こうした「動くが正しくない」コードが最も発見しにくく、最もダメージが大きいリスクになります。 テストケースの洗い出しでは、AIが強力なレビュアーになった 一方で、テストケースの作成にはAIを大いに活用しました。 計算ロジックでは、正常系だけでなく、境界値や例外パターンをどれだけ洗い出せるかが品質を左右します。人間だけで考えていると、自分が実装した前提に引っ張られて、どうしても見落としが出る。 そこで、実装したロジックや仕様メモをAIに渡し、「この仕様に対して、漏れやすい境界値や異常系を洗い出して」と依頼しました。「テストコードを書いて」ではなく、テスト観点の網羅性を問う使い方です。 たとえば、月途中入社・退職のテストケースでは、人間が考えた「月初入社」「月末退職」「月途中入社」に対して、AIは「入社日と退職日が同月」「締め日をまたぐ入社」「支給日前日の退職」「休日に重なる場合」といった観点を追加で出してきました。すべてが有効なケースではありませんでしたが、ドメイン上検討すべき観点として有用なものが多くありました。 AIにテスト観点を出させ、人間がドメイン上正しいかを確認し、必要なケースをテストコードに落とし込む。この進め方により、給与計算ロジック周辺でカバレッジ90%以上を達成できました。 AIは実装者としてだけでなく、「自分たちが見落としている観点はないか」を問うレビュアーとして有効でした。 ボトルネックは「コードを書く」から「何を作るか決める」に移った コーディングエージェントを使うことで、実装速度は確実に上がりました。しかし、実装が速くなると、ボトルネックは別の場所に移ります。 以前は「考えたことを形にする」のに時間がかかっていました。今は、形にすること自体は速い。すると、「何を形にすべきか」を決める部分が律速になる。 何を作るべきか。何を作らないか。仕様の曖昧さをどう解消するか。ドメイン上の正しさをどう判断するか。AIが出したコードをどう評価するか。 コードを書く時間は確かに減りました。しかし、それは「よいコンテキストを作る」「AIに適切に依頼する」「出てきたものを評価する」「ドメイン上正しいかを判断する」という時間に置き換わっています。 言い換えると、コーディングエージェントはエンジニアの仕事をなくすのではなく、エンジニアの仕事の重心を変える。手を動かす比重が減り、判断する比重が増える。新規プロダクトの立ち上げでは、この変化の意味は大きいと感じました。 特に重要なのは、実装の前段にある「何を、なぜ、どう作るか」を整理する力です。ドメインエキスパートとの議論を設計に落とす力、仕様の曖昧さを言語化する力、AIが正しく動けるだけの精度で要件を整理する力。コーディングエージェントが強力になるほど、この上流の質がプロダクトの質を決めるようになります。 まとめ 「間違えてはいけない」と「速く試行錯誤したい」。この二つの要求に対して、バクラク給与の立ち上げでたどり着いた答えはシンプルでした。速く回すべきところはコーディングエージェントで速く回し、間違えてはいけないところは人間が責任を持つ。 結果として、バクラク給与は当初のリリース予定より2ヶ月前倒しでリリースすることができました。これはコーディングエージェントの力だけで実現したものではありません。ドメインエキスパートとの仕様言語化、顧客との高速なフィードバックループ、人間が主導した計算ロジックの実装。それらすべてが噛み合った結果です。 実装が速くなった分、エンジニアの仕事の重心は「コードを書く」から「何を作るかを決め、正しさを判断する」に移っています。バクラク給与の新規プロダクト立ち上げではこの変化がより鮮明に見えました。 LayerXの「すべての経済活動を、デジタル化する。」というミッションに共感いただける方、技術の力で社会課題解決に貢献したい方は、ぜひ以下からご応募ください! jobs.layerx.co.jp
こんにちは。バクラク事業部で機械学習エンジニアをしている伊藤(@sbrf248)です。最近はオンライン上で日々流れてくる情報が膨大なので、頭の整理のため紙とペンをよく使うようになりました。GWには(手の届く範囲で)少し高価なボールペンを買ってみました。 さて、近頃はAI・LLMを組み込んだプロダクトやシステムが当たり前になってきています。 私の携わる「バクラク」でも様々なAIエージェントをプロダクトに組み込み、あらゆるバックオフィス業務の自動化を進めています。 bakuraku.jp AI・LLMを組み込む場合、最初に作ったものがそのまま完成ということはあまりなく、継続的な性能の改善が求められるケースが多くあります。特に、ユーザーごとに体験を最適化するパーソナライズされたシステムの場合は、ユーザーのフィードバックをいかにして収集し活用するかが重要です。 日々重要性が高まってきている「ユーザーのフィードバックをいかにして収集し活用するか」について、先日開催されていたHuman-Computer Interactionの学会であるCHI(CHI2026, CHI2025)の論文を中心に事例を調査してみました。このブログでは、調査した研究事例やポイントを紹介します。 全体像 LLMに限らず、機械学習モデル等を組み込むAIシステムは多々ありますが、ここではLLMを中心に考えます。LLMを組み込んだシステムは、大まかには次のような構成になると思います。 流れとしては、何らかの出力に対してユーザーからのフィードバックがシステムに与えられると、それを使ってシステムの一部が更新され、結果的に出力に反映されます。 更新する対象は、大まかに3種類に分けられます。 モデル自体 プロンプト その他(入力データや前処理、LLMの出力のフィルタ処理など) LLM本体を自前でservingするのは運用コストが大きいため、OpenAIなどのプロバイダが提供するAPIを利用するのが一般的だと思います。また、フィードバックを反映するコストを踏まえると、多くのケースで更新対象はプロンプトやその他の部分になるでしょう。 ここからは、「ユーザーから何らかの形式で与えられたフィードバックをプロンプトやその他の部分に反映し、システムの出力を改善する」問題についての研究事例を見ていきます。 研究事例 Feedback by Design: Understanding and Overcoming User Feedback Barriers in Conversational Agents [1] この論文では、会話エージェントにおけるユーザーのフィードバックの品質について調査しています。 会話エージェントはチャットを使ってAIとやりとりするプロダクトで、代表的なものにChatGPTやClaude Codeなどが挙げられます。ここで、ユーザーのフィードバックは「自然言語」として与えられます。つまり、「〜〜みたいに修正して」のような文章を入力してシステムに送信することで、それがプロンプトなどの形式でLLMに渡され、出力が更新されます。 この論文の著者は、会話エージェントのフィードバックは低頻度・低品質になりやすいと指摘しています。理由は大きく4つ挙げられています。 AIの出力が文脈・目的からずれていく: 会話エージェントが元々あったゴールを維持できず、制約を無視したり別の方向に進んだりした結果、ユーザーが「追加で説明しても直らない」と判断してしまう。 出力を検証するコストが高い: 出力にハルシネーションや存在しない引用があると、ユーザーはまず文章自体の正しさを確認しなければならず、負荷が重くなる。 モデルが曖昧さを確認しないため、ユーザーだけが説明責任を負う: ユーザーの指摘が曖昧でも、モデルが確認質問をせず修正を入れてしまうことがある。そのためユーザーは「何を・どの粒度で・どう直すべきか」を自分で言語化し続ける必要がある。 どの程度の情報を与えればよいかわからない: モデルが重要な点を落としたり、逆に不要な変更を加えたりする。それら1つ1つに対するフィードバックが難しく、「try again」のような最小限の指示になってしまう。 要するに、どこをどう直せばいいのかユーザーにとってわかりづらい状況で、かつ自由形式の入力欄だけが与えられていると、今の出力をより良くするためにどうフィードバックすればいいかをユーザーが考えて言語化する必要があります。これは負荷が大きく、結果として短く曖昧な修正やセッションのリセットなどに退避してしまうため、品質の低下やそもそもフィードバック自体を避けてしまう結果につながってしまいます。 これを踏まえて、論文では出力の一部をハイライトして「この部分が違う」と指摘できるようにする、修正前後の差分を見せる、AIがどのようにフィードバックを解釈したかを明示する、Undo/Redoを用意する、といったインターフェース設計を考案・検証し、体験の改善を確認しています。 この研究から、ユーザーの操作にある程度の型を用意し、その中で試行錯誤しやすい状況を作り出すことで、高品質なフィードバックが期待できるようになると示唆されます。 End User Authoring of Personalized Content Classification [2] この論文では、ユーザーが自身の好みに合わせてYouTubeコメントの分類器を作るというタスクについて、3つのフィードバック方式を実験的に比較しています。 Label System: サンプルとなるコメントが与えられるので、それを分類器の学習に使うかどうかを選択する。分類器は簡単なMLベースのモデルを利用する。 [2] より引用 Rule System: 用意されたインターフェースを使い、コンテンツを分類するルールを作成する。 [2] より引用 Prompt System: LLM分類器のプロンプトを調整(文章の修正やfew-shot exampleの変更)する。 [2] より引用 結果としては、Prompt Systemが最も早く高精度の分類器を作成できました。一方で、参加者はLabel SystemやRule Systemの方が、やることが明確で使いやすいと感じたようです。またRule Systemについては、特定の語句やイベントに基づくフィルタリングという観点では高い精度を出していたようです。 この結果を踏まえて著者は、それぞれのメリットを活かすハイブリッドな手法が適切ではないかと述べています。例えば、少数のLabel Exampleからプロンプトを生成したり、Rule SystemとPrompt Systemを併用した手法などが挙げられています。 この研究からも、プロンプトをユーザーが直接編集するよりは、操作にある程度の制約を設けつつ、ユーザーが迷わない形でフィードバックを送れる設計が良い選択肢となり得ることがわかります。 Promptimizer: User-Led Prompt Optimization for Personal Content Classification [3] この論文は、ユーザーに様々な形態のフィードバック方式を提供しつつ、コンテンツ分類器のプロンプトを改善する手法を提案しています。 [3] より引用 具体的には、ユーザーは初期説明の記述、誤分類された例へのラベル付け、失敗パターンの優先付けなど、複数の粒度のフィードバックを段階的に与えることができます。重要なのは、最終的に出来上がるプロンプトがユーザーにとって読める・解釈可能な形で残ることです。完全自動の最適化だと、内部でプロンプトがどんどんブラックボックス化していき、ユーザーがよくわからないままプロンプトが更新されていく状況に陥ってしまう場合があります。 論文中の実験では、ユーザーは完全自動のプロンプト最適化よりもPromptimizerの方式(人間が途中に介入できる形)を有意に好むという結果が示されています。また、YouTubeのコメント管理ツールへの組み込み実験では、3週間の実利用を通じて、ユーザーがそれぞれの目的に合った多様なフィルタを作り、運用できることが確認されています。 この研究からは、自動最適化を導入する場合でも、ユーザーが過程に関与できる余地と、結果を解釈できる出力を残すことが重要だとわかります。 Preference-Guided Prompt Optimization for Text-to-Image Generation [4] この論文も同様にプロンプト最適化の手法ですが、ユーザーは2つの選択肢からより良いものを選ぶだけというかなり限定的なフィードバック方式です。 [4] より引用 対象タスクはtext-to-imageの画像生成で、ユーザーが画像生成のプロンプトを直接書いて調整するのは負担が大きいという課題に取り組んでいます。画像生成は出力が予測しにくく、頭の中のイメージを文章に落とし込むこと自体が難しい一方で、「2つ並べたらどちらが好みか」は比較的簡単に判断できます。この性質を利用して、ユーザーには2択選好だけを返してもらい、システム側がプロンプトを自動的に更新していきます。 提案手法の特徴は、ユーザーのフィードバックを使って探索方向を狭めていく(exploit)一方で、適度に新しい方向も試す(explore)バランスを取っている点です。選好フィードバックだけでは、初期の好みに引きずられて局所最適に陥りやすいので、このバランスは実用上重要です。実験では、手動でプロンプトを書き換える場合に比べて少ない反復回数・低い認知負荷で満足のいく結果に到達できることが示されています。 ここから言えるのは、「ユーザーに言語化させずに済むフィードバック方式」も有力な選択肢になるということです。タスクによっては、自由記述や明示的なルール記述よりも、選好比較の方がユーザーの認知負荷が小さく、フィードバックを継続的に集めやすくなります。 Data-Prompt Co-Evolution: Growing Test Sets to Refine LLM Behavior [5] この論文は、フィードバックループの中でプロンプトの更新とそれを検証するテストデータの更新を同時に行う手法を提案しています。 [5] より引用 ここで扱っているのは、「プロンプトを直してもそれが過去にうまく動いていたケースを壊していないかわからない」という問題です。 従来の機械学習開発でもありましたが、テストデータを十分に整備せずプロンプトエンジニアリングを進めると、今注目しているサンプルの精度は改善したものの、実は過去問題なかったサンプルの精度が悪化する、といった問題に直面する可能性があります。 論文は、この問題に対して、テストデータとプロンプト指示を一緒に育てていくワークフローを提案しています。具体的には、ユーザーが運用中に見つけたエッジケースをテストセットに追加し、なぜその挙動が望ましいのか・望ましくないのかという根拠を明文化し、修正したプロンプトを成長したテストセットに対して評価する、というループを回します。 この研究の重要な視点は、ユーザーからのフィードバックを「単発の修正要求」で終わらせず、「テストケース+期待挙動+根拠」というセットで保存していくところにあります。こうしておくと、後からプロンプトを変更した際にも回帰的に検証でき、改善の積み上げが効きやすくなります。 まとめ ここまで、人間からAIへのフィードバック方式を、研究事例をふまえて紹介しました。個人的には、適切な制約を加えることで、自由記述形式よりもユーザーが使いやすく、かつ高品質なフィードバックが実現できる事例が見られたのが印象的でした。 もちろん前提として、考えているタスクの性質やシステムへの組み込み方によって、最適なフィードバック体験は異なります。その上で、(当たり前のことではありますが、)ユーザーが迷わずにフィードバックでき、それが出力の改善に納得できる形で結びつくような一連の体験を目指すべき、ということは一貫していると感じます。 LLMの表現力が高まっているからこそ、プロダクトを作って提供するだけでなく、使っていただくユーザーと共にプロダクトを磨き込めるような体験を目指していきたいと思います。 最後になりましたが、LayerXではAI・機械学習に携わるエンジニアを募集中です。ユーザー価値を第一に考えたAIエージェントの社会実装に興味のある方は、ぜひご応募ください! open.talentio.com <a href="https://jobs.layerx.co.jp/opend
こんにちは。バクラク事業部 BizOps部 データグループの@civitaspoです。 先日、光栄にも Snowflake Data Superhero に選出いただきました。これからも Snowflake のマニアックな話を届けていきますので応援よろしくお願いします! www.snowflake.com はじめに さて、今回の記事は Snowflake-managed Iceberg table のデータ取り込み方式に関する内容です。Snowflake-managed Iceberg table とは、CATALOG = 'SNOWFLAKE' を指定して作成する、Snowflake を catalog とする Iceberg table を指します。 docs.snowflake.com Snowflake-managed Iceberg table には既存の Parquet ファイルを取り込む方法として、COPY INTO ... LOAD_MODE = ADD_FILES_COPY があります。このロード方式は、Snowflake が Parquet ファイルを読み込んで再書き込みする COPY INTO ... LOAD_MODE = FULL_INGEST とは異なり、Iceberg-compatible な Parquet ファイルを Snowflake-managed Iceberg table の base location に server-side copy し、Snowflake-managed Iceberg table へ登録します。 docs.snowflake.com ADD_FILES_COPY は Parquet ファイルの scan / rewrite を避けられるため、データ取り込みにかかるコストを大きく下げられる可能性があります。一方で、ADD_FILES_COPY は制約が厳しいロード方式であるため、通常の COPY と同じ感覚で使えるものではありません。file format、case-sensitiveなカラム名マッチング、partitioning、clustering、ネスト型のデータ、Schema Evolution などについて、公式ドキュメントや実測結果から、制約を理解して使用する必要があります。 本記事では、2026月5月1日時点の Snowflake 公式ドキュメントと、 筆者の検証環境における実測結果をもとに、Snowflake-managed Iceberg table に対する COPY INTO ... LOAD_MODE = ADD_FILES_COPY の仕様を整理します。 はじめに おことわり tl;dr ADD_FILES_COPY は何をするのか ドキュメントから読み取れる仕様 実測で確認した ADD_FILES_COPY の挙動 PARTITION BY を指定したテーブルでは ADD_FILES_COPY を使用できない CLUSTER BY を指定してもファイル配置は変わらない ネスト型は VARIANT 型のカラムへ格納できない Schema Evolution は COPY 前に行う 同じファイルを再 COPY した場合 skip される 実装前に確認すること おわりに We are hiring 🔥 おことわり 本記事は、Snowflake-managed Iceberg table へ Parquet ファイルを取り込む方式を設計・検証している方向けに書いています。Snowflake、Apache Iceberg、Parquet、COPY構文に関する基本的な用語は前提知識として扱います。 また、本記事で扱う内容は、主に Snowflake-managed Iceberg table の COPY INTO ... LOAD_MODE = ADD_FILES_COPY に関する以下の5点です。 ADD_FILES_COPY の公式ドキュメント上の必須条件と非対応条件 PARTITION BY / CLUSTER BY を指定した Snowflake-managed Iceberg table に対する ADD_FILES_COPY の挙動 CLUSTER BY とロード対象ファイルの配置が scan bytes に与える影響 ネスト型のデータを構造化データ型 OBJECT / ARRAY / MAP として扱う場合のスキーマ互換性 ADD_FILES_COPY を使う設計で必要になる COPY 前のスキーマ互換性チェック / Schema Evolution の順序 一方、この記事では COPY 実行時に消費する Snowflake credits の定量比較や、Google BigQuery の export、AWS Glue jobs などで生成した Parquet ファイルが Snowflake-managed Iceberg table の型要件を満たすかどうかの網羅的な検証は扱いません。 なお、実測結果は2026年5月1日に筆者の検証環境で観測したものです。Snowflake のドキュメントに明記されていない挙動は、今後変わる可能性があります。 tl;dr Snowflake-managed Iceberg table は、Parquet ファイルを取り込む方式として COPY INTO ... LOAD_MODE = ADD_FILES_COPY をサポートしています。ADD_FILES_COPY は、Iceberg-compatible な Parquet ファイルを Snowflake-managed Iceberg table の base location に server-side copy し、data file として登録するロード方式です。Snowflake 側で Parquet ファイルを scan / rewrite しないため、取り込みコストを抑えられる可能性があります。 一方で、ADD_FILES_COPY はロード時の変換、フィルタリング、データ型変換、再クラスタリングを行いません。Parquet ファイルとコピー先のテーブル定義は、COPY 実行前に互換である必要があります。 重要な点は次のとおりです。 ロード対象の Parquet ファイルは、COPY INTO <table> FROM @stage/path の形式で名前付き stage 経由で参照する必要がある。internal stage と external stage が利用できる。 external stage を使う場合、その cloud provider は Snowflake アカウントの cloud provider に揃える必要はない。AWS 上の Snowflake アカウントからでも、Google Cloud Storage や Azure Blob Storage を backend に持つ external stage からロードできる。ただし、cloud provider をまたぐデータ転送の latency とコストは別途評価する必要がある。 コピー先は Snowflake-managed Iceberg table だが、PARTITION BY を指定したテーブルでは ADD_FILES_COPY を使用できない。 Parquet ファイルの top-level column name とコピー先のカラム名は、大文字・小文字を区別して一致させる必要がある。コピー先のカラム型は、Parquet ファイルの型と互換である必要がある。 CLUSTER BY を指定したテーブルへの COPY は成功する。ただし、ロード時に Parquet ファイルが clustering key に沿って書き換えられたり、配置し直されたりするわけではない。scan bytes は、ロード対象ファイルの配置と統計情報に依存する。 ネスト型のデータは、Parquet のネスト型と互換性のある構造化データ型 OBJECT / ARRAY / MAP として扱う。通常の Parquet struct / list / map を Iceberg v3 VARIANT に変換することはできない。 Snowflake-managed Iceberg table の Schema Evolution は、ADD_FILES_COPY によって自動実行されるものではない。ロード対象の Parquet ファイルに合わせてテーブル定義を進化させる必要がある場合は、COPY 前に ALTER ICEBERG TABLE を実行する。コピー先のテーブル定義にない field を含む Parquet ファイルを先にロードすると、その field はロード済みの data file から復元できない。 同じ staged file の再 COPY は、FORCE = TRUE を指定しない限り skip される。再ロードする場合は、FORCE = TRUE、ファイル内容の変更、または TRUNCATE TABLE を使う。 warehouse のサイズを上げても COPY 時間が大きく短縮するとは限らない。Snowflake のドキュメントでは、ADD_FILES_COPY の処理の大部分は warehouse ではなく、Snowflake が管理する Cloud Services 側の計算リソースに依存すると説明されている。 ADD_FILES_COPY は何をするのか ADD_FILES_COPY は Snowflake-managed Iceberg table に Parquet ファイルをロードする方式の1つです。ここでいう Parquet ファイルは、Apache Iceberg がサポートするデータ型のみで構成された Parquet ファイルのことです。 docs.snowflake.com Snowflake が解釈できる Iceberg のデータ型は、Snowflake のドキュメントに一覧されています。 docs.snowflake.com ADD_FILES_COPY は、ロード対象の Parquet ファイルを Snowflake-managed Iceberg table の base location に server-side copy したうえで、Snowflake-managed Iceberg table に登録します。ロード対象の Parquet ファイルをそのまま参照登録する方式ではないため、metadata-only load や zero-copy load ではありません。一方で、 FULL_INGEST のように Snowflake が Parquet ファイルをスキャンして完全に新しい data file として再構成する方式でもありません。ADD_FILES_COPY は Parquet ファイルをコピーして data file として登録するのみで、scan / rewriteは行いません。 ADD_FILES_COPYを実現する、最小構成のクエリは次のとおりです。 COPY INTO <iceberg_table> FROM @<stage>/<path>/ FILE_FORMAT = (TYPE = PARQUET USE_VECTORIZED_SCANNER = TRUE) LOAD_MODE = ADD_FILES_COPY MATCH_BY_COLUMN_NAME = CASE_SENSITIVE; Snowflake-managed Iceberg table 側では、Parquet ファイルのフッターに記録された top-level のカラム名と大文字・小文字を区別して一致させる必要があります。小文字を含むカラムが定義されたParquetファイルをロードする場合、コピー先のテーブル側で quoted identifier を使ってカラム定義します。 CREATE ICEBERG <sp
LayerX QAエンジニアの小山です。 昨今、AIコーディングアシスタント(特にClaude Code等)の進化により、コードの実装やテスト追加のスピードが飛躍的に向上しています。しかし、AIにコードを書かせる際に「どこまで厳密なエラーハンドリングが必要か」「テストはどの程度書くべきか」といったことに迷われた経験はないでしょうか? 今回は、バクラク事業部の品質の定義やテスト戦略などを言語化し、Claude Codeが動く際にリスクの高い箇所を守るように動いてもらい、テストも同時に生成してもらう、早期テストで時間とコストを節約する試みについてご紹介します。 ソフトウェアテストの原則「早期テストで時間とコストを節約する」 筆者はJSTQB FLの公認コースのトレーナーを15年ほどしているのですが、JSTQB FLシラバスの中に「テストの原則」として7つの原則があります。その中の1つとして「早期テストで時間とコストを節約する」というものがあります。 この原則はプロセスの早い段階で欠陥を取り除くと、後半に発生する故障が少なくなり、結果的に品質コストが削減される。そのため早い段階で欠陥を見つけるために静的テストと動的テストの両方をなるべく早い時期に開始すべきであるというものです。 これはテストの実行だけを意味するものではなく、要件のレビューや質問なども含めた活動を意味します。つまりは最もコスパが良いのは欠陥の予防であり、できるだけ早くフィードバックをするアジャイル開発とも親和性が高い原則です。 この原則をClaude Codeが動く時に適用したいと考えました。 skillsに盛り込む判断基準 どうすれば予防ができるのか?を考えると、AIが予防をするための判断基準をいくつか言語化する必要があります。本skillsでは以下の判断基準を持たせるようにしました。 目指す品質 目指す品質を判断するためのリスクと基準 自動テストのスコープ 上記を定義することで、コードを書くときやテストを書くとき、レビューをするときやテストを検討する際にコンテキストに応じてAIが以下のように思考し、動くことができます。 今目指すべき品質のゴールは何か 今守るべきリスクは何か リスクに応じてどのくらいの基準で作るか それぞれの自動テストで担保すべきスコープは何か 目指す品質の言語化 私たちが目指すゴールイメージをAIに伝えるため、目指す品質の言語化を行いました。バクラクは経済をデジタル化するプロダクト群であり、お客様のセンシティブな情報を預かるプロダクト群のため、一定以上の品質の水準をキープする必要があります。加えて高頻度でリリースを行うことも同時に目指しています。品質をキープするためにコーディング規約などさまざまな取り組みをしていますが、その一環としてプロダクトのフェーズごとに異なる「バクラク事業部が目指す品質」を定義しました。定義の内容の具体についてここでの言及は避けますが、プロダクトのフェーズによって変わる品質の側面において何を優先すべきかといった方針を定義しています。市場にリリースしてフィードバックを集めたいプロダクトのフェーズと、すでに運用状態にあるプロダクトのフェーズでは大事にするポイントが違う、ということを言語化しています。 リスクと基準の言語化 目指す品質の言語化はできたのですが、それを適用しようとするとプロダクトごとにカスタマイズをして適用する必要が出てきます。バクラク事業部ではコンパウンド戦略を取っており、複数のプロダクトそれぞれ固有のリスクが存在します。いわゆるseverityの度合いとその影響範囲をまとめたものです。このリスクにおいては特定の期間単位で振り返りながらチームでリスクの定義をアップデートする習慣が既に社内にできています。このプロダクト固有のリスクをプロダクトごとに分けてskills上で定義した上で、カバレッジの基準*1 などを決めました。 自動テストの内容やスコープの言語化 LayerXではテストピラミッド戦略として、自動テストのレイヤーごとにどのような内容を検証し、確認すべきかを定義しています。こちらの言語化は以前からしていたので新しい取り組みではないのですが、詳しくは バクラク事業部のテストピラミッド設計 - LayerX エンジニアブログ をご覧いただければ幸いです。 ※こちらのテストピラミッドについても、テストの責務の内容については更新がされており上記ブログよりも詳細な粒度での言語化ができています。 Claude Codeへの統合:AIが自らフェーズとリスクを判断し適用する 言語化した上記の情報を統合し、Claude Code Agent Skillsとして実装しました。例えばClaude Codeにタスクを依頼すると、Claude Codeは以下のように動作します。 フェーズ判断 タスクの内容から、当該プロダクトがどのようなフェーズにあるプロダクトなのかを判断し、不明な場合はHITL(Human In The Loop)でユーザーに質問する形を取ります。 リスク評価 対象の機能カテゴリなどを参考に、リスクを判断します。この機能関連の開発である旨をAIに伝えることで、当該機能のリスクレベルをAIが評価をして徹底度を判断します。全プロダクトのリスク情報を読み込むとcompaction(コンテキストの肥大化・圧縮による精度低下)を引き起こすリスクがあるため、開発対象のプロダクトのリスクのみを読む仕組みにしています。 基準の適用 判断したフェーズとリスクレベルに基づき、実装方針、テスト戦略(カバレッジ目標など)加えてコーディング規約を自動適用します。(なおコーディング規約については個別に持たせている方が多いため近いうちスコープから外す予定。) なおプロダクトの開発だけでなく、テストの計画・設計や自動テストの整備相談などにも柔軟に対応します。筆者は情報を与えた上でテスト計画を立案してもらったり、リスクの高いポイントの洗い出しをしてもらうといった使い方をしています。個人的には企画や設計の段階で「怖いポイント」を提案してもらう方がフィードバックがさらに早くなるのでおすすめです。 結果 実際にこのスキルがある状態でどのような効果が見込めるのかをskill-creatorのEvalsを使って計測をし、改善効果が見込めることを確認しました。 特定のプロダクトのリスクが高い機能を作る、とした場合にはClaude Codeは自らリスクが高いと判断し、トランザクション管理やロールバック処理、定義した基準のユニットテストカバレッジを目指した堅牢なコードを生成しました。 特定のプロダクトの○○と言う機能を作る、仕様はこのような仕様である、という情報を与えた場合にはnot with skillでのテストカバレッジは65%、with skillでのテストカバレッジは95% という結果になりました。 エンジニアは生成されたコードやテストケースを確認し、動作確認をして妥当性を判断したり、チューニングをしたりすることができます。 もちろん納得がいかない場合は別のプロンプトで動き直すといったトライ&エラーとフィードバックループを高速に回すことができます。 筆者も既にテスト計画を作ってもらったり、自動テストの整備の優先度を決めてもらったりしているのですが、今のところ悪くない実感を得られています。 終わりに AIがコードを書く時代において、人間の重要な役割は「あるべき姿を定義(言語化)すること」へとシフトしつつあります。暗黙知になっている品質の姿を言語化し型に落とし込むことでQuality HarnessとしてAIに組み込み、フィードバックループを更に早めることができるようになっています。言語化をしてみようかな、と少しでも思っていただけたなら幸いです。 モデルの性能向上に伴い、スピードと質がどんどん上がっていく世界はもう近いところにきていると考えています。その世界の中でどういったポイントを抑えることで、より良いプロダクトを作る方向に持っていけるか、今後もQAエンジニアとして真摯に考えていきたいと思います。 LayerX バクラク事業部QAでは、そんな世界を共に楽しめる仲間を募集しています。興味のある方はぜひエントリーいただければ幸いです。本記事についてもカジュアルに話したい方は是非! 【バクラク】QAエンジニア_ポテンシャル枠 / 株式会社LayerX 【バクラク】QAエンジニア / 株式会社LayerX 【バクラク】シニアQAエンジニア / 株式会社LayerX jobs.layerx.co.jp *1:カバレッジ基準についてはCapers Jones氏のペーパーを参考に設定しています。
はじめに こんにちは、バクラクビジネスカード開発チームの @budougumi0617 です。 先月まではテックリードでしたが、4月からエンジニアリングマネージャになりました。 バクラクビジネスカードは2022年からサービスを提供している法人様向けクレジットカードのサービスです。昨年2025年からはETCカードのサービス提供も開始しました。 prtimes.jp 本記事では、もともとクレジットカードの決済データを受け取ることのみを前提に設計されていたバクラクビジネスカードのデータベースに対して新しくETCカードの決済データも受け入れられるようにマイグレーションを行なったときに考えたことをまとめます。 要点を先に書くと、次の3つです。 約3年分の決済レコードが蓄積された本番DBに対して、サービス無停止でスキーマ変更を完了した テーブルごとに許容できるマイグレーション時間も考慮しながらマイグレーション戦略を切り分けた 本番DBの複製を利用した実データを用いたマイグレーション時間の事前計測 決済系プロダクトでスキーマ拡張を行うエンジニアや、歴史あるサービスでの大きめのDBマイグレーションを検討している方にとって、なんらかのヒントになれば幸いです。 背景:クレジットカードの体験をETCカードでも提供する バクラクビジネスカードは他のバクラクプロダクトと連携し、クレジットカードの決済から経費精算・経理担当の会計処理までのシームレスな体験を提供しています。 「クレジットカードの体験をETCカードでも提供してほしい」と希望するお客様の声は常にありました。 ここで、バクラクビジネスカードはプロセシングプラットフォームであるインフキュリオン様のXardを利用してクレジットカード機能を開発しています。 2025年、XardでETCカード発行サービスがリリースされることになり、バクラクでもETCカードを提供することとなりました。 インフキュリオン、「Xard(エクサード)」の新オプション機能として 「ETCカード発行」サービスを2025年7月より提供開始 - Infcurion, Inc. Xardを利用したETCカードは、通常のクレジットカードと同じAPIを用いてETCカードの発行および高速道路利用時の決済情報を受け取ることができます。 バクラクビジネスカードのオプションとしてETCカードを新たに提供するにあたり、技術的に大きな問いが1つありました。 それは、クレジットカードの決済情報を受け取ることのみを前提としたデータベースのテーブルをETCカードの決済情報にも対応させることでした。 クレジットカードの決済情報テーブルをETCカードの決済情報にも対応するように拡張する バクラクビジネスカードは、Xardからの決済リクエスト、Webhookを受信して決済を処理しています。 クレジットカード(以降クレカ)の決済情報とETCカードの決済情報では含まれる内容に違いがあることが事前にわかっていました。 クレカとETCの決済データの差分 ETCの決済データは、クレジットカードの決済データといくつかは共通している一方、各々で固有の項目もそれなりにあります。たとえば以下のとおりです。 共通の項目:決済金額、決済日時、カードを特定する情報 など クレカ固有の項目:加盟店名などISO8583に定義された情報 1 ETC固有の項目:入口・出口インターチェンジ名、車種 など 2 2つの大きな制約 今回のカード・決済情報テーブルのETCカード対応拡張に着手するうえで、外せない制約が2つありました。 1つ目は「無停止要件」です。クレジットカードはお客様が24時間365日利用するサービスです。 決済を止めるメンテナンスはどうしても回避できない場合を除き行わない方針で開発・運用をしています。 2つ目は「既存データ量」です。バクラクビジネスカードはリリースから3年以上が経過し、主要テーブルには3年分の決済情報のレコードが蓄積されていて、今この瞬間にも新たな決済明細のレコードが増え続けています。ローカルでは一瞬のマイグレーションも、本番では何分・何十分とかかる可能性がありました。 「無停止」と「3年分のレコードを抱えたテーブルへの変更」が同時に求められる、これが今回の出発点です。 テーブルがどのようなユースケースで利用されているか考える スキーマ拡張の議論を始めると、つい「どんなテーブル設計が綺麗か」「どんなマイグレーションが速いか」という話に目が向きがちです。 しかし、今回のETC対応で最初に行なったのは、そのテーブルが本番でどう利用されているかを見るという工程でした。同じスキーマ変更要件でも、当てるテーブルの利用パターンが違えば、許される操作の幅も大きく変わります。 バクラクビジネスカードの決済処理を構成するテーブルは、本番での利用のされ方が大きく異なる2系統に分けられました。 系統A:決済処理APIに紐づくテーブル 指定時間以内にレスポンスを返さなければ決済そのものが失敗扱いになる経路で参照・更新されます 既存処理に与える影響を限りなく小さくする必要があります 許されるのは「短時間で完了し、マイグレーション後もアプリの挙動に影響がない」スキーマ変更です 系統B:Webhook経由の非同期処理に紐づくテーブル 外部からのイベントを弊社システム内部のキュー経由で非同期に処理する経路で参照・更新されます 一時的な処理失敗はキューで再処理できる設計になっており、ある程度の処理遅延・実行時間を許容できる経路です サブテーブル分離など、踏み込んだスキーマ変更も現実的な選択肢になります この2系統を同列に扱ってしまうと、「全テーブルを最速で完了させる」という強い縛りが生まれ、選べる打ち手が極端に狭くなります。逆に切り分ければ、系統Aには本当に安全な操作だけを当て、系統Bには踏み込んだ変更を入れる、という割り当て方ができます。 非同期経路のWebhook基盤の設計については、過去の発表資料もあわせてご覧ください。 speakerdeck.com 系統ごとに違うアプローチを選ぶ テーブルの利用パターンが違うことを踏まえて、ETC対応のスキーマ拡張も系統ごとに違うアプローチをとりました。 系統A:既存テーブルに最小限の変更だけを当て新規テーブルを追加する API処理側のテーブルには、既存クエリの形を変えずに済む最小限のスキーマ変更だけを当てる方針にしました。 具体的には、決済の親テーブルに種別を見分けるカラムを1つ追加し、ETC固有の付随情報は新規テーブルに寄せる構造です。 もともと決済処理APIの内容を格納しておくテーブルは2つに分かれていました。 IDや受信時刻など基本的な情報を持つ親テーブル 金額や加盟店名などに関する詳細テーブル そこで詳細テーブルはクレカ用として既存のまま残し、ETC側は新規に別の詳細テーブルを用意することにしました。 判断軸はシンプルで、API処理側では次の3点が強く求められたからです。 既存クエリへの影響を最小化すること 明細表示や集計クエリを1テーブルで完結させること(UNIONや分岐を増やさない) マイグレーション中・マイグレーション後もアプリケーションが決済を処理し続けられること 修正前後の構造のイメージは次のとおりです。 flowchart LR subgraph Before["修正前"] A1["親テーブル"] A2["詳細情報テーブル"] A1 --> A2 end subgraph After["修正後"] B1["親テーブル</BR>種別カラム"] B2["詳細サブテーブル<BR/>(クレカ用とする)"] B3["ETC用詳細情報テーブル<BR/>(新規)"] B1 --> B2 B1 -- "種別=ETC のときのみ参照" --> B3 end Before --> After 系統B:共通テーブル+種別ごとのサブテーブルに分ける Webhook経由側のテーブルも最終的な形は似た構造になるように変更しました。 Webhookで受け取る生データは最初1テーブルで全てを含んでいました。 そのテーブルにETCカードのカラムを追加するとクレカ・ETCカードでお互いNULLになるカラムが増え、意味づけも複雑化します。 そこで、共通テーブルには種別を見分ける情報を持たせ、種別固有のデータはサブテーブルに完全分離する形にしました。修正前後の構造のイメージは次のとおりです。 flowchart LR subgraph Before["修正前"] A1["共通テーブル<br/>(クレカ用カラムも含む)"] end subgraph After["修正後"] B1["共通テーブル<br/>+ 種別カラム<br/>(クレカ用カラムは後日DROP)"] B2["クレカ用<br/>詳細サブテーブル<br/>(新規)"] B3["ETC用<br/>詳細サブテーブル<br/>(新規)"] B1 -- "種別=クレカ" --> B2 B1 -- "種別=ETC" --> B3 end Before --> After このアプローチではデータの扱いが大きく代わりますが、共通テーブルへのカラムのDROPを後日行えばマイグレーション実行直後でもアプリケーションは問題なく動きます。 そのかわり新設したサブテーブルへ過去分のデータをバックフィルする作業や、共通テーブル側の旧カラムを段階的に整理していく作業も発生します。 しかしこれらの「時間のかかるデータ移行作業」でも、Webhook経路の場合は移行作業中にWebhook処理が失敗してもキューの再実行によりリカバリが可能です。 同じ要件、違う実装手段 ETC対応で必要だった共通要件をまとめると、次の3つでした。 カード種別を識別する仕組み ETC固有データを保持する場所 既存クレカ機能への影響を出さないこと 同じ要件でも、系統Aと系統Bでは実装手段が違います。 系統A:既存テーブルの延長線上で考える 系統B:多少実行コストがかかってもデータを整理する 「全テーブルで同じマイグレーション戦法をとる」という方針を強引に当てはめないことで、それぞれのテーブルの本番利用に合った形に落とし込めました。 マイグレーションを当てる前に、実データ量で測る 設計のアプローチが決まっても、本番にそのまま実行してよいとは限りません。先に述べたとおり、ローカルでは一瞬で完了するDDLでも本番のデータ量では想定外のロックや所要時間で決済を巻き込むリスクがあります。 ここで効いてきたのが、本番相当の実データ量でDDLの実行時間を必ず検証するという工程でした。バクラクプロダクトでは、権限管理された本番クローン環境で、事前にDDLを検証できるBytebaseをフロントにおいたSQL実行基盤があります。詳しくは以下の記事をご覧ください。 tech.layerx.co.jp この基盤の上で、今回投入したいDDLを実際に流し、所要時間とロックの挙動を計測しました。チェックしたのは主に次の点です。 カラム追加が ALGORITHM=INSTANT で完了するか、それともテーブルコピーになるか 「カード種別を識別するカラム」の追加が高速で終わるか 新規インデックス追加にかかる時間と、その間の参照/更新への影響 DROP COLUMN にどれくらい時間がかかるのか 検証で得られた所要時間が、系統A・系統Bそれぞれの戦略を本番に当てて良いかの判断根拠になりました。 複製環境ではデータが消える DROP COLUMN も気兼ねなく試せたため、戦略の幅も広く取れます。何より「確かめはしたけれど本番で本当に問題ないか不安」という気持ちにならずに自信をもってマイグレーション実行日を迎えることができました。 実施と結果 運用中のテーブルへのマイグレーションのため、実際のマイグレーションではアプリケーションコードのCRUDが壊れないかをQAで確認しつつ、本番環境へメンテナンスタイムなしでマイグレーションを実施していきました。 結果として、今回の一連のスキーマ変更について、マイグレーション起因の決済失敗は確認されず、メンテナンスタイムなしで完了することができました。 2025年夏にリリースしてからとくに問題なくETCカードの処理も行えています。 学びと振り返り 今回のプロジェクトで、後続のスキーマ変更にも持ち越したい学びは次のあたりです。 マイグレーションアプローチは、テーブルの本番利用に合わせて選ぶ API処理に紐づくテーブルには軽量な変更を当て、Webhook経由のテーブルには踏み込んだ変更を当てる、という割り当てがETC対応で機能しました。同じ考え方は決済に限らず、ECの注文テーブルへの配送種別追加、SaaSの課金テーブルへの新プラン追加など、SLAの厳しい経路と緩い経路が同居するシステム全般に応用できます。 マイグレーションは実データ量で測ってから当てる ローカルでの感覚と本番でのふるまいは違います。本番相当のデータで実際に流して数字を見ることで、「この変更は適用できる」「この方針は見直すべき」という判断を実測値にもとづいて行えました。 本番クローン環境は意思決定の精度を上げる 単なる検証ツールではなく、設計や戦略そのものを引き返す根拠として機能しました。 おわりに クレジットカードは止められないからこそ、「止めずにどう変えるか」を設計・意思決定の中心に置く必要があります。今回はETCカードの追加という形で、その難しさと面白さに、設計と運用の両面から向き合いました。 テーブルの本番利用を見てからアプローチを決めること、そして実データ量で実行時間を必ず検証すること、この2つが今回の無停止マイグレーションを支えました。 バクラクビジネスカードを開発しているバクラク事業部のPayment開発部では今年もBPSP(Business Payment Service Provider)をリリースしたり、新たな決済サービスの開発を続けています。 <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fprtimes.jp%2Fmain%2Fhtml%2Frd%2Fp%2F000000607.000036528.html" title="バクラク請求書発行、カード払い可能な請求書を発行できる「カード決済オプション」リリース。債権回収業務の安定化をサポート。" class="embed-c
はじめに こんにちは、TechPR の太田です。 先日、LayerXのオフィスを会場として、エンジニア向けキャリアイベント「国際女性デー特別イベント-Give to Gain-エンジニアの私たちが次の世代へ還元できること」が開催されました。 「エンジニアのキャリアをエンパワーメントする」という本イベントの趣旨に賛同し、今回は会場提供という形でサポートさせていただきました。 当日は、これからのキャリアを真剣に考える多くのエンジニアの皆さまにご来場いただき、熱気あふれる一日となりました。 属性の枠を超え、ポテンシャルを最大化するサポート イベント内では、弊社からも企業LTとして @ar_tama が登壇しました。 LayerXではこれまでも、女性エンジニア支援の取り組みなど、DEI(ダイバーシティ、エクイティ&インクルージョン)を推進する活動を行ってきました。 LTではこうした事例に触れつつ、ジェンダーなどの属性による枠を超えて、誰もがポテンシャルを最大化できる環境をサポートしていきたいという想いをお伝えしました。 私たちは行動指針にある「徳」を大事にする組織として、特定の誰かだけでなく、コミュニティ全体がフェアに挑戦できる土壌を整えることを、技術広報の立場からも大切にしています。 note.layerx.co.jp 主催者の想いに応える。LayerX初の「託児所」設置への協力 今回のイベントにおいて、LayerXとしても新たな試みへの協力を行いました。それが、オフィス内への託児所の設置です。 子育て中の方々にとっても、ライフイベントに左右されずインスピレーションを得られる場を作りたい。そんな主催のWAKE Career様の想いを受け、以下の皆さまと連携する形で、この仕組みを試験的に導入しました。 託児スポンサー: 株式会社サイバーエージェント様 託児運営: 株式会社Josan-she’s様 会場提供: 株式会社LayerX LayerXにとってオフィス内に託児機能を設けるのは初めてのことで、正直なところ、「まず社内の誰に相談すればいいのか」すらわからない状態からのスタートでした。 しかし、主催者様からのご提案を形にするべく声を上げると、すぐに法務や労務、総務チームが「どうすれば安全に実現できるか」を自分事として捉え、運営を担当したJosan-she's様と契約や動線の相談をしながら、驚くほどの短期間で並走してくれました。 実際の利用者は数名というスモールスタートではありましたが、「前例がないから」と止まるのではなく、「コミュニティにとって価値があるなら、まずはやってみよう」と全職種がスピード感を持って動く。弊社のカルチャーである「Be Animal」を、私自身が改めて実感した出来事でした。 託児エリアはJosan-she's様により運営され、助産師資格を持つ専門家がお子さまをお預かりしました。写真は許可を得て掲載しています。 会場を彩る企業ブース 当日は会場内に、エンジニアコミュニティを支える各社によるブースも出展され、交流に花を添えてくださいました。 Nstock株式会社さま:キートップのキーホルダーやトートバッグを配布。ステキなデザインに、足を止めて見入る参加者の方が多くいらっしゃいました。 株式会社マイクロニティさま: オリジナルプリングルスのノベルティを配布。おしゃれで美味しいノベルティは懇親会でも話題となっていました。 こうした企業同士の交流が生まれる場を自社オフィスで提供できたことを、とても嬉しく感じています。 参加者の皆さまからいただいた温かい声 このような賑やかな雰囲気の中、終了後には多くの参加者の方から熱いフィードバックをいただきました。 「社内に女性エンジニアがいなかったが、出産とキャリアの両立について直接質問できて勇気をもらえた」 「"つよつよエンジニア"と比較して自己肯定感が下がっていたけれど、自分自身のワクワクに目を向けようと思えた」 「男性ばかりの職場では聞けない話が聞けて、一人じゃないと思えた」 等身大の悩みや経験が共有される場に立ち会えたことで、会場を提供した私たちも、改めてこのイベントの意義を噛み締めています。 おわりに たとえ一歩ずつであっても、選択肢を増やしていくことがこれからのエンジニアコミュニティにとって大切なことだと信じています。 LayerXはこれからも、エンジニアコミュニティの発展と、誰もが属性に関わらず挑戦し続けられる社会の実現に向けて、多角的なサポートを続けていきます。 ご来場いただいた皆さま、主催のWAKE Career様、そして協力企業の皆さま、本当にありがとうございました!
はじめに ケース1: 決済実行時の保存失敗 — 仕様は「ユーザー体験を壊さない」 課題と仕様 設計の考え方 ケース2: 送金結果不明 — 仕様は「統制・監査上の説明可能性」 課題と仕様 設計の考え方 おわりに はじめに こんにちは。LayerX でソフトウェアエンジニアをしている @ysakura_ です。バクラク請求書発行のカード決済機能の新規開発を担当しました。発行した請求書に対してクレジットカードでの決済を受け付けられるようにする機能です。取引先がクレジットカードで支払うと、バクラク請求書発行の利用者には銀行振込で請求書の代金が入金されます。カード決済の受付から銀行振込での入金まで、一連の処理を担う基盤です。 bakuraku.jp この決済機能は外部の決済サービスプロバイダ(以下、PSP)や銀行 API と連携して動いています。決済が成功したのに自システムの保存が失敗した、送金 API を呼び出したが結果が返ってこない。立ち上げの中で、こうした整合性の課題に繰り返し向き合いました。本稿では、この 2 つのケースとそれぞれの判断プロセスを紹介します。具体的な実現方法は連携先の仕様に依存しますが、判断のプロセス自体は異なる技術制約のもとでも応用できると考えています。 ケース1: 決済実行時の保存失敗 — 仕様は「ユーザー体験を壊さない」 ※ 本節では「利用者」を、カード決済機能を利用する取引先の決済担当者として扱います。 課題と仕様 決済処理は外部の PSP と連携して進みます。次のシナリオを考えてみます。 PSP では決済が成立した。しかし決済サービスへの結果保存が失敗した DB 接続の一時的なエラーなどで、PSP では決済が成立したのに決済サービスの保存が失敗するケースがあります。決済サービスから見れば「失敗」ですが、PSP 側では成立しています。 本稿のシーケンス図では、実線(→)はリクエスト・レスポンス、点線(⇢)はイベント通知やプッシュ通知を表します。 図1: 決済成立後に内部保存が失敗するケース カード会社によっては、決済成立時に利用者へ即座に通知が届きます。そのため、画面上の表示と外部からの通知が食い違う可能性があります。ここで失敗を示すと、そうした食い違いが混乱を招きます。「決済を取り消して再実行する」案もありましたが、成立後に決済を取り消す判断はさらに強い混乱を生むため、チームで議論し、PSP で成立した決済は取り消さず、決済サービスの内部状態をPSP側に合わせる方向で仕様を決めました。 仕様: 外部で確定した結果に合わせて、内部状態を更新して回復する。 設計の考え方 実現方法としては、不整合をその場で解消する(決済の取り消しや保存の即時リトライなど)のではなく、利用者にはまず成功として応答し、後続で PSP の決済状態を照会して内部状態を同期する方式を採りました。図式化すると次のようになります。 図2: 成功応答と後続での整合性回復 ケース2: 送金結果不明 — 仕様は「統制・監査上の説明可能性」 決済だけでなく、その先の送金フェーズでも同じ構造が現れました。本機能では決済完了後にバクラク請求書発行の利用者へ代金の送金を行いますが、外部の銀行 API との間で新たな整合性課題に直面します。 課題と仕様 送金処理は外部システムと連携して進みます。次のシナリオを考えてみます。 送金に関する処理を進めたが、ネットワーク断絶等の一時的な障害で結果が返ってこない。処理がどこまで進んだのか分からない 図3: 送金 API のレスポンス不達シナリオ なお、今回の送金 API は冪等性(同じ操作を何度実行しても結果が変わらない性質)が担保されており、技術的にはリトライしても重複送金は発生しません。ケース 1 と同様に、この場面でも複数の回復手段があり、技術的な妥当性だけを見れば候補はいくつかありました。 しかし、チームで議論する中で 1 つの論点が浮かび上がりました。冪等性によって技術的な安全性は担保されていますが、冪等な API への再リクエストは、元のリクエストが相手に到達していれば「結果の確認」になり、到達していなければ「新たな送金の実行」になります。同じ操作の意味が状況によって変わる構造は、統制・監査の観点では説明が難しくなります。 仕様: 確認系と実行系の責務を厳密に分離する。 設計の考え方 仕様が「確認系は実行を伴わない」と決まったことで、冪等な API への再リクエストで結果を確定させる案は採用しませんでした。結果が不明な状態を無理にその場で解消するのではなく、参照系の API で送金状態を照会し、新たな送金として実行されないかたちで不整合を解消する方針を採りました。 おわりに ここまで 2 つのケースを見てきました。いずれもトレードオフを引き受けています ── 瞬間的な不整合を許容したり、自動解決できないケースが残ったり。それでも「ユーザーの業務を止めない」「ユーザーに混乱を与えない」という方針は守りたいと考えていました。 振り返ると、2 つのケースはいずれも同じパターンに沿っています。 ケース 境界 優先した観点 対応の方向性 1. 決済保存失敗 決済サービス × 外部 PSP 利用者の混乱回避 外部で確定した事実を基準に回復方針を決める 2. 送金結果不明 決済サービス × 外部銀行 API 説明可能性の確保 確認と実行の意味を混在させない 2 つのケースはそれぞれ異なりますが、いずれも仕様を明確にしたことで技術選択が絞り込めた事例です。技術制約が先に来るケースもありますが、今回は仕様起点で判断できた例として紹介しました。 補償トランザクション、即時リトライ、冪等な API への再リクエスト。いずれも検討しうる手法ですが、重要なのは技術的な優劣ではなく、仕様と技術制約を踏まえてどの手法を選ぶか です。 「競合したときにユーザーにどう見えるべきか」「監査でどう説明するか」── これらは技術の問いではなく、仕様の問いです。PdM(プロダクトマネージャー)や事業側とこの問いを共有し、仕様を明確にすることこそが、整合性設計を正しく進める鍵だと考えています。 最後に、バクラクの決済事業ではソフトウェアエンジニアを募集しています。仕様やその前段となる判断軸から議論し、それを技術設計に落とし込むプロセスに日常的に携われる環境です。興味のある方はぜひカジュアル面談や求人ページをご覧ください。 jobs.layerx.co.jp open.talentio.com
LayerX BizOps 部データグループのさえない (@saeeeeru) です。最近は娘と『名探偵プリキュア!』にハマっています。「自分で見て、感じて、考えて、"本当"の答えを出す」。AI 時代だからこそ刺さるメッセージです(推理パートをちゃんと解けるようになりたい)。 前回の記事では、dbt Python model から外部 API を呼び出す実装パターンを紹介しました。今回はその応用として、LLM の Web Search 機能を使って公開情報を取得し、それをデータパイプラインに組み込む実践例を書きます。 この記事では、まず LLM の Web Search 機能をどう使うとデータパイプラインに載せやすい形になるのか を説明し、そのうえで Snowflake / dbt にどう載せたのか、そして本番運用の中でどんな品質課題が見えてきたのか、という順に整理します。 Web Search を Snowflake / dbt のパイプライン設計にどう載せるか この記事の技術が必要になった背景には、分析基盤である Snowflake にない外部の公開情報を継続的に取り込みたい、というモチベーションがありました。たとえば、既存の属性情報だけでは判断材料が不足し、企業サイトやニュースリリースのような一次情報を補助的に参照したくなるケースがあります。 こうした情報を毎回人手で見に行く運用は継続しづらい一方で、自然言語のまま取得しても構造化データではないためデータ処理対象として扱いづらいです。この章では、外部 Web 上の公開情報をどう取得し、どうすれば Snowflake / dbt のパイプラインで扱える形にできるか を説明します。 外部 Web Search の実装パターン 外部の公開情報を取得する手段として、まず古典的なスクレイピングがあります。しかし、企業サイト・ニュースリリース・メディア記事など多様なソースの Web ページを対象にする場合、サイトごとにパーサーを書いて構造化するのは現実的ではありません。取得したい情報が「資金調達をしたか」「事業内容は何か」といった、自然言語の意味を解釈したうえでの抽出である以上、LLM を介する必要がありました。 LLM を使って外部の公開情報を取得・構造化する場合、実装パターンは大きく 2 つあります。 検索 API と LLM API を分けるパターン : 検索と要約・抽出を別々の API で組み合わせる Web Search を内包した LLM API を使うパターン : 検索と応答生成を 1 つの API でまとめて扱う 今回の設計では、検索から構造化までを LLM に任せて実装と運用をシンプルに保ちたかったため、後者の Web Search を内包した LLM API を採用しました。具体的には Azure OpenAI の Responses API + web_search_preview を使っています。 ただ、LLM の応答を自然文のまま返させるだけでは後続のデータ処理につなぎにくいため、Responses API の応答は JSON として返させるよう設計しました。Snowflake 上ではまず半構造化データとして保持し、必要な情報を後段で扱いやすい形に整えていきます。次の節では、「何を抽出し、どの粒度で持つか」という出力スキーマの設計を説明します。 スキーマ設計の重要性 スキーマの具体的な定義はユースケースによって異なりますが、重要なのは以下の 2 つの補助情報を含めることです。 Web Search を使っても誤りは残るため、重要なのは「誤りをなくすこと」よりも、「後から確認ができる情報を残すこと」だと考えています。 confidence: high / medium / low の 3 段階で、モデル自身の確信度を自己申告させる evidence: 出典 URL と該当箇所のスニペットを配列で返させる これにより、利用者が情報を鵜呑みにせず、「根拠を確認してから判断する」運用を組み込みやすくなります。confidence はあくまで自己申告であり、evidence も正しさそのものを保証するわけではありませんが、少なくとも確認を始める手がかりは残せます。 ここまでが、「Web Search をどう実装し、データパイプラインに載せるにはどんな出力スキーマが必要か」という話です。続いて、その処理を Snowflake / dbt の中でどう実装・運用したかを見ていきます。 なぜ dbt Python model で LLM API を呼び出す構成にしたのか 今回の本番構成では、dbt Python model から LLM API を呼び出す形を採用しました。 正直なところ、一番大きかったのは、Snowflake / dbt の既存パターンの延長として、データエンジニアが実装・運用しやすかったことがあります。 LayerX のデータ基盤では、各種 SaaS API を呼び出す dbt Python model の実装がすでに多数あります(前回の記事で紹介したパターン)。そのため、LLM API の呼び出しも同じパターンに載せるのが自然でした。 全体像は次の通りです。 graph TD subgraph Snowflake MASTER["マスターデータ"] subgraph dbt["dbt レイヤー構成"] PY["dbt Python model<br/>対象レコードの抽出 / API 呼び出し / JSON 格納"] LND["landing<br/>取得直後の RAW_JSON"] SRC["src<br/>不正データ除外 / 取得手段の隠蔽"] DWH["dwh<br/>再利用を意識した再構成"] MART["mart<br/>用途特化の利用データ"] end end subgraph RESP["Responses API"] API["Web Search<br/>+ JSON 応答"] end MASTER --> PY PY -- "Snowflake から<br/>外部 API へ接続" --> API API -- "JSON 応答" --> PY PY --> LND LND --> SRC --> DWH --> MART MASTER -.-> DWH MASTER -.-> MART ここでいうデータのレイヤーは、社内では大きく landing、src、dwh、mart に分かれています。役割としては、landing は取得直後のデータを保持する層、src は取得手段を隠蔽しつつほぼ生データとして扱いやすくする層、dwh は再利用を意識して再構成する層、mart は用途特化で利用する層です。今回の記事で主に扱うのは、landing -> src までです。 このように、dbt に載せておくと、Snowflake 上の既存データを参照しながら、Responses API の呼び出し対象を絞り込めます。これはコストを抑えるのに効くため、後述する Incremental 戦略の前提になります。それに加えて、 Snowflake 上の他のユースケースにも展開しやすいという見通しもありました。 実装: dbt Python model × LLM API (Web Search) dbt Python model の実装 この節では dbt Python model 側の実装を紹介しますが、Snowflake に寄った実装詳細になっています。 コードのポイントを先に整理しておきます。 並列実行: Responses API は 1 リクエストあたり 10〜30 秒かかるため、ThreadPoolExecutor(max_workers=10) で並列化しています。10 は Snowflake 上での実行負荷と API 側の待ち時間を見ながら調整した値です Incremental: dbt.is_incremental + leftanti join で、直近 N 日以内に処理済みのレコードをスキップします。N の値は取得対象の情報の更新頻度を意識して設計します リトライ: JSON パースエラーと API エラー(429 等)を Exponential backoff で共通処理しています Note: 認証情報は Snowflake の Generic Secret + External Access Integration で管理しています。また、 dbt.ref() と dbt.is_incremental は、どちらも dbt が組み込みで用意している参照・条件分岐のための機能です # landing レイヤー : dbt Python model の疑似コード import json import logging import time from concurrent.futures import ThreadPoolExecutor, as_completed from datetime import datetime, timezone import _snowflake import pandas as pd import requests from snowflake.snowpark import Session from snowflake.snowpark.functions import col # ユースケースに応じた JSON Schema を定義 # 構造化出力オプションに渡し、出力形式を一定範囲で制約する OUTPUT_SCHEMA = { "type": "json_schema", "json_schema": { "name": "web_search_result", "strict": True, "schema": { # ... ユースケースに応じたスキーマ定義 ... }, }, } SYSTEM_PROMPT = "ここにプロンプトを入れる" MAX_WORKERS = 10 MAX_RETRIES = 3 BACKOFF_BASE_SECONDS = 1 SKIP_DAYS = 30 def build_payload(input_text: str) -> dict: return { "input": input_text, "instructions": SYSTEM_PROMPT, "tools": [{"type": "web_search_preview"}], "text": {"format": OUTPUT_SCHEMA}, } def extract_output_text(response_json: dict): """ Responses API のレスポンスから JSON 文字列を取り出す。 実際のレスポンス構造は SDK / API バージョンに合わせて実装する。 """ for item in response_json.get("output", []): if item.get("type") != "message": continue for content in item.get("content", []): if content.get("text"): return content["text"] return None def call_llm_api(input_text: str, api_key: str, endpoint: str, deployment: str) -> dict: """1 件分の Web 検索 + 構造化出力を取得する。""" # 実際の URL 形式は利用中の API に合わせて組み立てる url = f"{endpoint}/openai/deployments/{deployment}/responses" head