🎯 Um navegador em nuvem personalizável e anti-detecção alimentado por Chromium desenvolvido internamente, projetado para rastreadores web e agentes de IA. 👉Experimente agora
De volta ao blog

Conjunto de Prompting de Marca: Dê a um Agente de Navegador Algo que Ele Possa Clicar

Daniel Kim
Daniel Kim

Lead Scraping Automation Engineer

07-Aug-2026

TL;DR:

  • Um modelo de visão que pediu coordenadas de pixel em uma página de catálogo ao vivo não acertou o alvo nenhuma vez — 0 de 5 tentativas em uma sessão, 0 de 15 em três — e não foi aleatório: a uma temperatura de 0, ele retornou quase o mesmo ponto a cada tentativa.
  • A solicitação de conjunto de marcas — numerando cada elemento clicável e pedindo ao modelo um índice em vez de uma coordenada — obteve 5 de 5 na mesma sessão e 15 de 15 no total, na mesma página com o mesmo modelo.
  • A marcação não é gratuita: o prompt cresceu de 1.343 para 2.100 tokens (+56,4%), cerca de $0.00021 por decisão à taxa do modelo testado.
  • Em um navegador CDP remoto, page.set_viewport_size() não redimensiona o layout do viewport. Em doze sessões novas em três execuções independentes, innerWidth não foi alterado pela chamada e a janela real variou de 800 px a 3.840 px — nunca o tamanho solicitado — enquanto a captura de tela sempre teve exatamente o tamanho pedido.
  • Essa é a causa raiz: a imagem sobre a qual o modelo raciocina e o espaço onde seu clique cai estão em diferentes quadros de coordenadas, e o deslocamento muda por sessão. Um índice resolvido através do DOM é imune a tudo isso.
  • O plano gratuito Scrapeless cobre as execuções do navegador em nuvem neste guia.

Um agente de navegador tem que converter "abrir o segundo livro" em um clique em algum lugar específico. Essa conversão — ancoragem — é onde a maioria das execuções do agente falha silenciosamente. O modelo lê a página corretamente, explica seu plano corretamente e, em seguida, clica em algum lugar que não é a coisa que acabou de descrever.

A correção usual é a solicitação de conjunto de marcas: desenhar uma caixa numerada sobre cada elemento clicável, entregar ao modelo a imagem mais uma lista dos números e deixar que ele responda 60 em vez de (564, 623). A técnica vem do artigo Set-of-Mark da Microsoft, e agora está na maioria das pilhas de agentes de alguma forma.

O que é difícil de encontrar é uma medida. Este guia constrói ambas as abordagens contra a mesma página ao vivo com o mesmo modelo, executa cada uma cinco vezes em uma única sessão e relata o que realmente aconteceu — incluindo o detalhe do navegador remoto que explica por que a versão das coordenadas falha de forma tão consistente.

Por que as Coordenadas Falham

Comece pela versão que falha, porque a forma de sua falha é a parte interessante.

A tarefa: em um catálogo de livros ao vivo, abrir a página do produto para o livro intitulado Soumission. O modelo recebe uma captura de tela e é solicitado um ponto de clique.

python Copy
import base64, json, os, urllib.request
from urllib.parse import urlencode
from playwright.sync_api import sync_playwright

TARGET = "https://books.toscrape.com/"
TASK = "open the product page for the book titled 'Soumission'"
MODEL = "google/gemini-2.5-flash-lite"
CDP = "wss://browser.scrapeless.com/api/v2/browser?" + urlencode({
    "token": os.environ["SCRAPELESS_API_KEY"], "sessionTTL": 300, "proxyCountry": "US"})


def ask(png, prompt):
    body = {"model": MODEL, "max_tokens": 200, "temperature": 0,
            "messages": [{"role": "user", "content": [
                {"type": "text", "text": prompt},
                {"type": "image_url", "image_url": {
                    "url": "data:image/png;base64," + base64.b64encode(png).decode()}}]}]}
    req = urllib.request.Request(
        "https://openrouter.ai/api/v1/chat/completions", data=json.dumps(body).encode(),
        headers={"Authorization": f"Bearer {os.environ['OPENROUTER_API_KEY']}",
                 "Content-Type": "application/json"})
    d = json.load(urllib.request.urlopen(req, timeout=120))
    return d["choices"][0]["message"]["content"].strip(), d["usage"]


