ToolSimulator: testes escaláveis de ferramentas para agentes de IA

O problema de testar agentes de IA que chamam ferramentas externas

Agentes de IA modernos não se limitam a raciocinar: eles chamam APIs, consultam bancos de dados, acionam serviços MCP (Protocolo de Contexto de Modelo) e interagem com sistemas externos para concluir tarefas. O comportamento do agente depende não só do seu raciocínio, mas também do que essas ferramentas retornam — e é exatamente aí que o teste se complica.

Testar contra APIs reais cria três obstáculos sérios. Primeiro, dependências externas atrasam tudo: limites de taxa, instabilidades e necessidade de conectividade tornam impraticável rodar centenas de casos de teste. Segundo, chamadas reais geram efeitos colaterais reais — enviar e-mails de verdade, alterar bancos de produção ou confirmar reservas indesejadas. Terceiro, muitas ferramentas lidam com dados sensíveis, como registros de usuários e informações financeiras, criando riscos de conformidade desnecessários.

A alternativa clássica são os mocks estáticos, mas eles também têm um limite importante: funcionam bem para cenários simples e previsíveis, porém quebram em fluxos multi-etapas com estado. Imagine um agente de reserva de voos que primeiro busca opções e depois verifica o status de uma reserva — a segunda resposta precisa depender do que aconteceu na primeira chamada. Um mock com resposta fixa não consegue capturar essa dinâmica.

O que é o ToolSimulator

A AWS apresentou o ToolSimulator como parte do Kit de Desenvolvimento de Software (SDK) do Strands Evals. Trata-se de um framework de simulação de ferramentas baseado em Modelo de Linguagem de Grande Escala (LLM) que intercepta chamadas às ferramentas registradas e as redireciona para um gerador de respostas inteligente — sem nunca acionar a implementação real.

O gerador usa o esquema da ferramenta, a entrada do agente e o estado atual da simulação para produzir uma resposta realista e contextualmente adequada. Não são necessários fixtures escritos à mão.

Três capacidades centrais sustentam essa proposta:

  • Geração adaptativa de respostas: as saídas refletem o que o agente realmente solicitou, não um template fixo. Uma busca por voos de Seattle para Nova York retorna opções plausíveis com preços e horários realistas.
  • Suporte a fluxos com estado: o ToolSimulator mantém um estado compartilhado consistente entre chamadas, permitindo testar interações de banco de dados, fluxos de reserva e processos multi-etapas sem tocar em sistemas de produção.
  • Validação de esquema: respostas são validadas contra modelos Pydantic definidos pelo desenvolvedor, capturando respostas malformadas antes que cheguem ao agente e quebrem a camada de pós-processamento.

Como o ToolSimulator funciona

O fluxo de trabalho segue três etapas: decorar e registrar as ferramentas, opcionalmente configurar o contexto da simulação e, por fim, deixar o ToolSimulator interceptar as chamadas durante a execução do agente.

Imagem original — fonte: Aws

Etapa 1: Decorar e registrar

Cria-se uma instância do ToolSimulator e envolve-se a função da ferramenta com o decorator @simulator.tool(). O corpo real da função pode ficar vazio — o ToolSimulator intercepta as chamadas antes de chegarem à implementação:

from strands_evals.simulation.tool_simulator import ToolSimulator

tool_simulator = ToolSimulator()

@tool_simulator.tool()
def search_flights(origin: str, destination: str, date: str) -> dict:
    """Search for available flights between two airports on a given date."""
    pass  # The real implementation is never called during simulation

Etapa 2: Configurar o contexto (opcional)

Por padrão, o ToolSimulator infere o comportamento de cada ferramenta a partir do seu esquema e docstring — nenhuma configuração adicional é necessária para começar. Quando se quer mais controle, três parâmetros opcionais estão disponíveis:

  • share_state_id: vincula ferramentas que compartilham o mesmo backend sob uma chave de estado comum. Alterações feitas por uma ferramenta ficam visíveis para chamadas subsequentes de outras ferramentas ligadas ao mesmo ID.
  • initial_state_description: inicializa a simulação com uma descrição em linguagem natural do estado pré-existente. Quanto mais contexto, mais realistas e consistentes serão as respostas geradas.
  • output_schema: um modelo Pydantic que define a estrutura esperada da resposta. O ToolSimulator gera respostas que obedecem estritamente a esse esquema.

Etapa 3: Simulação em execução

Quando o agente chama uma ferramenta registrada, o wrapper do ToolSimulator intercepta a chamada, valida os parâmetros do agente contra o esquema da ferramenta, produz uma resposta compatível com o output_schema e atualiza o registro de estado para que chamadas subsequentes vejam um ambiente consistente.

