複数のアプリを個人開発で運用していると、依存ライブラリのリリースノートやストアの規約ページを定期的に読み直す作業が積み上がっていきます。私はこの手の調べ物を Claude Code に任せているのですが、あるとき手元のプロキシログを眺めていて、ひとつのセッションの中で同じリリースノートのページを何度も取りに行っていることに気づきました。
読み返せば当然です。複数のページを横断して比較するとき、人間だって基準になるページに何度も戻ります。エージェントも同じことをします。
Claude Code の v2.1.233 で、WebFetch の URL キャッシュ TTL を CLAUDE_CODE_WEBFETCH_CACHE_TTL_MS で調整できるようになりました。既定は15分とされています。この数字を触るべきかどうかは、自分の調べ物がどういう形をしているかで変わります。ここでは、その形を自分の手で見てから決める手順を書きます。
まず「同じURLを何回踏んでいるか」を目で見る
設定値の話に入る前に、自分のタスクの実際のアクセスの形を知る必要があります。外部サイトを相手に数えるのは難しいので、手元に小さな観測用サーバを立てて、そこを取りに行かせるのが早道です。
# server.py — アクセスを数えるだけの観測用サーバ
import http.server, socketserver, datetime, collections
HITS = collections.Counter()
class Handler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
HITS[self.path] += 1
ts = datetime.datetime.now().strftime("%H:%M:%S")
print(f"{ts} GET {self.path} (hit #{HITS[self.path]})", flush=True)
body = f"doc for {self.path}\n".encode()
self.send_response(200)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, *args):
pass # 既定のアクセスログは重複するので黙らせる
with socketserver.TCPServer(("127.0.0.1", 8731), Handler) as httpd:
print("listening on http://127.0.0.1:8731", flush=True)
httpd.serve_forever()python3 server.py で起動し、同じ URL を3回叩くと、こう出ます。
15:06:33 GET /release-notes (hit #1)
15:06:33 GET /release-notes (hit #2)
15:06:33 GET /release-notes (hit #3)
15:06:33 GET /policy (hit #1)
素朴な仕掛けですが、これで十分です。標準出力へ即時に流すために flush=True を付け、既定のアクセスログを log_message で止めています。この2点を省くと、行が遅れて出たり二重に出たりして、回数の判断がぶれます。
あとは http://127.0.0.1:8731/release-notes のようなローカル URL を含む調べ物を Claude Code に投げ、ヒット数の伸び方を眺めます。同じパスの hit # が伸び続けるなら、そのタスクは再取得の多い形をしています。伸びないなら、TTL を触っても得るものはほとんどありません。
なお TTL の効き方そのもの(どの単位で共有されるのか、セッションをまたぐのか)は環境によって差が出るところなので、断定せずに、この観測サーバで自分の環境の挙動を確かめるのが確実です。
取得回数と鮮度は、片方を取れば片方が下がる
自分のアクセスの形が見えたら、TTL を変えたときに何がどう動くかを見積もります。実際のセッションを何度も回すのは時間がかかるので、アクセス列を入力にして計算してしまうほうが早いです。
# ttl_model.py — アクセス列から、取得回数と手元データの古さを出す
import random
def build_trace(minutes=90, seed=7):
"""基準ページに何度も戻り、たまに新しいリンク先を踏む調べ物の形"""
rnd = random.Random(seed)
core = [f"https://example.test/core/{i}" for i in range(4)]
trace = []
for t in range(minutes):
for _ in range(rnd.choice([0, 1, 1, 2])):
if rnd.random() < 0.62:
trace.append((t, rnd.choice(core)))
else:
trace.append((t, f"https://example.test/leaf/{rnd.randrange(400)}"))
return trace
def count_fetches(trace, ttl_min):
last, fetches = {}, 0
for t, url in trace:
prev = last.get(url)
if prev is None or t - prev >= ttl_min:
fetches += 1
last[url] = t
return fetches
def staleness(trace, ttl_min):
"""各アクセス時点で、手元にある内容が何分前のものかを集計する"""
last, ages = {}, []
for t, url in trace:
prev = last.get(url)
if prev is None or t - prev >= ttl_min:
last[url] = t
ages.append(0)
else:
ages.append(t - prev)
return max(ages), sum(ages) / len(ages)
trace = build_trace()
for ttl in (0, 5, 15, 60, 240):
mx, av = staleness(trace, ttl)
print(f"TTL={ttl:>3}分 取得={count_fetches(trace, ttl):>3}回 "
f"最大の古さ={mx:>2}分 平均の古さ={av:>4.1f}分")私の手元でこのスクリプトを走らせた結果です。アクセス総数86回・ユニーク URL 30本という、90分ぶんの調べ物を想定した列になっています。
| TTL | 実取得回数 | 再取得を省けた割合 | 手元データの最大の古さ | 平均の古さ |
|---|---|---|---|---|
| 0分(毎回取得) | 86回 | 0.0% | 0分 | 0.0分 |
| 5分 | 64回 | 25.6% | 4分 | 0.6分 |
| 15分(既定) | 45回 | 47.7% | 14分 | 3.8分 |
| 60分 | 34回 | 60.5% | 59分 | 14.4分 |
| 240分 | 30回 | 65.1% | 85分 | 24.3分 |
これはあくまで、この入力に対する計算結果です。数字そのものを持ち帰るのではなく、build_trace を自分のログから作った実際のアクセス列に差し替えて、自分の形での傾きを見てください。
私自身、この表を作るまでは「長くしておけば得だろう」と漠然と考えていました。実際には、そう単純ではありませんでした。15分から60分へ伸ばして減らせる取得は13%ぶんしかないのに、手元データの平均の古さは3.8分から14.4分へ、およそ4倍になります。効きが大きいのは0分から15分までの区間で、そこから先は鮮度だけが一方的に削られていきます。既定の15分が、ちょうど曲がり角の手前に置かれていることがわかります。
用途ごとに、どちらを重く見るかを決める
ここまでの2つの数字をどう天秤にかけるかは、調べている対象の変化の速さで決まります。私が普段扱う3種類の調べ物で言うと、こうなります。
| 調べ物の種類 | 対象の変わりやすさ | 寄せる方向 | 理由 |
|---|---|---|---|
| リリースノート・変更履歴の追跡 | 日単位で変わる | 既定のまま、あるいは短く | 作業中に更新が入る可能性は低いものの、古い記述のまま結論を出すと手戻りが重い |
| 規約・ポリシーの読み合わせ | 月単位で変わる | 長めに寄せる | 同じ条文へ何度も戻る形になりやすく、セッション中に変わることはまず考えなくてよい |
| 公開中のページの表示確認 | 自分の手で変える | 短く、または無効に寄せる | 直したものを取りに行っているので、古い内容が返ると確認の意味がなくなる |
3行目が、私が一番失敗しやすかったところです。自分で直したページを確認させているのに、直す前の内容をもとに「問題ありません」と返ってくる。原因が自分の設定にあると気づくまで、直しかたのほうを疑って時間を使いました。キャッシュは、自分が書き換える対象に対しては、ただの嘘つきになります。
設定を当てて、当たっていることを確かめる
値はミリ秒で渡します。1時間なら以下です。
# そのコマンドの間だけ効かせる
CLAUDE_CODE_WEBFETCH_CACHE_TTL_MS=3600000 claude
# 確認用途など、キャッシュを寄せたくないとき
CLAUDE_CODE_WEBFETCH_CACHE_TTL_MS=0 claudeシェルの設定ファイルへ書き込んで常時有効にすることもできますが、私は用途ごとに前置きで渡す形に落ち着きました。調べ物と、自分が直したページの確認とでは、望ましい向きが正反対になるからです。片方に合わせて固定すると、もう片方で必ず困ります。
設定したら、先ほどの観測サーバへ戻って当たっているかを見ます。値を変える前と後で同じ調べ物を投げ、同じパスの hit # の伸び方が変わっていれば効いています。変わらないなら、値の書き方か、渡しているプロセスの範囲が違っています。
環境変数の全体像は「Claude Code 環境変数 完全リファレンス」に整理してあります。同じ v2.1.233 で入った Bash ツールのメモリ上限については「ビルドがメモリを食い尽くしたときの止まり方は、cgroup と ulimit で別物でした」で扱っています。
次の一歩
まずは観測サーバを起動して、いつも投げている調べ物をそのまま一度流してみてください。hit # が2桁まで伸びるパスがあるなら、TTL を伸ばす価値があります。伸びないなら、既定のままで構いません。設定値を決める前に、自分のタスクの形を一度見ておくと、迷いが減ります。
小さな環境変数ひとつですが、こうして数字にしてみると、何を犠牲にして何を得ているのかがはっきりします。お読みいただきありがとうございました。