Compaction bir agent için “context kaybı” demek. Search bu kaybın en sık tetikleyicisi. Tool result’ı kaynağında kesmeyen her wrapper er ya da geç compaction’ı çağırır.
AI agent için kod arama yazısında “agent’a dönen tool result’ı her zaman sıkıştır” demiştim, token bütçesi yazısında bu sıkıştırmanın hangi flag’lerle ölçülebileceğini açtım. Bu yazıda iki tarafı birleştiriyorum: çıktıyı hangi sırayla, hangi formatta, hangi cache ile sıkıştıracağın somut bir playbook.
Baskın Lever: Çıktıyı Kaynağında Kes
Token bütçesi yazısındaki ölçümün en net çıkarımı: çıktıyı agent context’ine düştükten sonra kısaltmak geç kalmış müdahaledir. Maliyet zaten ödenmiş olur. Asıl tasarruf tool çağrısının kendi komutunda olur, sonrasında değil.
Yüksek frekanslı bir sorguda ölçüm tablosu çarpıcı (298 dosyalık ceaksan-v4.0 src/‘de ölçtüm; tiktoken cl100k_base, Spoke 2 notebook):
| Variant | Token | Default’a oran |
|---|---|---|
rg "import" src | ~47,000 | 1.0x (baseline) |
rg --vimgrep ... | ~57,000 | 1.22x |
rg -C 2 ... | ~285,000 | 6.06x |
rg ... | head -50 | ~1,200 | 0.025x (40x küçük) |
rg -l ... | ~7,800 | 0.17x |
40 kat tasarruf orta-büyük context window’larda bile kritik. Tek bir rg "import" çağrısı 200k’lık bir bütçenin %23’ünü yiyor, | head -50 ile %0.6’ya iniyor.
Buradan üç madde öne çıkıyor:
- Default
rg+| head -50yüksek frekanslı sorgularda zorunlu, opsiyonel değil --vimgrepflag’i tasarruf etmiyor, biraz daha pahalı. Default zatenfile:line:veriyor-C Nkullanırken match yoğunluğunu önceden tahmin et. 100+ match’li sorguda-C 2çıktıyı 6 kata kadar büyütür, search bütçesini tek çağrıda tüketebilir
İki Fazlı Strateji: Discovery → Targeted
Tek rg çağrısı her şeyi vermesin. Aşağıdaki ikili pratikte hem ucuz hem reasoning-uyumlu:
# Faz 1: Discovery (dosya listesi)
rg -l "newsletterCluster" src
# Faz 2: Targeted (sadece ilgili görünen 2-3 dosya)
rg "newsletterCluster" src/content/posts/2026/05/06.*/tr.mdx -C 1
Faz 1’in cost’u sabit ve düşük. Spoke 2 ölçümünde 298 dosyalık ceaksan-v4.0 src/ klasöründe bu değer ~5.8k token çıktı; benzer ölçekli repolarda ~5-8k bandında gözleniyor. Agent dosya listesine bakar, hangi 2-3’üne girmesi gerektiğine karar verir, faz 2 sadece o dosyalarda -C 1 veya Read yapar. Toplam maliyet tek seferlik rg -C 2’nin onda biri civarında çıkar.
Bu pattern Anthropic’in Claude Code’da kullandığı “list-then-read” yaklaşımıyla aynı, sadece search tarafına taşınmış hali.
Tool Result Format’ı: file:line + Snippet
Pillar yazısında verilen file:line + 2 satır context formatı zengin ama pahalı. Ölçümde aynı sonuç kümesi için varyantlar:
| Format | Tipik tok/match |
|---|---|
file:line (sadece konum) | ~20 |
file:line: match_line | ~40-60 |
file:line + 1 satır before/after | ~120-180 |
file:line + 2 satır context | ~200-300 |
İlk pass için file:line: match_line (rg default davranışı) yeterli. Agent reasoning’inde “bu match’in çevresine bakmam gerek” diyorsa o anda Read ile spesifik dosyaya gider. Önden context vermek büyük çoğunluğu muhtemelen israf.
Pratik kural: tool result her zaman en kısa bilgilendirici formda olsun, derinleşmeyi agent’ın kararına bırak.
File-Cache: Aynı Dosyayı İkinci Kez Okuma
Spoke 2’deki Read distribution ölçümünde tail önemliydi: dosyaların %6’sı 5k token’i geçiyor, %2’si 10k’yı geçiyor, maks 51k. Bu dosyalar agent’ın “tekrar okuyayım, emin olamadım” döngüsüne girerse context bir kalemde bitebilir.
Session scope’lu basit bir in-memory cache problemi çözer:
from pathlib import Path
import tiktoken
class FileCache:
"""Session scope tool-side cache. Agent'a aynı dosyayı iki kez göndermez."""
def __init__(self, enc=tiktoken.get_encoding('cl100k_base')):
self.enc = enc
self._store = {} # key: (path, mtime, range) -> str
def read(self, path: str, line_range: tuple[int, int] | None = None) -> str:
p = Path(path)
mtime = p.stat().st_mtime_ns
key = (str(p), mtime, line_range)
if key in self._store:
return self._store[key]
text = p.read_text(errors='ignore')
if line_range:
lines = text.splitlines()
a, b = line_range
text = '\n'.join(lines[a-1:b])
self._store[key] = text
return text
def tokens_saved(self, hits: int, avg_size: int) -> int:
return hits * avg_size
Üç tasarım kararı bilinçli:
- Cache key’inde
mtimevar. Dosya değişirse cache’i atla. Cross-session persistence yapılmıyor çünkü kod tipik bir agent session’ında değişebilir line_rangeopsiyonel. Agent dosyanın sadece bir parçasını istediyse o parçayı cache’le; sonradan tam dosya istenirse ayrı entry açılır. Üst-sınıf optimizasyon değil, doğruluk içintokens_savedmetriği var. Wrapper günde 10 task’tan sonra “ne kadar tasarruf ettim” sorusuna sayısal cevap veriyor; budget discipline’ı görünür kılıyor
Budget-Aware Wrapper: Summarize, Sonra Truncate
Cache cevabı kısaltmaz; sadece tekrarı önler. Yeni çıktının kendisi de bir bütçe altına girmeli. Search ve Read için tek bir wrapper:
class BudgetedToolWrapper:
"""Tool çıktısını budget eşiklerine göre kısaltır."""
def __init__(self, context_window: int, search_ratio: float = 0.15, enc=None):
self.enc = enc or tiktoken.get_encoding('cl100k_base')
self.search_budget = int(context_window * search_ratio)
self.tool_result_cap = int(self.search_budget * 0.20) # tek call max %20
self.used = 0
def _tokens(self, s: str) -> int:
return len(self.enc.encode(s))
def _summarize(self, text: str, target_tokens: int) -> str:
lines = text.splitlines()
kept = []
budget = target_tokens
for line in lines:
t = self._tokens(line) + 1
if budget - t < 0:
kept.append(f'... [+{len(lines) - len(kept)} satır, summarized]')
break
kept.append(line)
budget -= t
return '\n'.join(kept)
def wrap(self, raw_result: str) -> str:
tokens = self._tokens(raw_result)
if tokens <= self.tool_result_cap:
self.used += tokens
return raw_result
target = int(self.tool_result_cap * 0.6) # kapasitenin %60'ı, headroom için
summarized = self._summarize(raw_result, target)
self.used += self._tokens(summarized)
return summarized
Bu wrapper rejimi spoke 2’deki BudgetedSearchAgent’ın search tarafıyla aynı prensip; burada Read ve rg sonuçlarına uygulanmış hali. Constructor context_window parametresini runtime alır, hardcode model kapasite varsayımı yok.
Tek hassas nokta: _summarize yukarıdaki halinde basit line-truncate yapıyor. Production’da bunu rg çıktısı için “kalan match sayısını + dosya başına grup oluşturma” şeklinde geliştirmek faydalı. Prensip aynı.
Pre-Compaction Trigger’ları: Üç Erken Uyarı
Wrapper iyi kurulduysa compaction tetiklenmemeli. Tetiklendiyse üç ihtimalden biri var. Üçü de ölçülebilir:
- Tek tool result search bütçesinin %15-20’sini aşıyor. Wrapper kapanı
tool_result_capdoğru ayarlanmamış. Cap’i düşür veya summarize agresifleştir - Aynı dosya 3+ kez okunuyor. File-cache devrede değil ya da cache key yanlış (mtime kontrolü olmadan her okuma yeni entry açıyor olabilir). Cache hit-rate metriği eklendi mi?
- Context window doluluk %70’i geçiyor ve hala 5+ tool call gerek. Görev planlaması yanlış; agent erken summarize edip yeni turn’e devretmeli (handoff pattern)
İlk iki sinyali wrapper içinde yakalamak kolay; üçüncüsü için agent’ın kendi prompt’unda kontrol gerekli. Önerilen kontrol satırı:
Before any tool call, estimate context fill: if > 70%, summarize work-in-progress
and request a fresh turn. Do not continue accumulating tool results.
Bu satır basit görünüyor ama kendi denemelerimde compaction olaylarını önemli ölçüde azalttığını gözlemledim; spesifik bir N henüz yok.
Ölçüm: Bir Hafta Sonra Neye Bakacaksın
Üç metrik, üç eşik:
| Metrik | İyi | Sınır | Müdahale |
|---|---|---|---|
| Avg tokens per search task | < 8k | 8-15k | > 15k |
| File-cache hit rate | > 40% | 20-40% | < 20% |
| Compaction events per task | 0 | 0.1-0.3 | > 0.3 |
Bu sayıları üretmek için fancy gözlemleme aracı şart değil. Wrapper’a basit bir log ekle, günün sonunda 5 satır SQL ile özetle. Asıl mesele sayıyı görmek, mükemmel ölçmek değil.
Pillar Wrapper’ı ile Birlikte Düşünmek
Pillar yazısında üç katmanlı search (lexical → structural → semantic) önerdim. Spoke 2’de bütçe disiplinini, bu yazıda format ve cache disiplinini ekledim. Üçü bir araya geldiğinde agent kod arama wrapper’ı şuna benziyor:
class AgentSearchStack:
def __init__(self, context_window: int):
self.cache = FileCache()
self.budget = BudgetedToolWrapper(context_window, search_ratio=0.15)
def search(self, query: str, lang: str = 'auto') -> str:
# Faz 1: discovery
files = run(['rg', '-l', query, 'src']).splitlines()
if not files:
return 'no matches'
if len(files) > 20:
# Çok geniş; agent'a önce dosya listesi dön
return self.budget.wrap('\n'.join(files[:50]))
# Faz 2: targeted match
out = run(['rg', query, *files])
return self.budget.wrap(out)
def read(self, path: str, line_range=None) -> str:
raw = self.cache.read(path, line_range)
return self.budget.wrap(raw)
Production-grade değil, prensip taşıyıcı. Asıl mesaj: search ve read aynı budget altında çalışıyor, aynı cache’i paylaşıyor, agent her ikisinin de raw versiyonunu hiç görmüyor.
Sıradaki Adım
Bu cluster’ın dördüncü yazısı LLM-free SpecAgent: AST tabanlı forecasting bütçenin daha verimli kullanılmasının ileri tekniğini, yani agent’ın bir sonraki sorgusunu önceden tahmin edip cache’lemeyi anlatacak. ast-grep’in bu wrapper’da yeri ne olacak: ucuz değil, ama structural pattern gerektiren forecasting’de tek aday.
Bu yazının pratik karşılığı: 4 cluster yazısının uzun versiyonu, akademik literatür özetleri, ölçüm notebook'u, hazır policy snippet'leri ve karar ağacı şablonu. ceaksan.com Premium tier kapsamında.
Premium'a Katıl- 01 Çıktıyı kaynağında kes:
rg | head -50baskın lever. Yüksek frekanslı sorguları kapamadan agent context'i sürmez. - 02
--vimgreptasarruf etmiyor; ölçümde 1.15-1.22 kat daha pahalı.-C Nmatch yoğunluğuna göre 2-6 kat şişiriyor. - 03 İki fazlı strateji: önce
rg -lile dosya kümesi, sonra hedeflenmişrgveyaRead. Tek seferde tüm match dump yanlış. - 04 Tool result wrapper'ı her zaman budget-aware olsun. Eşik aşan çıktı önce summarize, sonra truncate; raw call agent'a doğrudan açılmaz.
- 05 Pre-compaction trigger: tek tool result search bütçesinin %15-20'sini aşıyor, aynı dosya 3+ okunuyor, doluluk %70 üstü. Üçü de erken müdahale sinyali.
+ Compaction otomatik tetikleniyorsa neden manuel sıkıştırma gerek?
Otomatik compaction bir kurtarma mekanizması, fix değil. Anthropic'in compaction davranışı dokümantasyonuna ve kendi gözlemime göre, compaction olduğunda agent daha önce gördüğü detayları kaybedebiliyor: hangi dosyada hangi satırı incelediği, hangi sonucu elediği gibi. Manuel sıkıştırma compaction'ı hiç tetiklememek için, böylece reasoning continuity korunur.
+ `rg --vimgrep` daha yapısal değil mi? Neden kaçınmalı?
Yapısal görünüyor ama her satıra file:line:column: metadata ekliyor. Ölçümde default rg çıktısına göre %15-22 daha fazla token üretti, default zaten file:line: veriyor. Üçüncü kolon (column) agent için ek bilgi değil; sadece byte yiyor. Default + | head -50 daha ucuz.
+ Tool result format'ı için en pratik şablon ne?
file:line | code_snippet tek satır formatı. Pillar yazısında verilen file:line + 2 satır context versiyonu daha zengin ama 3 kat daha pahalı. İlk pass'ta tek satır kullan, agent ihtiyaç duyarsa kendisi spesifik dosyayı Read'le açar. Bu Anthropic'in Claude Code'da kullandığı _tool result clearing_ stratejisiyle uyumlu.
+ File-cache hangi seviyede tutulmalı?
Session scope'ta. Aynı agent turn'leri arasında paylaşılan in-memory dict yeterli. Cross-session persistence gerekmez (kod değişir, cache eskir). Cache key olarak (file_path, mtime, byte_range) üçlüsü çakışmaları önler. [SWEzze](https://arxiv.org/abs/2603.28119) %51-71 inference-time sıkıştırma oranı bildiriyor; bu kazancın büyük bölümü session cache + dedup tarafından geliyor, magic değil.
+ Tool result wrapper'ı bu kadar disiplinli olmazsa ne olur?
Üç tipik bozulma: (1) tek rg çağrısı search bütçesini bir kalemde yutuyor, (2) agent aynı dosyayı 4-5 kez okuyup her seferinde tam içeriği context'e yazıyor, (3) compaction tetikleniyor ve hangi reasoning step'inin nereye dayandığı kayboluyor. Üçü de pillar yazısındaki Tier B → Tier C drift'in nedeni.