手触り感のあるContext Engineering – LayerX エンジニアブログ

こちらはLayerXエージェントブログリレー2日目の記事です(1日目のponさんの怒涛のTKG記事(not Tamago kake gohan)もぜひご覧ください)。

こんにちは、CEO室でAI Agent開発のPdMをやっているKenta Watanabeです。

近年のLLM関連技術の急速な発達により、自社プロダクトの開発にLLMを活用する方も増えてきているのではないかと思います。一方で、LLMの確率的な振る舞いからプロダクションで安定稼働する機能やAI Agentの開発に苦戦している方も同時に多いのではないかと思います。

そういった中で、6月頃からContext Engineeringと呼ばれるLLMをうまく稼働させるための技術が話題になってきました。Context Engineeringというキーワードがバズり出した起源やContext Engineering自体の解説は各所で話題になっていると思うので省略しますが、要は「エンジニアが各々で試行錯誤していたLLMをうまく動かすための試み」にどんぴしゃな名前がついたことで話題になっている状態で、おそらくLLMと向き合っていたエンジニアの方であれば「これこれ、これが大変なのよ・・!」と共感できる知見が日々公開されている状況なのではないかと思います。

今日はContext Engineeringに関するこの夏話題の情報を簡単に解説したいと思います。

またLMMを活用した開発に取り組み始めた方からすると、これらの情報を見つつも「実際にLLMを活用した開発を行う際にどういう流れで試行錯誤をするのか?」がイメージつきにくい面もあるかと思うので、後半では具体的なタスクを手に取りながらContext Engineeringの過程を追っていきたいと思います。

もともとPrompt Engineeringについてはこの数年間でかなりの情報が出揃っており、日々手元のタスクを動かすためのPromptのtuningを行っている方は多いのではないかと思います。anthropicやOpenAIといったモデルメイカーからも具体的なテクニックやプロンプトガイド(4.1, 5)がたくさん紹介されています。

そんな中Context Engineeringというキーワードは6月にShopify CEO @tobiツイートや元OpenAIの@karpathyツイートによって一気に認知されるようになりました。同時期にcognitionのエンジニアである@walden_yanによるブログ記事でもContext Engineeringについて述べられています。この記事では、Devinのようなlong runningなCoding Agentを開発する過程での試行錯誤として、複数のSub Agentsに分解したタスクを行わせて、その後その結果をMainのAgentで集約するのではSub Agents間でのタスク実行を一貫性を持って行うのが難しいため、

  1. 直列で複数のタスクを実行する
  2. long runningなタスクでContext Windowが巨大化し溢れるのを防ぐために、小さな別のモデルでContextを圧縮する

というテクニックが紹介されています。

ほとんど同じタイミングでAnthropicから公開された記事では、Research Agentの内部実装として実際のリサーチタスクを行うSubagentsをメインAgentから並列処理も含めて動的に活用する手法が紹介されています。

また、これらの記事が公開されContext Engineeringというキーワードが浸透する前にも、humanlayerの@dexhorthyは12-factor-agentsというgithub repositoryの中で、プロダクショングレードのプロダクトをLLMを活用して開発する上でのテクニックを公開するなど、徐々にLLMを安定的に稼働させるためのテクニックはAI Engineerの間で議論されてきていました。

その後もManusからLLMのKVキャッシュをフル活用するためのテクニックが公開されたり、long runningなagentを開発していて必ず直面する精度劣化の問題を詳細の調査したreportがChromaから公開されたりと、かなり活発にContext Engineeringに関係する技術や知見が議論されています。

LLMを利用してAI Agentや何かしらの機能開発を試みていたエンジニアにとって「どうLLMを通常のprogramの中に組み込むか?安定させるか?」は技術が急速に発達しつつも具体的な情報が多くない中でかなりの悩みの種だったのではないかと思います。そしてこれらの情報は一定の解決の方向性を示しつつメンタルモデルを整理するための情報となり、「名付けられてなかったが行っていた試行錯誤」の過程が徐々に体系化されていく、大興奮の夏になっているのではないかと思います。

こういった情報がたくさん公開されつつも、LLMを利用した開発自体が完全に新しい枠組みがゆえに、まだまだ開発の度にたくさんの試行錯誤をしているエンジニアの方が大半なのではないかと思います。そこで、今回はLayerX内での具体的な問題設定を前提にした「Context Engineeringの試行錯誤の過程」を1Stepずつ追いながら共有したいと思います。特に今回はデモを動かすためのPromptingやEngineeringではなく、プロダクションで安定稼働する成果物を作ることを想定した上での過程にフォーカスしたいと思います。