with sync_playwright() as p:
    browser = p.chromium.connect_over_cdp(CDP)
    page = browser.new_page()
    page.set_viewport_size({"width": 1280, "height": 1400})
    page.goto(TARGET, wait_until="domcontentloaded")

    answer, usage = ask(page.screenshot(),
        f"This is a 1280x1400 browser screenshot. To {TASK}, give the pixel coordinates "
        f'of the element to click. Reply with JSON only: {{"x":int,"y":int}}')
    print("model answered:", answer)

    c = json.loads(answer[answer.index("{"):answer.rindex("}") + 1])
    landed = page.evaluate(
        "([x,y]) => { const e = document.elementFromPoint(x,y);"
        "return e ? (e.closest('a')?.getAttribute('href') || '<'+e.tagName+'>') : null }",
        [c["x"], c["y"]])
    print(f"click ({c['x']},{c['y']}) lands on -> {landed}")
    print("prompt tokens:", usage["prompt_tokens"])
    browser.close()

O modelo não recusa, vacila ou retorna nada malformado. Ele responde imediatamente e confiantemente:

text Copy
model answered: {"x": 564, "y": 623}
click (564,623) lands on -> <BODY>
prompt tokens: 1343

<BODY> significa que o clique não acertou nada — plano de fundo da página vazio, não um link de forma alguma. O agente agora relatará que clicou, esperará por uma navegação que nunca acontece e continuará com um plano baseado em um passo que silenciosamente não ocorreu.

Execute isso cinco vezes na mesma sessão e a falha não se average:

tentativa resposta elemento atingido resultado
1 (564, 623) <BODY> FALHA
2 (563, 630) <BODY> FALHA
3 (563, 630) <BODY> FALHA
4 (563, 630) <BODY> FALHA
5 (564, 623) <BODY> FALHA

0 de 5. Uma falha instável é suportável — você testa três vezes, pega a maioria e segue em frente. Essa falha é estável. A uma temperatura de 0, o modelo retorna o mesmo ponto dentro de alguns pixels, e cada repetição cai no mesmo espaço vazio. A votação de autoconsistência retornaria a resposta errada três vezes e chamaria isso de consenso.

Repetir toda a comparação em três sessões separadas deu o mesmo veredicto cada vez: 0 de 15 para coordenadas. O que mudou entre as sessões foi apenas como ele errou — em uma sessão, cada clique caiu no produto ao lado do alvo em vez de no fundo vazio. O alvo que ele atingiu mudou com a sessão; o fato de que errou não mudou.

A Incompatibilidade de Quadro Abaixo

A explicação óbvia é que modelos de visão são simplesmente ruins em coordenadas precisas, o que a literatura de ancoragem de GUI apoia amplamente. Em um navegador remoto, há uma segunda causa empilhada em cima disso, e vale a pena saber porque também quebra abordagens que nem envolvem um modelo.

Pergunte à página qual tamanho ela acha que é, antes e depois de definir o viewport:

python Copy
import os
from urllib.parse import urlencode
from playwright.sync_api import sync_playwright

CDP = "wss://browser.scrapeless.com/api/v2/browser?" + urlencode({
    "token": os.environ["SCRAPELESS_API_KEY"], "sessionTTL": 300, "proxyCountry": "US"})

with sync_playwright() as p:
    for run in range(1, 5):
        browser = p.chromium.connect_over_cdp(CDP)
        page = browser.new_page()
        before = page.evaluate("() => [innerWidth, innerHeight]")
        page.set_viewport_size({"width": 1280, "height": 900})
        page.goto("https://books.toscrape.com/", wait_until="domcontentloaded")
        after = page.evaluate("() => [innerWidth, innerHeight]")
        shot = page.screenshot()
        print(f"run{run}: innerWH before={before} after={after} png_bytes={len(shot)}")
        browser.close()

Quatro sessões novas:

text Copy
run1: innerWH before=[1550, 1050] after=[1550, 1050] png_bytes=196373
run2: innerWH before=[1240, 560] after=[1240, 560] png_bytes=116304
run3: innerWH before=[1680, 1002] after=[1680, 1002] png_bytes=171659
run4: innerWH before=[3840, 1152] after=[3840, 1152] png_bytes=5289

