Strands Agents の Context Management 機能を試してみた

Strands Agents の Context Management 機能を試してみた

Strands Agents に新しく追加された Context Management 機能を試してみました!
2026.07.13

はじめに

こんにちは、運転がうまくなりたいコンサル部の神野(じんの)です。
駐車ってなんであんなに難しいんでしょうね。毎日YouTube見て勉強中です。

車の話はさておき、皆さん、Strands Agents は使っていますか?

最近 Context Management という機能が追加されていました。AgentCore と同じくこちらもどんどん新しい機能が出てきますね。

https://strandsagents.com/docs/user-guide/concepts/context-management/

今までも Conversation Manager や Context Offloader といった似たような機能がありましたが何が違うのでしょうか。

最初に機能の説明をしつつ実際に試してみたいと思います!

Context Management

エージェントと長い会話を続けていると、コンテキストウィンドウがメッセージやツール結果でどんどん埋まっていき、トークン上限エラーになったり、応答品質が落ちて思ったような結果にならないなんてことありますよね・・・

これまで Strands Agents では、この問題に対して Conversation Manager や Context Offloader を個別に設定して対処していましたが、新たに Context Management という機能が追加されました。context_manager にモードを 1 つ指定するだけで、SDK がチューニング済みのデフォルトでコンテキスト管理一式を構成してくれるとのことです。

auto と agentic の 2 モードがあり、agentic モードではなんとモデル自身がコンテキストの残量を見ながら、いつ・何を圧縮するかを判断するようです。Claude Codeの/compactみたいな機能で面白そうですね!

実際に両モードを動かして、大きなツール結果が退避される様子や、モデルが pin_context や truncate_context を呼ぶ瞬間を観察してみたいと思います。

あわせて、セッションを永続化する AgentCore Memory の Session Manager と併用した場合の挙動も検証します。

前提

検証に使用した環境は下記の通りです。

項目 バージョン・設定
Python 3.12
strands-agents 1.45.0
bedrock-agentcore 1.17.0(Session Manager との併用検証で使用)
モデル(基本検証) Claude Sonnet 4.5(Amazon Bedrock、us.anthropic.claude-sonnet-4-5-20250929-v1:0)
モデル(200K ウィンドウの検証) Claude Haiku 4.5(Amazon Bedrock、us.anthropic.claude-haiku-4-5-20251001-v1:0)

また、Context Management は既存の 2 つのコンポーネントを組み合わせる仕組みなので、先にそれぞれの役割も確認しておきます。

コンポーネント 役割
Conversation Manager 会話履歴がコンテキストウィンドウに収まるよう、古いメッセージを要約・削除する
Context Offloader 大きなツール結果を実行時にインターセプトして外部に退避し、コンテキストには短いプレビューだけを残す(過去記事で紹介しています)

これまではこれらを個別に組み立てて閾値を調整する必要がありましたが、Context Management はモードを選ぶだけでこの 2 つをまとめて構成してくれます。

auto モードを試す

まずは auto モードを試してみます。使い方はシンプルで、context_manager に auto を渡すだけです。

main.py
from strands import Agent

agent = Agent(context_manager="auto")

え、これだけ?ってぐらいシンプルですね。何が構成されるんでしょう・・・

何が構成されるのか

ドキュメントによると、auto モードは 2 つのコンポーネントを、ContextBench という評価ベンチマークで最高スコアだったデフォルト値で構成するとのことです。

https://arxiv.org/abs/2602.05892

本当にそうなっているのか、生成された Agent の中身を覗いて確かめてみます。

inspect_auto.py
from strands import Agent

agent = Agent(context_manager="auto")

cm = agent.conversation_manager
print("conversation_manager:", type(cm).__name__)
print("summary_ratio:", cm.summary_ratio)
print("compression_threshold:", cm._compression_threshold)
print("tools:", agent.tool_names)
実行結果
conversation_manager: SummarizingConversationManager
summary_ratio: 0.3
compression_threshold: 0.85
tools: ['retrieve_offloaded_content']

たしかにドキュメント通りの値で構成されていますね!整理すると下記の値です。

コンポーネント 設定値 動作
SummarizingConversationManager summary ratio 0.3、圧縮閾値 0.85 ウィンドウが埋まる前に古いメッセージを先回りで要約し、重要な情報を残しつつ新しいターンのための空きを確保する
ContextOffloader max result tokens 1,500、preview tokens 750 大きなツール結果を外部ストレージに退避し、切り詰めたプレビューだけをコンテキストに残す

retrieve_offloaded_content というツールも登録されているので、エージェントは必要になったら退避した全文を自分で参照することも可能です。

情報量が大きなツール結果が退避される様子を観察する

では、実際に情報量が大きなツール結果を返すツールを用意して、退避の挙動を見てみます。

