0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

最初は、夏休みの自由研究のつもりでした。

本業は建築士です。

AI企業でも、ソフトウェア会社でもありません。

それが現在、2台のNVIDIA DGX Sparkが国や自治体の公共工事を毎日巡回する、本番サービスになっています。

公告PDF、設計書ZIP、落札結果、有資格者名簿を集め、OCRとローカルLLMで読み、「この会社は参加できるか」「誰がいくらで落札したか」「どの専門工事会社へ営業すべきか」まで整理します。

2026年7月31日時点で、ローカルAIの実行台帳は458,900回になりました。

直近は1日7,000〜12,000回。外部の生成AI APIへ支払う従量料金は0円です。

そして、これは実験用のデモではありません。

  • Webサービスを公開済み
  • iPhoneアプリをApp Storeで公開済み
  • AndroidアプリをGoogle Playで公開済み
  • 10都道府県を本番運用中

まず、現在の規模を見てください

指標 実測値 時点
ローカルAI累計実行 458,900回 2026-07-31
直近の日次AI処理 7,000〜12,000回 直近7日
入札案件 256,050件 2026-07-31
応札記録 1,269,348件 2026-07-31
建設会社 429,206社 2026-07-31
PostgreSQL 34GB・336テーブル・704インデックス 2026-07-31
Webリクエスト 413,759件/日 24時間実測
AIエージェント並列実行 39体・約6時間 福岡市町村展開
生成した市町村パーサ 21件 同上
本番公開 10県 + Web + iOS + Android 2026-08-01

AI実行台帳の最古は2026年5月25日です。7月31日までの68日間で割ると、単純平均でも次の量になります。

458,900回 ÷ 68日 ≒ 6,749回/日

この45.8万回はチャット回数ではありません。

  • 公告文から参加条件を抽出する
  • 工種を分類する
  • 案件と会社の説明を作る
  • 数量内訳書を読む
  • 画像PDFをOCRする
  • CAPTCHA画像を認識する
  • 類似案件用の埋め込みを作る

といった、本番データ処理の記録です。

2026年8月1日15:19時点の公開画面では、276,885案件、236,391落札結果、429,446社まで増えています。

作りたかったのは「入札情報の一覧」ではない

公共工事の情報自体は、すでに公開されています。

問題は、公開されている場所も形式も、すべてばらばらなことです。

  • 県の電子入札ポータル
  • 市町村の新着情報
  • 年度ごとに分かれたHTML表
  • 1案件ずつ公開されるPDF
  • ZIPに入った設計書と図面
  • 検索しなければ表示されない落札結果
  • PDFだけで公開される有資格者名簿

データは公開されていても、そのままでは「自社が参加できるか」という判断には使えません。

そこで、1件の公共工事を次の流れで処理しています。

利用者に見せたいのは、元PDFの一覧ではありません。

  • 今日、自社が参加できそうな案件
  • 必要な工種、等級、経審点、営業所要件
  • 設計書から拾った数量と概算見積
  • 過去に参加した会社と落札率
  • 周辺で動いている元請・専門工事会社

つまり、公開情報を建設会社が次の行動を決められるデータへ変えることが目的です。

公開されている原本 Newsatsuが作る情報
入札公告PDF 参加条件、工種、等級、経審点、地域要件
設計書・数量内訳書 工事項目、数量、概算見積の材料
落札結果 落札会社、落札率、参加会社、価格傾向
有資格者名簿 会社の対応工種、許可、営業可能地域

ここまでを人間が案件ごとに読むのではなく、ローカルAIが毎日処理します。

最も大きく開発速度が変わったのは、市町村ごとに異なるパーサを作ったときでした。生成20体・検証19体のAIエージェントを同時に動かし、約6時間で21市町村分を作りました。

ただし、そのまま使えたわけではありません。速さと品質をどう両立したかは、後半で実測結果まで書きます。

2台のDGX Sparkをどう使っているか

