, ,

Construindo um Chat com GPT-5 em Azure Functions: Da Ideia à Produção

No meu primeiro artigo com IA e visando aprofundar conhecimentos alinhados a Infraestrutura em Azure, resolvi colocar em desenvolvimento este laboratório prático:

Como construí uma API de chat com IA em arquitetura serverless na Azure, e tudo o que aprendi quebrando a cara pelo caminho.

Há, utilizei o Claude da Anthropic como apoio no desenvolvimento da Function.

Toda vez que vejo um tutorial de “deploy de IA em 5 minutos” eu desconfio. Porque na vida real, entre clicar em “Create” no portal e ver a aplicação respondendo no Postman, muita coisa pode dar errado. Versões de Python que não batem, endpoints que retornam 404 sem motivo aparente, configurações fantasmas que ninguém te conta sobre, modelos com nome inventado pela memória.

Inicialmente comecei com execução local na minha máquina e depois rodando com o agente cloud.

Este post é o relato honesto de uma jornada real: construir uma API REST de chat com GPT-5 rodando em Azure Functions Flex Consumption, com deploy automatizado via GitHub Actions.

Tudo o que vou descrever foi efetivamente executado e está em produção.

Alguns erros aqui estão registrados nos deploys estão registrados lá no repositório.

Se você está construindo algo parecido com Function — ou simplesmente quer entender as pegadinhas do Flex Consumption antes de cair nelas — segue comigo.

Este artigo só descreve as dificuldades que tive, para executar o passo-a-passo você precisa acessar este repositório e colocar o mão na massa:

https://github.com/ErickMedeiros/zafitec-ai-chat/blob/deploy/github-actions/guia-de-configuracao.md


O meu Resultado esperado era , como abaixo:

Uma API HTTP simples:

POST /api/chat
Body: { "message": "sua pergunta" }
→ 200 OK
{ "response": "resposta do GPT-5..." }

Resultado real abaixo

Por baixo dos panos:

Cliente
│ HTTP POST
Azure Function (Python 3.10, Flex Consumption)
│ chat.completions.create()
Azure OpenAI — gpt-5-nano

Stack final:

ComponenteVersão / Valor
RuntimePython 3.10
PlanoAzure Functions Flex Consumption
ModeloAzure OpenAI gpt-5-nano
SDK Pythonopenai==1.59.6
API version2025-01-01-preview
CI/CDGitHub Actions

Parece simples, né? Não foi.


Capítulo 1: O primeiro erro 404

Tinha o Function App provisionado, código Python escrito, e rodei o que parece o comando óbvio:

func azure functionapp publish zafitec-ai-chat --no-build

Resposta:

Error [...] Uploading archive... (NotFound).
Server Response: {"type":"https://tools.ietf.org/html/rfc9110#section-15.5.5","title":"Not Found","status":404}

Confuso. O Function App existe (Running, confirmei via az functionapp list). Por que 404 no upload?

A primeira pista veio do aviso amarelo:

Local python version '3.10.0' is different from the version expected for your deployed Function App.
This may result in 'ModuleNotFound' errors. Please [...] change the virtual environment on your local machine to match ''.

Note o '' no final. Vazio. Como assim a versão esperada é vazia?

Rodei diagnósticos:

az functionapp show -n zafitec-ai-chat -g RG-AISERVICES --query "properties.siteConfig.linuxFxVersion" -o tsv
# (vazio)

O Function App estava sem linuxFxVersion. Aplicativo “casca” — provisionado mas sem stack de runtime configurado. Foi quando comecei a desconfiar que tinha algo estrutural diferente.


Capítulo 2: A descoberta que mudou tudo

Tentei aplicar linuxFxVersion via CLI e aí caí no clássico erro do PowerShell com pipe:

az functionapp config set -n zafitec-ai-chat -g RG-AISERVICES --linux-fx-version "Python|3.10"
'3.10' não é reconhecido como um comando interno

O PowerShell interpretou o | como pipe de comando, não como caractere literal. Solução: o operador --% que diz “para de parsear daqui pra frente”:

az --% functionapp config set -n zafitec-ai-chat -g RG-AISERVICES --linux-fx-version "Python|3.10"

Mas a confirmação seguinte ainda vinha vazia. Estranho. Foi quando rodei um diagnóstico aparentemente trivial:

