> Nível 8 de Agent Harness Patterns, uma trilha de padrões sobre como funcionam os agentes de IA. Versão web: https://harnesspatterns.dev/pt/patterns/prompt-caching · Todos os padrões (em inglês): https://harnesspatterns.dev/llms.txt

# Cache de prompts

A cada turno, o loop manda ao modelo a requisição inteira de novo. Quase tudo é igual à vez anterior, e o provedor consegue lembrar, desde que você não mude o jeito como ela começa.

## O problema

Um modelo não lembra de nada entre chamadas, então o loop reenvia tudo a cada turno: o system prompt, a lista de ferramentas e o histórico inteiro. Num agente que roda dez turnos, as instruções longas do alto viajam dez vezes, e são pagas dez vezes.

E deixam tudo mais lento: antes de escrever uma única palavra, o modelo precisa ler a requisição inteira de novo, desde o primeiro token.

## A solução

Os provedores guardam uma memória de curta duração das requisições que acabaram de ler. Quando uma requisição nova começa exatamente como uma recente, eles reaproveitam a parte que coincide e só processam o resto. Essa parte é cobrada por uma fração do preço (muitas vezes um décimo, dependendo do provedor e do modelo), e a resposta começa antes.

A regra está na palavra *começa*: a coincidência é contada a partir do primeiro token e para no primeiro que é diferente. Um agente se encaixa naturalmente, porque cada requisição é a anterior mais algumas mensagens no final. Você só precisa não quebrar isso:

- **Algo que muda, lá no alto**
   a hora, um id de requisição, o nome do usuário
   Mantenha o system prompt fixo. O que muda vai na última mensagem, no final.
- **Ferramentas que se mexem**
   outra ordem, uma ferramenta adicionada no meio do caminho
   Monte a lista de ferramentas uma vez, numa ordem fixa, e mande a mesma a cada turno.
- **Reescrever o passado**
   editar, cortar ou resumir mensagens antigas
   Só acrescente no final. Quando precisar reescrever, tudo o que vem depois desse ponto é pago de novo.
- **Trocar de modelo**
   um modelo mais barato para um turno
   Cada modelo tem seu próprio cache. Uma troca o começa do zero.

Alguns provedores fazem cache sozinhos quando a requisição é longa o bastante: a OpenAI faz a partir de 1.024 tokens. Outros, como a Anthropic, só fazem cache onde você marca. De qualquer forma, o uso que eles devolvem diz quantos tokens de entrada vieram do cache, então dá para conferir.

## O elenco

O mesmo elenco de sempre, numa noite de jogos.

- **O Simon** (a requisição): Cada rodada repete a sequência inteira e acrescenta algo no final, como cada turno reenvia a requisição.
- **As luzes** (seus blocos): Amarelas para o system prompt e as ferramentas, e depois uma por mensagem, nas cores do histórico.
- **Astor** (o loop): Toca a sequência inteira para o Oráculo a cada turno, e depois roda as ferramentas como sempre.
- **O Oráculo** (o modelo): Seu balão é o cache do provedor: as luzes que ele já conhece, a partir da primeira.
- **O placar** (o uso): Tokens de entrada enviados, lidos do cache e pagos, com os do cache a um décimo.
- **A sala** (as ferramentas): A prateleira de jogos e o telefone: `game_shelf` e `order_pizza`.

## O código

**Com astorlm:** o agente monta seu system prompt uma vez e o reenvia sem mudanças a cada turno, com as mesmas ferramentas na mesma ordem, e o histórico só cresce no final. Sua parte é deixar o que muda fora de `systemPrompt`. Cada `turn_end` traz `cacheReadTokens`, que o `OpenAIProvider` lê da resposta.

**Do zero:** o loop do nível 2, com o começo fixo montado uma vez fora do loop, e uma linha que registra `cached_tokens` do uso de cada resposta.

**Com astorlm**

