AIの業務活用

AIに文書を質問し行番号の引用を検証する方法

文書についてAIが回答するとき、各主張に行番号の引用を付けると検証しやすくなります。小さな合成文書を使い、引用付き回答をAIに求め、各引用範囲をPythonで自動確認する方法を説明します。

目次を表示

この翻訳はAIで作成しました。コード、単位、数値は原文と併せて確認してください。各言語のネイティブ話者による校閲は、まだ完了していません。 English

対象読者レポート、メモ、規程などのテキスト文書についてAIチャットアシスタントに質問し、回答の根拠を簡単に確認したい人向けです。

準備するもの
  • Python 3.12
  • レビュー中に行番号を固定できるプレーンテキスト文書

01行番号の引用を求める理由

AIチャットアシスタントが文書について回答するとき、重要なのは回答が自然に見えるかどうかではありません。各主張が、与えた文書によって実際に裏付けられているかを確認することが重要です。行番号の引用を求めれば、レビューする人は文書全体を探し直さず、直接元の箇所へ戻れます。

実用的な確認手順は2段階に分けられます。まず、ソフトウェアで各引用が実在する行番号を指しているかを確認し、その行を再表示します。次に、人が引用された文章を読み、その内容が実際に主張を支えているか確認します。自動確認は壊れた参照や存在しない参照を見つけられますが、それだけで意味的な裏付けまで証明できるわけではありません。

02小さな合成文書を作る

次の合成プロジェクトメモを使います。以下の7行を表示どおりに project_note.txt へ保存します。例を意図的に小さくしているため、すべての回答と引用を手作業でも確認できます。

内容
L1Project Atlas weekly note
L2Prototype test moved from 12 Sep to 16 Sep.
L3Reason: replacement sensor arrived late.
L4Analysis owner: Mina.
L5Draft results due 18 Sep.
L6Final review meeting scheduled 21 Sep.
L7Budget status unchanged.

質問を「どの予定が変更され、なぜ変更され、その後の締切は何か」とします。元の文書には、プロトタイプ試験が 12 Sep から 16 Sep へ変更されたこと、理由は交換用センサーの到着遅延だったこと、ドラフト結果の締切が 18 Sep、最終レビュー会議が 21 Sep と明記されています。

03プロンプトで引用形式を厳密に指定する

単に文書を引用するよう依頼するのではなく、後でスクリプトから解析できるよう正確な形式を指定します。この例では、すべての事実文の末尾に [L2-L3] または [L5] のような引用を付けるよう求めます。

  1. 提供した文書だけを使用するよう指定します。
  2. 引用形式を正確に [Lx] または [Lx-Ly] に限定します。
  3. 直前の主張を直接裏付けない行は引用しないよう求めます。
  4. 文書に根拠がなければ推測せず、文書からは回答できないと述べるよう指定します。
  5. レビュー中は元ファイルを変更せず、行番号を固定します。

04一般的な引用付き回答を確認する

適切な回答は次のようになります。プロトタイプ試験は 12 Sep から 16 Sep へ変更され、その理由は交換用センサーの到着が遅れたためです。 [L2-L3] ドラフト結果の締切は 18 Sep で、その後の最終レビュー会議は 21 Sep に予定されています。 [L5-L6]

この回答には2つの引用範囲があります。1つ目は2行目と3行目、2つ目は5行目と6行目を参照する必要があります。回答を信頼する前に、チェッカーでその範囲が実在するか確認し、各引用に対応する元の文章をそのまま表示できます。

05Pythonですべての引用を自動確認する

AIの回答を ai_answer.txt に保存します。以下のスクリプトは project_note.txt と ai_answer.txt を読み、正規表現で引用を抽出し、不正な範囲を検出し、確認結果を outputs/citation_check.txt に保存します。元文書とAI回答は変更せず、出力レポートがすでに存在する場合は停止します。

python
import re
from pathlib import Path

DOCUMENT_FILE = Path("project_note.txt")
ANSWER_FILE = Path("ai_answer.txt")
OUTPUT_DIR = Path("outputs")
OUTPUT_FILE = OUTPUT_DIR / "citation_check.txt"

CITATION_RE = re.compile(r"\[L(\d+)(?:-L?(\d+))?\]")


