Ricardo Ferreira、本番級 AI アプリケーションのための文脈エンジニアリングを解説
本文の状態
日本語全文を表示中
詳細モードで約45分の本文を読めます。
同じ出来事の情報源
この情報源を基点に整理
InfoQ AI/ML
本番環境向け AI アプリケーションには、プロンプトエンジニアリングだけでなくコンテキストエンジニアリングが不可欠である。
AI深層分析を開く2026年9月2日 20:37
AI深層分析
キーポイント
長短期メモリの統合戦略
Redis を活用して長期記憶と短期記憶を効果的に統合し、LLM の文脈管理能力を向上させる手法が紹介される。
トークン制限とコスト管理
要約機能によるトークン数の制御や、再ランキング・セマンティックキャッシュを用いたコンテキストローテーションの防止策が提案されている。
厳格な制約下での実装
API コストの指数関数的増加を抑制しつつ、厳しいレイテンシ要件を満たすための実用的なアーキテクチャが議論される。
実践的な教訓の共有
発表者は特定のソリューション開発における成功と失敗の両方を含む教訓を、ストーリー形式で語る予定である。
Alexa デバイス設定との関連性
発表者が構築したアプリケーションは、Alexa デバイスのセットアップに関連する物語を持っている。
重要な引用
Ricardo Ferreira discusses moving beyond simple prompt engineering to build production-grade AI applications.
He shares practical architectural strategies for integrating long-term and short-term memory using Redis.
This presentation is actually going to be a mix of lessons learned when I've developed a specific solution that I'm going to talk about. I learned the easy and the hard way.
Also, it's going to be a mix of storytelling, because these applications that I've developed actually have a story.
編集コメントを表示
編集コメント
InfoQ の QCon AI イベントで発表されたこの内容は、理論的な議論を超えて現場のエンジニアが直面する具体的な課題への解決策を提示している。Redis を活用したアーキテクチャパターンは、大規模 LLM アプリケーションの設計において即座に参照できる実践的な指針となるだろう。
Source Article
元記事を日本語で読む
本文に関係しない購読案内、埋め込み通知、サイト内プロモーションは除いています。
InfoQ ホームページ
プレゼンテーション一覧
プロンプトを超えて:本番環境向け AI のためのコンテキストエンジニアリング
プレゼンテーションを見る
再生時間:
ダウンロード
47:34
/presentations/context-engineering-redis-llm-architecture/en/slides/slide-1787815892133.jpg)
概要
Ricardo Ferreira は、単なるプロンプトエンジニアリングの域を超え、本番環境で動作する AI アプリケーションを構築するための実践的なアプローチについて語ります。Redis を活用した長期・短期メモリの統合戦略や、要約機能による LLM のトークン制限への対応、再ランク付けとセマンティックキャッシュを用いたコンテキストの劣化(コンテキストロト)対策、そして厳格なレイテンシ要件下で API コストが指数関数的に増大するのを抑える手法など、具体的なアーキテクチャ戦略を共有します。
登壇者紹介
Ricardo Ferreira は Redis のシニア・デベロッパー・アドボケートを務めています。分散システム、データベース、ソフトウェア開発の分野で 25 年以上の実績を持ちます。キャリア初期はソフトウェアエンジニアリングとデベロッパー教育に注力していましたが、その後ソリューションアーキテクチャへと転身し、データ集約型アプリケーションの設計、構築、展開を顧客支援してきました。
コンファレンスについて
QCon AI は、これらのワークロードを安全にスケールさせるために必要なエンジニアリング分野に特化した、実践者主導のイベントです。本番環境で同業他社が採用しているアーキテクチャのプレイブックや、失敗から得たメトリクスへの直接アクセスを提供します。
INFOQ EVENTS
2026 年 9 月 17 日午後 1 時(EDT)## PR の向こう側:アジェンシー型ソフトウェアデリバリーの新たなコントロールプレーン 登壇:モヒト・スーマン(Harness シニアプロダクトマネージャー)
2026 年 10 月 8 日午後 12 時(EDT)## AI が開発を加速させる時代、CI パイプラインは追いつけるか? 登壇:エリック・メタジ(Datadog ソフトウェアデリバリープロダクトマーケティングマネージャー)、ロヒン・チャンドラ(Datadog CI/CD最適化プロダクトマネージャー)
トランスクリプト
リカルド・フェレイラ: こんにちは、リカルド・フェレイラです。今日は「コンテキストエンジニアリング」についてお話しします。このプレゼンは、私が実際に構築した特定のソリューションを通じて得た教訓を交えた内容になります。成功の秘訣だけでなく、苦労して学んだ失敗談も含まれます。また、物語として語る部分もあります。なぜなら、私が開発したアプリケーションにはそれぞれにストーリーがあるからです。
例えば、Alexa デバイスのセットアップをしている最中に、自分が作ったものとの関連性に気づきました。私は Redis で働いています。
初期の実装
私が実際に何を作ってきたのか、そのストーリーをお話しします。そうすれば私の背景がご理解いただけるでしょう。
私は Redis のチーム内で「開発者体験(Developer Experience)」という部署に所属しています。私たちが約 2 年前から取り組んできたプロジェクトの一つが、Redis を基盤としたオープンソースプロジェクト「Agent Memory Server(AMS)」の構築です。
このプロジェクトの目的は、Redis の上に非常に高速なメモリレイヤーを構築し、開発者がその上で短期記憶と長期記憶を実装できるようにすること。そして、より自然で人間らしい対話を実現するための基盤を提供することでした。
このプロジェクトを開発したチームには、同様に「このオープンソースプロジェクトを実際のアプリケーションに活用する」という要請も寄せられました。私たちは OSS として公開すると同時に、社内でテストを実施しました。その際求められた要件は、「この種のオープンソースプロジェクトに負荷をかけられるようなもの」を作ることでした。
私は過去にアレクサのスキル開発に関わってきた経験があります。その中で、アレクサスキルを開発する際には非常にユニークで興味深い課題があることを確信しています。その一つが、アレクサとのインタラクションには8秒という固定されたタイムアウトが存在することです。この時間枠内に必ず応答する必要があります。
そこで私は、この制約をオープンソースプロジェクトでも活用できないかと考えました。これが最初の動機です。2つ目の動機は、今ご覧いただいているスライドにある通り、過去にアレクサデバイスを利用したことがある方の中で、実際に知識を得る時間よりも「知らないこと」に時間を費やしている方が圧倒的に多いのではないかという点です。
もし今、私のアレクサが準備できていれば、すぐにテストしてみましょう。
リカルド・フェレイラ: 皆さん、同じ話を何度も聞かされた経験はありますか?実は私にもこんなアイデアがありました。もしアレクサの背後に「脳」を置いたらどうなるだろう?あるいは、大規模言語モデル(LLM)をアレクサの裏側に配置したら?そこで私が作ったのが、「マイ・ジャ维斯」というスキルです。名前の由来については後ほどお話しします。
仕組みはこうなっています。マイ・ジャ维斯はあくまで「スキル」の一つです。ここではアレクサデバイスが中心となり、この対話を支えるのは Lambda 関数です。通常、Alexa スキルのバックエンドはこのように構成されます。そして、その Lambda 関数の上には2つのレイヤーを重ねています。
一つ目は、Alexa スキル公式の SDK です。これは Alexa からのインタラクションで送られてくる JSON ペイロードを包み込む、いわばラッパーのようなものです。二つ目には、私が使っている「LangChain4j」というフレームワークがあります。これは LangChain フレームワークの Java 版ですが、厳密に言えば LangChain プロジェクトの一部ではありません。実際には、LlamaIndex や Spring AI と並ぶ、より複雑なフレームワークの一つです。
このようにして LLM をバックエンドとして支え、今回は OpenAI のモデルを使用しました。その結果、実際に「Alexa、マイ・ジャ维斯にコンテキストエンジニアリングとは何かを説明して」と話しかけることが可能になりました。
Alexa: コンテキストエンジニアリングとは、システムがコンテキスト情報を理解し、解釈し、それに応答するよう設計するための芸術であり科学です。これは、AI アシスタントのような機械が、ユーザーとのやり取りや環境からの手がかりから関連データを処理・優先順位付けできるようにするアルゴリズムやモデルを作成することを含みます。この分野は、ユーザーの意図と状況的文脈を理解することで、パーソナライズされた正確な応答を提供できる知的システムを開発するために不可欠です。
Ricardo Ferreira: 非常に洞察に富んだ回答ですね。
コンテキスト問題の多様なタイプ
この開発を始めてから、私はそう感じました。名前の「My Jarvis」はマーベル・ユニバースの有名な AI キャラクターに由来しており、私も大のマーベルファンです。
開発に熱中するにつれて、より多くの質問、より詳細な質問をするようになり、そのスキルを日常生活に取り入れようと試みました。しかし、気づいたのは、このスキルは確かに回答を引き出すことはできるが、それは非常に簡単なことだということです。実際に LLM(大規模言語モデル)に問い合わせると、ほぼあらゆる質問に応答できてしまいます。
重要なのは人間側の側面です。質問をする際の態度や、モデルと呼ばれるものとの対話における振る舞いこそが、多くの時間を占める要素なのです。最初はそうしたやり取りの経験がおかしいと感じましたが、次第に自分が「コンテキスト・ポイズニング(文脈汚染)」という一連の問題に直面していることに気づきました。これは最初は何の意味もなさそうに見える事象や、「コンテキスト・ディストラクション(文脈の混乱)」、つまり必要以上に多くのデータや情報を処理しようとして、適切な回答を導き出すための時間を浪費してしまう現象です。
さらに「コンテキスト・コンフュージョン(文脈の混同)」もあります。これは無関係なデータを無理やり組み合わせた結果生じる問題です。また、有名な問題として、「コンテキストが大きくなるほど、その質は低下する」という現象も挙げられます。最終的には「コンテキスト・クラッシュ」が頻発します。これは「これが真実だ」「これが偽だ」といった情報の混同や、回答のバージョン管理とタイミングの不一致などによって引き起こされます。これらのデータの変異が重なり合い、非常に質の低い回答を生み出してしまうのです。
新しいモデルを解決策として使うことへの疑問
この場では確約はしませんが、このような状況に直面した際、多くのソフトウェア開発者が最初に抱く直感は「バックエンドが問題なのではないか」というものかもしれません。もしバックエンドが機能不全を起こし、処理能力が追いついていないなら、どうすればよいでしょうか?おそらく「改善する」か「変更する」と考えるでしょう。かつてオンプレミスのリレーショナルデータベースを使っていた時代と同じ発想です。オンプレムのリレーショナルデータベースが負荷に耐えられなくなったとき、私たちは何をしたでしょうか?CPU やメモリを増やしたり、サイドキャッシュを追加したりしました。
私は ChatGPT に対してより新しいモデル(例:5.4)を試してみましたが、その結果、コストは増大する一方で期待したような効果は得られませんでした。私の仮説では、「より新しく、コンテキストウィンドウが広いモデルを使えば、より詳細な回答が得られるはずだ」というものでしたが、これは残念ながら誤りでした。モデルを変更しても、実際のユーザー体験には何の変化もなかったのです。
ここから得られた最初の教訓は、AI 実装において「常に最新モデルへのアップグレードが問題解決につながるわけではない」という事実です。私のケースでは、それが解決策になることは決してありませんでした。
エージェントには文脈が必要
私が実際に必要としていたこと、そして当時は気づいていなかったことが「コンテキストエンジニアリング」です。この実装が始まったのは約一年半前でした。
その当時、「コンテキストエンジニアリング」という用語はまだ一般的になりつつありましたが、私は最近まで「ハッチェンエンジニアリング(Harness Engineering)」という概念にもようやく追いついたところです。当時は、ソフトウェアエンジニアとして直面するさまざまな課題を一つひとつ実用的に解決する必要があり、それらを直感的なアプローチやチームとの協働で対応していました。
しかし、「リカルドさん、その做法には名前がありますよ」と指摘されました。「コンテキスト・リーランキング(reranking)」などです。私の背景は AI エンジニアリングではなく、ソフトウェアエンジニアリングにあるため、AI 界隈では一般的とされる多くのプラクティスが私にとっては新鮮で、知られていなかったのです。
私が実際にこれらの問題を修正する際に費やした時間のほとんどは、直感的な対応に充てられていました。業界全体ではなく、私個人(リカルド)に尋ねるなら、私が感じたことを整理し、文脈エンジニアリングという概念を以下のように定義しました。
これは単なるプロンプト作成を超えたものです。かつて私は「エージェントがより明確かつ賢く思考できるよう、洗練されたシステムプロンプトを作成しよう」という直感に走りました。しかし、それは別の誤りでした。私が最終的に作り出したのは、それぞれの悪い体験を実用的に解決するための、非常に意図的で精巧なアプローチのシリーズです。
これが文脈エンジニアリングの本質だと私は考えます。アレクサが先ほど提示したような、より概念的で洗練された回答が存在することは承知しています。しかし、私がその時点で感じたのはまさにこのことです。
第1章:各課題の実用的な解決
本発表では2つの章に分けて解説します。最初の章では、私が直面した個々の問題と、それらをどのように解決し、そして何が「易しい道」と「困難な道」から学んだ教訓なのかを順を追って説明します。
第2章ではコストについて議論します。文脈エンジニアリングを導入することで生じる、付随的なコストです。これは、アプリケーションで文脈エンジニアリングを実践として導入しようとする方々にとって、私が最も強調しておきたい重要な注意点です。必ずコストがかかることを理解しておく必要があります。
私が最初に直面した問題の一つは、おそらく皆さんにとって自明のことですが、LLM は「今日が何月何日、何時か」を判断するのが苦手だということです。LLM を使った経験がある方の中で、「2021 年だから」という理由で回答を鵜呑みにしてしまったという方はいますか?実際には、私たちは 2026 年に生きています。2021 年ではありません。
私が最初に着手したことのひとつは、リマインダーシステムの構築でした。自分のスキルを活用して自分自身に何かを思い出させる機能を実装したいと考えたのです。
では、ここでテストしてみましょう。「アレクサ、マイ・ジャ维斯 に、2 分後に挨拶をするよう覚えておいて」と伝えてください。
Alexa: 「2 分後に挨拶をする必要があることをメモしました。リマインダーを設定しましょうか?」
Ricardo Ferreira: 「はい、お願いします。」
今お見せしているのは、Redis 内部に保存された実際の LTM(Long-Term Memory:長期記憶)です。この長期記憶は、あなたが保持すべき実際の情報や記憶であるテキストと、その背後でベクトル検索が行われるためのテキストベクトルの組み合わせによって構成されています。
ここで重要なのは、LLM(大規模言語モデル)には時間という概念が最初から備わっていない点です。これをどう解決するか?ツールを使うことです。私は日付や時刻に関する文脈を LLM に教えるために独自のツールを作成しました。これにより、正確な日付と時間を記憶できるようになります。
特に「来週の水曜日の予約を思い出させて」といったリマインダー機能が必要になった際、この仕組みの重要性はさらに高まります。
LangChain4j を使ったことがない方のために説明すると、LangChain4j でエージェントを作成するのは非常にシンプルです。モデルとツールのリストを関連付け、構築するだけで AI サービス「ChatAssistant」が完成し、そこからクエリを実行できます。これだけです。
こうして実際に動き出すと、私がプレゼンテーションを行うべき正確なタイミングで、リマインダーが適切に表示されるようになります。
次に、私が直面した2つ目の問題についてお話しします。これは、過去の質問を記憶し続ける必要があるような問いかけをした際に生じたものです。
例えば、「はい、私の名前はリカルドです」と答えた後、「リカルドさん、はじめまして」と言ったり、「私の名前を知っていますか?」と聞いたりすると、「申し訳ありませんが、私はあなたを存じ上げません」と返されてしまいます。これは、まだ短期記憶のコンテキストを導入する前だったからです。
おそらく一部の皆さんには聞こえていないかもしれませんが、このやり取りから約2分後に「リカルドさん、はじめまして」というメッセージがポップアップしました。私の Alexa デバイスを事前に設定していれば防げたはずですが、その時は準備不足でした。
もう一つの気づきは、LLM(大規模言語モデル)はステートレスであるという点です。これは誰もが理解している当たり前の事実でしょう。
そこで私は、短期記憶をサポートするオープンソース製品「Redis Agent Memory」を使用しました。この機能はセッションに限定されたものであり、「Session Memory」と呼ばれています。その名が示す通り、寿命が短く、TTL(Time To Live)ベースで管理されるものです。
LangChain4j では、基本的には ChatMemoryStore というインターフェースを実装し、任意の永続型ベクトルストアを作成します。私はこれをラッパーとして実装し、MCP または REST APIs を通じて呼び出せるようにしました。当初は REST APIs を使用していましたが、後に MCP に切り替えました。
この Session Memory の仕組みについて、開発者である皆さんには具体的なイメージを持っていただければと思います。画面をリフレッシュすると、これまでに Alexa と行ったすべてのやり取りが表示されます。ご覧の通り、これは JSON ペイロードとして構成されています。
これは、常に追加され続ける JSON ペイロードです。これが裏側で動作するセッションメモリの様子であり、Redis エージェントメモリアーキテクチャの核心を要約したものです。
この仕組みは Redis の上に薄いラッパーを実装し、背景では LLM を活用して短期記憶から長期記憶を生成します。会話を続ける限り、Redis エージェントメモリは常に LLM を使用し続けます。ここで重要なのは、これを自動的に長期記憶へ昇格させる点です。
LangChain4j との連携方法もこの仕組みに含まれています。特筆すべき実装として、ユーザーとシステム間で交わされた直近 10 メッセージをいつでも取得できる ChatMemory の実装があります。これにより、私が以前述べたような「名前を覚えてくれる」機能を実現し、記憶が定着する様子を確認できました。
この ChatMemory の実装において、最初に気づいたのは LLM がツール間で双方向の呼び出しを行うという点でした。その実装が原因で、すべてのツールを無限ループで呼び出すバグが発生していました。
ご想像のとおり、Alexa のタイムアウトは 8 秒です。しかし、この無限ループにより処理時間が 10 秒を超えてしまいました。そこで、メッセージ間の単純なパススルー機能を持つ「WorkingMemoryChat」という独自の実装に置き換える必要がありました。
旧実装では直近の 10 件のメッセージをバッファとして保持しようとしており、ChatMemory が無限に成長し続ける問題がありました。新しい実装は、この無限ループの問題と、セッションメモリが TTL(Time To Live)で管理されることで会話履歴が永遠に残ってしまうという二つの問題を同時に解決しました。
私の設定した TTL は 5 分です。直近の会話を覚えておくには、5 分という時間は適切な時間だと言えます。
では、5 分後に Alexa に「私の名前は何か」と尋ねたとしましょう。すると Alexa は「知りません」と答えます。TTL(有効期限)が切れてしまったため、もはや誰であるかの記憶が残っていないからです。この出来事がきっかけとなり、私は長期記憶(LTM)の実装を始めるようになりました。文脈(コンテキスト)にも長期記憶を活用する必要があります。
LangChain4j や、Python 版の LangChain でも同様に実装できる方法として、「ContentRetriever」という概念があります。これは RAG(検索拡張生成)に直接対応する抽象的な実装で、入力を受け取ると一連の結果を返す仕組みです。私は独自に getLongTermMemories を実装し、Redis Agent Memory と通信可能なメモリサーバーを作成しました。
結局のところ、これは単純なベクトル検索(セマンティック・サーチ)の呼び出しに過ぎません。以下がプロンプトの内容です。「このクエリにベクトル検索レベルで一致する上位 k 件を返してください。HNSW インデックスタイプを使用し、スケーラビリティが高く効率的である一方、メモリ使用量も抑えられるようにしてください。」
直近の記憶をすべて取得して関連付け、より共感できる回答を提供できるようにします。例えば、「来週に歯科の予約がある」という情報を基に、数分後に同じ質問を繰り返しても「はい、来週に歯科の予約がありますね」と一貫した回答が返ってきます。
デバイスに情報や機能を追加するにつれて、私の体験がどのように向上していくかお分かりいただけたでしょうか。しかし、これは最初から無料で得られるものではありませんでした。当初は単純な実装で、LLM(大規模言語モデル)を配置すればすべて任せられると考えていました。
より人間らしい体験を提供し始めると、すべての要素を意図的に設計する必要があることに気づくはずです。それは無償では実現できないのです。
この経験から得た教訓の一つは、まず自宅内でこの Alexa スキルを使い始めたことです。息子の妻も自分たちの思い出や対話のために使い始めました。そこで必要になったのがマルチテナンシーの対応です。
最終的に私が採用したのは、AI 業界で「ポストフィルタリング付きベクトル検索」と呼ばれる手法です。これはまずベクトル検索を実行して一連の結果を取得し、その中からメタデータ(ここではテナント ID や所有者 ID)に基づいて後処理を行うというものです。Alexa に話しかけると、音声や顔認識によって誰かが検出され、内部でその人物に割り当てられた ID がこのフィルタリングに使われます。
これをやらなければなりません。なぜなら、私が尋ねた質問が、妻や 15 歳の息子(最近ではかなり危険な存在です)の思い出によって誤って回答されてしまう可能性があるからです。これらのテナントを明確に分離する必要があります。
次に、おそらく非常にシンプルな概念ですが、「トップ K」はいくつに設定すべきかという点です。5?10?15?適切な回答を生成するために必要なメモリ量はどれくらいでしょうか。この判断が、私が最終的にこの手法を採用した理由です。
また、すべての機能が記憶機能に依存する必要があるわけではありません。私が言いたいのは、これは具体的なユースケースの一つだということです。
「お父さん、あなたが頻繁に変更しているガレージドアのコードを Alexa に教えることはできる?」
このようにするにはどうすればよいでしょうか?一つの方法は、自分専用の記憶を作成することです。「Alexa、これを覚えていて」と指示するのです。あるいは、家に関わるすべての事項に関する一連の指示を PDF 文書にまとめて埋め込むことも可能です。
私は最終的に「ナレッジベース(知識ベース)」と呼ばれるものを構築しました。これは長期的な記憶の新たなカテゴリと言えます。ナレッジベースを実装するには、もう少し複雑な運用が必要です。
具体的には、2 つのリトリーバー(検索機構)を用意します。1 つはユーザー固有の長期記憶を検索するためのものであり、もう 1 つは一般的なナレッジベース全体から情報を取得するためのものです。
スライドの長さの関係で簡略化されていますが、本来は各ユースケースが何を行うのかをより詳細に記述した説明が含まれます。冗長になることは承知していますが、それぞれの機能について明確な記述が必要です。
ここで示すように、私は LinkedIn 上で旧世代の LLM を活用し、ユーザーからの質問に応じて「ユーザーの記憶情報」から取得すべきか、「ナレッジベース」から取得すべきかを判断させています。これは質問の性質に依存するものです。
例えば、「タッチパッドによるワイヤレスガレージドアの開閉方法」を尋ねられた場合、LLM は「コード 7121 でガレージドアを開けられます」というナレッジベースからの回答を返します。ただし、このコードは既に更新済みであり、セキュリティ上の懸念はありません。
これは興味深い事例です。まず、LLM を単なる質問応答ツールとしてだけでなく、継続的な対話を支えるサブシステムとして活用している点が注目されます。ここでは LLM への直接呼び出し(インラインコール)を行い、ナレッジベースの参照を最小限に抑えています。
つまり、実装においては、単に機能を実装するのではなく、意図的かつ巧みに設計することが不可欠であるという教訓が得られます。
もう一つのユースケースは非常に興味深いものでした。
「マイ・ジャービス、先週会ったジョン・ドウについて思い出して」と尋ねると、「ジョン・ドウは〜」と回答が返ってきます。その後で「彼はどこに住んでいるの?」と続けた場合、これは対話が続いている最中の質問です。
人間であれば瞬時に「先ほど言ったジョン・ドウのことだ」と理解できるでしょう。しかし、Alexa のようなシステムでは「誰が住んでいるのですか?私が先ほどジョン・ドウについてお話ししたばかりですよ」といった混乱が生じます。
この種の混乱は、単なる記憶の問題ではありません。LLM(大規模言語モデル)が文脈に基づいてどのような関連付けを行うかという問題です。
LangChain4j の用語で言えば、これは「クエリ変換器の圧縮(compression of a queryTransformer)」と呼ばれる技術を使用していることに気づきました。実際の LLM にプロンプトを与えれば、システムはこのような処理を自律的に行うことができます。つまり、「彼」という言葉が文脈の中で「ジョン・ドウ」を指すように、その場で置換や関連付けが行われるのです。
これは文脈エンジニアリングの優れた事例の一つです。もう一つの手法が「圧縮」ですが、人間にとっては自然な行為でも、AI サービスと対話する際にはこれを教える必要があります。私は LangChain4j で事前構築された機能を用いて、この実践を実装しました。
例えば、「私の好きな色は何ですか?」と尋ねてみてください。すると「あなたの好きな色は黒です」という回答が返ってきます。これは事実です。また、「Java でコードを書くのが好きですね」とも言われます。なぜこのように答えたのでしょうか?私が以前説明した通り、ベクトル検索では質問に答える前に上位 k 件の結果を抽出する仕組みになっています。
ベクトル検索は完全一致の検索ではありません。その名称自体がすべてを物語っています。実際に「好きな色」について尋ねた時点では、「好きなプログラミング言語」と「好きな色」の 2 つのエントリが関連していたのです。
これら 2 つの項目は結果として取得され、LLM が回答を生成する際のコンテキストの一部となりました。LLM を責める気はありません。非常に完成度が高く、詳細な回答を提供してくれました。
「Java でコードを書くのが好き」「黒色が好き」という点については、人間が直面する課題として解決する必要があります。私はこの問題をどのように解決したのでしょうか?
モデルは取得した情報をすべて出力しようとする傾向がありますが、もう少し選別性を持たせる必要があります。私が採用したのは、コンテキストエンジニアリングの文脈では必ずしも一般的ではありませんが、AI 全体や AI エンジニアリングの分野でよく使われる手法です。
それは「フューショット・デザイン(few-shot design)」と呼ばれる技術です。これは、システムプロンプトの形式で、正例と負例、あるいは矛盾する事例といった一連のサンプルを LLM に提示することで、設計中の特定のドメインモデルにおけるユーザーの意図をより明確に伝えることを可能にする手法です。
まず何より、構造化されたコンテキストを少しだけ活用する必要がありました。LangChain4j では、この機能はコンテキストインジェクターを使って実現できます。これにより、構造化された入力を作成することが可能です。
実際には多くのパターンがありましたが、私の元の実装では約 17 種類作成しました。もちろん、これらすべてをスライドに載せることはできませんが、一例として挙げましょう。「質問に対してどのプログラミング言語を使うべきか?」という問いに対し、コンテキストには「好きな色」「誕生日(10 月 5 日)」「Java を好む」といった情報が含まれます。すると、LLM はこれに基づいて適切な回答を選択します。
これは LLM に対して、「どう考えるか」だけでなく「どのように振る舞うか」を示すヒントのようなものです。つまり、行動の設計(behavioral engineering)や few-shot の活用と言えます。few-shot を実装することで、より客観的な回答が得られるようになりました。これが私の目指していた成果につながったのです。
LLM が時々陥る別の重要な混乱の例として、"Alexa、今週の優先事項は何?"や"今の優先事項は?"といった問いへの回答が挙げられます。例えば、"JSON のサポート追加と歯科医院への予約"を優先事項と答えた場合、人間であれば"PR(プルリクエスト)"という言葉から文脈を理解し、"JSON サポートの追加"こそが真の優先事項だと推察するはずです。なぜなら、"歯科医院に行くこと"はプルリクエストとは無関係であり、単に歯科医院へ行くだけでプルリクエストを開くことはあり得ないからです。
このケースでは、文脈自体の意味(セマンティック・意味)を教える必要があり、これも私が取り組むべき課題の一つでした。ここで採用したコンテキストエンジニアリングの手法は、"reranking"(再ランク付け)です。
これは非常に一般的な技術です。私がリランキング(reranking)を採用した理由は、トップ k 件の結果から取得された複数のメモリを扱う前に、LLM に重要度や優先順位に関する推論を行わせる必要があったからです。
この課題には、Few-shot テクニックで解決しました。コンテキストに含めるべき正しいメモリーが複数存在する場合があり、それらには明確な優先順位をつける必要があります。これがリランキングと呼ばれる処理の本質です。
幸いなことに、LangChain4j もリランキングをサポートしています。基本的にはスコアリングモデル(scoring model)をインスタンス化するだけで済みます。今回は Cohere を使用しましたが、当時はこの機能を実装し API で提供していた唯一のオープンソース LLM モデルでした。現在は状況が変わっている可能性もありますが、Cohere は非常に高性能でコストも極めて安いため、引き続き利用しています。
この結果、私は「ContentAggregator」という仕組みを思い浮かべました。名前の通り、コンテンツを集約する役割です。
実はこれら一連の処理は、実行中(in-flight)に起こっています。LLM に対して実際に質問への回答を依頼する前段階で、すべてが完了しているのです。
具体的には、ベクトル検索から上位 k 件の結果を取得し、再ランク付けを行い、80% という最低スコア閾値を満たすものだけをフィルタリングする「ContentAggregator」を作成します。この閾値は私が手動で調整したもので、当初はさまざまな数値を試しました。
こうして私のパイプラインには、「コンテキストインジェクター」が追加されました。また、クエリを圧縮するための「QueryTransformer」、知識ベースと長期メモリのどちらを使うかを判断する「QueryRouter」、そして再ランク付けを行う「ContentAggregator」も備えるようになりました。
これらすべては、「LLM に質問に答えてほしい」という単純な仮定から始まったものです。
これは非常に良い例であり、私がこのプレゼンテーション全体で強調したい点です。コンテキストエンジニアリングを適用する際は、すべてのプロセスが意図的・計画的に行われなければなりません。実装においても、各要素はすべて意図を持って組み合わされる必要があります。
再ランク付け(reranking)という手法を実装した結果得られた回答が、まさにその例です。この手法はコンテキストエンジニアリングの分野では非常に人気があります。
この取り組みから得た教訓は主に2つあります。まず1つ目は運用上の課題です。
Alexa のスキル(機能)のバックエンドは AWS Lambda 関数であることをお話ししましたね。Lambda 関数には制限があり、Terraform やコンソールなどを通じて直接アップロードする場合、JAR ファイルのサイズは最大 50 メガバイトに抑えなければなりません。
私は最終的に S3 バケットを利用するアプローチを採用しましたが、それでも上限は 250 メガバイトです。しかし、この制限でも私の目的には足りませんでした。当初は Cohere を利用する予定ではなかったのです。
私が Cohere を使わざるを得なかったのは、上記のような制約が原因でした。私は「ms-marco-MiniLM-L-6」というモデル(ONNX 形式)のいずれかを使用していました。概念的には、リランキングのために独自にモデルをデプロイすることも可能ですし、その特定のモデルこそが私が求めていた問題をすべて解決するものでした。
しかし、実際に使用しようとした JAR ファイルは、AWS のインフラがサポートしている上限の2倍のサイズになっており、利用することができませんでした。
そのため、私は依存関係を持たない軽量な Cohere というサービスを利用することにしました。その結果、JAR ファイルのサイズは 100 メガバイト未満に抑えられ、非常に軽量なものになりました。
また、このプロセスに関わる皆さんへの注意喚起としてお伝えしたいことがあります。モデルを変更すると、リランキング手法で使用するスコアリングモデルや閾値(min score)に直接的な影響が出ます。例えば、ms-marco では 80% で機能していた設定が、Cohere では通用しなくなりました。私はこれらの数値を再調整する必要がありました。
つまり、モデルの変更はリランキングプロセスのキャリブレーション(較正)作業に直結するのです。まるで「今回のリリースですべての問題を解決した」とGitHub のイシューを閉じても、誰かがモデルを変更した瞬間に問題が再発するような状況が、システム的に繰り返されることになります。
Chapter 2: Dealing with the Monthly Cost
さて、第 2 章の「コスト」について話しましょう。この話題に入る前に、文脈エンジニアリングの問題を体系的に解決する過程で、LLM を単なる質問応答のための呼び出しとしてだけでなく、より頻繁に活用していることに気づいたでしょうか?これが本節で議論しようとしていることの序章です。
当時、私は分析を行いました。冒頭でお話しした通り、この実験全体は、私たちが開発したオープンソースプロジェクトである「Agent Memory Server」が、人間との相互作用による負荷がかかるユースケースに適しているかどうかを検証するために行いました。結果として、その適性は確認されました。ただし、Redis Agent Memory Server の外側でほぼすべての修正が必要になるという結論に至りました。
私が得た気づきの一つに、最小限の実験を行う際、私自身と妻、そして自宅で暮らす息子を被験者として活用したことが挙げられます。
1 日あたりの平均クエリ数は、ユーザーあたり 10 件程度です。LLM を利用した場合のコストは、世帯あたり約 4.20 ドラという定額でした。一方、Cohere は非常に安価で、ユーザー 1 人あたり 1 日 10 クエリでも 1 ドル未満です。
ここで私が犯したミスを思い出してください。冒頭でお話しした通り、短期記憶の説明の時点で私はその種を蒔きました。どこでお話したか覚えていますか?その際、LangChain4j の組み込み実装を使って、チャット履歴の直近 10 メッセージを保持していました。しかし、ツール呼び出しなどで問題が発生したため、私は独自の実装を作成しました。これは単純なパススルーで、コンテキストが無限に蓄積される仕組みです。
この経験は驚くべきものでした。5 分程度経過しても、私がその時の質問を投げかければ、文脈を保持しているため非常に詳細な回答が得られるのです。
コンテキストの成長は、LLM のコストや再ランク付けにおける Cohere のコストとは全く異なり、線形ではなく指数関数的に増加します。その理由は、各インタラクションごとにコンテキストが継続的に拡大していくからです。
LLM がツールに対して複数の呼び出しを実行すると、それらすべての応答は私が使用していた JSON データ構造に永遠に追加され続けます。その結果、消費されるトークン数が非常に多くなり、コストも膨れ上がります。
「リカルドさん、そんなに高くないでしょう」とお考えの方もいるかもしれません。確かに、1 ユーザーあたり数セントという計算になるケースもあります。小規模な家族(1 人または 3 人程度)の利用であれば問題ないでしょう。
しかし、これを大規模展開した場合どうなるでしょうか?スケールすると、これは非常に深刻な問題に発展します。
私はいくつかの試算を行いましたが、コストは指数関数的に増加する傾向にあります。私たちが目指すべきは、一定時間(定数時間)での処理です。つまり、平坦で一定のコスト構造を確立することが不可欠なのです。
この種の質問は、以前に説明した通りです。コンテキストに質問データを追加し、その後に詳細な回答を記述します。その後、LLM のツール呼び出しに伴うすべての情報や付随するデータを含め、それらの回答もすべてコンテキストに蓄積していきます。
私が実際にこの課題に対処するために実施した手法については、後ほど詳しく説明します。LangChain4j や LangChain には、「TokenWindowChatMemory」という組み込みの実装が用意されています。この実装の仕組みはまず、対象のモデルがサポートするトークンの最大数を設定することから始まります。
私が開発を始めた当時は GPT-3.5 を使用していました。記憶が確かならば、その時点での最大トークン数は 4,096 でした。これは、当時のモデルがサポートしていたトークン数であり、定数として提供されていた値です。また、LLM の実装に固有の、トークン数を推計するためのカウント推定器も指定する必要があります。
トークン推計器とは何でしょうか。これは、特定の LLM がどのようにトークンをカウントするかを教えるためのものです。信じられないかもしれませんが、例えば OpenAI のモデルから Anthropic のモデルに切り替えた際、ツールや人間との会話、AI との会話に関する情報をパースする仕組みが異なります。データモデル自体も、実際のペイロードも異なるのです。このため、トークンカウントの戦略はモデルごとに変わります。
ここで伝えたいのは、このトークンウィンドウが最大トーク数という上限を設ける点です。4,096 に近づくと、そこで上限に達します。これは「最後の 10 件」や「最後の任意の数」だけをバッファとして保持する非常に単純な実装と似た挙動を示します。ここでは古いものをすべて切り捨て、新しいものだけを残しますが、その基準はメッセージ数ではなくトークン数です。これを考えるとなかなかの改善点だと言えます。
少なくともこの方法なら、コスト問題を解決できます。つまり、コストを一定に保つことができるのです。これはコストを指数関数的な増加から平坦な状態へと変えるための、実用的な解決策と言えるでしょう。
では、実際に問題となったのは何でしょうか?誰か推測できるでしょうか。それは「文脈の喪失」です。なぜなら、この実装には古くなったものをすべて切り捨てるというルールが組み込まれていたからです。しかし、もしその古いメッセージが会話にとって非常に重要だった場合はどうなるでしょうか。このような実装のように、単に恣意的に削除を始めてはいけません。これが私が最初に試した解決策です。
最近では、文脈エンジニアリングの別の手法である「要約」も活用しています。私は独自の実装として ChatMemory を作成しました。これはもう一つの LLM の呼び出しを行い、「これまでの文脈から、会話に関連する事実や嗜好、あるいは強い主張をすべて抽出して要約してください」と指示します。
コードを見てみると、この実装がどのように機能しているかがわかります。また、トークン数の見積もり手法も併用しています。これは非常に優れたアイデアだと考えているからです。最終的にはコストを抑えつつ、より賢明な実装を実現する必要があります。単に恣意的にメッセージを削除するのではなく、可能な限り意識的・意図的に削除を行うことが求められます。これが私がこの問題を解決した方法です。
私が次に直面する問題、それは私が発見した中で最も興味深く価値のある課題の一つです。実際に LLM に問い合わせを行う際、「今夜の夕食は誰が来るのか?」という質問を、ジョンとスーザンの両方が来ると答えるケースがあります。同じ質問を、あなたや家族の他のメンバーが繰り返すことがあります。答えは完全に同じです。もしかしたら質問の言い回しが変わるかもしれません。別のセッションで聞かれることもあるでしょう。しかし、答えそのものは変わりません。Redis に保存された JSON ペイロード内の不変ベクトルエントリも本質的に同一です。何も変化していません。なぜ、同じプロンプトに対して再度呼び出しを行う必要があるのでしょうか?
Redis の仕組みや、長年にわたり Redis が有名になった理由をご存知の方ならお分かりいただけるでしょう。Redis で最も得意としていることは何か?それはデータのキャッシュです。そこで私は「セマンティック・キャッシング(意味的キャッシング)」の活用を始めました。
これは「コンテキストエンジニアリング」という umbrella の一部となった、もう一つの技術です。私たちはこれを「セマンティック・キャッシング(意味的キャッシュ)」と呼んでいます。
一般的なキャッシングとの違いは何でしょうか?通常のキャッシングは、ある特定の時点における非常に客観的で直接的な処理です。「キー」が与えられたら、それを元に検索を行います。もしキャッシュに存在すればそこから取得し、なければバックエンドへアクセスしてデータを取得した上で回答を返します。
一方、セマンティック・キャッシングとの決定的な違いは、実装において最も厄介だった部分にあります。ここでもまずキャッシュを検索しますが、その際にクエリの「意味」を評価するのです。例えば、以下の2つの質問は同じことを聞いているとみなせるでしょうか?
- 質問 A: 「この機能の使い方を教えてください。」
- 質問 B: 「この機能をどう使えばいいですか?」
表面上の表現は異なりますが、本質的に同じ問いであり、同じ回答を返すはずです。つまり、意味論的には等価です。セマンティック・キャッシングでは、このように「意味が同じ」と判断されたクエリに対してはキャッシュヒット(一致)とみなし、キャッシュミスが発生しないように設計されています。これにより、従来のキャッシングの課題が解決されました。
冗長な呼び出しの約 75% を削減できたのは、半分以下に減らせたという点で喜ばしい成果です。しかし、課題も残っています。特に英語が母国語ではない私のように、ブラジル出身者にとって発音や言葉選びが独特な場合、ごく一部の単語の違いだけでセマンティックキャッシュのヒットが発生し、LLM への問い合わせではなくキャッシュから返答が返ってくるという現象が起きることがあります。これは時として煩わしいものです。
セマンティックキャッシングは、コンテキストエンジニアリングを実装する際にぜひ取り組むべき領域の一つです。ただし、この手法には注意が必要です。リランキングモデルと同様に、キャッシュのヒットとミスをいつどのようにトリガーするかを常に適切に調整する必要があります。私が実装した「LangCache」という仕組みでは、もともと LangChain 用のキャッシュとして設計されたため、その名前で呼んでいます。
LangChain のドキュメントを見ると、このパラメータについて言及されています。これは、キャッシュに対して実行されるベクトル類似度検索におけるアサーションの精度を調整する役割を果たします。
ここで実装しているパターンは、何という名前でしょうか?「Cache」で始まり、「Aside」で終わるものです。答えは「Cache Aside(キャッシュ・アサイド)」です。これはキャッシュ・アサイドの実装であり、特別な難しさはありません。まずキャッシュを確認し、そこにデータが存在しない場合にのみ LLM にアクセスします。
コンテキストエンジニアリングに関する最終的な注記
コンテキストエンジニアリングは、単なる飾りや実装上のオプション機能ではありません。AI を活用してユーザーに人間らしい体験を提供する本格的な実装において、これはアーキテクチャの一部として不可欠なものとなっています。
Alexa スキルデバイスでの実装を通じて得た教訓の一つは、設計段階で事前に考慮すべき事項が多数あったということです。その都度対応(リアクティブ)するのではなく、アーキテクチャや設計の観点から先回りして準備(プロアクティブ)できていれば、後からの修正を繰り返すことなくスムーズに実装を進められたはずです。
私がこの場に来たのは、これらの教訓をお伝えし、皆さんにも先回りしたアプローチを取ってほしいと考えているからです。コンテキストエンジニアリングがアーキテクチャに根付いてしまえば、その後の工程は格段に容易になり、より自然な流れで進められるようになると思います。
リソース
実装したコードはすべて GitHub に公開しています。リポジトリをクローンして、実際に触ってみてください。Alexa スキルの開発方法を学びたい場合、少なくとも Java 版のコードがすべて揃っています。ここで紹介したコンテキストエンジニアリングのパターンも含まれています。
なお、これは私が自宅環境で構築したシステムの簡易版です。実際には、血圧を毎日記録し続けるツールを実装しており、医師との面談時には「過去数週間の平均値はこれです」といった形でデータを提供できます。
要約処理における LLM の使い分け
メッセージ量を削減するために要約処理を行いましたが、より精度の高い回答を得るために異なる大規模言語モデル(LLM)を試したことはありますか?
結論から言うと、私は OpenAI のモデルを使用し続けました。他のモデルでより良い結果が得られるかどうかは未確認ですが、検証する価値はあるでしょう。
要約付きプレゼンテーション一覧 もご覧ください
録画日:2026 年 9 月 2 日
関連記事
今日のまとめ
AIデイリーブリーフで今日の重要ニュースをまとめ読み