Alura > Cursos de Inteligência Artificial > Cursos de IA para Desenvolvimento de Software > Conteúdos de IA para Desenvolvimento de Software > Primeiras aulas do curso Desenvolvimento assistido por IA: prompts, harness e MCP na prática

Desenvolvimento assistido por IA: prompts, harness e MCP na prática

Prompt e Context Engineering - Apresentação

Apresentando o curso e o instrutor

Olá! Seja bem-vinde a este curso. Meu nome é Rafael Fischer, serei seu instrutor.

Trabalho com tecnologia para empresas do exterior desde 2021 e programo desde 2005. Recentemente, tenho trabalhado intensamente com um método de desenvolvimento mais orientado a agentes, utilizando IaaS (Infraestrutura como Serviço), e quero compartilhar um pouco do meu conhecimento sobre esse tema.

Apresentando os conteúdos e pré-requisitos

Neste curso, nós vamos aprender sobre Prompt Engineering (Engenharia de Prompt), Context Engineering (Engenharia de Contexto) e Harness Engineering (Engenharia de Harness), além de MCPs, como aplicar regras, aplicar guias, utilizar diversas ferramentas e vários componentes que vão nos ajudar durante o desenvolvimento assistido por IaaS (Infraestrutura como Serviço), uma forma moderna e contemporânea de desenvolver software.

Como alguns pré-requisitos, esperamos que você tenha concluído os cursos da trilha DevFullstack do Futuro e que possua conhecimentos de front-end (interface) e back-end (servidor), pois neste curso vamos utilizar conhecimentos de ambas as áreas. Vamos desenvolver uma aplicação Fullstack (pilha completa), utilizando o que há de mais moderno em termos de práticas de desenvolvimento orientadas a agentes, com Context Engineering (Engenharia de Contexto), Harness Engineering (Engenharia de Harness) e Prompt Engineering (Engenharia de Prompt).

Apresentando o projeto do curso

Para concluir esta introdução, vamos mostrar qual será o projeto que vamos desenvolver.

Esta é uma aplicação Fullstack (pilha completa). Nós vamos trabalhar no front-end (camada de interface) e no back-end (camada de serviços). Trata-se de um gerenciador de tarefas que nos permitirá exercitar as ações principais presentes em qualquer sistema: criar elementos, consultar, listar, editar e excluir.

Esse conhecimento é generalizável e potencialmente aplicável em diversos projetos ao longo da carreira.

Demonstrando a aplicação e encaminhando para a aula 1

Para contextualizar, há uma tela de login (autenticação). Essa parte já ficará pronta, então não será necessário se preocupar com ela. Nós vamos nos concentrar em criar, editar, listar e atualizar os elementos. Para demonstrar rapidamente a parte interna da aplicação, criaremos uma conta de teste.

Dentro do sistema, podemos listar todas as tarefas, criar uma nova tarefa que será registrada e persistida no banco de dados, atualizar o estado da tarefa e visualizar um indicador de sessão. Também podemos excluir uma tarefa e removê-la da nossa lista.

É um projeto simples, mas que exercitará, de forma prática, as operações fundamentais que costumamos encontrar em sistemas em geral.

Estamos muito entusiasmados com este curso e esperamos que você aproveite. Vamos direto para a nossa Aula 1.

Prompt e Context Engineering - Prompt Engineering

Contextualizando o tema e objetivos

Introdução

Neste vídeo, vamos comentar sobre a engenharia de prompts (instruções): o que é e algumas boas práticas, com conselhos práticos para criar bons prompts (instruções). Antes, queremos problematizar a situação.

Problematizando com exemplos práticos

Problematização

Vamos imaginar que estamos interagindo com o Claude Code ou mesmo com o ChatGPT e decidimos que queremos programar, que queremos recodificar uma API (interface de programação de aplicações), um endpoint (rota de API) para listar tarefas do projeto no qual estamos trabalhando. Então, pedimos à nossa LLM (Large Language Model, Modelo de Linguagem Grande), ao ChatGPT ou ao Claude: "Crie uma API (interface de programação de aplicações) para listar tarefas para nós."

Depois de um tempo, obtemos a resposta. Ao revisar, temos um código em Python usando Flask, que é o framework (estrutura de suporte) HTTP do Python, com a lista de tarefas totalmente hardcoded (inserida diretamente no código). Além disso, há uma rota definida como /tarefas, quando a convenção do nosso projeto seria, em inglês, /tasks. Estranho, certo?