計算基盤はNVIDIA DGX Spark 2台です。

各機は20コアのArm CPU、128GBの統合メモリ、NVIDIA GB10を搭載しています。2台は200GbEの専用線で接続しています。

ノード 主な役割
DGX Spark 1 計算ノード、重い集計・バッチ、バックアップ
DGX Spark 2 PostgreSQL、クローラ、FastAPI、Next.js、Ollama
Cloudflare 外部公開、D1の版管理、R2の生成済みHTML配信

本番機には、PostgreSQL、クローラ、API、Web、ローカルLLMが同居しています。

見た目はAI基盤ですが、実際にはデータ収集、検索、モバイルAPI、エッジ配信まで含む小さなデータセンターです。

ただし、2台あるから単純に余裕があるわけではありません。10県の時点で本番機のCPU使用率は88〜95%に達しています。この話は後半で触れます。

ローカルLLMを45.8万回動かして分かったこと

モデル実行にはOllamaを使っています。

外部生成AI APIを使わない理由は3つです。

  1. 官公庁資料と自社の判定情報を外部へ送らない
  2. 全案件にAIを適用しても、呼び出し回数で費用が増えない
  3. モデルとプロンプトを固定し、同じ条件で再実行できる

直近7日の利用実績です。

用途 モデル 実行回数
条件・工種・数量の抽出 gpt-oss:20b 40,985回
案件・会社の説明 Gemma 4 12B 11,022回
類似検索用埋め込み bge-m3 3,427回
CAPTCHA画像認識 qwen3-vl 2,692回
重い曖昧性解消 gpt-oss:120b 8回

最も大きな120Bモデルは、7日間で8回しか動いていません。

一番使われていたのは20Bモデルでした。

大きなモデルを毎回呼ぶより、用途を狭くし、小さなモデルを検証器と組み合わせた方が本番では使いやすかったからです。

全AIジョブには、次の情報を残しています。

model_name
model_version
model_digest
prompt_version
schema_version
normalizer_version
validator_version

同じ入力でも、モデルかプロンプトか検証器が変われば別の処理です。

「AIがそう言った」ではなく、どのAIが、どの指示で、どの原文を根拠に出したかまで追えるようにしています。

AIに読ませる。でも、最終判断はさせない

公告PDFから参加条件を読む作業は、LLMと非常に相性がよいです。

  • 地域要件
  • 工種
  • 等級
  • 経審点
  • 建設業許可
  • 営業年数
  • 工事実績

ただし、「この会社は参加できるか」という最終判断はPythonの決定論ルールで行います。

facts = llm_extract(document)
facts = require_verbatim_evidence(facts, document)

if not has_concrete_requirements(facts):
    verdict = "needs_check"
else:
    verdict = deterministic_match(company, facts)

ここで最も重要なのは、次の2つを分けることです。

条件がない     -> 参加条件なし
条件が取れない -> 判定不能

値を取得できなかったとき、AIが推測で埋めると危険です。そのため、LLMは事実の候補を出すところまで。原文との照合と最終判断は外側で行います。

数量内訳書の品質ゲートでは、120文書・2,343行を検査しました。

検査項目 誤り率
桁誤り 0.000%
列取り違え 0.043%
数量の原文非接地 0.341%

最も多かった失敗は、派手なAI幻覚ではなく、単価列と金額列の取り違えでした。

AIを使うほど、最後はAIを信じなくてよい設計が必要でした。

AIエージェント39体を6時間動かした

このシステムで最も「AIを使った」と実感したのは、福岡県内の市町村展開です。

未配線だった23市町村を調査すると、共通の電子入札システムは1件もありませんでした。

同じCMSを使っていても、ある市は落札者をHTMLで公開し、別の町はPDFだけ、別の自治体は月別ページです。市町村ごとにパーサを作る必要がありました。

そこで、役割を分けてAIエージェントを並列実行しました。

