LangChain Blog出典公開日

コンテキストエンジニアリングは検索から始まる

持ち込み可のオープンブック試験を受けていると想像してみてください。理論上はすべての答えが手元にあるのに、なぜかそれがかえって正解を 見つける ことを難しくします。

重要なポイント

  • エージェントは限られたコンテキストウィンドウで動作しますが、情報検索は3つの点で破綻します。非効率な検索、一度に大量のコンテキストを扱うこと、そして無関係または古いコンテキストです。
  • Deep Agents ハーネスは、初期状態のままでも検索を小規模に保ちます。組み込みのファイルシステムツールは読み込む前に検索を行います。glob はファイル名をマッチさせ、grep はマッチする行を検索し、read_file は一度に1ページずつ読み取ります。サイズ超過のツール結果はオフロードされます。完全な結果はファイルに保存されますが、エージェントにはパスとプレビューだけが渡されます。
  • カスタム検索ツール(RAG など)には、「検索・オフロード・委任(retrieve, offload, delegate)」を使いましょう。結果をファイルに保存し、そのパスを返し、サブエージェントに読み込ませます。メインエージェントは関連する情報だけを見ることになります。

持ち込み可のオープンブック試験を受けていると想像してみてください。理論上はすべての答えが手元にあるのに、なぜかそれがかえって正解を 見つける ことを難しくします。

  • そもそもどこから探し始めればいいのでしょうか?
  • 情報源が正しいとどうすれば判断できるのでしょうか?
  • 無駄な検索にエネルギーを使い果たさないように、時間はどう最適化すればいいのでしょうか?

必要な情報を効率よく見つけることは、決して簡単ではありません。

エージェントに質問を渡すと、同様のジレンマに直面します。エージェントには限られたコンテキストウィンドウしかないため、私たち人間と同じように、特定のタスクを完了するには(すべてを暗記しているわけではないので)情報を 検索 する必要があります。検索の際、エージェントは以下の点で苦労します:

  • 非効率な検索: 検索のたびにトークンと時間がかかります。検索が下手なエージェント(例:中身を確認するだけですべてのファイルを開く、同じクエリを言い換え続けるなど)は、有用な何かを見つける前にコンテキストを検索結果で埋め尽くしてしまうかもしれません。
  • 一度に大量のコンテキスト: データの完全なエクスポートや十数件のドキュメントを一度に出力するような検索を1回行うだけで、コンテキストウィンドウが埋まってしまい、本来の作業のための余裕がなくなります。
  • 無関係または古いコンテキスト: 古い情報や話題外の情報でも、モデルには正しい答えと同じくらい説得力があるように見えることがあります。いったんコンテキストウィンドウに入り込むと、エージェントはそれを基に処理を進めてしまい(その誤りはその後のすべてのステップに波及します)。

幸い、エージェントには 正しい 情報を検索するための手法がいくつかあります。これらの手法を戦略的に適用し、与えられたタスクに対して 適切なタイミングで適切なコンテキスト をモデルに提供するのが、 エージェントハーネス の役割です。

このブログでは、エージェントの検索(目の前のタスクに必要な情報だけを見つけて取り込むこと)における課題、それらに対処するために使える手法、そしてエージェントハーネスを設計する際にこれらをどう考慮するかについて( LangChainのDeep Agents ハーネスを例として)解説します。

コンテキストがいったんコンテキストウィンドウに 入った 後の管理方法については、こちらの Context Engineering に関するブログをご覧ください.

私たちの例:競合分析エージェント

この仕組みを実際のユースケース、すなわち競合分析エージェントにどう適用できるか見ていきましょう。毎週月曜日、エージェントは数百社の競合企業を調査し、チームのテンプレートに従ってレポートを作成します。エージェントには以下の作業が必要です:

  • Company A に関する先週のメモを見つけること
  • 巨大な市場データのエクスポートから特定の売上数値を調べること
  • ニュースアーカイブから新しい情報を検索すること

