← 参照コンテンツに戻る MOKUMOKU GUIDE

AIエージェント 低コスト大量バッチ処理(HBOパターン)

実践技術教科書 · Haiku Batch Orchestration

対象読者: Claude Code / AIエージェントを業務で使っているエンジニア。ベクトルDB・Ollama・ChromaDB・SBERT の基礎知識がある方。 本書の目的: AIエージェントの「推論コスト」と「計算コスト」を分離し、9時間・17タスク・4万件超のデータ処理を極小コストで完走させた技術体系を、再利用可能な形で伝える。


目次

第I部 技術概念と原理

  1. なぜ「推論」と「計算」を分離するのか
  2. 三層コストモデル — Opus / Haiku / ローカル推論
  3. HBOパターンの3原則
  4. タスク分類マトリクス — 何をどこに委託するか
  5. リソース競合の物理学 — なぜOllamaは共有できないか

第II部 用語辞典

  1. 用語辞典

第III部 システム構成

  1. 全体アーキテクチャ
  2. タスクフロー設計図 — 17タスクの依存関係
  3. データフロー — SQLite・ChromaDB・DuckDB の接続

第IV部 歴史と背景

  1. 技術選定の経緯 — なぜこの構成に至ったか
  2. 失敗の系譜 — 6つの障害と学び

第V部 手順・マニュアル

  1. 9時間バッチを計画する10ステップ
  2. パターン別実装レシピ
  3. トラブルシューティング辞典

第I部 技術概念と原理


1. なぜ「推論」と「計算」を分離するのか

AIエージェントに大量のバッチ処理を任せる場面を想像してほしい。4万件の特許をベクトル化する、1,200冊の書籍にCross-Encoderでリランキングをかける、570件のドキュメントをChromaDBに投入する——こうした処理を、最も高性能なモデル(Opus級)に全て任せるとどうなるか。

コストが処理量に比例して爆発する。

しかし冷静に考えると、4万件のベクトル化において「推論」が必要なのは最初の10分だけだ。どのスクリプトを使うか判断し、パラメータを決め、テスト実行して疎通を確認する——ここまでが「考える仕事」。残りの数時間は、同じAPIを同じパラメータで繰り返し叩くだけの「計算の仕事」である。

             推論(考える)         計算(動かす)
             ┌──────────┐          ┌──────────────────────────┐
4万件の      │ どのスクリ│          │ 同じAPIを40,000回叩く    │
ベクトル化   │ プト?    │          │                          │
             │ パラメータ│          │ → Ollamaで無料            │
             │ は?テスト│          │ → 進捗をJSONで記録        │
             │ OK?      │          │ → エラーは3回リトライ     │
             └──────────┘          └──────────────────────────┘
              ↑ ここだけOpus        ↑ ここはPython直接実行
              (5分、高コスト)       (6時間、コストゼロ)

この分離がHBOパターンの核心だ。推論にはOpus(高精度モデル)を使い、スクリプト作成にはHaiku(低コストモデル)を使い、実際の計算はOllamaやSBERTなどのローカル推論エンジンに任せる。タスクが10倍になっても推論コストはほぼ一定。増えるのは無料の計算時間だけだ。

コスト
  ^
  |     / 全部Opus
  |   /
  |  /
  | /  /-- 全部Haiku
  |/ /
  |/---------- HBO パターン
  +------------------------------> 処理量

2. 三層コストモデル

HBOパターンは3つの層でコストを最適化する。

              +---------+
              |  Opus   |  計画・判断・エラー修復
              | (最小限) |  使用量: セッション全体で1回
              +----+----+
                   |
            +------+------+
            |   Haiku     |  スクリプト作成・レポート生成
            |  (低コスト)  |  使用量: タスクあたり1回
            +------+------+
                   |
         +---------+---------+
         | Ollama / SBERT    |  ベクトル化・分類・スコアリング
         | SQLite / ChromaDB |  使用量: 数万回(全て無料)
         +-------------------+

各層の役割を明確にする:

モデル/ツール 役割 コスト特性
計画層 Opus タスク設計、依存関係分析、エラー診断 高単価・最小量
生成層 Haiku Pythonスクリプト作成、MDレポート生成 低単価・中量
実行層 Ollama / SBERT / SQLite ベクトル化、LLM分類、DB操作 無料・大量