役割 エージェント数 仕事
生成担当 20体 実ページを調査し、パーサ候補を作る
検証担当 19体 別経路で原本と出力を照合する
合計 39体 同一サイトへの同時アクセスを避けて並列実行

結果は、約6時間で21市町村分のパーサでした。

人間1人では同時に21自治体を調べられません。AIエージェントだからできた速度です。

しかし、提出コードをそのまま本番へ入れられたわけではありません。

独立検証 件数
CONFIRMED 6
PARTIALLY_WRONG 13

生成担当の提出物も、次の状態でした。

出力状態 件数
そのまま構文が通る 15
先頭に異物が混入 4
/* placeholder */だけ 1

調査内容は正しいのに、最終出力だけ途中で切れているケースもありました。

そのため、AIエージェントの成果物は完成品ではなく、高速に作られたPRとして扱っています。

AIが生成
  ↓
構文・import検査
  ↓
固定原本によるパーサテスト
  ↓
別担当が原本と独立照合
  ↓
金額・日付・件数の不変条件
  ↓
dry-run・退避・適用前後の再測定

速さを作ったのは39体のエージェントです。

本番にできた理由は、その後ろに940件のテストと原本照合を置いたことでした。

24万案件の類似検索を「検索しない」ことにした

2台のAIマシンを使っているため、最初はGPUが最初に限界になると思っていました。

実際のCPU使用量は違いました。

処理 CPU使用量の実測
PostgreSQL 8.9〜9.7コア相当
クローラ 3.3〜7.9コア相当
API 0.34コア相当
Web 0.18コア相当

律速はAIではなく、PostgreSQLとクローラでした。

特に重かったのが、約24万案件の類似検索です。

pgvectorのHNSW検索をリクエストごとに実行しており、平均1,667.76ms。累計1,402,283回、累積実行時間は2,338,677秒、約27日分でした。

そこで、検索を速くするのではなく、オンラインで検索すること自体をやめました。

週次バッチで全案件の類似上位6件を一括計算し、約147万行の事前計算表へ保存します。

方式 実測・推定時間
HNSWを1件ずつ全案件に実行 推定約82時間
NumPy/BLASで全件を一括計算 約17分
事前計算表からDB取得 0.66ms + 0.74ms
HTTP応答 4.8〜15.2ms
全件処理: 82時間 × 60 ÷ 17分 ≒ 289倍
DB部分: 1,667.76ms ÷ (0.66ms + 0.74ms) ≒ 1,191倍

最速の検索クエリを作るより、同じ答えを何度も検索しない方が速い。

AI基盤を作っていて、最大の高速化が「AIでもGPUでもなく、事前計算表だった」のは意外でした。

Cloudflareは高速化より「本番機に描かせない」ために使った

Webへの24時間リクエストは413,759件。その92.8%がボットでした。

過去案件の長尾URLへ、世界中から1回ずつアクセスされます。通常のCDNキャッシュでは、同じ場所から同じURLが再訪されず、案件詳細ページのヒット率は2.7%でした。

そこで、Cloudflare D1へ最新版のポインタ、R2へ生成済みHTMLを置きました。

Workerは、最新版の存在を確認できた場合だけHTMLを返します。少しでも怪しければNext.jsへ素通しします。

24時間の結果です。

指標 実測値
Workerが受けた要求 30,149件
オリジンへ素通し 26,958件
本番機の描画を肩代わり 3,191件(10.6%)
Workerエラー 0件
エッジヒット時TTFB 61.6ms
素通し時TTFB 64.3ms

TTFBはほぼ変わっていません。

それでも成功です。目的は数msの高速化ではなく、CPU使用率88〜95%の本番機へ同じページを描かせないことだからです。

もちろん、何度も壊した

ここまでの数字だけを見ると、最初から計画どおりに作れたように見えます。

実際には、かなり壊しています。