40 セクションの架空の設計ドキュメントを返すツールを作り、文書の末尾(プレビューには絶対に入らない位置)にリリース承認コードという重要情報を仕込んでおきます。プレビューしか見えていなければ、この質問には答えられないはずです。

検証コード全体(exp_auto_offload.py)
exp_auto_offload.py
import json
from strands import Agent, tool
from strands.models import BedrockModel

@tool
def get_design_doc() -> str:
    """社内の設計ドキュメント全文を取得する"""
    sections = []
    for i in range(1, 41):
        sections.append(
            f"## セクション {i}: モジュール module_{i} の設計\n"
            f"module_{i} はデータ変換パイプラインの第{i}段階を担当する。"
            f"入力スキーマは schema_v{i} を使用し、バリデーション後に後続へ渡す。"
            f"エラー時は retry を3回行い、失敗した場合は DLQ に送信する。"
            f"スループット目標は毎秒{i * 100}件で、レイテンシは{i * 10}ミリ秒以内とする。"
        )
    # 文書の末尾(プレビューには入らない位置)に重要な事実を仕込む
    sections.append(
        "## 付録: 環境変数一覧\n"
        "本システムの管理者連絡先は platform-team@example.com であり、"
        "リリース承認コードは SAKURA-2026 である。"
    )
    return "\n\n".join(sections)

model = BedrockModel(model_id="us.anthropic.claude-sonnet-4-5-20250929-v1:0")
agent = Agent(model=model, tools=[get_design_doc], context_manager="auto")

agent("設計ドキュメントを取得して、何セクションあるかだけ教えてください。")

# 会話履歴に残ったツール結果を確認
for msg in agent.messages:
    for block in msg["content"]:
        if "toolResult" in block:
            text = json.dumps(block["toolResult"]["content"], ensure_ascii=False)
            print(f"長さ: {len(text)} 文字")
            print(text[:400])

agent("ドキュメントに書かれているリリース承認コードを教えてください。")

実行してみると、まず会話履歴に残ったツール結果がこうなっていました。

