筆者は ShannonBase の開発者です。前回の記事([MySQL 8.4 のまま分析クエリを速くする] https://qiita.com/shannonbase/items/cc961a550aa21c69603d )の続編にあたります。前回は列指向エンジンの話でしたが、今回はデータベースプロセスの中で推論と Agent を動かす話です。
この記事でやること
データベースの中で埋め込み生成(ONNX Runtime)を動かす
自然言語で問い合わせて SQL を生成・実行させる
AI が書き込みをする場合の承認フロー(HITL)を動かす
3 番目が本題です。読み取りだけなら気楽ですが、AI に UPDATE を任せる話になった瞬間に設計の問題が変わります。
:::note warn 本記事の SQL はバージョンによってパラメータ名が変わる可能性があります。正確な構文はリポジトリのドキュメントを参照してください。 :::
なぜデータベースの中で動かすのか
一般的な構成だと、業務 DB・ベクトルストア・フィーチャーストア・LLM ゲートウェイの 4 つを繋ぐことになります。このパイプラインの厄介なところは、壊れても静かに壊れることです。埋め込みだけが古いまま残ってもエラーは出ず、少し間違った答えを返し続けます。
ShannonBase は ONNX Runtime と LightGBM をデータベースプロセス内に統合しているので、埋め込み生成のためにデータを外に出す必要がありません。スキーマが変われば埋め込みも同じイベントで追随します。
- スキーマ埋め込みの確認
Agent が SQL を組み立てるには、スキーマの意味を理解する必要があります。ShannonBase はテーブル定義の埋め込みを内部で保持し、DDL と DROP のイベントで自動的に同期します。手動の再インデックスは不要です。
sql
USE bench;
CREATE TABLE customers (
c_custkey BIGINT PRIMARY KEY,
c_name VARCHAR(64) NOT NULL,
c_segment VARCHAR(32) NOT NULL,
c_churned TINYINT(1) NOT NULL DEFAULT 0,
c_signup_at DATE NOT NULL
) ENGINE=InnoDB;
このテーブルを作った時点で、埋め込みは生成済みです。
:::note info 初期実装ではこの埋め込み生成が DDL のクリティカルパス上にあり、フレームグラフで見ると CPU の約 40% を占めていました。DDL が重い場合はこの経路を疑ってください。 :::
- LLM プロバイダを設定する
DeepSeek、DashScope、Anthropic、Ollama に対応しています。ローカルで完結させたい場合は Ollama を選んでください。
sql
-- 例: Ollama(ローカル)
SET GLOBAL shannon_llm_provider = 'ollama';
SET GLOBAL shannon_llm_endpoint = 'http://localhost:11434';
SET GLOBAL shannon_llm_model = 'qwen2.5:14b';
:::note warn リモートプロバイダを使う場合、エンドポイントは HTTPS が強制されます。平文 HTTP は拒否されます。API キーを扱う以上、ここは緩められません。 :::
- 読み取り系を試す
sql
CALL sys.shannon_chat('先月に解約した顧客をセグメント別に数えて');
生成された SQL と実行結果が返ります。
ここで「なぜ SELECT sys.shannon_chat(...) ではないのか」 と思った方へ。これは実装上の都合ではなく、MySQL の制約に由来します。
- なぜ FUNCTION ではなく PROCEDURE なのか
MySQL では、ストアドファンクションやトリガの中で明示的なトランザクション制御(COMMIT / ROLLBACK)ができません。
ER_COMMIT_NOT_ALLOWED_IN_SF_OR_TRG
Explicit or implicit commit is not allowed in stored function or trigger.
この制約は呼び出しチェーン全体に伝播します。 Agent は内部で SQL を実行し、承認状態を確定させるためにコミットを打つ必要があるため、FUNCTION として実装すると必ずこのエラーに突き当たります。したがって sys.shannon_chat は PROCEDURE でなければなりません。
これは ShannonBase 固有の話ではなく、MySQL 上で「内部で SQL を実行して状態を確定させる仕組み」を作る人全員に関係する制約です。FUNCTION にすると呼び出し側の式の中に埋め込めて便利そうに見えますが、トランザクション制御が必要になった瞬間に設計をやり直すことになります。最初から PROCEDURE にしておくのが安全です。
関連して、sp_head.cc の option_bits の save/restore が、ネストした START TRANSACTION が立てた OPTION_BEGIN を消してしまい SIGABRT になる、という問題も踏みました。ストアドプログラムとトランザクション制御の組み合わせは、想像以上に踏み固められていない領域です。
- 書き込み系と HITL 承認
ここからが本題です。
sql
CALL sys.shannon_chat('休眠中の顧客のステータスを inactive に更新して');
実行されません。 代わりに実行計画が返り、承認待ち状態になります。
plan_id: 7f3a...
statement: UPDATE customers SET ... WHERE ...
affected_rows_estimate: 1,284
status: PENDING_APPROVAL
内容を確認してから承認します。
sql
CALL sys.shannon_approve('7f3a...');
承認待ちの間、トランザクションは開いていない
ここが設計上いちばん重要な点です。
人間の承認は待ち時間が読めません。5 秒かもしれないし、翌朝かもしれない。その間トランザクションを開いたままにすると、ロックが残って他の処理が止まります。
そこで、実行計画を確定した行としてコミットしておき、承認が来たときに CAS(Compare-And-Swap)とリース機構で状態遷移させる方式にしています。これにより、
二重承認が起きない(CAS が弾く)
承認後に計画がすり替わらない(計画は確定済みの行)
承認者が消えてもロックが残らない(リースが失効する)
「AI に業務データを触らせる」という話は、賢さの問題ではなくこの状態機械の設計の問題です。
review_mode
すべての書き込みで承認を挟むか、特定の条件のみにするかは review_mode で制御します。運用初期は全件承認、慣れてきたら閾値ベース、という段階移行が現実的です。
- セキュリティ上の注意
開発の過程で、update_data のホワイトリストを迂回できる脆弱性を見つけて修正しました。これはコミット履歴に残っています。
AI がデータベースを触る仕組みは、権限設計を外から検証できる状態にないと使えないと考えています。どんなプロンプトが組み立てられ、どの権限で SQL が実行され、承認をすり抜ける経路がないか。これは「大丈夫です」と言われて信じる話ではなく、コードを読んで確かめる話です。ShannonBase を OSS にしている理由のひとつがこれです。
運用時の最低限として:
Agent 専用の MySQL ユーザーを作り、必要最小限の権限だけ付与する
本番投入前に review_mode を全件承認で一定期間運用し、生成 SQL のログを確認する
LLM プロバイダに送られる内容(スキーマ情報が含まれます)が自社ポリシー上問題ないか確認する。気になる場合は Ollama でローカル完結させる
ハマりどころ
SELECT sys.shannon_chat(...) が動かない → PROCEDURE です。CALL を使ってください(上記 4 節)。
承認したのに実行されない → リースが失効している可能性があります。計画の status を確認してください。
埋め込み生成で DDL が遅い → 既知の経路です。大量の DDL を流すマイグレーション時は注意してください。
LLM プロバイダへの接続が拒否される → HTTPS 強制です。エンドポイントのスキームを確認してください。
まとめ
推論をデータベースプロセス内に置くと、埋め込みとデータの同期ずれが構造的になくなる
MySQL の ER_COMMIT_NOT_ALLOWED_IN_SF_OR_TRG は呼び出しチェーン全体に伝播する。内部で SQL を実行して状態を確定させる仕組みは PROCEDURE で作る
AI に書き込みを任せる設計の核心は、トランザクションのライフサイクルを人間の承認待ち時間から切り離すこと