0Pricing
AI Agents · Lekcja

Identyfikowanie wolnych i kosztownych kroków

Profilowanie typu waterfall: na co agent przeznacza czas i budżet?

Identyfikowanie wolnych i kosztownych kroków to bezpłatna lekcja AI Agents na CoddyKit. To lekcja 3 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej AI Agents, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs AI Agents zawiera 4 lekcji w sumie.

Profilowanie wydajności agentów

Problemy z wydajnością agentów należą do dwóch kategorii: powolne kroki (duże opóźnienie) i kosztowne kroki (duży koszt tokenów). Oba rodzaje problemów pogarszają komfort użytkownika i zwiększają koszty operacyjne. Pierwszym krokiem jest pomiar.

Pomiar czasu każdego kroku

Proszę użyć time.perf_counter() do pomiaru czasu z wysoką precyzją. Mierzy on czas rzeczywisty, w tym oczekiwanie na operacje wejścia-wyjścia — dokładnie to, co ma znaczenie dla opóźnienia kroku agenta.

import time
from contextlib import contextmanager

@contextmanager
def timer(step_name: str, timings: dict):
    start = time.perf_counter()
    try:
        yield
    finally:
        end = time.perf_counter()
        duration_ms = (end - start) * 1000
        timings[step_name] = duration_ms
        print(f'{step_name}: {duration_ms:.1f}ms')

# Usage
timings = {}

with timer('entity_extraction', timings):
    time.sleep(0.05)  # Simulate work

with timer('vector_search', timings):
    time.sleep(0.12)  # Simulate work

with timer('llm_call', timings):
    time.sleep(0.80)  # Simulate LLM latency

print('\nTimings:', timings)
print('Slowest step:', max(timings, key=timings.get))

Budowanie profilera kroków

Profiler kroków opakowuje każdy krok agenta i rejestruje czas oraz koszt. Po zakończeniu uruchomienia generuje wykres wodospadowy przedstawiający czas trwania poszczególnych kroków.

import time
from dataclasses import dataclass, field
from typing import List, Optional

@dataclass
class StepProfile:
    name: str
    start_ms: float
    end_ms: float
    duration_ms: float
    prompt_tokens: int = 0
    completion_tokens: int = 0
    cost_usd: float = 0.0
    error: Optional[str] = None

class StepProfiler:
    def __init__(self):
        self.steps: List[StepProfile] = []
        self.run_start = time.perf_counter()
    
    def start_step(self, name: str) -> float:
        return time.perf_counter()
    
    def end_step(self, name: str, start_time: float, tokens: dict = None, error: str = None):
        end = time.perf_counter()
        run_elapsed = (start_time - self.run_start) * 1000
        duration = (end - start_time) * 1000
        
        profile = StepProfile(
            name=name,
            start_ms=run_elapsed,
            end_ms=run_elapsed + duration,
            duration_ms=duration,
            error=error
        )
        if tokens:
            profile.prompt_tokens = tokens.get('prompt', 0)
            profile.completion_tokens = tokens.get('completion', 0)
        self.steps.append(profile)
        return profile

profiler = StepProfiler()
t = profiler.start_step('entity_extraction')
time.sleep(0.05)
profiler.end_step('entity_extraction', t, {'prompt': 200, 'completion': 50})
print('Step recorded:', profiler.steps[0].duration_ms)

Wykres wodospadowy w terminalu

Proszę wyświetlać prosty tekstowy wykres wodospadowy przedstawiający czasy kroków. Wizualnie pokazuje on, kiedy uruchamiane są poszczególne kroki i ile trwają — podobnie jak wodospad sieciowy w przeglądarce, ale przeznaczony dla agentów.

class Step:
    def __init__(self, name, start_ms, end_ms, error=False):
        self.name, self.start_ms, self.end_ms, self.error = name, start_ms, end_ms, error
    @property
    def duration_ms(self):
        return self.end_ms - self.start_ms

class StepProfiler:
    def __init__(self, steps):
        self.steps = steps

def print_waterfall(profiler):
    if not profiler.steps:
        print('No steps recorded')
        return

    total_ms = max(s.end_ms for s in profiler.steps)
    bar_width = 50

    print('\n=== Agent Step Waterfall ===')
    for step in profiler.steps:
        start_pos = int(step.start_ms / total_ms * bar_width)
        end_pos = int(step.end_ms / total_ms * bar_width)
        bar = ' ' * start_pos + '#' * max(1, end_pos - start_pos) + ' ' * (bar_width - end_pos)
        status = 'ERR' if step.error else '   '
        print(f'{status} {step.name:<20} {step.duration_ms:>6.0f}ms  |{bar}|')

    print(f'\nTotal run: {total_ms:.0f}ms')

