この記事はエムスリー Advent Calendar 2025 6日目の記事です。
AI・機械学習チームの池嶋(@mski_iksm)です。
gokartは、AI・機械学習チームのメンバーを中心にOSSとして開発されているパイプラインツールであり、チーム内のほとんどのPythonプロダクトで使用されています。
本記事では、LLMを使った自然言語へのタグ付け処理システムにおいて、gokartを活用することで、増分データの管理コストとLLM利用コストを最小化した事例を紹介します。

LLMバッチにおける「増分データの管理」とコストの課題
AI・機械学習チームでは、医療記事やアンケート回答といった自然言語データに対し、LLMを用いて自動で話題分類(タグ付け)を行うシステムを日次バッチで運用しています。
このシステムにおける最大の課題は、「日々DBに追加されるデータに対し、日次での増分のみを処理できるよう、差分を管理しなければならない」という点でした。
DBには継続的に新規文章がINSERTされます。しかし、毎日全データに対してタグ付けをし直していると、LLMのAPIコスト・実行時間ともに増大してしまいます。そのため、過去に処理済みのデータに対する再処理は避ける必要があります。
新規テキストだけをタグ付けするには、「どこまで処理したか」というステート管理ロジックを整備すれば実現できますが、これはメンテナンスコストの面で課題となっていました。
解決策:gokartによるキャッシュ管理
この課題に対し、パイプラインツールである gokart を導入し、そのキャッシュ機構を「処理済みフラグ」として利用するアプローチを採用しました。
gokartとは
gokartは、エムスリーのメンバーを中心にOSSとして開発しているPythonのデータパイプラインツールです。
Spotify製のLuigiをラップしており、タスク間の依存関係解決や並列実行といった基本機能に加え、入力データのハッシュ値に基づいた強力なキャッシュ管理機能や、パラメータ管理の容易さを特徴としています。
実装アプローチ
具体的な実装方針は以下の通りです。
1. タスク粒度の細分化
通常、バッチ処理ではデータをまとめて処理することが一般的ですが、今回はあえて「1テキスト(記事等であれば1記事ごと、アンケート等であれば1回答ごと) = 1 gokartタスク」という粒度まで分割しました。こうすることで、タグ付けの完了/未完了を1テキストごとに判定できるようになります。
class TagWithLLM(gokart.TaskOnKart): text = luigi.Parameter() def run(self): tagged_text: TaggedText = self._tag_with_llm(text=self.text) self.dump(tagged_text)
2. キャッシュによる増分判定
gokartはタスク実行時にパラメータに基づいたハッシュ値を計算し、出力ファイル(キャッシュ)が存在すれば処理をスキップします。この仕様を活用し、以下のように動作させました。
- 処理済みデータ(昨日以前): キャッシュが存在するため、LLMリクエストは発生せず即終了(loadのみ)。
- 新規データ(当日分): キャッシュがないため、タスクが実行されLLMリクエストが飛ぶ。
これにより、昨日以前からDBに存在していたテキストのタスクは、処理済みのキャッシュを読み込むことでLLM処理が不要となり、当日追加された新規テキストのみをLLMで処理すれば良くなります。
こうして、自前でステート管理ロジックを一切書くことなく、「未処理分だけを自動的に実行する」という要件を満たすことができました。
LLMのように呼び出しに時間がかかる処理の場合には、このキャッシュ機構がかなり強力な威力を発揮します。
副次的なメリット
このアーキテクチャは、データ変更への耐性強化や、コスト効率、パフォーマンスの面でも大きなメリットをもたらしました。
DBのデータが変更された場合
DB上のデータは、同じ記事アイテムであっても文章が一部修正されるなど、内容に変更が生じることがあります。こうした場合、記事IDは変わりませんが、タグ付け処理の再実行が必要になります。
上記TagWithLLMのサンプルコードでは、textという形でテキスト本文自体がタスクのパラメータに含まれています。そのため、記事本文が変更されるとパラメータのハッシュ値(=キャッシュのキー)も変更され、新規記事の場合と同様にタスクが再実行されます。
逆に、全く同じ文章であれば既存のキャッシュを使い回すことも可能です。これはアンケート回答のように、同一の短文が投稿されやすいケースで効果的です。
Spot VM/Instanceでの低コスト運用
タスク単位でキャッシュが効くため、バッチ全体の耐障害性が高くなります。
AWS Spot InstanceやGCP Spot VMのような「安価だが中断リスクのあるインスタンス」で実行し、仮に途中でプリエンプト(中断)されても、再実行すれば「未完了のタスク」のみが処理再開されます。これにより、計算リソースの大幅なコストダウンが可能になりました。
並列処理によるスループット向上
タスク粒度が細かいため、並列化による恩恵を受けやすいのもこの実装のメリットの1つです。
gokartでは、パイプライン全体を複数のworkerに展開するだけで、簡単に分散処理が可能です。
gokartにはキャッシュ機能に加えて、「優先順位が同じタスクはできるだけランダムに実行する」「Redisでロックを取って別workerと重複実行を防ぐ」機能があります。
これにより、複数のインスタンスで同時にバッチを走らせても、多数のタスクが効率的に分散処理され、スループットが向上します。
詳細な仕組みは以下の記事をご参照ください。
まとめ
本記事では、自然言語テキストに対するタグ付け基盤において、gokartを活用した事例を紹介しました。
キャッシュ機構を「処理済みのステータス管理」として転用することで、以下の成果を実現しています。
- 実装コスト削減: 複雑な差分管理ロジックの実装・保守が不要に
- APIコスト削減: 確実なキャッシュ制御によるLLMの二重実行防止
- インフラコスト削減: 耐障害性の向上によるSpotインスタンスでの運用
日々データが増え続けるアプリケーションの運用において、この「ステートレスなバッチ設計」は非常に有効なアプローチだと考えています。同様の課題をお持ちの方は、ぜひgokartの活用を検討してみてください。
We are hiring!
AI・機械学習チームでは、医療現場やWebでの課題解決に取り組むエンジニアを募集しています。
「機械学習モデルの社会実装」や「MLOps基盤の構築・改善」に興味がある方、コスト意識を持った技術選定に関心のある方は、ぜひカジュアル面談でお話ししましょう!