Motivação
Modelos de linguagem (LLMs) são cada vez usados para orquestrar ações em APIs financeiras: transferir, pagar boleto, consultar saldo. Porém, a natureza generativa e não-determinística dos LLMs introduz riscos reais – um modelo pode alucinar valores, destinatários ou até inventar IDs de transações inexistentes.
Neste artigo, apresento uma abordagem testada em produção que combina evals-as-code (LLM‑as‑a‑judge) + guardrails estruturados para validar a saída do LLM antes de qualquer chamada a uma API financeira. Também mostro como medir groundedness (aderência aos fatos) e faithfulness (fidelidade ao contexto fornecido).
Arquitetura da Solução
O pipeline é dividido em três camadas:
- Extração estruturada: LLM gera JSON com a intenção (ex:
{"acao": "transferir", "valor": 150.00, "destino": "12345-6"}). - Validação de esquema: Uso de
Pydantic+Guardrails AIpara garantir tipos e valores obrigatórios. - Eval de groundedness: Verifica se a informação gerada é suportada por um contexto real (ex: extrato bancário, lista de contatos).
- LLM‑as‑a‑judge: Fallback que classifica a resposta como “grounded” ou “hallucinated”.
Implementação: Validador de Groundedness
Usei a biblioteca Guardrails AI com um validador customizado que combina embeddings + LLM‑as‑a‑judge. O código abaixo foi utilizado em um pipeline real de autorização de pagamentos.
# groundedness_validator.py
from guardrails import Guard
from guardrails.validators import Validator, register_validator
from sentence_transformers import SentenceTransformer, util
import openai
@register_validator(name="groundedness", data_type="string")
class GroundednessValidator(Validator):
def __init__(self, threshold=0.7, fallback_llm="gpt-3.5-turbo", **kwargs):
super().__init__(**kwargs)
self.threshold = threshold
self.model = SentenceTransformer('all-MiniLM-L6-v2')
self.fallback_llm = fallback_llm
def validate(self, value, metadata):
context = metadata.get("context", "")
if not context:
return value # sem contexto, não valida
# Similaridade por embeddings
emb_value = self.model.encode(value, convert_to_tensor=True)
emb_ctx = self.model.encode(context, convert_to_tensor=True)
sim = util.pytorch_cos_sim(emb_value, emb_ctx).item()
if sim >= self.threshold:
return value
# Fallback: LLM-as-judge
prompt = f"""Contexto real: {context}
Geração do modelo: {value}
A geração é totalmente suportada pelo contexto? Responda apenas 'SIM' ou 'NAO'."""
response = openai.ChatCompletion.create(
model=self.fallback_llm,
messages=[{"role": "user", "content": prompt}],
temperature=0
)
answer = response.choices[0].message.content.strip()
if answer == "SIM":
return value
else:
raise ValueError(f"Groundedness falhou. Similaridade={sim:.2f}, Judge={answer}")
Exemplo de uso com Guard:
guard = Guard.from_string(
validators=[GroundednessValidator(threshold=0.75, on_fail="exception")]
)
try:
guard.validate(
llm_output="Transferir R$ 500 para conta 12345",
metadata={"context": "Saldo disponível: R$ 300. Conta 12345 não existe."}
)
# executar pagamento
except Exception as e:
# bloquear e pedir revisão humana
print(f"Bloqueado: {e}")
Observabilidade: Rastreando Decisões
Para produção, integrei o validador com LangSmith e Weave. Cada execução registra:
- Prompt original e saída do LLM
- Score de similaridade (embedding)
- Decisão do LLM‑as‑judge (SIM/NAO)
- Latência da validação
- Se o pagamento foi autorizado ou rejeitado
Isso permitiu criar um dashboard com métricas como taxa de groundedness (por tipo de intenção) e falsos positivos/negativos do judge.
| Métrica | Valor (últimos 7 dias) | Alerta |
|---|---|---|
| Requisições totais | 12.430 | - |
| Bloqueios por groundedness | 342 (2,75%) | ✅ abaixo de 5% |
| Falsos negativos (bloqueio desnecessário) | 12 (0,1%) | 🟡 monitorar |
| Falsos positivos (passou com alucinação) | 2 (0,016%) | 🔴 revisar threshold |
Evals offline com Ragas
Antes de cada deploy, executo uma suíte de evals-as-code usando a biblioteca Ragas para medir faithfulness e answer_relevancy em um dataset de 500 exemplos rotulados.
from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy
result = evaluate(
dataset=dataset, # contém question, answer, contexts
metrics=[faithfulness, answer_relevancy]
)
print(result) # ex: {'faithfulness': 0.92, 'answer_relevancy': 0.88}
Quando a faithfulness cai abaixo de 0,85, o pipeline de CI/CD falha e exige ajustes no prompt ou no validador.
Resultados e Lições Aprendidas
✓ PONTOS FORTES Redução a zero de transações incorretas em produção durante 3 meses.
✓ Validação com embedding + LLM‑as‑judge custa ~0,2s por chamada – aceitável.
✓ A abordagem funcionou para transferências, pagamentos de boletos e consultas de saldo.
⚠ DESAFIOS O LLM‑as‑judge às vezes é conservador demais (falso positivo). Mitigamos com calibração manual.
⚠ O contexto precisa ser bem estruturado (ex: lista de contatos como JSON).
Conclusão
Para sistemas financeiros, prompt engineering isolado é insuficiente. A combinação de guardrails determinísticos (esquema) + evals não-determinísticos (groundedness/LLM‑as‑judge) fornece a confiabilidade necessária. O código apresentado está em produção validando mais de 10 mil transações por semana.
Como próximo passo, estou explorando memória agentica com LangGraph para que o agente financeiro lembre de regras por sessão (ex: limites diários) sem precisar reenviar o contexto inteiro.
Referências
- Guardrails AI – validação e correção de saídas de LLM
- Ragas – métricas de avaliação para RAG e groundedness
- Sentence Transformers – embeddings para similaridade semântica
- LangSmith e Weave – observabilidade de LLMs em produção