LayerXではお客様向けに経費申請や勤怠等のバックオフィス業務を行うためのサービスを提供しているため、ここではバックオフィス業務で使いそうなタスクとして「あるルール文書があったときに、あるクエリに関係する文章をルール文書内から位置付きで抽出する」というものを考えてみます。ルール文書には例えば経費申請における承認ルールや社内規程などが入り、クエリには「社内懇親ランチの上限金額」や「給与支給の日付」といった内容が入ることをイメージしてみます。位置付きで抽出するのは、何かしらのUIやログにおいて元ソースを確認するために利用すると想定してください。

naiveに実装してみる

めちゃくちゃnaiveにこのタスクをLLMを使って解く場合「LLMに全て解かせる」というやり方が考えられます。特に最近のモデルの目覚ましい進化を考慮すると、不必要に頭を使わずにモデルに任せるのが得策かもしれません。まずは、この実装を想定して以下のPromptを書いてみます。

あなたはある文書からユーザの入力に関係する箇所を抽出するエージェントです。

* ユーザの入力に関連する文字列をから抽出してください
* 複数箇所該当する場合は複数箇所を抽出してください
* 各関連する文字列ごとにの中での開始位置(文字単位)も抽出してください
* 関連する文字列の抽出が難しい場合は`errorMessage`にその理由を出力してください



下記のフォーマットで出力してください。


startIndex="[開始位置]">[関連する文字列]


[抽出に失敗した時の失敗理由]



[実際の文書を入れる]

このPromptをもとに、例として社内の賃金規定をの中に入れて「給与支給の日付」というユーザ入力を投げてo1で実行してみます。

すると下記のような結果で、startIndexの値を正しく結果が得られませんでした(※フォーマットや実際の中身を説明のため微修正してます)



startIndex="551">毎月1日より末日までの期間について計算し、当月25日に支給する



そこで最新のGPT5を使ってみます。



startIndex="-1">毎月1日より末日までの期間について計算し、当月25日に支給する


正確なstartIndex(文字オフセット)の算出には、提供テキストのエンコーディングおよび改行コードの前提が必要であり、本インターフェース上で厳密に特定することが困難なため、startIndexを-1で代替しました。

まだ難しいようなので、effortをhighにして頑張ってもらいます。



startIndex="254">毎月1日より末日までの期間について計算し、当月25日に支給する



なんとか正しい値が抽出できましたが、成功したり失敗したりとムラがあります。最初に書いたプロンプトはかなり雑な内容のため、このままPrompt Engineeringを駆使することでもっと安定して抽出できるようになるかもしれません。

native実装の問題点

ただし、この方法にはたくさんの問題があります。まず最初にかなりのコストとレイテンシーが発生します。GPT5にて最後の成功した設定の場合、実に16.3k tokensをoutputに使っており、約5分実行にかかっています(playground上での簡易実行のため実行環境やtier次第では前後します)。というのも、現状のLLMは本質的に文字数のカウントは得意ではないため、効率の悪いthinkingが大量に発生するため、このような結果になっています。

このようにnaiveにLLMに任せるタスクを大きく設定してモデルの能力に任せるだけでは、プロダクションで動くプロダクトを作る上では大きな問題があります。仮にデモで動いたとしても、安定稼働させることやレイテンシー・コスト面で大体の要求水準を満たすことは難しくなります。また、この状態でいくらPrompt Engineeringを頑張っても工数に見合った結果は得られないことが想像できます。

そこで次はLLMに任せるタスクの粒度を小さくすることを試みたいと思います。LLMは文字数のカウントは得意ではないですが、文字数カウントの部分を「ある文字列の中でのsubstringが全体の中で何文字目から始まるか」という問題に落とし込めば、これはdeterministicなcodeでなんの問題もなく解けるようになります。なので、LLMには文字位置の抽出は任せずに関連する文字列の抽出だけを任せ、位置の特定はdeterministicなプログラムで行うように処理を分解してみます。

LLMの特性を考慮して処理を分解する

まずは先人の知恵を借りるためにどういう実装方法があるかリサーチしてみます。おもむろにDeep Researchを使ってみると、Claude Citationsという、どんぴしゃでやろうとしている機能実装を見つけました。しかし、一通りこの機能を検証してみたところ、抽出される文字列の量が大きく(1000文字単位など)、また(日本語起因なのかどうかは不明だが)関連性の全くない文字列が抽出されるといった状況で、かつ抽出文字列のcontrolが効かないため利用を諦めました。

そこで、似たようなタスクを行っているOpen Source Projectの実装が先日Googleよりリリースされていたのをおもむろに思い出しました。LangExtractは先日Googleよりリリースされた「非構造データから構造化データをLLMを使って抽出する」ライブラリで、例えば構造化されていない医療データから薬剤名をcitation付きで抽出するなどといった利用方法が想定されています。この実装を参考に、タスクを少し分解することにしてみます。