Para ilustrar essa primeira situação, segue um exemplo simples de como esse retorno poderia aparecer:

from flask import Flask, jsonify

app = Flask(__name__)
tarefas = ["comprar leite", "estudar", "malhar"]

@app.route("/tarefas")
def listar():
  return jsonify(tarefas)

app.run()

Note como a lista está fixa no código e a rota segue o padrão em português (/tarefas), destoando do esperado no projeto (/tasks). Esse tipo de desalinhamento é comum quando o prompt inicial é demasiado genérico.

Agora, suponhamos que, em outra sessão, em vez de simplesmente dizer "Crie uma API (interface de programação de aplicações) para listar tarefas", sejamos mais específicos e digamos: "Crie uma API (interface de programação de aplicações) para listar as tarefas em TypeScript, com o input (entrada) x — o x pode ser substituído por qualquer estrutura de dados desejada — devolvendo também o input (entrada) y", sem detalhar aqui, mas presumindo que seja uma estrutura de dados pensada previamente, com validações e consultando o banco de dados. A partir dessa interação com uma LLM, com o Claude Code ou com o ChatGPT, obtemos como resultado um código em TypeScript, com validações, que respeita o payload (corpo da requisição) — isto é, o input (entrada) que tínhamos em mente — e cuja saída esperada realiza as validações de forma correta.

Um exemplo de retorno mais alinhado com o pedido, incorporando validações e uma consulta a dados, poderia ser:

import { z } from "zod";
import express from "express";

const app = express();

const QuerySchema = z.object({
  status: z.enum(["pendente", "concluida"]).optional(),
  limit: z.coerce.number().min(1).max(100).default(20),
});

type Tarefa = { id: string; titulo: string; status: "pendente" | "concluida" };

app.get("/tarefas", (req, res) => {
  const parsed = QuerySchema.safeParse(req.query);
  
  if (!parsed.success) {
    return res.status(400).json({ error: parsed.error.flatten() });
  }
  
  const { status, limit } = parsed.data;
  const tarefas: Tarefa[] = buscarTarefas({ status, limit });
  res.json({ data: tarefas, total: tarefas.length });
});

Perceba como esse exemplo já traz: tipagem explícita (TypeScript), validação de entrada com Zod, tratamento de erro com status 400 quando a query é inválida e um esboço de integração com dados via buscarTarefas. Tudo isso emerge de um prompt mais específico e contextualizado.

Qual é a diferença entre essas duas situações? Basicamente, isso exemplifica a aplicação de técnicas de engenharia de prompts (instruções).

Definindo a engenharia de prompts

Definição de engenharia de prompts

Se fôssemos definir formalmente o que é engenharia de prompts (instruções), diríamos que é o processo de projetar e refinar as instruções que enviamos em nossas interações com modelos de linguagem.

Como acabamos de ver, para realizar a mesma tarefa — a mesma intenção — podemos obter resultados totalmente diferentes, dependendo de como estruturamos nossos prompts (instruções) e do contexto fornecido. Uma característica da engenharia de prompts (instruções) é que se trata de uma disciplina extremamente iterativa. Há bastante tentativa e erro quando trabalhamos e fazemos a engenharia do nosso prompt (instrução).

É provável que a primeira versão do prompt (instrução) que criamos não seja a mais adequada — isso normalmente ocorre. Na primeira versão da interação com a LLM, muitas vezes não obtemos a resposta desejada. Mas tentamos novamente, refinamos, adicionamos contexto e, gradualmente, vamos nos aproximando do que desejamos.

Esse é um conselho: se não obtivermos um bom resultado em uma interação com a LLM, talvez o problema esteja no nosso prompt (instrução). Vale a pena insistir, refinar, pegar o prompt (instrução) inicial, adicionar mais contexto, ou aplicar algumas técnicas que vamos comentar agora, e testar novamente para observar o resultado.

Detalhando os componentes essenciais

Componentes de um bom prompt

Com relação aos componentes de um bom prompt (instrução), ele costuma ter algumas seções que o tornam eficaz.

É altamente recomendável que, em cada interação com uma LLM (Modelo de Linguagem Grande), deixemos claro qual é o objetivo e qual é a tarefa que deve ser realizada pela IA, de forma não ambígua e, de preferência, apenas uma tarefa por interação. Quanto mais específicos formos, melhores serão os resultados.