az functionapp deployment list-publishing-credentials -n zafitec-ai-chat -g RG-AISERVICES
Invalid command. This is not currently supported for Azure Functions on the Flex Consumption plan.

Flex Consumption.

Eu nem sabia que tinha provisionado em Flex Consumption.

Confirmei:

az functionapp show -n zafitec-ai-chat -g RG-AISERVICES --query "properties.sku" -o tsv
# FlexConsumption
az functionapp show -n zafitec-ai-chat -g RG-AISERVICES --query "properties.functionAppConfig.runtime"
# {
# "name": "python",
# "version": "3.10"
# }

E lá estava: a versão do Python configurada em properties.functionAppConfig.runtimeoutro lugar do schema, totalmente diferente do linuxFxVersion que eu vinha consultando.

Tudo que eu tinha aprendido sobre Azure Functions nos últimos anos era parcialmente inválido para esse plano novo.


Capítulo 3: Por que Flex muda tudo

Vale parar e entender. Flex Consumption foi lançado pela Microsoft em 2024 como evolução do plano Consumption clássico. Mas é uma evolução com mudanças incompatíveis que ainda não estão refletidas em boa parte da documentação e tooling.

AspectoConsumption clássicoFlex Consumption
Endpoint SCM (*.scm.azurewebsites.net)✅ existe❌ não existe
linuxFxVersion (config)usadoignorado
functionAppConfig.runtimen/afonte da verdade
FUNCTIONS_WORKER_RUNTIME (app setting)obrigatórioinválido
SCM_DO_BUILD_DURING_DEPLOYMENTusadoinválido
func azure functionapp publish⚠️ parcial
az webapp log tail
Logs em tempo realKudu LogStreamApplication Insights

Quando o func faz publish, ele tenta uploadar para https://<app>.scm.azurewebsites.net/api/zipdeploy. Em Flex, esse endpoint não existe. 404 garantido.

Quando você tenta az webapp log tail, ele tenta conectar em https://<app>.scm.azurewebsites.net/logstream. Mesma coisa. 404.

Teste local:


Capítulo 4: Tentativas frustradas

Passei pelo que talvez você esteja pensando agora: tentei vários caminhos alternativos.

Tentativa 1 — az functionapp deployment source config-zip:

Funcionou no sentido de retornar "Deployment was successful.", mas a função simplesmente não registrava no host. O az functionapp function list voltava vazio.

Tentativa 2 — az functionapp deploy --type zip:

Erro 415 (Unsupported Media Type) — o comando moderno ainda tem inconsistências com Flex.

Tentativa 3 — REST API OneDeploy direto:

Outro 415, dessa vez com uma página HTML de erro do ASP.NET clássico — sinal claro de que o endpoint nem é o certo.

Tentativa 4 — empacotar com dependências (.python_packages):

Deploy ok, mas a função continuava sem registrar. O Flex tem um mecanismo de build próprio que não combina bem com pacotes pré-buildados.

Cada tentativa demorava 2-5 minutos pra responder, e o feedback do Flex sobre por que a função não registrava era… nenhum. O Log Stream do portal só mostrava heartbeats:

[Verbose] Ping Status: { "hostState": "Running" }
[Information] Executing StatusCodeResult, setting HTTP status code 200

Nenhum traceback. Nenhum import error. O worker simplesmente não tentava carregar minha função, como se o pacote não tivesse sido reconhecido como código Python válido.


Capítulo 5: GitHub Actions resolve

A virada veio quando parei de tentar contornar e fui direto pro caminho oficialmente recomendado pela Microsoft para Flex Consumption: a action Azure/functions-action@v1 com a flag sku: 'flexconsumption'.

Estrutura:

name: Deploy to Azure Function App (Flex Consumption)
on:
push:
branches: [main, deploy/github-actions]
workflow_dispatch:
env:
AZURE_FUNCTIONAPP_NAME: zafitec-ai-chat
PYTHON_VERSION: '3.10'
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install dependencies
shell: bash
run: |
python -m pip install --upgrade pip
pip install --target=".python_packages/lib/site-packages" -r requirements.txt
- name: Login to Azure
uses: azure/login@v2
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Deploy to Function App
uses: Azure/functions-action@v1
with:
app-name: ${{ env.AZURE_FUNCTIONAPP_NAME }}
package: '.'
scm-do-build-during-deployment: false
enable-oryx-build: false
sku: 'flexconsumption'