この構造が効く理由は、各層のスケーリング特性が異なるからだ。

結果として、全体コストは処理量に対して対数的に増加する。


3. HBOパターンの3原則

原則1: 分離原則 — 「考える」と「動かす」を分ける

 NG パターン                          HBO パターン
+----------------+                   +----------------+
| Haiku Agent    |                   | Haiku Agent    |
|  スクリプト    |                   |  スクリプト    |---> .py作成
|  作成 + 実行   |---> 権限エラー    |  作成のみ      |
|                |    で停止         +----------------+
+----------------+                          |
                                     +------v------+
                                     | 親プロセス   |
                                     |  直接実行    |---> 確実に完走
                                     | (bash bg)    |
                                     +-------------+

Haikuエージェントはサブプロセスとして動作し、Bash実行権限が親プロセスと異なるスコープで制限されることがある。実際の運用では、11回のHaikuエージェント投入のうち3回が権限問題で実行フェーズでブロックされた。

対策: スクリプト「作成」(Write権限のみ)と「実行」(Bash権限)を分離する。Haikuにはスクリプトを書かせるだけにして、実行は親プロセスから直接行う。

適用判断基準: - 実行に30秒以上かかるバッチ処理 → 必ず分離 - source .venv/bin/activate が必要 → 必ず分離 - SSH経由のリモート操作 → 必ず分離 - 単純なファイル読み書きのみ → 分離不要(Haikuに任せてOK)

原則2: 資源分離原則 — 計算リソースの帯域競合を避ける

ローカルLLMサーバー(Ollama等)は、複数プロセスから同時にリクエストを受けると応答速度が1/Nに低下する。これは推論がGPU/CPUを排他的に使用するためだ。

 NG: 3プロセスがOllama争奪         OK: リソース分離

 [分類]  [embed]  [score]           [分類]         [SBERT]
   |       |        |                 |               |
   v       v        v                 v               v
 +-------------------+           +--------+     +--------+
 |   Ollama API      | <-渋滞   | Ollama |     | CPU    |
 | (実効速度 1/3)    |          | API    |     | (並列) |
 +-------------------+           +--------+     +--------+

リソース競合マップ:

リソース 排他性 同時実行時の影響
Ollama API 準排他 応答速度が1/Nに低下
SBERT (CPU) 共有可 メモリ依存、2並列まで実用的
ChromaDB 書込 排他 DB lockエラー(致命的)
SQLite 書込 排他 ロック待ち
SSH 共有可 問題なし

最適スケジューリング: Ollamaを使うタスクは直列に、CPUを使うタスクは並列に。

Phase 1: [A: Ollamaベクトル化 37K件]  ← Ollama独占
             完了後
Phase 2: [G: Ollama分類] + [P: SBERT] + [Q: CrossEncoder]
              Ollama         CPU          CPU    ← 資源分離
Phase 3: [K: Ollama embed] + [M: Ollama scoring]
              ← Gより優先度低いが、G完了待たず並行投入

原則3: 段階的信頼原則 — まずテスト、次にバッチ

6時間のバッチ処理を開始して5時間後にカラム名エラーで全件失敗——これは避けたい。

Step 1: Haiku がスクリプト作成
        |
Step 2: 親が --demo / --test で疎通確認 (5件)
        | OK
Step 3: 親が全件バッチを run_in_background で起動
        |
Step 4: progress.json を定期確認で監視
        | エラー発生
Step 5: 親が直接修正して再起動 (Haikuに戻さない)

この原則で重要なのはStep 5だ。エラーが起きたとき、Haikuエージェントに修正を再委託するのではなく、親プロセス(Opus)が直接修正する。理由は、Haikuは元のコンテキストを持っていないため、修正に必要な文脈を再度説明するコストが、直接修正するコストを上回るからだ。


4. タスク分類マトリクス