LLMには「ある文書からユーザの入力に関係する箇所を抽出する」タスクのみを行わせて、文字位置特定はdeterministicなcodeで行うことにします。文字位置特定の際に、元の文書との完全一致が必要なためPromptも少し改善します。また、後々の利用シーンを考えて抽出の仕方も少し追加指定します。

あなたはある文書からユーザの入力に関係する箇所を抽出するエージェントです。

* ユーザの入力に関連する文字列をから抽出してください
* 複数箇所該当する場合は複数箇所を抽出してください
*** に含まれる文字列を絶対に改変せずにそのまま抽出してください
* 関連する文字列は文章の途中で区切らずに抽出してください**
* 関連する文字列の抽出が難しい場合は`errorMessage`にその理由を出力してください



下記のフォーマットで出力してください。


[関連する文字列]


[抽出に失敗した時の失敗理由]



[実際の文書を入れる]

この結果として抽出される文字列を配列として受け取り、元の文書に対してsubstring一致で検索することで完全に正確な文字列位置を取得することができます(↑のPromptでは結果をXML形式で出力していますが、もちろんJSON出力にて利用する形でも問題ありません)。

このように問題を分解し、LLMに解かせるタスクを小さくかつLLMが得意な内容に絞り込み、LLMに解かせる必要のない問題をdeterministicなcodeで解くことで、LLMの柔軟性を享受しつつ安定した動作が可能になります。文字列抽出部分をLLMで行っているため、抽出する文章の条件や境界などはかなり柔軟に指定することが可能な一方で、文字位置の抽出は文字列の抽出さえ行えていれば失敗することはなくなるため、evaluationの工数も下げられます。

ただし、文字列抽出まで問題を小さくしたとしても、LLMは元文書の文字列を100%正しく出力できるわけではありません。そのため、なるべく100%に近く文字列を維持するためのPrompt Engineeringに加えて、多少間違えたとしても文字位置を割り出すために工夫を入れたくなります。

改めてになりますが、langextractは先日Googleよりリリースされた「非構造データから構造化データをLLMを使って抽出する」ライブラリで、例えば自然言語で構造化されていない医療データから薬剤名をcitation付きで抽出するなどといった利用方法が想定されています。

  1. Precise Source Grounding: Maps every extraction to its exact location in the source text, enabling visual highlighting for easy traceability and verification.
  2. Reliable Structured Outputs: Enforces a consistent output schema based on your few-shot examples, leveraging controlled generation in supported models like Gemini to guarantee robust, structured results.
  3. Optimized for Long Documents: Overcomes the “needle-in-a-haystack” challenge of large document extraction by using an optimized strategy of text chunking, parallel processing, and multiple passes for higher recall.

実装の大まかな方針を知りたいため、おもむろにClaude Codeで調べてみます。

ちょっと読みづらいのでおもむろにClaudeに突っ込んでVisualizeしてもらいます。

ということで、Step4の部分を見てみると、LangExtractではLLMが抽出した文字列が元の文字列と多少ずれることを想定して、元文字列との比較をするときにfuzzyな手法でのマッチングも可能にしています。さらに、LLMが文字列の抽出自体に失敗する可能性も考慮して、複数回抽出を実行しrecallを高める仕組みも揃えています。

繰り返しになりますが、このようにLLMの解かせるタスクを絞り込むことでLLMの柔軟性を享受しながらも、LLMの弱い部分や確率的な振る舞いに基づく失敗をリカバリする仕組みをdeterministicなcode側で補完することで、より安定稼働するアプリケーションに近づけられるのではないかと思います。

ここまで具体的なContext Engineeringの過程を「あるルール文書があったときに、あるクエリに関係する文章をルール文書内から位置付きで抽出する」というタスクを例にとって見てきましたが、改めてContext Engineeringはなんぞやという整理をしたいと思います。色々な形での整理が考えられると思いますが、今回は1つの切り口として以下の分解方法をしたいと思います。

(1)1つのLLM処理に任せるタスクの範囲を絞る

LLMの柔軟性に任せて大きなタスクを丸ごとLLMに処理させるのは一見魅力的に見えます。LLMの柔軟性・処理能力をフルに活用でき、将来的な変更にもPromptの変更のみで行いやすそうで、必要なcodeの量も減ります。モデルの進化速度も踏まえて、不必要にcodingを行わずにLLMに任せた方が良い気もしてきます。

ですが、このやり方ではLLMの出力を安定させるのは相当に難しく、デモだと良くてもプロダクションでユーザに受け入れられるレベルの安定性を出すまでの道のりは遠くなります。このため、タスク全体を分解しながらLLMに任せるタスクのサイズや内容を適切に絞り込んでいくことが重要になります。ただし、細かく区切りすぎても得られる安定性の度合いに対してcode部分の実装が複雑になりすぎ、将来的な変更が不必要に重たくなることもあるため、1つのLLM callに任せるタスクの粒度を見極めることは重要になります。LLMの柔軟性を失わないように設計することも必要になります。