Deploy em Github Action

Resultado do deploy

Três detalhes que importam:

  1. pip install --target=".python_packages/lib/site-packages" — empacota dependências num caminho que o Functions Python reconhece nativamente.
  2. sku: 'flexconsumption' — sem isso, a action tenta o caminho clássico e dá ruim.
  3. Service Principal com role Contributor restrita ao Function App específico (não na subscription inteira) — segurança em primeiro lugar.

Push da branch, e:

1m 49s. Tudo verde.

E no Postman:

⚡ Status 200 OK em 2.99 segundos com resposta completa do GPT-5-nano.


Capítulo 6: As pegadinhas do GPT-5

Mas teve uma pegadinha extra. A primeira chamada após o deploy bem-sucedido retornou:

{ "response": "" }

Status 200. Resposta vazia.

GPT-5 não é um modelo de chat tradicional — é um modelo de raciocínio. Ele consome tokens primeiro pensando internamente (reasoning tokens) antes de gerar a resposta visível. Se você não dá margem suficiente, todos os tokens vão pro raciocínio e sobra zero pra resposta.

Configurações importantes:

ParâmetroGPT-4o e anterioresGPT-5
max_tokens❌ Erro 400
max_completion_tokensUse este
temperaturequalquersó default (=1)
reasoning_effortn/aminimal, low, medium, high

A correção:

completion = client.chat.completions.create(
model=deployment,
messages=[
{"role": "system", "content": "Você é um assistente técnico..."},
{"role": "user", "content": user_message},
],
max_completion_tokens=4000, # margem ampla
reasoning_effort="minimal", # raciocínio mínimo = mais resposta
)

reasoning_effort="minimal" é a chave para chat conversacional. Para problemas complexos (matemática, código difícil), você sobe para medium ou high. Para chat geral, minimal é o ideal — respostas mais rápidas e menos tokens consumidos sem propósito.

Após o ajuste, push, deploy automático em 2 minutos, e:

{
"response": "Olá! Posso te ajudar a verificar se está tudo funcionando em produção. Para começar, preciso de algumas informações rápidas..."
}

🎉 Funcionando perfeitamente.


Capítulo 7: Bug bônus do httpx

Antes de chegar nesse ponto, ainda apanhei de um bug clássico no ecossistema Python. Durante o teste local:

TypeError: Client.__init__() got an unexpected keyword argument 'proxies'

Causa: incompatibilidade entre openai<1.55 e httpx>=0.28. A httpx 0.28 removeu o argumento proxies, mas a openai antiga ainda passava ele internamente.

Solução: pinar versões compatíveis no requirements.txt:

azure-functions==1.21.3
openai==1.59.6

Lição: sempre pine versões em projetos serverless. Builds reproduzíveis valem ouro quando você está debugando às 23h.


A receita final

Para quem quer evitar a jornada de descoberta e ir direto ao que funciona:

1. Provisionar Function App em Flex Consumption