Como exemplo, estamos construindo um prompt (instrução). Suponhamos que queiramos usar a IA para revisar um pull request (solicitação de alteração), isto é, o código de uma pessoa colega. Partindo dessa suposição, já definimos o contexto: anteriormente indicamos que a LLM (Modelo de Linguagem Grande) deve atuar como revisora de código. A instrução clara seria: queremos que revise a função a seguir, a função do código da pessoa colega, e aponte problemas de desempenho e legibilidade. Dizer apenas “revisar a função” é ambíguo; ao acrescentarmos mais contexto — isto é, “apontar problemas de desempenho e legibilidade” — ficamos mais específicos, o que é positivo do ponto de vista do prompt (instrução).

Para tornar esse exemplo concreto, considere que a função a ser revisada é:

def get_active_users(users):
  result = []
  for u in users:
    if u.active == True:
      result.append(u)
  return result

Outro ponto importante a incluir em nossos prompts (instruções) é a questão dos exemplos, o que muitas pessoas chamam de few-shot (poucos exemplos). Imaginemos que mentoramos uma pessoa e pedimos que faça algo sem fornecer exemplos de como fazer. Ela precisará inferir muitas coisas — “adivinhar”, por assim dizer — e, dependendo de como formulamos o pedido, talvez não consiga inferir corretamente. A partir do momento em que fornecemos exemplos de como executar a tarefa e de como chegar ao resultado desejado, aumentamos a probabilidade de que essa pessoa entregue o que esperamos. Com as LLMs (Modelos de Linguagem Grande) é a mesma lógica: se oferecermos exemplos, é mais provável que sigam o que esperamos. No nosso caso hipotético de revisão de código, se adicionarmos exemplos de frases que a LLM (Modelo de Linguagem Grande) poderia usar durante a avaliação, melhor. Assim, a LLM (Modelo de Linguagem Grande) terá um caminho mais claro a seguir e irá pela direção que desejamos.

Para guiar o tom da revisão com exemplos (few-shot), podemos incluir algo como:

Outro componente importante são as restrições. Assim como dizemos à LLM (Modelo de Linguagem Grande) o que deve fazer, convém também deixar claro o que não deve fazer. Declarar os casos negativos muitas vezes é até mais importante do que os positivos. Neste caso, não queremos, por exemplo, que, durante a revisão, a IA reescreva a função, mude a linguagem de programação ou faça qualquer outra coisa além de revisar. Queremos que se mantenha focada nesse aspecto. A LLM (Modelo de Linguagem Grande) tende, às vezes, a realizar ações adicionais ou coisas que não pedimos; por isso, é útil especificar esses casos negativos — as restrições.

Aplicando isso ao nosso exemplo de revisão, podemos explicitar:

Outro componente extremamente importante é o formato de saída. Se quisermos uma saída específica, precisamos tornar isso explícito para a LLM (Modelo de Linguagem Grande). Se quisermos que a avaliação seja apresentada de determinada maneira, temos de explicitar qual é essa forma. Isso é extremamente importante; o output (formato de saída) talvez seja um dos componentes mais cruciais que podemos incluir em nossos prompts (instruções). Por mais potente que seja o modelo, o formato de saída costuma ser um ponto de falha — “falha”, entre aspas, porque o modelo não consegue acertar se não tiver o contexto. Fornecer o contexto sobre o formato de saída é muito útil nas interações. No exemplo de prompt (instrução) que estamos construindo, colocamos como última parte o formato de saída: no processo de avaliação, a saída gerada pela LLM (Modelo de Linguagem Grande) será uma lista com 3 marcadores (3 itens), cada qual contendo um problema e uma sugestão de correção. Estamos indicando exatamente a estrutura de cada item para evitar erros.

Especificando o formato de saída no nosso caso:

Apresentando outras técnicas complementares

Existem também outras técnicas comumente utilizadas. As que mencionamos até aqui são as mais importantes, mas vale pontuar brevemente outras:

Avaliando a relevância e otimizações

Sobre a engenharia de prompts (instruções), há quem diga que “morreu”. Na prática, ela continua muito ativa e brilha em várias situações específicas. Suponhamos que exista um fluxo de trabalho repetitivo, como extrair informações diariamente de 10.000 PDFs. Se criarmos um prompt (instrução) bem otimizado, iterado e refinado, é mais provável obtermos um bom resultado e até economizarmos dinheiro do que com um prompt (instrução) pouco refinado, pois conseguiremos reduzir tokens (unidades de texto) quando necessário e torná-lo mais conciso sem perda de qualidade.

