はじめに
AgentCore でエージェントを作るとき、プロンプトは頑張って書いても、ツールの渡し方や記憶のさせ方、権限、ログの取り方は「動いたからそのまま」になりがちなんですよね。
この「モデルの周り」を設計する考え方が、最近よく聞くハーネスエンジニアリングです。
この記事は、2026年10月6日 (土) 13:00〜18:00に開催された、「JAWS-UG 新潟 #34 AWS Step Functions ハンズオン」で登壇した内容をもとにしています。AgentCore Harness を使う話ではありません。Runtime・Gateway・Memory・S3といった AgentCore の部品でハーネスを組むにはどうするかを、構成図とコードで解説していきます。
ハーネスエンジニアリングとは
モデル以外は全部ハーネス
ハーネスは、もともと馬具のことです。力の強い馬に手綱や鞍を付けて、狙った方向へ走らせるための道具ですね。
この「力の強い馬」が LLM です。馬そのものは強くても、手綱がないと思った方向には走ってくれません。モデルは賢いけれど、それだけでは仕事にならない、ということです。
LangChain はこれを Agent = Model + Harness という式で説明しています。
"If you're not the model, you're the harness."
(モデル以外はすべてハーネスだ。)
AWS も AgentCore Harness の GA のときに "If the model is the brain, the harness is the body"(モデルが脳なら、ハーネスは体)と書いていました。
つまり、ループ、ツール、記憶、実行環境、権限、観測など、モデル以外の全部をひっくるめてハーネスと呼んでいます。
プロンプト → コンテキスト → ハーネス
ハーネスエンジニアリングは、プロンプトエンジニアリング、コンテキストエンジニアリングの流れの先にあります。Anthropic は「コンテキストエンジニアリングは、プロンプトエンジニアリングが自然に進んだ先にある」と書いています。
3つは入れ子の関係です。
| 問い | 広まった時期 | 中身 | |
|---|---|---|---|
| プロンプトエンジニアリング | 何と言うか | 2022年ごろ〜 | 1回の指示文をどう書くか。役割、手順、出力形式 |
| コンテキストエンジニアリング | 何を見せるか | 2025年ごろ〜 | 手順書、ツール結果、過去の会話、メモリーなどから、毎回必要なものを選び抜いて渡す |
| ハーネスエンジニアリング | どんな環境で働かせるか | 2026年〜 | モデルの周りに、働く環境ごと作る |
コンテキストエンジニアリングまでは、「モデルに何を見せるか」の話でした。ハーネスエンジニアリングは、見せる仕組みに加えて、権限で止める、検証してやり直させる、といった「見せずに守る」仕組みまで含みます。
ハーネスの8つの要素と AgentCore
ハーネスの要素を8つに分けると、それぞれに AgentCore と Strands Agents の部品が対応します。
| 要素 | 何を決めるか | AgentCore / Strands の部品 |
|---|---|---|
| ループ | 推論とツール呼び出しをどう回し、どこで止めるか | Strands Agents |
| 実行環境 | どこで、どの単位で隔離して動かすか | AgentCore Runtime |
| ツール | 何を、どう渡すか | AgentCore Gateway |
| 境界 | 誰が呼べて、何をしてよいか | AgentCore Identity・Policy |
| 記憶 | 会話を越えて何を覚えるか | AgentCore Memory |
| 置き場所 | 作業中のファイルや長い結果をどこに置くか | session storage・S3 Files・S3 |
| 観測 | 何が起きたかをどう残すか | AgentCore Observability |
| 評価 | 品質をどう測るか | AgentCore Evaluations |
なお、ハーネスの話には2つの文脈があります。Claude Code や Codex のようなコーディングエージェントを使う側のハーネス(AGENTS.md、lint、テストなどでリポジトリを整える話)と、LLM アプリを提供する側のハーネス(実行基盤の話)です。この記事は後者を扱います。
ループエンジニアリング
8つのうち、ループの回し方と止め方に絞った「ループエンジニアリング」という言葉も出てきています。IBM の解説の定義はこうです。
"Loop engineering is the practice of designing agentic workflows, or loops, that iteratively guide AI agents toward completing user-defined goals with minimal human intervention."
(人の手をなるべく借りずに、決めたゴールへ AI エージェントを繰り返し導くループを設計すること。)
指示を出す、出来を確かめる、だめならやり直す、どこかで止める。Claude Code がファイルを読んで、書いて、テストして、エラーを見て直す、を繰り返しているのがまさにこれです。後半の GoalLoop がこの部分の部品になります。
部品は盛れば効くわけではない
では8つ全部を積めば完成かというと、そうではありません。
"every component in a harness encodes an assumption about what the model can't do on its own"
(ハーネスの部品はどれも「モデルはこれを自力でできない」という仮定の上にある。)
部品を1つ足すたびに、モデルの弱点を1つ仮定していることになります。モデルが賢くなればその仮定は外れて、部品はただの重りになります。実際に Anthropic も、モデルが新しくなったタイミングでハーネスの一部を外したそうです。
これを数字で確かめたのが、電通総研のテックブログです。
小さな銀行のコアサービスを題材に、ハーネスの部品を6種類に分け、それぞれ「あり」と「なし」で比べています。モデルは Opus と Haiku の両方です。
| 部品 | 結果 |
|---|---|
| 作業状態を state.json に外出しする | 効いた |
| 今の正しい資料はどれかを示す | 効いた(Opus で不合格21件 → 0.7件) |
| リトライの指示 | ほぼ効かず、コストとターン数が増えた |
| Skill | ほぼ効かず |
| 型チェックのフック | ほぼ効かず |
| 別エージェントによる点検 | Haiku には効いたが、コストは倍くらい |
効いた2つに共通するのは、モデルが知りようのない情報を外から渡す部品だったことです。考える役を肩代わりさせる部品は、モデルが自力でできることを重ねていただけでした。
なので、この記事では「全部入れる」ではなく、困った所に部品を足していく進め方をとります。
AgentCore で組むときの全体像
Harness と Runtime + Strands
AgentCore には、エージェントを動かす入口が2つあります。
| AgentCore Harness | Runtime + Strands | |
|---|---|---|
| 始め方 | 設定だけで動く | コードを書く |
| ループ | マネージド | 自分で書く |
| 部品のつなぎ | 宣言する | 自分でつなぐ |
| 育て方 | export で Strands のコードへ | そのまま育てる |
AgentCore Harness は2026年6月に GA しました。中身は Strands Agents で、Runtime の上で動くマネージドなループです。agentcore export harness で Strands のコードとして書き出すこともできます。
上の公式ドキュメントの比較表を見ると、Runtime 側では次の項目が「Custom」、つまり自分で実装するものになっています。
- Memory(短期・長期)、Gateway、Browser、Code Interpreter とのつなぎ込み
- コンテキストの切り詰め方、実行回数の上限
- 送信側の Identity、Observability、lifecycle hooks
裏を返すと、この表は Harness が内側でやっていることの一覧です。この記事では、これを Runtime と Strands で一つずつ組み直していきます。
全体の構成
最終的に組み上がる構成はこうなります。
| 部品 | この構成での役割 |
|---|---|
| Runtime | Strands で書いたエージェントを、セッションごとの microVM で動かす |
| Identity | アプリからの呼び出しを認証し、外部 API の鍵を預かる |
| Bedrock | モデルを呼ぶ |
| Gateway + Policy | ツールをまとめて公開し、呼んでよいかを判定する |
| Memory | ユーザーごとの好みや知見を覚える |
| S3 | 長いツール結果や共有する資料を置く |
| Observability | トレースを CloudWatch に集める |
| Evaluations | トレースを採点する |
4つの段階で育てる
この全体は一度に作らず、4つの段階で育てます。
- Runtime・Identity・Observability の最小構成で動かす
- トレースを見て、どこでつまずいているかを知る
- 症状に合う部品を足す
- 品質を評価し続ける
「部品は盛れば効くわけではない」を、そのまま手順にしたものです。
①最小構成で動かす
最初に入れるのは3つだけです。
| 部品 | 役割 | 最初から入れる理由 |
|---|---|---|
| Runtime | エージェントを動かす | 隔離の単位は後から変えにくい |
| Identity | 呼べる人と、使える権限を決める | 鍵をコードに書く作りは後から剥がしにくい |
| Observability | 何が起きたかを記録する | 以降の段階すべての材料になる |
この3つは、モデルの賢さとは関係がありません。モデルが賢くなっても要らなくならないので、最初から入れておきます。
Runtime はセッションごとに使い捨て
Runtime は、セッションごとに専用の microVM が立ちます。ユーザー A とユーザー B のセッションは別々の microVM で動き、終了時には microVM とメモリごと破棄されます。
セッションの寿命はこうなっています。
| 項目 | 値 |
|---|---|
| アイドルタイムアウト(既定) | 15分 |
| 最大の寿命(既定) | 8時間 |
| 変え方 |
LifecycleConfiguration で 60〜28,800秒(8時間)の範囲で設定 |
| 長い処理 |
/ping が HealthyBusy を返している間はアイドルで止まらない |
| 明示的に止める | StopRuntimeSession |
設計で大事なのは、セッションが終わると中身が消える前提で作ることです。作業中のファイルも、会話の履歴も、そのままでは残りません。残したいデータは最初から外に置く設計にしておきます。どこに置くかは③で解説します。
2026年9月に GA した Runtime(V2)は、スナップショットから復元して起動します。公式の発表では、コールドスタートの P75が1.9〜2.0秒で、V1の5.4〜30秒から大きく縮みました(中身が空のエージェントで測った値です)。
ただし、起動時に作った値はスナップショットに焼き込まれ、全インスタンスで同じになります。乱数、ID、トークン、現在時刻、認証情報などは、起動時ではなくリクエストを処理するハンドラの中で作る必要があります。ホスト名と PID も全インスタンスで localhost と 1 になるので、識別子には使えません。
8時間では足りない処理には、自分のアカウントの EC2で動かす Runtime Instances があり、最長14日動かせます。こちらは VPC が必須です。
Identity で、呼べる人と使える権限を分ける
Identity には、呼ばれる側(inbound)と呼ぶ側(outbound)の2つの向きがあります。
inbound 側では、エージェントを呼べる人を JWT(Cognito などの IdP が発行したもの)や IAM で絞ります。outbound 側では、エージェントが外部 API を叩くための OAuth トークンや API キーを Identity に預けます。こうしておけば、鍵をコードや環境変数に書かずに済みます。ハーネスの要素でいう「境界」は、まずここから始まります。
Observability は最初から入れておく
Observability を有効にすると、OpenTelemetry のトレースが CloudWatch に集まり、LLM の呼び出しとツールの呼び出しが一手ずつ並んで見えます。
このトレースは、②で失敗を見つける材料になり、④で品質を評価する材料にもなります。同じデータを2回使えるので、最初に入れておくのが一番お得です。
②トレースで失敗を見る
トレースを見ると、どこでつまずいているかが分かります。上の図はイメージですが、たとえばこういうことが、帯の長さや並び順から見えてきます。
- ログを取ってくるツールが38,000トークン返してくる
- その次の LLM の呼び出しがやたら長考して、判断が鈍る
- 最後に、承認を取らずに削除のツールを呼んでいる
症状が分かれば、効く部品はおのずと決まります。
| トレースに出る症状 | 原因 | 効く部品 |
|---|---|---|
| 長いツール結果のあとで判断が鈍る | コンテキストが重い | ContextOffloader で S3へ退避 |
| 会話の続きや作業中のファイルを忘れる | セッションが終わると消える | session storage・Memory |
| 触ってはいけない API を叩く | 止める仕組みがプロンプトにしかない | Gateway + Policy |
| できたと言うが、できていない | 結果を確かめていない | GoalLoop・Evaluations |
ここから先は、この表の右の列を順番に組んでいきます。
③症状に合わせて部品を足す
足すものは大きく3種類です。
- 長い結果や途中経過は、モデルの外(S3、Memory)に逃がす
- ツールや資料は全部渡さず、検索して選ばせる
- 危ない操作は、エージェントの外で止める
なぜコンテキストを軽く保つのか
まずは前提です。Chroma の検証では、Claude も GPT も Gemini も、入力が長くなるほど正確さが落ちています。context rot(コンテキストの劣化)と呼ばれている現象で、Anthropic の記事でも引用されています。
Anthropic は、コンテキストを「収穫逓減する有限の資源」として扱うべきだと書いています。長いツール結果をそのまま会話に積むと、その後のすべての推論がそれを読み続けることになります。なので、長いものは外に置いて、必要な所だけ読ませるのが基本方針になります。
置き場所は、残したい範囲によって3段に分かれます。
長いツール結果は ContextOffloader で S3に逃がす
長いツール結果は、Strands の ContextOffloader というプラグインで逃がせます。
動きはこうです。
- ツールが結果を返したタイミングでフックが動き、トークン数を数える
- 既定では2,500トークンを超えたら、全文を外の置き場所(ここでは S3)に保存する
- 会話には、先頭のプレビューと、保存した場所の参照だけを残す
- 続きが必要になったら、エージェントが
retrieve_offloaded_contentツールで要る所だけ取り出す
組み込みは、Agent の plugins に1つ足すだけです。
from strands import Agent
from strands.storage import S3Storage
from strands.vended_plugins.context_offloader import ContextOffloader
agent = Agent(
plugins=[
ContextOffloader(
storage=S3Storage("my-bucket", prefix="offload/"),
max_result_tokens=5_000, # 退避する基準。既定は 2,500
preview_tokens=2_000, # 会話に残すプレビュー。既定は 1,000
)
]
)
置き場所は S3のほかに、strands.storage の InMemoryStorage(メモリ)や LocalFileStorage(ローカルファイル)も選べます。Runtime ではセッションが終わるとローカルのファイルも消えるので、S3を選ぶのが自然です。
なお、S3 上のキーには offloader/ が自動で付くので、この例では offload/offloader/<toolUseId>_<連番> の形で保存されます。また既定では 20 サイクルたった古い退避分は消されるので(evict_after_cycles=20)、ずっと残したい結果の置き場所には向きません。
退避すると、会話にはこういう形で残ります。
[Offloaded: 1 blocks, ~38000 tokens]
(取り出し方の案内)
(先頭のプレビュー)
[Stored references:]
<ref> (text, 152340 chars)
retrieve_offloaded_content には、会話に残った参照(reference)を必ず渡します。そのうえで、pattern で正規表現を、line_range で行の範囲を指定でき、context_lines で前後の行も一緒に取れます。参照だけを渡せば全文が返ります。ERROR を含む行だけ読む、のような読み方を、エージェントが自分で選べるわけです。
import と引数は strands-agents 1.57.2 のソースで確かめています。ただ、S3Storage と組み合わせた公式サンプルは見つけられておらず、上のコードは仕様から組んだ例です。
作業中のファイルは session storage に置く
次は作業中のファイルです。エージェントにコードを書かせたり、ファイルを加工させたりすると、作業ディレクトリが要ります。でも Runtime は、セッションが終わると microVM ごと消えます。
session storage を使うと、セッションを止めても、同じセッション ID で再開すればファイルが戻ってきます。書き込みはセッション中に裏で複製されていて、止めるときに残りを書き出す仕組みです。
設定は、Runtime の filesystemConfigurations にマウント先のパスを指定するだけです。
aws bedrock-agentcore-control update-agent-runtime \
--agent-runtime-id <runtime-id> \
--agent-runtime-artifact <既存と同じ設定> \
--role-arn <実行ロールの ARN> \
--filesystem-configurations \
'[{"sessionStorage":{"mountPath":"/mnt/workspace"}}]'
VPC も追加の IAM も要りません。Strands の FileSessionManager の storage_dir をこのパスにすれば、会話の履歴も一緒に残せます。
| 項目 | 内容 |
|---|---|
| 状態 | プレビュー(プレビュー中は無料) |
| 容量 | 1セッション1GB |
| 分離 | セッション単位。ほかのセッションからは見えない |
| リセット | 14日使わないか、Runtime のバージョンを更新したとき |
公式ドキュメントでは、ファイルの状態は session storage、会話履歴や学んだ知見は Memory という切り分けになっています。
エージェント同士の共有は S3 Files で
ただ、session storage はセッションの中に閉じた置き場所です。エージェント同士で資料を受け渡したいときは、S3 Files をマウントします。
エージェント A が reports/ に書いて、エージェント B がそれを読む、という形です。S3 Files は S3のバケットと双方向に同期するので、ほかのシステムからは普通の S3として読めます。
AWS の Storage Blog には、中間結果をプロンプトではなく S3 Files 上のファイルで受け渡すマルチエージェントの構成例が出ています。ブログの言葉を借りると "files become working memory that persists after a session ends"(ファイルが、セッションが終わっても残る作業記憶になる)です。
こちらは session storage と違って、ネットワークの準備が要ります。
| 項目 | 必要なもの |
|---|---|
| ネットワーク | Runtime を VPC モードにし、マウントターゲットと同じ AZ のサブネットに置く |
| セキュリティグループ | NFS なので TCP 2049を開ける |
| IAM | Runtime のロールに s3files:ClientMount、s3files:ClientWrite、s3files:GetAccessPoint
|
運用上の注意も2つあります。
- 同じファイルに複数のセッションが同時に書くとぶつかるので、ファイル名はセッション ID などで分ける
- S3側への反映は、書き込みが止まってから約60秒後。書いた直後に S3 API で読もうとすると、まだないことがある
ユーザーの好みは Memory に覚えさせる
ファイルではなく、「このユーザーは回答を箇条書きで欲しがる」のような知見を会話をまたいで覚えさせたいときは、AgentCore Memory を使います。
Memory は2段になっています。
- 会話を保存すると、まず短期記憶(生のイベント)としてそのまま残る。単位は actorId と sessionId で、保持期間は Memory を作るときに3〜365日の範囲で決める(
eventExpiryDuration) - 選んだ戦略に沿って、裏で非同期に要点を抜き出し、長期記憶として貯める
- 次の会話のときに長期記憶を検索して、関係のあるものだけをコンテキストに足す
長期記憶の戦略は4種類です。
| 戦略 | 貯めるもの |
|---|---|
| SUMMARIZATION | 会話の要約 |
| SEMANTIC | 事実 |
| USER_PREFERENCE | ユーザーの好み |
| EPISODIC | 出来事の流れ |
Strands からは、AgentCore の SDK にある AgentCoreMemorySessionManager でつなぎます。
from strands import Agent
from bedrock_agentcore.memory.integrations.strands.config import (
AgentCoreMemoryConfig,
RetrievalConfig,
)
from bedrock_agentcore.memory.integrations.strands.session_manager import (
AgentCoreMemorySessionManager,
)
config = AgentCoreMemoryConfig(
memory_id=MEM_ID,
session_id=SESSION_ID, # Runtime のセッション ID を渡す
actor_id=ACTOR_ID, # ユーザーを表す ID
retrieval_config={
"/preferences/{actorId}/": RetrievalConfig(top_k=5, relevance_score=0.7),
},
)
agent = Agent(
session_manager=AgentCoreMemorySessionManager(config, region_name="us-east-1")
)
namespace は「誰の、何の記憶か」を分ける住所のようなもので、{actorId} の部分がユーザーごとに置き換わります。この例では、好みを /preferences/{actorId}/ から上位5件、関連度0.7以上だけ取ってきています。
ちなみに AgentCore Harness の Memory 連携は、呼び出しのたびに長期記憶を topK=10、relevanceScore=0.2で取ってきて、推論の前に文脈へ足しているそうです。RetrievalConfig の既定値も同じ 10 件・0.2 なので、何も指定しなければ Harness と同じ取り方になります。自分で組む場合は、どれだけ取ってくるかを自分で決められるのが利点です。取りすぎればまたコンテキストが重くなるので、ここは調整のしどころです。
置き場所の選び方
ここまでの置き場所を並べるとこうなります。
| ContextOffloader(S3) | session storage | S3 Files | Memory | |
|---|---|---|---|---|
| 置くもの | 長いツール結果 | 作業中のファイル | 共有する資料 | 好みや知見 |
| 共有の範囲 | エージェント | 1セッション | エージェント間 | ユーザー単位 |
| 用意するもの | バケットと plugins の設定 | mountPath だけ | VPC と IAM | 戦略を選ぶ |
| 状態 | — | プレビュー(1GB) | GA | GA |
誰と共有したいか、いつまで残したいかで選ぶと迷いにくいです。
ツールも資料も、検索して取ってこさせる
コンテキストを軽くする考え方は、ツールにも使えます。ツールの説明はそれだけでトークンを食うので、何十個も並べると、それだけでコンテキストが重くなります。
そこで、最初から全部を渡すのではなく、要るものをエージェントに検索させます。Gateway のツール検索、ContextOffloader の部分取得、Memory の検索。この3つはどれも、Anthropic が "just in time" と呼んでいる「必要になったら取りに行く」仕組みです。
Gateway を作るときに searchType を SEMANTIC にしておくと、x_amz_bedrock_agentcore_search という検索用のツールが1つ増えます。
{
"method": "tools/call",
"params": {
"name": "x_amz_bedrock_agentcore_search",
"arguments": { "query": "find order information" }
}
}
エージェントは、まずこのツールに「注文情報を探したい」のように聞いて、要るツールだけを受け取ります。ちなみに、AgentCore CLI で作った Gateway では最初から有効です。
危ない操作は Gateway の Policy で止める
次は危ない操作です。
プロンプトに「削除はするな」と書いても、モデルの解釈次第で破られることがあります。プロンプトでの禁止は、モデルが読んで判断するお願いでしかありません。
なので、止めるルールはエージェントのコードの外、Gateway に置きます。判定がコードの外にあれば、モデルがどう解釈しても判定そのものは変えられません。
Gateway の中では、この順番で処理されます。
2026年8月に入ったレート制限(RPM や TPM などの上限)は、Policy の判定より前にかかります。interceptor との前後関係は、公式ドキュメントからは読み取れませんでした。
ルールは Cedar で書きます。運用ツールの delete_instance を誰にも呼ばせないルールはこうです。
forbid(
principal,
action == AgentCore::Action::"OpsTarget___delete_instance",
resource
);
action の名前は「ターゲット名 + アンダースコア3つ + ツール名」の形になります。
判定のルールはシンプルです。
| 状況 | 結果 |
|---|---|
| forbid が1つでも当たる | 拒否 |
| permit が当たり、forbid は当たらない | 許可 |
| どれも当たらない | 拒否(既定で拒否) |
拒否されると、エージェントにはエラーの結果(isError: true)が返るので、エージェントは呼べなかったことが分かります。
いきなり本番で止めるのが怖い場合は、LOG_ONLY モードで「何が止まるか」をログで確かめてから、ENFORCE に切り替えられます。
「残高を見てから送金」もルールで書ける
2026年8月に入った Temporal policies(時間的ポリシー)を使うと、セッションの中で過去に何をしたかを条件にできます。こちらは Cedar 互換の Dogwood という言語で書き、作るときは definition.policy.statement に入れます。公式ドキュメントの例で、直近1時間に同じ口座の残高照会をしていなければ送金を許さない、というルールです。
permit (
principal,
action == AgentCore::Action::"FundsTarget___transfer_funds",
resource == AgentCore::Gateway::"<arn>"
)
when temporal {
formerly within 1h
AgentCore::Action::"FundsTarget___get_account_balance"
::response{
eventResource: resource,
output.accountId: context.input.toAccount
}
};
演算子は formerly within のほかに since within、count、sum などがあり、「5分以内の送金が3回を超えたら止める」のような回数の条件も書けます。この回数には、今まさに判定している呼び出しも含まれます。
ただし、履歴はセッション単位です。セッションはセッション ID とユーザーの組で決まり、遡れるのは24時間までです。セッション ID が変わると数え直しになるので、これだけで確実に止められるわけではないんですよね。公式ドキュメントも、回数の条件はセキュリティ制御ではないと注記しています。JWT の claim、IAM、ツール、モデルなどの単位で RPM や TPM を絞れるレート制限と組み合わせるのが現実的だと思います。
④品質を評価し続ける
最後の段階は、品質を測り続けることです。ここには「実行したあとに測る」ものと「実行中に確かめる」ものがあります。
Evaluations で実行後に測る
AgentCore Evaluations には使い方が3つあります。
| 使い方 | モード | 中身 |
|---|---|---|
| CI の品質ゲート | on-demand | 正解(ground truth)をつけて採点する |
| 本番の見張り | online | 本番のトレースを抜き取って採点する(0.01〜100%、既定は10%) |
| 決まった条件の確認 | Lambda で書く評価器 | 合否と点数を自分のコードで返す |
CI については、AWS のブログに GitHub Actions を使った構成例が出ています。
本番では online のモードで抜き取って採点し、結果は CloudWatch のメトリクス(名前空間は Bedrock-AgentCore/Evaluations)に出るので、スコアが落ちたらアラームを鳴らせます。どちらも、①で入れた Observability のトレースが材料です。
GoalLoop で実行中に合否を出す
Evaluations は実行したあとの評価です。実行中に確かめて、その場で直させたいときは Strands の GoalLoop を使います。
エージェントが応答するたびに検証役が合否を出します。合格ならそのまま返し、不合格なら、何がだめだったかのフィードバックを次の指示にしてもう一度やらせます。ループエンジニアリングの「出来を確かめる、だめならやり直す」の部分です。
from strands import Agent
from strands.vended_plugins.goal import GoalLoop
def word_count_validator(response, agent):
text = " ".join(
block["text"] for block in response["content"] if "text" in block
)
words = len(text.split())
if words <= 50:
return True
return {"passed": False, "feedback": f"Too long ({words} words). Cap at 50."}
agent = Agent(
plugins=[GoalLoop(goal=word_count_validator, max_attempts=5, timeout=30.0)]
)
検証役の関数は、エージェントの最後の応答と、エージェント自身を受け取ります。合格なら True、不合格なら passed と feedback を持つ辞書を返します。上の関数は、Strands の docstring にある例そのままです。goal には、こういう関数のほかに、自然言語の条件を渡せば別のモデルが判定してくれます。
ここで大事な注意が1つあります。GoalLoop は既定では回数も時間も無制限です(どちらも float("inf"))。両方を省略すると警告は出ますが、そのまま合格するまで回り続けます。max_attempts と timeout は必ず決めておきましょう。
steering でツールを呼ぶ前に口を挟む
もう1つ、Strands の steering という仕組みもあります。こちらはツールを呼ぶ前に口を挟めます。
from strands import Agent
from strands.vended_plugins.steering import Guide, Proceed, SteeringHandler
class OpsSteering(SteeringHandler):
async def steer_before_tool(self, *, agent, tool_use, **kwargs):
if tool_use["name"] == "delete_instance":
return Guide(reason="先に承認を取ること")
return Proceed(reason="ok")
agent = Agent(tools=[...], plugins=[OpsSteering()])
steering も ContextOffloader や GoalLoop と同じプラグインなので、plugins に渡して組み込みます。
| 返す値 | 起きること |
|---|---|
| Proceed | そのまま呼ぶ |
| Guide | 呼ぶのをやめ、指示をエージェントに返す |
| Interrupt | 人に判断を渡す |
Guide を返すとツールの呼び出しは取り消され、エージェントには "Tool call cancelled. 先に承認を取ること You MUST follow this guidance immediately." という形で理由が届きます。ただ止めるだけでなく「なぜだめか」が伝わるので、エージェントが自分で手順を直せます。
どこで止めて、どこで確かめるか
ここまでで、止める・確かめる仕組みが4つ出てきました。置き場所とタイミングで整理するとこうなります。
| 仕組み | 動く場所 | タイミング | 向いている用途 |
|---|---|---|---|
| Gateway の Policy | エージェントのコードの外 | ツールを呼ぶ前 | 絶対に通してはいけない操作 |
| steering | エージェントのコードの中 | ツールを呼ぶ前 | 手順の誘導、人への確認 |
| GoalLoop | エージェントのコードの中 | 応答のあと | その場でのやり直し |
| Evaluations | AgentCore 側 | 実行のあと | リリースの判断、本番の監視 |
steering は柔軟ですが、コードの中にある以上、コードやプロンプトの作り次第で抜けることがあります。絶対に止めたいものは Gateway の Policy、誘導したいものは steering、と使い分けるのがよいと思います。
部品は困ってから足す
ここまでの話をひとことで言うと、部品は困ってから足すです。
| 部品 | いつ入れるか |
|---|---|
| Runtime・Identity・Observability | 最初から |
| ContextOffloader・session storage・S3 Files・Memory | トレースで「重い」「忘れる」が見えてから |
| Gateway のツール検索・Policy | ツールが増えてから、危ない操作が出てきてから |
| GoalLoop・steering・Evaluations | 品質の基準が決まってから |
そして、モデルが賢くなったら、外せる部品がないかを見直します。電通総研の検証や Anthropic の話のとおり、使っていない部品はただの重りになるからです。
さいごに
ハーネスエンジニアリングは、部品をたくさん積む技術ではなく、モデルが知りようのないものを渡し、モデルに任せてはいけないものを外で止める技術だと私は捉えています。
Runtime と Strands で自分で組むと、どこに何を足したかがコードとトレースで全部見えるので、「この部品は本当に効いているのか?」を確かめながら育てられるのが良いところです。
AgentCore を、ハーネスを組む基盤としてぜひ使ってみてください。
宣伝
その1
新潟支部ではオンラインのLT会を11月に開催します!
ぜひ参加ください!
その2
技術書展で本書きます!
こちらもぜひ!
参考
