set_viewport_size() teve nenhum efeito no layout do viewportinnerWidth e innerHeight são idênticos antes e depois da chamada em todas as quatro execuções. O tamanho real da janela também não é algo que você escolheu. Execuções repetidas do mesmo script retornaram 2.560×1.408, 1.920×1.070, 1.440×839, 800×570 e 3.840×1.050, entre outros; em doze sessões em três execuções, o layout do viewport nunca teve o tamanho solicitado, e as larguras variaram de 800 px a 3.840 px.
A captura de tela, por sua vez, tinha exatamente o tamanho solicitado toda vez. Veja o que isso faz em run4: uma página disposta para uma janela de 3.840 px, capturada em uma tela de 1.280 px, produziu um 5 KB PNG onde as outras execuções produziram 116–196 KB. O corte capturou principalmente uma disposição vazia. Essa captura de tela é o que o modelo teria sido solicitado a raciocinar.

O mesmo script contra um navegador lançado localmente se comporta da maneira que você esperaria, o que torna fácil perder isso durante o desenvolvimento:

text Copy
local innerWH: [1280, 1400]
after set_viewport_size: [1280, 900]

Localmente, a chamada funciona e os frames concordam. Conecte-se a um navegador remoto e a chamada silenciosamente para de funcionar.

Assim, a página se dispõe para uma janela de 3.840 px, o PNG entregue ao modelo tem 1.280 px de largura, e a coordenada que o modelo lê desse PNG é então usada contra um DOM usando a geometria mais ampla. Os dois frames estão ligados por um fator que muda toda vez que você abre uma sessão — que é exatamente por isso que a falha cai em um produto vizinho em uma sessão e em um fundo vazio em outra. Quando connect_over_cdp se conecta a um navegador já em execução, o Playwright é um cliente desse navegador em vez de seu proprietário, e a emulação do viewport é uma propriedade dos contextos que ele cria — uma distinção documentação do contexto do Playwright traça explicitamente.

O modelo não está adivinhando aleatoriamente. Ele está lendo a imagem corretamente e respondendo no frame da imagem, e então esse número é usado em um diferente.

Marcando Elementos em vez disso

O Set-of-Mark remove o problema do frame ao nunca transportar uma coordenada através dele. O modelo retorna um índice; o índice é resolvido de volta para um elemento pela própria página, na geometria da página.

A passagem de marcação percorre os elementos interativos, filtra aqueles que um usuário poderia realmente clicar, desenha uma caixa numerada sobre cada um, e retorna uma lista paralela. Salve como mark.js:

javascript Copy
() => {
  const sel = 'a[href], button, input, select, textarea, [role="button"], [onclick]';
  const out = [];
  document.querySelectorAll('#som-layer').forEach(n => n.remove());
  const layer = document.createElement('div');
  layer.id = 'som-layer';
  layer.style.cssText = 'position:fixed;inset:0;pointer-events:none;z-index:2147483647';
  document.body.appendChild(layer);
  const vw = innerWidth, vh = innerHeight;
  let i = 0;
  for (const el of document.querySelectorAll(sel)) {
    const r = el.getBoundingClientRect();
    if (r.width < 8 || r.height < 8) continue;
    if (r.bottom < 0 || r.top > vh || r.right < 0 || r.left > vw) continue;
    const cs = getComputedStyle(el);
    if (cs.visibility === 'hidden' || cs.display === 'none' || cs.opacity === '0') continue;
    const box = document.createElement('div');
    box.style.cssText = `position:absolute;left:${r.left}px;top:${r.top}px;width:${r.width}px;height:${r.height}px;border:2px solid #E11D48;box-sizing:border-box`;
    const tag = document.createElement('div');
    tag.textContent = i;
    tag.style.cssText = `position:absolute;left:${r.left}px;top:${Math.max(0, r.top - 14)}px;background:#E11D48;color:#fff;font:bold 12px monospace;padding:0 4px;line-height:14px`;
    layer.append(box, tag);
    out.push({ i, tag: el.tagName.toLowerCase(),
               name: (el.innerText || el.getAttribute('aria-label') || el.value || '').trim().slice(0, 60),
               x: Math.round(r.left + r.width / 2), y: Math.round(r.top + r.height / 2) });
    i++;
  }
  return out;
}

