毎月の月初に、個人開発で運用しているアプリ4本ぶんの月次運用レポートを Claude API で生成しています。クラッシュの傾向、ストアレビューへの返信記録、AdMob の収益変動。ひと月分を一つのドキュメントに整形すると、日本語で4万字近くになります。
この長さになると、悩みの種は出力上限との付き合い方でした。生成が途中で切れるたびに「ここまでの続きから書いてください」と投げ直し、つなぎ目を読み直して整える。見出しのレベルがずれていたり、同じ注意書きが二度現れたり、後半だけ文体が少し変わっていたり。この継ぎ作業に、毎月20分ほど取られていました。
6月9日に公開された Claude Fable 5 は、最大 128,000 トークンの出力に対応しています。6月22日までは主要プランに追加費用なしで同梱されているため、試すなら今のうちだと考え、この月次レポート生成を「一括出力」に置き換えてみました。結論から言うと、継ぎ目の手直しはゼロになり、費用も分割方式より約26%下がりました。ただし、ストリーミング前提の実装と、いくつかの落とし穴への備えが必要です。本文ではその実装と実測値を順に残します。
分割生成で実際に起きていたこと
最初に、置き換える前の運用を正直に書いておきます。これまでは max_tokens を 16,384 に設定し、レポートを4回に分けて生成していました。2回目以降のリクエストでは、それまでに生成された本文を入力に含めて「この続きから」と依頼する方式です。
この方式には、使い続けて見えてきた問題が三つありました。
継ぎ目の品質が安定しない 。「続きから書いてください」と依頼すると、「承知しました。続きを記載します」のような前置きが入ったり、直前の段落を少し言い直してから再開したりすることがあります。毎回ではないのですが、4回つなげば高い確率でどこかに混入します。
構成がドリフトする 。後半のリクエストでは Claude が文書全体を「資料として渡された本文」越しにしか見ていないため、見出しレベルの解釈が変わることがありました。前半は ### だった粒度が後半は ## になっている、という具合です。
入力トークンが累積する 。2回目は1回目の本文を、3回目は1〜2回目の本文を入力に再送します。つまり分割回数が増えるほど、同じテキストに何度も入力課金がかかります。
どれも致命的ではありません。ただ、毎月発生する20分の検品と手直しは、自動化の趣旨からすると本末転倒だと感じていました。
Fable 5 の出力仕様を確認する — 128k 出力と常時適応思考
Claude Fable 5 は Opus の上に位置づけられた Mythos クラスの一般提供版で、コンテキストウィンドウは 100 万トークン、出力は最大 128,000 トークンです。API でのモデル文字列は claude-fable-5、価格は入力 $10 / 出力 $50(100万トークンあたり)です。1M コンテキストはかつて Claude Sonnet 4.5 / Sonnet 4 の1Mトークンコンテキスト廃止:2026年4月30日までに完了すべき移行ガイド で書いたとおりベータ提供が終了した経緯がありますが、Fable 5 では標準仕様として戻ってきた形になります。
実装前に押さえておきたい仕様が二つあります。
常時適応思考と output_tokens の関係
Fable 5 は「常時適応思考」を備えていて、タスクの難しさに応じて考える量を自動調整します。私の実行ログでは、保存された本文から概算したトークン数より usage の output_tokens が1〜2割ほど多く記録されていました。思考分が出力側に乗っていると理解しています。費用を見積もるときは「本文の長さから逆算」ではなく「実測の usage から計算」が安全です。
長い出力はストリーミングが前提
128k 級の出力は、生成に数分かかります。非ストリーミングの一発リクエストで待つ設計にすると、HTTP 接続のタイムアウトと正面衝突します。Anthropic SDK も長時間リクエストにはストリーミングを促す挙動になっているため、最初からストリーミングで受け切る実装にしておくのが現実的です。
ストリーミングで受け切る基本実装
何を解決するコードかを先に書くと、「128k 枠の長文を一回のリクエストで受け取り、受信しながらファイルに書き続ける」実装です。
import anthropic
client = anthropic.Anthropic() # ANTHROPIC_API_KEY は環境変数で渡す
PROMPT = """あなたはアプリ運用レポートの編集者です。
以下の資料をもとに、月次運用レポートを Markdown で作成してください。
構成は「概況 / クラッシュ / レビュー対応 / 収益 / 来月の課題」とします。
<資料>
{source_digest}
</資料>
"""
def generate_single_pass (source_digest: str , out_path: str ):
"""128k 枠を使い、長文レポートを一回のリクエストで受け切る"""
with client.messages.stream(
model = "claude-fable-5" ,
max_tokens = 128000 , # 出力枠を上限まで確保しておく
messages = [{
"role" : "user" ,
"content" : PROMPT .format( source_digest = source_digest),
}],
) as stream:
with open (out_path, "w" , encoding = "utf-8" ) as f:
for text in stream.text_stream:
f.write(text) # 受信のたびに逐次書き込む(切断に備える)
f.flush()
final = stream.get_final_message()
print ( "stop_reason:" , final.stop_reason)
print ( "usage:" , final.usage)
return final
5月分のデータで実行したときの出力はこうでした。
stop_reason: end_turn
usage: Usage(input_tokens=6120, output_tokens=63807, ...)
なぜこう書くのか、の要点は二つです。まず max_tokens=128000 は「必ず 128k 使う」という意味ではなく、上限の確保です。実際の出力が 64k 前後でも、枠を広く取っておけば途中で切られることがなくなります。次に、text_stream を受けるたびに f.write と f.flush で逐次保存している点です。数分かかるストリームは、途中で切れる前提で設計しておくと後段の再開処理が素直になります。
stop_reason の確認は習慣にすることを推奨します。end_turn なら Claude が自分の判断で書き終えた合図、max_tokens なら枠に当たって切られた合図です。分割運用の頃は max_tokens 到達が「仕様」だったので意識していませんでしたが、一括運用ではここが品質の生命線になります。
切断に備える — 部分保存と assistant 継ぎ足しによる再開
数分のストリームは、ネットワーク事情で落ちることがあります。私の検証中も、Wi-Fi の切り替わりで一度だけ途中切断に遭いました。このときに「最初からやり直し」にしないための再開設計です。
def build_resume_messages (original_prompt: str , partial_path: str ) -> list :
"""切断時の部分保存ファイルから、続きを生成するための messages を組み立てる"""
with open (partial_path, encoding = "utf-8" ) as f:
partial = f.read()
# 末尾の書きかけ段落は捨てて、段落境界までを「ここまで書いた」とする
cut = partial.rfind( " \n\n " )
done = partial[:cut] if cut > 0 else partial
# assistant メッセージとして途中経過を渡すと、Claude はその続きから生成する
return [
{ "role" : "user" , "content" : original_prompt},
{ "role" : "assistant" , "content" : done},
]
ポイントは、ユーザーメッセージで「続きを書いてください」と頼むのではなく、生成済みの本文を assistant メッセージとして渡す ことです。会話の最後が assistant メッセージで終わっていると、Claude はその文字列の直後から続きを生成します。前置きの挨拶や言い直しが原理的に入り込まないので、分割時代に悩まされた継ぎ目の混入をほぼ防げます。
もう一つの工夫は、部分保存の末尾を段落境界(\n\n)まで切り詰めてから渡すことです。書きかけの文の途中から継ぐより、段落単位で継いだほうが、つなぎ目の文法事故が起きにくいというのが実測しての感触です。
分割生成と一括生成の費用を実測で比べる
費用の比較は、同じ Fable 5・同じ資料・同じ指示で「128k 枠の一括」と「16k 枠 ×4回の分割」を両方実行して、usage の実測値で行いました。方式の差だけを見たかったので、モデルは揃えています。
PRICE_IN = 10.0 # USD / 100万入力トークン(Fable 5)
PRICE_OUT = 50.0 # USD / 100万出力トークン
def cost_usd (input_tokens: int , output_tokens: int ) -> float :
return input_tokens / 1e6 * PRICE_IN + output_tokens / 1e6 * PRICE_OUT
# 5月分レポートの実測 usage
runs = {
"一括(128k 枠・1回)" : [( 6_120 , 63_807 )],
"分割(16k 枠・4回)" : [
( 6_118 , 16_214 ),
( 22_337 , 16_402 ), # 2回目以降は既出本文を再送するため input が膨らむ
( 38_512 , 16_087 ),
( 54_805 , 14_869 ),
],
}
for label, batches in runs.items():
cin = sum (i for i, _ in batches)
cout = sum (o for _, o in batches)
print ( f " { label } : input { cin :, } / output { cout :, } / $ { cost_usd(cin, cout) :.2f } " )
実行結果です。
一括(128k 枠・1回): input 6,120 / output 63,807 / $3.25
分割(16k 枠・4回): input 121,772 / output 63,572 / $4.40
出力トークンはほぼ同じなのに、分割方式は入力が約12万トークンに膨らんでいます。既出本文の再送がそのまま入力課金になっているためで、差額は $1.15、一括のほうが約26%安い計算でした。1ドル150円換算でひと月あたり約170円の差です。
正直に書くと、金額そのものは劇的ではありません。prompt caching を併用すれば再送分の単価はさらに下がるので、コスト差だけを理由に移行を勧めるつもりはないです。私にとって決め手だったのは、毎月20分の継ぎ目検品が消えたことと、構成のドリフトがなくなったことでした。費用は「悪化しないどころか少し下がる」という確認ができれば十分だと考えています。
品質面で変わったこと・変わらなかったこと
一括化して3回分(3〜5月のデータで再生成して比較)を検品した範囲では、次の変化がありました。
検品は人の目だけに頼らず、最低限を機械化しています。
import re
def verify_report (path: str , stop_reason: str ) -> list :
"""一括生成したレポートの最低限の検品"""
problems = []
if stop_reason != "end_turn" :
problems.append( f "stop_reason が { stop_reason } (途中で切れている可能性)" )
with open (path, encoding = "utf-8" ) as f:
text = f.read()
# 見出しレベルの飛び(## の直後に #### が来る等)を検出
levels = [ len (m) for m in re.findall( r " ^( # {2,4} )\s " , text, re. MULTILINE )]
for a, b in zip (levels, levels[ 1 :]):
if b - a >= 2 :
problems.append( "見出しレベルの飛びがあります" )
break
# 同名 H2 の重複(分割時代の典型的な事故)
h2 = re.findall( r " ^ ## \s + (. + )$ " , text, re. MULTILINE )
if len (h2) != len ( set (h2)):
problems.append( "同名の H2 が重複しています" )
return problems
分割時代にこのチェックを書いたときは「同名 H2 の重複」が月に一度は引っかかっていました。一括化後の3回では検出ゼロです。チェック自体は残しています。生成系の運用では、問題が消えた後もガードを外さないほうがよいというのが、これまでの実感です。
ハマりやすい点 — stop_reason・タイムアウト・credits の読み違え
一括化の検証でつまずいた点、つまずきかけた点をまとめておきます。
max_tokens の枠不足に気づかない
一括化した直後、試しに max_tokens を 32,000 のままにして実行してしまい、stop_reason: max_tokens で切れたことがありました。分割時代の感覚で「切れたら続きを頼めばよい」と流しそうになりましたが、一括運用ではここで止まって枠を広げて再実行するのが正解です。stop_reason を必ずログに残し、end_turn 以外なら検品を通さない、という運用にしています。
非ストリーミングでのタイムアウト
検証初日に、ストリーミングなしの messages.create で 64k 出力を待つ実装を試して、長時間リクエストに対する SDK の警告に行き当たりました。出力が長くなるほど接続を張りっぱなしにする時間も延びるので、128k 枠を使う前提なら最初からストリーミング一択だと考えたほうが早いです。
6月23日以降の credits 消費の読み違え
Fable 5 は 6月22日まで Pro / Max などの主要プランに追加費用なしで同梱されていますが、6月23日以降は usage credits の消費対象になります。出力 $50/MTok 相当の消費レートは、Sonnet 系の感覚で回すと残高の減りに驚くはずです。私は月次レポートのような「長文・月1回」の工程だけを Fable 5 に割り当て、日次の軽い工程は従来モデルに残す配分にしました。このあたりの工程仕分けの考え方は月次クレジット移行を前に、自動パイプラインの工程配分を見直した記録 に書いています。
高リスク領域のフォールバック
Fable 5 にはサイバーセキュリティ等の高リスク領域で Opus 4.8 にフォールバックする安全設計があります(発動はセッションの5%未満とされています)。運用レポート生成という用途では、3ヶ月分の再生成を通じて一度も遭遇しませんでした。ただし無人実行に組み込むなら、応答のモデル情報をログに残しておくと、後から「この月だけ様子が違う」を説明できるようになります。
6月22日の無料同梱期間が終わる前に試しておくこと
最初の一歩としては、いま分割生成している工程のうちいちばん継ぎ目に悩まされているもの を一つ選び、max_tokens=128000 のストリーミング実装に置き換えて、usage の実測値を取ることをお勧めします。無料同梱の期間中なら、検証そのものに追加費用がかかりません。実測の input / output トークンが手元に残れば、6月23日以降に credits でいくら消費するかは本文中の cost_usd のような数行の関数で見積もれます。
私自身、Fable 5 をすべての工程に使う判断はしていません。それでも「長文を一回で出し切る」という一点については、分割運用に戻る理由が見当たらなくなりました。同じように継ぎ目の手直しに時間を取られている方の参考になれば幸いです。