O que é Condições de Parada?
Condições de Parada (Stop Conditions) é um padrão de projeto de IA para apps com LLM e agentes, do grupo Comportamento de agentes. Definir com clareza quando o agente terminou, desistiu ou precisa de ajuda.
A Larissa colocou no ar um agente de pesquisa. Ele recebe uma pergunta de mercado, busca na web, lê fontes e escreve um relatório. As primeiras semanas foram bem. Aí começaram três tipos de reclamação.
Algumas tarefas nunca terminavam. O agente continuava procurando mais uma fonte, até o orçamento acabar.
Outras terminavam cedo demais. Depois de duas buscas, o agente escrevia um relatório confiante, com informação rasa.
E algumas terminavam com: não consegui terminar. Sem dizer o que tinha encontrado, nem por que parou.
As três têm uma raiz só. Terminar estava definido numa frase do prompt. Quando tiver informação suficiente, escreva o relatório. O modelo decidia sozinho o que era suficiente, e não havia outro jeito de terminar. Nesta aula, você vai aprender a desenhar o fim de um agente, para toda execução terminar, e terminar no estado certo.
Trate o fim como parte do desenho. Com quatro saídas explícitas, cada uma conferida por código sempre que possível.
Sucesso. O objetivo foi cumprido, e existe um critério verificável para isso. O teste passa. O relatório tem o número exigido de fontes válidas.
Limite. O orçamento de passos, tempo ou custo acabou. O agente entrega o que tem e diz que parou no limite.
Desistência. O agente concluiu que não consegue fazer a tarefa, por falta de acesso ou de informação. E explica por quê.
E pedido de ajuda. O agente precisa de uma decisão, ou de um fato, que só uma pessoa tem.
O modelo pode sinalizar sucesso, desistência ou pedido de ajuda, por uma ferramenta própria, ou por saída estruturada. O seu código confirma o sucesso quando consegue. E o limite pertence sempre ao seu código. O juiz encerra a partida no apito, não quando um time cansa.
Cada turno do loop termina com uma decisão do modelo.
Mas o código confere o orçamento antes de tudo. Sem orçamento, a execução para com o estado atual, e o motivo.
O modelo sinaliza uma de quatro intenções, de forma estruturada.
E terminei não basta. O código confere o critério de sucesso, quando ele é verificável.
Sucesso confirmado encerra a tarefa.
Desistir é uma saída válida, com a explicação do que foi tentado.
E pedir ajuda pausa a execução, até uma pessoa responder.
De volta ao agente da Larissa. O critério de sucesso virou uma regra no código. O relatório precisa citar pelo menos três fontes independentes, e todo link precisa abrir. O orçamento é de vinte e cinco passos.
No passo seis, o modelo diz: relatório pronto. A checagem acha duas fontes válidas de três. Recusado. E o modelo recebe o motivo. Falta uma fonte independente.
Então ele continua. No passo catorze, diz que terminou de novo. Quatro fontes válidas, todos os links abrem. Aceito. Motivo da parada: sucesso.
Aquele primeiro terminei era exatamente o relatório raso das reclamações. A checagem devolveu o motivo, e o modelo continuou trabalhando. Sem ela, a tarefa teria parado no passo seis, com uma fonte a menos.
Agora uma pergunta sem resposta pública, como a margem de lucro de uma empresa fechada. Existem duas saídas boas.
No passo nove, o agente desiste. Nenhuma fonte pública dá a margem. Só achei estimativas de terceiros, listadas abaixo.
Ou, se ele continuar tentando, o código para no passo vinte e cinco e entrega um resumo do que achou.
A saída ruim, a que esse padrão evita, é um relatório confiante com um número inventado.
Uma pergunta para você. O seu agente de código chama finish, com terminei, e a nota: corrigi o bug. Os testes continuam vermelhos. O que o loop deve fazer?
Recusar o sucesso, dizer ao modelo quais testes falham e continuar o loop. A declaração do modelo é um pedido para terminar, não uma prova. Quando existe critério verificável, quem decide é o código.
E também não é desistência. O modelo não desistiu. Ele achou que tinha terminado. Encerrar aqui desperdiçaria uma tarefa que pode estar a dois passos de passar. E o orçamento continua valendo, então o loop não fica preso nisso para sempre.
Não chute o teto. Olhe os traces das execuções que terminaram em sucesso e veja quantos passos, segundos e dólares elas usaram. Depois coloque o teto acima da grande maioria, com folga.
Digamos que 95% das execuções de sucesso usam até catorze passos, e a mais longa usa dezenove. Um teto de vinte e cinco dá espaço para o caso difícil e corta cedo um loop preso. Um teto de duzentos, por via das dúvidas, deixa uma execução presa gastar dez vezes mais antes de parar.
E revise os limites quando o tipo de tarefa mudar. Um teto certo para corrigir um teste é curto demais para migrar um módulo.
Agora, os erros comuns. Um limite que lança exceção. A tarefa some, sem estado, sem registro e sem resposta. O limite é uma saída como as outras. Ele entrega o que foi feito e diz que parou ali.
Sucesso é o modelo ter dito que terminou. Se existe um teste, um schema ou uma contagem, use. A opinião do modelo só decide quando não há nada para conferir.
Um prompt que empurra para a desistência. Frases como na dúvida, desista, fazem a taxa de desistência disparar. Diga exatamente quando desistir. Falta de acesso, dado que não existe, permissão negada.
E pedir ajuda sem ninguém do outro lado. Se a pessoa demora horas, a execução fica pendurada. Defina um prazo de espera, e o que acontece quando ele vence.
Em produção, registre um motivo de parada em todo trace, e olhe a distribuição por tipo de tarefa. Sucesso, limite, desistência e ajuda.
E vigie de perto as paradas por limite. Digamos que no agente de navegador, as paradas por limite vão de 3% para 25% das execuções, numa semana. Ninguém fez deploy. Dobrar o orçamento só deixa as execuções presas gastarem mais. Trocar de modelo é o palpite mais caro e menos provável. Abra os traces. Um salto desses, sem mudança de código, normalmente vem do mundo lá fora. Um portal mudou, ou uma ferramenta começou a falhar em silêncio. É exatamente por isso que o motivo da parada vai para o trace.
E monte pelo menos um caso de eval por saída. Uma tarefa fácil espera sucesso. Uma tarefa impossível de propósito, como um portal fora do ar, espera desistência. Uma tarefa que depende de decisão de negócio espera pedido de ajuda. E uma ferramenta que responde ok sem progresso espera parada por limite. Se o agente responde a tarefa impossível com sucesso, você achou o caso mais perigoso de todos. Um falso sucesso.
Na página, todas as versões dão ao modelo uma ferramenta finish, com três tipos, terminei, desisto e preciso de ajuda, e rodam o loop dentro de um orçamento de passos e de tempo.
Quando o sinal é terminei, o código roda os testes. Se passam, sucesso. Se não, o modelo é avisado, e o loop segue.
E quando o orçamento acaba, devolve uma parada por limite, com um resumo.
Em Python e TypeScript, o sinal é lido dentro do loop. No LangGraph, o mesmo sinal é lido numa aresta. Limite, sucesso, desistência e humano viram destinos do grafo. Não frases num prompt.
Então, a ideia para levar. Um agente precisa de um fim desenhado, não de um fim torcido. Sucesso que o código confirma, limite que é do código, desistência que explica e um jeito de pedir ajuda a uma pessoa. Aí toda execução termina num estado conhecido, e o usuário sabe o que recebeu, e por quê.
Na próxima aula, a gente sai de como um agente termina para o que ele tem nas mãos: tool sprawl. Até lá.
Stop Conditions
Condições de Parada (Stop Conditions) é um padrão de projeto de IA para apps com LLM e agentes, do grupo Comportamento de agentes. Definir com clareza quando o agente terminou, desistiu ou precisa de ajuda.
Definir, fora do modelo, quando um agente terminou, quando desistiu e quando precisa de ajuda. Serve para todo agente que roda em loop e precisa acabar sempre num estado conhecido.
O juiz de uma partida: o jogo acaba no apito, não quando um dos times cansa.
Larissa colocou em produção um agente de pesquisa. Ele recebe uma pergunta de mercado, busca na web, lê fontes e escreve um relatório. Nas primeiras semanas deu certo. Depois começaram três tipos de reclamação.
Algumas tarefas nunca terminavam: o agente seguia buscando “mais uma fonte” até o orçamento acabar. Outras terminavam cedo demais: depois de duas buscas, o agente escrevia um relatório confiante com informação rasa. E algumas terminavam com “não consegui concluir”, sem dizer o que tinha encontrado e por que tinha parado.
Os três problemas têm a mesma raiz. “Terminar” estava definido em uma frase do prompt: “quando tiver informação suficiente, escreva o relatório”. O modelo decidia sozinho o que era suficiente, e não havia nenhum outro jeito de acabar.
A pergunta que este padrão responde: como garantir que todo loop de agente termine, e termine no estado certo?
Trate o fim como parte do design, com quatro saídas explícitas, cada uma checada por código sempre que possível:
O modelo pode sinalizar sucesso, desistência ou pedido de ajuda com uma ferramenta própria ou com saída estruturada. O código confirma o sucesso quando dá, e o limite é sempre do código.
Não é uma ideia exótica. No texto “Building effective agents” (dezembro de 2024), a Anthropic recomenda que agentes tenham condições de parada, como um número máximo de iterações, para manter o controle. Este padrão leva a recomendação adiante: além do teto, cada jeito de terminar ganha um nome e um tratamento.
Volte ao agente de Larissa. O critério de sucesso virou regra em código: o relatório precisa citar pelo menos três fontes independentes, e todo link precisa abrir. O orçamento é de 25 passos. Um trace com as quatro saídas funcionando:
step 6 finish(kind="done", note="Relatório pronto")
verify: 2 fontes válidas de 3 exigidas -> rejeitado
tool_result: "Critério não cumprido: falta 1 fonte independente."
step 7 web_search("market share pagamentos instantâneos 2026")
...
step 14 finish(kind="done", note="Relatório pronto, 4 fontes")
verify: 4 fontes válidas, todos os links abrem -> aceito
stop.reason = success
O primeiro “concluí” do modelo era o mesmo relatório raso das reclamações. A verificação devolveu o motivo, e o modelo continuou. Sem ela, a tarefa teria parado no passo 6 com uma fonte a menos.
Agora uma pergunta sem resposta pública, como a margem de lucro de uma empresa de capital fechado. Duas saídas são boas: no passo 9 o agente chama finish(kind="give_up", note="Nenhuma fonte pública traz a margem; achei só estimativas de terceiros, listadas abaixo"). Ou, se continuar tentando, o código para no passo 25 e entrega o resumo do que achou, com stop.reason = limit. A saída ruim, que o padrão impede, é um relatório confiante com um número inventado.
flowchart TD
L(["1. Volta do loop"]):::llm --> B{"2. Orçamento ok?"}
B -->|não| LIM(["3. Parada por limite"]):::io
B -->|sim| S{"4. Sinal do modelo"}
S -->|continuar| L
S -->|concluí| V["5. Verificar critério"]:::tool
V -->|passou| OK(["6. Sucesso"]):::io
V -->|falhou| L
S -->|não consigo| G(["7. Desistência explicada"]):::io
S -->|preciso de ajuda| H((("8. Humano"))):::human
schema Signal { kind: "continue" | "done" | "give_up" | "need_help", note: string }
function run(task):
while true:
if not budget.check(steps=25, seconds=600, dollars=2.00): # 2
return finish("limit", summarize(state)) # 3
reply = llm_with_tools(SYSTEM, TOOLS + [signal_tool], state) # 1 e 4
signal = reply.signal or Signal("continue")
trace.span("signal", signal)
if signal.kind == "done":
if verify(task.success_criteria, state): # 5
return finish("success", reply.text) # 6
state.add("O critério ainda não foi cumprido: " + failed_checks())
elif signal.kind == "give_up":
return finish("gave_up", signal.note) # 7
elif signal.kind == "need_help":
answer = human.ask(signal.note) # 8
state.add(answer)
else:
state.add(execute(reply.tool_calls))
max_tokens e trate o motivo de parada da API.finish (ou saída estruturada) com os quatro tipos de sinal.Não chute o teto. Olhe os traces das execuções que terminaram com sucesso e veja quantos passos, segundos e reais elas usaram. O teto fica acima da grande maioria delas, com folga, para que o limite pegue execuções presas e não tarefas difíceis.
Suponha que 95% das execuções bem-sucedidas usem até 14 passos e a mais longa use 19. Um teto de 25 deixa espaço para o caso difícil e corta o loop preso cedo. Um teto de 200 “para garantir” deixa uma execução presa gastar dez vezes mais antes de parar.
Revise os limites quando o tipo de tarefa mudar. Um teto certo para “corrigir um teste” é curto demais para “migrar um módulo”.
Cada saída precisa de uma resposta pensada para quem recebe. Escreva as quatro antes de ir para produção:
| Saída | O que o usuário recebe | O que o sistema faz |
|---|---|---|
| Sucesso | O resultado e a evidência da verificação (“4 fontes, todos os links conferidos”) | Registra stop.reason=success |
| Limite | O que foi feito até ali, o que faltou e onde parou | Guarda o estado para retomar ou revisar |
| Desistência | O motivo concreto e o que foi tentado | Manda para a fila de revisão |
| Pedido de ajuda | Uma pergunta objetiva, com as opções quando houver | Pausa a execução com prazo de espera |
Pense no agente de navegador que baixa boletos. “Não consegui” não ajuda ninguém. “O portal pediu um captcha na tela de login às 3h12; baixei 14 de 20 boletos, os 6 restantes estão listados abaixo” deixa a pessoa terminar o trabalho em dois minutos.
gave_up disparar. Diga quando desistir: falta de acesso, dado inexistente, permissão negada.| Aspecto | O que acontece | Como aparece no trace |
|---|---|---|
| Chamadas | Nenhuma extra no caminho normal; uma no fechamento por limite | span wrap_up só quando há limite |
| Latência | Verificações de sucesso somam o tempo delas | span verify após cada done |
| Falha: sucesso falso | O modelo declara “concluí” e a verificação é fraca | tarefa com stop=success falhando no eval |
| Falha: desistência demais | Prompt ou ferramentas tornam desistir fácil demais | taxa de gave_up subindo |
| Falha: ajuda demais | O agente pergunta o que poderia descobrir sozinho | taxa de need_help acima da meta |
{ "task": "Baixe a fatura de setembro no portal X", "fixture": "portal_offline", "expected_stop": "gave_up" }
Monte pelo menos um caso por saída. Uma tarefa fácil espera success. Uma tarefa impossível de propósito (portal fora do ar, arquivo que não existe) espera gave_up. Uma tarefa que depende de uma decisão de negócio espera need_help. E uma fixture que faz a ferramenta responder “ok” sem progresso espera limit. Se o agente responde a tarefa impossível com success, você achou o caso mais perigoso: sucesso falso.
stop.reason em todo trace, com painel por tipo de tarefa.| Prós | Contras |
|---|---|
| Toda execução termina num estado conhecido | Exige critério de sucesso escrito para cada tipo de tarefa |
| Sucesso deixa de ser opinião do modelo | Verificações custam tempo |
| Desistências viram dado útil | Mais caminhos de fim para testar |
| O usuário sabe o que recebeu e por quê | Pedido de ajuda exige alguém para responder |
As abas usam uma ferramenta finish com os sinais e um critério verificado por código. Python e TypeScript leem o sinal dentro do while. LangGraph lê o mesmo sinal numa aresta: limite, sucesso, desistência ou humano são destinos do grafo, não frases no prompt.
Python. Use quando você quer as quatro saídas explícitas num loop escrito à mão.
import subprocess
import time
import anthropic
client = anthropic.Anthropic()
FINISH = {
"name": "finish",
"description": "Chame quando terminar, desistir ou precisar de ajuda. Nunca responda sem chamar esta ferramenta.",
"strict": True,
"input_schema": {
"type": "object",
"properties": {
"kind": {"type": "string", "enum": ["done", "give_up", "need_help"]},
"note": {"type": "string", "description": "Resultado, motivo da desistência ou pergunta para a pessoa"},
},
"required": ["kind", "note"],
"additionalProperties": False,
},
}
def tests_pass() -> bool: # 5. critério verificável
return subprocess.run(["pytest", "-q"], capture_output=True).returncode == 0
def run(task: str, tools: list, execute, ask_human, max_steps=25, max_seconds=600) -> dict:
messages, start = [{"role": "user", "content": task}], time.monotonic()
for _ in range(max_steps):
if time.monotonic() - start > max_seconds:
break
reply = client.messages.create(
model="claude-opus-5-5", max_tokens=8000, tools=tools + [FINISH], messages=messages,
)
messages.append({"role": "assistant", "content": reply.content})
results = []
for block in (b for b in reply.content if b.type == "tool_use"):
if block.name != "finish":
results.append({"type": "tool_result", "tool_use_id": block.id, "content": execute(block)})
continue
kind, note = block.input["kind"], block.input["note"]
if kind == "done" and tests_pass():
return {"stop": "success", "result": note} # 6
if kind == "give_up":
return {"stop": "gave_up", "result": note} # 7
content = ask_human(note) if kind == "need_help" else "Os testes ainda falham. Continue."
results.append({"type": "tool_result", "tool_use_id": block.id, "content": content})
messages.append({"role": "user", "content": results or "Use uma ferramenta ou chame finish."})
return {"stop": "limit", "result": "Parei no limite. Último estado no trace."} # 3O que você ganhou: cada saída é um caminho de código testável. O que você perdeu: mais código que um while simples, e o fechamento por limite ainda é uma mensagem fixa.
TypeScript. Use quando o agente roda em Node e você quer a ferramenta finish tipada.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const FINISH: Anthropic.Tool = {
name: "finish",
description: "Chame quando terminar, desistir ou precisar de ajuda. Nunca responda sem chamar esta ferramenta.",
strict: true,
input_schema: {
type: "object",
properties: {
kind: { type: "string", enum: ["done", "give_up", "need_help"] },
note: { type: "string" },
},
required: ["kind", "note"],
additionalProperties: false,
},
};
type Stop = { stop: "success" | "gave_up" | "limit"; result: string };
export async function run(
task: string,
tools: Anthropic.Tool[],
execute: (b: Anthropic.ToolUseBlock) => Promise<string>,
verify: () => Promise<boolean>,
askHuman: (q: string) => Promise<string>,
maxSteps = 25,
): Promise<Stop> {
const messages: Anthropic.MessageParam[] = [{ role: "user", content: task }];
for (let step = 0; step < maxSteps; step++) {
const reply = await client.messages.create({
model: "claude-opus-5-5", max_tokens: 8000, tools: [...tools, FINISH], messages,
});
messages.push({ role: "assistant", content: reply.content });
const results: Anthropic.ToolResultBlockParam[] = [];
for (const b of reply.content) {
if (b.type !== "tool_use") continue;
if (b.name !== "finish") {
results.push({ type: "tool_result", tool_use_id: b.id, content: await execute(b) });
continue;
}
const { kind, note } = b.input as { kind: string; note: string };
if (kind === "done" && (await verify())) return { stop: "success", result: note };
if (kind === "give_up") return { stop: "gave_up", result: note };
const content = kind === "need_help" ? await askHuman(note) : "O critério ainda falha. Continue.";
results.push({ type: "tool_result", tool_use_id: b.id, content });
}
messages.push({ role: "user", content: results.length ? results : "Use uma ferramenta ou chame finish." });
}
return { stop: "limit", result: "Parei no limite. Último estado no trace." };
}O que você ganhou: o tipo Stop obriga quem chama a tratar cada saída. O que você perdeu: o mesmo que em Python, mais a verificação injetada por parâmetro.
Python + LangGraph. Use quando cada saída do loop é um destino do grafo.
from typing import Literal
from langgraph.graph import END, START, StateGraph
from typing_extensions import TypedDict
class State(TypedDict):
steps: int
max_steps: int
signal: str # continue | done | blocked | help
verified: bool
output: str
def turn(state: State) -> dict:
# 1. uma volta; o modelo devolve o sinal estruturado
signal = ask_model(state) # sua chamada, saída em schema
return {"signal": signal, "steps": state["steps"] + 1}
def route(state: State) -> Literal["turn", "limit", "success", "give_up", "human"]:
if state["steps"] >= state["max_steps"]:
return "limit" # 2 e 3
if state["signal"] == "continue":
return "turn"
if state["signal"] == "done":
return "success" if state["verified"] else "turn" # 5
if state["signal"] == "blocked":
return "give_up" # 7
return "human" # 8
graph = StateGraph(State)
graph.add_node("turn", turn)
graph.add_node("limit", lambda s: {"output": "parei no teto"})
graph.add_node("success", lambda s: {"output": "ok"})
graph.add_node("give_up", lambda s: {"output": "não consegui"})
graph.add_node("human", lambda s: {"output": "aguardando pessoa"})
graph.add_edge(START, "turn")
graph.add_conditional_edges("turn", route, ["turn", "limit", "success", "give_up", "human"])
for name in ("limit", "success", "give_up", "human"):
graph.add_edge(name, END)
app = graph.compile()recursion_limit do LangGraph corta o grafo inteiro. Ele não substitui estas arestas. O loop em outras stacks está em Loop ReAct.
O que você ganhou: cada parada é um nó com nome no trace. O que você perdeu: a verificação do critério (verified) continua uma função sua, chamada antes de aceitar done.
| Stack | Onde mora a parada |
|---|---|
| Python, TypeScript | O if depois de finish |
| Python + LangGraph | A aresta com cinco destinos |
| Python + LangChain, OpenAI Agents, Pydantic AI, AI SDK | Só o teto de passos. As outras paradas você escreve |
Responda as 6 perguntas para concluir a aula. Só assistir não conclui: a checagem é o que conta para a trilha e para o certificado.
Condições de Parada (Stop Conditions) é um padrão de projeto de IA para apps com LLM e agentes, do grupo Comportamento de agentes. Definir com clareza quando o agente terminou, desistiu ou precisa de ajuda.
Nos cenários reais Agente de código, Agente de navegador para tarefas repetitivas e Pesquisa profunda com citações.