O que é Trilha de Auditoria?
Trilha de Auditoria (Audit Log) é um padrão de projeto de IA para apps com LLM e agentes, do grupo Segurança e confiabilidade. Registrar quem pediu, o que o modelo viu, o que decidiu e o que executou.
Audit Log
Trilha de Auditoria (Audit Log) é um padrão de projeto de IA para apps com LLM e agentes, do grupo Segurança e confiabilidade. Registrar quem pediu, o que o modelo viu, o que decidiu e o que executou.
Registrar, de forma imutável e consultável, quem pediu, o que o modelo viu, o que ele decidiu e o que o sistema executou. Serve para sistemas que decidem sobre dinheiro, dados ou pessoas e precisam responder por cada decisão depois.
A caixa-preta do avião: ninguém lê durante o voo, e ela é a primeira coisa procurada quando algo dá errado.
Luana é gerente de compliance de uma fintech. O assistente financeiro do app agenda pagamentos com aprovação do cliente. Um cliente abriu uma reclamação no Banco Central: diz que nunca aprovou um Pix de R$ 2.300.
O time tinha traces de observabilidade, mas eles eram amostrados a 10%, ficavam guardados por 14 dias e não registravam o que o cliente viu na tela de aprovação. O log da aplicação dizia “pix agendado” e um id de usuário. Não havia como provar se o cliente aprovou, o que ele viu quando aprovou, nem por que o assistente sugeriu aquele pagamento.
Observabilidade serve para o time de engenharia entender o sistema. Auditoria serve para responder a um cliente, a um auditor ou a um juiz. As perguntas são diferentes, e o registro também.
A pergunta que este padrão responde: como guardar evidência suficiente para explicar e defender cada decisão que o sistema tomou?
Crie um registro de auditoria separado dos traces, com quatro propriedades:
A reclamação chega em abril. O Pix foi em janeiro. Com o registro certo, Luana busca action=schedule_pix e o id do cliente e monta a história:
event_id: a9f3...
ts: 2026-01-12T14:32:11Z
actor: { type: "user", id: "cli_8821" }
action: schedule_pix
prompt_version: finance-assistant@7
model: claude-sonnet-5-5
proposal: { amount_brl: "2300.00", dest_name: "Fornecedor X", dest_key_type: "cnpj" }
shown_to_human: { title: "Agendar Pix", amount: "R$ 2.300,00", dest: "Fornecedor X", key: "**.***.***/****-**" }
decision: approved
reviewer: cli_8821 (biometria no app)
execution: { status: "ok", external_id: "E0000000020260112..." }
input_ref: obj_91c2 (pedido original, em armazenamento separado)
Isso responde três perguntas do Banco Central. Quem pediu: o titular, autenticado. O que ele viu: valor, nome e chave mascarada. O que o banco executou: o id da transação bate com o Pix. Sem shown_to_human, sobra um “pix agendado” que não prova nada. Sem prompt_version, não dá para saber se o assistente daquela semana estava sugerindo o destinatário errado.
O trace de observabilidade daquele dia já tinha sido amostrado para fora e apagado na retenção de 14 dias. Auditoria não pode herdar essas duas propriedades.
Compare os dois registros:
| Pergunta | Trace | Evento de auditoria |
|---|---|---|
| Para quem | Engenharia, debug | Cliente, auditor, juiz |
| Amostragem | 10% é comum | Nenhuma nas ações com efeito |
| Retenção | Dias ou semanas | O período que a regulação pede |
| O que o humano viu | Em geral não guarda | Campo obrigatório |
| Dá para editar | Muitas vezes sim | Só acréscimo, hash encadeado |
flowchart LR
U(["1. Pedido do usuário"]):::io --> A(["2. Modelo propõe"]):::llm
A --> H((("3. Aprovação"))):::human
H --> X["4. Execução"]:::tool
U --> E["5. Montar evento"]:::tool
A --> E
H --> E
X --> E
E --> R["6. Mascarar e assinar"]:::tool
R --> L[("7. Registro só de acréscimo")]:::store
L --> Q{"8. Consulta e relatório"}
schema AuditEvent {
event_id: string, ts: datetime, actor: { type: "user" | "agent" | "reviewer", id: string },
action: string, input_ref: string, prompt_version: string, model: string,
proposal: any, rationale: string, shown_to_human: any?, decision: "approved" | "rejected" | "auto",
execution: { status: string, external_id: string? }, prev_hash: string, hash: string
}
function record(ctx, proposal, review, execution):
event = AuditEvent(
actor = ctx.user, action = proposal.tool, input_ref = store_input(ctx.input), # 1
prompt_version = ctx.prompt_version, model = ctx.model,
proposal = proposal.args, rationale = proposal.reason, # 2
shown_to_human = review.preview, decision = review.decision, # 3
execution = execution, # 4
)
event = redact_pii(event) # 6
event.prev_hash = audit_log.last_hash()
event.hash = sha256(canonical_json(event))
audit_log.append(event) # 7
Escreva o schema como uma lista de perguntas, não como uma cópia do span:
| Campo | Pergunta que ele responde |
|---|---|
actor |
Quem pediu? |
proposal |
O que o modelo queria executar? |
shown_to_human |
O que a pessoa viu na hora de aprovar? |
decision e reviewer |
Quem autorizou, e como? |
execution.external_id |
O que de fato saiu no banco, no email, no ERP? |
prompt_version e model |
Qual sistema estava no ar? |
input_ref |
Qual pedido original, sem copiar dado pessoal para o evento? |
A justificativa que o modelo escreve (rationale) ajuda quem lê daqui a seis meses. Não é prova. Prova é o que entrou, o que foi mostrado e o que saiu.
Cada evento guarda o hash do anterior e o próprio hash do conteúdo canônico, sem o campo hash. Se alguém editar um evento no meio, a cadeia quebra na verificação noturna. Isso não impede um administrador com acesso ao disco; impede a edição silenciosa na aplicação.
Gravações paralelas precisam de um único escritor por cadeia (fila, lock, ou uma cadeia por partição, por exemplo por actor.id). Sem isso, dois Pix no mesmo milissegundo apontam para o mesmo prev_hash e a verificação acusa conflito mesmo sem fraude.
rationale do modelo como prova. O modelo descreve o que ele acha que fez. O registro descreve o que o sistema fez.A trilha começa na refatoração Adicionar Trace. Trace primeiro, para o time ver o sistema. Auditoria em cima, para o que precisa sobreviver ao debug.
| Aspecto | O que acontece | Como aparece |
|---|---|---|
| Latência | Gravação síncrona soma milissegundos; assíncrona com fila não soma | duração da gravação no span da ação |
| Custo | Armazenamento por anos, maior quando guarda entradas completas | volume do registro por mês |
| Falha: evento faltando | Ação executada sem registro | conciliação entre execuções e eventos com diferença |
| Falha: dado pessoal exposto | Evento com dado sensível sem máscara | achado em auditoria interna |
| Falha: evento incompleto | Falta a prévia mostrada ou a versão do prompt | consulta que não consegue explicar a ação |
Grave o evento na mesma transação da execução, ou numa fila com entrega garantida e conciliação. Assíncrono sem conciliação é o jeito educado de perder o Pix que mais vai ser contestado.
Os números de armazenamento abaixo são ilustrativos. Suponha 5 mil Pix por dia, 2 KB por evento mascarado, e o pedido completo de 20 KB só no armazenamento separado.
| O que você guarda | Ordem de grandeza por ano |
|---|---|
| Só o evento mascarado | alguns gigabytes |
| Evento mais o pedido completo | dezenas de gigabytes |
Gigabytes por ano são baratos perto de não conseguir responder ao Banco Central. O custo que dói é dado pessoal demais no evento, não o disco.
{ "check": "reconstruct", "action_id": "pix-88123", "must_include": ["actor", "proposal", "shown_to_human", "decision", "external_id", "prompt_version"] }
O teste de reconstrução é o eval deste padrão. Uma pessoa do time jurídico, sem acesso ao código, tenta contar a história do Pix só com o evento. Se ela pergunta “mas o que o cliente viu?”, falta campo. Se ela pergunta “esse Pix saiu mesmo?”, falta external_id.
O registro concentra informação sensível. Separe dados pessoais em armazenamento próprio, com chave de acesso diferente, e guarde só referências no evento quando possível. Aplique Mascaramento de Dados Pessoais antes da gravação e defina retenção de acordo com a base legal.
| Prós | Contras |
|---|---|
| Responde a clientes, auditores e reguladores | Custo de armazenamento por anos |
| Mostra exatamente o que a pessoa aprovou | Mais um schema para manter junto com o sistema |
| Base para investigar incidentes e vieses | Concentra dados sensíveis |
| Adulteração fica detectável | Exige disciplina para registrar toda ação nova |
As duas abas gravam um evento autocontido, encadeado por hash, num armazenamento só de acréscimo.
Python. Use quando as ações rodam em Python e o registro vai para um banco com tabela só de inserção.
import hashlib
import json
import uuid
from datetime import datetime, timezone
def canonical(obj: dict) -> str:
return json.dumps(obj, sort_keys=True, ensure_ascii=False, separators=(",", ":"))
def record(db, ctx, proposal: dict, review: dict, execution: dict) -> dict:
prev = db.fetch_one("SELECT hash FROM audit_log ORDER BY seq DESC LIMIT 1")
event = {
"event_id": str(uuid.uuid4()),
"ts": datetime.now(timezone.utc).isoformat(),
"actor": {"type": "user", "id": ctx.user_id}, # 1
"action": proposal["tool"],
"input_ref": ctx.input_ref, # entrada guardada à parte
"prompt_version": ctx.prompt_version,
"model": ctx.model,
"proposal": proposal["args"], # 2
"rationale": proposal.get("reason", ""),
"shown_to_human": review.get("preview"), # 3
"decision": review["decision"],
"reviewer": review.get("reviewer"),
"execution": execution, # 4
"prev_hash": prev["hash"] if prev else "genesis",
}
payload = canonical(event)
event["hash"] = hashlib.sha256(payload.encode()).hexdigest() # 6
# 7. a tabela só tem permissão de INSERT para este serviço
db.execute("INSERT INTO audit_log (event_id, body, hash) VALUES (%s, %s, %s)",
(event["event_id"], payload, event["hash"]))
return eventO que você ganhou: cada evento conta a história inteira da ação e a cadeia de hashes denuncia edições. O que você perdeu: gravações concorrentes precisam de serialização (uma fila ou um lock) para a cadeia ficar correta.
TypeScript. Use quando as ações rodam em Node e você grava numa fila antes do armazenamento final.
import { createHash, randomUUID } from "node:crypto";
type AuditEvent = {
event_id: string; ts: string;
actor: { type: "user" | "agent" | "reviewer"; id: string };
action: string; input_ref: string; prompt_version: string; model: string;
proposal: unknown; rationale: string; shown_to_human: unknown;
decision: "approved" | "rejected" | "auto"; reviewer?: string;
execution: { status: string; external_id?: string };
prev_hash: string; hash?: string;
};
const canonical = (o: unknown): string =>
JSON.stringify(o, (_k, v) =>
v && typeof v === "object" && !Array.isArray(v)
? Object.fromEntries(Object.entries(v).sort(([a], [b]) => a.localeCompare(b)))
: v,
);
export async function record(
log: { lastHash(): Promise<string | null>; append(e: AuditEvent): Promise<void> },
e: Omit<AuditEvent, "event_id" | "ts" | "prev_hash" | "hash">,
): Promise<AuditEvent> {
const event: AuditEvent = {
...e,
event_id: randomUUID(),
ts: new Date().toISOString(),
prev_hash: (await log.lastHash()) ?? "genesis",
};
event.hash = createHash("sha256").update(canonical(event)).digest("hex"); // 6
await log.append(event); // 7. armazenamento só de acréscimo
return event;
}O que você ganhou: o tipo AuditEvent obriga a preencher prévia, decisão e execução. O que você perdeu: o mesmo cuidado com concorrência; serialize as gravações por um único escritor.
Trilha de Auditoria (Audit Log) é um padrão de projeto de IA para apps com LLM e agentes, do grupo Segurança e confiabilidade. Registrar quem pediu, o que o modelo viu, o que decidiu e o que executou.
Nos cenários reais Assistente financeiro com aprovação, Triagem de currículos com controle de viés e Assistente jurídico.