ДокументацияПрактические руководства (Cookbooks)Проверка цитат и фактчекинг

Проверка цитат и фактчекинг

Catch wrong or hallucinated citations by checking against the source document. One TypeSafe Choice question decides whether the quote's context supports the claim, and its confidence can flag the citation for human review.

Выявляйте неверные или галлюцинированные цитаты путем сверки с исходным документом. Один вопрос TypeSafe Choice определяет, подтверждает ли контекст цитаты утверждение, а его показатель уверенности (confidence) позволяет направить сомнительную цитату на проверку человеку.

LLM отвечает на вопрос и прикрепляет цитаты: для каждого утверждения указывается раздел исходного документа и цитата, на которую оно опирается. Некоторые из этих цитат оказываются ошибочными или вымышленными: цитата может вообще отсутствовать в документе либо присутствовать дословно, но в контексте, утверждающем прямо противоположное.

Проверять каждую цитату вручную слишком долго: нужно найти документ, отыскать в нем цитату, а затем прочитать достаточный фрагмент контекста, чтобы понять, действительно ли он подтверждает сказанное.

Чтобы автоматизировать эту проверку, мы сначала ищем отсутствующие цитаты с помощью обычного строкового сопоставления, а затем используем вопрос Choice, чтобы проанализировать контекст каждой найденной цитаты и определить, подтверждает ли он утверждение.

MERMAID ACTIONS={TRUE} THEME={NULL} api.wedstack.ru/v1
%%{init: {"flowchart": {"wrappingWidth": 330}}}%%
flowchart LR
    cite["исходный документ + цитата"]

    match{"присутствует ли цитата<br/>в источнике?"}
    fab["пометить <b>fabricated</b>"]

    subgraph request[" "]
        q["Choice &mdash; как раздел<br/>соотносится с утверждением?<br/>supports &rarr; пометить <b>verified</b><br/>contradicts &rarr; пометить <b>contradicted</b><br/>says nothing &rarr; пометить <b>unsupported</b>"]
    end

    gate{"уверенность<br/>&ge; 0.8?"}
    stand["оставить вердикт"]
    review["проверка человеком"]

    cite --> match
    %% the two edges that reach the call come first, so they stay adjacent; the
    %% string match's own verdict is declared last and lands below them
    match -- "найдена" --> request
    match -- "нет цитаты" --> request
    match -- "не найдена" --> fab
    request --> gate
    gate --> stand
    gate --> review

Ниже восемь цитат из ответа LLM о спецификации RFC 7519 (JSON Web Token) проходят через эту проверку. Четыре корректные цитаты получили вердикт verified с уверенностью 0.93 или выше. Все четыре специально внедренные ошибки были успешно выявлены: сфабрикованная цитата, противоречащее утверждение и две неподтвержденные цитаты, отправленные человеку на проверку.

Функция check_citation(), которую вы создадите в этом руководстве, принимает исходный документ и одну цитату и возвращает один из четырех вердиктов: verified, unsupported, contradicted или fabricated. Она также возвращает показатель уверенности, позволяющий отметить случаи, требующие внимания человека.

Настройка

BASH THEME={NULL} api.wedstack.ru/v1
pip install ipython "typesafe-sdk>=0.5.7" cooksafe --extra-index-url https://pypi.typesafe.ai/

Затем установите переменную TYPESAFE_API_KEY. Каждый вызов API кэшируется в файле json_cache.json, который поставляется вместе с руководством, поэтому при повторном запуске воспроизводятся сохраненные показатели без фактических запросов к API. Удалите этот файл, чтобы выполнить все вызовы вживую.

Приведенные ниже значения получены на модели jev-1.12 от 2026-08-16.

PYTHON THEME={NULL} api.wedstack.ru/v1
import json
import os
import re
from pathlib import Path
from time import perf_counter

from cooksafe import JsonCache, make_playground_link
from IPython.display import Markdown, display
from typesafe_sdk import Choice, TypeSafeClient

TYPESAFE_MODEL = "jev-1.12"
AUTO_ACCEPT = 0.8  # start high for more human review as you build trust in the model