すべてのタスクが同じパターンで実行できるわけではない。以下のマトリクスで最適な実行方式を選択する。

                    実行時間
               短 (< 30min)       長 (30min - 数時間)
            +------------------+------------------------+
  Haiku     | GKS修復           | 記事レビュー            |
  単独で    | todo棚卸し        | SSH経由サービス修復      |
  完結      | (読み→分析→書き)  | (診断→修正→検証)       |
            +------------------+------------------------+
  Haiku     | orgmemory同期     | DocFlow 570件embed     |
  作成 +    | themes投入        | スコアリング 2,240件    |
  親実行    | rerank構築        | 45K分類                |
            |                   | 37K ベクトル化         |
            +------------------+------------------------+
  親プロセス | (該当なし)        | SBERT 27K分析          |
  直接のみ  |                   | (venv依存が複雑)       |
            +------------------+------------------------+

判断フローチャート:

タスクを受け取る
    |
    +-- 実行時間 < 30秒? --> 親が直接実行
    |
    +-- 既存スクリプトがある?
    |     |
    |     +-- YES --> Haiku不要、親が直接実行
    |     |
    |     +-- NO --> Haikuにスクリプト作成を委託
    |
    +-- venvが必要?
    |     |
    |     +-- YES --> 親が直接実行(Haikuはvenv activateが不安定)
    |     |
    |     +-- NO --> Haikuに実行も委託可能
    |
    +-- Ollama / 外部APIを使う?
          |
          +-- YES --> run_in_background で起動(長時間)
          |
          +-- NO --> 通常実行

5. リソース競合の物理学

なぜOllamaに3プロセスを同時に投げると遅くなるのか。

ローカルLLMサーバーは、推論リクエストを受けると以下の処理を行う:

  1. トークン化: 入力テキストをトークンに変換(高速、並列可)
  2. KVキャッシュ確保: モデルの注意機構のためのメモリを確保(GPU VRAM排他)
  3. 推論実行: Transformerの各層を順に計算(GPU排他)
  4. デコード: 出力トークンを文字列に変換(高速、並列可)

ステップ2と3がボトルネックだ。GPUのVRAMとコンピュートユニットは物理的に1つしかない。複数リクエストが来ると、サーバーは以下のいずれかで対応する:

Ollamaはキュー方式のため、3プロセスが同時にリクエストすると実効速度は1/3ではなく、コンテキストスイッチのオーバーヘッドを含めて1/3〜1/4になる。

Embedding API(/api/embed)の場合: nomic-embed-textは軽量(261MB)なので、推論自体は高速(0.05秒/件)。ボトルネックはネットワークレイテンシとリクエストのシリアライズだ。この場合、3並列でも実効速度の低下は1/2程度に収まる。

LLM生成API(/api/generate)の場合: qwen3:8b(5GB)やqwen3:14b(9GB)は重い。1リクエストあたり1〜5秒かかり、並列時はGPUを完全に占有する。3並列では実効速度が1/3以下になる。

結論: Embedding APIは緩い並列を許容できるが、LLM生成APIは直列が最適。


第II部 用語辞典


6. 用語辞典

AIモデル・エージェント

用語 意味
Opus Anthropic Claude の最上位モデル。高精度な推論・計画・コード生成に使用。コスト高。
Haiku Claude の最軽量モデル。高速・低コスト。スクリプト生成やレポート作成に十分な品質。
Ollama ローカルでLLMを実行するオープンソースサーバー。API互換でGPU推論を提供。コストゼロ。
nomic-embed-text Ollama上で動作する軽量Embeddingモデル(768次元、261MB)。ベクトル化の主力。
qwen3:8b / 14b Alibaba開発のLLM。Ollama上で分類・スコアリングに使用。
run_in_background Claude Codeのバックグラウンド実行機能。長時間コマンドを非同期で実行し、完了通知を受け取る。
subagent Claude Code内で起動される子エージェント。特定のタスクを委託され、完了後に結果を返す。

ベクトルDB・検索

用語 意味
ChromaDB Pythonネイティブのベクトルデータベース。PersistentClientでローカルファイルに永続化。
Embedding テキストを固定長の数値ベクトルに変換すること。意味的な類似検索を可能にする。
SBERT (Sentence-BERT) Sentence-Transformersライブラリ。BERTベースの文埋め込みモデル。ローカルCPU実行可能。
CrossEncoder クエリとドキュメントのペアを同時にエンコードし、関連度スコアを出力するモデル。bi-encoderより高精度だが低速。
Reranking bi-encoderで粗く取得した候補を、CrossEncoderで精緻に再ランキングする二段階検索手法。
FTS5 SQLiteの全文検索拡張。BM25ランキングを提供。ベクトル検索との併用(ハイブリッド検索)が有効。