これらはそれぞれ異なる種類の検索であり、したがって異なる問題に直面します。私たちはエージェントを Deep Agents ハーネス で構築しているため、これらの問題を最小限に抑えるためにすぐに使えるテクニックがいくつかあります。

1. 非効率な検索

私たちのエージェントは競合企業ごとに3つのものを見つけなければなりません。それぞれに十数回の試行(中身を確認するためだけにファイルを開く、ソースが異なる用語を使っているため検索語を言い換えるなど)がかかるとすると、レポートの一文すら書く前にコンテキストウィンドウが失敗した検索で埋まってしまいます。

したがって、最善の検索とはエージェントが繰り返さずに済む検索であり、それは仕事ごとに適切なツールを使うことを意味します。

組み込みのファイルシステムツールから始める

エージェントが情報を検索する最も一般的な方法の1つは、ファイルシステムを使うことです。しかし、エージェントが中身を確認するためにすべてのファイルを開くと、非常に非効率な検索になります。読み込んだファイルはすべてコンテキストウィンドウに入るため、見るだけでコンテキストが埋まってしまうのです。

より良いアプローチは、まず 名前 と キーワード を確認することです(開巻試験ですべてのページを読むのではなく、本の索引をスキャンするようなやり方です)。それをまさに実現する2つの一般的なファイルシステムツールがあります:

  • glob は名前のパターンでファイルを検索します。たとえば、 **/company-a*.md は Company A に関する先週のメモを見つけます。
  • grep はファイルをコンテキストウィンドウに読み込まずに内部を検索します。リテラルテキストに一致し、一致したファイル、一致した行、または件数を返すことができます。

すべてのディープエージェントには以下が付属します。 これらのツールが組み込まれている、ならびに ls, read_file, write_file および edit_file。これらのツールは、ファイルがどこにあっても(エージェントの状態、ローカルディスク、永続ストア、サンドボックス)同様に機能します。すべてエージェントの バックエンド.

キーワードだけでは不十分なときに検索ツールを追加する

Grep 探すべき言葉が分かっているときには便利ですが、一致するのはあくまでその 正確 与えるテキスト次第だ。これは先の例の3つ目の仕事、つまりニュースアーカイブから新着を検索するというタスクにとって問題になる。エージェントが「Company A funding」でgrepした場合、「Company A closes $50M Series B」というタイトルの記事を見逃してしまう。それはまさにエージェントが探しているものなのに、だ。

厳密な単語ではなく意味で検索する(セマンティック検索)には、 検索ツール 〜に支えられた ベクトルストア「検索ツールとは、エージェントが情報を取得するために呼び出せる任意のツールのことです。クエリを受け取り、何らかのデータソースで調べ、見つかった内容(最も近い一致)を返します。」

検索ツールのマッチ方法は、その背後にあるデータソースによって異なります。ベクトルストアを基盤とする場合は意味によってマッチしますが、検索ツールは他のデータソースの上に構築することもできます:

  • 構造化された検索(売上高や顧客記録など)のためのSQLデータベース
  • 検索API(ウェブからの最新情報用)
  • An MCPサーバー (ドキュメント検索のような、他の人がすでに構築したツールの場合)

セマンティック検索がどのように機能するかをより深く知りたい方は、次をご覧ください LangChainのガイド.

エージェントにいつ検索するかを判断させる

多くの検索構成では、最初に一度だけ検索を行い、その後に回答します。これはFAQボット(2ステップRAG))ですが、私たちのエージェントには当てはまりません。Company C についての調査の途中で、エージェントが知らなかった製品発表についての記事を見つけたとしましょう。すると今度はその製品の価格情報が必要になります(調査開始時点では調べる必要があることすら知りようがなかったものです)。検索が1回しかなければ、その情報がないままレポートが送信されてしまいます。

リサーチエージェントには〜が必要です agentic retrieval (2段階RAGの代わりに): エージェントが、いつ検索するか、何を検索するか、さらに情報が必要かどうかを判断します。Deep Agentsでは、デフォルトで既にこのように動作します。エージェントはループで実行され、必要な回数だけ任意のツールを呼び出せます。