client = TypeSafeClient(
    api_key=os.environ.get("TYPESAFE_API_KEY", "cache-only"),
    base_url=os.environ.get("TYPESAFE_ENDPOINT"),
    timeout=120.0,
)
json_cache = JsonCache(Path("json_cache.json"))

Загрузка источника и цитат

Источником служит спецификация RFC 7519 (JSON Web Token), загруженная с rfc-editor.org и сохраненная рядом с этим руководством в файле rfc7519.txt. Код ниже удаляет колонтитулы страниц и разбивает текст на пронумерованные разделы.

Восемь цитат в файле citations.json были сгенерированы LLM по тексту RFC. Четыре из них точны; остальные четыре мы отредактировали так, чтобы они не прошли проверку.

PYTHON EXPANDABLE THEME={NULL} api.wedstack.ru/v1
PLAINTEXT api.wedstack.ru/v1
58,365 characters, 45 numbered sections, 8 citations

A citation with a quote:
{
  "id": "aud_reject",
  "claim": "If a validator does not find itself in a token's audience list, it has to reject the token.",
  "quote": "If the principal processing the claim does not identify itself with a value in the \"aud\" claim when this claim is present, then the JWT MUST be rejected.",
  "section": "4.1.3"
}

A claim-only citation:
{
  "id": "iat_future",
  "claim": "The \"iat\" claim requires validators to reject tokens whose issue time is in the future.",
  "quote": null,
  "section": "4.1.6"
}

Поиск каждой цитаты в источнике

Цитата, которой нет в источнике, является сфабрикованной, и чтобы это понять, никакая нейросеть не требуется. Нормализуйте пробелы и фигурные кавычки, чтобы цитата совпадала даже при переносах строк в RFC, а затем выполните поиск подстроки. Совпадение также указывает, из какого раздела взята цитата, и этот раздел передается модели для чтения на следующем шаге.

Цитата может указывать раздел, не приводя прямых выдержек из него. В таком случае сопоставлять нечего, поэтому мы берем указанный раздел и сразу переходим к модели.

PYTHON THEME={NULL} api.wedstack.ru/v1
def normalize(text: str) -> str:
    """Collapse whitespace and fold curly quotes, so a quote matches across line wraps."""
    table = str.maketrans({"“": '"', "”": '"', "‘": "'", "’": "'"})
    return re.sub(r"\s+", " ", text.translate(table)).strip()


def find_quote(sections: dict[str, str], quote: str) -> str | None:
    """The number of the section that contains the quote verbatim, or None."""
    needle = normalize(quote)
    for number in sorted(sections, key=lambda n: [int(p) for p in n.split(".")]):
        if needle in normalize(sections[number]):
            return number
    return None


def locate(sections: dict[str, str], citation: dict) -> tuple[str, str | None]:
    """Step 1 for one citation: a status, plus the section step 2 will read."""
    if citation["quote"] is None:
        return "section-only", sections[citation["section"]]
    number = find_quote(sections, citation["quote"])
    if number is None:
        return "missing", None
    return "found", sections[number]


for citation in CITATIONS:
    status, section = locate(SECTIONS, citation)
    where = f"section of {len(section):,} chars" if section else "not in the source"
    print(f"{citation['id']:<18}{status:<14}{where}")
PLAINTEXT api.wedstack.ru/v1
epoch_seconds     found         section of 3,122 chars
aud_reject        found         section of 761 chars
sig_reporting     missing       not in the source
clock_skew        found         section of 529 chars
exp_required      found         section of 529 chars
pii_encryption    found         section of 1,653 chars
iat_future        section-only  section of 270 chars
duplicate_names   found         section of 918 chars

Проверка того, подтверждает ли источник утверждение

Цитата, оставшаяся на данном этапе, дословно присутствует в источнике. Однако этого недостаточно: цитата может быть точной, но построенное на ней утверждение — ложным. Чтобы проверить это, необходим контекст цитаты — раздел, найденный на шаге 1.