profiler = StepProfiler([Step('plan', 0, 120), Step('search', 120, 480), Step('generate', 480, 900, error=True)])
print_waterfall(profiler)

Zbieranie opóźnienia P95 w wielu uruchomieniach

Pojedynczy pomiar nie wystarcza. Proszę zbierać dane o czasie z wielu uruchomień i obliczać opóźnienia P50, P95 oraz P99 dla każdego kroku. Opóźnienie P95 to próg, poniżej którego kończy się 95% uruchomień.

import statistics
from collections import defaultdict

class LatencyCollector:
    def __init__(self):
        self.step_durations = defaultdict(list)
    
    def record(self, step_name: str, duration_ms: float):
        self.step_durations[step_name].append(duration_ms)
    
    def percentile(self, data: list, pct: float) -> float:
        sorted_data = sorted(data)
        index = int(len(sorted_data) * pct / 100)
        return sorted_data[min(index, len(sorted_data) - 1)]
    
    def report(self):
        print('=== Latency Report (ms) ===')
        print(f'{"Step":<30} {"Count":>6} {"P50":>8} {"P95":>8} {"P99":>8} {"Max":>8}')
        print('-' * 75)
        for step_name, durations in sorted(self.step_durations.items()):
            p50 = self.percentile(durations, 50)
            p95 = self.percentile(durations, 95)
            p99 = self.percentile(durations, 99)
            max_d = max(durations)
            print(f'{step_name:<30} {len(durations):>6} {p50:>8.0f} {p95:>8.0f} {p99:>8.0f} {max_d:>8.0f}')

collector = LatencyCollector()
import random
for _ in range(100):
    collector.record('vector_search', random.gauss(120, 30))
    collector.record('llm_call', random.gauss(800, 150))
collector.report()

Identyfikowanie powolnych kroków P95

Po zebraniu metryk należy zidentyfikować kroki, których opóźnienie P95 jest nieproporcjonalnie wysokie. Na nich proszę skupić działania optymalizacyjne — typowymi rozwiązaniami są buforowanie, przetwarzanie równoległe lub przejście na tańszy model.

class LatencyCollector:
    def __init__(self, step_durations):
        self.step_durations = step_durations
    def percentile(self, durations, p):
        s = sorted(durations)
        k = int(len(s) * p / 100)
        return s[min(k, len(s) - 1)]

def find_optimization_targets(collector, p95_threshold_ms=500):
    targets = []
    for step_name, durations in collector.step_durations.items():
        p95 = collector.percentile(durations, 95)
        avg = sum(durations) / len(durations)
        p95_to_avg_ratio = p95 / avg if avg > 0 else 0

        target = {
            'step': step_name,
            'p95_ms': round(p95),
            'avg_ms': round(avg),
            'p95_to_avg_ratio': round(p95_to_avg_ratio, 2),
            'call_count': len(durations),
            'needs_optimization': p95 > p95_threshold_ms
        }

        if target['needs_optimization']:
            if p95_to_avg_ratio > 2.0:
                target['suggestion'] = 'High variance: consider timeout and retry or caching'
            else:
                target['suggestion'] = 'Consistently slow: consider parallel execution or faster model'

        targets.append(target)

    targets.sort(key=lambda x: x['p95_ms'], reverse=True)
    return targets

collector = LatencyCollector({'search': [100, 150, 900], 'generate': [400, 420, 410]})
targets = find_optimization_targets(collector, p95_threshold_ms=500)
for t in targets:
    print(f"{t['step']}: P95={t['p95_ms']}ms, Avg={t['avg_ms']}ms, Action: {t.get('suggestion', 'OK')}")

Buforowanie kosztownych wyników narzędzi

Najskuteczniejszą optymalizacją kosztownych kroków jest buforowanie. Jeśli istnieje prawdopodobieństwo ponownego wykonania tego samego wywołania narzędzia przy tych samych danych wejściowych, należy zapisać wynik w buforze i przy następnym wywołaniu zwrócić go natychmiast.

import hashlib
import json
import time
from typing import Callable, Any

