O blog da AWS

Padrões assíncronos para chamar agentes do Amazon Bedrock AgentCore em pipelines Serverless

Por Daniel Abib e Alexandre Farber, Amazon Web Services.

Padrões de invocação assíncrona para agentes do Amazon Bedrock AgentCore em pipelines Serverless eliminam custos de computação ociosa enquanto seu agente de IA processa solicitações. Um exemplo comum é a validação de documentos: em um back office de financiamento imobiliário, um agente pode ler um registro de propriedade ou contrato de empréstimo, raciocinar sobre se as informações estão completas e consistentes, e retornar um veredicto sobre o qual as etapas subsequentes atuam. O Amazon Bedrock AgentCore fornece uma plataforma para construir, conectar e otimizar agentes em escala, com qualquer framework ou modelo.

Esses agentes introduzem uma característica que as etapas tradicionais de pipeline não possuem: eles pensam por um tempo antes de responder. Quanto tempo depende do prompt, do modelo e do documento, mas raramente é instantâneo, e essa latência muda a forma como você deve chamá-lo. A implementação inicial mais comum é um serviço de computação, como uma função do AWS Lambda, que invoca o agente e aguarda a resposta. Enquanto essa função aguarda, ela não faz nada, mas ainda está em execução, e você é cobrado por cada segundo dela.

É útil ver onde o custo realmente recai, porque os dois lados da chamada são cobrados de forma diferente. O runtime do Amazon Bedrock AgentCore, uma capacidade do Amazon Bedrock AgentCore, tem um modelo baseado em consumo que não cobra por CPU enquanto o agente está ocioso. Por exemplo, enquanto aguarda um modelo de linguagem grande gerar uma resposta, ou uma ferramenta ou chamada Model Context Protocol (MCP) retornar, você é cobrado pela memória durante esse tempo, mas não pela CPU. O serviço de computação que chamou o agente não tem esse comportamento. Uma função Lambda, container ou instância do Amazon Elastic Compute Cloud (Amazon EC2) que emite uma chamada síncrona fica bloqueada. Ela mantém (e paga por) sua alocação completa de computação até que o agente responda. Portanto, o desperdício não está do lado do agente. É o chamador, ocioso em uma conexão aberta.

Isso faz com que o custo do chamador acompanhe o tempo de execução do agente. Uma função que bloqueia no agente é cobrada por essencialmente todo o tempo de processamento, enquanto uma função que inicia o agente e retorna é cobrada apenas pelo breve despacho. A solução é liberar a computação do chamador durante a espera e retomar o pipeline somente quando o agente tiver um resultado. Nesta publicação, mostramos três padrões que fazem isso (callback de token de tarefa, integração direta de serviço e função durável) e os contrastamos com o antipadrão de bloqueio.

Um pipeline de exemplo

Para comparar os padrões em pé de igualdade, executamos cada um através do mesmo pipeline e alteramos apenas a etapa que chama o agente. O pipeline é um cenário deliberadamente simples e fictício (validação de documentos para financiamento imobiliário) escolhido para manter a orquestração clara. Não é o ponto da publicação. Ele representa qualquer fluxo de trabalho que chama um agente (ou outro serviço lento) e então age sobre o resultado, então imagine seu próprio caso de uso em seu lugar.

O pipeline tem cinco estágios:

  1. Extrair: Uma função AWS Lambda realiza reconhecimento óptico de caracteres (OCR) e extração de texto do documento. (A extração é simulada, então o cenário é executado sem documentos reais.)
  2. Identificar: Uma função Lambda classifica o documento e define flags de roteamento (shouldOrganize, shouldValidate).
  3. Rotear: Um estado Choice direciona o fluxo com base nessas flags.
  4. Organizar e Validar: Um estado Parallel organiza o documento enquanto, em um ramo separado, o agente do Amazon Bedrock AgentCore o valida. Este ramo Validate é a única parte que muda entre os padrões.
  5. Resultado: Uma função Lambda processa o veredicto do agente e decide a próxima ação (aprovar ou retornar para correção).