Один вопрос Choice на каждую найденную цитату покрывает три варианта соотношения раздела и утверждения. Вариант с наибольшей вероятностью становится вердиктом, а параметр AUTO_ACCEPT (0.8 в коде выше) определяет дальнейшие действия:

  • уверенность равна 0.8 или выше: вердикт принимается автоматически;
  • ниже 0.8: человек должен подтвердить вердикт перед выполнением каких-либо действий.

Начните с высокого значения порога и снижайте его по мере того, как вы убедитесь в надежности работы модели на ваших документах.

PYTHON EXPANDABLE THEME={NULL} api.wedstack.ru/v1

Проверка каждой цитаты

Все восемь цитат проходят одинаковую проверку:

PYTHON THEME={NULL} api.wedstack.ru/v1
print(f"{'citation':<18}{'quote':<14}{'relation':<14}{'conf':>6}  {'verdict':<13}{'action':>7}")
for citation in CITATIONS:
    result = check_citation(SECTIONS, citation)
    answer = result["answer"]
    relation = answer["choice"] if answer else "-"
    conf = f"{answer['confidence']:.2f}" if answer else "-"
    action = "auto" if result["auto"] else "review"
    print(
        f"{result['id']:<18}{result['status']:<14}{relation:<14}{conf:>6}"
        f"  {result['verdict']:<13}{action:>7}"
    )
PLAINTEXT api.wedstack.ru/v1
citation          quote         relation        conf  verdict       action
epoch_seconds     found         supports        0.93  verified        auto
aud_reject        found         supports        0.95  verified        auto
sig_reporting     missing       -                  -  fabricated      auto
clock_skew        found         supports        0.99  verified        auto
exp_required      found         contradicts     0.99  contradicted    auto
pii_encryption    found         says_nothing    0.27  unsupported   review
iat_future        section-only  says_nothing    0.56  unsupported   review
duplicate_names   found         supports        0.99  verified        auto

Четыре цитаты получили вердикт verified, одна — fabricated, одна — contradicted, и две — unsupported.

  • epoch_seconds, aud_reject, clock_skew и duplicate_names — корректные цитаты. Все они получили оценку verified с показателем уверенности 0.93 или выше, что заметно выше порога AUTO_ACCEPT.
  • sig_reporting даже не передавалась в модель. Ее цитата отсутствует в RFC, поэтому строковое сопоставление сразу пометило ее как fabricated.
  • exp_required цитирует раздел 4.1.4 слово в слово, однако этот же раздел говорит: "Use of this claim is OPTIONAL" (использование этого клейма необязательно). Поэтому утверждение признано противоречащим (contradicted) с уверенностью 0.99.
  • pii_encryption и iat_future вернули результат unsupported с оценками 0.27 и 0.56 соответственно — оба ниже порога, поэтому оба отправлены человеку. Пример pii_encryption показывает, почему одного строкового сопоставления недостаточно: цитата присутствует в источнике дословно, но исходный раздел вообще ничего не говорит о выдвинутом утверждении.

Чтобы применить этот подход к собственным данным, замените rfc7519.txt и citations.json. Функции load_source() и split_sections() написаны с учетом разметки RFC, поэтому для документов другой структуры потребуется собственный парсинг.

Строковое сопоставление является точным после нормализации: усеченная или слегка перефразированная цитата будет помечена как fabricated. Для рабочей системы, которая должна допускать нестрогие цитаты, потребуется нечеткий поиск (fuzzy matching).

Открыть в Playground

По ссылке доступны утверждение и раздел для одной из цитат, а также сам вопрос. Откройте ее, чтобы запустить такой же запрос в браузере.

PYTHON THEME={NULL} api.wedstack.ru/v1
example = next(c for c in CITATIONS if c["id"] == "exp_required")
_, example_section = locate(SECTIONS, example)
playground_link = make_playground_link(
    {"claim": example["claim"], "section": example_section}, QUESTIONS, models=[TYPESAFE_MODEL]
)
display(Markdown(f"🔗 [Open one citation's claim + section in the TypeSafe playground]({playground_link})"))

Открыть утверждение + раздел цитаты в TypeSafe Playground →