```ts
import { OpenAIProvider, createLocalAgent, tool } from 'astorlm'
import { z } from 'zod'

const gameShelf = tool({
  name: 'game_shelf',
  description: 'List the board games on the living-room shelf.',
  schema: z.object({}),
  execute: async () => home.shelf(), // your code
})

const orderPizza = tool({
  name: 'order_pizza',
  description: 'Order pizza for delivery. Returns how long it will take.',
  schema: z.object({ size: z.enum(['medium', 'large']), count: z.number().int().min(1) }),
  execute: async ({ size, count }) => pizzeria.order(size, count), // your code
})

const agent = await createLocalAgent({
  // Any OpenAI-compatible endpoint: OpenAI, Ollama, LM Studio, vLLM, a proxy…
  provider: new OpenAIProvider({
    baseURL: 'http://localhost:11434/v1', // e.g. Ollama's default address
    model: 'your-model', // e.g. 'llama3.1', 'gpt-4o-mini'
    apiKey: 'YOUR_API_KEY', // local servers usually ignore it
  }),
  // The start of every request. astorlm builds it once and resends it unchanged every turn.
  // Nothing that changes goes here: no clock, no request id, no user name.
  systemPrompt: HOUSE_RULES, // a long, fixed text: the more of it, the more the cache saves
  tools: [gameShelf, orderPizza], // same tools, same order, every turn
  maxTurns: 10,
})

// OpenAI caches long prompts on its own (from 1,024 tokens), and says how much it reused.
agent.on('event', (event) => {
  if (event.type !== 'turn_end' || !event.usage) return
  const { inputTokens, cacheReadTokens = 0 } = event.usage
  console.log(`turn ${event.turn}: ${cacheReadTokens} of ${inputTokens} input tokens from cache`)
})

// Need the time? Put it at the end, in the message, where it only changes what comes after it.
const now = new Date().toLocaleTimeString()
await agent.run(`Game night for four: see what games we have, and order pizza. (It is ${now}.)`)
```

**TypeScript**

```ts
// Prompt caching from scratch. Plain fetch, no SDK.
// There is nothing to build: the provider caches. Your job is to not break it.

// Any OpenAI-compatible endpoint: OpenAI, Ollama, LM Studio, vLLM, a proxy…
const LLM = {
  baseURL: 'http://localhost:11434/v1', // e.g. Ollama's default address
  model: 'your-model', // e.g. 'llama3.1', 'gpt-4o-mini'
  apiKey: 'YOUR_API_KEY', // local servers usually ignore it
}

type ToolCall = { id: string; function: { name: string; arguments: string } }
type Message =
  | { role: 'system' | 'user'; content: string }
  | { role: 'assistant'; content: string | null; tool_calls?: ToolCall[] }
  | { role: 'tool'; tool_call_id: string; content: string }

const tools: Record<string, (args: Record<string, string>) => Promise<string>> = { game_shelf: gameShelf, order_pizza: orderPizza }

// 1. The fixed start, built once: same text and same tools, in the same order, every request.
const SYSTEM: Message = { role: 'system', content: HOUSE_RULES } // ✗ never `It is ${new Date()}` up here
const TOOL_SCHEMAS = Object.freeze([/* one JSON Schema per tool, always in this order */])

export async function runAgent(prompt: string, maxTurns = 10): Promise<string> {
  // 2. The history only grows at the end. Editing an old message breaks the cache from there on.
  const messages: Message[] = [SYSTEM, { role: 'user', content: prompt }]

  for (let turn = 1; turn <= maxTurns; turn++) {
    const res = await fetch(`${LLM.baseURL}/chat/completions`, {
      method: 'POST',
      headers: { 'content-type': 'application/json', authorization: `Bearer ${LLM.apiKey}` },
      body: JSON.stringify({ model: LLM.model, messages, tools: TOOL_SCHEMAS }),
    })
    const { choices, usage } = await res.json()

    // 3. Check it's working: how much of the input the provider read from its cache.
    const cached = usage?.prompt_tokens_details?.cached_tokens ?? 0
    console.log(`turn ${turn}: ${cached} of ${usage?.prompt_tokens} input tokens from cache`)

    const [choice] = choices
    const reply: Message = choice.message
    messages.push(reply)
    if (choice.finish_reason !== 'tool_calls' || reply.role !== 'assistant') return reply.content ?? ''

    for (const call of reply.tool_calls ?? []) {
      const output = await tools[call.function.name]!(JSON.parse(call.function.arguments || '{}'))
      messages.push({ role: 'tool', tool_call_id: call.id, content: output })
    }
  }
  throw new Error(`No answer after ${maxTurns} turns`)
}
```