事故 影響 原因
日付検索が一度も効いていなかった 約17,000行を無音で欠落 存在しないフォーム項目へ日付を設定
全国で同名が1社なら同一法人と判断 355件を別会社へ誤帰属 名前の一意性を法人同定に使用
一括UPDATEの結合キーが非一意 1,707行を汚染 書き込み前の一意性確認不足
ページを大量に先回り生成 節約量の13〜17倍の負荷 生成対象と実際の要求が不一致
安全装置が通常操作を事故判定 CIを11日停止・788回スキップ 監視役自身の停止を監視していなかった

特に怖かったのは、エラーではなく正常終了です。

長野県では、1年分の検索を12日ずつに分けて上限を回避したつもりでした。しかし、検索フォームにコードが想定した日付フィールドが存在せず、期間は一度も適用されていませんでした。

例外は0件。ログは成功。データだけが欠けていました。

現在は、次の状態を別々に保存しています。

状態 意味
empty_confirmed 原本を確認し、本当に0件
unmeasurable 条件が効いたか確認できず、測定不能
failed 閉場、認証、通信、上限超過などで失敗

「設定した」は入力で、「効いた」は結果です。

この区別を入れてから、0件を成功の証拠に使わなくなりました。

45.8万回のAI処理を本番にする5つの不変条件

失敗を繰り返した結果、品質保証は次の5原則へ収束しました。

原則 実装上の意味
原文接地 保存する値が原文に存在するか確認する
誤PASS禁止 不明なら参加可能ではなくneeds_checkにする
無音の空白禁止 本当の0件と取得不能を分ける
効果を測る 設定値ではなく、結果が変わったか確認する
冪等・可逆 dry-run、退避、前後比較、2回目差分0を確認する

2026年8月1日時点で、クローラ系テストは940件成功。県展開用の17トラップはFAIL 0測定不能 0まで改善しました。

ただし、緑色の表示だけでは信用しません。

  • 何件を検査したか
  • 最後に成功したのはいつか
  • 旧バグを戻したら赤くなるか
  • 本番と同じ依存関係で動いたか
  • 原本との独立照合があるか

まで確認します。

0件を検査してPASSしたテストは、FAILより危険です。

47都道府県は、まだ完成していない

Newsatsuは現在10県で本番運用しています。

全国47都道府県の進出状態は管理していますが、「全国対応済み」とは書けません。

本番機は10県の時点でCPU使用率88〜95%。PostgreSQLだけで最大9.7コアを使っています。現在の構成を単純に4.7倍する余力はありません。

次に必要なのは、3台目のGPUではなく次の分離です。

  1. PostgreSQLをWeb・クローラと別筐体へ移す
  2. 読み取り処理と書き込み処理を分ける
  3. 県依存データを分割する
  4. 履歴取得と集計バッチを計算ノードへ逃がす
  5. 新県ごとに原本の母数を登録し、欠落を測れるようにする

47県分のコードを書くことより、47県で毎日壊れずに動いたと証明する方が難しいと分かりました。

まとめ

建築士1人で始めたシステムが、現在は次の規模になりました。

  • DGX Spark 2台
  • ローカルAI累計458,900回
  • 約27万案件、約43万社
  • PostgreSQL 34GB、336テーブル、704インデックス
  • AIエージェント39体を並列実行
  • 約6時間で21市町村のパーサ候補を作成
  • 10県、Web、iOS、Androidで本番公開

ただAIにコードを書かせただけでは、ここまでは動きませんでした。

原本を保存し、AIの出力を検証し、不明を不明のまま扱い、壊れたら戻せる仕組みを作る。その地味な部分があって、初めて39体のAIエージェントと45.8万回のローカルAIを本番で使えました。

このシステムのすごさは、AIを45.8万回呼んだことだけではありません。

45.8万回呼んでも、どの原本から、どのモデルが、なぜその値を出したか追えることだと思っています。

実際のサービスはこちらです。

参考資料

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?