O exemplo abaixo mostra uma simulação completa de um assistente de busca de voos:

from strands import Agent
from strands_evals.simulation.tool_simulator import ToolSimulator

# 1. Create a simulator instance
tool_simulator = ToolSimulator()

# 2. Register a tool for simulation with initial state context
@tool_simulator.tool(
    initial_state_description="Flight database: SEA->JFK flights available at 8am, 12pm, and 6pm. Prices range from $180 to $420.",
)
def search_flights(origin: str, destination: str, date: str) -> dict:
    """Search for available flights between two airports on a given date."""
    pass

# 3. Create an agent with the simulated tool and run it
flight_tool = tool_simulator.get_tool("search_flights")
agent = Agent(
    system_prompt="You are a flight search assistant.",
    tools=[flight_tool],
)
response = agent("Find me flights from Seattle to New York on March 15.")
print(response)
# Expected output: A structured list of simulated SEA->JFK flights with times
# and prices consistent with the initial_state_description you provided.

Recursos avançados do ToolSimulator

Instâncias independentes para testes paralelos

É possível criar múltiplas instâncias do ToolSimulator lado a lado. Cada instância mantém seu próprio registro de ferramentas e estado, permitindo executar configurações de experimento em paralelo no mesmo código:

simulator_a = ToolSimulator()
simulator_b = ToolSimulator()

# Each instance has an independent tool registry and state --
# ideal for comparing agent behavior across different tool setups.

Estado compartilhado para fluxos multi-etapas

Para ferramentas com estado — como getters e setters de banco de dados — o ToolSimulator mantém consistência entre chamadas. Usa-se share_state_id para vincular ferramentas que operam no mesmo backend e initial_state_description para inicializar o contexto:

@tool_simulator.tool(
    share_state_id="flight_booking",
    initial_state_description="Flight booking system: SEA->JFK flights available at 8am, 12pm, and 6pm. No bookings currently active.",
)
def search_flights(origin: str, destination: str, date: str) -> dict:
    """Search for available flights between two airports on a given date."""
    pass

@tool_simulator.tool(
    share_state_id="flight_booking",
)
def get_booking_status(booking_id: str) -> dict:
    """Retrieve the current status of a flight booking by booking ID."""
    pass

# Both tools share "flight_booking" state.
# When search_flights is called, get_booking_status sees the same
# flight availability data in subsequent calls.

Também é possível inspecionar o estado antes e depois da execução do agente para validar que as interações com as ferramentas produziram as mudanças esperadas:

initial_state = tool_simulator.get_state("flight_booking")
# ... run the agent ...
final_state = tool_simulator.get_state("flight_booking")
# Verify not just the final output, but the full sequence of tool interactions.

Dica: como o initial_state_description aceita linguagem natural, é possível usar um DataFrame.describe() para gerar resumos estatísticos de dados tabulares e passá-los diretamente como descrição de estado — sem nunca acessar os dados reais.

Validação de esquema de resposta com Pydantic

Para ferramentas que seguem especificações rígidas — como OpenAPI ou MCP — define-se a resposta esperada como um modelo Pydantic e passa-se via output_schema:

from pydantic import BaseModel, Field

class FlightSearchResponse(BaseModel):
    flights: list[dict] = Field(
        ..., description="List of available flights with flight number, departure time, and price"
    )
    origin: str = Field(..., description="Origin airport code")
    destination: str = Field(..., description="Destination airport code")
    status: str = Field(default="success", description="Search operation status")
    message: str = Field(default="", description="Additional status message")

@tool_simulator.tool(output_schema=FlightSearchResponse)
def search_flights(origin: str, destination: str, date: str) -> dict:
    """Search for available flights between two airports on a given date."""
    pass

# ToolSimulator validates parameters strictly and returns only valid JSON
# responses that conform to the FlightSearchResponse schema.

Integração com pipelines de avaliação do Strands Evals

O ToolSimulator se encaixa naturalmente no framework de avaliação do Strands Evals. O exemplo abaixo mostra um pipeline completo — da configuração da simulação ao relatório do experimento — usando o GoalSuccessRateEvaluator para pontuar o desempenho do agente em tarefas de chamada de ferramentas:

from typing import Any
from pydantic import BaseModel, Field
from strands import Agent
from strands_evals import Case, Experiment
from strands_evals.evaluators import GoalSuccessRateEvaluator
from strands_evals.simulation.tool_simulator import ToolSimulator
from strands_evals.mappers import StrandsInMemorySessionMapper
from strands_evals.telemetry import StrandsEvalsTelemetry

# Set up telemetry and tool simulator
telemetry = StrandsEvalsTelemetry().setup_in_memory_exporter()
memory_exporter = telemetry.in_memory_exporter
tool_simulator = ToolSimulator()

