「頼んだことをしない」エージェントを前に、手が止まった
夜間に回しているスクリプトの出力を朝に開いたら、頼んだはずの集計ではなく、リポジトリのファイルを片端から読んだ形跡だけが残っていたのです。そんな経験をした方は、少なくないのではないでしょうか。
最初に疑うのは、たいていモデルです。賢さが足りないのか、指示が長すぎたのか、コンテキストが埋まったのか。私もプロンプトを書き直す方向に時間を使ってしまいがちでした。ところが先月、GitHub に上がっていた報告を読んで、疑う順番が違っていたかもしれないと感じました。
報告されている内容は、こうです。claude -p の後ろに --append-system-prompt を置くと指示どおりに動き、前に置くと -p のタスクが無視されてリポジトリを探索し始める、というものです。モデルも、システムプロンプトも、タスクも、ツールも同じまま、並びだけを入れ替えた比較で差が出たといいます。
最初にお伝えしたいのは、この現象が一般に起きると断定できる材料はまだない、ということです。ここでは「起きるかもしれない」前提で、自分の環境で数分で白黒をつける手順を書き残します。
報告の中身と、断定してはいけない範囲
報告者の環境を、読み取れる範囲で整理します。
| 項目 | 報告の内容 |
|---|---|
| Claude Code | 2.1.274 |
| OS / シェル | Windows 11 / PowerShell |
| モデル | Ollama 上のローカルモデル(Claude Code からは未認識のカスタムモデルとして扱われる) |
| 接続先 | Anthropic 互換のローカルエンドポイント |
| 起動オプション | --bare / --output-format stream-json / --verbose |
| 報告時点の状態 | Open・コメントなし |
報告者自身が、Anthropic のホスト型モデルでは確かめていないと明記しています。引数の解釈の問題なのか、互換エンドポイント側の経路の問題なのか、切り分けはついていません。私の手元でも再現は確認できておりません。ですから、これは「バグの解説」ではなく「同じ疑いを自分の環境で検査するための記事」です。
一方で、この報告には持ち帰れる部分がはっきりあります。モデルの出来を疑う前に、指示が届いているかどうかを、合図の一語で確かめるという検査の型です。
合図の一語で、指示の到達だけを測る
報告者が使った方法がそのまま応用できます。タスクを「この文字列だけを返して、ツールは使わないで」に絞り、返ってきた出力に合図の文字列が含まれるかを見るのです。
モデルの賢さを測るのではありません。指示が届いたかどうかだけを、機械的に判定します。届かなければ探索を始め、合図は出力に現れません。
次のスクリプトは、同じタスクを3通りの渡し方で3回ずつ投げ、合図が返った回数を数えます。
#!/usr/bin/env bash
# sentinel-order-check.sh
# 使い方: ./sentinel-order-check.sh ./core-agent.md
# 目的: タスクが届いたかを「合図の一語」で判定する(モデルの賢さは測らない)
set -u
SYS_FILE="${1:?システムプロンプトのファイルを渡してください}"
SYS_TEXT="$(cat "$SYS_FILE")"
TRIALS=3
# 合図は毎回変える。前回の出力を拾う誤判定を避けるため
SENTINEL="PURPLE-ELEPHANT-${RANDOM}${RANDOM}"
TASK="次の文字列だけをそのまま返してください。ほかには何も書かず、ツールも使わないでください: ${SENTINEL}"
count_hits() {
# 標準入力の出力に合図が含まれていれば 1、無ければ 0
if grep -qF -- "$SENTINEL"; then echo 1; else echo 0; fi
}
run_a() { # A: --append-system-prompt を先に置く
claude --append-system-prompt "$SYS_TEXT" -p "$TASK" 2>/dev/null
}
run_b() { # B: -p を先に置く
claude -p "$TASK" --append-system-prompt "$SYS_TEXT" 2>/dev/null
}
run_c() { # C: タスクは標準入力、システムプロンプトはファイル
printf '%s' "$TASK" | claude -p --append-system-prompt-file "$SYS_FILE" 2>/dev/null
}
for label in a b c; do
hits=0
for _ in $(seq "$TRIALS"); do
n="$(run_"$label" | count_hits)"
hits=$((hits + n))
done
echo "pattern ${label}: ${hits}/${TRIALS}"
doneなぜこう書くのか、3点だけ補足します。
合図に乱数を混ぜているのは、前回の出力やログに同じ文字列が残っていて、偶然「届いた」ことになるのを防ぐためです。3回ずつ回すのは、モデルの出力がぶれる分を差し引くためで、1回だけの結果で並びのせいだと決めつけると、今度は別の思い込みを作ってしまいます。そして C を入れているのは、並びの問題でも、長い文字列を引数で渡す問題でも、どちらにも依存しない経路を、対照として置いておくためです。
結果の読み方は単純です。A が 0/3 で B と C が 3/3 なら、並びか引数経由の渡し方に原因があります。3つとも同じなら、並びは無関係で、疑うべきは別の場所(モデル・エンドポイント・システムプロンプトの中身)に移ります。
並びに頼らない呼び出し方に寄せる
結果がどちらであっても、スクリプトの側でできる備えがあります。公式の CLI リファレンスとヘッドレス実行の説明で確認できる範囲だけを挙げます。
- タスクは標準入力から渡します。公式のヘッドレス実行の説明にも、
cat build-error.txt | claude -p '…'のように標準入力を渡す形が載っています(標準入力の上限は 10MB) - 長い追加指示は
--append-system-prompt-fileでファイルから読ませます。引数に長文を埋め込まずに済み、シェルのクォートの事故も減ります - 公式の例は
claude -pを先に書き、その後ろに--append-system-promptを続ける形です。迷ったら、公式の例と同じ並びに揃えておくのが無難です
報告者も最終的に、標準入力とファイル指定の組み合わせに落ち着いたと書いています。
# 並びに依存しにくい呼び出し(PowerShell でなく bash の例)
# タスクは標準入力、追加指示はファイル。権限は必要なものだけ許可する
cat ./task.md | claude -p \
--append-system-prompt-file ./prompts/core-agent.md \
--allowedTools "Read" \
--output-format jsonここで --allowedTools を最後に近い位置へ置いているのは、複数の値を取るオプションの後ろに、別の位置引数が続く形を避けたいからです。この並びが原因だと言い切る材料はありません。ただ、値を複数取れる指定の直後に自由な文字列を置かない、という書き方は、どの CLI でも事故を減らしてくれます。
検査をスクリプトの入口に常駐させる
私自身は、個人開発のアプリ運用で、ストア向けの文言の下書きなどを夜間にスクリプトから投げる使い方を想定して、この検査を入口に置いています。本番のタスクの前に、合図の一語だけを返させる呼び出しを1回走らせます。返らなければ、本番を実行せずに止めます。それだけです。
preflight() {
local token="PREFLIGHT-${RANDOM}${RANDOM}"
local out
out="$(printf '次の文字列だけを返してください: %s' "$token" \
| claude -p --append-system-prompt-file ./prompts/core-agent.md 2>/dev/null)"
if ! grep -qF -- "$token" <<<"$out"; then
echo "preflight failed: 指示が届いていません。本番を実行せずに停止します" >&2
return 1
fi
}
preflight || exit 1
# ここから本番のタスク1回分の呼び出しコストは増えますが、朝になって「何も頼んだ通りに動いていなかった」と知る側の損失のほうが、はるかに重いと感じています。
落とし穴
- 合図を固定文字列にしないでください。固定すると、ログに残った過去の出力で通ってしまいます
- 合否をモデルの出力の「長さ」や「それらしさ」で見ないでください。合図が含まれるかどうかの二択に絞ります
- 互換エンドポイントやカスタムモデルを使っている場合、並びの問題と、モデル側の追従性の問題は区別がつきません。Anthropic のホスト型モデルで同じ検査を走らせて、差が出るかを見てください
2>/dev/nullで標準エラーを捨てているので、検査が通らないときは一度外してエラーを読んでください
指示を書き直す前に、指示が届いたかを一語で確かめるようにしています。 順番を入れ替えるだけで、夜間の無駄な再実行がかなり減るのではないかと思います。
関連して、無人実行で「どのモデルが走ったか」を記録する話は別の記事にまとめています。
まずは今日の定期実行スクリプトに、preflight の10行だけを足すところから始めていただければと思います。私もそこから始めました。