class ToolResultCache:
    def __init__(self, ttl_seconds: int = 300):
        self.cache = {}
        self.ttl = ttl_seconds
    
    def _make_key(self, tool_name: str, args: dict) -> str:
        content = json.dumps({'tool': tool_name, 'args': args}, sort_keys=True)
        return hashlib.sha256(content.encode()).hexdigest()[:16]
    
    def get_or_compute(self, tool_name: str, args: dict, compute_fn: Callable) -> Any:
        key = self._make_key(tool_name, args)
        now = time.time()
        
        if key in self.cache:
            entry = self.cache[key]
            if now - entry['ts'] < self.ttl:
                print(f'Cache HIT for {tool_name}')
                return entry['result']
        
        print(f'Cache MISS for {tool_name} - computing...')
        start = time.perf_counter()
        result = compute_fn(**args)
        elapsed = (time.perf_counter() - start) * 1000
        print(f'{tool_name} computed in {elapsed:.0f}ms')
        
        self.cache[key] = {'result': result, 'ts': now}
        return result

cache = ToolResultCache(ttl_seconds=60)

def expensive_web_search(query: str) -> list:
    time.sleep(0.2)  # Simulate slow API call
    return [f'Result for: {query}']

result1 = cache.get_or_compute('web_search', {'query': 'AI news'}, expensive_web_search)
result2 = cache.get_or_compute('web_search', {'query': 'AI news'}, expensive_web_search)  # Cache hit

Wykrywanie kroków zużywających dużo tokenów

Proszę identyfikować kroki, które zużywają nieproporcjonalnie dużo tokenów. Duże prompty są często spowodowane dołączaniem większej ilości kontekstu, niż jest potrzebna, lub brakiem obcinania pobranych dokumentów.

def find_token_heavy_steps(tracker: 'CostTracker', token_threshold: int = 2000) -> list:
    heavy_steps = []
    for step in tracker.steps:
        total_tokens = step.prompt_tokens + step.completion_tokens
        if total_tokens > token_threshold:
            heavy_steps.append({
                'step': step.step_name,
                'total_tokens': total_tokens,
                'prompt_tokens': step.prompt_tokens,
                'completion_tokens': step.completion_tokens,
                'cost_usd': step.cost_usd,
                'suggestions': []
            })
            entry = heavy_steps[-1]
            if step.prompt_tokens > token_threshold * 0.9:
                entry['suggestions'].append('Prompt is very large: truncate context documents or summarize')
            if step.completion_tokens > 1000:
                entry['suggestions'].append('Large output: use max_tokens limit if full response not needed')
    
    heavy_steps.sort(key=lambda x: x['total_tokens'], reverse=True)
    return heavy_steps

tracker = CostTracker()
tracker.record('answer_generation', 'gpt-4o-mini', 4500, 1200)
tracker.record('entity_extraction', 'gpt-4o-mini', 200, 50)
heavy = find_token_heavy_steps(tracker)
for s in heavy:
    print(f"{s['step']}: {s['total_tokens']} tokens, suggestions: {s['suggestions']}")

Analiza przejścia na tańszy model

Nie każdy krok wymaga najpotężniejszego modelu. Proszę przeanalizować, które kroki korzystają z drogich modeli i czy wystarczyłby tańszy model. Proste zadania ekstrakcji rzadko wymagają GPT-4o.

MODEL_TIERS = {
    'gpt-4o': {'tier': 'premium', 'capabilities': ['complex reasoning', 'nuanced writing']},
    'gpt-4o-mini': {'tier': 'standard', 'capabilities': ['extraction', 'classification', 'summarization']},
    'claude-3-haiku-20240307': {'tier': 'fast', 'capabilities': ['simple tasks', 'routing']}
}

STEP_MODEL_RECOMMENDATIONS = {
    'entity_extraction': 'gpt-4o-mini',
    'intent_classification': 'gpt-4o-mini',
    'simple_summarization': 'gpt-4o-mini',
    'complex_reasoning': 'gpt-4o',
    'final_answer_generation': 'gpt-4o-mini'
}

def audit_model_usage(tracker: 'CostTracker') -> list:
    recommendations = []
    for step in tracker.steps:
        recommended = STEP_MODEL_RECOMMENDATIONS.get(step.step_name)
        if recommended and recommended != step.model:
            current_cost = step.cost_usd
            # Estimate cost with recommended model (rough calculation)
            recommendations.append({
                'step': step.step_name,
                'current_model': step.model,
                'recommended_model': recommended,
                'potential_savings': 'significant' if step.model == 'gpt-4o' else 'moderate'
            })
    return recommendations

