開発中のRAGシステムで、ある日突然「PDFアップロード時にエラーが発生する」という問い合わせが舞い込んできました。今回は、なぜこのエラーが発生したのか、どのように調査を進め、最終的にどのように解決したのか、そのプロセスを振り返ります。
システムの背景と発端
システム概要
- 目的:アップロードされたファイル内容をもとに、ユーザの質問に回答するRAGシステム
- 仕組み:ユーザがファイル(PDF、JSONなど)をアップロードし、その内容を保存し質問があれば適宜検索して回答を生成
- インフラ:ファイルはAWS S3上に保存。バックエンドはpythonでlangchainなどの各種ライブラリを活用
問題発生の発端
ある日、「PDFファイルをアップロードするとエラーになって使えない」との報告を受けました。
JSONなど他のフォーマットでは正常に動作しているとのことで、PDFに特有の問題かもしれないと感じました。
ログと現象の解析
エラー内容の確認
アプリケーションログを確認したところ、以下のメッセージが目に飛び込んできました。
- エラー内容:
langchain_community.document_loaders.s3_file.S3FileLoader
でファイルをload
する際に「HTTP Error 403: Forbidden」が発生。どういった理由から403になったかといった情報は得られず(実は後述するようになくはなかったのですが…)。
一見すると、S3へのアクセス権限の問題のように見えましたが、ログを丹念に追っていくと少し違う様相が…。
S3ログとの不整合
- S3アクセスログ:PDFアップロード時のS3ログをチェックすると、実際のアクセス記録が残っていない
- EC2からのアクセス:適当にEC2インスタンスを立ち上げて同じバケットに対してアクセスすると、正常にログが記録されるため、S3バケット自体に問題はなさそう
この時点で、エラーはS3のアクセス制御から発生しているのではなく、別の箇所で403エラーが発生している可能性に行き着きました。
調査の深堀と原因究明
スタックトレースの徹底解析
ログのスタックトレース(と対応するライブラリのソースコード)を改めて確認すると、langchainから呼ばれるunstructured
パッケージ内部で、NLTKを取得する処理中に403エラーが発生していることが判明しました。
スタックトレース部分の抜粋
File "/usr/local/lib/python3.10/site-packages/langchain_core/document_loaders/base.py", line 30, in load return list(self.lazy_load()) File "/usr/local/lib/python3.10/site-packages/langchain_community/document_loaders/unstructured.py", line 107, in lazy_load elements = self._get_elements() File "/usr/local/lib/python3.10/site-packages/langchain_community/document_loaders/s3_file.py", line 135, in _get_elements return partition(filename=file_path, **self.unstructured_kwargs) File "/usr/local/lib/python3.10/site-packages/unstructured/partition/auto.py", line 341, in partition elements = partition_pdf( File "/usr/local/lib/python3.10/site-packages/unstructured/documents/elements.py", line 605, in wrapper elements = func(*args, **kwargs) File "/usr/local/lib/python3.10/site-packages/unstructured/file_utils/filetype.py", line 706, in wrapper elements = func(*args, **kwargs) File "/usr/local/lib/python3.10/site-packages/unstructured/file_utils/filetype.py", line 662, in wrapper elements = func(*args, **kwargs) File "/usr/local/lib/python3.10/site-packages/unstructured/chunking/dispatch.py", line 74, in wrapper elements = func(*args, **kwargs) File "/usr/local/lib/python3.10/site-packages/unstructured/partition/pdf.py", line 210, in partition_pdf return partition_pdf_or_image( File "/usr/local/lib/python3.10/site-packages/unstructured/partition/pdf.py", line 357, in partition_pdf_or_image out_elements = _process_uncategorized_text_elements(elements) File "/usr/local/lib/python3.10/site-packages/unstructured/partition/pdf.py", line 966, in _process_uncategorized_text_elements new_el = element_from_text(cast(Text, el).text) File "/usr/local/lib/python3.10/site-packages/unstructured/partition/text.py", line 295, in element_from_text elif is_possible_narrative_text(text): File "/usr/local/lib/python3.10/site-packages/unstructured/partition/text_type.py", line 74, in is_possible_narrative_text if exceeds_cap_ratio(text, threshold=cap_threshold): File "/usr/local/lib/python3.10/site-packages/unstructured/partition/text_type.py", line 270, in exceeds_cap_ratio if sentence_count(text, 3) > 1: File "/usr/local/lib/python3.10/site-packages/unstructured/partition/text_type.py", line 219, in sentence_count sentences = sent_tokenize(text) File "/usr/local/lib/python3.10/site-packages/unstructured/nlp/tokenize.py", line 134, in sent_tokenize _download_nltk_packages_if_not_present() File "/usr/local/lib/python3.10/site-packages/unstructured/nlp/tokenize.py", line 128, in _download_nltk_packages_if_not_present download_nltk_packages() File "/usr/local/lib/python3.10/site-packages/unstructured/nlp/tokenize.py", line 86, in download_nltk_packages urllib.request.urlretrieve(NLTK_DATA_URL, tgz_file_path) File "/usr/local/lib/python3.10/urllib/request.py", line 241, in urlretrieve with contextlib.closing(urlopen(url, data)) as fp: File "/usr/local/lib/python3.10/urllib/request.py", line 216, in urlopen return opener.open(url, data, timeout) File "/usr/local/lib/python3.10/urllib/request.py", line 525, in open response = meth(req, response) File "/usr/local/lib/python3.10/urllib/request.py", line 634, in http_response response = self.parent.error( File "/usr/local/lib/python3.10/urllib/request.py", line 563, in error return self._call_chain(*args) File "/usr/local/lib/python3.10/urllib/request.py", line 496, in _call_chain result = func(*args) File "/usr/local/lib/python3.10/urllib/request.py", line 643, in http_error_default raise HTTPError(req.full_url, code, msg, hdrs, fp) urllib.error.HTTPError: HTTP Error 403: Forbidden
- 気づき:処理中にアクセスされるURLから、NLTKのリソースがS3上にホストされている可能性を感じる
外部情報との照合
「unstructured 403」などのキーワードで検索すると、GitHub上に同様の現象を報告するissue(Issue #3795)が見つかりました。
- 確認ポイント:ある時期を境に、NLTK取得時の403エラーが多発するようになったとの報告
- 原因:パッケージ側でのアクセス先変更や認証方法の見直しが行われた可能性
解決策とその適用
対応策の決定
調査の結果、今回利用していたunstructured
のバージョンは0.15.7でしたが、issueでは0.16.11でこの問題が解消されていると記述されていました。
- 対応:
unstructured
のバージョンを0.16.11へアップデート
結果の検証
アップデート後、PDFアップロードテストを実施。
- 検証結果:アップデート後は、以前発生していた403エラーが解消され、PDFファイルも正常に処理されるようになった
結論
今回のPDFアップロードエラーは、一見S3のアクセス権限に起因しているように見えたものの、実際はunstructured
パッケージ内部のリソース取得処理に起因していました。問題発生時は原因が複数のコンポーネント間で分散していることが多く、その背景や相互作用を正しく把握することが、問題解決への近道となります。
今回の経験を通じて、外部依存パッケージの管理やログの徹底分析の重要性を改めて実感しました。