az functionapp create `
--resource-group RG-AISERVICES `
--name zafitec-ai-chat `
--storage-account zafitecstorage `
--flexconsumption-location canadacentral `
--runtime python `
--runtime-version 3.10 `
--functions-version 4

A flag chave é --flexconsumption-location. Sem ela, vai pro Consumption clássico.

2. Código Python com tratamento granular

import logging, json, os
import azure.functions as func
from openai import AzureOpenAI, APIError, APIConnectionError, RateLimitError
app = func.FunctionApp()
_client = None
def get_client():
global _client
if _client is None:
_client = AzureOpenAI(
api_key=os.environ["AZURE_OPENAI_API_KEY"],
azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
api_version=os.getenv("AZURE_OPENAI_API_VERSION", "2025-01-01-preview"),
)
return _client
@app.function_name(name="HttpChat")
@app.route(route="chat", methods=["POST"], auth_level=func.AuthLevel.ANONYMOUS)
def http_chat(req: func.HttpRequest) -> func.HttpResponse:
try:
body = req.get_json()
user_message = (body or {}).get("message", "").strip()
if not user_message:
return _resp({"error": "Campo 'message' é obrigatório"}, 400)
completion = get_client().chat.completions.create(
model=os.environ["AZURE_OPENAI_DEPLOYMENT"],
messages=[
{"role": "system", "content": "Você é um assistente técnico..."},
{"role": "user", "content": user_message},
],
max_completion_tokens=4000,
reasoning_effort="minimal",
)
return _resp({"response": completion.choices[0].message.content or "(vazio)"}, 200)
except RateLimitError:
return _resp({"error": "Serviço sobrecarregado"}, 429)
except APIConnectionError:
return _resp({"error": "Falha de conexão"}, 502)
except APIError:
return _resp({"error": "Erro da API"}, 502)
except Exception:
logging.exception("Erro inesperado")
return _resp({"error": "Erro interno"}, 500)
def _resp(payload, status):
return func.HttpResponse(
json.dumps(payload, ensure_ascii=False),
mimetype="application/json",
status_code=status,
)

3. Service Principal com escopo restrito

$subId = az account show --query id -o tsv
az ad sp create-for-rbac `
--name "github-actions-zafitec-ai-chat" `
--role Contributor `
--scopes "/subscriptions/$subId/resourceGroups/RG-AISERVICES/providers/Microsoft.Web/sites/zafitec-ai-chat" `
--json-auth

JSON retornado vai como secret AZURE_CREDENTIALS no GitHub.

4. Workflow do GitHub Actions

(Visto no Capítulo 5 acima.)

5. Push e pronto

git add .
git commit -m "feat: minha mudança"
git push

A partir daqui, todo deploy é apenas isso. ~2 minutos do commit ao endpoint atualizado em produção.


Custos e performance

Para quem vai colocar em produção, alguns números reais do meu deploy:

  • Cold start (primeira chamada após deploy): 30-60s
  • Warm response time: 2-5s (a maior parte é a chamada ao GPT-5)
  • Custo do plano: Flex Consumption escala a zero — pagando só pelas execuções reais
  • Custo do GPT-5-nano: dos modelos mais baratos da família GPT-5; tipicamente <$0.50/1M tokens

Para uma carga de chat moderada (algumas centenas de mensagens por dia), o custo mensal fica na casa dos poucos dólares.


Lições aprendidas

Saindo dessa, aqui ficam as lições que valem mais que o tempo gasto:

1. Identifique seu plano antes de qualquer outra coisa.

Consumption, Premium, Flex Consumption, App Service, Container Apps — todos hospedam Azure Functions e todos têm fluxos de deploy diferentes. Os primeiros 30 segundos do troubleshooting deveriam sempre ser:

az functionapp show -n <app> -g <rg> --query "properties.sku" -o tsv

2. Em ambientes novos, prefira o caminho oficial sobre o “que sempre funcionou”.

func azure functionapp publish funcionou pra mim por anos no Consumption clássico. No Flex, ele é parcialmente quebrado. GitHub Actions é o caminho oficialmente suportado pela Microsoft, então vai por ele.

3. CI/CD não é “boas práticas” — é debugging mais barato.

Cada deploy via az ou func consumiu de 3 a 7 minutos do meu tempo. GitHub Actions roda em paralelo e me deixa fazer outras coisas. Mais importante: cada execução fica registrada com logs detalhados. Você não precisa lembrar o que tentou — está tudo lá.

4. Modelos de raciocínio mudam o jogo de prompt engineering.

GPT-5 não é “GPT-4 melhor”. É uma classe diferente de modelo. Saber quando usar reasoning_effort=minimal vs high faz diferença de 10x em latência e custo.

5. Pine versões de dependências.

Sempre. Sem exceção. Especialmente em ecossistemas Python onde mudanças de minor version já podem quebrar tudo (openai + httpx foi um caso clássico).


Recursos


Bom né?

Se você está construindo algo parecido e bateu nas mesmas paredes, espero que esse post tenha encurtado seu caminho. E se descobriu algo que eu não cobri aqui, me chama — sempre tem uma camada nova de ralação que ninguém documentou ainda.

Até o próximo deploy.

— Erick Medeiros – Microsoft MVP


Publicado originalmente em erickbmedeiros.com.br em 02 de maio de 2026.