検索のノウハウをスキルとしてパッケージ化する

エージェントが上手に検索できるよう、検索のノウハウを スキルとしてパッケージ化できます。スキルとは特定のタスク向けの一連の指示のことです(どのインデックスを使うか、クエリをどう組み立てるか、引用をどうフォーマットするかなど)。

エージェントは、タスクがそれを必要とするまで、各スキルの名前と短い説明しか見ません。タスクで必要になった時点で完全な指示を読み込みます。そのため、ニッチなノウハウを必要な時に利用でき、それ以外の時間はコンテキストを圧迫しません(この手法は プログレッシブ・ディスクロージャーと呼ばれます)。こちらに 動画による解説.

Progressive Disclosure (animated)

2. 一度に多すぎるコンテキスト

しかしながら、検索プロセスを最適化しても、 正しい 検索が多すぎる結果を返すことがあります。私たちのエージェントは市場データのエクスポートからたった1つの数字(B社の四半期収益)を必要としているだけですが、それを見つけるために40,000行すべてを読み込むとコンテキストウィンドウがいっぱいになってしまいます。検索ツールにも同じ問題があります。検索結果のニュース記事をすべてそのままモデルに返すと、1回の検索で4件、5社分では合計が急速に膨れ上がります。

大きなファイルを一度に1ページずつ読む

ファイルシステムツールは、できるだけ少ない量だけを返すように作られています。 read_file はファイルを一度に1ページずつ読み(デフォルトで100行)、エージェントはオフセット(何行目から読み始めるか)とリミット(何行読むか)を設定して、必要な部分にジャンプできます。つまりB社の収益を見つけるには、エージェントはまずgrepで検索し、その一致箇所の周囲のわずかな行だけを読みます。

ハーネスに巨大な結果をオフロードさせる

ツールが予想より多くの結果を返してしまうこともあります。ツールの結果が大きすぎる場合(約20,000トークンを超える場合)、ハーネスはそれを /large_tool_results/ 配下のファイルに保存し、エージェントにパスと短いプレビュー(最初と最後の数行)を渡します。エージェントはその後、 read_fileのオフセットとリミットを使って必要な部分だけを読み戻せます。

検索ツールにファイルパスを返させる

サイズ制限を待つのではなく、自分の検索ツールにも同じ考え方を適用できます。各結果をファイルに書き出し、パスだけを返すようにするのです。これは「検索、オフロード、委任(retrieve, offload, delegate)」と呼ばれるパターンの最初の部分です(LangChainのRAGガイド で全体を詳しく解説しています)。

ここでは簡略化された検索ツールを示します。このツールは検索を実行し、各結果をエージェントの バックエンド内のファイルとして保存し、ファイルパスのリストだけを返します:

backend = StateBackend()

@tool
def search_news(query: str) -> str:
    """Search the news archive and save each result to a file."""
    docs = vector_store.similarity_search(query, k=4)
    files = [(f"/retrieved/result_{i}.md", doc.page_content.encode()) for i, doc in enumerate(docs)]
    backend.upload_files(files)
    return "Saved results to: " + ", ".join(path for path, _ in files)

これでメインエージェントは、4つの完全な記事の代わりに4つの短いパスを見ることになります。

3. 無関係または古いコンテキスト

結果が小さくても、それが 間違った 結果であれば役に立ちません。あの4つのファイルのどれも、似たような名前の別の会社についてのものや、2年前の記事である可能性があります。脈絡のないテキストや古いテキストは、モデルにとって正しい答えと同じくらい説得力があるように見えるため、目標はそうしたテキストをメインエージェントのコンテキストから完全に締め出すことです。

サブエージェントに結果をフィルタリングさせる