O diagrama a seguir mostra o pipeline. Ele permanece o mesmo em todos os casos. Apenas o ramo Validate é trocado para demonstrar cada padrão de invocação.

Figura 1: O pipeline de exemplo. O fluxo horizontal executa Start, Extract, Identify, Route (Choice), um estágio paralelo Organize-and-Validate, Result e End. O ramo Validate é destacado como intercambiável, e uma legenda lista as quatro maneiras como é implementado: o antipadrão de bloqueio, Padrão 1 (token de tarefa), Padrão 2 (integração direta) e Padrão 3 (função durável).

Figura 1: O pipeline de exemplo. Apenas o ramo Validate destacado muda entre os padrões

Um único agente do Amazon Bedrock AgentCore atende todos os quatro casos. O agente inspeciona cada invocação e escolhe como responder: se receber um token de tarefa do AWS Step Functions, ele acorda essa execução quando terminar. Se receber um ID de callback de função durável, ele acorda a função durável. Se não receber nenhum dos dois, ele retorna o veredicto diretamente na resposta. Ou seja, você pode alterar o padrão de orquestração sem alterar ou reimplantar o agente.

Como o agente retorna o controle sem bloquear o chamador

O mecanismo é uma ação de retorno de controle no grupo de ações do agente. Quando o agente termina de raciocinar, ele chama um Lambda que envia o resultado e o token de tarefa de volta ao Step Functions. (O Padrão 2, descrito posteriormente, elimina esse Lambda completamente fazendo com que o Step Functions se integre diretamente com o AgentCore.)

O código a seguir mostra o núcleo desse Lambda:

# The tool the agent calls once it reaches a verdict
@tool
def conclude_validation(approved: bool, issues: list, summary: str) -> str:
    verdict = {"approved": approved, "issues": issues,
               "summary": summary, "source": "agentcore"}

    # A Step Functions task token was passed: resume that execution
    if task_token:
        sfn.send_task_success(taskToken=task_token, output=json.dumps(verdict))
        return "Step Functions resumed."

    # A durable-function callback ID was passed: resume the durable function
    if callback_id:
        lambda_client.send_durable_execution_callback_success(
            CallbackId=callback_id, Result=json.dumps(verdict).encode("utf-8"))
        return "Durable function resumed."

    # Neither was passed: this is a synchronous call, return the verdict inline
    return "Verdict recorded."

O ponto de entrada decide se deve executar em segundo plano ou de forma síncrona com base nos mesmos sinais:

@app.async_task
async def validate_document_async(prompt, document, extracted_text):
    # Background work; conclude_validation fires the right callback when done
    agent = build_agent()
    await agent.invoke_async(message(prompt, document, extracted_text))

@app.entrypoint
async def handler(event):
    task_token  = event.get("taskToken")    # passed by the task-token pattern
    callback_id = event.get("callbackId")   # passed by the durable-function pattern

    # Asynchronous: start the work and return "accepted" right away
    if task_token or callback_id:
        asyncio.create_task(validate_document_async(...))
        return {"status": "accepted"}

    # Synchronous: run now and return the verdict in the response
    agent = build_agent()
    await agent.invoke_async(message(...))
    return verdict

Com o agente no lugar, o restante da publicação se concentra nas quatro maneiras de chamá-lo.

Chamando o agente: Quatro abordagens

Começamos com o antipadrão de bloqueio para estabelecer o custo base, depois mostramos os três padrões que o evitam. O código e as definições de infraestrutura ao longo são trechos da amostra, incluídos para ilustrar cada padrão.

O antipadrão de bloqueio

A implementação mais direta chama o agente e aguarda a resposta na mesma função Lambda. Funciona, e é simples de implementar, razão pela qual é tão comum, mas a função permanece ativa durante todo o tempo em que o agente está pensando.

// The Lambda function blocks here until the agent responds
const response = await agentcore.send(
  new InvokeAgentRuntimeCommand({
    agentRuntimeArn: AGENT_RUNTIME_ARN,
    payload: new TextEncoder().encode(JSON.stringify(payload)),
    runtimeSessionId: sessionId,
  })
);
// The function stays alive and billed for the entire time the agent is thinking.