**Python**

```python
# Prompt caching from scratch. Standard library only, no SDK.
# There is nothing to build: the provider caches. Your job is to not break it.
import json
import urllib.request

# Any OpenAI-compatible endpoint: OpenAI, Ollama, LM Studio, vLLM, a proxy...
LLM = {
    "base_url": "http://localhost:11434/v1",  # e.g. Ollama's default address
    "model": "your-model",  # e.g. "llama3.1", "gpt-4o-mini"
    "api_key": "YOUR_API_KEY",  # local servers usually ignore it
}

TOOLS = {"game_shelf": game_shelf, "order_pizza": order_pizza}

# 1. The fixed start, built once: same text and same tools, in the same order, every request.
SYSTEM = {"role": "system", "content": HOUSE_RULES}  # never f"It is {datetime.now()}" up here
TOOL_SCHEMAS = (...)  # one JSON Schema per tool, always in this order

def chat(messages):
    request = urllib.request.Request(
        f"{LLM['base_url']}/chat/completions",
        data=json.dumps({"model": LLM["model"], "messages": messages, "tools": list(TOOL_SCHEMAS)}).encode(),
        headers={"Content-Type": "application/json", "Authorization": f"Bearer {LLM['api_key']}"},
    )
    with urllib.request.urlopen(request) as response:
        return json.load(response)

def run_agent(prompt, max_turns=10):
    # 2. The history only grows at the end. Editing an old message breaks the cache from there on.
    messages = [SYSTEM, {"role": "user", "content": prompt}]

    for turn in range(1, max_turns + 1):
        data = chat(messages)

        # 3. Check it's working: how much of the input the provider read from its cache.
        usage = data.get("usage") or {}
        cached = (usage.get("prompt_tokens_details") or {}).get("cached_tokens", 0)
        print(f"turn {turn}: {cached} of {usage.get('prompt_tokens')} input tokens from cache")

        choice = data["choices"][0]
        reply = choice["message"]
        messages.append(reply)
        if choice["finish_reason"] != "tool_calls":
            return reply.get("content") or ""

        for call in reply.get("tool_calls", []):
            output = TOOLS[call["function"]["name"]](**json.loads(call["function"]["arguments"] or "{}"))
            messages.append({"role": "tool", "tool_call_id": call["id"], "content": output})

    raise RuntimeError(f"No answer after {max_turns} turns")
```

## O que observar

- **O cache não dura.** Ele vive poucos minutos sem uso. Um agente que espera uma hora por uma pessoa, ou um heartbeat que bate a cada duas horas, paga de novo por inteiro a primeira requisição.
- **Compactação e cache puxam para lados opostos.** Encolher as mensagens antigas, o próximo nível, reescreve o passado, então cada token depois da primeira mudança é pago por inteiro no turno seguinte. Compacte poucas vezes, e em passos grandes.
- **Prompts curtos não vão para o cache.** Abaixo do mínimo do provedor, não há o que economizar. O cache compensa com system prompts longos, muitas ferramentas ou históricos longos: exatamente o que os agentes têm.
- **Meça.** Se `cacheReadTokens` continua em zero a partir do segundo turno, algo no começo está mudando. Compare duas requisições lado a lado e ache a primeira diferença.
- **Só economiza na entrada.** Os tokens que o modelo escreve custam o mesmo. Nos agentes, a entrada costuma ser a maior parte da conta, então continua sendo a maior economia possível.

## Padrões relacionados

- [0 · Sua caixa de ferramentas](https://harnesspatterns.dev/pt/patterns/your-toolkit.md)
- [2 · O loop do agente](https://harnesspatterns.dev/pt/patterns/agent-loop.md)
- [9 · A mochila enche](https://harnesspatterns.dev/pt/patterns/compaction.md)
- [15 · Observabilidade e avaliações](https://harnesspatterns.dev/pt/patterns/observability.md)
