0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Tsurugi UDTF 経由で Google OR-Tools を使う

0
Posted at

組合せ最適化を AI チャットから試し、実行本体は Tsurugi の UDTF(User-Defined Table Function)経由で Google OR-Tools に委ねる PoC を作成しました。求解 API を「問題スキーマ+ソルバーアダプター」というドメイン非依存の形にそろえたことで、AI エージェントは製造や物流といった業務知識をツール名に持たせず、共通の solve などで複数の最適化課題を扱えます。本稿ではその構成と設計意図をまとめます。あわせて、開発中に遭遇した Tsurugi UDF の仕様を勘違いしたトラブルも紹介します。

tsurugi-or.png

背景

組合せ最適化を、製造や物流といった具体題材を交えつつ試せる PoC が欲しく、あわせて利用者にはスケジューラや配送ソルバーの細部を強要したくありませんでした。やりたいことを自然言語で伝え、入力データを用意すれば試せる体験に寄せるため、フロントは AI チャットを中心にしました。求解エンジンとしては Google OR-Tools を選びました。制約を満たしつつ目的関数を良くする求解は計算負荷が大きく、アプリケーション本体と同じプロセスに抱え込むと構成が重くなりやすい、というのが当面の関心でした。

こうした重い処理を別プロセスへ分離し、SQL から呼び出し可能な形で置く手段として、Tsurugi の UDTF が好適だと判断しました。UDTF は gRPC で外部サーバーに処理を委譲でき、入力はデータベース側に置き、求解だけを分散配置できます。ニーズに対する道具選定として OR-Tools・Tsurugi・UDTF・AI チャットを組み合わせ、ドメイン固有のロジックはプリセット(問題スキーマ)とアダプター側に閉じ込め、コアの API とツール群は汎用のまま保つ方針に落ち着きました。

Google OR-Tools の紹介

Google OR-Tools は、線形計画、混合整数計画、制約プログラミング、ルーティングなど、組合せ最適化向けのソルバー群をまとめたオープンソースのソフトウェアスイートです。Python から利用でき、問題の性質に応じてソルバーを選ぶ、というのが一般的な使い方です。

