みなさんStrands AgentsのBlogをご存知でしょうか?
私は普段Strands Agentsのサイトを開くとき、見るのはいつもドキュメントかサンプルコードでした。知りたい機能を調べて、動くコードが見つかったらページを閉じる。そんな使い方をしていたので、Blogはほとんど開いたことがありませんでした。
ある日ふとBlogを覗いてみると、Introducing Harness Optimizer: Automatically optimize your agent harnessという記事が目に留まりました。エージェントのハーネスを自動で最適化する? 面白そうだったので読み進めてみました。
エージェントを作るハードルは下がった
生成AIエージェントを動かすまでのハードルは、ここ最近でかなり下がりました。
例えばAmazon Bedrock AgentCore Harnessでは、モデル、システムプロンプト、ツール、Skillなどを設定として宣言できます。エージェントループや実行環境はマネージドで提供されるため、オーケストレーションコードやコンテナを一から作らなくてもエージェントを構成できます。
出典: AgentCore harness - AWS公式ドキュメント
Strands Agentsでも、モデルとツール、システムプロンプトを組み合わせるだけでエージェントを作れます。
agent = Agent(
model=model,
tools=[list_transport_options],
system_prompt=INITIAL_SYSTEM_PROMPT,
)
こうした仕組みのおかげで、開発者だけでなく業務を知るBIZ側の担当者もエージェント作成に関わりやすくなってきました。
育てるのは簡単じゃない
ただ、エージェントを作れることと、業務で期待どおりに動き続けることは別の話です。
業務には原則と例外があります。「問い合わせにはFAQに基づいて回答する」が原則でも、製品不具合の報告にはFAQを飛ばしてエスカレーションする。こうした例外は、運用してみて初めて見つかることも少なくありません。
例外や業務知識、Skillやツールの使い方を正しく反映して、業務を遂行できるエージェントにチューニングしていくのは簡単ではありません。モデルやツールを変更すれば、それまでの指示で同じように動くとも限りません。
Harness Optimizerとは
Harness Optimizerrは、エージェントのシステムプロンプト、ツール定義、Skillなどを「Agent Harness」として捉え、実行結果から改善していくオープンソースライブラリです。モデルそのものを再学習するのではなく、モデルを取り囲むハーネスを育てるという発想です。
エージェントを実行し、結果をReward(報酬関数)で評価し、成功と失敗の違いからシステムプロンプトなどを更新する。機械学習の「実行→評価→パラメータ更新」に似たループを、エージェントの振る舞いの改善に適用します。
出典: Introducing Harness Optimizer
図中のFormulaは、ハーネスの中で最適化する対象を表しますエージェントを実行し、Rewardで評価し、その結果からシステムプロンプトを更新する流れを繰り返します。
今回はSystemPromptFormulaを使い、ツール定義などは固定したまま、システムプロンプトだけを最適化の対象にしました。公式ブログでは、Skill、Memory、ツールを対象とするFormulaも今後追加予定とされています。
更新の方法もシンプルです。成功した実行と失敗した実行を比較し、「成功側にあって失敗側にない判断パターン」を見つけて、それをシステムプロンプトへ追加します。
出典: Introducing Harness Optimizer
実行結果を成功と失敗に分け、両者の行動や判断を比較します。今回でいえば、ケースAの成功とケースBの失敗を比較し、確認するルールを取り出します。
今回のシナリオ
シナリオの舞台は私が住む富山県高岡市にしました。旅行者が10:00に新高岡駅を出発し、高岡市美術館へ向かいます。
利用できる交通手段はバスとタクシーの2つです。
| 交通手段 | 料金 | 所要時間 | 到着時刻 |
|---|---|---|---|
| バス | 300円 | 25分 | 10:25 |
| タクシー | 1,200円 | 10分 | 10:10 |
会社の出張規程は次のとおりです。
原則として最安の公共交通を使う。ただし、公共交通では訪問時刻に間に合わない場合、タクシーの利用を許可する。
この規程に対して、確認するケースを2つ用意しました。
| ケース | 訪問時刻 | 期待する選択 | 理由 |
|---|---|---|---|
| A | 10:40 | バス | バスでも間に合う |
| B | 10:15 | タクシー | バスでは10分遅れる |
ケースAは原則どおり最安のバスを選べばよいケースです。ケースBは、バスでは間に合わないためタクシーを選ぶ必要がある例外ケースです。
今回の問いはこうです。
Harness Optimizerは、最安のバスを選んで訪問時刻に遅れる失敗から、タクシーを利用する例外ルールをシステムプロンプトへ追加できるか。
なお、料金や所要時間は実験用の固定データです。実在の交通情報ではありません。
ツールとReward
エージェントが使うツールは、今回の実験用に自作したlist_transport_optionsだけです。外部APIには接続せず、固定データからバスとタクシーを返します。
@tool
def list_transport_options(
origin: str,
destination: str,
departure_at: str,
) -> dict[str, Any]:
fixture = load_fixture()
if (
origin != fixture["origin"]
or destination != fixture["destination"]
or not _departure_matches(departure_at, fixture["departure_at"])
):
return {"status": "not_found", "options": []}
return {
"status": "found",
"origin": fixture["origin"],
"destination": fixture["destination"],
"departure_at": fixture["departure_at"],
"options": copy.deepcopy(fixture["options"]),
}
最終結果は自然文ではなく、構造化された形式で受け取ります。
class TransportDecision(BaseModel):
selected_transport_id: Literal["bus", "taxi"]
arrival_at: datetime
price_yen: int
reason_code: Literal[
"cheapest_public_transport",
"public_transport_misses_visit_time",
]
「タクシーも検討しました」のような文章表現ではなく、実際に何を選んだかを機械的に評価できるようにしています。
Rewardは出張規程をそのまま判定条件にしました。次の条件をすべて満たせば1、1つでも失敗すれば0です。
-
ツールが
foundを返し、バスとタクシーの2候補を取得した -
選択した交通手段の到着時刻と料金が固定データに一致した
-
バスで間に合うケースAではバスを選んだ
-
バスでは間に合わないケースBではタクシーを選んだ
failures = []
if not transport_options_checked:
failures.append("transport_options_not_checked")
if selected_arrival > visit_at:
failures.append("selected_option_misses_visit_time")
if expected_transport_id == "taxi" and selected_id != "taxi":
failures.append("taxi_exception_not_applied")
return Reward(
reward=1.0 if not failures else 0.0,
metadata={
"failed_checks": failures,
"observed": observed,
"expected": expected,
},
)
0という点数だけでなく、何が原因で失敗したかもmetadataへ残すのがポイントです。
Before: 遅れると分かっていてもバスを選んだ
最適化前のシステムプロンプトには、出張規程の原則だけを書きました。
出張時は、利用可能な公共交通の中から最も安い手段を選んでください。
このプロンプトで2ケースを実行した結果です。
| ケース | 訪問時刻 | 選択 | 到着時刻 | Reward |
|---|---|---|---|---|
| A | 10:40 | バス | 10:25 | 1 |
| B | 10:15 | バス | 10:25 | 0 |
ケースAは期待どおりです。バスで訪問時刻に間に合い、タクシーより安いので、Rewardは1になりました。
ケースBでは、エージェントは次の事実を正しく認識していました。
-
バスは10:25到着なので、10:15の訪問時刻に10分遅れる
-
タクシーなら10:10に到着できる
それでも、最終的に選んだのはバスでした。「最も安い手段を選ぶ」というシステムプロンプトをそのまま優先したためです。
Rewardには次の失敗理由が記録されました。
selected_option_misses_visit_time
taxi_exception_not_applied
モデルは必要な情報を取得し、遅刻も認識しています。足りなかったのはモデルの知識ではなく、「時間に間に合わない場合は原則を上書きする」という判断ルールでした。
Optimizer実行: 成功と失敗を比較する
ケースAの成功トレースとケースBの失敗トレースを、Harness Optimizerへ渡しました。
formula = SystemPromptFormula(
system_prompt=INITIAL_SYSTEM_PROMPT,
)
optimizer = ContrastiveReflectionOptimizer(
formula,
system_prompt_template=load_builtin_template(
"contrastive_reflection/system_prompt.jinja"
),
task_message_template=load_builtin_template(
"contrastive_reflection/task_message_system_prompt.jinja"
),
model_config={
"model_id": HAIKU_4_5_MODEL_ID,
"max_tokens": 32000,
"temperature": 1.0,
"streaming": True,
},
region_name="ap-northeast-1",
n_sample_traces=-1,
stratified_sampling=True,
success_threshold=0.5,
)
optimizer.add_rollouts([case_a_rollout, case_b_rollout])
optimizer.add_rewards([case_a_reward, case_b_reward])
optimizer.step()
candidate = formula.get_tunable_params()["system_prompt"]
step()を1回実行した結果、Optimizerは元の日本語プロンプトを残したまま、Learned Behaviorsを追加しました。生成されたルールの中心部分です。
WHEN the public transportation option will arrive AFTER the scheduled
visit time, DO NOT select it regardless of cost.
WHEN all public transport options miss the visit time,
DO select private transportation (taxi) as the fallback option.
The implicit hierarchy is:
(1) arrive on time,
(2) among on-time options, choose the cheapest public transport.
日本語で整理すると、次の判断順序です。
1. 交通手段の到着時刻と訪問時刻を比較する
2. 間に合う公共交通があれば、その中から最安を選ぶ
3. 公共交通では間に合わない場合、タクシーを選ぶ
学習させたかった例外ルールが、そのまま追加されています。今回は生成された候補を編集せず、そのままAfterのシステムプロンプトへ反映しました。
After: ケースBだけタクシーへ変わった
更新後のシステムプロンプトで同じ2ケースを再実行しました。
| ケース | Before | After | 変化 |
|---|---|---|---|
| A: 10:40まで | バス、Reward 1 | バス、Reward 1 | 最安の原則を維持 |
| B: 10:15まで | バス、Reward 0 | タクシー、Reward 1 | 時刻の例外を適用 |
ケースAでは、バスで間に合うため、更新後も300円のバスを選びました。ケースBでは、バスの10:25到着と訪問時刻10:15を比較し、10:10に到着できるタクシーを選びました。
ケースBの構造化出力です。
{
"selected_transport_id": "taxi",
"arrival_at": "2026-08-08T10:10:00+09:00",
"price_yen": 1200,
"reason_code": "public_transport_misses_visit_time"
}
最安の公共交通を選ぶ原則は残しつつ、間に合わない場合だけタクシーへ切り替えられました。
一方で、プロンプトが長くなった分、使用tokensは増えています。
| フェーズ | 合計cycles | 合計tokens |
|---|---|---|
| Before | 4 | 6,987 |
| After | 4 | 7,694 |
成功件数は1/2から2/2へ変わり、合計tokensは707増えました。
実際に試して分かったこと
今回の実験で一番印象に残ったのは、「モデルが間違いに気づいていること」と「正しい行動を選べること」は別だという点です。
BeforeのケースBでは、モデルはバスでは遅れると分かっていました。タクシーなら間に合うことも分かっていました。それでも、システムプロンプトに例外がないためバスを選びました。最終回答だけを見れば「交通手段の選択を間違えた」ですが、実行トレースを見ると、問題はルールの優先順位にあると分かります。
Harness Optimizerは、この成功と失敗の差から「時間制約を先に確認する」というルールを取り出しました。
まとめ
バスとタクシーの選択という小さな実験でしたが、Harness Optimizerの「実行→評価→プロンプト更新」のループを一通り体験できました。
印象に残ったのは、モデルは遅刻すると分かっていたのにバスを選んだこと。足りなかったのはモデルの知識ではなく、システムプロンプトにある判断ルールでした。Harness Optimizerは、この「ルールの不足」を成功と失敗の比較から見つけ出し、プロンプトへ追加してくれました。
今回の実験では、ツールやRewardの実装にコードが必要でした。一方で、改善の起点になったのは、「何を正しい行動とするか」という業務ルールです。業務担当者が正しい行動を定義し、開発者がRewardへ落とし込み、生成された改善候補を一緒にレビューする。そんな役割分担でエージェントを育てていける可能性を感じました。
今回はシステムプロンプトだけを更新しましたが、今後MemoryやSkillといったハーネスの他の要素にも対応が広がれば、エージェントを育てる選択肢がさらに増えそうです。



