初めに
RAGの回答に文書名が付いていると、少し安心します。
ただ、実際に確認しようとすると、次のところで手が止まります。
- PDFの何ページの、どの段落なのか。
- Excelのどのsheet、どの行なのか。
- Wordを変換した場合、元のどの段落に対応するのか。
- TXTやMarkdownのようにページを持たないファイルは、どう開けばよいのか。
- 同じ文章が2回あるとき、どちらを根拠にしたのか。
そこで、自作のProduction Ready Suiteで、解析した要素から原文へ戻る操作と、RAG Chatの根拠から原文へ戻る操作を整理しました。この記事では、その実装と、合成資料を使って確認した画面を紹介します。
文書・質問・数値・ファイル名は検証用の合成データです。スクリーンショットはデスクトップの1600×1000で撮影し、アカウント、内部ID、接続情報をマスクしています。確認日は2026年11月12日です。
先に結論
ファイルに合わせて、位置の持ち方を3つに分けています。
| 表示するもの | 保存する位置 | 画面での見せ方 |
|---|---|---|
| PDF、PDFへ変換したPowerPoint、画像 | ページ番号、bbox、座標単位、ページサイズ | ページ画像の該当領域を強調 |
| TXT、Markdown、JSON、HTML | 要素ID、文字範囲、原本との対応表 | 該当する文字や原要素を強調 |
| CSV、構造化JSONLへ変換したExcel・Word | record ID、sheet・行・セル・段落の位置 | 構造化した原文の記録を強調 |
重要なのは、解析時点で位置を残し、引用時点の版へ戻ることです。
ハイライトを表示するためだけに、LLMへ原文をもう一度探させる処理は入れていません。保存した対応関係を使って、サーバーとブラウザで位置を計算します。
一方、位置の確認と、回答内容の正しさは別です。正しい位置を示せても、生成回答には誤りや不足があり得ます。原文を開く機能は、利用者が回答を確かめるためのものです。
1. 今回確認した全17形式
現在のアップロード受付の定義と、合成ファイルの拡張子を照合しました。対象は次の17個です。
| 形式 | 拡張子 | 今回の解析・変換経路 | 今回のハイライト |
|---|---|---|---|
.pdf |
PDFをDoclingで解析 | ページ上のbbox | |
| Word | .docx |
Word → 構造化JSONL | 段落・表などの記録 |
| 旧Word | .doc |
変換してからWord → 構造化JSONL | 変換後文書の段落・記録 |
| PowerPoint | .pptx |
Office → PDF → 解析 | 変換後PDFのbbox |
| 旧PowerPoint | .ppt |
Office → PDF → 解析 | 変換後PDFのbbox |
| Excel | .xlsx |
Excel → 構造化JSONL | sheet・行・セル範囲を持つ記録 |
| 旧Excel | .xls |
変換してからExcel → 構造化JSONL | sheet・行・セル範囲を持つ記録 |
| CSV | .csv |
CSV → 構造化JSONL | 元CSVの行に対応する記録 |
| テキスト | .txt |
Unstructured | 原本文字範囲・引用元段落 |
| Markdown | .md |
Unstructured | 原本文字範囲・引用元段落 |
| Markdown別拡張子 | .markdown |
Unstructured | 原本文字範囲・引用元段落 |
| JSON | .json |
通常JSONとして解析 | 本文の引用範囲、原JSONの対応箇所 |
| HTML | .html |
Unstructured | 本文の引用範囲、原HTMLの対応箇所 |
| HTML別拡張子 | .htm |
Unstructured | 本文の引用範囲、原HTMLの対応箇所 |
| PNG | .png |
画像をDoclingで解析 | 画像上のbbox |
| JPEG | .jpg |
画像をDoclingで解析 | 画像上のbbox |
| JPEG別拡張子 | .jpeg |
画像をDoclingで解析 | 画像上のbbox |
.jsonには、通常のJSONに加えて、XMLの要素・属性・子ノードをJSONで表した「XMLツリーJSON」の専用解析方式もあります。これはJSONの解析方式の違いです。今回の17個に、.xmlというアップロード拡張子を追加して数えているわけではありません。
同じWordでもPDFへ変換して扱う経路はあります。本稿の形式別検証では、表に書いた文書レシピを1つずつ確認しています。全parserと全文書レシピの組合せを網羅した試験ではありません。
2. 解析と回答を、同じ位置情報でつなぐ
処理の流れは次のとおりです。
解析結果には本文だけでなく、次のような情報を持たせます。
{
"element_id": "element-2",
"text": "説明用の合成本文",
"page_number": 2,
"bbox": [72, 120, 400, 180],
"section_path": ["章A", "節B"]
}
これは説明用の例です。実際には、解析方式に応じた座標単位や原本対応、chunkの版なども使います。
解析画面で使うとき
右側で構造化要素やChunkを選び、その要素IDと解析成果の版を左側のプレビューへ渡します。画像ならページとbbox、テキストなら保存した対応表、構造化文書ならrecordを使います。
RAG Chatで使うとき
回答の根拠は、文書IDだけでなく、recipe、chunk set、元要素への対応を持ちます。そのため、回答したときの根拠から、同じ解析版の原文を読み直せます。
文書を再処理して古い引用の版が使えなくなった場合は、版が変わったことを表示します。新しい文書の似た文章へ、自動で付け替えることはしません。
3. PDF・PowerPoint・画像:bboxで示す
ここで解決したいこと
ページ番号だけでは、利用者はページ内を探し直す必要があります。そこで、抽出した要素のbboxをページ画像へ重ねます。
bboxには、少なくとも次の違いがあります。
- 左上・右下を表す
xyxyか、左上・幅・高さを表すxywhか。 - ページ上の絶対座標か、0〜1の比率か、百分率か。
- どのサイズ・向きのページに対する座標か。
共有UIのnormalizeBboxForPreviewがこれらを扱い、表示用の百分率へ変換します。たとえば、左上原点の絶対座標を持つxyxyなら、基本の計算は次です。
const leftPercent = x0 / pageWidth * 100;
const topPercent = y0 / pageHeight * 100;
const widthPercent = (x1 - x0) / pageWidth * 100;
const heightPercent = (y1 - y0) / pageHeight * 100;
実装では範囲の検証、ページの回転、表示対象ページの選択も行います。計算した矩形を、表示中のページ画像と同じ枠の中へ配置します。
PDFの解析画面
選択した要素と、左側のページ上の領域を対応させています。
RAG ChatからPDFの根拠を開く
同じページviewerを使い、回答へ渡した根拠の場所を確認できます。
PowerPointは、変換後PDFの位置を使う
.pptxと.pptは、今回の文書レシピではPDFへ変換してから解析しています。強調しているのは引用時点の変換後PDFの領域です。
解析画面では「処理後」を選ぶと、このPDFを確認できます。元のPowerPointの編集画面上に図形を重ねている機能ではありません。
PNG、JPG、JPEGもページ画像と同様に領域を重ねます。スキャンや画像の認識品質、解析器が出したbboxの精度には依存します。
4. TXT・Markdown:文字の位置で示す
TXTやMarkdownでは、原本文字列上の位置を保存します。ページのbboxを作る必要はありません。
保存する対応表の概念例です。
{
"coordinate_unit": "unicode_codepoint",
"sha256": "<原本bytesのSHA-256>",
"source_length": 1200,
"elements": {
"element-2": [
{"source_start": 100, "source_end": 140}
]
}
}
実際の対応表には形式・版などの情報も含みます。表示するときは原本のhash、対応表の形式、範囲を検証します。
PythonとJavaScriptの「文字数」を混ぜない
Python側の位置はUnicode codepointです。JavaScriptの通常の文字列indexはUTF-16なので、絵文字などで数がずれます。
共有のCodepointHighlightでは、文字列をcodepointの配列へ変換してから半開区間[start, end)を切り出します。
const characters = Array.from(text);
const quoted = characters.slice(start, end).join("");
実装は、不正な範囲を除き、重なる範囲をまとめてからmarkを描画します。
同じ文章が2回あるとき
文書全体を文字検索して、最初に見つかった文章を塗る方式にはしていません。保存した元要素に対象を限定し、その範囲内で文字の対応を調べます。
引用の文字範囲が一意ならexact、要素まで確認できる場合はelementとして扱います。元要素を確認できなければunresolvedとなり、精密な位置として見せません。
5. CSV・Excel・Word:構造化した記録を示す
この3種類では、今回の文書レシピで構造化JSONLを経由しています。
CSVの行、Excelのsheetと行、Wordの段落や表を、recordとして保存します。表示にも、そのrecordの位置と構造を使います。
CSV
CSVでは、記録の番号と原本の物理行を区別します。セル内改行があると、1記録が原本の複数行に対応するからです。変換器が記録した位置を引き継ぎ、読取時に原本対応を推測し直さないようにしています。
Excel
sheet名、行、セル範囲を表示し、該当する記録を表で読めるようにしています。
Word
見出し、段落、表の位置を保持します。.docを変換して扱う場合は、画面にも変換後文書の位置であることを示します。
共有のSourceRecordViewが表示する精度は、記録単位です。Excelの元アプリの見た目や、Wordの元ページの組版を再現したbbox表示ではありません。表の場合は該当recordの値を強調します。
ここで、変換後JSONLのhashと、record ID・本文の一致も検証します。別の版や別の記録を、原文に対応しているように見せないためです。
元ファイルのダウンロードは別操作です。ExcelをJSONLへ変換していても、元のXLSXやXLSを取得できます。
6. JSON・HTML:表示本文と原本の対応も保持する
JSONやHTMLでは、画面で読む本文と、保存された原本の文字列が一致するとは限りません。
JSONならescapeやノードの位置、HTMLならタグやentityがあります。そこで、引用本文の強調に加えて、「原本の対応箇所」を別に開けるようにしています。
通常JSONとXMLツリーJSON
通常JSONの対応表と、XMLツリーJSONのjson_source_mapを、それぞれの保存形式に合わせて読みます。XMLツリーJSONではJSON Pointerと文字範囲を保持するため、同じ本文を持つ別ノードも区別できます。
HTMLとHTM
HTMLは、本文の要素と、原HTMLのタグ片の対応を読みます。原コードは文字列として表示し、scriptや外部resourceを実行しません。
本文上の引用範囲と、原コードの対応範囲では精度が違う場合があります。原要素の対応だけが確認できたときは、その旨も表示します。
表の中の引用
解析器が表を縦線形式で保存する場合もあります。その場合は元文字列上の各セルの範囲を保持し、共有のDataTableへ渡します。
HTMLの表と縦線形式の表の両方について、引用範囲と交差するセルを強調します。同じ値が複数セルにあっても、値の文字検索で全部を塗る方式ではありません。
行途中で切れた表など、構造が確かめられない断片は、原文字列の範囲表示を残します。
7. 根拠を開いた後、前後の文脈も読めるようにする
引用した短い範囲だけでは、条件や前提を読み取りにくいことがあります。
そのため、原文readerには次の操作を用意しています。
| 操作 | 読み込むもの |
|---|---|
| 引用位置に戻る | 保存された元の引用要素と範囲 |
| 本節を読む | 引用が属する節の冒頭 |
| 親章節を読む | 親の章節の冒頭 |
| 目次 | 保存された章節の読み取り位置 |
| 前文・後文を読む | 同じ解析版の原要素を原順序で読み進める |
| 原本をダウンロード | 元ファイル |
前後文を得るために、検索順位で前後のchunkを連結しているわけではありません。同じ版の解析結果から、有界な窓として本文を読みます。画面にも「追加の参考文脈」と「引用の元となる段落」を分けて表示します。
章節のない文書では章節操作を無効にし、続きがなければ前後文のボタンを無効にします。クリックのたびに回答を生成し直す処理はありません。
8. 回答の引用が未確定だったら、無理に塗らない
今回の実モデルによるRAG Chatでは、生成された根拠行の位置が未確定になるケースもありました。
この場合、案内を表示して、当該入口からの強調を抑止します。位置が未確定の状態を、成功したハイライトへ数えていません。
一方、AIへ渡した根拠一覧には、保存されたchunkと元要素の対応があります。その一覧から原文を開き、確認できた元要素を読む操作も検証しました。
本稿の形式別の「Chatの画面」は、回答の根拠行、またはこの取得済み根拠一覧から開いた画面です。入口の違いは検証記録へ残しています。17形式を確認したことは、任意の質問の生成回答や、全引用行の精密な定位を保証する意味ではありません。
9. RAG検索とAgent Chatでも使えるか
RAG検索
RAG検索とRAG Chatは、CitationSourceSheetを共有します。根拠を表示する部分は、同じ原文readerとページviewerへ進みます。
今回は17形式それぞれについて、独立した合成資料の範囲で実検索を行い、検索結果から原文を開きました。検索streamの置き換えは行っていません。
Agent Chat
Agentは、公開MCPの検索結果から根拠を保存します。画面で原文を開くときは、Agent backendが閲覧者として短期の署名URLを取得し、そのURLから本文やページ画像を読みます。
Agent Chat
→ Agent backend
→ RAGの公開MCP
→ 閲覧者と根拠・版に結び付いた署名URL
→ 共有の原文reader / ページviewer
本文を読むfetchはCookieとRefererを送らず、署名URLの読み取り側では利用者の現在の権限、対象範囲、引用版を確認します。
2026年11月12日、合成文書・保存済みAgent Runを使い、17形式すべての原文ハイライトを実環境で確認しました。 検証用Agentは下書き実行です。
PDF・PPTX・PPT・PNG・JPG・JPEGでは、元要素を持つ根拠からページ画像のbboxを表示します。
TXT・MD・Markdown別拡張子・HTML・HTM・通常JSONでは、共通の原文readerで文字範囲を表示します。
CSV・XLSX・XLS・DOCX・DOCでは、保存された構造化JSONLを引用版のreaderへ渡し、記録を表示します。Excelは行とセル範囲、Wordは段落と見出しを確認できます。
この5形式では、署名URLから元ファイルをダウンロードし、合成原本とSHA-256が一致することも確認しました。表示用のJSONLへ変換していても、元ファイルのdownloadは置き換わりません。
構造化Officeは、保存された派生ファイルのMIMEに応じてreaderを選び、引用版のJSONLを読み込みます。表示時にもhash・record ID・本文の一致を確認します。
形式別の入口と確認範囲は、付録の検証記録にまとめました。これらは原文表示の確認であり、生成回答の内容や全引用行の精度を保証する意味ではありません。
10. 形式別の画面一覧
すべてデスクトップで撮影した合成資料です。サムネイルの代わりに、各形式の画像へ直接リンクしています。
| 形式 | ファイル解析の実画面 | RAG Chatの実画面 |
|---|---|---|
.pdf |
解析と強調 | Chatの根拠 |
.docx |
解析と強調 | Chatの根拠 |
.doc |
解析と強調 | Chatの根拠 |
.pptx |
解析と強調 | Chatの根拠 |
.ppt |
解析と強調 | Chatの根拠 |
.xlsx |
解析と強調 | Chatの根拠 |
.xls |
解析と強調 | Chatの根拠 |
.csv |
解析と強調 | Chatの根拠 |
.txt |
解析と強調 | Chatの根拠 |
.md |
解析と強調 | Chatの根拠 |
.markdown |
解析と強調 | Chatの根拠 |
.json |
解析と強調 | Chatの根拠 |
.html |
解析と強調 | Chatの根拠 |
.htm |
解析と強調 | Chatの根拠 |
.png |
解析と強調 | Chatの根拠 |
.jpg |
解析と強調 | Chatの根拠 |
.jpeg |
解析と強調 | Chatの根拠 |
11. 実装で分かったこと
元要素への対応は、chunk化の前から残す
後から根拠の文字列だけを使って原文を探そうとすると、重複文、表、変換後の文書で位置が曖昧になります。要素ID、版、文字範囲、原本や派生物への対応を、解析から引き継いでおくと扱いやすくなります。
bboxと記録の位置は、それぞれの精度で見せる
画像上の領域、抽出要素内の文字範囲、Excelの行、Wordの段落は、同じ種類の座標ではありません。画面の説明も、それぞれに合わせています。
前後文と回答に使った引用を混ぜない
利用者が追加で開いた前後文は、読解を助けるための文脈です。回答生成時に使われた引用とは表示を分けます。
ハイライトのための再生成を増やさない
保存された対応関係で処理できる表示は、コードで計算します。対応が不足する旧記録や、位置が確認できない引用は、その状態を表示します。
12. 関連する実装と、手元で確認するコマンド
主な実装箇所です。
局所の回帰を確認する例です。repositoryの依存関係を準備してから実行します。これらは決定論的なテスト・画面のfixture回帰であり、実サービスを使った今回の確認とは分けます。
cd rag/backend
uv run pytest tests/test_source_reader.py tests/test_source_map_resolve.py tests/test_structured_source_reader.py
cd rag/frontend
npx playwright test e2e/citation-text-preview.spec.ts
cd agent/frontend
npx playwright test e2e/chat-answer-evidence.spec.ts
まとめ
Production Ready RAGでは、17形式の原文を、ページのbbox・原本文字範囲・構造化recordという3つの方法で表示しています。
解析結果とChatの根拠を同じ元要素・版へ結び付け、利用者が原文を開いて確認できるようにしました。原本対応が確認できないケースも、精度の違いを残して表示します。
RAGの回答を確認しやすくするには、回答の文章だけでなく、利用者が根拠へ戻る操作まで一緒に作っておくと役立ちます。
関連記事
付録:形式別検証記録
検証範囲・形式別の結果・制約を開く
確認日:2026年11月12日。desktop 1600×1000。文書・質問は合成データ。画像はアカウント欄・内部ID・接続情報をmaskした。
実行範囲
- アップロード受付の17拡張子を実装定義と照合。全parser・全レシピの組合せを保証するものではない。
- RAG:実配備の解析、実際に生成したChat回答、実際の検索streamで確認。検索応答の置換なし。
- 各行で原文の読み込み完了と、文字markまたはbbox overlayが1件以上あることを確認。文書要約のカードを原文位置の証拠に使っていない。
- Chatの生成引用行に位置不足の案内が出る場合は、取得済み根拠一覧の元要素から開いた。引用行が不明な状態を精密定位成功と数えない。
- Agent:形式ごとの合成文書に限定した公開MCP検索を実行。全17 Runは完了。下書きAgentによる試験。
- 実配備のfrontend・backendと保存済み合成Runを使用。原文応答の置換や回答再生成は行っていない。
RAG:17形式の結果とスクリーンショット
すべての画像は直接開けるPNG。Chatの入口も列で区別する。
| 拡張子 | 解析 | Chatの入口 | Chat | 検索 |
|---|---|---|---|---|
.pdf |
成功 | 回答の引用行 | 成功 | 成功 |
.docx |
成功 | 回答の引用行 | 成功 | 成功 |
.doc |
成功 | 回答の引用行 | 成功 | 成功 |
.pptx |
成功 | 取得済み根拠一覧 | 成功 | 成功 |
.ppt |
成功 | 取得済み根拠一覧 | 成功 | 成功 |
.xlsx |
成功 | 取得済み根拠一覧 | 成功 | 成功 |
.xls |
成功 | 取得済み根拠一覧 | 成功 | 成功 |
.csv |
成功 | 取得済み根拠一覧 | 成功 | 成功 |
.txt |
成功 | 取得済み根拠一覧 | 成功 | 成功 |
.md |
成功 | 取得済み根拠一覧 | 成功 | 成功 |
.markdown |
成功 | 取得済み根拠一覧 | 成功 | 成功 |
.json |
成功 | 回答の引用行 | 成功 | 成功 |
.html |
成功 | 取得済み根拠一覧 | 成功 | 成功 |
.htm |
成功 | 取得済み根拠一覧 | 成功 | 成功 |
.png |
成功 | 取得済み根拠一覧 | 成功 | 成功 |
.jpg |
成功 | 取得済み根拠一覧 | 成功 | 成功 |
.jpeg |
成功 | 取得済み根拠一覧 | 成功 | 成功 |
Agent Chat:実環境の受入結果
2026年11月12日、17形式を実環境で検証し、全件成功。実配備のfrontend・backendを使用。隔離frontend・原文応答の置換・回答の再生成は行っていない。同じ保存済み合成Runの、元要素を持つ根拠から原文を開いた。
- 17形式すべてで実際のmarkまたはbbox overlayの描画を確認。
- CSV/XLSX/XLS/DOCX/DOCでは構造化recordと、その中のmarkも確認。
- 同5形式の署名URLによる原本downloadはHTTP 200、合成原本とSHA-256一致。
- テキストreaderのブラウザリクエストにCookieとRefererがないことを確認。
- アカウント欄・内部IDをmaskした1600×1000のdesktop画像を再撮影。
| 拡張子 | 配備後の表示 | 構造化record | 元ファイルdownload | スクリーンショット |
|---|---|---|---|---|
.pdf |
ページbbox・成功 | 対象外 | 今回の再検証は対象外 | 配備後の実画面 |
.docx |
文字範囲・成功 | 確認済み | 原本hash一致 | 配備後の実画面 |
.doc |
文字範囲・成功 | 確認済み | 原本hash一致 | 配備後の実画面 |
.pptx |
ページbbox・成功 | 対象外 | 今回の再検証は対象外 | 配備後の実画面 |
.ppt |
ページbbox・成功 | 対象外 | 今回の再検証は対象外 | 配備後の実画面 |
.xlsx |
文字範囲・成功 | 確認済み | 原本hash一致 | 配備後の実画面 |
.xls |
文字範囲・成功 | 確認済み | 原本hash一致 | 配備後の実画面 |
.csv |
文字範囲・成功 | 確認済み | 原本hash一致 | 配備後の実画面 |
.txt |
文字範囲・成功 | 対象外 | 今回の再検証は対象外 | 配備後の実画面 |
.md |
文字範囲・成功 | 対象外 | 今回の再検証は対象外 | 配備後の実画面 |
.markdown |
文字範囲・成功 | 対象外 | 今回の再検証は対象外 | 配備後の実画面 |
.json |
文字範囲・成功 | 対象外 | 今回の再検証は対象外 | 配備後の実画面 |
.html |
文字範囲・成功 | 対象外 | 今回の再検証は対象外 | 配備後の実画面 |
.htm |
文字範囲・成功 | 対象外 | 今回の再検証は対象外 | 配備後の実画面 |
.png |
ページbbox・成功 | 対象外 | 今回の再検証は対象外 | 配備後の実画面 |
.jpg |
ページbbox・成功 | 対象外 | 今回の再検証は対象外 | 配備後の実画面 |
.jpeg |
ページbbox・成功 | 対象外 | 今回の再検証は対象外 | 配備後の実画面 |
検証範囲と制約
- URLと原文readerの関連画面spec:34成功。desktop/375pxを含む。lint・build・完全CI成功。
- 原文プレビューと構造化readerの関連pytest:32成功。MIME判定、記録表示、元Office bytesの保持、派生hash不一致409を確認。ruff・mypy・完全CI成功。
- 実環境では17形式の原文ハイライトと、CSV/Excel/Wordの原本downloadを確認した。
- 本文の正しさやLLMの全引用行の精度を100%保証するテストではない。版・元要素・引用位置が確認できる範囲を表示する。
再確認で直した試験手順
- 初回の一部画像は読み込み完了前を撮影したため採用せず、mark/bbox表示を待って撮り直した。
- 要約カードにはページのbboxがない。元要素を持つ根拠を選び直した。要約の表示を、精密な原文位置として数えない。
- PPTの解析画面では処理後PDFを選択。画面が表示中のページに元要素があることを確かめた。
- HTMLの初回は再描画中のDOM参照が外れたため、再取得するlocatorで再実行した。