print('Model downgrade analysis function defined')

Profilowanie na produkcji

Na produkcji należy próbkować dane profilowania zamiast rejestrować każde uruchomienie. Proszę rejestrować 10–20% uruchomień w pełnych szczegółach, a dla pozostałych agregować metryki. Dzięki temu można utrzymać rozmiar danych i narzut na rozsądnym poziomie.

import random

class SampledProfiler:
    def __init__(self, sample_rate: float = 0.1):
        self.sample_rate = sample_rate
        self.full_profiles = []
        self.aggregate_timings = defaultdict(list)
    
    def should_profile_full(self) -> bool:
        return random.random() < self.sample_rate
    
    def record_run(self, profiler: 'StepProfiler', full_profile: bool):
        # Always record aggregate timing
        for step in profiler.steps:
            self.aggregate_timings[step.name].append(step.duration_ms)
        
        # Only store full profiles for sampled runs
        if full_profile:
            self.full_profiles.append(profiler.steps)
    
    def get_summary(self) -> dict:
        return {
            'full_profiles_stored': len(self.full_profiles),
            'steps_tracked': {k: len(v) for k, v in self.aggregate_timings.items()}
        }

sampled = SampledProfiler(sample_rate=0.1)
print('Sampled profiler: recording 10% of runs in full detail')
print('Summary:', sampled.get_summary())

Łączenie sygnałów opóźnienia i kosztu

Najbardziej praktycznymi celami optymalizacji są kroki, które są jednocześnie powolne i kosztowne. Krok powolny, ale tani (jedno wywołanie LLM z małym promptem), może nie być wart optymalizowania. Proszę skupić się na krokach o wysokich wartościach w obu wymiarach.

def combined_optimization_priority(profiler: 'StepProfiler', tracker: 'CostTracker') -> list:
    # Build combined step data
    cost_by_step = {s.step_name: s.cost_usd for s in tracker.steps}
    
    combined = []
    for step in profiler.steps:
        cost = cost_by_step.get(step.name, 0)
        # Priority score: normalize and combine
        # High latency + High cost = top priority
        priority = (step.duration_ms / 1000) + (cost * 1000)  # Rough normalization
        combined.append({
            'step': step.name,
            'duration_ms': round(step.duration_ms),
            'cost_usd': round(cost, 6),
            'priority_score': round(priority, 3)
        })
    
    combined.sort(key=lambda x: x['priority_score'], reverse=True)
    return combined

print('Combined optimization priority function defined')
print('High priority = slow + expensive')

Sprawdzenie wiedzy: Profilowanie wydajności

Proszę sprawdzić, czy rozumie Pan/Pani profilowanie wydajności agentów.

Podsumowanie profilowania wydajności

Skuteczne profilowanie wydajności agentów obejmuje: precyzyjny pomiar czasu za pomocą time.perf_counter(), wykresy wodospadowe do wizualizacji nakładania się kroków, zbieranie opóźnienia P95 z wielu uruchomień, identyfikowanie kroków zużywających dużo tokenów w celu ich obcinania, buforowanie wyników narzędzi w celu eliminowania powtarzających się kosztownych wywołań, analizę przejścia na tańszy model w celu obniżenia kosztów wykonywania kroków oraz próbkowanie profilowania na produkcji, aby utrzymać narzut na rozsądnym poziomie.

Często zadawane pytania

Czy lekcja „Identyfikowanie wolnych i kosztownych kroków” jest bezpłatna?

Tak — pełny tekst „Identyfikowanie wolnych i kosztownych kroków” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu AI Agents, przejdź na CoddyKit PRO. Kurs AI Agents zawiera 4 lekcji w sumie.

Co nauczysz się w „Identyfikowanie wolnych i kosztownych kroków”?

Profilowanie typu waterfall: na co agent przeznacza czas i budżet? Ćwiczysz AI Agents z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć AI Agents?

Nie wymagamy żadnego doświadczenia. AI Agents w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 3 z 4.

Ile czasu zajmuje lekcja „Identyfikowanie wolnych i kosztownych kroków”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji AI Agents?

Tak. Każda lekcja AI Agents zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Analiza śladów za pomocą LangSmith i Langfuse
  2. Profilowanie liczby tokenów i kosztów dla poszczególnych kroków
  3. Identyfikowanie wolnych i kosztownych kroków
  4. Analiza przyczyn źródłowych awarii agentów
← Powrót do AI Agents