Ao trabalhar com modelos menores, é interessante dedicar atenção à engenharia de prompts (instruções), porque esses modelos são mais sensíveis a variações do prompt (instrução). Em fluxos ágeis de desenvolvimento, haverá situações em que usaremos modelos mais antigos ou menos potentes, e, ao interagir com eles, convém ter ainda mais cuidado com o prompt (instrução).

Em qualquer interação mais séria com uma LLM (Modelo de Linguagem Grande) — escrever um e-mail, produzir código ou realizar algo que envolva algum tipo de risco — é recomendável adotar uma postura profissional e aplicar técnicas de engenharia de prompts (instruções). Isso tende a melhorar os resultados, mesmo utilizando um modelo potente e de última geração. Nessas situações mais críticas ou profissionais, vale ter um enfoque mais rigoroso e ser mais estrito em relação aos nossos prompts (instruções) do que costumamos ser. Aplicar essa forma de pensar costuma gerar resultados melhores nas interações com LLMs (Modelos de Linguagem Grande).

Prompt e Context Engineering - Conhecendo o Metaprompt

Continuando o curso e introduzindo o metaprompt

Vamos continuar com nosso curso. Neste vídeo, vamos falar sobre o Metaprompt (metaprompt).

No vídeo anterior, apresentamos várias técnicas e recursos que podemos utilizar para criar prompts (instruções) melhores. Há muitos aspectos a considerar. E se dissermos que podemos usar a própria LLM (Modelo de Linguagem) para nos ajudar a gerar os prompts? Basicamente, é disso que se trata o Metaprompt.

Explicando o fluxo do metaprompt

Vamos explicar brevemente como funciona nosso fluxo quando utilizamos o Metaprompt. Na interação inicial com a LLM, não nos preocupamos, necessariamente, em incluir todas as características e componentes mostrados no vídeo anterior. Focamos em fornecer o máximo de contexto possível, e isso é tudo. Podemos escrever de forma não estruturada, sem nos prender a seções. O que mais importa nessa etapa é fornecer o máximo de contexto. Forneça contexto. Escreva bastante, sem se preocupar com correção gramatical ou com a estrutura do texto. O prompt inicial será vago e imperfeito.

Em seguida, com base nesse contexto inicial, pedimos: “IA, refine isto para nós. Transforme este prompt e este contexto que escrevemos em um prompt profissional que uma LLM consiga entender e que execute a tarefa da forma esperada.” A LLM aplicará boas práticas e técnicas de prompt engineering (engenharia de instruções) e devolverá um prompt final revisado, gerado por ela.

Nesse momento, precisamos validar. Nós, como pessoas, revisaremos se o prompt gerado está de acordo com o que esperamos. Depois disso, podemos ir à LLM ou à IA com a qual queremos executar a tarefa, colar o prompt formalizado e bem estruturado e, assim, possivelmente obter resultados melhores ao interagir com as IAs.

Introduzindo o exemplo prático

Metaprompt na prática.

Partindo do exemplo de um vídeo anterior, no qual precisávamos criar uma API para listar tarefas, podemos começar a definir vários aspectos dessa API. Queremos que seja uma API para listar tarefas, escrita em TypeScript, com as validações necessárias, que realize consultas ao banco de dados e que utilize, como entrada do endpoint (ponto de extremidade), o estado da tarefa. Ao final, queremos que liste todas as tarefas que correspondam a esse estado, exibindo o ID, o título e, possivelmente, também o estado, além de um contador para indicar quantas tarefas totais existem nesse endpoint (ponto de extremidade). Para a URL do recurso desse endpoint (ponto de extremidade), podemos definir que seja /tasks.

Solicitando o refinamento do prompt

Poderíamos literalmente comunicar isso à LLM ou escrever essas especificações para a LLM. Porém, no Metaprompt (metaprompt), ao final de todo esse contexto, faríamos uma delimitação com um pequeno separador, apenas para distinguir claramente o que é contexto e o que é instrução, e diríamos à LLM o seguinte: Com base no contexto anterior, melhore este prompt (instrução), seguindo as melhores práticas de Prompt Engineering (Engenharia de Prompts). Em seguida, incluiríamos: contexto, instruções, exemplos, restrições e formatos de saída. Submeteríamos essa solicitação à LLM.

