MACI: Multi-LLM Adaptive Conformal Inference
This repository packages the code of MLAI-Yonsei/MACI as an installable library and command-line tool.
MACI filters the claims in an LLM response so that, with a user-chosen probability 1 - alpha, none of the claims it keeps is false. Each claim's factuality score is an ensemble of several LLM scorers, and the filter is calibrated separately for each group of prompts.
Abstract
Ensuring factuality is essential for the safe use of Large Language Models (LLMs) in high-stakes domains such as medicine and law. Conformal inference provides distribution-free guarantees, but existing approaches are either overly conservative, discarding many true-claims, or rely on adaptive error rates and simple linear models that fail to capture complex group structures. To address these challenges, we reformulate conformal inference in a multiplicative filtering setting, modeling factuality as a product of claim-level scores. Our method, Multi-LLM Adaptive Conformal Inference (MACI), leverages ensembles to produce more accurate factuality-scores, which in our experiments led to higher retention, while validity is preserved through group-conditional calibration. Experiments show that MACI consistently achieves user-specified coverage with substantially higher retention and lower time cost than baselines.
Installation
Requires Python 3.9 or newer and git. The package is not on PyPI yet, so install it from GitHub:
pip install "git+https://github.com/MLAI-Yonsei/maci-conformal.git"
To also run the CCI (conditional conformal) baseline, which needs PyTorch:
pip install "maci-conformal[cci] @ git+https://github.com/MLAI-Yonsei/maci-conformal.git"
These install the latest main. To pin a release, add the tag at the end of the URL, for example ...maci-conformal.git@v0.1.0.
To work on the code:
git clone https://github.com/MLAI-Yonsei/maci-conformal.git
cd maci-conformal
pip install -e ".[cci,dev]"
Check the install with maci info. It lists dependency versions, the data folder, and whether CCI is available.
Two ways to use it
| You want to… | Commands |
|---|---|
| Reproduce the paper's benchmark (no API key needed) | maci data download → maci run → open report.html |
| Filter hallucinated claims out of your own LLM answers | maci models → maci calibrate → maci check |
Reproduce the benchmark
maci data download # WikiBio, ~36 MB, checked against pinned SHA-256 hashes
maci run # BCI, CCI and MACI over 10 random splits
Data goes to ~/.cache/maci/data by default. Set MACI_DATA_DIR or pass --data-dir to use another folder. maci data status shows which files are present and intact.
Each run writes one folder, maci_outputs/<time>/:
| File | What it holds |
|---|---|
report.html |
Open in a browser: coverage and retention per method and subgroup, plus every answer with each claim marked as kept or removed and true or false. Missed hallucinations (false claims a method kept) are red, and a filter shows only those answers. |
predictions.jsonl |
One line per answer and method: every claim with its label, score, each LLM's score and whether it was kept. |
results.json |
Per-run coverage and retention, overall and per subgroup. |
config.toml |
The exact settings of the run. Rerun it with maci run -c config.toml. |
log.txt |
The full log. |
To browse the same results in the terminal:
maci inspect maci_outputs/<time> --method MACI --only-missed --show-model-scores
Common variations:
maci run --methods MACI --alpha 0.05 --n-runs 3
maci run --conditional-groups false_claim_risk view_count --target-tpr 0.9
maci run --help # every option
MedLFQA: only the MedLFQA score files are published. The claim dataset (
med_scores/medlfqa_dataset.pkl) is not, so--dataset medlfqaworks only if you supply that file yourself.
Check your own answers with OpenRouter models
MACI needs per-claim scores from several LLMs. The package gets them through OpenRouter, so any model listed there can be a scorer.
1. Get a key and pick models. Searching models needs no key:
export OPENROUTER_API_KEY=sk-or-... # https://openrouter.ai/keys
maci models llama 70b # id, context size, $ per million tokens
maci models --sort price qwen
2. Calibrate. Each model scores claims whose true/false labels are known, and MACI learns how far it can trust those scores:
maci calibrate --models meta-llama/llama-3.3-70b-instruct qwen/qwen-2.5-72b-instruct --alpha 0.1
This uses the first 500 WikiBio answers by default, downloading them if needed. Before sending anything it shows the number of requests and a cost estimate and asks you to confirm. Two models on 500 answers cost roughly $0.20 at current prices. Replies are cached in ~/.cache/maci/llm, so rerunning costs nothing. The result is maci_calibrator.json, together with a coverage and retention check on held-out answers.
To calibrate on your own labelled data, which is the better choice when your questions are not biographies, pass a .jsonl file with one answer per line:
{"question": "What does aspirin do?", "answer": "...", "claims": [{"text": "Aspirin is an NSAID.", "is_supported": true}, {"text": "Aspirin was discovered in 2001.", "is_supported": false}]}
maci calibrate --input my_labelled.jsonl --models <model ids...>
3. Check new answers. Claims are extracted by an LLM, scored by the calibrated models, and filtered:
maci check -q "Who was Marie Curie?" -a "Marie Curie won two Nobel Prizes. She was born in Vienna."
maci check --input answers.jsonl --output checked.jsonl # many at once; claims optional
Example output (scores depend on the models):
[1] Who was Marie Curie?
MACI [group=low]: kept 1/2 claims
✓ kept 0.912 ? Marie Curie won two Nobel Prizes.
- removed 0.204 ? Marie Curie was born in Vienna.
Filtered answer: Marie Curie won two Nobel Prizes.
Add --show-model-scores to see what each LLM said, or --json for machine-readable output. If the input has labels, check also reports coverage on them.
What the guarantee means. With alpha = 0.1, at least 90% of answers keep only true claims, on average over answers like the calibration data. It holds when new answers resemble the calibration answers and are scored the same way. So check reuses the models, prompt and temperature stored in the calibrator file, and you should calibrate on data like yours. An answer whose group had no calibration data gets all of its claims removed, and the output says so.
To benchmark new models the same way as the paper's, score the benchmark claims and point maci run at the scores:
maci score --models openai/gpt-4o-mini meta-llama/llama-3.3-70b-instruct --limit 500
maci run --scores-dir maci_scores --models openai/gpt-4o-mini meta-llama/llama-3.3-70b-instruct --limit-samples 500
Configuration
Settings come from three places. Later ones override earlier ones:
- built-in defaults
- a TOML config file:
--config FILE, or./maci.tomlif it exists - command-line flags
maci config init # writes a commented maci.toml
maci config show --alpha 0.2 # prints the effective settings after merging
maci run --config maci.toml
The main settings:
| Section | Key | Default | Meaning |
|---|---|---|---|
[data] |
dataset |
"auto" |
wikibio, medlfqa, or auto |
data_dir |
$MACI_DATA_DIR or ~/.cache/maci/data |
benchmark files | |
scores_dir |
none | extra score files, e.g. from maci score |
|
limit_samples |
2000 |
maximum number of answers to load | |
[experiment] |
methods |
["BCI", "CCI", "MACI"] |
methods to run (CCI is skipped if PyTorch is missing) |
alpha |
0.1 |
target miscoverage (coverage ≥ 1 − alpha) | |
n_runs, random_seed |
10, 123 |
repeated splits; run i uses seed random_seed + i |
|
calib_ratio, test_ratio |
0.75, 0.25 |
split sizes | |
conditional_groups |
all three | false_claim_risk, medical_content, view_count |
|
[maci] |
models |
the paper's 3 scorers | OpenRouter ids of the ensemble members |
target_tpr |
0.8 |
true-positive target for the ensemble weights | |
n_bins |
3 |
calibration groups per grouper | |
[bci] |
score_type |
"frequency" |
frequency, selfeval, logprob, ensemble |
[cci] |
boosting_epochs |
100 |
|
adaptive_alpha |
false |
per-sample adaptive alpha | |
[llm] |
base_url |
OpenRouter | any OpenAI-compatible endpoint, e.g. a local vLLM server |
api_key_env |
"OPENROUTER_API_KEY" |
environment variable that holds the key | |
extractor_model |
first scorer | model that splits answers into claims | |
max_workers, cache |
8, true |
parallel requests; disk cache of replies | |
scoring_prompt, extraction_prompt |
built-in | custom prompt templates | |
[output] |
output_dir |
"maci_outputs" |
each run gets a folder here |
report, save_predictions, report_samples |
true, true, 200 |
HTML report and per-claim file |
Typos and wrong types are rejected with a pointer to the fix, for example Unknown key in [experiment] 'alhpa' (did you mean 'alpha'?) or maci.models must be a list. Relative paths in a config file are resolved against the folder that contains it. True/false flags come in pairs, for example --time-profile and --no-time-profile.
Using MACI from Python
Each input item is a dict holding the sample and one per-claim score array per scorer. Scores are in [0, 1], higher meaning more likely true:
entry = {
"sample": {
"prompt": "Tell me about aspirin.",
"response": "...",
"atomic_facts": [
{"atom": "Aspirin is an NSAID.", "is_supported": True}, # labels only needed for calibration
{"atom": "Aspirin was discovered in 2001.", "is_supported": False},
],
},
"scores": {
"gpt-x": [0.93, 0.12], # one score per atomic fact
"llama-y": [0.88, 0.31],
},
}
Calibrate on labelled data, then filter new responses, which need no labels:
from maci_conformal import SubgroupOptimizedMACI, FalseClaimRiskGrouper
maci = SubgroupOptimizedMACI(
model_names=["gpt-x", "llama-y"],
grouper=FalseClaimRiskGrouper(), # or your own ConditionalGrouper
random_state=0,
)
maci.fit(calibration_entries, alpha=0.1)
filtered, retention = maci.predict(new_entries)
item = filtered[0]
item["sample"]["filtered_claims"] # the claims that were kept
item["sample"]["kept_mask"] # True/False per claim, in the original order
item["sample"]["claim_scores"] # ensemble score per claim
item["group"] # which calibration group the answer fell into
maci.save("maci_calibrator.json") # reload with SubgroupOptimizedMACI.load(...)
To score claims with OpenRouter from Python, use LLMClient, score_claims and extract_claims, or score_entries and check_answers from maci_conformal.pipeline.
To group prompts differently, subclass ConditionalGrouper and implement compute_values(samples) -> np.ndarray, which returns one number per sample. MACI splits samples into n_bins groups by quantiles of that value and calibrates each group separately.
Other building blocks:
MACIAdaptiveConformal(score_function): the single-group MACI filter with any score functionBasicConformal(score_function): split-conformal baseline (keeps claims scoring above the threshold)maci_conformal.conditional: the CCI baseline ([cci]extra)run_experiments(config)andload_dataset(...): the benchmark runner used bymaci run
Reproducing the paper environment
requirements.txt pins the exact versions used for the paper (Python 3.9). The original entry point still works from a clone. It forwards to maci run and uses the repository's data/, logs/ and analysis/experiment_results/ unless a config file or flag says otherwise. Results stay identical to the original scripts.
pip install -r requirements.txt
python experiments/run_experiment.py --dataset-type wikibio --conditional-groups false_claim_risk
Development
pip install -e ".[cci,dev]"
pytest
python -m build && twine check dist/*
Releasing to PyPI
- One-time setup: on PyPI, add a trusted publisher for project
maci-conformal, repositoryMLAI-Yonsei/maci-conformal, workflowpublish.yml, environmentpypi. GitHub creates thepypienvironment on the first run. You can add required reviewers to it in the repository settings. - Bump
__version__insrc/maci_conformal/__init__.py. - Create a GitHub Release with tag
v<version>, for examplev0.2.0. ThePublish to PyPIworkflow builds the package, checks that the tag matches the version, and uploads it.
CCI Attribution
Our implementation of the Conditional Conformal Inference (CCI) baseline is a direct adoption of the work from the conformal-safety repository. The installable package uses the published conditionalconformal library. For full reproducibility, a local copy of the original modules is kept in the conditional-conformal/ directory. That code is not the work of the MACI project. For details, see the original repository: conformal-safety
Metadata
Release files for maci-conformal 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| maci_conformal-0.2.0.tar.gz | 84.2 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| maci_conformal-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 160.5 kB
Release files / maci_conformal-0.2.0.tar.gz
| Download URL | maci_conformal-0.2.0.tar.gz |
|---|---|
| Size | 84.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2027d6f69069eb8b910aa29181f5206209e443fb7dc4a3bcea490a7003eb1c02
|
|
BLAKE2b-256 checksum How to use checksums |
398d50c5b62c6ad8912ace93a21147b73f6f43f13f30ad482ba7d94a16906753
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.
Transparency logRelease files / maci_conformal-0.2.0-py3-none-any.whl
| Download URL | maci_conformal-0.2.0-py3-none-any.whl |
|---|---|
| Size | 76.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b571337757cebb9af30aa1fcc0dba996f5e304e0db8b4c669e02bc3a585d3b75
|
|
BLAKE2b-256 checksum How to use checksums |
083b0c6774bca97f72f69a01b335872b2838c3cff25e29dc1a6cde79a980445a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.
Transparency logSecurity for maci-conformal 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for maci_conformal-0.2.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.