DeepEvaldeepeval 4.2 · Python 3.9+
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
23 small wins to finish your pathNext lesson

Faithfulness: did it stick to what it read

Until now the bot answered from nothing. Most real bots read something first: a policy page, a help article, a row from a database. That changes what can go wrong, and what to measure.

python
POLICY = [
    "Order A17 shipped on 3 March by courier.",
    "Refunds are paid within 5 working days.",
]


def retrieve(question):
    asked = set(question.lower().strip("?").split())
    return [doc for doc in POLICY if asked & set(doc.lower().strip(".").split())]

Two policy lines and a search that keeps any line sharing a word with the question. It is a crude retriever, which is useful, because three lessons from now the metrics have to show which half of the bot gets things wrong.

Example
print(retrieve("Where is order A17?"))

One line came back for that question. What the bot read is as much a part of the test case as what it said, and it goes in a field of its own: retrieval_context.

An answer that stays with the documents

Example
from deepeval.metrics import FaithfulnessMetric
from deepeval.test_case import LLMTestCase
from pretend_judge import PretendJudge

question = "Where is order A17?"
grounded = LLMTestCase(input=question, actual_output="Order A17 shipped on 3 March.",
                       retrieval_context=retrieve(question))
faithfulness = FaithfulnessMetric(model=PretendJudge())
faithfulness.measure(grounded)
print(faithfulness.score, faithfulness.reason)

Faithfulness scores how much of the answer is supported by what was read. Everything the answer says is in the document it read, so it scores 1.

An answer that adds something

Example
invented = LLMTestCase(input=question, retrieval_context=retrieve(question),
                       actual_output="Order A17 shipped on 3 March. It arrives tomorrow.")
faithfulness.measure(invented)
print(faithfulness.score, faithfulness.reason)

The bot added a delivery date that no document mentions, and faithfulness still scores 1. This is the part that surprises people, and it is deliberate: the metric only lowers the score for a claim the documents contradict. A claim they merely do not mention is recorded as unknown, and unknown does not count against the answer.

Example
strict = FaithfulnessMetric(model=PretendJudge(), penalize_ambiguous_claims=True)
strict.measure(invented)
print(strict.score, strict.reason)

penalize_ambiguous_claims=True changes that, and now the invented sentence costs half the score. Which one you want depends on your bot: for a support bot that must not invent policy, the strict one is closer to what you mean.

An answer that contradicts them

Example
wrong = LLMTestCase(input=question, retrieval_context=retrieve(question),
                    actual_output="Order A17 was cancelled. We are sorry.")
faithfulness.measure(wrong)
print(faithfulness.score, faithfulness.reason)

Cancelled, when the document says shipped. The judge marks that claim as a contradiction, so half of the answer's claims are unfaithful and the score halves.

What the judge was asked

Faithfulness asks three questions. The facts in the documents, the claims in the answer, and then a verdict on each claim.

python
@answers("faithfulness.Truths")
def faithfulness_truths(prompt, judge):
    return {"truths": sentences(section(prompt, "Text"))}
python
@answers("faithfulness.Claims")
def faithfulness_claims(prompt, judge):
    return {"claims": sentences(section(prompt, "AI Output"))}
python
@answers("faithfulness.Verdicts")
def faithfulness_verdicts(prompt, judge):
    truths = section(prompt, "Retrieval Contexts")
    verdicts = []
    for claim in listed(prompt, "Claims"):
        found = share(claim, truths)
        if found == 1:
            verdicts.append({"verdict": "yes"})
        elif found > 0:
            verdicts.append({"verdict": "no", "reason": f"The context says otherwise about: {claim}"})
        else:
            verdicts.append({"verdict": "idk", "reason": f"The context does not mention: {claim}"})
    return {"verdicts": verdicts}

yes when every word that matters is in the documents, no when the claim is about something the documents cover and says something else, and idk when they are silent. The score is the share of claims that are not no.

The fourth question is the reason, which every judged metric asks last. This judge reads the score back out of the prompt rather than writing prose about it.

python
def read_back_score(prompt):
    score = re.findall(r"Score:\s*\n([\d.]+)", prompt)
    return f"The score is {score[-1]}." if score else "No score was given."
python
def sentences(text):
    return [s.strip() for s in re.split(r"(?<=[.!?])\s+", text) if s.strip()]
python
def listed(prompt, name):
    return ast.literal_eval(section(prompt, name) or "[]")
Faithfulness or hallucination
DeepEval also has a HallucinationMetric. It compares the answer with context, the documents you say are true, rather than retrieval_context, the documents your bot actually retrieved. Faithfulness is the one for a RAG bot, because it grades the bot on what it was given.
Try it yourself
  • Add a third policy line and an answer that mixes two of them.
  • Make the bot answer with a different courier and watch the verdict change from unknown to a contradiction.

Slow is fine. Stopping is the only problem.