A duração cobrada da função acaba sendo aproximadamente igual ao tempo de processamento do agente. Os próximos três padrões eliminam esse custo ocioso, cada um fazendo uma troca diferente. Em particular, o Padrão 2 usa a integração otimizada do Step Functions para AgentCore Harness (InvokeHarness), removendo o Lambda completamente.

Padrão 1: Callback de token de tarefa com uma função despachadora

Este padrão mantém uma função Lambda no caminho para lógica personalizada, mas remove o custo ocioso. O Step Functions invoca a função com a integração waitForTaskToken, que passa um token de tarefa e então pausa a execução. A função usa o token para iniciar o agente, depois retorna em alguns segundos. A execução permanece pausada, não cobrando nada por computação, até que o agente chame SendTaskSuccess com esse token para retomá-la.

// Start the agent, pass the task token, and return without waiting
const response = await agentcore.send(
  new InvokeAgentRuntimeCommand({
    agentRuntimeArn: AGENT_RUNTIME_ARN,
    payload: new TextEncoder().encode(JSON.stringify({ ...payload, taskToken })),
    runtimeSessionId: sessionId,
  })
);
// Returning here does not complete the step. Step Functions stays paused until
// the agent calls SendTaskSuccess with this task token.
return { dispatched: true };

O estado correspondente passa o token do contexto e define um timeout e heartbeat como rede de segurança, para que um agente silencioso falhe a execução de forma limpa em vez de deixá-la pausada indefinidamente:

"ValidateDispatch": {
  "Type": "Task",
  "Resource": "arn:aws:states:::lambda:invoke.waitForTaskToken",
  "Parameters": {
    "FunctionName": "${ValidateDispatcherFunctionArn}",
    "Payload": {
      "taskToken.$": "$$.Task.Token",
      "document.$": "$.document",
      "extractedText.$": "$.extract.extractedText",
      "executionId.$": "$$.Execution.Id"
    }
  },
  "TimeoutSeconds": 120,
  "HeartbeatSeconds": 60,
  "Next": "AgentCoreValidation"
}

Custo. Uma função Lambda é executada, mas apenas o tempo suficiente para iniciar o agente e retornar: alguns segundos, independentemente de quanto tempo o agente leva depois. Você paga por esse breve despacho, não pela espera, porque a função já foi encerrada enquanto o agente trabalha. A espera é mantida pela execução pausada do Step Functions, que não cobra por computação ociosa. Esta é a diferença chave da versão de bloqueio, onde o tempo cobrado da função acompanha o tempo de processamento do agente.

Padrão 2: Integração direta de serviço

Quando você não precisa de código personalizado em torno da chamada do agente, pode remover a função despachadora e tirar o Lambda do caminho completamente. O Step Functions pode chamar o Amazon Bedrock AgentCore diretamente através de sua integração de serviço AWS SDK, então a resposta do agente flui diretamente para o próximo estado. O ramo Validate então se torna um único estado Task:

"ValidateDirect": {
  "Type": "Task",
  "Resource": "arn:aws:states:::aws-sdk:bedrockagentcore:invokeAgentRuntime",
  "Parameters": {
    "AgentRuntimeArn": "${AgentRuntimeArn}",
    "RuntimeSessionId.$": "States.Hash($$.Execution.Id, 'SHA-256')",
    "Payload.$": "States.JsonToString($.prep.agentInput)"
  },
  "ResultSelector": { "raw.$": "$.Response" },
  "TimeoutSeconds": 120,
  "Next": "ParseVerdict"
}

Custo. Não há função Lambda no caminho, então não há computação Lambda ociosa para pagar. O Step Functions mantém a espera, e um fluxo de trabalho Standard cobra por transição de estado em vez de pela duração da espera, então o custo significativo durante o processamento é o próprio agente.

Padrão 3: Função durável Lambda