# Define the response schema
class FlightSearchResponse(BaseModel):
    flights: list[dict] = Field(
        ..., description="Available flights with number, departure time, and price"
    )
    origin: str = Field(..., description="Origin airport code")
    destination: str = Field(..., description="Destination airport code")
    status: str = Field(default="success", description="Search operation status")
    message: str = Field(default="", description="Additional status message")

# Register tools for simulation
@tool_simulator.tool(
    share_state_id="flight_booking",
    initial_state_description="Flight booking system: SEA->JFK flights at 8am, 12pm, and 6pm. No bookings currently active.",
    output_schema=FlightSearchResponse,
)
def search_flights(origin: str, destination: str, date: str) -> dict[str, Any]:
    """Search for available flights between two airports on a given date."""
    pass

@tool_simulator.tool(share_state_id="flight_booking")
def get_booking_status(booking_id: str) -> dict[str, Any]:
    """Retrieve the current status of a flight booking by booking ID."""
    pass

# Define the evaluation task
def user_task_function(case: Case) -> dict:
    initial_state = tool_simulator.get_state("flight_booking")
    print(f"[State before]: {initial_state.get('initial_state')}")
    search_tool = tool_simulator.get_tool("search_flights")
    status_tool = tool_simulator.get_tool("get_booking_status")
    agent = Agent(
        trace_attributes={
            "gen_ai.conversation.id": case.session_id,
            "session.id": case.session_id
        },
        system_prompt="You are a flight booking assistant.",
        tools=[search_tool, status_tool],
        callback_handler=None,
    )
    agent_response = agent(case.input)
    print(f"[User]: {case.input}")
    print(f"[Agent]: {agent_response}")
    final_state = tool_simulator.get_state("flight_booking")
    print(f"[State after]: {final_state.get('previous_calls', [])}")
    finished_spans = memory_exporter.get_finished_spans()
    mapper = StrandsInMemorySessionMapper()
    session = mapper.map_to_session(finished_spans, session_id=case.session_id)
    return {"output": str(agent_response), "trajectory": session}

# Define test cases, run the experiment, and display the report
test_cases = [
    Case(
        name="flight_search",
        input="Find me flights from Seattle to New York on March 15.",
        metadata={"category": "flight_booking"},
    ),
]

experiment = Experiment[str, str](
    cases=test_cases,
    evaluators=[GoalSuccessRateEvaluator()]
)
reports = experiment.run_evaluations(user_task_function)
reports[0].run_display()

A função de tarefa recupera as ferramentas simuladas, cria um agente, executa a interação e retorna tanto a saída do agente quanto a trajetória completa de telemetria. Essa trajetória dá aos avaliadores como o GoalSuccessRateEvaluator acesso à sequência completa de chamadas de ferramentas e invocações do modelo — não apenas à resposta final.

Boas práticas para avaliação baseada em simulação

  • Comece com a configuração padrão para cobertura ampla. Adicione overrides apenas para os ambientes de ferramentas que você precisa controlar com precisão.
  • Forneça valores ricos em initial_state_description para ferramentas com estado: inclua faixas de dados, contagens de entidades e contexto de relacionamentos.
  • Use share_state_id para ferramentas que interagem com o mesmo backend, garantindo que operações de escrita sejam visíveis para leituras subsequentes.
  • Aplique output_schema para ferramentas que seguem especificações rígidas, como OpenAPI ou MCP.
  • Valide sequências de interação com ferramentas, não apenas saídas finais — inspecione mudanças de estado antes e depois da execução do agente.
  • Comece pelos cenários de interação mais comuns e expanda para casos extremos conforme sua prática de avaliação amadurece.
  • Complemente os testes baseados em simulação com testes pontuais contra APIs reais para caminhos críticos de produção.

Como começar

A instalação é feita com um único comando:

pip install strands-evals

Para continuar explorando o ToolSimulator e o Strands Evals, a AWS recomenda consultar a documentação do Strands Evals para ver todas as opções de configuração, incluindo gerenciamento avançado de estado e avaliadores personalizados. Também é possível experimentar o exemplo oficial para ver o ToolSimulator em ação e estendê-lo com mais ferramentas e fluxos multi-etapas.

Para o backend de LLM que alimenta a geração de respostas do ToolSimulator, a AWS indica o Amazon Bedrock. Para estratégias de implantação serverless de agentes que funcionam bem com testes baseados em ToolSimulator, vale explorar o AWS Lambda.

Fonte

ToolSimulator: scalable tool testing for AI agents (https://aws.amazon.com/blogs/machine-learning/toolsimulator-scalable-tool-testing-for-ai-agents/)

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *