LLMバッチ推論によるドキュメントサイトのメンテナンス効率化

書くのを少し迷いましたが、ユースケースとしては有用だと思いましたのでこちらにまとめます。

# LLMバッチ推論とは

大量のテキストに対して[LLMをバッチ処理で適用](https://docs.databricks.com/aws/ja/machine-learning/model-inference)して、効率改善などビジネス上の課題を解決します。

# 適用条件

- テキストが大量にある
- 処理内容がLLMに適している(要約、情報抽出、翻訳、内容チェックなど)

# 適用事例

- [Databricksマニュアルサイト](https://docs.databricks.com/aws/ja/)の品質チェック

# 背景

Databricksの日本語マニュアルは英語からの機械翻訳がベースとなっています。私を含めて日本人のエンジニア数名でチェックはしているのですが、どうしてもチェック漏れが発生していました。以下の画像のように、英単語があると語順がおかしくなるケースが散見されていました。

![Screenshot 2025-04-22 at 9.38.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/640bd48c-db30-4b5a-833d-fde5e6d94fff.png)

新たに翻訳されたページの連絡はもらっているものの、すべてのページの目検をやり続けるには限界があります。

# アプローチ

- マニュアルページの一覧は[サイトマップ](https://docs.databricks.com/aws/ja/sitemap.xml)として公開されているので、これをクローリングしてDeltaテーブル化します。このテーブルに対してLLMバッチ推論で一括でチェックするようにしました。
- プロンプトは以下の通りです。判定結果だけでなく、理由も出すように指示しています。

> あなたは日本語が流暢なDatabricksの専門家です。右のDatabrickマニュアルを確認します。日本語としておかしい箇所、特に語順の間違いに注意してください。「SDKDatabricksOAuth」のような空白を含まない英語の羅列に注意してください。語順がおかしい可能性が高いです。固有名詞が英語のままであることは許容しますが、一般用語の翻訳漏れにも注意してください。全体的に日本語としての品質が低い場合にはNG、問題がなければOKを返してください: 

- 判定結果はダッシュボードで確認できるようにしました。

# 実装

## サービングエンドポイントの準備

LLMバッチ推論では高いスループットが必要になるので、[Provisioned Throughput](https://docs.databricks.com/aws/ja/machine-learning/foundation-model-apis/deploy-prov-throughput-foundation-model-apis)(毎回翻訳を悩むのですが、スループット配備済みとかなんですかね。要ははスループット保証型です)のモデルサービングエンドポイントを使っています。モデルはLlama 3.1 405b instructです。

![Screenshot 2025-05-02 at 15.30.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/7d7fa0d5-f16a-41b5-8eb1-aecdcfb714ae.png)

## ノートブック

```py
%pip install bs4 lxml
%restart_python
```

```py
import requests
from bs4 import BeautifulSoup

def extract_title_and_contents(url):
  response = requests.get(url)
  soup = BeautifulSoup(response.content.decode("utf-8", "ignore"), 'html.parser')

  # 更新時刻
  last_modified = soup.find('time').get_text()

  # タイトル
  title = soup.find('title').get_text()
  # 本文
  contents = soup.find('div', class_="theme-doc-markdown markdown").get_text()

  return title, contents, last_modified
```

### クローリング

```py
import requests
from bs4 import BeautifulSoup

# 指定されたURL
sitemap_url = "https://docs.databricks.com/aws/ja/sitemap.xml"

# URLの内容を取得
response = requests.get(sitemap_url)
soup = BeautifulSoup(response.content, "lxml-xml")

# URLを抽出
urls = [loc.text for loc in soup.find_all("loc")]

doc_pages = []
count = 0

# 抽出されたURLを表示
for url in urls:

    # 除外
    # https://docs.databricks.com/aws/ja/release-notes/
    # https://docs.databricks.com/aws/ja/sql/language-manual/
    # https://docs.databricks.com/aws/ja/error-messages/

    if (
        url.startswith("https://docs.databricks.com/aws/ja/release-notes/")
        or url.startswith("https://docs.databricks.com/aws/ja/sql/language-manual/")
        or url.startswith("https://docs.databricks.com/aws/ja/error-messages/")
    ):
        continue

    print(url)
    title, contents, last_modified = extract_title_and_contents(url)
    doc_pages.append((url, title, contents, last_modified))

    count = count + 1
```

### Deltaテーブルに保存

```py
import pyspark.sql.functions as f
import pyspark.sql.types as T
from pyspark.sql.types import TimestampType

df = spark.createDataFrame(
    doc_pages,
    "url string, title string, contents string, last_modified string"
).withColumn("last_modified", f.to_timestamp("last_modified", "yyyy年M月d日"))

display(df)
```
![Screenshot 2025-05-02 at 15.30.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/53cae00d-e34e-42d1-9e36-ed239922a896.png)

```py
df.write.mode("overwrite").saveAsTable("takaakiyayoi_catalog.doc_maintenance.databricks_docs")
```

### LLMバッチ推論

[ai_query関数](https://docs.databricks.com/aws/ja/sql/language-manual/functions/ai_query)を使うことで、SQLから直接LLMにアクセスできます。出力を構造化するように`responseFormat`を指定しています。

```sql
%sql
INSERT INTO takaakiyayoi_catalog.doc_maintenance.databricks_doc_judge_result
SELECT *,
  ai_query(
    'taka-llama-pt',
    "あなたは日本語が流暢なDatabricksの専門家です。右のDatabrickマニュアルを確認します。日本語としておかしい箇所、特に語順の間違いに注意してください。「SDKDatabricksOAuth」のような空白を含まない英語の羅列に注意してください。語順がおかしい可能性が高いです。固有名詞が英語のままであることは許容しますが、一般用語の翻訳漏れにも注意してください。全体的に日本語としての品質が低い場合にはNG、問題がなければOKを返してください: " || contents,
    responseFormat => 'STRUCT<doc_judge:STRUCT<result:STRING, explanation:STRING>>'

    ) AS judge_result,
  current_date() AS processed_date
  FROM takaakiyayoi_catalog.doc_maintenance.databricks_docs
```

実行結果にはOKかNGの判定結果、そして判定理由が含まれています。

![Screenshot 2025-05-02 at 15.36.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/8dfc7f96-d9ed-46f7-b770-29987c756a3b.png)

## ジョブの設定

ノートブックとダッシュボードを組み合わせます。

![Screenshot 2025-05-02 at 15.37.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/804028fd-3e02-4d01-90a0-ba7919ffd9a4.png)

## ダッシュボード

判定結果の時系列変化と、最新の判定結果を確認できるようにしています。

![Screenshot 2025-05-02 at 15.38.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/c259b138-d71e-4826-9502-1aecc405bcc2.png)

以下のように判定結果のJSONは直接読み込むことができますので、ダッシュボードへの繋ぎ込みも楽です。

```sql
SELECT
  *,
  judge_result:result AS OK_NG,
  judge_result:explanation AS REASON
FROM
  takaakiyayoi_catalog.doc_maintenance.databricks_doc_judge_result
WHERE
  processed_date = (
    SELECT
      MAX(processed_date)
    FROM
      takaakiyayoi_catalog.doc_maintenance.databricks_doc_judge_result
  )
```

# まとめ

- LLMバッチ推論は簡単にジョブに組み込めるので、内容チェックからダッシュボードの可視化まで自動化できます。
- バッチ処置では、複数の値(今回の例なら判定結果と理由)を返却するように指示できるので、後段で柔軟な処理が可能です。
- Databricksには[スループット保証のAPI](https://docs.databricks.com/gcp/ja/machine-learning/foundation-model-apis/deploy-prov-throughput-foundation-model-apis)があるので、高速にバッチ処理を行うことができます。

効率化しつつも、引き続きドキュメントの品質改善に努めてまいります。今後は複数観点(内容の平易さなど)でのチェックも組み込みたいと考えています。

### はじめてのDatabricks

[はじめてのDatabricks](https://qiita.com/taka_yayoi/items/8dc72d083edb879a5e5d)

### Databricks無料トライアル

[Databricks無料トライアル](https://databricks.com/jp/try-databricks)