def main():
    if not DOCUMENT_FILE.is_file():
        raise SystemExit(f"Document not found: {DOCUMENT_FILE}")

    if not ANSWER_FILE.is_file():
        raise SystemExit(f"AI answer not found: {ANSWER_FILE}")

    if OUTPUT_FILE.exists():
        raise SystemExit(f"Output already exists: {OUTPUT_FILE}")

    lines = DOCUMENT_FILE.read_text(encoding="utf-8").splitlines()
    answer = ANSWER_FILE.read_text(encoding="utf-8")
    matches = list(CITATION_RE.finditer(answer))

    if not matches:
        raise SystemExit("No citations found in AI answer.")

    report = []
    report.append(f"document_lines={len(lines)}")
    report.append(f"citations_found={len(matches)}")
    report.append("")

    invalid_count = 0

    for index, match in enumerate(matches, start=1):
        start = int(match.group(1))
        end = int(match.group(2)) if match.group(2) else start
        citation = match.group(0)

        valid = 1 <= start <= end <= len(lines)
        report.append(f"citation_{index}={citation}")
        report.append(f"valid_range={valid}")

        if not valid:
            invalid_count += 1
            report.append("source=INVALID LINE RANGE")
            report.append("")
            continue

        for line_number in range(start, end + 1):
            source_text = lines[line_number - 1]
            report.append(f"L{line_number}: {source_text}")

        report.append("")

    report.append(f"invalid_citations={invalid_count}")

    OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
    OUTPUT_FILE.write_text("\n".join(report) + "\n", encoding="utf-8")
    print(f"Wrote: {OUTPUT_FILE}")


if __name__ == "__main__":
    main()

06各主張と抽出された元文書を比較する

この合成回答では、引用が2つ見つかり、不正な引用範囲は0になるはずです。[L2-L3] は試験日変更と交換用センサーの遅延に対応し、[L5-L6] はドラフト結果の締切と最終レビュー会議の日程に対応します。

引用抽出される行手作業での結論
[L2-L3]L2とL3変更された試験日と、その理由を裏付けています。
[L5-L6]L5とL6ドラフト締切と最終レビュー日の根拠になっています。

次に、[L8] のような誤った引用を意図的に試します。合成文書は7行しかないため、スクリプトではこの範囲を無効として扱う必要があります。文章の中ではもっともらしく見えても、実際の元文書を参照できない引用を検出できます。

07よくあるミスと限界を理解する

  • AI回答を生成した後に元文書を編集しないでください。行の追加や削除で番号が変わります。
  • チェッカーが [Lx] 形式を想定しているなら、ページ名や曖昧な表現など別形式の引用をそのまま受け入れないでください。
  • 引用先の行が存在するだけで、その行が自動的に主張を証明すると考えないでください。
  • AIが文書内の別々の場所から2つの事実を組み合わせながら、一方だけしか引用していないケースを確認してください。
  • 文書に根拠がない場合は一般知識で穴を埋めず、根拠がないことを明示するよう求めてください。
  • 長い文書では、後のレビューでも同じ行番号を再現できるよう固定した元文書のコピーを保存してください。

この方法はプレーンテキストのレビューには適していますが、PDF、スプレッドシート、OCR結果、動的に生成される文書では行境界が安定しない場合があります。その場合は、ページ番号、段落ID、セクションID、表セルなど、より安定した引用単位を使う方が適切です。

したがって、このワークフローは2層構造です。最初に引用形式と行範囲を自動検証し、その後に各引用箇所が実際に回答を支えているかを人が確認します。両方を組み合わせることで、引用のないAI回答より文書Q&Aの監査が容易になります。

実行・検証の記録

2026-09-21 · hand-checked example · Python 3.12

  • 合成文書は記事に示したとおり正確に7行です。
  • 例の回答には正確に2つの引用範囲 [L2-L3] と [L5-L6] が含まれています。
  • [L2-L3] は合成元文書と手作業で照合し、変更されたプロトタイプ試験日と明示された理由が含まれることを確認しました。
  • [L5-L6] は合成元文書と手作業で照合し、ドラフト結果の締切と最終レビュー会議の日付が含まれることを確認しました。
  • [L8] は7行の文書範囲外なので、示したロジックでは無効な引用範囲として扱われます。
  • 正規表現は意図した形式である [L2]、[L2-L3]、[L2-3] を受け付けます。
検証範囲の限界
  • この記事のPythonコードは実行していません。記載した動作は手作業でコードを確認した結果に基づいています。
  • スクリプトは引用形式と参照行の存在を確認するだけで、その行が主張を意味的に裏付けているかどうかは判断しません。
  • この例は行番号が固定されたプレーンテキストを前提としており、OCR誤認識、PDFレイアウト変更、表、レビュー途中で改行位置が変わる文書は対象としていません。

サイト全体の執筆・検証方針

参考資料

説明と例は独自に作成しました。関連する動作や概念は、以下の公式資料で確認できます。