データベース・ストレージ

用語 意味
SQLite ファイルベースの軽量RDB。patent_search.db(45,121件)の基盤。
DuckDB 分析特化の列指向DB。書籍メタデータの管理に使用。
progress.json バッチ処理の進捗を記録するJSONファイル。中断→再開(resume)を可能にする。
DB lock SQLite/ChromaDBで複数プロセスが同時書込みすると発生するロック。致命的エラーの原因。

インフラ・デプロイ

用語 意味
launchd / plist macOSのサービス管理デーモン。plistファイルでサービス定義。launchctlで操作。
Tailscale WireGuardベースのVPN。リモートマシンへのセキュアな接続に使用。
EINTR Unix シグナルによるシステムコール中断。ファイルスキャン中にI/Oが重いと発生。リトライで対処。

第III部 システム構成


7. 全体アーキテクチャ

+---------------------------------------------------------------+
|                     Opus 親プロセス                              |
|              (計画・判断・実行制御・エラー修復)                   |
|                                                                 |
|  +----------+  +----------+  +----------+  +----------+        |
|  | Haiku    |  | Haiku    |  | Haiku    |  | Haiku    |        |
|  | Agent 1  |  | Agent 2  |  | Agent 3  |  | Agent N  |        |
|  | (script  |  | (script  |  | (report  |  | (SSH     |        |
|  |  作成)   |  |  作成)   |  |  生成)   |  |  修復)   |        |
|  +----+-----+  +----+-----+  +----+-----+  +----+-----+        |
|       |             |             |             |                |
|  +----v-------------v-------------v-------------v-----------+   |
|  |          親プロセス直接実行レイヤー                        |   |
|  |     (bash run_in_background / progress.json監視)          |   |
|  +----+-------------+-------------+-------------------------+   |
+-------+-------------+-------------+-----------------------------+
        |             |             |
   +----v----+   +----v----+   +----v-----+
   | Ollama  |   |  SBERT  |   | SQLite   |
   | Mac mini|   | ローカル |   | ChromaDB |
   | (GPU)   |   | CPU     |   | (ファイル)|
   |  無料   |   |  無料   |   |   無料   |
   +---------+   +---------+   +----------+

コンポーネント間通信

Haiku Agent --[Write]--> Python Script (.py)
                              |
Parent Process --[Bash]--> python3 script.py --[HTTP]--> Ollama API
                              |                             |
                              +--[File I/O]--> ChromaDB     |
                              |                             |
                              +--[File I/O]--> SQLite       |
                              |                             |
                              +--[File I/O]--> progress.json|
                              |                             v
Parent Process --[Read]----> progress.json         nomic-embed-text
                              |                    qwen3:8b / 14b
                              v
                         完了通知 → 次タスク起動

8. タスクフロー設計図

実際に実行した17タスクの依存関係と時系列:

時刻  0h     1h     2h     3h     4h     5h     6h     7h     8h     9h
      |------|------|------|------|------|------|------|------|------|

[A] 特許ベクトル37K =====(39min)=>
[B] GKS ChromaDB ==(5min)=>
[C] todo棚卸し ===(1.5min)=>
[D] ランドスケープ ===(3min)=>

                  [E] Mac mini修復 ========(4min)=>
                  [F] watchdog+同期 ==(1min)=>     (E完了後)

[G] 45K分類Phase1+2 =(即完了)= Phase3 ================================>
    (IPC+keyword)               (Ollama LLM、数時間)

[H] 記事レビュー =======(5min)=>
[I] NotebookLM 3NB ===(3min)=>
[J] GKS修復 =(1min)=>

                        [K] DocFlow 570 embed =======(13min)=>
                        [L+O] orgmemory+themes =====(5min)=>
                        [M'] scoring 3cat ========================>

                        [P] SBERT 27K ==(1.5min)=>  (CPU, Ollama非使用)
                        [Q] rerank index =(2min)=>   (CPU, Ollama非使用)

依存関係: - E → F: Mac mini修復完了後にスクリプト同期 - A 完了 → K,M' 起動: Ollama帯域の解放 - P, Q: CPU専用のため、Ollamaタスクと並行可能


9. データフロー

[External DB CSV] --import--> [patent_search.db]
                               SQLite 45,121件
                                    |
                    +---------------+---------------+
                    |               |               |
                    v               v               v
              [batch_embed]   [batch_categorize]  [relevance_scoring]
              Ollama embed    Phase1: IPC rule    Ollama qwen3:14b
              nomic-embed     Phase2: keyword     スコア1-5判定
              768次元         Phase3: Ollama LLM
                    |               |               |
                    v               v               v
              [ChromaDB]      [auto_category列]   [scored_*.csv]
              patent_search   patent_search.db    data/processed/
              37,777 docs     に直接UPDATE

[Obsidian vault] --> [layer2_embedder.py] --> [ChromaDB]
  1,193 books MD      nomic-embed-text        book_insights: 1,193
  41 themes MD                                book_summaries: 2,627
  27 insights MD                              book_raw_text: 149,608
                                              vault_themes_insights: 283

[docflow.db] --> [docflow_vectorize_v2.py] --> [ChromaDB]
  570 docs MD     nomic-embed-text              docflow_docs: 570

[ChromaDB] --> [build_rerank_index.py] --> [rerank_index.json]
book_insights   CrossEncoder                20 queries x top-50
1,193 docs      BAAI/bge-reranker-base      701KB JSON

[bizc_27k.csv] --> [analyze_27k.py] --> [.npy] + [clusters.csv]
  27,776 patents   SentenceTransformer       384d embeddings
                   KMeans(30)                30 clusters

第IV部 歴史と背景


10. 技術選定の経緯

2023年  ChromaDB v0.3 リリース
  |  PythonネイティブのベクトルDB。pip installだけで動く手軽さが
  |  個人プロジェクトでの採用を加速。
  |
2024年  Ollama の普及
  |  ローカルLLM実行が「docker pullするだけ」の手軽さに。
  |  API互換により、OpenAI向けコードをローカルに移植可能に。
  |  embedding APIの追加で、ベクトル化もローカルで完結する世界が到来。
  |
2024年  Claude Code(旧Claude CLI)登場
  |  AIエージェントがターミナルで直接コマンドを実行できるように。
  |  「計画→コード生成→実行→検証」のループが1セッションで回る。
  |
2025年  Haiku / Opus モデルの分化
  |  コスト差が明確になり「高精度モデルで計画、低コストモデルで実行」の
  |  パターンが経済的に合理的に。
  |
2026年  本セッション — HBOパターンの確立
        Opus(計画)+ Haiku(生成)+ Ollama/SBERT(実行)の
        三層分離を体系化。9時間17タスクで実証。

なぜOllamaか: クラウドAPI(OpenAI等)は従量課金のため、4万件のベクトル化では$40〜$100のコストが発生する。Ollamaなら同じ処理がゼロコスト。品質差は検索用途では許容範囲。

なぜChromaDBか: Pinecone等のマネージドサービスは月額課金。ローカルファイルベースのChromaDBは無料で、SQLiteと同じ感覚で使える。個人・小チーム規模では最適解。

なぜSBERTか: Ollamaのnomic-embed-textは汎用だが、日本語特許の多言語クラスタリングにはparaphrase-multilingual-MiniLM-L12-v2(384次元)の方が精度が高い。CPU実行で145〜343件/秒と十分高速。


11. 失敗の系譜

このセッションで遭遇した6つの障害と、そこから得た教訓:

障害1: Haikuエージェントの権限ブロック

症状: HaikuエージェントがBash権限を拒否され、スクリプトを実行できない。 原因: サブプロセスとして動作するエージェントの権限スコープが親と異なる。 教訓: 原則1(分離原則)の確立。Haikuにはスクリプト作成のみ委託。

障害2: ChromaDB "Collection does not exist"

症状: collection.add()直後に「コレクションが存在しない」エラー。 原因: 先に起動した重複プロセスがChromaDBのSQLiteファイルをロック→破損。 教訓: バッチ起動前にps | grep <script>で重複プロセスを確認・kill。ChromaDBのパスを変えて完全クリーンから再開。

障害3: Ollama帯域競合による速度低下

症状: 3プロセスが同時にOllamaを叩き、1件あたりの処理時間が3倍に。 原因: Ollamaのキュー方式による排他的GPU使用。 教訓: 原則2(資源分離原則)の確立。Ollamaタスクは優先度順に直列化。CPUタスク(SBERT)は並行OK。

障害4: CSVカラム名の不一致

症状: KeyError: 'date' — DataFrameにdateカラムが無い。 原因: CSVのカラム名がfiling_dateだったが、スクリプトはdateを参照。 教訓: 原則3(段階的信頼原則)--demoで5件テスト → カラム確認 → 全件実行。

障害5: macOS launchd Python バージョン不整合

症状: health-monitorがexit 1で即クラッシュ。 原因: plistが/usr/bin/python3(3.9)を指定していたが、スクリプトがPython 3.10+構文(int | None)を使用。 教訓: plistのPythonパスは/opt/homebrew/bin/python3を明示的に指定する。

障害6: EINTR(Interrupted System Call)

症状: watchdogがファイルスキャン中にInterruptedError。 原因: ~/DownloadsフォルダでChrome等のI/Oが重く、os.scandir()がシグナルで中断。 教訓: ファイル操作にはリトライラッパーを必ず実装する:

def glob_with_retry(path, pattern, retries=3):
    for attempt in range(retries):
        try:
            return list(path.glob(pattern))
        except InterruptedError:
            if attempt == retries - 1:
                return []
            time.sleep(0.1)

第V部 手順・マニュアル


12. 9時間バッチを計画する10ステップ

前提条件

Step 1: 棚卸し — 既存Pythonスクリプトのリスト化

# プロジェクト内の全Pythonスクリプトを列挙
find ~/projects ~/virtual-company -name "*.py" \
  -not -path "*/.venv/*" -not -path "*/__pycache__/*" \
  -not -path "*/site-packages/*" | sort

探すべきもの: - embedvectorizeindexsync を含むスクリプト(ベクトル化系) - scoreclassifycategorize を含むスクリプト(分類系) - batchpipelineprocess を含むスクリプト(バッチ系)

Step 2: ギャップ分析 — データ量 vs ベクトル量

# SQLite件数 vs ChromaDB件数の差分が「鉱脈」
import sqlite3, chromadb

db = sqlite3.connect("data/patent_search.db")
total = db.execute("SELECT COUNT(*) FROM patents").fetchone()[0]

client = chromadb.PersistentClient(path="vectordb")
vectorized = sum(c.count() for c in client.list_collections())

gap = total - vectorized
print(f"Total: {total}, Vectorized: {vectorized}, Gap: {gap}")
# Gap > 0 なら、そこがバッチ処理の対象

Step 3: リソースマップ作成

+------------+-----------+------------------+
| リソース    | 排他性    | 使用するタスク     |
+------------+-----------+------------------+
| Ollama GPU | 準排他    | ベクトル化, 分類  |
| CPU        | 共有可    | SBERT, 統計処理  |
| ChromaDB   | 書込排他  | 全embed系        |
| SSH        | 共有可    | リモート修復      |
+------------+-----------+------------------+

Step 4: 依存関係の整理

# 直列必須
Mac mini修復 --> スクリプト同期

# 並列可能
ベクトル化 || SBERT分析 || レポート生成

# リソース競合(擬似直列)
Ollama分類 ~~ Ollamaスコアリング(同時実行は非推奨)

Step 5: 段階的ファネルの設計

LLM分類タスクは、3段階ファネルでLLM呼び出しを最小化する:

Phase 1: ルールベース分類(IPC/CPCコードマッピング)
         → 即時、全件処理、LLM不要
         → 全体の30-40%をカバー

Phase 2: キーワードマッチング(正規表現)
         → 即時、残り件数の20-30%をカバー

Phase 3: LLM推論(Ollama)
         → 遅い(1-5秒/件)、残り30-40%のみ

Step 6: Haikuエージェントへの委託

# スクリプト作成のみをHaikuに委託
Agent(
    model="haiku",
    prompt="""
    Create a Python script at ~/scripts/batch_embed.py that:
    1. Reads from SQLite (path: ...)
    2. Embeds using Ollama nomic-embed-text (URL: ...)
    3. Stores in ChromaDB (path: ...)
    4. Saves progress to .progress.json
    5. Supports --resume flag
    """,
    run_in_background=True
)

Step 7: テストラン

# 5件で疎通確認
python3 scripts/batch_embed.py --test
# または
python3 scripts/batch_embed.py --limit 5

確認項目: - [ ] Ollamaに接続できるか - [ ] ChromaDBにデータが入るか - [ ] progress.jsonが生成されるか - [ ] カラム名は正しいか

Step 8: 全件バッチ起動

# run_in_backgroundで起動
python3 scripts/batch_embed.py --resume 2>&1

Claude Codeのrun_in_backgroundを使えば、完了時に自動通知を受け取れる。

Step 9: 定期監視

# progress.jsonで進捗確認
cat data/.progress.json
# {"last_id": 15000, "count": 15000, "total": 45121, "timestamp": "..."}

# プロセス生存確認
ps aux | grep batch_embed | grep -v grep

Step 10: 資源解放と次タスク投入

完了したタスクのプロセスが終了すると、Ollamaの帯域が解放される。この時点で次のOllamaタスクを投入する。

タスクA完了 → Ollama帯域解放 → タスクB起動

13. パターン別実装レシピ

Recipe A: Ollama Embed バッチ(最頻出)

#!/usr/bin/env python3
"""汎用Ollamaベクトル化バッチテンプレート"""
import json, os, sys, time, urllib.request, sqlite3
from pathlib import Path
import chromadb

OLLAMA_URL = os.environ.get("OLLAMA_HOST", "http://<サーバーIP>:11434")
PROGRESS = Path("data/.embed_progress.json")

def embed(text, model="nomic-embed-text"):
    """3回リトライ付きOllama embed"""
    payload = json.dumps({"model": model, "input": text[:2000]}).encode()
    req = urllib.request.Request(
        f"{OLLAMA_URL}/api/embed", data=payload,
        headers={"Content-Type": "application/json"})
    for attempt in range(3):
        try:
            with urllib.request.urlopen(req, timeout=60) as resp:
                return json.loads(resp.read().decode())["embeddings"][0]
        except Exception:
            if attempt == 2: return None
            time.sleep(2)

def save_progress(last_id, count, total):
    PROGRESS.write_text(json.dumps({
        "last_id": last_id, "count": count, "total": total,
        "timestamp": time.strftime("%Y-%m-%d %H:%M:%S")}))

def main():
    resume = "--resume" in sys.argv
    last_id = 0
    if resume and PROGRESS.exists():
        last_id = json.loads(PROGRESS.read_text()).get("last_id", 0)

    db = sqlite3.connect("data/db.sqlite")
    db.row_factory = sqlite3.Row
    client = chromadb.PersistentClient(path="vectordb")
    collection = client.get_or_create_collection("my_collection")

    rows = db.execute(
        "SELECT id, text, title FROM docs WHERE id > ? ORDER BY id",
        (last_id,)).fetchall()

    batch_ids, batch_docs, batch_metas, batch_embs = [], [], [], []
    for i, row in enumerate(rows):
        emb = embed(row["text"])
        if emb is None: continue

        batch_ids.append(str(row["id"]))
        batch_docs.append(row["text"][:4000])
        batch_metas.append({"title": row["title"] or ""})
        batch_embs.append(emb)

        if len(batch_ids) >= 50:
            collection.upsert(
                ids=batch_ids, documents=batch_docs,
                metadatas=batch_metas, embeddings=batch_embs)
            batch_ids, batch_docs, batch_metas, batch_embs = [], [], [], []
            save_progress(row["id"], i + 1, len(rows))

    if batch_ids:  # 最後の端数
        collection.upsert(
            ids=batch_ids, documents=batch_docs,
            metadatas=batch_metas, embeddings=batch_embs)

if __name__ == "__main__":
    main()

Recipe B: 3段階ファネル分類

# Phase 1: ルールベース (即時)
IPC_MAP = {"H02J50": "WPT", "G06K19": "RFID", "H01Q": "ANTENNA", ...}

def classify_by_ipc(ipc_str):
    for prefix, cat in sorted(IPC_MAP.items(), key=len, reverse=True):
        if prefix.upper() in (ipc_str or "").upper():
            return cat
    return None

# Phase 2: キーワード (即時)
KEYWORD_MAP = {
    "WPT": [r"wireless.?power", r"power.?transfer", r"rectenna"],
    "RFID": [r"\bRFID\b", r"backscatter", r"transponder"],
}

def classify_by_keywords(title, abstract):
    text = f"{title} {abstract}".lower()
    scores = {cat: sum(1 for p in pats if re.search(p, text, re.I))
              for cat, pats in KEYWORD_MAP.items()}
    return max(scores, key=scores.get) if any(scores.values()) else None

# Phase 3: LLM (遅い、最終手段)
def classify_by_llm(title, abstract):
    prompt = f"Classify: {title} {abstract[:300]}\nCategory:"
    # Ollama API call...

Recipe C: SBERT ローカル分析

# CPU実行、Ollama不要
from sentence_transformers import SentenceTransformer
import numpy as np

model = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')
texts = df["abstract"].fillna("").tolist()
embeddings = model.encode(texts, batch_size=128, show_progress_bar=True)
np.save("embeddings.npy", embeddings)

# KMeansクラスタリング
from sklearn.cluster import KMeans
km = KMeans(n_clusters=30, random_state=42, n_init=10)
df["cluster"] = km.fit_predict(embeddings)

Recipe D: CrossEncoder Rerank Index

from sentence_transformers import CrossEncoder
import chromadb, json

ce = CrossEncoder('BAAI/bge-reranker-base')
client = chromadb.PersistentClient(path="~/knowledge/db/chromadb")
col = client.get_collection("book_insights")
docs = col.get(include=["documents", "metadatas"])

queries = ["検索クエリ1", "検索クエリ2", ...]
index = {}
for query in queries:
    pairs = [(query, doc[:512]) for doc in docs["documents"]]
    scores = ce.predict(pairs)
    ranked = sorted(enumerate(scores), key=lambda x: x[1], reverse=True)
    index[query] = [{"id": docs["ids"][i], "score": float(s)}
                    for i, s in ranked[:50]]

with open("rerank_index.json", "w") as f:
    json.dump(index, f, ensure_ascii=False)

14. トラブルシューティング辞典

症状 原因 対処
ChromaDB: Collection does not exist 重複プロセスによるDB破損 ps aux \| grep <script> で重複kill → ChromaDBディレクトリ削除 → 再実行
Ollama timeout (60s) 他プロセスがGPU占有中 他のOllamaタスクの完了を待つ。またはtimeoutを120sに延長
KeyError: 'column_name' CSV/DBのカラム名不一致 df.columns / PRAGMA table_info(table) で実際のカラム名を確認
exit code 127 (launchd) plist内のスクリプトパスが不正 cat <plist> でProgramArgumentsを確認。フルパスに修正
exit code 1 (launchd) Pythonバージョン不整合 plistのPythonパスを/opt/homebrew/bin/python3に変更
InterruptedError: EINTR I/O重負荷下でのシステムコール中断 glob_with_retry()ラッパーを実装(3回リトライ)
Permission denied (Haiku agent) サブプロセスの権限スコープ制限 Haikuにはスクリプト作成のみ委託。実行は親から直接
DB readonly 別プロセスがSQLite/ChromaDBをロック中 重複プロセスをkill。または別パスに新規DB作成
Ollama応答が異常に遅い 複数プロセスの帯域競合 ps aux \| grep ollama で他プロセス確認。優先タスクを先に完走
requests.post がハングする Ollamaが別リクエスト処理中 urllib.request に切り替え(timeout明示)。requestsは内部バッファリングでハングしやすい

本書は、AIエージェントによる9時間17タスクのバッチ処理セッションから抽出された技術体系です。個別の実装は変わっても、「推論と計算の分離」「リソース競合の管理」「段階的信頼の構築」という3原則は、あらゆるAIバッチ処理に適用できます。

← 参照コンテンツ一覧に戻る