
プロンプトを何度直しても精度が上がらない——そういう壁にぶつかったとき、疑うべき場所がもう一つあります。
「LLM に何を渡しているか」、つまり入力フォーマットです。
Microsoft が公開している OSS markitdown を使うと、Word・Excel・PDF といった Office ファイルを Markdown に変換して LLM に渡せる状態に整えられます。
CLI と Python API の実際のコードを見ながら、「変換してから渡す」という前処理の考え方を掘り下げます。
LLMへの入力、フォーマットで精度が変わる
「精度が出ない」と感じたとき、多くのエンジニアはまずプロンプトを疑います。
でも、プロンプトを磨く前に確認すべきことが一つあります。
LLM に実際に渡っているテキストが、どんな状態になっているかを目で見てみることです。
PDF を API に渡すとき、内部では何が起きているか想像してみましょう。
pdfminer のようなテキスト抽出処理が走ったとき、出力されるのはいわゆる「生テキスト」です。
見出しの構造は消え、2段組レイアウトなら左右のカラムが混ざって出てきます。
表はセルの構造を失い、値が空白区切りで一列に並ぶだけになります。
ヘッダーやフッターがドキュメント本文と区別なく混入することもあります。
こういう状態のテキストを精度の高いモデルに渡しても、文書構造を推測することに注意が消費され、本来の質問への回答精度が落ちます。
HTML はどうでしょうか。
タグで構造が表現されている分、生 PDF よりはましです。
ただしマークアップ・スクリプト・スタイル定義がコンテンツと混在するため、実際のコンテンツ量に対してトークン数が膨らみやすいです。
MindStudio の調査によれば、HTML を Markdown に変換することでトークン数が 80〜90% 削減されるケースがあるとされています。
Word・PDF でも変換前後で 50〜70% のトークン削減が見込めるとのことで、処理コスト面でも無視できない差です。
では、なぜ Markdown が LLM との相性がいいのか。
Markdown が向いている理由は、学習データとの親和性と、見出し階層が Attention の手がかりになる点にあります。
GitHub・技術ドキュメント・ブログなど、Markdown 形式のテキストはウェブ上に大量に存在します。
現代の LLM はそれらを学習しているため、Markdown の記法を自然に解釈できます。
さらに H1・H2・H3 の一貫した階層があると、ドキュメントの構造が明示されるため、セクション境界の認識がしやすくなります。
見出しのない連続テキストの塊よりも、構造化された Markdown の方が、特定セクションへの注目精度が上がります。
入力フォーマットの見直しは、プロンプト改善よりも即効性がある場合があります。
「バイナリをそのまま渡す」から「変換してから渡す」という一歩が、精度とコストの両方に影響します。
markitdownのインストールと対応フォーマットの実態
markitdown は Microsoft が公開している Python 製の OSS です。
さまざまなファイル形式を統一インターフェースで Markdown に変換でき、ローカルで完結して追加のクラウドサービスに依存しないことが特徴です。
Docling のような高機能な OSS もありますが、markitdown は軽量・シンプルで「まずこれ一本で試す」という用途に向いています。
インストールは pip で完結します。
Python 3.10 以上が必要なので、事前にバージョンを確認してください。
# 基本インストール
pip install markitdown
# 音声変換・画像OCRなどのオプション依存も含める場合
pip install "markitdown[all]"
対応フォーマットは次のとおりです。
- Word(.docx) — 見出し・箇条書き・太字などの書式を Markdown に変換
- Excel(.xlsx) — 複数シートをシート名の見出し付きで展開
- PDF — テキスト層を抽出して Markdown に整形
- PowerPoint(.pptx) — スライドごとにセクションを区切って変換
- 画像(.jpg / .png など) — alt テキスト化(LLM を別途呼ぶオプションで画像説明の生成も可能)
- 音声・動画 — 文字起こしテキストに変換(オプション依存が必要)
- HTML・CSV・JSON — それぞれ構造を保ちながら Markdown 形式へ
一方で苦手なケースもあります。
段組みや図表が混在した PDF(プレスリリース・学術論文など)は、変換後に構造が崩れやすいです。
数式は LaTeX 形式として残るケースもありますが、変換精度にばらつきがあります。
Excel の複数シートはシート名が Markdown の見出しとして挿入されたあとに各シートの内容が展開される形になり、実務では扱いやすい挙動です。
CLI での単体変換はこのコマンドだけです。
# ファイルを変換してMarkdownファイルとして保存
markitdown input.docx -o output.md
# 標準出力に流して先頭だけ確認する
markitdown input.pdf | head -50
変換結果を後段で使う前にパイプ経由で先頭を確認する習慣をつけておくと、想定外の崩れに早めに気づけます。
Python APIで変換パイプラインを組む
CLI は手軽ですが、複数ファイルの処理や後続の API 呼び出しと連携させるなら Python API の方が柔軟です。
基本は MarkItDown クラスをインスタンス化して convert メソッドにファイルパスを渡すだけです。
返り値オブジェクトの text_content プロパティに、変換済みの Markdown テキストが文字列として格納されています。
from markitdown import MarkItDown
md = MarkItDown()
result = md.convert("document.docx")
# 変換済みMarkdownテキスト
print(result.text_content)
変換結果を実際に使う前に、中身を目視確認することを強くすすめます。
見出し階層が H1〜H3 で対応しているか、Excel の複数シートがシート名の見出しとともに展開されているか、表が Markdown のテーブル形式になっているか——この三点を変換後テキストの先頭 200 行ほどで確認するだけでも、後段でどんな崩れが起きるか事前に察知できます。
トークン数の見積もりも変換後にやっておきましょう。
実際の値はモデルやトークナイザーによって異なるため、tiktoken などで実測するのが確実です。
使おうとしているモデルのコンテキスト上限と変換後のテキスト量を比較する習慣をつけておくと、後で詰まることが減ります。
実務では複数ファイルをまとめて処理したい場面が多いはずです。
指定ディレクトリ内の Office ファイルをすべて Markdown に変換して保存するスクリプトの例を示します。
from pathlib import Path
from markitdown import MarkItDown
def convert_directory(input_dir: str, output_dir: str) -> None:
input_path = Path(input_dir)
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
extensions = {".docx", ".xlsx", ".pptx", ".pdf"}
md = MarkItDown()
for file in input_path.rglob("*"):
if file.suffix.lower() not in extensions:
continue
try:
result = md.convert(str(file))
out_file = output_path / file.with_suffix(".md").name
out_file.write_text(result.text_content, encoding="utf-8")
print(f"変換完了: {file.name} -> {out_file.name}")
except Exception as e:
print(f"変換失敗: {file.name} - {e}")
if __name__ == "__main__":
convert_directory("./docs", "./docs_md")
extensions のセットを変えれば対象フォーマットを絞ることができます。
変換に失敗したファイルはスキップして続行する形にしているので、複雑なレイアウトの PDF が混在していても途中で止まりません。
本番環境では失敗ログをファイルに書き出す処理を追加しておくと、変換品質の追跡が楽になります。
変換済みMarkdownをLLM APIに渡す最小構成
変換済みのテキストを手に入れたら、あとは API のメッセージに埋め込むだけです。
OpenAI Chat Completions API への渡し方の最小コードを示します。
from markitdown import MarkItDown
from openai import OpenAI
client = OpenAI() # OPENAI_API_KEY 環境変数を参照
md = MarkItDown()
result = md.convert("report.docx")
document_text = result.text_content
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "system",
"content": "あなたは社内文書の要約・分析を行うアシスタントです。"
},
{
"role": "user",
"content": f"以下のドキュメントを要約してください。\n\n{document_text}"
}
]
)
print(response.choices[0].message.content)
Anthopic の公式 SDK を使う場合も、構造はほぼ同じです。
from markitdown import MarkItDown
import anthropic
client = anthropic.Anthropic() # ANTHROPIC_API_KEY 環境変数を参照
md = MarkItDown()
result = md.convert("report.docx")
document_text = result.text_content
message = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=2048,
system="あなたは社内文書の要約・分析を行うアシスタントです。",
messages=[
{
"role": "user",
"content": f"以下のドキュメントを要約してください。\n\n{document_text}"
}
]
)
print(message.content[0].text)
Anthopic は OpenAI 互換の API レイヤーも提供しており、OpenAI SDK の base_url を切り替えることで OpenAI ライブラリのまま Claude を呼ぶことも一応できます。
ただし機能の差異があるため、本番コードでは anthropic 公式ライブラリを使う方が安全です。
コードを動かす前に一つ確認してほしいことがあります。
機密文書を外部 API に送る前に、社内の情報セキュリティポリシーを確認してください。
契約書・財務資料・個人情報を含むファイルをそのままクラウド API に渡せるかどうかは、組織によって判断が異なります。
ローカルで動作する LLM(Ollama 経由など)と組み合わせるか、処理対象のファイル種別をあらかじめ絞ることで対処できる場面が多いです。
コンテキスト長の問題も実務では頻繁に出てきます。
社内ドキュメントは想像以上に長いことが多く、変換後の Markdown が数万文字を超えることも珍しくありません。
上限を超えそうなファイルはセクション単位で分割してから渡す、もしくは RAG やベクトル DB と組み合わせる構成を検討する必要があります。
markitdown はあくまでその「前段」を担うツールで、変換済みの Markdown を検索インデックスに流し込む部分は別途実装が必要です。
更新頻度の低い社内ドキュメントなら、変換結果を .md ファイルとしてキャッシュしておくのが現実的です。
毎回の API 呼び出しのたびに変換を走らせるより、初回変換済みのテキストを使い回す方が処理時間もコストも抑えられます。
変換後のテキストを一度 print して眺めるだけで、プロンプト改善より先にやるべきことが見えてくるはずです。
株式会社ホコサキは、山口県宇部を拠点に Web 制作・業務システム開発・AI 活用支援を手がけています。
LLM を活用した業務効率化や前処理パイプラインの設計について相談したい方は、お気軽にお声がけください。
お問い合わせはこちら

