İçeriğe geç
ceaksan

Compaction Uyumlu Arama Çıktısı: Pratik Bir Playbook

Agent context window'u dolmadan önce search çıktısını sıkıştırmanın yolu var. file:line + 2 satır context formatı, file-cache, dedup ve tool result wrapper'ları için kod örnekleri, ölçüm sonuçları.

22 May 2026 4 dk okuma
TL;DR

Agent araması için tek baskın token tasarrufu lever'ı: çıktıyı kaynağında kes. Ölçümde rg | head -50 yüksek frekanslı sorgularda çıktıyı ~40 kat küçültüyor (47k → 1.2k token); --vimgrep ucuz değil, biraz daha pahalı; -C N match yoğunluğuna göre 2-6 kat şişiriyor. Pratik playbook: iki fazlı strateji (rg -l ile discovery → daraltılmış Read), file:line + 2 satır çıktı formatı, file-cache + session dedup, budget-aware wrapper, pre-compaction trigger'ları (tool result > %15-20 search budget, aynı dosya 3+ okuma, doluluk %70+). Compaction bir kurtarma değil, bir başarısızlık sinyali.

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):

VariantTokenDefault’a oran
rg "import" src~47,0001.0x (baseline)
rg --vimgrep ...~57,0001.22x
rg -C 2 ...~285,0006.06x
rg ... | head -50~1,2000.025x (40x küçük)
rg -l ...~7,8000.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:

  1. Default rg + | head -50 yüksek frekanslı sorgularda zorunlu, opsiyonel değil
  2. --vimgrep flag’i tasarruf etmiyor, biraz daha pahalı. Default zaten file:line: veriyor
  3. -C N kullanı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:

FormatTipik 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:

  1. Cache key’inde mtime var. Dosya değişirse cache’i atla. Cross-session persistence yapılmıyor çünkü kod tipik bir agent session’ında değişebilir
  2. line_range opsiyonel. 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çin
  3. tokens_saved metriğ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:

  1. Tek tool result search bütçesinin %15-20’sini aşıyor. Wrapper kapanı tool_result_cap doğru ayarlanmamış. Cap’i düşür veya summarize agresifleştir
  2. 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?
  3. 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İyiSınırMüdahale
Avg tokens per search task< 8k8-15k> 15k
File-cache hit rate> 40%20-40%< 20%
Compaction events per task00.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.

Agent Search Engineering

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
Neler var
  • 4 cluster yazısının uzun versiyonu
  • BudgetedToolWrapper + FileCache production-grade
  • Jupyter notebook ile policy benchmark
  • Hazır CLAUDE.md ve .cursorrules snippet'leri
  • Karar ağacı şablonu (PDF + Mermaid kaynak)
Önemli Noktalar
  • 01 Çıktıyı kaynağında kes: rg | head -50 baskın lever. Yüksek frekanslı sorguları kapamadan agent context'i sürmez.
  • 02 --vimgrep tasarruf etmiyor; ölçümde 1.15-1.22 kat daha pahalı. -C N match yoğunluğuna göre 2-6 kat şişiriyor.
  • 03 İki fazlı strateji: önce rg -l ile dosya kümesi, sonra hedeflenmiş rg veya Read. 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.
Sık Sorulan Sorular (FAQ)
+ 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.