A failure-taxonomy stress test of a real-time faithfulness verifier in voice financial RAG.
This repository contains the code, data, and experiment outputs for the paper, which asks whether the judge that gates every claim a voice assistant speaks about a company's SEC filings can be trusted in production. We compare two judges for the same task — real-time, claim-level faithfulness verification over 10-K/10-Q filings:
Both are stressed against a failure taxonomy of LLM judges: numerical sensitivity (swapped units, shifted magnitudes, flipped directions), self-preference across generator families, and prompt-style sensitivity (zero-shot / few-shot / chain-of-thought).
Key findings. The small NLI judge is more trustworthy than the LLM judge on every facet measured: claim-level F1 = 0.828 vs 0.602 against human labels, at roughly two orders of magnitude lower latency (~7 ms vs ~3.6 s per claim). The LLM judge's near-perfect adversarial sensitivity (99.7%) is an artifact of a calibration failure — it labels nearly every claim unfaithful, so its robustness does not reflect genuine discrimination. There is no self-preference in the LLM judge, and chain-of-thought prompting hurts rather than helps.
.
├── pyproject.toml # installable package (pip install -e .)
├── config.yaml # single source: models, thresholds, top_k, paths
├── src/faithfulvoice/ # the library package
│ ├── config.py # load_config() + constants + path layout
│ ├── evalio.py # JSONL load/save helpers
│ ├── pipeline.py # retrieval -> generation -> verification (async)
│ ├── ingest/ # cleaner, parser, metadata, chunker, qdrant
│ ├── retrieval/ # dense_sparse + graphrag + metadata_hybrid
│ ├── verification/ # verifier: verify_single + compute_metrics
│ ├── voice/ # stt (Moonshine), tts (Kokoro), pipeline, rag
│ ├── graph/ # entity extraction, Neo4j builder, checkpoint
│ └── modal/ # images, volumes, common (the Modal app)
│
├── experiments/ # runnable entrypoints e1..e11 + run.py + human_eval/
├── scripts/ # data-pipeline CLIs
├── tests/ # pytest suite
├── data/ # raw/, cleaned/, processed/, entities/, queries/, audio/
├── results/ # exp1..exp11 (experiment outputs)
└── docs/ # phase-by-phase implementation + result reporting
python -m venv venv && source venv/bin/activate # Windows: venv\Scripts\activate
pip install -e ".[dev]" # installs the faithfulvoice package + deps
docker compose up -d qdrant # vector DB (add `neo4j` for the GraphRAG ablation)
Config lives in config.yaml. API keys are read from environment variables
(GROQ_API_KEY, NVIDIA_API_KEY, HF_TOKEN, NEO4J_PASSWORD); none are stored
in this repository.
Three experiments instantiate the failure taxonomy. Each is self-contained:
a dataset-building step, one run per judge, and a chart step. Outputs land in
results/exp{9,10,11}/.
| Exp | Facet | Script | What it tests |
|---|---|---|---|
| E9 | Numerical sensitivity | e9_adversarial_perturb.py | Does the judge flag grammatically fluent but numerically wrong claims? Three perturbations: unit_swap, magnitude_shift, directional_flip. |
| E10 | Self-preference | e10_self_preference.py | Does the LLM judge favor outputs from its own model family? A generator-by-judge matrix over 8 generators. |
| E11 | Reasoning-chain sensitivity | e11_cot_ablation.py | Does "think step by step" improve verdicts, or hurt? Zero-shot direct vs few-shot direct vs chain-of-thought. |
Judges under test. FinFaithVerifier (fine-tuned HHEM, run via Modal) and
openai/gpt-oss-120b (LLM-as-judge, run via the Groq API). A DeBERTa NLI
verifier (cross-encoder/nli-deberta-v3-base, threshold 0.7) serves as the
NLI baseline.
# E9 — adversarial numerical perturbations
python experiments/e9_adversarial_perturb.py --dry-run
modal run experiments/e9_adversarial_perturb.py --judge-type nli --model finetuned_hhem
python experiments/e9_adversarial_perturb.py --judge-type llm --model openai/gpt-oss-120b
python experiments/e9_make_charts.py
# E10 — self-preference matrix
python experiments/e10_generate_dataset.py
modal run experiments/e10_self_preference.py --judge-type nli --model finetuned_hhem
python experiments/e10_self_preference.py --judge-type llm --model openai/gpt-oss-120b
python experiments/e10_make_charts.py --baseline-nli finetuned_hhem
# E11 — prompt-style ablation
python experiments/e11_cot_ablation.py --model openai/gpt-oss-120b
python experiments/e11_make_charts.py
Full protocol, run commands, and output schemas: docs/PHASE7_JUDGe_Workshop.md.
The judge operates in a live pipeline; E1–E8 build and evaluate the system it gates, and produce the human labels against which both judges are scored.
audio → Moonshine STT → retrieval (Qdrant: BGE dense + BM25 sparse, reranked, top_k=8)
→ generation (Llama-3.1-8B) → verifier (HHEM / DeBERTa-NLI, threshold 0.7)
→ Kokoro TTS → audio
| Exp | Script | What | Outputs |
|---|---|---|---|
| E1 | e1_generate_dataset.py | Audit set (queries -> RAG + verifier) | results/exp1/ |
| E2 | e2_generator_compare.py | Compare 8 generators | results/exp2/ |
| E3 | e3_verifier_ablation.py | 7 NLI verifiers vs human labels | results/exp3/ |
| E4 | e4_approach_compare.py | NLI vs HHEM / GPT-judge / RAGAS / SelfCheckGPT | results/exp4/ |
| E5 | e5_annotation_prep.py + human_eval/ | Human annotation -> gold labels (Fleiss' kappa) | results/exp5/ |
| E6 | e6_latency.py, e6_voice_modal.py | Latency overhead (text + voice) | results/exp6/ |
| E7 | e7_finetune.py | FinFaithVerifier (distill -> fine-tune -> eval) | results/exp7/ |
| E8 | e8_analysis.py | Synthesis: domain / claim-type / heatmap / correlation | results/exp8/ |
Human labels (results/exp5/majority_labels.jsonl) are the gold set used to
score both judges in E3/E4/E9/E11.
~953 queries over 10 companies (NVDA, AMD, INTC, AAPL, MSFT, GOOGL, META, AMZN,
TSLA, JPM) × 6 archetypes (Direct, False Premise, Multi-Hop, Temporal Comparison,
Qualitative, Out-of-Scope) × 5 domains (revenue, supply chain, risk factors,
guidance, litigation), grounded in SEC 10-K/10-Q filings.
Queries: data/queries_full_dataset/merged_queries.jsonl.
Each experiment writes to a matching results/exp*/ directory, included here as
reference output. To regenerate: install the package, start Qdrant
(docker compose up -d qdrant), set the required API keys, and run the matching
experiments/e*.py entrypoint (locally or via modal run).
All models are open-source and self-hostable (MIT / Apache-2.0 / permissive); no proprietary dependencies on the core path.
2 commits
HTML
99.7%
A failure-taxonomy stress test of a real-time faithfulness verifier in voice financial RAG.
This repository contains the code, data, and experiment outputs for the paper, which asks whether the judge that gates every claim a voice assistant speaks about a company's SEC filings can be trusted in production. We compare two judges for the same task — real-time, claim-level faithfulness verification over 10-K/10-Q filings:
Both are stressed against a failure taxonomy of LLM judges: numerical sensitivity (swapped units, shifted magnitudes, flipped directions), self-preference across generator families, and prompt-style sensitivity (zero-shot / few-shot / chain-of-thought).
Key findings. The small NLI judge is more trustworthy than the LLM judge on every facet measured: claim-level F1 = 0.828 vs 0.602 against human labels, at roughly two orders of magnitude lower latency (~7 ms vs ~3.6 s per claim). The LLM judge's near-perfect adversarial sensitivity (99.7%) is an artifact of a calibration failure — it labels nearly every claim unfaithful, so its robustness does not reflect genuine discrimination. There is no self-preference in the LLM judge, and chain-of-thought prompting hurts rather than helps.
.
├── pyproject.toml # installable package (pip install -e .)
├── config.yaml # single source: models, thresholds, top_k, paths
├── src/faithfulvoice/ # the library package
│ ├── config.py # load_config() + constants + path layout
│ ├── evalio.py # JSONL load/save helpers
│ ├── pipeline.py # retrieval -> generation -> verification (async)
│ ├── ingest/ # cleaner, parser, metadata, chunker, qdrant
│ ├── retrieval/ # dense_sparse + graphrag + metadata_hybrid
│ ├── verification/ # verifier: verify_single + compute_metrics
│ ├── voice/ # stt (Moonshine), tts (Kokoro), pipeline, rag
│ ├── graph/ # entity extraction, Neo4j builder, checkpoint
│ └── modal/ # images, volumes, common (the Modal app)
│
├── experiments/ # runnable entrypoints e1..e11 + run.py + human_eval/
├── scripts/ # data-pipeline CLIs
├── tests/ # pytest suite
├── data/ # raw/, cleaned/, processed/, entities/, queries/, audio/
├── results/ # exp1..exp11 (experiment outputs)
└── docs/ # phase-by-phase implementation + result reporting
python -m venv venv && source venv/bin/activate # Windows: venv\Scripts\activate
pip install -e ".[dev]" # installs the faithfulvoice package + deps
docker compose up -d qdrant # vector DB (add `neo4j` for the GraphRAG ablation)
Config lives in config.yaml. API keys are read from environment variables
(GROQ_API_KEY, NVIDIA_API_KEY, HF_TOKEN, NEO4J_PASSWORD); none are stored
in this repository.
Three experiments instantiate the failure taxonomy. Each is self-contained:
a dataset-building step, one run per judge, and a chart step. Outputs land in
results/exp{9,10,11}/.
| Exp | Facet | Script | What it tests |
|---|---|---|---|
| E9 | Numerical sensitivity | e9_adversarial_perturb.py | Does the judge flag grammatically fluent but numerically wrong claims? Three perturbations: unit_swap, magnitude_shift, directional_flip. |
| E10 | Self-preference | e10_self_preference.py | Does the LLM judge favor outputs from its own model family? A generator-by-judge matrix over 8 generators. |
| E11 | Reasoning-chain sensitivity | e11_cot_ablation.py | Does "think step by step" improve verdicts, or hurt? Zero-shot direct vs few-shot direct vs chain-of-thought. |
Judges under test. FinFaithVerifier (fine-tuned HHEM, run via Modal) and
openai/gpt-oss-120b (LLM-as-judge, run via the Groq API). A DeBERTa NLI
verifier (cross-encoder/nli-deberta-v3-base, threshold 0.7) serves as the
NLI baseline.
# E9 — adversarial numerical perturbations
python experiments/e9_adversarial_perturb.py --dry-run
modal run experiments/e9_adversarial_perturb.py --judge-type nli --model finetuned_hhem
python experiments/e9_adversarial_perturb.py --judge-type llm --model openai/gpt-oss-120b
python experiments/e9_make_charts.py
# E10 — self-preference matrix
python experiments/e10_generate_dataset.py
modal run experiments/e10_self_preference.py --judge-type nli --model finetuned_hhem
python experiments/e10_self_preference.py --judge-type llm --model openai/gpt-oss-120b
python experiments/e10_make_charts.py --baseline-nli finetuned_hhem
# E11 — prompt-style ablation
python experiments/e11_cot_ablation.py --model openai/gpt-oss-120b
python experiments/e11_make_charts.py
Full protocol, run commands, and output schemas: docs/PHASE7_JUDGe_Workshop.md.
The judge operates in a live pipeline; E1–E8 build and evaluate the system it gates, and produce the human labels against which both judges are scored.
audio → Moonshine STT → retrieval (Qdrant: BGE dense + BM25 sparse, reranked, top_k=8)
→ generation (Llama-3.1-8B) → verifier (HHEM / DeBERTa-NLI, threshold 0.7)
→ Kokoro TTS → audio
| Exp | Script | What | Outputs |
|---|---|---|---|
| E1 | e1_generate_dataset.py | Audit set (queries -> RAG + verifier) | results/exp1/ |
| E2 | e2_generator_compare.py | Compare 8 generators | results/exp2/ |
| E3 | e3_verifier_ablation.py | 7 NLI verifiers vs human labels | results/exp3/ |
| E4 | e4_approach_compare.py | NLI vs HHEM / GPT-judge / RAGAS / SelfCheckGPT | results/exp4/ |
| E5 | e5_annotation_prep.py + human_eval/ | Human annotation -> gold labels (Fleiss' kappa) | results/exp5/ |
| E6 | e6_latency.py, e6_voice_modal.py | Latency overhead (text + voice) | results/exp6/ |
| E7 | e7_finetune.py | FinFaithVerifier (distill -> fine-tune -> eval) | results/exp7/ |
| E8 | e8_analysis.py | Synthesis: domain / claim-type / heatmap / correlation | results/exp8/ |
Human labels (results/exp5/majority_labels.jsonl) are the gold set used to
score both judges in E3/E4/E9/E11.
~953 queries over 10 companies (NVDA, AMD, INTC, AAPL, MSFT, GOOGL, META, AMZN,
TSLA, JPM) × 6 archetypes (Direct, False Premise, Multi-Hop, Temporal Comparison,
Qualitative, Out-of-Scope) × 5 domains (revenue, supply chain, risk factors,
guidance, litigation), grounded in SEC 10-K/10-Q filings.
Queries: data/queries_full_dataset/merged_queries.jsonl.
Each experiment writes to a matching results/exp*/ directory, included here as
reference output. To regenerate: install the package, start Qdrant
(docker compose up -d qdrant), set the required API keys, and run the matching
experiments/e*.py entrypoint (locally or via modal run).
All models are open-source and self-hostable (MIT / Apache-2.0 / permissive); no proprietary dependencies on the core path.
2 commits
HTML
99.7%