ある夜、四つのサイトの下書きをまとめて生成していたときのことです。
最初の数本は指定どおりの見出し構成で返ってきました。ところが二十本目あたりから、頼んでいないはずの「まとめ」節が静かに混ざり始めていたのです。エラーは出ません。モデルは自信たっぷりに、少しずつ違うものを書いていました。
気づいたのは翌朝、差分を眺めていたときでした。胸がざわつきました。壊れていたのではなく、指示が薄れていたのです。
長い会話ほど、システムプロンプトの効き目は静かに落ちていきます。その「効かなくなり方」を感覚ではなく数値で捉え、プロンプトの構造と会話途中の手当てで立て直すまでを、動くコードと実測値でたどっていきます。
なぜ長い会話で指示は薄れるのか
指示が効かなくなる背景には、いくつかの独立した力が重なっています。
ひとつは位置の問題です。システムプロンプトは会話の先頭に固定されますが、ターンが積み上がるほど、モデルが直近の文脈に引きずられる度合いが増します。先頭のルールは相対的に「遠く」なります。
もうひとつは希釈です。二十ターンぶんのやり取りが入ると、最初に置いた十行の制約は、全体のごくわずかな割合になります。ルールの絶対量は変わらなくても、比重が落ちるのです。
そして競合です。会話の中で新しい要望や例外を挟むと、それが暗黙の上書き指示として働き、元のルールと静かに衝突します。
これらは「モデルが忘れる」という単純な話ではありません。追従の優先順位が、文脈の重心とともにずるずると移動していく現象です。だからこそ、まず測ることから始めます。
追従度を数値で捉える
感覚で「なんとなく崩れてきた」と言っている限り、対策の効果も検証できません。そこで、ルールを機械的に判定できるチェックの集合として定義し、会話のターンごとに追従率を出すハーネスを用意します。
ここでは題材として「見出しは必ず H2 で始める」「箇条書きを使わない」「回答の最後に出典を書かない」の三つを守らせる想定にします。判定は正規表現などの決定的なチェックで行い、モデルの自己申告には頼りません。
import os
import re
from anthropic import Anthropic
client = Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
SYSTEM_PROMPT = """あなたは技術記事の下書きを書くアシスタントです。
次のルールを常に守ってください。
1. 見出しは必ず ## から始めてください。
2. 箇条書き(- や * の行頭)を使わないでください。
3. 回答の末尾に「出典」や「参考」の節を付けないでください。"""
# 各ルールを決定的な関数で判定する(True = 追従できている)
def rule_heading(text: str) -> bool:
return bool(re.search(r"^##\s", text, re.MULTILINE))
def rule_no_bullets(text: str) -> bool:
return not re.search(r"^\s*[-*]\s", text, re.MULTILINE)
def rule_no_sources(text: str) -> bool:
return not re.search(r"(出典|参考文献|References)", text)
CHECKS = {
"heading": rule_heading,
"no_bullets": rule_no_bullets,
"no_sources": rule_no_sources,
}
def adherence(text: str) -> float:
passed = sum(1 for fn in CHECKS.values() if fn(text))
return passed / len(CHECKS)次に、同じ「探り」の依頼を会話の各ターンで投げ、そのつど追従率を記録します。会話履歴は積み上げ続けるので、ターンが進むほど文脈は長くなります。探りの依頼文は毎回ほぼ同じにして、変化がプロンプトの長さ由来であることを切り分けます。
PROBE = "この見出しに続く本文を200字程度で書いてください。テーマは任意で構いません。"
def run_session(turns: int, reanchor_every: int = 0):
history = []
curve = []
digest = "厳守: (1)見出しは##から (2)箇条書き禁止 (3)末尾に出典節を付けない"
for t in range(1, turns + 1):
# 一定間隔でルール要約を会話に打ち直す(0なら無効)
if reanchor_every and t % reanchor_every == 0:
history.append({"role": "user", "content": digest})
history.append({"role": "assistant", "content": "承知しました。厳守します。"})
history.append({"role": "user", "content": PROBE})
resp = client.messages.create(
model="claude-sonnet-5",
max_tokens=400,
system=SYSTEM_PROMPT,
messages=history,
)
out = resp.content[0].text
history.append({"role": "assistant", "content": out})
curve.append((t, adherence(out)))
return curveこのハーネスの肝は、判定を人間の目やモデルの自己評価に委ねない点にあります。追従率が 1.0 なら全ルール順守、0.66 ならひとつ崩れている、と一目で分かります。まず run_session(50) を素の状態で走らせて、崩れの形を観察します。