# Agent Latency Optimization and Performance Tuning

**Last updated:** 2026-02
**Status:** GA
**Category:** Agent Orchestration & Automation

---

## Introduksjon

Responstid er en kritisk kvalitetsfaktor for AI-agenter. Brukere forventer sub-sekund initial respons og fullstendige svar innen få sekunder. I multi-agent-systemer akkumuleres latency gjennom hver orkestreringsbeslutning, modellkall, verktøyinvokasjon og data-retrieval. Uten bevisst optimalisering kan en agent som involverer 3-4 LLM-kall raskt nå 15-30 sekunders total responstid, noe som er uakseptabelt for interaktive scenarier.

Azure OpenAI tilbyr flere mekanismer for latency-optimalisering: modellvalg (GPT-4o mini for lavest latency), streaming for opplevd rask respons, prompt-caching for gjentatte forespørsler, og batching for asynkrone workloads. I tillegg gir Azure API Management som AI Gateway mulighet for intelligent routing, semantic caching og request-deduplication.

For agentsystemer spesifikt er de største latency-driverne antall sekvensielle LLM-kall, størrelsen på kontekstvinduer, og ventetid på eksterne verktøy. Parallellisering av uavhengige operasjoner, prefetching av sannsynlige data-behov, og async-patterns for verktøybruk er de mest effektive optimaliseringene.

## Kjernekomponenter

| Komponent | Formål | Teknologi |
|-----------|--------|-----------|
| Streaming | Reduser opplevd latency | Azure OpenAI streaming, SSE |
| Prompt Caching | Reduser time-to-first-token for gjentatte prefixer | Azure OpenAI prompt caching |
| Request Batching | Samle bulk-operasjoner | Azure OpenAI Batch API |
| Semantic Caching | Cache semantisk like forespørsler | APIM semantic caching policy |
| Model Selection | Velg riktig modell for oppgavens krav | GPT-4o mini, GPT-4o, Model Router |
| Async Patterns | Parallelliser uavhengige operasjoner | C# async/await, Python asyncio |

## Latency-anatomi for agentsystemer

### Typisk breakdown

```
┌─────────────────────────────────────────────────────┐
│ Total agent response: ~8-15 sekunder (uten opt.)    │
│                                                      │
│  Router LLM-kall:          ~0.5-1.5s                │
│  RAG retrieval:             ~0.3-1.0s               │
│  Specialist LLM-kall:      ~2-5s                    │
│  Tool invocation (API):    ~0.5-3s                  │
│  Response formatting:       ~0.5-1.5s               │
│  Content filtering:         ~0.1-0.3s               │
│                                                      │
│ Etter optimalisering:      ~2-5 sekunder            │
└─────────────────────────────────────────────────────┘
```

### Latency-metrikker

| Metrikk | Beskrivelse | Måling |
|---------|------------|--------|
| Time to First Token (TTFT) | Tid til første token i streamed respons | Azure Monitor, streaming |
| End-to-End Request Time | Total tid for komplett respons | API Gateway metrics |
| Token Generation Rate | Tokens per sekund under generering | Calculated metric |
| Tool Call Latency | Tid brukt på verktøyinvokasjoner | Custom spans |
| Orchestration Overhead | Tid brukt i routing/orkestrering | Custom spans |

## Response Streaming

### Implementering med Semantic Kernel

```csharp
// Streaming reduserer opplevd latency dramatisk
public async IAsyncEnumerable<string> StreamAgentResponse(
    ChatCompletionAgent agent,
    string userMessage,
    AgentThread thread)
{
    var message = new ChatMessageContent(
        AuthorRole.User, userMessage);

    await foreach (var chunk in
        agent.InvokeStreamingAsync(message, thread))
    {
        if (!string.IsNullOrEmpty(chunk.Content))
        {
            yield return chunk.Content;
        }
    }
}

// Bruk med SignalR for web-klienter
public class AgentHub : Hub
{
    public async Task SendMessage(string query)
    {
        await foreach (var token in
            _orchestrator.StreamAgentResponse(query))
        {
            await Clients.Caller.SendAsync("ReceiveToken", token);
        }
        await Clients.Caller.SendAsync("ResponseComplete");
    }
}
```

### Streaming med Azure OpenAI direkte

```python
import asyncio
from openai import AsyncAzureOpenAI

client = AsyncAzureOpenAI(
    azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
    api_key=os.environ["AZURE_OPENAI_KEY"],
    api_version="2024-12-01-preview"
)

async def stream_response(messages: list):
    stream = await client.chat.completions.create(
        model="gpt-4o",
        messages=messages,
        stream=True,
        # Optimaliseringer
        max_tokens=500,  # Begrens generering
        temperature=0.3,  # Lavere = raskere konvergens
    )

    async for chunk in stream:
        if chunk.choices[0].delta.content:
            yield chunk.choices[0].delta.content
```

