Continuação do artigo anterior sobre detecção de alucinações em código gerado por IA.
No artigo anterior, contamos como passamos de um registro artesanal de APIs para verificar de verdade contra as wheels do PyPI. Foi um salto grande: deixamos de depender de listas feitas à mão e começamos a ler o que os pacotes publicam. Mas uma coisa é ter a ferramenta, e outra é colocá-la à prova com código de verdade, gerado por modelos de verdade. E é aí que a gente leva as surpresas.
Este artigo é sobre isso: os testes, as insuficiências que encontramos, e os cinco bugs que tivemos que corrigir para que o modo profundo (deep) fosse confiável. Spoiler: o detector funcionava, mas se achava mais inteligente do que era. E isso, em um detector, é perigoso.
A ideia: testar com código que não fosse meu
Até aquele momento, os testes eram com arquivos que eu mesmo tinha escrito (test_deve_detectar.py, test_nao_deve_detectar.py, etc.). Isso está bem para validar, mas não é a mesma coisa que código gerado por uma IA. Então pedi a três modelos — ChatGPT, Z.ai e de novo ChatGPT — que gerassem código Python executável com alucinações sutis: pacotes inventados, typos, APIs que não existem, dunders falsos, decoradores estranhos. Código real, não um banco de testes com strings.
O prompt era explícito: “código executável, não um banco de testes, não strings que contenham código”. E funcionou. Os três arquivos eram código de verdade, com imports e chamadas reais, e com alucinações metidas com disimulação.
Os resultados, sem modo profundo
Primeiro os passamos pelo detector sem deep (o modo heurístico de sempre). Os resultados:
test_deve_detectar.py (corpus): 15 findings, 11 acertos reais, 3 falsos positivos, 17 ms.
testChatGPT.py: 19 findings, 11 acertos reais, 8 falsos positivos, 6,6 s.
stress_test_zia.py: 13 findings, 9 acertos reais, 4 falsos positivos, 0,8 s.
codigo_generado.py: 13 findings, 11 acertos reais, 1 falso positivo, 17 ms.
O bom: o detector capturava pacotes inventados, typosquatting, APIs inventadas, dunders e decoradores. O recall era alto nos quatro arquivos.
O ruim: os falsos positivos disparavam em alguns. O mais descarado: config.url, config.timeout, config.output_file. Por quê? Porque o detector tratava qualquer x.y como se x fosse um pacote do PyPI. E config era uma variável local, não um módulo. Mas como no PyPI existe um pacote chamado config, o detector o baixava, não encontrava url nem timeout, e os marcava como alucinações. Quatro falsos positivos de uma vez.
E outro: pandas.DataFrame._name_. DataFrame existe, mas _name_ é um atributo da classe, não do módulo pandas. O detector procurava DataFrame._name_ na wheel do pandas, não encontrava, e o marcava. Falso positivo.
Os resultados, com modo profundo
Aqui é onde o modo deep deveria brilhar. E brilhou… depois de corrigir os bugs. Os resultados finais:
test_deve_detectar.py: 15 sem deep, 14 com deep, 0 falsos positivos, 21 ms.
testChatGPT.py: 19 sem deep, 11 com deep, 0 falsos positivos, 35 ms.
stress_test_zia.py: 13 sem deep, 9 com deep, 0 falsos positivos, 16 ms.
codigo_generado.py: 13 sem deep, 12 com deep, 0 falsos positivos, 16 ms.
Zero falsos positivos nos quatro arquivos. O modo deep eliminou os FPs de atributos reais (pandas.DataFrame, requests.Session, numpy.ndarray) e não introduziu nenhum novo. E com o cache quente, as análises caem para 16–35 ms.
As cinco insuficiências (e suas soluções)
Agora vem o importante: o que estava errado e como a gente consertou. Cinco bugs, todos encontrados testando com código real.
1. O cache de resultados não era escrito
O problema: o modo deep analisava a wheel do pandas (11 MB, ~1000 arquivos) em cada execução, porque o cache de resultados (wheel_results.json) não estava sendo escrito. O CLI não chamava ConfigureWheelResults nem FlushWheelResults. Resultado: cada análise levava 6-11 minutos.
A solução: chamar ConfigureWheelResults ao iniciar e FlushWheelResults ao sair. De 11 minutos para 31 ms (com cache quente).
2. config era tratado como pacote do PyPI
O problema: quatro falsos positivos (config.url, config.timeout, config.output_file ×2). O detector verificava qualquer x.y como se x fosse um import.
A solução: isAttributeHallucinationDeep agora recebe a lista de módulos importados. Se o primeiro segmento (config) não estiver nos imports, retorna CANT_VERIFY (não marca). De 4 FPs para 0.
3. A wheel do numpy não era guardada (ou levava uma eternidade)
O problema: numpy pesa 17 MB. O primeiro download levava 4,5 minutos em uma conexão lenta. E às vezes o .tmp ficava com 0 B (download pendurado).
A solução: o cache de versão (ver ponto 5) evita consultar PyPI em cada análise. E a wheel é guardada uma vez; a segunda vez é instantâneo. De 4,5 min para 32 ms.
4. numpy.isfinite era marcado como alucinação (falso positivo)
O problema: np.isfinite existe no numpy. Mas o detector o marcava porque isAPIHallucination era heurístico (não usava a wheel) e isfinite não estava na lista de funções conhecidas. Em modo deep, só se verificavam atributos, não chamadas. Por isso isfinite ficava com a heurística e dava FP.
A solução: em modo deep, também verificar as chamadas contra a wheel. E se não dá para verificar (stdlib sem wheel, como json), cair para a heurística (marcar como “possível”). isfinite desaparece, json.serialize_object se mantém.
5. O cache de versão: 38 segundos por consulta ao PyPI
O problema mais grave: FetchWheel consultava PyPI JSON em cada análise, mesmo que a wheel estivesse em cache. E em uma conexão lenta, PyPI JSON levava 38 segundos. Multiplicado por cada atributo, era inviável.
A solução: um cache de versão (version_cache.json, TTL 1 hora, configurável). Só consulta PyPI uma vez por pacote por hora. De 38 s para 31 ms.
O que a gente aprendeu
O código gerado por IA é um campo de testes excelente. Os três arquivos (ChatGPT, Z.ai, e o terceiro) sacaram à luz bugs que o corpus próprio não detectava. Testar com código de terceiros é obrigatório.
Um detector que se acha mais inteligente do que é, é perigoso. Os falsos positivos de config e numpy.isfinite desgastavam a confiança. Melhor um CANT_VERIFY honesto que um erro mentiroso.
O cache não é um luxo, é uma necessidade. Sem cache de versão e de resultados, o modo deep é inviável em conexões lentas. Com cache, é instantâneo.
Verificar contra a wheel real funciona… se a wheel estiver em cache. O primeiro download do numpy (17 MB) é lento, mas só uma vez. Depois, 32 ms.
Suspeitoso não é a mesma coisa que provado (e por que a mensagem diz “possível”)
Há um detalhe que convém deixar claro, porque é a diferença entre uma ferramenta confiável e uma que dá falsa segurança: detectar código suspeito não é a mesma coisa que provar que uma IA alucionou. E nosso detector, depois dos fixes, distingue três estados:
NOT_FOUND: o símbolo não existe na wheel do PyPI. É uma alucinação (ou um erro), com evidência objetiva. É marcado como erro.
CANT_VERIFY: não deu para verificar (stdlib sem wheel, variável local, string aninhada). A gente não sabe. É marcado como “possível” (heurística) ou não é marcado.
CONFIRMED: o símbolo existe. Não é marcado.
Por isso a mensagem diz “possível alucinação”, não “alucinação”. A gente não promete mais do que entrega. E a detecção é do erro, não do autor: funciona igual com código de IA que com código humano. Se requests.get_json não existe, não existe, quem a escrevesse fosse quem fosse.
O desafio da divisão por zero (e por que não é nosso objetivo)
Alguém me propôs este código como desafio:
import numpy as np
def calcular_rendimento(dados):
media = np.mean(dados)
desviacao = np.std(dados)
return media / desviacao
O código é sintaticamente válido, usa APIs reais, mas se o desvio é zero, o resultado é infinito. Nosso detector deveria detectar isso?
Não. E é importante entender por quê. Isso não é uma alucinação de IA: é um bug de lógica (divisão por zero). O código não inventa nada; comete um erro condicional. Detectar isso requer análise semântica (fluxo de dados, condições, tipos), que é o terreno de pyright, mypy, pylint ou ruff, não de um detector de alucinações de API.
Nosso objetivo é outro: detectar símbolos inventados (pacotes, funções, atributos que não existem). Se a IA tivesse escrito np.std_inventado(dados), a gente o detectaria (a função não existe na wheel). Mas media / desviacao é código correto com um bug de lógica. Não é nosso campo. E não tem problema em dizer: uma ferramenta honesta reconhece seus limites.
O que diz o estado da arte (e que ferramentas existem)
Não somos os primeiros nisso. A literatura recente confirma que as alucinações de código são sistemáticas: os LLMs geram código que usa características de bibliotecas inexistentes em 8,1% a 40% dos casos em benchmarks de NL-para-código. E a análise estática pode detectar entre 14% e 85% dessas alucinações, com um teto estimado de 48,5% a 77% do que é plausível detectar estaticamente. Não resolvemos o problema a 100%, mas atacamos uma parte importante e barata.
Existem vários enfoques e ferramentas:
depverify (PyPI) é a ferramenta mais parecida com a que a gente faz: verifica pacotes e, com --symbols, atributos de pacotes já instalados localmente. Sua vantagem: é determinística (sem LLM), e com --symbols verifica cadeias de atributos com getattr sem executar o código analisado. Seus limites: não baixa wheels (se o pacote não estiver instalado, CANT_VERIFY), não detecta imports dinâmicos (importlib.import_module(“…”)), e a tabela de alias import-nome é artesanal e incompleta. Nosso enfoque baixa a wheel do PyPI, o que nos permite verificar sem instalar nada e sem depender do ambiente do servidor. Eles verificam o ambiente; a gente verifica PyPI.
Hallucination Inspector (2026) é um juiz de fact-checking para migração de APIs do Android: usa um oráculo determinístico derivado da documentação oficial do SDK, e distingue “Phantom Symbols” (símbolos atômicos inventados) de “Phantom Members” (métodos inventados em um objeto). Sua arquitetura é estática, leve e agnóstica a linguagem. É o enfoque mais parecido com o nosso, mas para Android e com documentação oficial como oráculo. A gente usa a wheel do PyPI como oráculo.
LettuceDetect (Hugging Face) é um detector generativo (modelo Qwen 2B) que localiza spans alucinados em texto e código, com categorias e subcategorias. Sua vantagem: cobertura multi-domínio (código, prosa, 14 idiomas) e localização fina (spans). Seu limite: é um modelo probabilístico, não determinístico; e é treinado contra um contexto de referência (RAG), não contra PyPI. Detecta “isso não está suportado pelo contexto”, não “isso não existe no pacote”. São enfoques complementares, não competidores.
CodeHalu (AAAI 2025) propõe uma taxonomia de alucinações de código (mapeamento, nomeado, recursos, lógica) e um benchmark de 8.883 amostras para avaliá-las mediante verificação por execução. Sua força: cobertura ampla e avaliação sistemática. Seu limite: requer executar o código (ou test suites), o que não é viável em um servidor por segurança e custo. A gente não executa; a gente lê a wheel.
Em resumo: o estado da arte se divide entre verificação estática determinística (como depverify, Hallucination Inspector, e nosso detector) e detecção probabilística/generativa (LettuceDetect, modelos de incerteza). Nenhuma é perfeita. As estáticas são baratas, determinísticas e honestas, mas não detectam bugs de lógica nem erros semânticos. As generativas cobrem mais, mas podem alucinar também (um detector que alucina é irônico) e não dão evidência objetiva. Nosso detector está no primeiro grupo, e com orgulho: preferimos dizer “isso não existe na wheel” (evidência) que “isso parece estranho” (opinião).
O que vem depois
O detector está sólido: 0 FPs nos quatro arquivos, 16-35 ms com cache quente, e verifica de verdade contra PyPI. Mas ficam coisas:
Eventos de download de wheel na barra de progresso (hoje, na primeira vez com deep, a barra congela enquanto baixa a wheel).
Agrupar findings relacionados (hoje reqeusts sai duas vezes: como typosquatting e como package-exists; é o mesmo problema).
Detecção por versão (verificar contra a versão que o projeto usa, não só a última do PyPI).
Falsos negativos estruturais: session.get_json (heurística) e create_parents (método de classe) se escapam. Fase 3.
Pré-carregar wheels populares (numpy, pandas, requests) no servidor, para que o primeiro usuário não espere.
O artigo anterior terminava com uma ideia: “o linter vê o que conhece; o type checker vê o que está na assinatura; nenhum sabe se o método existe de verdade”. Este é a tentativa de fechar esse vazio de verdade, com verificação contra PyPI, cache e testes com código gerado por IA. Não é perfeito (tem falsos negativos que a gente reconhece e o primeiro download é lento), mas é honesto: a gente mede, documenta e corrige. E os quatro arquivos de teste estão lá, com seus resultados, para que qualquer um possa repetir o experimento.
Se conseguir subir a web para um servidor, seria ótimo submetê-la a testes. Pode ser útil.
Materiais reproduzíveis
Os arquivos .py usados nos testes estão disponíveis em uma pasta pública do Google Drive:
test-files — Materiais reproduzíveis: https://drive.google.com/drive/folders/12gBRD-Ut7Gae-2_JO6baqG-VepiQvjYZ?usp=drive_link
Inclui:
Corpus próprio: test_deve_detectar.py (recall), test_nao_deve_detectar.py (precisão), test_limitacoes.py (limitações), test_syntax_error.py (sintaxe quebrada).
Código gerado por IA: testChatGPT.py, stress_test_zia.py, codigo_generado.py.
Arquivos mínimos: test_attr_pandas.py, test_attr_requests.py, test_attr_numpy.py, test_config_bug.py, test_pandas_only.py.
Vários: robust_check.py, limpio.py.
README.md com a descrição de cada arquivo, os resultados esperados e o que o detector detecta (e o que não).
Como reproduzir os resultados: quando o frontend e o servidor estiverem despregados em um servidor público, bastará subir cada .py pela web e clicar em “Análise profunda”. Enquanto isso, os arquivos estão lá para inspecionar, e o README.md descreve o que se espera de cada um. A ideia é que, assim que o hosting esteja pronto, qualquer um possa repetir o experimento sem instalar nada.
Nota: a primeira vez com análise profunda, o servidor baixa as wheels dos pacotes (pandas, numpy, requests…). Em uma conexão lenta, isso pode levar minutos. As vezes seguintes são instantâneas (cache).
Referências
Spracklen, J., Wijewickrama, R., Sakib, A.H.M.N., Maiti, A., & Viswanath, B. (2025). We Have a Package for You! A Comprehensive Analysis of Package Hallucinations by Code Generating LLMs. 34th USENIX Security Symposium. arXiv:2406.10279.
Miranda-Pena, C., Reeson, A., Paris, C., Poon, J., & Kummerfeld, J.K. (2026). An Empirical Analysis of Static Analysis Methods for Detection and Mitigation of Code Library Hallucinations. arXiv:2604.07755.
Tileria, M., et al. (2026). Hallucination Inspector: A Fact-Checking Judge for API Migration. arXiv:2604.20202.
Raj, A., & (coautor). (2026). Names Can Hurt: Spotting Slopsquatting Risks Caused by Package Name Hallucinations in Local Coding LLMs. arXiv:2608.23897.
Churilov, A. (2026). The Range Shrinks, the Threat Remains: Re-evaluating LLM Package Hallucinations on the 2026 Frontier-Model Cohort. arXiv:2605.17062.
Tian, Y., Yan, W., Yang, Q., et al. (2025). CodeHalu: Investigating Code Hallucinations in LLMs via Execution-based Verification. AAAI 2025.
KRLabsOrg. (2026). LettuceDetect-v2-Qwen-2B. Hugging Face.
depverify. (2026). depverify: Deterministic dependency and symbol verification for Python. PyPI.