CLI com IA: Construindo Ferramentas de Linha de Comando em Python e JavaScript que Usam LLMs de Verdade
O problema real: CLIs que chamam IA e quebram no primeiro timeout
A maioria dos tutoriais de "CLI com IA" para no openai.chat.completions.create(). O resultado é uma ferramenta que funciona na demo, trava no primeiro rate limit e não oferece nenhuma experiência decente quando a API demora 8 segundos para responder.
Construir uma CLI que integra LLMs para uso real exige resolver três problemas que ninguém cobre: streaming de resposta para o terminal não parecer congelado, fallback entre provedores quando um cai, e cache local para não torrar créditos repetindo a mesma pergunta. Este post resolve os três, em Python e em JavaScript, com código que roda.
Anatomia de uma CLI com IA que funciona
Antes de código, a estrutura. Uma CLI inteligente tem quatro camadas:
- Parser de argumentos: recebe input do usuário (prompt, arquivo, flags).
- Camada de cache: verifica se já respondeu aquela pergunta antes.
- Client HTTP com retry e fallback: chama o provedor primário, cai para o secundário se necessário.
- Renderer de output: streaming para o terminal com formatação.
A tabela abaixo compara as escolhas de biblioteca em cada camada:
| Camada | Python | JavaScript (Node.js) |
|---|---|---|
| Parser de argumentos | argparse (stdlib) ou click | commander ou yargs |
| Cache local | diskcache ou SQLite via sqlite3 | keyv com adapter SQLite |
| Client HTTP | httpx (async, streaming nativo) | fetch nativo (Node 18+) com ReadableStream |
| Streaming no terminal | sys.stdout.write + flush | process.stdout.write |
| Formatação de Markdown | rich | marked-terminal |
Se você já trabalha com ferramentas de automação em JavaScript, a camada de parser e output vai parecer familiar. A diferença é que agora o "meio" da CLI faz uma chamada de rede lenta e imprevisível.
Python: CLI completa com streaming e cache
Começando pelo parser. O click é mais ergonômico que argparse para CLIs que crescem:
# cli.py
import click
import hashlib
import json
import sqlite3
import os
DB_PATH = os.path.expanduser("~/.cache/aicli/cache.db")
def get_cache_db() -> sqlite3.Connection:
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
conn = sqlite3.connect(DB_PATH)
conn.execute(
"CREATE TABLE IF NOT EXISTS cache "
"(key TEXT PRIMARY KEY, response TEXT, created_at REAL DEFAULT (unixepoch()))"
)
return conn
def cache_key(model: str, prompt: str) -> str:
# SHA256 garante chave fixa independente do tamanho do prompt
raw = f"{model}::{prompt}"
return hashlib.sha256(raw.encode()).hexdigest()
@click.command()
@click.argument("prompt")
@click.option("--model", default="gpt-4o-mini", help="Modelo a usar")
@click.option("--no-cache", is_flag=True, help="Ignora cache local")
def ask(prompt: str, model: str, no_cache: bool):
"""Envia um prompt para o LLM e exibe a resposta com streaming."""
db = get_cache_db()
if not no_cache:
key = cache_key(model, prompt)
row = db.execute("SELECT response FROM cache WHERE key = ?", (key,)).fetchone()
if row:
click.echo(row[0])
return
response_text = stream_completion(model, prompt)
if response_text and not no_cache:
db.execute(
"INSERT OR REPLACE INTO cache (key, response) VALUES (?, ?)",
(cache_key(model, prompt), response_text),
)
db.commit()
Agora a parte que importa: o client HTTP com streaming. Usar httpx em modo async permite ler chunks conforme chegam, em vez de esperar a resposta inteira:
# client.py
import httpx
import sys
import os
import json
OPENAI_URL = "https://api.openai.com/v1/chat/completions"
ANTHROPIC_URL = "https://api.anthropic.com/v1/messages"
def stream_completion(model: str, prompt: str) -> str:
"""Tenta OpenAI primeiro. Se falhar com status >= 500 ou timeout, cai para Anthropic."""
---
Leia o artigo completo em [https://www.vivodecodigo.com.br/backend/cli-ia-python-javascript-llm-ferramentas-linha-comando](https://www.vivodecodigo.com.br/backend/cli-ia-python-javascript-llm-ferramentas-linha-comando)