問題の特徴 よく使うソルバー
時間軸があり、工程の重複禁止が中心 CP-SAT(cp_model
車両・訪問順序・距離が中心 Routing(pywrapcp
容量制約のある詰め込み CP-SAT など

今回の PoC では次の 3 題材をデモ用プリセットとして同梱しました。

  • 区間ジョブショップ相当の製造スケジューリング(CP-SAT)
  • 倉庫・配送の基本 VRP(Routing)
  • コンテナへの 3D 箱詰め(CP-SAT)

アーキテクチャ全体像

論理構成は次のとおりです。

ブラウザの SPA は FastAPI に話し、チャットは LangGraph エージェントが担当します。求解の呼び出し元が REST であれ AI ツールであれ、経路は共通の solve パイプラインに集まり、tsurugi-dbapi 経由で Tsurugi に APPLY Solve を発行します。OR-Tools の実行自体は UDTF 側の gRPC サーバー内だけに置き、バックエンドのイメージには含めませんでした。セッション入力は Tsurugi の専用テーブルに置き、tgsql からも同じ UDTF を直接試せるようにしています。

Docker Compose ではフロントエンド、バックエンド、Tsurugi、UDTF、初回の DDL/DML 適用などをまとめて起動できます。LLM(OpenAI または Ollama)は任意で、未設定でも DB・API・UI までは立ち上がります。

ドメイン非依存の設計

製造ラインや配送といった語彙をコア API に出すと、題材を増やすたびにエンドポイントやツール名が増えます。PoC ではそれを避け、次の形にそろえました。

役割
求解 API / UDTF Solve solve(session_id, problem_schema, solver, params) の汎用呼び出し
問題スキーマレジストリ 入力テーブル構造・既定ソルバー・解の形状(solution_type)を定義
ソルバーアダプター スキーマ入力を OR-Tools モデルに変換し、結果行を返す
PoC プリセット ジョブショップ、VRP、3D 箱詰めなどのデモ定義

新しい題材は、スキーマ定義(YAML)とアダプターを足すだけで済み、コアの REST と AI ツールは変えません。ジョブショップ用スキーマの抜粋は次のとおりです。

problem_schema_id: scheduling.interval_job_shop
display_name: 区間ジョブショップ
default_solver: cp_sat
candidate_solvers:
  - cp_sat
solution_type: timeline
adapter: app.solvers.interval_job_shop:IntervalJobShopAdapter
input_tables:
  - logical_name: tasks
    required: true
    columns:
      - name: task_id
        type: int
        required: true
  - logical_name: operations
    required: true
    columns:
      - name: task_id
        type: int
        required: true
      - name: sequence
        type: int
        required: true
      - name: resource_id
        type: int
        required: true
      - name: duration
        type: int
        required: true

入力はセッション単位の物理テーブル(session_{id}_{logical_name})に展開されます。解の形状は solution_typetimeline / route_2d / packing_3d / table)として返し、フロントエンドのウィジェット選択に使います。

UDTF 連携の要点と踏んだ落とし穴

単一の Solve と proto の制約

OR-Tools 実行は、単一の UDTF Solve(gRPC Server Streaming)に集約しました。引数はスカラー 4 つ、session_idproblem_schemasolvermax_time_seconds です。Tsurugi UDF の proto では、レスポンス message に stream が必要であり、repeated は使えません。出力はフラットなスカラー列の列挙に限られます。

ドメインごとに列セットが違うため、レスポンスをスキーマ別に分けたくなりますが、proto は 1 つに保ちました。統一 SolveResponse にスケジューリング用・VRP 用・パッキング用の列を並べ、使わない列は 0 や空文字の既定値とします。列数は増えますが、プラグインとカタログの扱いは単純になります。

SELECT r.task_id, r.sequence, r.resource_id, r.start_time, r.end_time, r.objective_value
FROM (
  SELECT 'demo-sched-001' AS session_id,
         'scheduling.interval_job_shop' AS problem_schema,
         'cp_sat' AS solver,
         CAST(30 AS INT) AS max_time_seconds
) AS p
APPLY Solve(p.session_id, p.problem_schema, p.solver, p.max_time_seconds) AS r;

上例の CAST(30 AS INT) は、後述する型の落とし穴への対策です。最初からこう書いていたわけではありません。

誤診のはじまり:not declared と「引数は最大 3 つ」

スパイク検証の当初、SolveRequest は上記どおり 4 フィールドでした。次のように整数リテラルのまま max_time_seconds を渡すと、コンパイルに失敗しました。

FROM (
  SELECT 'demo-sched-001' AS session_id,
         'scheduling.interval_job_shop' AS problem_schema,
         'cp_sat' AS solver,
         30 AS max_time_seconds
) AS p
APPLY Solve(p.session_id, p.problem_schema, p.solver, p.max_time_seconds) AS r;
table-valued function not declared: Solve

スカラー関数として呼ぶと、次のメッセージが出ました。

function not found: Solve(character-varying(*), character-varying(*), character-varying(*), int8())

ここで 4 番目が int8() であることには気づかず、「4 引数目でカタログに載っていない」と解釈しました。max_time_seconds を proto から消し、string 3 引数にすると成功したため、「UDTF の SQL 引数は最大 3 つ」と断定し、設計・要件も 3 引数前提に寄せ、開発チームへ Issue まで提出しました。

公式サンプル相当の Split で引数個数を変えながら追試したのですが、string のリテラルは成功し、int32 を SELECT 1, 2, 3, ... のように渡すと失敗します。この差を「個数の制限」と「型の問題」で切り分けきれず、string の結果を int にも一般化してしまいました。実際には、int32 をリテラルで渡す場合は引数 1 つでも同じく not declared になります。

再現しない、という回答と真因

開発チームからは再現せず動作する、という回答がありました。こちらでも Docker 環境を起こし直して追試すると、文字列 4 引数の UDTF は問題なくコンパイルでき、「4 以上は登録されない」という結論は崩れました。udf-plugin-viewer では int 入力もカタログ上 int4 として正しく見えており、プラグイン読み込み自体は健全でした。

残る差分は SQL 側の型でした。Tsurugi では SELECT 1 AS a の整数リテラルは BIGINT(int8)として推論されます。一方、proto の int32 はカタログ上 INT4 です。APPLY のオーバーロード解決は名前・個数・表値関数種別・型の一致が必要で、型が合わないときもエラー文言は table-valued function not declared になります。「未登録」と「型不一致」が見分けにくい、というのが誤診の直接原因でした。

proto カタログ(SQL)型
int32 INT(INT4)
int64 BIGINT(INT8)
string CHARACTER / VARCHAR

CAST(n AS INT) またはテーブルの INT 列を渡せば、int32 入力の 4 引数 UDTF もコンパイル成功します。3 引数が成功していたのはすべて string だったからであり、上限 3 という制限があったわけではありません。Solve は 4 引数に戻し、生成 SQL では常に CAST(... AS INT) を付けるようにしました。Issue は誤報告として訂正しています。

これから試す方向けのチェックリスト

  1. udf-plugin-viewer などでカタログ上の type_kindint32 なら int4)を確認する
  2. SQL 側の式の型を疑い、リテラル 130 が BIGINT になっていないかを見る
  3. 失敗時はまず CAST(... AS INT)INT 列で再試行してから「カタログ未登録」と判断する
  4. string は成功・int は失敗、という対照があるときは、引数個数より型を疑う
  5. スカラー呼び出し時の int8() などの型表示をエラーメッセージから読み取る