## Request Batching

### Azure OpenAI Batch API

For ikke-interaktive workloads tilbyr Batch API 50% kostnadsreduksjon og høyere throughput:

```python
# Batch API for bulk-operasjoner (24-timers SLA)
import json
from openai import AzureOpenAI

client = AzureOpenAI(
    azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
    api_key=os.environ["AZURE_OPENAI_KEY"],
    api_version="2024-12-01-preview"
)

# Opprett batch-fil med JSONL
batch_requests = []
for i, query in enumerate(evaluation_queries):
    batch_requests.append({
        "custom_id": f"eval-{i}",
        "method": "POST",
        "url": "/chat/completions",
        "body": {
            "model": "gpt-4o-mini",
            "messages": [
                {"role": "system", "content": "Evaluer følgende..."},
                {"role": "user", "content": query}
            ],
            "max_tokens": 200
        }
    })

# Skriv JSONL-fil
with open("batch_input.jsonl", "w") as f:
    for req in batch_requests:
        f.write(json.dumps(req) + "\n")

# Last opp og start batch
file = client.files.create(
    file=open("batch_input.jsonl", "rb"),
    purpose="batch"
)
batch = client.batches.create(
    input_file_id=file.id,
    endpoint="/chat/completions",
    completion_window="24h"
)
```

## Prefetching Strategies

### Proaktiv data-henting

```python
import asyncio

class PrefetchingOrchestrator:
    """Parallelliser data-henting med LLM-klassifisering"""

    async def process_query(self, query: str) -> str:
        # Start routing og data-henting PARALLELT
        routing_task = asyncio.create_task(
            self.classify_intent(query)
        )
        # Prefetch de mest sannsynlige datakildene
        common_data_task = asyncio.create_task(
            self.fetch_common_context(query)
        )

        # Vent på routing-resultat
        routing = await routing_task
        common_data = await common_data_task

        # Hent spesialisert data basert på routing
        specialized_data = await self.fetch_specialized_data(
            routing.target_agent, query
        )

        # Kombiner kontekst og generer svar
        context = {**common_data, **specialized_data}
        return await self.generate_response(
            routing.target_agent, query, context
        )

    async def fetch_common_context(self, query: str) -> dict:
        """Hent data som sannsynligvis trengs uansett agent"""
        user_profile, recent_history = await asyncio.gather(
            self.get_user_profile(),
            self.get_recent_interactions(limit=3)
        )
        return {
            "user_profile": user_profile,
            "recent_history": recent_history
        }
```

## Semantic Caching med APIM

### APIM Semantic Cache Policy

```xml
<!-- Azure API Management semantic caching for AI-forespørsler -->
<policies>
    <inbound>
        <!-- Sjekk semantic cache før videresending -->
        <azure-openai-semantic-cache-lookup
            score-threshold="0.90"
            embeddings-backend-id="embedding-backend"
            embeddings-backend-auth="system-assigned" />
    </inbound>
    <outbound>
        <!-- Lagre respons i cache for fremtidige like forespørsler -->
        <azure-openai-semantic-cache-store
            duration="3600" />
    </outbound>
</policies>
```

### Cache-invalidering

```python
class AgentCacheManager:
    """Håndter cache-invalidering for agent-systemer"""

    def __init__(self, redis_client):
        self.redis = redis_client

    async def cache_response(
        self, query_embedding: list, response: str, ttl: int = 3600
    ):
        key = self._embedding_to_key(query_embedding)
        await self.redis.setex(key, ttl, response)

    async def invalidate_on_knowledge_update(
        self, updated_sources: list
    ):
        """Når kunnskapsbase oppdateres, invalider relaterte cacher"""
        # Fjern alle cache-entries som refererer til oppdaterte kilder
        for source in updated_sources:
            pattern = f"agent_cache:*:{source}:*"
            keys = await self.redis.keys(pattern)
            if keys:
                await self.redis.delete(*keys)

    async def invalidate_on_model_change(self):
        """Ved modellbytte, flush hele cachen"""
        await self.redis.flushdb()
```

## Async-Awaitable Patterns

### Parallell verktøyinvokasjon