ただベーシックなところで、LLMに吐かせる必要のないIDまでLLMに吐かせたりしない(APIに投げるパラメータを作る際に、実行時に確定しているuserIdをLLMに出力させずに、deterministicにくっつける)など迷うことなくやるべきタスクの絞り込みもあります。

(2)任せたLLMのsteeringを実験的に行う

いわゆるPrompt Engineeringを初めとして、LLMの出力を望む方向にsteeringしていく活動になります。想定される入力値を使い実験的にPromptの変更と結果の観測を繰り返しながらも、再現性を持たせながらアセットとして積み上がるようにevaluationのdatasetを作りながら進めることが前提になります。具体的な入力値と期待される出力値がないと評価ができないためドメイン知識をより一層深く理解したり、evalsの粒度感を決めたりといったPrompt Rngineering以外の周辺作業も広義の意味では入るのではないかと思います。

実際のところは、この作業過程で(1)の過程に問題が見つかることもあるため、(1)と(2)を行き来しながら作業を進めていくことがほとんどではないかと思います。

そして、これらの活動両方を「LLMの能力(得意・不得意)を解像度高く経験的に理解し、LLMの都合に合わせながら」行うことが重要だと感じます。これまでの内容を見て「これまでのSoftware Engineeringと同じような思考性」でのEngineeringだと感じた方も多いのではないかと思います。複雑なタスクをどうシンプルなタスクに落とし込むなどComputer Science的な思考性が共通しているのは間違いありません。一方で、LLMの挙動はこれまでの(多くの)Software Engineeringと違い「確率的な振る舞い」との向き合いがその中心にあります。またその挙動はルールを理解すれば確定的に決まるものではなく「経験的に」しか知ることができません(おまけにこの挙動はモデルごとに異なります)。特にBtoB SaaSなど安定した挙動が求められえるプロダクトにおいてはこのLLMの特性を軸にSoftwareを組み立てていくことが欠かせません。

Context Engineeringからは少し外れるトピックかもしれませんが、LLM処理を含んだSoftwareにおいてレイテンシの大部分はLLMのtoken出力部分になります。そのため、パフォーマンス改善一つをとっても、LLMのautoregressiveな出力の概念やKV-Cacheの動作の理解した上で、これまでのEngineering概念でのチューニング(並列化など)を行うことが必要になります。

また、複数のLLM Callを組み合わせたpipeline(Multi Agentと呼ばれるものも含む)を開発するときもSoftware Engineering的な責務の切り分けを軸としたり、メンタルモデルとして妥当性のある分解(例:「Manager」とか「Editor」とかで区切る)ではなく、LLMが動作しやすい単位 = Contextをどう分けるとLLMの柔軟性を保ちながら安定して挙動できるか、というのがまず最初の基準になります。

LayerXではLLMが前提になった世界での経済活動のデジタル化や摩擦の解消へ真正面から向き合うべく、LLMを活用したAI Agentの開発に真正面から向き合って取り組んでいます。今回紹介したような内容以外にも、AI Agentを開発する中で出てくる下記のような課題を、プロダクショングレードのプロダクトを作る過程で1つずつ向き合いながら対応していってます。

  • evaluationはどの粒度で行うべきか?evaluation環境をどう構築するか?
  • Context Rotがある中でContextをどこまで使うべきか?
  • 求められるContext Engineeringに既存frameworkの実装が追いつかない中でどうするか?
  • Tool承認やHuman as toolとしてのHITLの実装
  • Agentの認可・認証はどうすべきか?
  • 既存のSaaS群とのintegration

そして、これらの課題に向き合う仲間を全力で募集しています。少しでもこれらのLLM活用に興味のある方がいればぜひ一度お話をさせてください!また、LLMやAIに興味はあってCoding Agentやモック制作にAIは使っているけど、作る側での知識や経験がないEngineerやPdMの方も多いのではないかと思いますが、そういった方でもぜひお声がけください。AI関連の技術は進歩が急激ではありますが、そもそもまだほとんど経験者のいない新しい技術です。AIへの圧倒的な好奇心を持って試行錯誤ができる方であればAIを利用したプロダクトの開発経験は問いません。

ここまでの速度で技術が発展し、全く新たなプロダクトを生み出せる機会というのは10年に一度しかないと思います。この激動の時代をまたとない機会と捉えて集中しつつも、全力で楽しんでLLMに向き合える方をお待ちしています!

jobs.layerx.co.jp

また、この記事はLayerXエージェントブログリレー2日目の記事です。
明日以降も毎日AI Agentに関する記事が公開されますので、LayerX Tech公式Xアカウント私のXをフォローしてチェックいただければ幸いです!




Source link

関連記事

コメント

この記事へのコメントはありません。