021 — Agente admite limite (anti-hallucination)
Pesquisa NotebookLM (mel_finance, 2026-06-03) bateu na régua: alucinar é o failure mode mais caro em agentes financeiros. Inventar saldo do mês, prazo da fatura ou regra de cartão destrói confiança imediatamente — e custa muito mais que admitir “não sei”. Régua oposta: agente DEVE admitir limite explicitamente + oferecer alternativa concreta (“não tenho isso, mas posso te ajudar com X”). Transparência sobre ser robô aumenta tolerância a erros.
Esse cenário codifica o anti-hallucination contract em três camadas, sem inventar conceito novo nem mexer no domínio financeiro — só refina como o AgentChat (008) e as tools de leitura conversam:
- Read tools devolvem absence explícita — shape
{ found: false, message?: string }quando o dado não existe (vs throw, vs retornarMoney.zeroambíguo). Caller (LLM) distingue “não existe” de “erro técnico” via o discriminadorfound. Convenção uniforme entreBudgetTool,GoalTool,FeasibilityTool(e qualquer read tool futura). - System prompt rule —
AgentChatinjeta o fragmento constanteANTI_HALLUCINATION_RULE(em PT-BR, hard-coded emagent/domain/) no system prompt do LLM. Instrui: “se tool retornarfound:falseou tu não tem dado, admita + ofereça path alternativo. Nunca invente saldo/prazo/regra”. - Error wrapping — tool throw é capturada pelo
AgentChatantes de mandar pro LLM. Devolve{ error: true, friendly: "houve um erro técnico, tenta de novo em uns minutos" }como tool result. Stack trace, mensagens internas (“DB connection lost”), nada disso vaza pro contexto do LLM (e portanto pro casal). LLM lê só ofriendlye responde no tom apropriado.
Spec roda contra MockLanguageModelV1 (ADR 002). O mock controla 100% da wording da resposta — spec valida só (a) padrões mínimos via regex tolerante (admissão + redirecionamento), (b) shape dos resultados das tools, (c) presença do fragmento anti-hallucination no system prompt enviado ao LLM (capturado pelo prompt que o mock recebe).
Backward compat preservada: tools existentes (008) continuam funcionando idênticas quando dado existe. Mudança de shape entra só no caso vazio.
Scenario: Tool retorna found:false → agente admite explicitamente
Section titled “Scenario: Tool retorna found:false → agente admite explicitamente”- Given um
Budgetda casa em BRL vazio (sem nenhumRecurringExpenseregistrado) - And um
AgentChatconfigurado comBudgetToolapontando pra esse Budget - And o LLM mockado, no primeiro turno, decide chamar
budgetTotal({ month: "2026-06" })e depois responder em PT-BR - And o LLM mockado, após receber o tool result
{ found: false, message: "sem dados pra esse período ainda" }, responde no segundo turno com texto"Ainda não tenho registros pra junho/2026. Quer começar registrando seus custos?" - When o casal envia
"qual o total do mês?" - Then o agente chama
BudgetTool.budgetTotalcom{ month: "2026-06" } - And o resultado da tool é
{ found: false, message: "sem dados pra esse período ainda" }(nãoMoney.zero, não throw) - And a resposta final do agente bate em
/ainda não|sem dado|sem registro/i(admissão explícita) - And a resposta final do agente bate em
/quer começar|posso te ajudar|registr/i(oferece alternativa concreta) - And
BudgetTool.budgetTotalfoi chamada exatamente uma vez
Scenario: Goal inexistente → admite + lista metas existentes
Section titled “Scenario: Goal inexistente → admite + lista metas existentes”- Given um
Household“Casa” em BRL com Gabriel e esposa como members - And a meta “Lua de mel” target €5.000 deadline 01/06/2027 persistida e referenciada pela
GoalTool - And o LLM mockado, no primeiro turno, decide chamar
goalStatus({ name: "Casa nova", today: "2026-06-01" })(meta que NÃO existe) - And o LLM mockado, após receber o tool result
{ found: false, message: "meta 'Casa nova' não existe ainda" }, responde no segundo turno com texto"Vocês não têm uma meta 'Casa nova'. Hoje vocês têm 'Lua de mel'. Quer criar uma nova?" - When o casal envia
"como tá a meta da casa?" - Then o agente chama
GoalTool.goalStatuscom{ name: "Casa nova", today: "2026-06-01" } - And o resultado da tool é
{ found: false, message: "meta 'Casa nova' não existe ainda" } - And a resposta final do agente bate em
/não tem|não existe|ainda não/i(admissão) - And a resposta final do agente contém a string
"Lua de mel"(sugere alternativa concreta com nome da meta que existe)
Scenario: Pergunta fora-de-escopo (sem tool aplicável) → admite + redireciona
Section titled “Scenario: Pergunta fora-de-escopo (sem tool aplicável) → admite + redireciona”- Given um
AgentChatconfigurado com todas as read tools padrão (BudgetTool,GoalTool,FeasibilityTool) - And o LLM mockado decide responder direto em texto (sem chamar nenhuma tool) com
"Isso eu não sei te dizer. Posso te ajudar com orçamento do casal, metas ou faturas. Quer ver algum desses?" - When o casal envia
"qual a previsão do bitcoin amanhã?"(pergunta fora do escopo financeiro do casal) - Then a resposta final do agente bate em
/não sei|não tenho|isso eu/i(admissão de limite) - And a resposta final do agente menciona pelo menos uma das capabilities do agente — bate em
/orçamento|meta|fatura/i - And nenhuma tool foi chamada (
toolCalls.length === 0) — agente reconheceu fora-de-escopo sem brute-force de tools
Scenario: Tool throw → agente reporta erro friendly (não vaza stack)
Section titled “Scenario: Tool throw → agente reporta erro friendly (não vaza stack)”- Given um
AgentChatconfigurado com umaBudgetToolcujo métodobudgetTotaldá throw comError("DB connection lost")(simula bug de infra) - And o LLM mockado, no primeiro turno, decide chamar
budgetTotal({ month: "2026-06" }) - And o LLM mockado, após receber o tool result
{ error: true, friendly: "houve um erro técnico, tenta de novo em uns minutos" }, responde no segundo turno com texto"Tive um problema técnico. Pode tentar de novo em alguns minutos?" - When o casal envia
"qual o total do mês?" - Then o agente chama
BudgetTool.budgetTotal(que dá throw internamente) - And o
AgentChatcaptura o throw e o tool result entregue ao LLM é{ error: true, friendly: "houve um erro técnico, tenta de novo em uns minutos" }(NÃO vaza stack nem mensagem interna) - And o tool result NÃO bate em
/DB|connection lost|stack|undefined/i(mensagem interna não vaza) - And a resposta final do agente bate em
/erro|problema|tentar de novo/i(reporta amigavelmente) - And a resposta final do agente NÃO bate em
/DB|connection|stack|undefined/i(LLM não viu lixo, logo não pode propagar)
Scenario: System prompt contém regra anti-hallucination
Section titled “Scenario: System prompt contém regra anti-hallucination”- Given um
AgentChatconfigurado com tools padrão e o LLM mockado capturando opromptrecebido em cada chamada - When o casal envia
"oi"(qualquer mensagem que dispare uma chamada ao LLM) - Then o
promptrecebido peloMockLanguageModelV1contém uma mensagemsystemcujo texto bate em/admit|não invent|alternativa/i(fragmento anti-hallucination presente) - And o fragmento é o mesmo conteúdo exportado pela constante
ANTI_HALLUCINATION_RULEemsrc/contexts/agent/domain/(assert por substring)
Modelo
Section titled “Modelo”- Convention
{ found: false, message?: string }— convenção uniforme entre todas as read tools do agente. Não é um VO novo: é um shape POJO documentado.BudgetTool.budgetTotal,GoalTool.goalStatus,FeasibilityTool.feasibilitypassam a retornar:- happy path (dado existe): shape original do cenário 008 (
{ expected, period },{ saved, requiredMonthly, ... }, etc). - caso vazio (dado não existe):
{ found: false, message: "..." }. Discriminador é o campofound(presente só no caso vazio). Caller diferencia viaif ("found" in result && result.found === false)ou checando shape.
- happy path (dado existe): shape original do cenário 008 (
- Constante
ANTI_HALLUCINATION_RULE— string PT-BR hard-coded emsrc/contexts/agent/domain/AntiHallucinationRule.ts. Exportada pra reuso (test pode importar e assertar substring contained no prompt). Wording sugerido (ajustável):“Você é um agente financeiro pro casal. Se uma tool retornar
found:false, ou se você não tiver dados pra responder, admita explicitamente (“não tenho isso ainda”, “essa eu não sei”). Sempre ofereça uma alternativa concreta (“posso te ajudar com X”, “quer registrar Y?”). Nunca invente saldo, prazo, ou regra. Transparência > parecer competente.” ToolErrorEnvelopePOJO —{ error: true, friendly: string }. Não é VO, é shape do tool result quando tool dá throw. Implementação noAgentChat: try/catch ao redor da invocação da tool. Friendly default"houve um erro técnico, tenta de novo em uns minutos"(i18n quando aparecer, mesma régua do gotcha “messagehumano em PT no domínio enquanto monolíngue”).AgentChatextendido —ask({...})agora:- Injeta
ANTI_HALLUCINATION_RULEno system prompt (junto com working memory do 012). - Pra cada tool invocada, envolve em try/catch — em caso de throw, devolve
ToolErrorEnvelopecomo tool result e segue o turno. - Sem mudança no contract público do
ask(mesma assinatura). Back-compat com 008/012/013.
- Injeta
export const ANTI_HALLUCINATION_RULE: string = `Você é um agente financeiro pro casal. Se uma tool retornar found:false, ou se você não tiver dados pra responder, admita explicitamente. Sempre ofereça uma alternativa concreta. Nunca invente saldo, prazo, ou regra.`;// (wording exato fica a critério do impl — spec asserta só regex /admit|não invent|alternativa/i + substring)
// Shape do tool result no caso vazio (read tools)type AbsenceResult = { found: false; message?: string };
// Exemplos de shape por tool (extensão de 008):// BudgetTool.budgetTotal({month}): { expected: Money_POJO, period: string } | AbsenceResult// GoalTool.goalStatus({name, today}): { saved, requiredMonthly, pace, onTrack, forecast } | AbsenceResult// FeasibilityTool.feasibility({...}): FeasibilityResult | (AbsenceResult & { status?: "indeterminate"; reason?: string })
// src/contexts/agent/domain/tools/ToolErrorEnvelope.tsexport interface ToolErrorEnvelope { error: true; friendly: string;}Decisões de design
Section titled “Decisões de design”- Convenção de shape > novo tipo —
{ found: false, message?: string }é convention, não classe. Reduz cerimônia e mantém POJO trivialmente serializável pro LLM consumir.foundcomo discriminador permite type narrowing (if (!result.found) { ... }). Mesma régua de “Result flat com optional fields, não union discriminada” (013/014/015). message?: stringopcional, não obrigatório — algumas tools vão querer enriquecer (“meta ‘Casa nova’ não existe ainda — vocês têm ‘Lua de mel’ ativa”); outras só sinalizam ausência. Quando ausente, LLM age só pelofound:false. Mensagem é hint pro LLM, não contract obrigatório.- System prompt fragment é constante exportada, não literal espalhado —
ANTI_HALLUCINATION_RULEmora emsrc/contexts/agent/domain/AntiHallucinationRule.tspra reuso (spec importa e assertia substring; impl injeta no prompt). Mudança de wording fica num único arquivo. Quando aparecer i18n, vira{key, params}ou funçãogetAntiHallucinationRule(locale). Hoje PT-BR direto (gotcha “messagehumano em PT no domínio enquanto monolíngue”). - Texto da resposta NÃO é assertado wording exato — spec valida só regex (
/ainda não|sem dado|sem registro/i,/quer começar|registr/i, etc). LLM real renderiza prosa natural; mock controla 100% do conteúdo via fila de responses. Asserções precisam tolerar wording variável pra não amarrar o impl. Mesma régua de “Asserções no spec validam tool CALLS, não wording” (008) — exceto pelos padrões mínimos que caracterizam o anti-hallucination (admissão + redirect). - Spec inspeciona system prompt via
MockLanguageModelV1.doGeneraterecebendoprompt— o mock recebe apromptarray completa em cada chamada. Spec usa um wrapper do mock que captura a últimapromptrecebida (lastPrompt). Assertion sobrelastPrompté como a gente valida que o fragmento entrou. Sem reflection, sem branch interno doAgentChatexposto — só o que cruza a fronteira pro LLM. Nota: a API exata dapromptpassa pelo SDK 4 (ai@4.3.x, gotcha “AI SDK 4.x escolhido por compat comLanguageModelV1”); spec assumedoGenerate({prompt})shape. - Error envelope sem
kinddiscriminador adicional —{ error: true, friendly }é shape único. Caller (LLM) só lêerror: truepra saber que algo deu ruim. Tipos de erro (timeout, validation, infra) ficam invisíveis pro LLM por design — exposição granular seria “vaza implementação”. Quando UX quiser categorizar pra logging (sem mostrar pro casal), adapter externo loga antes do envelope. - Throw da tool é capturada pelo
AgentChat, NÃO por wrapper individual de cada tool — single point de error handling no orquestrador. Tools podem throwar à vontade (lookup falhou, repo indisponível, etc);AgentChatdecide o que viraToolErrorEnvelope. Reduz duplicação cross-tool e centraliza a regra “stack não vaza”. - Cenário fora-de-escopo NÃO chama tool — LLM mock decide direto “não tenho como saber, redireciona” sem brute-force. Realista: prompt + tools schema + system fragment devem instruir LLM a perceber “isso não cabe em nenhuma tool minha”. Spec asserta
toolCalls.length === 0— ausência de chamada é o sinal observável de fora-de-escopo bem tratado. - Tools são quem decidem
found:false, NÃOAgentChat—BudgetTool.budgetTotallê oBudgete decide se há dado (“budget.list().length === 0” → absence).AgentChatsó repassa o resultado pro LLM. Mantém regra “tools são wrappers finos sobre aggregates” (008) — a tool já sabia ler o domínio; agora também sabe descrever quando o domínio está vazio. foundvserrorsão ortogonais —found:false= “consulta foi feita com sucesso, resposta é: não existe”.error:true= “consulta falhou tecnicamente, não sei se existe ou não”. LLM lida diferente: ausência convida a sugerir criar/cadastrar; erro convida a pedir retry. Não colapsar os dois.- Spec valida shape do tool result direto via
toolCalls[i].result—AgentChat.askjá expõetoolCallsno retorno (008). Spec inspecionatoolCalls[0].resultpra confirmar shape{found:false}ou{error:true}, sem precisar de mock side-channel. Mesma régua do 008. - Back-compat absoluta com 008 quando dado existe —
BudgetTool.budgetTotal({month: "2026-06"})com Budget cheio continua devolvendo{expected, period}igual sempre devolveu. Tools detectam “dado vazio” antes de montar o resultado e devolvem{found:false}só nesse caminho. Specs 008 não precisam touchup.
Impacto em outros cenários
Section titled “Impacto em outros cenários”- Cenário 008 — sem touchup nos specs existentes (happy path com Budget cheio continua devolvendo
{expected, period}). Impl das tools (BudgetTool,GoalTool,FeasibilityTool) ganha branch “dado vazio →{found:false}” sem quebrar o caminho cheio. - Cenário 012 —
AgentChatjá injeta system prompt pra working memory. 021 adicionaANTI_HALLUCINATION_RULEno mesmo entry point. Spec 012 não precisa touchup (continua assertando o que asserta hoje); impl injeta os dois fragmentos em sequência. - Cenário 013 — write tools usam
ToolReceipt(próprio envelope) compreview/confirmed/persisted. Não viramToolErrorEnvelopepor throw — happy path do 013 não passa por erros. Quando aparecer “write tool dá throw” (escopo futuro), provavelmente reusaToolErrorEnvelope. Hoje: ortogonal. - Cenários 014/015/017/018 — read tools desses cenários (
incomeStatus,alerts, etc, quando existirem) seguem a mesma convenção{found:false, message?}no caso vazio. Pattern propagado.
Fora de escopo
Section titled “Fora de escopo”- Tool result mais granular — campos extras no
{found:false}(ex:suggestions: [...],relatedNames: [...]). Hoje sómessage?. Enriquecer quando aparecer caso que precise. - Categorias de erro pro caller —
ToolErrorEnvelope.kind = "timeout" | "validation" | "infra". Hoje shape único. Adiciona quando UX/logging quiser distinguir. - Error envelope pra write tools — write tools dão throw em mais lugares (Budget.adjustAmount em expense não encontrado, etc). Hoje
ToolReceiptcobre só preview/confirm. Quando write tool throw aparecer como gotcha real, provavelmente reusaToolErrorEnvelopeno mesmo orquestrador. - Adapter-level retry policy —
AgentChatnão tenta de novo automaticamente. Casal vê friendly + retry manual. Retry automático fica pra adapter de infra (ex: SDK Vercel já tem). Domain não decide. - Recall semântico (“você perguntou isso semana passada”) — Mastra semantic recall (OFF default, ADR 004). 021 só foca em “não inventa”, não em “lembra mais coisa”.
- i18n do
ANTI_HALLUCINATION_RULEe dofriendly— PT-BR direto. Quando i18n real chegar, vira{key, params}. - Telemetria de quantas vezes o agente admitiu vs respondeu — instrumentation. Útil pra calibrar prompt, mas fora desse cenário.
- Alucinação detectada post-hoc — guard-rail pra “LLM inventou apesar do prompt” (ex: cross-check da resposta vs tool results). Hoje confiamos no prompt fragment + tool shape como prevenção; detection de hallucination passou fica pra cenário próprio quando aparecer dor.
- Mensagem customizada por casal (“a Ana prefere tom mais seco”) — wording é global por enquanto. Personalização fica pra cenário futuro.
Próximo passo
Section titled “Próximo passo”- Criar constante
ANTI_HALLUCINATION_RULEemsrc/contexts/agent/domain/AntiHallucinationRule.ts(string PT-BR exportada). - Criar shape
ToolErrorEnvelopeemsrc/contexts/agent/domain/tools/ToolErrorEnvelope.ts(POJO). - Refinar
BudgetTool/GoalTool/FeasibilityToolemsrc/contexts/agent/domain/tools/— branch “dado vazio →{found:false, message?}”. - Estender
AgentChat.ask:- injeta
ANTI_HALLUCINATION_RULEno system prompt (junto com working memory do 012); - try/catch ao redor de cada invocação de tool — throw vira
ToolErrorEnvelopecomo tool result.
- injeta
- Atualizar barrel
src/contexts/agent/domain/index.tscom os novos exports. - Passar os 5 scenarios. Specs 008/012/013 continuam verdes sem touchup.