Check an AI summary sentence by sentence against the source
Break an AI summary into individual claims, link each claim to specific source sentences, and mark unsupported or overstated statements before reuse. A small synthetic document shows why fluent summaries still need evidence checks.
Content checked 2026.09.20Hand-checked example
Show contents
Who this is forThis guide is for people who use AI to summarize reports, notes, or research material and want a traceable sentence-by-sentence review.
What you need
Python 3.12 and a terminal command that starts that version.
A text editor that can save UTF-8 text and CSV files.
A working folder where the script can create a new folder beneath outputs.
Only the Python standard library is required: csv and pathlib.
01Treat every summary sentence as a claim
An AI summary can be grammatically clean while still changing a cause, dropping a limitation, or adding a conclusion that the source never made. A useful review therefore asks a simple question for every summary sentence: which exact part of the source supports this statement?
This tutorial uses three verdicts. SUPPORTED means the source directly supports the sentence. PARTIAL means only part of the sentence is supported or important qualification is missing. UNSUPPORTED means the source does not support the claim or contradicts it.
02Create a synthetic source document and AI summary
The following source and summary are synthetic and were written specifically for this article. Save the first block as source.txt and the second as ai_summary.txt.
text
S1|The pilot processed 20 test cases.
S2|Eighteen cases completed successfully.
S3|Two cases stopped during input validation before the solver started.
S4|Among the 18 completed cases, the median runtime was 42 seconds.
S5|The test was performed on one workstation under the same software configuration.
S6|No production deployment or long-term reliability test was performed.
text
A1|The pilot processed 20 cases, and 18 completed successfully.
A2|Two cases failed because the solver crashed.
A3|The median runtime for completed cases was 42 seconds.
A4|The results show that the automation is reliable enough for production use.
The source contains 6 sentences. The AI summary contains 4 sentences. The values are intentionally small enough to verify without software.
03Compare each summary sentence with the source
A1 is supported by S1 and S2. A2 is contradicted by S3: the source says the two cases stopped during input validation before the solver started, not because of solver crashes. A3 is directly supported by S4. A4 goes beyond the evidence because S6 explicitly says no production deployment or long-term reliability test was performed.
Summary ID
Verdict
Evidence
Reason
A1
SUPPORTED
S1;S2
The case count and successful completions match the source.
A2
UNSUPPORTED
S3
The source attributes the stops to input validation before solver start.
A3
SUPPORTED
S4
The 42-second median is stated for the 18 completed cases.
A4
UNSUPPORTED
S6
Production readiness was not tested.
The expected review therefore contains 2 supported sentences and 2 unsupported sentences. None of the four synthetic sentences requires the PARTIAL label.
04Record the review in a traceable CSV
Save the following as summary_review.csv. The evidence column contains source sentence IDs rather than copied paragraphs, which keeps the review compact while preserving traceability.
csv
summary_id,verdict,evidence,reason
A1,SUPPORTED,S1;S2,The case count and successful completions match the source.
A2,UNSUPPORTED,S3,The source attributes the stops to input validation before solver start.
A3,SUPPORTED,S4,The 42-second median is stated for the 18 completed cases.
A4,UNSUPPORTED,S6,Production readiness was not tested.
This review remains a human judgment. The next script does not decide whether a claim is true. It checks whether every AI summary sentence received a valid verdict and whether every cited source ID actually exists.
05Check that every sentence was reviewed
Save the following script as check_summary_review.py. It reads the source, summary, and completed review CSV, verifies IDs and verdicts, and writes a new validated report without modifying any input file.
python
import csv
from pathlib import Path
SOURCE = Path("source.txt")
SUMMARY = Path("ai_summary.txt")
REVIEW = Path("summary_review.csv")
OUTPUT_DIR = Path("outputs") / "summary_source_check"
OUTPUT = OUTPUT_DIR / "validated_review.csv"
VALID_VERDICTS = {"SUPPORTED", "PARTIAL", "UNSUPPORTED"}
def read_id_text(path: Path) -> dict[str, str]:
result = {}
with path.open("r", encoding="utf-8") as stream:
for line_number, line in enumerate(stream, start=1):
line = line.rstrip("\n")
if not line:
continue
item_id, separator, text = line.partition("|")
if not separator or not item_id or not text:
raise ValueError(f"Invalid line {line_number} in {path}.")
if item_id in result:
raise ValueError(f"Duplicate ID {item_id} in {path}.")
result[item_id] = text
return result
def main() -> None:
for path in (SOURCE, SUMMARY, REVIEW):
if not path.is_file():
raise FileNotFoundError(f"Missing input: {path}")
if OUTPUT_DIR.exists():
raise FileExistsError(f"Output folder already exists: {OUTPUT_DIR}")
source = read_id_text(SOURCE)
summary = read_id_text(SUMMARY)
reviewed = {}
with REVIEW.open("r", encoding="utf-8-sig", newline="") as stream:
reader = csv.DictReader(stream)
required = {"summary_id", "verdict", "evidence", "reason"}
if reader.fieldnames is None or not required.issubset(reader.fieldnames):
raise ValueError("Review CSV is missing a required column.")
for row in reader:
summary_id = row["summary_id"].strip()
verdict = row["verdict"].strip()
evidence_ids = [x.strip() for x in row["evidence"].split(";") if x.strip()]
if summary_id not in summary:
raise ValueError(f"Unknown summary ID: {summary_id}")
if summary_id in reviewed:
raise ValueError(f"Duplicate review: {summary_id}")
if verdict not in VALID_VERDICTS:
raise ValueError(f"Invalid verdict for {summary_id}: {verdict}")
if not evidence_ids:
raise ValueError(f"No evidence listed for {summary_id}.")
unknown = [item for item in evidence_ids if item not in source]
if unknown:
raise ValueError(f"Unknown source IDs for {summary_id}: {unknown}")
if not row["reason"].strip():
raise ValueError(f"Missing reason for {summary_id}.")
reviewed[summary_id] = {
"summary_id": summary_id,
"summary_text": summary[summary_id],
"verdict": verdict,
"evidence": ";".join(evidence_ids),
"reason": row["reason"].strip(),
}
missing = [item for item in summary if item not in reviewed]
if missing:
raise ValueError(f"Summary sentences not reviewed: {missing}")
OUTPUT_DIR.parent.mkdir(parents=True, exist_ok=True)
OUTPUT_DIR.mkdir()
fields = ["summary_id", "summary_text", "verdict", "evidence", "reason"]
with OUTPUT.open("x", encoding="utf-8", newline="") as stream:
writer = csv.DictWriter(stream, fieldnames=fields)
writer.writeheader()
for summary_id in summary:
writer.writerow(reviewed[summary_id])
counts = {verdict: 0 for verdict in VALID_VERDICTS}
for row in reviewed.values():
counts[row["verdict"]] += 1
print(f"Summary sentences checked: {len(summary)}.")
print(
f"Supported: {counts['SUPPORTED']}; partial: {counts['PARTIAL']}; "
f"unsupported: {counts['UNSUPPORTED']}."
)
print(f"Output: {OUTPUT.as_posix()}")
if __name__ == "__main__":
main()
06Check the expected result
For the synthetic review above, all 4 summary sentences have a valid verdict and valid source references. The expected console output is shown below. It was derived by hand and is not an execution log.
Confirm that every A-ID appears exactly once in the review.
Confirm that every cited S-ID exists in source.txt.
Confirm that unsupported claims are not silently rewritten as supported claims.
Keep the original AI summary unchanged so the review remains auditable.
Run the script again without changing OUTPUT_DIR. It should stop with FileExistsError instead of overwriting the previous validation.
07Recognize common errors and limits
Problem
What to do
One summary sentence contains two separate claims
Split the sentence into claim-sized units or review each claim separately.
Evidence only supports part of a sentence
Use PARTIAL and write the missing qualification in the reason column.
The source is silent on a conclusion
Mark it UNSUPPORTED instead of treating a plausible inference as a sourced fact.
A source sentence is cited but does not actually support the claim
Fix the human review; valid IDs alone do not prove semantic support.
Different sources disagree
Record the disagreement explicitly instead of forcing a single unsupported conclusion.
The Python script validates review completeness and reference IDs, not truth. Semantic support still requires human reading or a separate evidence-evaluation process. A wrong human verdict can pass the structural checker.
For long reports, use page numbers, paragraph IDs, table numbers, or quoted source locations instead of informal references. Also distinguish direct source statements from calculations, interpretations, and external knowledge. A summary should not quietly turn one category into another.
Execution and verification record
2026-09-20 · hand-checked example · target: Python 3.12 · standard library: csv, pathlib · no execution
Treat an AI-generated regex as a draft, not a finished rule. Build a small synthetic test table, compare expected and actual matches, revise the pattern, and save a review report before using it on real data.
Treat an AI-generated SQL query as a draft and test it on a tiny SQLite database with known answers. Compare the AI result with a manually calculated expected table before using the query on real data.
Show an AI a few examples of the exact output structure you want, then validate the returned JSON before using it. A small synthetic ticket dataset demonstrates how formatting examples and automatic checks work together.
Turn “automate this” into an executable task description. Attach a synthetic sample with no sensitive data and a hand-checked expected result to complete a request for a script that totals work logs by team.