ローカルLLMを使ってRAGシステムを組み最新の論文を追いやすくする試み
arXivには1日あたり全体では800件ほどの論文が投稿されます。Quantum Physicsカテゴリだけでも50~100件ほどです。これだけの量を自力で追うのは現実的ではないでしょう。そこで、投稿論文をLLMで要約してRAGのナレッジベースに蓄積し、全体の動向を把握しやすくするシステムを実験的に構築しました。正直前の記事と思いっきり内容が被っているのですが、1つ1つの論文を詳しく読むというよりも、全体の動向を把握することに重点を置いたシステムであるという点では違いがあります。
この記事のコードはGitHubで公開しています。
システム全体図
このシステムはDocker Compose上で動作させることを想定しています。主要なコンテナは3つ(RAGFlow, Ollama, Python worker)です。この他に補助的なコンテナ(ElasticSearchやMySQL)もいくつか動いています。
RAGFlow: RAG基盤のフレームワーク
RAGFlowはその名の通りRAGシステムを構築するためのフレームワークです。チャットbotと会話するためのWeb UIを提供し、内部のナレッジベースやOllamaと連携しユーザーの質問に回答します。RAGフレームワークはRAGFlowに限らず様々なものがあり、実際どれを使ってもこのシステムを構築することは可能だと思います。しかし当時注目されていたRAPTORに対応しているところに勢いを感じたので選んだという経緯があります。
Ollama: ローカルで動かすLLMランタイム
Ollamaはローカル環境で大規模言語モデル(LLM)を動かせるランタイムです。特にこだわりがあるわけではなく、とりあえず動作させやすいという理由で今回はOllamaを使っています。
RAGFlowコンテナからはナレッジベースからの検索やユーザーへの回答生成のために呼び出され、Python workerからも一時的なプチRAGシステムの構築のために呼び出されます。
Python worker: レポート作成担当
内部でレポート生成プログラムを動作させるためのコンテナです。
システム動作準備
- Docker Composeで各コンテナを起動
- Ollamaに必要なLLMをインストール
- RAGFlowの初期設定
- レポート生成プログラムの初期設定
Docker Composeで各コンテナを起動
RAGFlowのリポジトリで公開されているRAGFlowを動作させるためのdocker-compose.ymlの上にこのシステムで使うOllamaとPython workerのサービスを乗せるという構成になっているため、少し込み入った内容になります。
- RAGFlowのリポジトリと、このシステムのリポジトリ(qllm)をクローン
git clone https://github.com/infiniflow/ragflow.git
git clone https://github.com/forest1040/qllm.git
- RAGFlowのディレクトリに移動
cd /path/to/ragflow
- 動作確認済みのコミットをチェックアウト
git checkout c0799c53b32c4ab981c4fc46607adf2b2897d7cc
-
docker/.env
を編集
-
TIMEZONE
の値を修正 (Asia/Tokyo) -
VOLUME_DIR
の値を追加
VOLUME_DIR=/path/to/qllm/ragflow/volumes
-
ES_GROUP
の値を追加、${VOLUME_DIR}/ragflow_elasticsearch
に対しての書き込み権限があるgidを設定
ES_GROUP=xxxxx
-
OLLAMA_PORT
の値を追加、特に問題がなければ11434が良い
OLLAMA_PORT=11434
- docker-composeファイルの修正
cp /path/to/qllm/ragflow/docker-compose-base.yml docker/docker-compose-base.yml
cp /path/to/qllm/ragflow/docker-compose.yml docker/docker-compose.yml
- qllmのディレクトリに移動
cd /path/to/qllm/research_paper_report_generation
- 環境変数ファイル(.env)を作成
RAGFLOW_DIR=/path/to/ragflow
- 起動
docker compose --profile elasticsearch -f docker-compose-ragflow.yml up -d
必要なLLMをダウンロード
docker compose --profile elasticsearch -f docker-compose-ragflow.yml exec ollama ollama pull hf.co/rinna/qwen2.5-bakeneko-32b-instruct-gguf:Q8_0
docker compose --profile elasticsearch -f docker-compose-ragflow.yml exec ollama ollama pull bge-m3
docker compose --profile elasticsearch -f docker-compose-ragflow.yml exec ollama ollama pull llama3.3
他にも好みのモデルがあればダウンロードしてOK
RAGFlowの初期設定
RAGFlowのWeb UIから、初期設定を行います。まずブラウザで http://localhost:80 にアクセスします。
モデルの設定
RAGFlowのマニュアル(Deploy LLM locally)に従ってモデルの設定を行います。
5-1の手順で入力するModel type, Model nameはそれぞれ以下の通り。
- chat, hf.co/rinna/qwen2.5-bakeneko-32b-instruct-gguf:Q8_0
- embedding, bge-m3
- chat, llama3.3
5-2の手順で入力するbase URLは
http://ollama:11434
6の手順では、Chat modelにllama3.3, Embedding modelにbge-m3を設定します。
6まで進められれば設定完了です。
ナレッジベースの作成
RAGFlowのマニュアル(Configure knowledge base)に従って、arxiv_paper_reportsという名前でナレッジベースを作成します。それ以外の設定は特に変更する必要はありません。
レポート生成プログラムの設定ファイルを準備
- 依存パッケージのインストール
docker compose --profile elasticsearch -f docker-compose-ragflow.yml exec worker pip install -t python_packages -r requirements.txt
- 設定ファイルを作成
現在のディレクトリ(main.pyやdocker-compose-ragflow.ymlがあるディレクトリ)に設定ファイル(config.yml)を作成します。各項目の内容については設定ファイルを参照してください.
※RAGFlow関連の設定項目について
-
ragflow_api_key
: RAGFlowにログインして設定画面(右上のアバター) > API > API KEY > Create new key で新しいAPIキーを作成できるのでこの値を設定して下さい。 -
ragflow_base_url
: RAGFlowのポートを変更していない場合はhttp://ragflow:80
に設定して下さい。ポートを変更している場合はその値に修正して下さい。 -
ragflow_dataset_id
: RAGFlowにログインしてアップロードしたい対象のナレッジベースの設定画面に移動するとURLに id=xxx のような形でナレッジベースのIDが表示されているのでその値を設定して下さい。
phases:
- DOWNLOAD
- INDEX
- SC
- REPORT
- RAGFLOW
scirate: true
target_date: 2025-09-10
topics:
- quant-ph
num_papers_per_topic: 3
outline_file_path: outline.md
work_dir: results/20250910
max_retry: 1
llm_model_name: hf.co/rinna/qwen2.5-bakeneko-32b-instruct-gguf:Q8_0
embedding_model_name: bge-m3
evaluator_model_name: llama3.3
ollama_base_url: http://ollama:11434
evaluator_guideline: The response was generated with the necessary information given as context for the answer. And as a result of reference to that context, the answer should be to the effect of "exists" rather than "does not exist". If the answer is to the effect of "does not exist", then the requirements of this guideline is not met, but it should be noted that it is the context that should be improved, not the answer in particular. If the question is not the sort to be answered with "does it exist or not", then the requirements of this guideline should be determined to have been met.
section_content_query_instruction: The query should guide the research to gather relevant information for this part of the report. The query should be clear, short and concise. This subsection describes the paper with the same title as the title of this subsection. The query should ask for a brief summary of this paper and its key findings and contributions. The word "query" does not mean SQL query.
section_content_answer_instruction: Please answer this query in Japanese. However, there is no need to force the translation of nouns. Depending on the situation, consider using the original language as is, or katakana for words that are commonly expressed in katakana. The answer should provide a brief summary of the context related to the query, followed by a specific description of key findings and contributions. If specific experimental results are presented, please introduce them. If specific experimental results are not shown, do not say something like “no specific experimental results” and output as if there were no instructions regarding the experimental results.
introduction_instruction: Output in Japanese. Heading is not required. Output only the introduction. There is no need to output the rest of the report.
conclusion_instruction: Output in Japanese. Heading is not required. Output only the conclusion.
ragflow_api_key: ragflow-xxx
ragflow_base_url: http://ragflow:80
ragflow_dataset_id: xxx
- アウトラインファイルを作成
現在のディレクトリ(main.pyやdocker-compose-ragflow.ymlがあるディレクトリ)にレポートのアウトラインファイル(outline.md)を作成します。
# Recent Advances in Quantum Physics: An Overview of Key Findings and Contributions from Recent Papers
## 1. Introduction
## 2. Latest Papers
## 3. Conclusion
- 実行
docker compose --profile elasticsearch -f docker-compose-ragflow.yml exec worker python main.py
運用時にはcrontabなどを用いて1日1回実行する想定です。ただ、現状コマンドライン引数で対象の日付や中間ファイルの保存先を変更することができないため、運用までには機能を追加する必要があります(追加します)。
以上がシステムの準備からレポートのアップロードまでの手順です。
レポート作成プログラム設定ファイル
- phases: 実行するフェーズを指定します。1フェーズずつ経過を確認して実行したい場合や、中間ファイルから再開したい場合に活用してください
- paper_ids: arXivからダウンロードするpdfファイルのIDを直接指定します。これを設定した場合、
scirate
,target_date
,topics
,num_papers_per_topic
の項目は無視されます - scirate: SciRateを参照して評価の高い論文を取得します
- target_date: SciRateから検索する際の対象となる日付を設定します。scirateがtrueの場合のみ有効です
- topics: 論文を検索する分野を指定します
- num_papers_per_topic: 取得する論文の数を設定します。topicsで指定した1分野ごとに、この数の論文を取得します
- outline_file_path: 使用するアウトラインファイルへのパスを指定します。相対パスを指定した場合、main.py が存在するディレクトリからの相対パスと解釈します
- work_dir: 作成したレポートと中間ファイルを格納するディレクトリを指定します。相対パスを指定した場合、main.py が存在するディレクトリからの相対パスと解釈します
- max_retry: SCフェーズでLLMがイマイチな内容を出力した際に最大何回までやり直すかを設定します。設定回数を超えて失敗した場合にはエラー終了します
- llm_model_name: レポートの内容を生成するモデル名を指定します
- embedding_model_name: indexingやretrievalに使用するembeddingモデル名を指定します
- evaluator_model_name: SCフェーズで生成した内容を評価するモデル名を指定します
- ollama_base_url: OllamaのURLを設定します
- evaluator_guideline: evaluatorの評価基準を設定します
- section_content_query_instruction: SCフェーズで内容生成のためのプロンプトを生成する際の指示を設定します
- section_content_answer_instruction: SCフェーズで内容を生成する際の指示を設定します
- introduction_instruction: Instructionを生成する際の指示を設定します
- conclusion_instruction: Conclusionを生成する際の指示を設定します
- ragflow_api_key: RAGFlowのAPIキーを設定します
- ragflow_base_url: RAGFlowのURLを設定します
- ragflow_dataset_id: レポートのアップロード先にするRAGFlowのナレッジベースのIDを指定します
レポート作成プログラムの流れ
ここからはレポート作成プログラムの流れについて解説していきます。各部分に貼っているコードは参考実装です。手元でシステムを動かす際に実装する必要はありません(レポート作成プログラム内でほぼ同じ内容のコードが動作しています)。
- SciRateを参照して1日以内に投稿された評価の高い論文を調べる
- 論文をarXivからダウンロード
- それぞれの論文をLLMを使って要約し、1つのレポートとして出力
- RAGシステムが参照する情報源(ナレッジベース)にレポートをアップロード
SciRateを参照して1日以内に投稿された評価の高い論文を調べる
SciRateはarXivに投稿された論文に"いいね"をつけて注目度や内容の良さを可視化しているサイトです。
1日50~100件なら全部ダウンロードしてしまってもデータ量の問題はないと思いますが、内容の良さや注目度の高いものだけを集めるのも1つの手かと思い、SciRateを参照してScites(いいね)の多い論文のみをピックアップします。
上位xx件という絞り方でもいいし、Scitesがyy個以上という絞り方でも良いと思います。今回は上位xx件で実装しています。
import requests
import bs4
import os
scirate_url = 'https://scirate.com'
def scirate_request(category, date, range_):
r = requests.get(url=os.path.join(scirate_url, 'arxiv', category),
params={
'date': date,
'range': range_,
})
return r.text
def extract_info(row):
title_element = row.select_one('.title > a')
paper_id = title_element['href'].split('/')[-1]
return {
'title': title_element.decode_contents(),
'id': paper_id,
}
def fetch_top_arxiv_paper_ids(category, date, range_=1, max_result=5):
soup = bs4.BeautifulSoup(scirate_request(category, date, range_),
'html.parser')
return [
extract_info(x)['id']
for x in soup.select_one('ul.papers').select('.row', limit=max_result)
]
>>> ids = fetch_top_arxiv_paper_ids('quant-ph', '2025-09-10')
>>> ids
['2509.07255', '2509.07937', '2509.07573', '2509.07276', '2509.07271']
論文をarXivからダウンロード
先の手順で調べた論文のPDFをarXivからダウンロードします。Python用のarXiv APIのラッパーライブラリが作られているようですので、こちらを使わせてもらいます。
後の手順で使うので、論文のタイトルも別で保存しておきます。
import json
import time
import arxiv
topic = 'quant-ph'
target_date = '2025-09-10'
num_papers = 5
work_dir_path = '/path/to/work_dir'
pdf_dir_path = os.path.join(work_dir_path, 'papers')
titles_file_path = os.path.join(work_dir_path, 'pdf_titles.json')
client = arxiv.Client()
search = arxiv.Search(
id_list=fetch_top_arxiv_paper_ids(
topic,
target_date,
max_result=num_papers,
))
for result in client.results(search):
titles.append(result.title)
result.download_pdf(dirpath=pdf_dir_path)
time.sleep(1)
with open(titles_file_path, 'w') as f:
json.dump(titles, f)
それぞれの論文をLLMを使って要約し、1つのレポートとして出力
この手順については様々な方により、より良い方法が日々提案されているところだとは思いますが、今回はllamacloudのデモを参考に実装しました。
大まかな手順としては以下の通りです。
- ダウンロードしたPDFをパース
- パースした文章を基に要約用の一時的なベクトルストアを作成
- アウトラインファイルに従ってレポートを作成
つまり、ダウンロードしてきたPDFファイルを基に一時的なプチRAGシステムを作り、このプチRAGシステムに対し「<論文のタイトル>というタイトルの論文を要約して下さい」と依頼することでレポートを作っていくという流れです。
(読まなくても良いコメント:) 今回は一時的なプチRAGシステムを作ってレポートを作成する流れにしているが、コンテキスト長の長いLLMに本文を丸々載せた上で要約してもらっても良いかもしれない。この辺りについてはRAG vs Long Contextとしてその長短を議論されているところではある。
ダウンロードしたPDFをパース
pymupdf4llmというPDFパーサを使ってダウンロードしたPDFファイルをパースします。1つ後の手順で使うLlamaIndexというフレームワークと互換性のある無料のライブラリということで、今回使うことにしました。
from pathlib import Path
import pymupdf4llm
def list_papers(directory):
return [
os.path.join(directory, file.name)
for file in Path(directory).glob('*.pdf')
]
def parse_papers(file_paths):
documents = []
llama_reader = pymupdf4llm.LlamaMarkdownReader()
for index, file_path in enumerate(file_paths):
title = ' '.join(file_path.split('.')[2].split('_'))
document = llama_reader.load_data(file_path)
for x in document:
x.metadata['title'] = title
documents.extend(document)
return documents
documents = parse_papers(list_papers(pdf_dir_path))
パースした文章を基に要約用の一時的なベクトルストアを作成
PDFファイルから抽出した文章を一時的なベクトルストアに保存します。ベクトルストアという言葉は聞き慣れないかもしれません。簡単に説明すると、「文章(に限らないが)の意味の近さ」を比較して検索できるデータベースです。RAGシステムが参照する知識の保存形式として主流な形式です。専用のAIモデル(Embeddingモデルと言います)を使い文章を1つのベクトルに変換し、このベクトル同士の類似度を文章の意味の類似度として解釈します。もちろんこのEmbeddingモデルの性能にベクトルストアの性能が左右されることになります。
from llama_index.core import VectorStoreIndex, Settings
from llama_index.embeddings.ollama import OllamaEmbedding
embed_model_name = 'bge-m3'
ollama_base_url = 'http://ollama:11434'
index_dir_path = os.path.join(work_dir_path, 'index')
Settings.embed_model = OllamaEmbedding(
model_name=embed_model_name, base_url=ollama_base_url)
index = VectorStoreIndex.from_documents(documents)
index.storage_context.persist(persist_dir=index_dir_path)
アウトラインファイルに従ってレポートを作成
アウトラインファイルに従って内容をLLMに生成させていきます。
コードがかなり長いためこの記事にコードを貼ることを断念しました。generate_section_contents, generate_reportの部分がレポートの作成を担当する関数です。
RAGシステムが参照する情報源(ナレッジベース)にレポートをアップロード
RAGFlow APIを使って生成したレポートをアップロードします。RAGFlowの設定については後のセクションで解説します。
from uuid import uuid4
from ragflow_sdk import RAGFlow
def upload_to_ragflow(rag_object, dataset_id, document_path):
dataset = rag_object.list_datasets(id=dataset_id)[0]
filename = f'{uuid4()}.md'
with open(document_path, 'rb') as f:
content = f.read()
dataset.upload_documents([{
'display_name': filename,
'blob': content,
}])
document = dataset.list_documents(keywords=filename, page_size=1)[0]
dataset.async_parse_documents([document.id])
ragflow_api_key = 'xxx'
ragflow_base_url = 'http://ragflow:80'
ragflow_dataset_id = 'xxx'
upload_to_ragflow(
RAGFlow(api_key=ragflow_api_key, base_url=ragflow_base_url),
ragflow_dataset_id,
result_file_path,
)
まとめ
この記事ではRAGFlow + OllamaによるローカルRAGシステム上にarXivの最新論文の要約レポートを蓄積していき、動向を把握しやすくするシステムを構築する手順について解説しました。また、要約レポートをLLMを使って生成する流れについても解説しました。今回はシステム構築までの紹介でしたが、今後は実運用を通して精度や運用性を検証し、改善点を見つけていきたいと考えています。そのあたりも追って記事化できればと思います。
Discussion