ただし、メインエージェントは 依然として どのファイルが重要かを判断するためにそれらのファイルを読む必要があります(そして、開いた脈絡のない記事はすべてそのコンテキストに入り込んでしまいます)。ここで「retrieve, offload, delegate(検索、退避、委任)」パターンの「delegate(委任)」の部分が登場します: サブエージェント が検索された各ファイルを読み、重要な事実だけを返します。

  1. サブエージェントを定義します:description はメインエージェントにいつそれを使うべきかを伝え(1回の呼び出しにつき1ファイル)、システムプロンプトはサブエージェントにそのファイルで何をすべきかを伝えます。
chunk_analyst = {
		"name": "chunk-analyst",
		"description": "Analyze one retrieved file. Pass the question and a single path under /retrieved/.",
		"system_prompt": "Use read_file to read the assigned file. Return the key facts and the source URL.",
}
  1. メインエージェントに検索ツールとサブエージェントを与えます。 search_newsで使用したものと同じバックエンドを渡せば、サブエージェントはツールが保存したファイルを読むことができます。
agent = create_deep_agent(
		model=model,
		tools=[search_news],
		backend=backend,  # same instance, so read_file can see /retrieved/
		subagents=[chunk_analyst],
)

これでフローは次のようになります:

  1. 検索:メインエージェントが search_news を呼び出し、4つのパスを受け取ります。
  2. 委任:各パスをチャンクアナリストのサブエージェントに送ります。それらは並列で実行できます。
  3. 統合:各アナリストは出典付きの短い要約を返します。メインエージェントはそれらを組み合わせ、欠けているものがあれば再度検索します。

生の結果はファイルに送られ、ノイズの多い読み込みはそれぞれ独自のコンテキストウィンドウを持つサブエージェント内で行われ、メインエージェントは結論だけを見ることになります。脈絡のないものはすべてサブエージェントのコンテキストにとどまり、メインエージェントのコンテキストウィンドウはクリアなまま保たれます!

最新のソースから取得する

フィルタリングによって脈絡のない結果は排除できますが、古い結果は修正できません。当社の競合分析エージェントは毎週月曜日に実行されるため、先月のインデックスでは今週のニュースを捉えられません。

解決策:検索APIやMCPサーバーは、エージェントに現在の情報を提供できます(数か月前にインデックスされたものの代わりに)。データに日付が含まれているなら、検索ツールで日付によるフィルタリングを行うことでも、古い結果を排除できます。

取得したテキストには注意する

逆に言えば、関連性のある最新の結果も問題を引き起こす可能性があります!取得したテキストはデータであって指示ではないため、取得したものには何でも プロンプトインジェクション (そして、プロンプトやデリミタのどの戦略によってもこれを完全に防ぐことはできません)。出力を必ず検証してください:回答が期待どおりのソースを引用しているか、主張がそのソースと一致しているかを確認しましょう。

覚えておくべき検索戦略

(どのハーネスを使って構築する場合でも)注意すべき点は以下のとおりです:

  • 機能する中で最も効率的なツールを使う。 知っている単語で検索できるなら、embeddingルックアップよりgrepの方が優れています。セマンティック検索は本当に必要なときにのみ使うようにしましょう。
  • ページ単位で読む(一度にすべて読み込まずに)。大きなファイルは一度に読み込まず、分割して読みましょう。
  • ペイロードの代わりにポインタを返す。 取得したコンテンツをファイルに書き出し、エージェントにはそのパスを渡す。
  • 読み取りを委任する(サブエージェントに読み取りと要約を任せ、メインエージェントには結論だけを見せる)。
  • グラウンディングを検証する(回答を取得した内容と照らし合わせて確認し、取得したテキストは信頼できないものとして扱う)。

試してみる

uv add deepagents

ライブインストラクターからもっと学びたいですか?私は11月9日にこれを取り上げます:登録は こちら!

‍

検索は物語の半分にすぎません:情報がどのようにエージェントに届くかという話です。エージェントが時間をかけて情報を蓄積していくとき、そのコンテキストウィンドウをどう管理すればよいのでしょうか?

詳しく知るにはこちらをご覧ください:

原文の出典

LangChain Blog

内容について

原文の公開と権利は出典元に帰属します。

機械翻訳 · 原文をご参照ください