Fix with your own test
You may already test your LLM's answers: a function you wrote, a DeepEval metric, a Ragas
score. Pass that test to fix() and counterfix uses it to judge every re-run:
import counterfix
def mentions_refund_window(record):
return "30 days" in record.answer_text
result = counterfix.fix(response, check=mentions_refund_window)
print(result.explain())
Who decides what
| Question | Who answers it |
|---|---|
| Did this call fail? | Your check |
| Why did it fail? | counterfix's diagnosis (F1 to F14) |
| Which change to try? | counterfix, from the cause it found |
| Did a re-run pass? | Your check, and no new failure (a cut-off answer, broken JSON...) may appear |
| Is the change really better than retrying? | counterfix's statistics: it must beat a plain retry |
So your tool keeps deciding what "good" means, and counterfix adds what your tool does not do: it finds the cause, tries one change at a time, and proves the change works.
What happens in each case
- Your check passes on the call: nothing is re-run;
stoppedsays "your check passes on this call: nothing to fix". - Your check fails and counterfix finds a cause: the normal search runs, with your
check as the pass test.
explain()showspass test: your check <name>. - Your check fails but counterfix finds no cause: nothing is re-run, and
stoppedsays so. A targeted change needs a cause; counterfix will not guess one. Without a judge, only the live checks ran: passjudge=...(or setGROQ_API_KEYorGEMINI_API_KEY) so facts and premises are checked too. - Your check raises on the original call: you get a
CheckErrorwith your error attached, because that is a bug in the check. If it raises on a re-run, that run counts as a fail and the error is shown inexplain().
You do not need a judge to confirm a fix when you pass a check: your check is the judge.
What a check returns
A check takes the CallRecord of an answer (record.answer_text, record.prompt_text,
record.messages, ...) and returns one of:
TrueorFalse;- a pair
(passed, reason), and the reason is shown when the check fails; - an object with a boolean
.passed; - or pass a counterfix
Metricobject itself, with a threshold and no reference needed.
A score is not a verdict: return score >= 0.8, not 0.8. counterfix refuses a bare
number, so a check that forgets the threshold never counts as a pass.
With DeepEval
from deepeval.metrics import AnswerRelevancyMetric
from deepeval.test_case import LLMTestCase
relevancy = AnswerRelevancyMetric(threshold=0.7)
def relevant(record):
case = LLMTestCase(input=record.prompt_text, actual_output=record.answer_text)
relevancy.measure(case)
return relevancy.is_successful(), relevancy.reason
result = counterfix.fix(response, check=relevant)
With Ragas
from ragas.dataset_schema import SingleTurnSample
from ragas.metrics import Faithfulness
faithfulness = Faithfulness(llm=my_ragas_llm)
def faithful_to(chunks):
def check(record):
sample = SingleTurnSample(
user_input=record.prompt_text,
response=record.answer_text,
retrieved_contexts=chunks,
)
score = faithfulness.single_turn_score(sample)
return score >= 0.8, f"faithfulness {score:.2f}"
return check
result = counterfix.fix(response, check=faithful_to(retrieved_chunks))
Both run an LLM to score, so every re-run costs one scoring call too. Keep max_runs
(30 by default) in mind.
From the command line
Put the check in a file and name it with --check:
# checks.py
def mentions_refund_window(record):
return "30 days" in record.answer_text
counterfix fix calls.jsonl --check checks.py:mentions_refund_window --caller groq:qwen/qwen3.8-27b
With --check, your check picks the failed calls in the log (calls it passes are
skipped), and the run reports which of them got a verified fix.
For a whole dataset, use counterfix.evaluate(..., metrics=[YourMetric()], fix=N): see
Evaluate a dataset for writing a Metric.
Have a DeepEval, Ragas or Promptfoo results file already? Pass it to counterfix fix
directly: see DeepEval, Ragas and Promptfoo.