AI エージェントとチャット UI

エージェントは LangGraph で組み立て、OpenAI API とローカル Ollama を設定で切り替えられるようにしました。ツール名にはドメイン名を含めません。

分類 ツール 用途
スキーマ list_problem_schemas 登録済みスキーマと必要入力の説明
スキーマ get_input_schema 入力テーブル・列定義
入力 set_input セッション入力の一括登録と検証
求解 solve 汎用求解
求解 get_solution 結果取得
メモ save_memo / get_memo / list_memos セッション横断の知識引き継ぎ

対話の流れは、不足項目の確認 → set_input で入力反映 → チャット内の課題プレビュー表示 → 利用者がプレビュー上のボタンで求解、という順にしています。AI が検証成功直後に勝手に solve しないことで、求解タイミングを人手で握れるようにしました。

求解結果は solution_type に応じてウィジェットを選びます。スケジューリングはガント風のタイムライン、VRP は簡易 2D 経路、3D 箱詰めは正射影の立体表示、それ以外やフォールバックは表です。ウィジェット右下には、リクエストから表示までの経過時間を出しています。

開発の流れ

工程は要件定義、設計、実装と単体テスト、結合テストの順で進めました。実装側は求解コア、データ層、REST、UDTF、AI チャット、フロントエンド、Docker Compose へと段階を分けています。

観点 内容
バックエンド FastAPI、Pydantic、LangGraph、tsurugi-dbapi
UDTF Python gRPC Server Streaming、ortools は UDTF 側のみ
フロント Vue.js、TypeScript、Quasar
検証 pytest、フロントのテストと型チェック、受入シナリオ

PoC の成功基準として、スケジューリング→タイムライン、VRP→経路図、再スケジュール、tgsql/dbapi からの UDTF、メモの引き継ぎ、docker compose up による一式起動、を設け、いずれも合格としました。途中で AI からの入力登録や 3D 箱詰めの追加、求解経路の UDTF 一本化など仕様の調整はありましたが、コアの「スキーマ駆動・ドメイン非依存」はそのまま維持しています。

成果物

まとめ

  • 組合せ最適化を AI チャットから試すニーズに対し、重い求解(OR-Tools)の分離実行先として Tsurugi UDTF(gRPC による分散配置)を選定し、PoC を作成しました。
  • 問題スキーマ・レジストリとソルバーアダプターにより、コア API をドメイン非依存に保てます。題材追加は定義とアダプターの追加で済みます。
  • UDTF の出力はフラットなスカラー列に限られるため、複数ドメインの列を 1 つの SolveResponse に統合する設計判断が必要でした。
  • table-valued function not declared は「未登録」とは限らず、SQL 整数リテラル(BIGINT)と proto int32(INT4)の不一致でも出ます。CAST(... AS INT) がその典型的な解決です。
0
0
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
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?