Se você prefere expressar a orquestração como código em um único lugar em vez de uma máquina de estados, uma função durável Lambda oferece o mesmo comportamento de custo. Com o SDK @aws/durable-execution-sdk-js, os estágios do pipeline se tornam chamadas context.step, o trabalho paralelo se torna context.parallel, e a espera pelo agente se torna context.waitForCallback. Durante essa espera, a função é suspensa e não é cobrada por computação. O agente a retoma com SendDurableExecutionCallbackSuccess.

// Suspend the function until the agent calls back
const result = await ctx.waitForCallback(
  "validate-agentcore",
  async (callbackId) =>
    dispatchAgentCore(callbackId, document, extractedText, executionId),
  { timeout: { seconds: 120 } }
);

Custo. Uma única função mantém todo o pipeline, mas não cobra por computação enquanto está suspensa aguardando o agente. Você paga pelas breves rajadas de execução entre suspensões, a mesma economia do padrão de token de tarefa, em vez de pela espera.

Medindo a diferença

O ponto não é qualquer número específico. O tempo de execução do agente varia com o prompt, o modelo e o documento. O que importa é a relação entre dois valores no padrão de token de tarefa: quanto tempo o estado Validate esteve ativo, versus quanto tempo a função despachadora foi realmente cobrada. Uma única execução de nossos testes torna a relação visível:

Step Functions, ValidateDispatch state
   Returned (TaskSubmitted): 14:08:19   <- the function returned and shut down
   Resumed  (TaskSucceeded): 14:08:34   <- the agent woke the execution
   State active ........ 19.6s

Lambda, dispatcher function (CloudWatch REPORT)
   Billed duration ..... 4.8s

Result: the state was active for 19.6s, but the function was billed for 4.8s.
The ~14.8s in between is wait time with no Lambda function running.

Você pode ver a mesma coisa no histórico de eventos do Step Functions: com o padrão de token de tarefa, um evento TaskSubmitted (a função retornou) é separado de TaskSucceeded (o agente retomou o fluxo) pelo tempo de processamento do agente. Uma invocação síncrona não tem tal lacuna.

A tabela a seguir resume como o ramo Validate (destacado no diagrama anterior) é implementado em cada padrão:

 

Bloqueio (antipadrão) Padrão 1: Token de tarefa Padrão 2: Integração direta Padrão 3: Função durável
Orquestrador Step Functions Step Functions Step Functions Lambda (código)
Função Lambda no caminho sim, ativa e cobrada sim, mas retorna cedo nenhuma a função durável (suspende)
Computação Lambda ociosa durante a espera paga toda a espera nenhuma nenhuma nenhuma
Código personalizado em torno da chamada do agente sim sim limitado (transformações de estado) sim
Desacopla o chamador do agente não sim não sim
Complexidade relativa mais baixa mais alta (token e IAM de callback) mais baixa média (checkpoint/replay)

Uma ressalva sobre a leitura disso: a duração total do pipeline não é uma métrica de comparação útil, porque é dominada pelo próprio tempo de raciocínio do agente, que varia de execução para execução e é essencialmente o mesmo em todos os cenários. A diferença significativa é quanto de computação você paga durante essa espera, capturada pela tabela e pela leitura de duração cobrada. O tempo cobrado do despachador permanece estável enquanto o tempo de execução do agente cresce. Os números anteriores vêm de uma única execução. Trate-os como uma ilustração da relação, não como um benchmark, e meça sua própria carga de trabalho.

Escolhendo um padrão

A tabela a seguir resume as trocas para ajudá-lo a escolher um padrão:

 

Bloqueio (antipadrão) Padrão 1: Token de tarefa Padrão 2: Integração direta Padrão 3: Função durável
Custo do chamador Tempo completo de processamento do agente Segundos (apenas despacho) Zero (sem Lambda) Segundos (apenas despacho)
Esforço de integração Baixo Médio-alto (IAM, heartbeat, timeout) Baixo (único estado Task) Médio (modelo checkpoint-and-replay)
Localização da lógica de negócio No Lambda (antes + depois) No Lambda (antes + depois) No Amazon States Language (ASL) apenas (funções intrínsecas) No Lambda (código sequencial)
Melhor para Protótipos, agentes curtos Lógica personalizada de pré/pós-processamento Orquestração pura, sem código personalizado Fluxos de trabalho assíncronos complexos em uma função