Ao final, teríamos um prompt (instrução) aproximadamente assim. Observamos como fica bem estruturado e profissional: o contexto está explícito; a instrução — a tarefa principal — já está dividida em subtarefas; os requisitos da tarefa a ser executada estão definidos; há exemplos de entrada e de saída; as restrições foram adicionadas; e o formato de saída foi especificado. Tudo fica bem exemplificado e explicado graças à ideia do Metaprompt (metaprompt).

Exemplificando a solicitação final

Para materializar essa etapa de “prompt refinado”, segue um exemplo de como a solicitação final à LLM pode ficar:

Contexto: Você é um dev backend sênior trabalhando num projeto TypeScript com
Node.js e Express, consultando um banco de dados relacional (ex: PostgreSQL) via um
ORM (ex: Prisma).

Instrução: Cria um endpoint GET /tasks que
- Receba status e opcionalmente limit para filtrar as tarefas
- Consulte o banco de dados e retorne as tarefas filtradas (ou todas, se status
não for informado)
- Cada tarefa deve conter id, titulo e status
- Retorne também o total de tarefas na resposta
- Valide o parâmetro recebido (ex: aceitar apenas valores válidos como
"pending", "in progress", "done")
- Trata erros (ex: status inválido, falha na consulta ao banco)

Exemplo de request:
GET /tasks?status=pending

Exemplo de resposta esperada:
{
  "total": 2,
  "tasks": [
    { "id": 1, "title": "Revisar PR", "status": "pending" },
    { "id": 2, "title": "Atualizar dependências", "status": "pending" }
  ]
}

Restrições: Não implemente autenticação/autorização neste endpoint. Não crie o
esquema do banco do zero - assuma que a tabela tasks já existe com as colunas id,
title e status.

Formato de saída: Código TypeScript completo do endpoint (rota + lógica), incluindo
validação com Zod e tratamento de erros. Pode usar comentários curtos explicando
decisões não óbvias.

Esse “prompt final” deixa explícitos o contexto, a instrução, exemplos, restrições e o formato de saída, permitindo que a LLM gere uma solução mais alinhada ao que esperamos.

Concluindo e apresentando implementação de exemplo

Para concluir, a partir desse input (entrada), desse prompt (instrução), obtivemos, como resultado, após enviarmos isso a uma LLM, um código bem estruturado, validado e alinhado às melhores práticas esperadas.

Como exemplo do tipo de implementação que pode retornar, veja um endpoint em TypeScript/Express com validação via Zod e resposta contendo as tarefas e o total:

Mostrando o endpoint de exemplo

import { z } from "zod";
import express from "express";

const app = express();

const querySchema = z.object({
  status: z.enum(["pendente", "concluida"]).optional(),
  limit: z.coerce.number().min(1).max(100).default(20),
});

type Tarefa = { id: string; titulo: string; status: "pendente" | "concluida" };

app.get("/tarefas", (req, res) => {
  const parsed = querySchema.safeParse(req.query);
  if (!parsed.success) {
    return res.status(400).json({ error: parsed.error.flatten() });
  }

  const { status, limit } = parsed.data;
  const tarefas: Tarefa[] = buscarTarefas(status, limit);
  res.json({ data: tarefas, total: tarefas.length });
});

Observação: neste exemplo gerado, os nomes estão em português (rota “/tarefas”, campos “titulo”, status “pendente”/“concluida”). A lógica, porém, corresponde exatamente ao que descrevemos para “/tasks” com os campos id/title/status e a contagem total. Ao aplicar o metaprompt ao seu contexto, você pode ajustar nomenclaturas (como a rota “/tasks” e valores de status “pending”/“in progress”/“done”) conforme o padrão do seu projeto.

Sobre o curso Desenvolvimento assistido por IA: prompts, harness e MCP na prática

O curso Desenvolvimento assistido por IA: prompts, harness e MCP na prática possui 290 minutos de vídeos, em um total de 54 atividades. Gostou? Conheça nossos outros cursos de IA para Desenvolvimento de Software em Inteligência Artificial, ou leia nossos artigos de Inteligência Artificial.

Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:

Aprenda IA para Desenvolvimento de Software acessando integralmente esse e outros cursos, comece hoje!

Conheça os Planos para Empresas