Os filtros de tamanho e visibilidade importam mais do que parecem. Sem o teste width < 8 || height < 8 você marca pixels de rastreamento e wrappers colapsadas; sem o teste de limites do viewport você marca o rodapé inteiro de uma página longa e entrega números ao modelo para coisas que ele não pode ver. Ambos produzem menus onde os índices e a imagem discordam, o que é pior do que não ter marcas.

A sobreposição é uma position:fixed camada com pointer-events:none, então ela pinta acima da página sem interceptar o clique que você está prestes a fazer e sem reflow de nada embaixo.

O centro de cada elemento é capturado de getBoundingClientRect() no momento da marcação, no sistema de coordenadas da página. Esse é o número que o clique usará, e ele nunca faz uma viagem de ida e volta pela imagem.

O Loop Completo

Marcação, captura de tela, solicitação de um índice, e clicando nele:

python Copy
import base64, json, os, pathlib, urllib.request
from urllib.parse import urlencode
from playwright.sync_api import sync_playwright

TARGET = "https://books.toscrape.com/"
TASK = "open the product page for the book titled 'Soumission'"
MODEL = "google/gemini-2.5-flash-lite"
MARK_JS = pathlib.Path("mark.js").read_text()
CDP = "wss://browser.scrapeless.com/api/v2/browser?" + urlencode({
    "token": os.environ["SCRAPELESS_API_KEY"], "sessionTTL": 300, "proxyCountry": "US"})


def ask(png, prompt):
    body = {"model": MODEL, "max_tokens": 200, "temperature": 0,
            "messages": [{"role": "user", "content": [
                {"type": "text", "text": prompt},
                {"type": "image_url", "image_url": {
                    "url": "data:image/png;base64," + base64.b64encode(png).decode()}}]}]}
    req = urllib.request.Request(
        "https://openrouter.ai/api/v1/chat/completions", data=json.dumps(body).encode(),
        headers={"Authorization": f"Bearer {os.environ['OPENROUTER_API_KEY']}",
                 "Content-Type": "application/json"})
    d = json.load(urllib.request.urlopen(req, timeout=120))
    return d["choices"][0]["message"]["content"].strip(), d["usage"]


with sync_playwright() as p:
    browser = p.chromium.connect_over_cdp(CDP)
    page = browser.new_page()
    page.goto(TARGET, wait_until="domcontentloaded")

    marks = page.evaluate(MARK_JS)
    shot = page.screenshot()
    page.evaluate("() => document.querySelectorAll('#som-layer').forEach(n => n.remove())")
    print(f"marked {len(marks)} interactive elements")

    menu = "\n".join(f"[{m['i']}] <{m['tag']}> {m['name']}" for m in marks)
    answer, usage = ask(shot,
        f"The screenshot has numbered red marks on every clickable element.\n{menu}\n\n"
        f"To {TASK}, which mark do you click? Reply with JSON only: "
        '{"index":int}. Do not explain.')
    print("model answered:", answer)

    idx = json.loads(answer[answer.index("{"):answer.rindex("}") + 1])["index"]
    chosen = next(m for m in marks if m["i"] == idx)
    print(f"chose [{idx}] {chosen['name']!r}")

    with page.expect_navigation(wait_until="domcontentloaded"):
        page.mouse.click(chosen["x"], chosen["y"])
    print("landed on:", page.url)
    print("prompt tokens:", usage["prompt_tokens"])
    browser.close()

Saída:

text Copy
marked 85 interactive elements
model answered: {"index": 60}
chose [60] 'Soumission'
landed on: https://books.toscrape.com/catalogue/soumission_998/index.html
prompt tokens: 2100

Cinco tentativas, mesma sessão, mesmo modelo, mesma temperatura:

tentativa índice rótulo pousou em resultado
1 60 Soumission soumission_998 ACERTO
2 60 Soumission soumission_998 ACERTO
3 60 Soumission soumission_998 ACERTO
4 60 Soumission soumission_998 ACERTO
5 60 Soumission soumission_998 ACERTO

5 de 5, na sessão onde as coordenadas marcaram 0 de 5, e 15 de 15 em todas as três sessões.

O determinismo corta o outro lado agora. A mesma propriedade que tornou a falha de coordenadas irreparável — uma resposta estável na temperatura 0 — faz a versão marcada confiavelmente certa em vez de confiavelmente errada.

Espere que os números absolutos mudem entre sessões. Como o viewport de layout é o que quer que a janela remota seja, o mesmo script marcou 85 elementos e escolheu o índice 60 em uma sessão, e 52 elementos e o índice 43 em outra. Ambos pousaram em soumission_998. As contagens são dependentes da sessão; o destino não é, o que é o que resolver através do DOM lhe oferece.

O Que Isso Custa

A marcação adiciona o menu de elementos ao prompt, e o menu cresce com a página:

abordagem tokens do prompt custo por decisão
captura de tela bruta → coordenadas 1.343 $0.00014
set-of-mark → índice 2.100 $0.00021
delta +757 (+56,4%) +49,1%

O prompt de captura de tela bruta é um fixo de 1.343 tokens porque a imagem é um tamanho fixo; o prompt marcado escala com quantos elementos você decidiu marcar. Cinquenta e seis por cento mais prompt para uma página com 85 elementos marcados, a uma taxa onde uma decisão custa cerca de um quinto de um centavo.

Esse escalonamento é o verdadeiro argumento para os filtros de visibilidade acima: cada elemento que você decide não marcar é tokens que você não gasta e um índice que o modelo não pode escolher por engano. Contra uma variação de 0 a 100% em se o agente clica na coisa certa, não é uma decisão próxima.

Onde Isso Ainda Quebra

Set-of-Mark corrige a fundamentação. Não corrige tudo.

Superfícies de Canvas e WebGL não têm elementos para marcar. Um mapa, um canvas de gráficos ou um jogo renderiza em pixels sem estrutura DOM por baixo. A marcação não encontra nada e você volta a coordenadas ou a quaisquer ganchos de acessibilidade que o componente expõe.

As marcações são limitadas ao viewport. Tudo abaixo da dobra não é marcado por design, então um agente que precisa de um elemento mais abaixo tem que rolar e remarcar. Trate a marcação como um passo por observação, não como uma configuração única.

Páginas densas produzem menus longos. Uma página com 400 controles gera um menu de 400 linhas, e a fatura de tokens cai em cada passo do loop. Filtre por papel, por região ou por proximidade à tarefa antes de marcar.

O índice é tão bom quanto o rótulo. Um elemento cujo nome acessível está vazio aparece como [31] <a> e o modelo não tem nada com que raciocinar. Esse é o mesmo problema de nomenclatura que leitores de tela enfrentam, e a solução é a mesma que a Computação do Nome e Descrição Acessível já especifica: prefira aria-label, recorra ao título ou ao texto próximo.

Re-marcar após cada navegação é obrigatório. Os índices são posicionais e são reatribuídos na próxima renderização. Manter um índice durante uma transição de página é um erro que parece uma falha de fundamentação.

Tudo o que está acima foi executado contra o Navegador de Scraping Scrapeless via CDP, que é por isso que a descoberta do viewport apareceu — um chromium.launch() local fornece o viewport que você solicitou e oculta o problema até que você implemente.

Conectar é uma URL WebSocket:

python Copy
import os
from urllib.parse import urlencode

CDP = "wss://browser.scrapeless.com/api/v2/browser?" + urlencode({
    "token": os.environ["SCRAPELESS_API_KEY"],
    "sessionTTL": 300,
    "proxyCountry": "US",
})
print(CDP.split("?")[0])

proxyCountry fixa a região de saída, o que é importante para qualquer catálogo que localiza preços ou disponibilidade. O restante do loop permanece inalterado — a API do Playwright é a mesma, tenha lançado o navegador ou se conectado a um, porque ambos falam o Protocolo DevTools do Chrome por baixo. Se o protocolo subjacente for desconhecido, o primer CDP cobre o transporte, e o loop do agente de uso de computador cobre o ciclo observar-decidir-agir que esse passo de fundamentação se encaixa.