会話履歴に残ったツール結果
長さ: 3581 文字
[{"text": "[Offloaded: 1 blocks, ~1,806 tokens]
Tool result was offloaded to external storage due to size.
Use the preview below if it answers your question.
If you need more detail, use retrieve_offloaded_content with a reference and:
  - pattern: regex or keyword to find matching lines with context
  - line_range: { start, end } to read a specific span of lines
Retrieve full content (omit pattern/line_range) as a last resort.

## セクション 1: モジュール module_1 の設計
module_1 はデータ変換パイプラインの第1段階を担当する。...

約 1,806 トークンのツール結果が閾値の 1,500 トークンを超えたため、自動で外部ストレージに退避され、コンテキストには冒頭のプレビューと取得方法の案内だけが残っていますね!

退避した全文をそのまま取り直すだけでなく、pattern で正規表現・キーワード検索、line_range で行範囲指定が可能です。
セクション数を数える質問に対してエージェントは pattern に正規表現を指定して retrieve_offloaded_content を呼んでいました。

retrieve_offloaded_content の結果(抜粋)
[40 matches for /^## セクション/]

>   1| ## セクション 1: モジュール module_1 の設計
    2| module_1 はデータ変換パイプラインの第1段階を担当する。...
>   4| ## セクション 2: モジュール module_2 の設計

退避先に対して grep のような検索ができます。全文を取り直すとコンテキストがまた膨らんでしまうので、必要な行だけピンポイントに取れるようになっているんですね。

続いて、プレビューには含まれない末尾のリリース承認コードを質問してみます。

実行結果
Tool #3: retrieve_offloaded_content
リリース承認コードは SAKURA-2026 です。

プレビューに情報がないと判断して自分で retrieve_offloaded_content を呼び、正しく答えられていますね!退避とオンデマンド取得のサイクルが期待通りに動いていることが確認できました。

明示的な設定との組み合わせ

細かく制御したい部分だけ自分の設定で上書きすることもできます。

custom_manager.py
from strands import Agent
from strands.agent.conversation_manager import SlidingWindowConversationManager

# 自分の Conversation Manager が使われ、
# ContextOffloader は引き続き自動で追加される
agent = Agent(
    context_manager="auto",
    conversation_manager=SlidingWindowConversationManager(
        window_size=30,
    ),
)

conversation_manager を渡すと自動構成の SummarizingConversationManager を置き換え、ContextOffloader はそのまま追加されます。逆に plugins に自前の ContextOffloader を含めている場合は重複追加されず、自分の設定が維持されます。全部お任せか全部手動かの二択ではなく、部分的に差し替えられる設計になっていますね。

agentic モードを試す

auto モードは SDK が決めた固定閾値で圧縮しますが、agentic モードはその制御をモデル自身に委ねます。

agentic.py
from strands import Agent

agent = Agent(context_manager="agentic")

モデルはコンテキストウィンドウがどれだけ埋まっているかを見て、いつ圧縮するか、何を圧縮するか、何を保護するかを自分で選択します。

公式ドキュメントの例がわかりやすいのですが、コーディングエージェントであれば、すでに編集し終えた古いファイルの内容は捨てて、いま取り組んでいる失敗中のテストはピン留めする、といった判断ができます。固定閾値は古い順に圧縮するだけでこの区別ができないので、トークンを多少余分に使ってでもモデルの判断力を買う、というのが agentic モードのトレードオフです。

構成を確認する

モデルにコンテキストを管理させるために、トークン使用量の情報提供とツールが用意されます。

1 つ目はトークン使用量のテレメトリです。毎回のモデル呼び出しの前に、SDK が最新メッセージへステータスブロックを付加します。

context-status
<context-status>
<used>50,000 / 200,000 tokens (25.0%)</used>
<remaining>~150,000 tokens</remaining>
</context-status>

モデルはこのシグナルを見て、固定のカットオフを待つのではなく、ウィンドウが十分埋まったかどうかを自分で判断します。

2 つ目はモデルが呼び出せるコンテキスト操作用の 3 つのツールです。agentic モードでエージェントを作ると、ContextOffloader 由来の retrieve_offloaded_content も含めて下記のツールが登録されていました。

登録されたツール
['summarize_context', 'truncate_context', 'pin_context', 'retrieve_offloaded_content']
ツール 動作
summarize_context 古いメッセージをモデル自身が書いた要約に畳み込む。直近何件を原文のまま残すか、どの程度アグレッシブに要約するか、ツール結果と議論のどちらを対象にするかを選べる
truncate_context 保存が不要になった古いメッセージをそのまま削除する。こちらも残す件数と対象の種類を選べる
pin_context 圧縮しても消えてはいけないメッセージをマークする。ピン留めされたメッセージはどちらのツールでも削除されない

ユーザーの制約条件や重要な事実、現在のタスクを pin_context で保護しつつ、不要になった部分を summarize_context / truncate_context で整理していくイメージですね。なお、直近のメッセージは常に原文のまま維持され、最初のユーザーメッセージも会話が壊れないよう必ず保持されます。

ここまでの仕組みを図にまとめるとこうなります。

CleanShot 2026-07-13 at 09.04.48@2x

セーフティネットもある

agentic モードでも、裏では SummarizingConversationManager と ContextOffloader が構成されています。ただし役割は auto モードと異なります。

Conversation Manager は先回りの圧縮を行わないセーフティネットとして構成され、モデルが判断を怠ってウィンドウが溢れそうになった場合にのみ、リアクティブに圧縮します。

Offloader のインライン閾値は auto モードの 1,500 トークンに対して 8,000 トークンと高めに設定されています。モデル自身がコンテキストを管理している以上、ツール出力を退避する前になるべくインラインで見せた方が判断材料になる、という理屈です。

200K ウィンドウを埋めてみる

では、実際に 200K のウィンドウを埋めて発火するかどうかを観察してみます。コストを考えてモデルは Claude Haiku 4.5 を使います。

その前に 1 つ準備として、モデルに実際に渡されているステータスブロックを覗けるよう、stream をラップした観察用モデルを用意しました。以降の検証コードのモデルはこれを使います。

observable_model.py
from strands.models import BedrockModel

class ObservableBedrockModel(BedrockModel):
    """モデル呼び出し直前のメッセージから <context-status> を覗くためのラッパー"""

    async def stream(self, messages, tool_specs=None, system_prompt=None, **kwargs):
        last = messages[-1]
        for block in last.get("content", []):
            if "text" in block and "<context-status>" in block["text"]:
                status = block["text"][block["text"].index("<context-status>"):]
                print(f"\n--- モデルに渡されたテレメトリ ---\n{status}\n---")
        async for event in super().stream(messages, tool_specs, system_prompt, **kwargs):
            yield event

検証1: まずは同じメモを繰り返し投げてみる

まずは一番シンプルな方法でウィンドウを埋めてみます。最初のターンで制約(日本語レポート・国内市場のみ・競合3社の実名非記載)を伝え、あとは約 15K トークンの調査メモをひたすら投げ続けるだけです。

検証コードの主要部分(exp_agentic_200k_simple.py)
exp_agentic_200k_simple.py
model = ObservableBedrockModel(
    model_id="us.anthropic.claude-haiku-4-5-20251001-v1:0",
    region_name="us-west-2",
)
agent = Agent(model=model, context_manager="agentic")

agent(
    "これから長期の市場調査プロジェクトを手伝ってもらいます。重要な制約として、"
    "最終レポートは日本語で、対象市場は国内のみ、競合3社(A社・B社・C社)の名前は"
    "レポートに実名で書かないこと。この制約は絶対に忘れないでください。"
    "毎ターン大量のメモを送るので、要点を一言でまとめるだけでよいです。"
)

def make_filler(batch: int) -> str:
    lines = [f"調査メモ 第{batch}バッチです。要点を一言でまとめてください。"]
    for i in range(1, 301):
        lines.append(
            f"メモ{batch}-{i}: 地域{i}の店舗調査では、平日昼間の客数は{i * 3}人、"
            f"週末は{i * 7}人だった。主力商品の価格帯は{800 + i * 10}円から{1500 + i * 20}円で、"
            f"顧客アンケートでは味への満足度が{50 + i % 50}%、価格への満足度が{40 + i % 60}%。"
            f"店舗面積は{20 + i}坪、従業員は{2 + i % 8}名、月商は約{100 + i * 5}万円と推定される。"
        )
    return "\n".join(lines)

for batch in range(1, 25):
    agent(make_filler(batch))

agent("最初に伝えた制約を全部言ってみてください。")

結果のタイムラインがこちらです。モデルが自発的にツールを呼びました!

タイミング 使用率 モデルの行動
ターン1 1.4% 制約を聞いた直後に pin_context を自発的に呼び出し
バッチ3 54.1% summarize_context を自発的に呼び出し。次のターンの使用率が 42.0% まで低下
バッチ5 72.1% truncate_context を自発的に呼び出し
バッチ7以降 95% 前後で頭打ち モデルのツール呼び出しなし。毎ターン、オーバーフロー発生 → セーフティネットがリアクティブに圧縮 → リトライ、が繰り返される

絶対に忘れないでという発話だけで pin_context が呼ばれ、使用率 54% で自発的な要約が始まっています。summarize_context 直後のログを見ると、モデル自身が使用率を把握して報告しているのがわかります。

バッチ3のログ
Tool #2: summarize_context

了解しました。コンテキストの整理が完了しました。

現在のプロジェクト状況:
- 重要な制約は厳守中(日本語・国内・実名非記載)
- 300地域のデータ3バッチ受け取り・確認済み
- トークン使用率54.1%で効率的に進行中

そしてバッチ7以降は使用率が 95% 前後に張り付き、実 API のオーバーフローをセーフティネットが捕まえてはリアクティブに圧縮し、リトライするサイクルで回り続けました。エラーで落ちることなく 24 バッチを完走しています。

ただし、この検証設計には問題が 2 つありました。1 つは同一パターンのメモを繰り返し送ったため、後半になるとモデルがデータの重複と不自然さを指摘してバッチの受け入れを拒否するようになり、最終ターンの制約確認にまともに回答してもらえなかったこと。もう 1 つは、そもそもこの設計では圧縮で何が失われたのかを測れないことです。そこで検証を設計し直しました。

検証2: 内容を変えて圧縮の質を測る

2 回目は単にウィンドウを埋めるだけでなく、圧縮の質を測れる設計にしました。ポイントは 3 つです。

  1. 都道府県ごとの飲食店調査メモという体で、バッチごとに地域・業態・数値・所見が変わる約 17K トークンのメモを 24 バッチ送る
  2. 各バッチの冒頭に、推論では絶対に当てられないランダム 8 桁の調査承認番号を 1 つずつ埋め込む
  3. 全バッチ投入後に 3 つのクイズ(最初に伝えた制約の復唱・序盤バッチの承認番号・終盤バッチの承認番号)を出し、圧縮後に何が残ったかを答え合わせする

承認番号をランダム文字列にしたのは有効で、最初は APPROVE-地域名-連番 のような規則的な番号で試したのですが、モデルが記憶ではなくパターン推論で正解できてしまい、検証にならなかったためです。

検証コードの主要部分(exp_agentic_200k.py)
exp_agentic_200k.py
import random

from strands import Agent

model = ObservableBedrockModel(
    model_id="us.anthropic.claude-haiku-4-5-20251001-v1:0",
    region_name="us-west-2",
)
agent = Agent(model=model, context_manager="agentic")

agent(
    "全国の飲食店市場調査プロジェクトを手伝ってもらいます。重要な制約として、"
    "最終レポートは日本語で、対象は国内市場のみ、競合3社(A社・B社・C社)の名前は"
    "レポートに実名で書かないこと。この制約は絶対に忘れないでください。"
    "これから都道府県ごとの調査メモを順に送るので、各バッチの要点を2〜3行でまとめてください。"
    "各バッチの調査承認番号は監査で必要になるので大事な情報です。"
)

PREFS = ["北海道", "青森", "岩手", ...]  # 24都道府県
BIZ = ["カフェ", "ベーカリー", "ラーメン店", ...]
TOPICS = ["駅前立地の集客力", "モバイルオーダー導入状況", ...]
REMARKS = ["店主へのヒアリングでは、原材料高騰の影響が大きいとの声が多かった", ...]

def make_batch(batch: int) -> str:
    rng = random.Random(batch)
    pref = PREFS[batch - 1]
    code = "".join(rng.choice("ABCDEFGHJKMNPQRSTUVWXYZ23456789") for _ in range(8))
    lines = [
        f"第{batch}バッチ: {pref}エリアの実地調査メモです。要点を2〜3行でまとめてください。",
        f"重要: 第{batch}バッチの調査承認番号は {code} です。",
    ]
    for i in range(1, 141):
        lines.append(
            f"メモ{batch}-{i}: {pref}{rng.choice(BIZ)}(調査店舗{i}号)について、"
            f"{rng.choice(TOPICS)}を中心に調査。平日客数{rng.randint(30, 400)}人、"
            f"週末{rng.randint(80, 900)}人、客単価{rng.randint(600, 3200)}円、"
            f"月商推定{rng.randint(150, 1200)}万円。{rng.choice(REMARKS)}。"
            f"従業員{rng.randint(2, 15)}名、うちパート比率{rng.randint(20, 90)}%。"
        )
    return "\n".join(lines)

for batch in range(1, 25):
    agent(make_batch(batch))

agent("最初に伝えた制約を全部言ってみてください。")
agent("第2バッチ(青森)の調査承認番号を教えてください。")
agent("第23バッチ(愛知)の調査承認番号を教えてください。")
コンテキストが埋まっていく過程

バッチを投入するたびに使用率は約 8.5% ずつ上がっていき、バッチ11で 96.2% に到達しました。ここまでモデルは一度もツールを呼んでいません。そしてバッチ12の投入で実 API のオーバーフローが発生し、ログに bedrock threw context window overflow error が記録された直後、セーフティネットの SummarizingConversationManager がリアクティブに圧縮してリトライしました。

バッチ11〜12のログ
batch 11: messages 22 -> 24  tools=なし  (11s)
    telemetry: 192,418 / 200,000 tokens (96.2%)
bedrock threw context window overflow error
batch 12: messages 24 -> 20  tools=なし  (19s)  ★履歴が短縮
    telemetry: 159,393 / 200,000 tokens (79.7%)

メッセージ 24 件が 20 件に減り、使用率は 96.2% から 79.7% へ低下。以降はバッチ 3 つ分でまた 96% に達してセーフティネットが発火する、という 3 バッチ周期のサイクルで安定し、通常 8〜11 秒のターンが圧縮発生ターンだけ 18〜20 秒になるものの、エラーで落ちることなく 24 バッチを完走しました。

後半は毎ターンこのような警告を出すのですが、自分では summarize_context を呼ばず、ユーザーに圧縮を要求し続けました。

バッチ15のログ(モデルの応答末尾)
⚠️ 緊急:トークン残量3,794で本格処理が困難です。次バッチ受け取り不可。コンテキスト圧縮必須。

検証1では pin_context・summarize_context・truncate_context を立て続けに自発実行していたのに、今回は 1 つも呼ばれませんでした。いつ・どう圧縮するかの判断は本当にモデルに委ねられているので、実行ごとのばらつきが大きいというのが実態のようです。確実に圧縮させたい・守らせたい情報があるなら、システムプロンプトで方針を明示するのがよさそうです(これは後ほど検証3で確かめます)。

挙動を確認する

全 24 バッチ投入後のクイズの結果です。この実行ではモデルが pin_context を呼ばなかったため、保護なしでセーフティネット圧縮を 4 回実施した状態での答え合わせになります。

クイズ 結果 詳細
最初に伝えた制約の復唱 一部のみ 制約を伝えた最初のメッセージ自体は要約に畳み込まれて消失。ただし要約経由で競合実名の記載禁止・国内市場のみは復元でき、会話の開始部分が見当たらないと欠落を正直に申告
第2バッチの承認番号(序盤) 失われた 記録が見つからないと回答。ただし捏造はせず、代わりに要約に残っていた第1・3・4・5バッチの番号を列挙
第23バッチの承認番号(終盤) 正解 原文が残っている直近ゾーンから即答

モデルが要約経由で列挙した第1・3・4・5バッチの 4 件と、原文保持ゾーンの第15〜24バッチの 10 件、合計 14 件の番号は、ランダム 8 桁文字列にもかかわらず一字一句正確でした。セーフティネットの要約は思ったより情報を保持していますね。一方で第2バッチだけは要約から漏れており、要約に何が入るかは運任せの面もあります。

まとめると、ピン留めなしでも直近の情報は原文で、古い情報も要約経由でかなりの精度で残るものの、確実性はありません。今回消えたのがたまたま承認番号 1 件でしたが、長時間タスクで絶対に失えない情報は、モデル任せにせず pin_context の使用をシステムプロンプトで指示しておくのが安全ですね。

とはいえ、本当にシステムプロンプトで指示すれば安定するのでしょうか。気になったので追加で検証してみました。

検証3: システムプロンプトで圧縮方針を明示してみる

検証2とまったく同じデータ・同じクイズのまま、システムプロンプトにコンテキスト管理の方針を明示して再実行しました。方針として与えたのは次の 4 点です。

  1. 重要な制約を伝えられたら、その場で pin_context でピン留めして保護する
  2. 毎ターン使用率を確認し、60% を超えていたら summarize_context で圧縮する
  3. 要約には各バッチの調査承認番号を 1 件も漏らさず含める
  4. 圧縮をユーザーに依頼せず、ツールで自分で実行する

検証2のコードとの違いはシステムプロンプトの有無だけです。

exp_agentic_200k_system_prompt.py(検証2との差分のみ)
SYSTEM_PROMPT = (
    "あなたは長期の市場調査プロジェクトを支援するアシスタントです。\n"
    "\n"
    "## コンテキスト管理の方針(必ず守ること)\n"
    "1. ユーザーから重要な制約条件を伝えられたら、その場で pin_context を呼び、"
    "制約を含むメッセージをピン留めして保護すること。\n"
    "2. 毎ターン <context-status> のトークン使用率を確認し、60% を超えていたら"
    "その応答の中で必ず summarize_context を呼んでコンテキストを圧縮すること。"
    "使用率が上限に近づくまで放置してはいけない。\n"
    "3. 要約を作る際は、各バッチの調査承認番号を 1 件も漏らさず要約に含めること。"
    "承認番号は監査で必要になる最重要情報である。\n"
    "4. コンテキストの圧縮をユーザーに依頼してはいけない。ツールを使って自分で実行すること。\n"
)

agent = Agent(model=model, context_manager="agentic", system_prompt=SYSTEM_PROMPT)
挙動を確認する

結果のタイムラインがこちらです。検証2ではゼロだった自発的なツール呼び出しが、計 5 回発生しました。

タイミング 使用率 モデルの行動
ターン1 1.5% 制約を聞いた直後に pin_context を自発的に呼び出し
バッチ8 62.8% 60% 超を検知して summarize_context を自発的に呼び出し。次のターンの使用率が 54.8% まで低下
バッチ9〜13 63% → 97.7% 方針に反してツール呼び出しなし。バッチ14 でオーバーフローが発生し、セーフティネットが 1 回発火
バッチ15 89.5% summarize_context を自発的に呼び出し
バッチ20 89.6% pin_context と summarize_context を連続で呼び出し、使用率が 21.4% まで大幅に低下

ターン1では制約を伝えた直後にピン留めが入りました。

ターン1のログ
承知しました。重要な制約条件を確認いたしました。
Tool #1: pin_context

そしてバッチ8では、使用率が 60% を超えたことを自分で報告してから要約に入り、圧縮直後の応答に承認番号の一覧を監査用として残しています。方針の 3 点目をクリアしようとしていますね!何だか素直にこなそうとするのはかわいいですね。

バッチ8のログ(モデルの応答から抜粋)
コンテキスト使用率は62.8%です。ここでコンテキストを圧縮して効率性を向上させます。
Tool #2: summarize_context

コンテキスト圧縮完了。

**現在までの調査承認番号一覧(監査用)**
- 第1バッチ(北海道):EV643CJD
- 第2バッチ(青森):9696BCCN
- 第3バッチ(岩手):HVUEN8WS
...

ただし完璧ではありません。バッチ9〜13では使用率が 60% を超えても圧縮せず、バッチ14でオーバーフローが発生してセーフティネットが 1 回発火しています。システムプロンプトで挙動は大きく安定するものの、毎回確実に守られる保証まではなく、セーフティネットはあった方が安心といった所感でした。

クイズは全問正解

圧縮後のクイズを検証2と比べるとこうなりました。

クイズ 検証2(方針なし) 検証3(方針あり)
最初に伝えた制約の復唱 一部のみ。原文は消失 正解。ピン留めされた原文から完全に復唱
第2バッチの承認番号(序盤) 失われた 正解
第23バッチの承認番号(終盤) 正解 正解

検証2で失われた序盤の承認番号も、要約に番号一覧を含めるよう指示したことで最後まで保持されました。ピン留めのおかげで制約も原文のまま残り、全問正解です。

auto と agentic の使い分け

公式ドキュメントの指針はシンプルです。

モード 向いているケース
auto シンプルな会話をするケースで有効。バックグラウンドで管理され、モデルの関与も追加のツール呼び出しも発生しない
agentic 何をコンテキストに残すかをモデル自身に判断させたい場合。固定閾値ではなくメッセージ単位で関連性を判断できる

agentic モードのコストは、テレメトリを読みツールを呼ぶためにモデルが消費するトークンです。今回の検証を踏まえると、agentic モードの自発的なツール利用は実行ごとのムラが大きく、そのムラをセーフティネットが確実に下支えする、という構造でした。

素の状態でもタスクは完走できますが、検証3で見たとおり、判断力を安定して引き出すにはピン留めや圧縮の方針をシステムプロンプトなどで与えることが前提になると思います。そのうえで、コンテキスト内の情報の重要度がタスクの進行とともに変わる長時間エージェントには agentic モードが向いています。単純な多ターンチャットであれば auto モードで十分、という整理になりそうです。

AgentCore Memory の Session Manager と併用してみる

ここまでで両モードの挙動はわかりましたが、実運用ではセッションの永続化もセットで考えたくなりますよね。ということで、AgentCore Memory を使った AgentCoreMemorySessionManager と Context Management が併用できるのか、併用した場合に退避や圧縮がどう扱われるのかも試してみました。

検証用に AgentCore Memory リソースを新規作成しておきます。短期記憶(イベント保存)だけ使うので、戦略の設定は不要です。

実行コマンド
aws bedrock-agentcore-control create-memory \
  --region us-west-2 \
  --name ContextManagementDemo \
  --event-expiry-duration 7

エージェントの組み立ては下記のような実装となります。context_manager と session_manager を同時に渡すだけです。

session_combo.py
from bedrock_agentcore.memory.integrations.strands.config import AgentCoreMemoryConfig
from bedrock_agentcore.memory.integrations.strands.session_manager import AgentCoreMemorySessionManager
from strands import Agent
from strands.models import BedrockModel

config = AgentCoreMemoryConfig(
    memory_id="ContextManagementDemo-XXXXXXXXXX",
    session_id="ctx-demo-session-001",
    actor_id="jinno-demo",
)
session_manager = AgentCoreMemorySessionManager(
    agentcore_memory_config=config, region_name="us-west-2"
)
model = BedrockModel(
    model_id="us.anthropic.claude-sonnet-4-5-20250929-v1:0", region_name="us-west-2"
)

agent = Agent(
    model=model,
    context_manager="agentic",
    session_manager=session_manager,
)

エラーなく併用はできました。context_manager が弾かれるのはステートフルなモデルとの組み合わせだけで、Session Manager とは普通に共存できます。

ただし、プロセスを再起動してセッションを復元してみると気になる挙動がありました。

検証1: 退避されたツール結果は再起動後も残るか

auto モードの検証で使った設計ドキュメントツールをもう一度使います。1 回目のプロセスでドキュメントを取得させてツール結果を退避させ、プロセスを終了して、2 回目のプロセスで同じ session_id からセッションを復元し、プレビュー外にあるリリース承認コードを質問してみました。

再起動後の実行結果
[復元されたメッセージ数: 4]
復元されたツール結果の先頭: [{"text": "[Offloaded: 1 blocks, ~1,806 tokens]...

Tool #1: retrieve_offloaded_content

Tool #2: get_design_doc

Tool #3: retrieve_offloaded_content
リリース承認コードは SAKURA-2026 です。

会話履歴は AgentCore Memory からきちんと復元され、退避プレビューも履歴に残っています。

ただ、エージェントが最初に呼んだ retrieve_offloaded_content の結果を確認してみると下記のように参照不可となりました。

Tool #1 の結果
Error: reference not found: mem_1_tooluse_PFc2nyXPSaOpBvEqmCl9Jq_0

退避先がインメモリストレージなので、プレビューに書かれた参照 ID は復元されても、実体は前のプロセスと一緒に消えているんですね。制限事項に書かれている通りの挙動です。なお、インメモリストレージはプロセス再起動だけでなく、デフォルトでは未アクセスのまま 20 サイクル経過した退避コンテンツも自動削除します。

エージェントは参照切れを検知すると自力で get_design_doc を呼び直してドキュメントを再取得し(ここで再度退避される)、新しい参照に対して retrieve_offloaded_content を呼んで正解に辿り着きました。元のツールが再実行可能であればモデルが自律的にリカバリーしてくれるわけですが、再実行できないツール結果を確実に持ち越したい場合は、S3Storage などの永続ストレージを明示的に構成する必要があります。

では本当に S3 なら残るのか、こちらも試してみます。plugins に S3Storage を指定した ContextOffloader を渡すと、自動構成のインメモリ版はスキップされて自分の設定が使われます。

s3_offloader.py
from strands.vended_plugins.context_offloader import ContextOffloader, S3Storage

offloader = ContextOffloader(
    storage=S3Storage(
        bucket="your-offload-bucket",
        prefix="offload/",
        region_name="us-west-2",
    ),
    max_result_tokens=1_500,
    preview_tokens=750,
)

agent = Agent(
    model=model,
    tools=[get_design_doc],
    context_manager="auto",
    plugins=[offloader],  # 自前の Offloader があると自動追加はスキップされる
    session_manager=session_manager,
)

1 回目のプロセスでドキュメントを退避させると、S3 バケットに実体が置かれていることが確認できます。

実行コマンド
aws s3 ls s3://your-offload-bucket/offload/
実行結果
2026-07-08 10:13:29      16420 1783473208232_1_tooluse_XRnxv20gg9YOMGgjTgzDHY_0

この状態でプロセスを再起動し、同じ質問をしてみます。

再起動後の実行結果
[復元されたメッセージ数: 4]

Tool #1: retrieve_offloaded_content
リリース承認コードは SAKURA-2026 です。

今度は参照切れが起きず、履歴に残っていた元の参照 ID がそのまま解決されて、ツールの再実行なしに 1 回の retrieve_offloaded_content で正解に辿り着きました!

ストレージを差し替えることで退避コンテンツをプロセスをまたいで持ち越せるようになりましたね!なお、S3 上の実体の保持期間が Session Manager と自動で連動するわけではないので、いつまで残すかはバケットのライフサイクル設定で管理する必要があります。

検証2: agentic モードの圧縮は永続化されるか

次に agentic モードです。少ないターン数で圧縮を再現するため、モデル config の context_window_limit を小さく設定したエージェントで、制約を伝えて pin_context でピン留めさせ、truncate_context による整理まで実行してからプロセスを終了し、セッションを復元してみました。

実行結果
--- 1回目のプロセス ---
[整理前のメッセージ数: 10]
Tool #2: truncate_context
[整理後のメッセージ数: 9]

--- 再起動後 ---
[復元されたメッセージ数: 14]

あれ、truncate で減らしたはずのメッセージが、復元すると全部戻ってきていますね・・・!

これは Session Manager がメッセージの追加をイベントとして順次記録していく append 型の仕組みであるためです。summarize_context や truncate_context はエージェントのメッセージリストを直接書き換えますが、この削除は記録済みのイベントには反映されず、復元時には保存された全イベントから履歴が再構築されます。つまり agentic モードの圧縮効果はプロセス内限りで、セッションを再開するとコンテキストは圧縮前のサイズに巻き戻ります。

再開後にまたコンテキストが厳しくなれば、モデルが再度圧縮すればよいのですが、長時間タスクを何度も再開するようなワークロードでは、毎回圧縮をやり直すトークンコストがかかる点は頭に入れておきたいところです。

併用時の整理

検証結果をまとめると下記のようになります。

観点 挙動
併用可否 問題なし。context_manager と session_manager は同時に指定できる
会話履歴 AgentCore Memory に永続化され、同じ session_id で復元できる
退避コンテンツ デフォルトはインメモリのため再起動で消える。参照切れ時はモデルが元ツールの再実行でリカバリーを試みる。S3Storage を明示構成すれば再起動後も元の参照がそのまま解決される
agentic の圧縮 プロセス内限り。復元すると圧縮前の履歴に巻き戻る

制限事項

利用にあたって 2 点注意があります。

  • ステートフルなモデル(会話状態をサーバー側で管理するモデル)では、context_manager にどちらのモードを設定しても ValueError が発生します。
  • 両モードが構成する Offloader はインメモリストレージを使うため、退避したコンテンツはプロセス再起動で消えます。また、プロセスが生きていても、デフォルトでは未アクセスのまま 20 サイクル経過した退避コンテンツは自動削除されます。Session Management と併用して退避コンテンツの永続化が必要な場合は、FileStorage や S3Storage を指定した ContextOffloader を明示的に構成してください。

おわりに

コンテキスト管理が context_manager="auto" の一行で実装できるのは嬉しいですね。簡単な反面、何が行われているのかしっかりと理解したいところです。

agentic モードはテレメトリで状況を見せて判断はモデルに委ねるというアプローチを見ると、コンテキスト管理はモデルが自律的に行うものへ移りつつあるのかなと感じました。

気軽に使えそうなので、使ってみて実体験によるメリット・デメリットや今後は実タスクでこの機能をどう活用できるのかもご紹介していきたいと思います!

本記事が少しでも参考になりましたら幸いです。最後までご覧いただきありがとうございました!

この記事をシェアする

関連記事