Melhores práticas

Proteja-se contra um agente que nunca responde. Defina TimeoutSeconds em cada estado waitForTaskToken para que a execução falhe com States.Timeout em vez de travar indefinidamente. Se seu agente envia heartbeats, também defina HeartbeatSeconds para detecção mais rápida de agente morto. Capture o erro e roteie para um caminho de falha ou revisão humana.

Use um ID de sessão estável entre tentativas. Defina sessionId para um valor derivado do contexto de execução (como o nome de execução do Step Functions) para que as tentativas retomem a mesma sessão do agente em vez de começar do zero. No padrão de token de tarefa, defina-o no despachador. No padrão de integração direta, defina-o nos parâmetros do estado Task.

Ative o AWS X-Ray. Ative Tracing: Active em suas configurações do Step Functions e Lambda. O X-Ray mostra exatamente quanto tempo o agente passou pensando versus quanto tempo o chamador passou esperando, confirmando que seu padrão realmente liberou computação durante a lacuna.

Dimensione a função despachadora para velocidade, não para a carga de trabalho do agente. O despachador apenas serializa uma solicitação e invoca um endpoint. 256 MB de memória e um timeout de 30 segundos geralmente são suficientes. O trabalho pesado acontece do lado do agente.

Custos

Esses padrões geram custos por computação Lambda, transições de estado do Step Functions, armazenamento de execução de função durável e, comum a todas as abordagens, runtime do Amazon Bedrock AgentCore e inferência de modelo do Amazon Bedrock. O custo do agente e do modelo é o mesmo em todos os quatro casos. A arquitetura altera apenas a sobrecarga de orquestração e, no antipadrão de bloqueio, a computação Lambda ociosa desperdiçada. Para preços atuais, consulte as páginas de preços de cada serviço.

Conclusão

Colocar um agente de IA em um pipeline é a parte simples. Chamá-lo economicamente é o que separa um protótipo de um design de produção. Uma função Lambda que bloqueia no agente é simples de implementar, mas silenciosamente cara, gastando a maior parte de seu tempo cobrado esperando. Ao longo desta publicação, apresentamos três maneiras de evitar isso: um callback de token de tarefa quando você precisa de uma função Lambda no caminho, uma integração direta de serviço para o caso mais simples, e uma função durável quando você prefere orquestração como código. Todos os três são acionados pelo mesmo agente do Amazon Bedrock AgentCore, que adapta sua resposta à forma como é chamado.

A amostra completa, incluindo o agente completo, as definições de máquina de estados e a função durável, está disponível no repositório sample-bedrock-agentcore-async-stepfunctions no GitHub. Para saber mais, revise a documentação do Amazon Bedrock AgentCore sobre processamento de tarefas assíncronas.

Para implantações de produção, use o Amazon Bedrock Guardrails para aplicar controles de IA responsável em entradas e saídas do agente.


Este conteúdo foi traduzido da publicação original do blog, que pode ser encontrado aqui.

Autores

Daniel Abib é Arquiteto de Soluções Especialista Sênior na Amazon Web Services.
Alexandre Farber é Gerente Sênior na Amazon Web Services.

Tradutores

Daniel Abib é Arquiteto de Soluções Sênior e Especialista em Amazon Bedrock na AWS, com mais de 25 anos trabalhando com gerenciamento de projetos, arquiteturas de soluções escaláveis, desenvolvimento de sistemas e CI/CD, microsserviços, arquitetura Serverless & Containers e especialização em Machine Learning. Ele trabalha apoiando Startups, ajudando-os em sua jornada para a nuvem.
https://www.linkedin.com/in/danielabib/
Nicolas Tarzia é Senior Technical Account Manager na AWS, com mais de 13 anos de experiência, com ampla experiência em arquitetura cloud, engenharia e design de software. Atualmente está habilitando empresas do ramo de ISV (Independent Software Vendors) simplificando a operação na nuvem e otimizando os custos em cloud. Sua área de interesse são tecnologias serverless.
https://www.linkedin.com/in/nicolastarzia