Dois hábitos se transferem das medições acima. Leia innerWidth e innerHeight da página em vez de assumir o viewport que você solicitou — em uma sessão remota, esses são números diferentes. E resolva cada clique através de um elemento que a página lhe forneceu, nunca através de uma coordenada calculada a partir de uma imagem.

Conclusão

A medição é contundente. Na mesma página, com o mesmo modelo e o mesmo prompt, pedir coordenadas de pixel conseguiu o elemento certo 0 vezes em 15; pedir um índice conseguiu 15 vezes em 15. A falha de coordenadas não foi ruído que uma amostragem repetida suavizaria — a temperatura 0 retornava o mesmo ponto e errava da mesma forma todas as vezes.

Por trás da fraqueza da fundamentação do modelo existe um erro mais claro: em um navegador remoto, a captura de tela e o DOM não estão no mesmo sistema de coordenadas, e set_viewport_size() não os colocará lá. Numerar os elementos contorna ambos os problemas pelo preço de algumas centenas de tokens.

Pronto para construir o loop contra um navegador gerenciado? Comece grátis com Scrapeless — o plano gratuito cobre cada execução neste guia, e os preços escalam a partir daí.

FAQ

Q: Por que meu agente de navegador clica no elemento errado, mesmo descrevendo o certo?

Porque descrever e localizar são habilidades separadas. O modelo lê a página corretamente e depois tem que emitir um número preciso, e números precisos a partir de uma imagem são sua saída mais fraca. Adicione um navegador remoto, onde a captura de tela e o DOM usam diferentes quadros de coordenadas, e o erro deixa de ser ocasional e se torna sistemático — nas execuções acima, errou em cada uma das 15 tentativas.

Q: O Set-of-Mark é melhor do que enviar o DOM ou a árvore de acessibilidade?
Eles resolvem metades diferentes. Uma representação textual diz ao modelo o que existe; marcas dizem onde essas coisas estão na imagem que está analisando e dão a ele um token que se resolve em um elemento real. As marcas também permanecem pequenas — um índice e um rótulo curto por elemento — enquanto o HTML bruto em uma página de catálogo modesta chega a cinco figuras de tokens.

P: Quantos tokens a marcação adiciona?

Na página testada, 757 tokens, levando o prompt de 1.343 para 2.100 — cerca de 56% a mais para 85 elementos marcados. O prompt de captura de tela bruta é fixo porque o tamanho da imagem é fixo; o prompt marcado escala com o número de marcas, então os filtros de visibilidade e tamanho estão controlando custos tanto quanto controle de precisão.

P: Isso funciona em um navegador remoto ou na nuvem?

Funciona melhor lá, e é mais necessário lá. Como o índice se resolve através de getBoundingClientRect() dentro da página, ele não é afetado pela discrepância entre o tamanho da captura de tela e o viewport real da janela remota — a discrepância que fez a abordagem de coordenadas falhar todas as vezes.

P: Eu preciso remarcar após cada ação?

Sim. Os índices são atribuídos em ordem de documento sobre elementos atualmente visíveis, então eles são reatribuídos após qualquer navegação, rolagem ou atualização do DOM. Marque como parte de cada passo de observação; reutilizar um índice de uma captura de tela anterior é a maneira mais comum como essa técnica é implementada de maneira errada.

P: O que acontece em páginas sem elementos clicáveis para marcar?

Superfícies de Canvas, WebGL e vídeo renderizam sem uma estrutura DOM para enumerar, então a marcação não retorna nada útil. Esses precisam de uma estratégia diferente — ganchos de acessibilidade onde o componente os fornece, ou uma abordagem de coordenadas com a discrepância do quadro explicitamente corrigida.

Na Scorretless, acessamos apenas dados disponíveis ao público, enquanto cumprem estritamente as leis, regulamentos e políticas de privacidade do site aplicáveis. O conteúdo deste blog é apenas para fins de demonstração e não envolve atividades ilegais ou infratoras. Não temos garantias e negamos toda a responsabilidade pelo uso de informações deste blog ou links de terceiros. Antes de se envolver em qualquer atividade de raspagem, consulte seu consultor jurídico e revise os termos de serviço do site de destino ou obtenha as permissões necessárias.

Artigos mais populares

Catálogo