```csharp
// Parallelliser uavhengige tool calls
public class OptimizedAgentToolHandler
{
    public async Task<ToolResults> ExecuteToolsParallel(
        IEnumerable<ToolCall> toolCalls)
    {
        // Grupper verktøykall etter avhengigheter
        var independentCalls = toolCalls
            .Where(t => !t.HasDependencies)
            .ToList();
        var dependentCalls = toolCalls
            .Where(t => t.HasDependencies)
            .ToList();

        // Kjør uavhengige kall parallelt
        var parallelResults = await Task.WhenAll(
            independentCalls.Select(tool =>
                ExecuteToolWithTimeout(tool, TimeSpan.FromSeconds(5))
            )
        );

        // Kjør avhengige kall sekvensielt
        var sequentialResults = new List<ToolResult>();
        foreach (var tool in dependentCalls)
        {
            var result = await ExecuteToolWithTimeout(
                tool, TimeSpan.FromSeconds(5));
            sequentialResults.Add(result);
        }

        return new ToolResults(parallelResults, sequentialResults);
    }

    private async Task<ToolResult> ExecuteToolWithTimeout(
        ToolCall tool, TimeSpan timeout)
    {
        using var cts = new CancellationTokenSource(timeout);
        try
        {
            return await tool.ExecuteAsync(cts.Token);
        }
        catch (OperationCanceledException)
        {
            return ToolResult.Timeout(tool.Name);
        }
    }
}
```

## Model Selection for Latency

### Modell-latency sammenligning

| Modell | TTFT (median) | Tokens/sek | Anbefalt bruk |
|--------|---------------|------------|----------------|
| gpt-4o-mini | ~200ms | ~120 | Routing, klassifisering, enkle svar |
| gpt-4o | ~400ms | ~80 | Komplekse resonneringer, RAG |
| gpt-4.1 | ~350ms | ~90 | Generell agent-bruk |
| gpt-4.1-mini | ~180ms | ~130 | Høyvolum, lav-latency |
| gpt-4.1-nano | ~100ms | ~150 | Ultra-lav latency, enkel klassifisering |

### Tiered Model Strategy

```python
# Velg modell basert på oppgavens kompleksitet
MODEL_TIERS = {
    "routing": "gpt-4.1-nano",       # Ultra-rask routing
    "simple_qa": "gpt-4o-mini",       # Enkle spørsmål
    "rag_synthesis": "gpt-4o",        # RAG med resonnering
    "complex_analysis": "gpt-4.1",    # Dype analyser
    "evaluation": "gpt-4o-mini",      # Batch-evaluering
}

async def select_model_for_task(task_type: str, context: dict) -> str:
    base_model = MODEL_TIERS.get(task_type, "gpt-4o-mini")

    # Override basert på kontekst
    if context.get("token_count", 0) > 4000:
        # Store kontekster trenger kraftigere modell
        return "gpt-4o"
    if context.get("requires_reasoning", False):
        return "gpt-4.1"

    return base_model
```

## Norsk offentlig sektor

| Aspekt | Krav | Latency-implikasjon |
|--------|------|---------------------|
| Data residency | Azure Norway East | +20-50ms vs. West Europe |
| Content filtering | Obligatorisk for offentlig sektor | +100-200ms per request |
| Audit logging | Full logging av alle kall | +10-30ms overhead |
| VNet isolation | Private endpoints | +5-15ms for DNS resolution |
| Token-grenser | Budget-begrensninger | Bruk mindre modeller der mulig |

## Beslutningsrammeverk

| Scenario | Anbefaling | Begrunnelse |
|----------|------------|-------------|
| Chat-bot med < 2s krav | Streaming + gpt-4o-mini + semantic cache | Lavest opplevd latency |
| Multi-agent med 3+ steg | Parallelliser uavhengige steg + prefetching | Reduserer sekvensielle ventetider |
| Høy-volum asynkront | Batch API + gpt-4o-mini | 50% kostnadsreduksjon, 24h SLA |
| RAG med store dokumenter | Prompt caching + chunk-optimalisering | Reduser TTFT for store prompts |
| Global distribusjon | APIM multi-region + Front Door | Nearest-region routing |

## For Cosmo

- **Streaming er alltid-på for interaktive agenter** -- det er den enkelttiltaket som mest forbedrer brukeropplevelsen, selv om total tid forblir lik.
- **Model tiering er obligatorisk** for kostnadseffektiv latency-optimalisering -- bruk nano/mini for routing og klassifisering, gpt-4o for kompleks resonnering.
- **Parallelliser aggressivt**: Prefetch data mens routing pågår, kjør uavhengige verktøykall parallelt, og bruk async/await konsekvent.
- **Semantic caching i APIM** gir dramatisk forbedring for repetitive forespørsler -- spesielt i kundestøtte-scenarier der mange brukere stiller lignende spørsmål.
- **Mål alltid TTFT og total latency separat** -- TTFT driver brukeropplevelse, total latency driver kostnad. Optimaliser begge men prioriter TTFT for interaktive scenarier.
