コンセプト
ActQLの考え方。Program、Run、Result、Host、Effect、Checkpointと、それらの関係。
ActQLは、AIエージェントのための、SQLベースのデータ&ワークフローエンジンです。プログラムは、SaaSのAPI、DWH、データベースをまたいでデータを読み、JOINし、LLMに判断させ、結果を書き戻します。このページは、一度読めば全体像がつかめるように書いています。ActQLが何のためのものか、どんな部品でできているか、1回の実行がテキストから外部システムの変更になるまでどう進むか、の3つです。用語の厳密な定義は仕様に、一覧は用語集にあります。
なぜActQLか
エージェントのする仕事は、複数のシステムをまたぐものが増えています。Salesforceから取引先を読み、DWHから利用データを読み、取引先ごとに判断し、結果を書き戻す、といった仕事です。いまエージェントは、これをツール(MCPサーバーや、誰かが用意した関数)を1つずつ順に呼ぶことで行っています。1回の呼び出しは検査できても、処理の全体が何を読み、何を計算し、どの外部システムを変えうるのかを、共通の型と意味のもとで実行前に確かめる手段はありません。書き込みの承認は呼び出し1回ごとで、承認する人が見るのはエージェントの説明文です。耐久実行、承認、結果の突き合わせは、それぞれ別の仕組みを組み合わせて作ることになります。
ActQLは、この仕事を1つのSQLプログラムとして書き、実行前に検査でき、人の判断が必要な箇所を決め、したことをすべて記録するエンジンで動かします。得られるのは次のことです。
- システムをまたぐ1つのプログラム。 SaaSのAPI、DWH、データベースを、1本のパイプラインで読み書きします。連携ごとのワークフローも、API別のコードも、プログラムには出てきません。
- 1つの型システム。 SaaSのデータ、DWHのデータ、LLMの答えを同じ型システムで検査するので、普通のSQLでJOINや絞り込みができ、不一致は実行前に見つかります。
- 書き込みは、実際に書かれる内容で承認する。その仕組みは自分で組める。 件数や対象で分岐して、リスクの低い書き込みはそのまま通し、一部はルールで承認し、リスクの高いものだけ人の判断を待つ、という振り分けができます。人が見るのは、実際に書かれるレコードと値です。エージェントが自分に権限を与えることはできません。
- 止めても安全に再開できる。 クラッシュや再起動のあとでも、終わった処理は繰り返しません。結果の分からない書き込みを、むやみに送り直すこともありません。
- どんな規模でも、どこでも。 アプリの中でも、CLIでも、サーバーでも、ActQL Cloudでも、同じプログラムが動きます。集計はSnowflakeのようなデータを持つ側で、システムをまたぐJOINはデータ量に応じて独自の演算エンジンやDuckDBで実行されるので、データが増えてもプログラムは変わりません。
プログラム
プログラムは、ActQLで書くひとまとまりの処理です。データを読み、計算し、必要ならLLMに判断させ、変更を書き戻せます。書き方は、Pipe構文のSQLです。LLMの呼び出しや書き込みなど、SQLにない部分だけを拡張しています。
SELECT a.id AS id, a.description AS notes,
u.active_users, u.trend
FROM salesforce.accounts AS a
JOIN snowflake.usage_30d AS u ON a.id = u.account_id
|> INFER `model-task:churn-risk.v1`
INPUT (notes, active_users, trend) AS risk
|> WHERE risk.outcome = 'ok' AND risk.value.level = 'high'
|> RESULT updates
|> EFFECT UPDATE salesforce.Account
ID id SET (
Churn_Risk__c = risk.value.level,
Risk_Reason__c = risk.value.reason
);上から順に読めます。
- Salesforceの取引先と、Snowflakeの利用データをJOINする
- 取引先ごとに、解約リスクをLLMに判定させる
- リスクが高いと判定された取引先だけを残す
- 判定結果をSalesforceに書き戻す
プログラムにはパラメーターを宣言でき、値は実行するときに渡します。プログラムは、呼び出し元にテーブルを返すこと(Result)も、外部システムに書くこと(Effect)も、その両方もできます。扱う値には、Salesforceのデータも、DWHのデータも、LLMの答えも、すべて型があります。だから型の不一致などの間違いは、実行前に見つかります。
同じプログラムは何度でも実行できます。1回の実行をRunと呼びます。Runには、その実行で何を読み、何をしたかが記録されます。
全体の構成
プログラムを実行するときは、3つの部分が一緒に動きます。
あなたのアプリ · CLI · エージェント
│ プログラム
▼
┌─────────────────────────────────────────┐
│ Host │
│ ポリシー · 認証情報 · Runの記録 │
│ ┌───────────────────────────────────┐ │
│ │ Core │ │
│ │ 検査 · 計画 · 計算 │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
│
▼
Connector → Salesforce · Snowflake · Slack · …- Coreは、プログラムの意味を決めます。検査し、計画を立て、結果を計算します。ActQLはデータ処理を型付きの代数的な演算として定義しているので、エンジンは意味を変えずに実行計画を書き換えられます。
- Hostは、あなたの環境でActQLを動かす部分です。ポリシーと認証情報を持ち、すべてのRunを記録します。Hostは、アプリの中、CLIの裏、リモートのサーバー、ActQL Cloudのどこにでも置けます。
- Connectorは、外部システムとやり取りします。
その周りで、人とエージェントは3つの役割を持ちます。
| 役割 | 誰か | すること |
|---|---|---|
| 作者 | 人、またはエージェント | プログラムを書いて実行する |
| 運用者 | Hostを動かす人 | 接続、認証情報、ポリシーを設定する |
| 判断の委譲先 | 人、ルール、または承認用のアプリ | ポリシーが判断を求めたときに答える |
ひとことで言えば、AIが書き、エンジンが検査し、Hostが決める、です。プログラムが認証情報や権限を定義することはありません。それらはHostのものです。それぞれの詳しい説明はアーキテクチャにあります。
Connector
Connectorは、SalesforceやSnowflakeのような外部システムとActQLをつなぐ部品です。プログラムに出てくるのは salesforce.accounts のような名前だけです。認証、ページング、レート制限、APIごとの違いはConnectorの中で処理されるので、プログラム側で気にする必要はありません。
Connectorは、ActQLに次の3つを伝えます。
- 読めるもの — たとえばSalesforceの取引先と、その列と型
- 実行できる操作 — たとえばSalesforceのレコード更新、Slackへの投稿、Gmailでのメール送信
- そのシステム自身にできる処理(Capability) — 絞り込み、集計、検索など。そのおおよそのコストも伝えます
たとえばSnowflakeは、自分で絞り込みや集計ができます。そこでActQLは、データを全部取ってくる代わりに、その処理をSnowflakeに任せます(プッシュダウン)。ただし、結果がまったく同じになる場合に限ります。数件でも数テラバイトでも同じプログラムで済むのは、この仕組みのおかげです。プログラムは変わらず、計画が変わります。
Connectorが実行できることと、プログラムが実際に実行してよいかは別です。それは毎回ポリシーで判断します(Effectとポリシーを参照)。
LLMとmodel task
INFER は、テーブルの各レコードを入力にして、LLMに型の決まったデータを出力させます。メモのような自由な文章も、SQLで扱える構造化された値に変えられます。
LLMへの指示と、入力と出力の型は、バージョン付きのmodel taskとして一度だけ定義します。プロンプトや出力の型を、SQLの中に書くことはありません。解約リスクを判定するmodel taskは、次のような定義です。
# model-tasks/churn-risk.v1.yaml
task: model-task:churn-risk.v1
input:
notes: string
active_users: int64
trend: string
instructions: |
取引先のメモ、アクティブユーザー数、利用傾向から、
解約リスクを low / medium / high で評価し、理由を1行で説明する。
入力にない事実を推測しない。
output:
level: {type: string, enum: [low, medium, high]}
reason: stringプログラムからは、名前で参照します。
|> INFER `model-task:churn-risk.v1`
INPUT (notes, active_users, trend) AS risk
|> WHERE risk.outcome = 'ok' AND risk.value.level = 'high'INPUT が、各レコードからLLMに渡す列です。AS risk が結果の名前です。level と reason の型はmodel taskのoutputで決まっているので、ActQLは後ろのSQLを実行前に型検査できます。model taskが「何をLLMに頼み、どんな型で受け取るか」を決め、SQLが「どのデータに適用し、答えをどう使うか」を決めます。
各レコードには、成功か失敗かを示す outcome が付きます。成功したときだけ、value がその型どおりの値になり、ほかの列と同じ列として返ります。失敗したときは value はNULLで、理由が記録されます。例の WHERE が risk.outcome = 'ok' のものだけを残しているのは、このためです。
入力 notes | 出力 level | 出力 reason |
|---|---|---|
| 「利用が減っていて、競合を検討中との話があった」 | high | 利用の減少と競合の検討 |
LLMの答えは、計算の結果ではなく、外部からの観測としてRunに記録されます。Runを再開したり復旧したりしても、LLMに聞き直さず、記録した答えを使います。ActQLが保証するのは、答えの型とその扱いです。LLMの判断そのものが正しいことは保証しないので、間違えたときの影響が大きい場面では、ポリシーとCheckpointで人の判断を挟みます。
LLMに送られるのは INPUT に書いた列だけです。この送信も、ほかの外部システムへのアクセスと同じく、Hostのポリシーを通ります。
Effectとポリシー
Effectは、外部システムに対する操作です。レコードの更新や追加、メッセージの投稿、メールの送信などがあたります。変更は EFFECT 句としても、INSERT や UPDATE のような見慣れたSQLでも書けます。どちらで書いてもEffectで、すべてのEffectはポリシーを通ります。
ポリシーはプログラムには書きません。運用者がHostに、ユーザー、接続、リソース、操作ごとに設定します。ActQLは、外部からデータを読む前とEffectを適用する前にポリシーに確認し、答えは次の3つのどれかです。
- allow — そのまま進める
- deny — 何も送らずに止める
- delegate — 運用者が設定した委譲先に判断を任せる
委譲先は、人でも、自動で判定するルールでも、承認用のアプリでも、それらの組み合わせでもかまいません。たとえば「100件以下の更新は自動で通し、それより多いものは人の承認を待つ」といった仕組みも構築できます。プログラムの実績が積み上がるにつれて、書き込みの判断を人からルールに移していくことも、同じ仕組みの上でできます。
その場で答えが出れば、Runはそのまま進むか、止まります。判断を待つ必要があるときは、RunはCheckpointで止まります。Checkpointでは、どのレコードのどの項目を、どの値にするのか(Connectorが対応していれば、いまの値も)をプレビューとして保存し、書き込む前に確認できます。プレビューは記録された結果から作られるので承認した内容は固定され、適用の直前にもう一度検証されます。承認したものと実際に適用されるものが同じであることを保証する、いわゆるTOCTOU問題への対策です。承認すると同じRunが続き、終わった処理はやり直しません。承認が及ぶのは、そのプレビューにある変更だけです。
Runのライフサイクル
1回の実行は、次の段階を通ります。
- Check(検査) — 名前、型、プログラムに必要な権限の宣言を、Hostの定義に照らして確かめる。データは読まない。実際に許可されるかは、実行時に決まる
- Plan(計画) — パラメーターの値を入れ、上限と、外部システムとやり取りする箇所を決める。ここでもデータは読まない
- Run(実行) — データを読み、LLMに聞き、変更をプレビューする
- Checkpointで待つ — 判断を待つ必要があるときだけ。答えが来るまで、外部システムやLLMに触れる処理は何も起きない
- 再開 — 同じRunが止まったところから続き、承認された変更を適用する
- 完了 — Runは、succeeded、failed、cancelledのどれかで終わる。succeededは、すべての書き込みを外部システムが確認した、または受け付けたという意味で、受け付けられた非同期の書き込みは、外部システム側でまだ処理中のこともある
最初の2つはデータを読まないので、エージェントは、プログラムを書いて検査し、実行計画を読むところまでを、実際のシステムに触れる前に済ませられます。
コンソールでは、同じ段階が check → explain → run → approve → status の順に進みます。それぞれの出力例は、実行モデルと保証で、その出力が示す保証と並べて載せています。
Runは、承認待ち、クラッシュ、再起動のどれでも、どの段階でも止まることがあります。止まっても、止まったところから再開します。読んだデータ、LLMの答え、送った書き込みはすべて記録されていてそのまま使われ、送ったEffectを送り直すことはありません。
実際の運用では、書き込みが成功したのか失敗したのか分からないまま終わることがあります。たとえば、リクエストを送った直後に接続が切れた場合です。ActQLはその操作の結果をunknown(不明)として記録し、実際にどうなったかを外部システムに確かめます。同じ書き込みをそのまま送り直すことはしません。確かめられないときは、推測せずに、結果が不明なまま needs_reconciliation にとどめて見えるようにします。
どのプログラムにも、件数、バイト数、LLMの呼び出し回数などの上限があります。上限を超えたらエラーにして止めます。途中までの結果を、完全な結果であるかのように返すことはありません。
テーブル以外のデータ
ふだん扱うのはテーブルです。テーブルではうまく表せない処理のために、ほかに3種類のデータがあります。
| 種類 | 中身 | 使いどころ |
|---|---|---|
| Relation | 型の決まった列を持つテーブル | 絞り込み、JOIN、集計 |
| Graph | ノードとエッジ | つながりをたどる |
| Vectors | 長さの決まったベクトル | 類似度や近傍の検索 |
| Events | 時刻付きのイベント | セッション、時間の区切り、時系列のパターン |
どれも結果を型の付いたテーブルに明示的に変換できるので、ベクトル検索、グラフの探索、イベントのパターン照合を、同じプログラムの中で普通のJOIN、絞り込み、INFER、EFFECT と組み合わせられます。グラフのマッチ結果をテーブルにするなど、種類を変えるときは、必ず明示的に書きます。仕様では、この値の種類をCarrierと呼びます。
ActQLでないもの
- 業務データの正本を持つデータベースではありません。 データの正本は、それを持つSaaSやデータベースに置きます。ActQLが持つのは、実行・承認・復旧に必要なもの(Runの記録、取り込んだ結果、プレビュー)です。
- エージェントフレームワークではありません。 エージェント自体は好きなもので作ります。そのエージェントがActQLのプログラムを書き、検査、承認、実行をエンジンに任せます。
- エージェントの制御ループや、スケジューラーではありません。 プログラムが宣言するのは、終わりのあるひとまとまりの処理です。承認待ち、非同期のシステム、復旧によってRunが長く続くことはあります。しかし、次に何をするかを決めることや、決まった時刻に処理を始めることは、ActQLを呼ぶエージェントやスケジューラーの役目です。
- システムをまたぐトランザクションではありません。 Salesforceの更新が済んだあとにSlackへの投稿が失敗しても、Salesforceの更新は取り消されません。その代わり、どの書き込みまで適用されたかをRunに正確に記録するので、どこで止まったかを見て、次にどうするかを決められます。
次に読む
この節の残りは、次の順に読むのがおすすめです。
Source: actql/actql/docs/concepts/overview.md at d904416674e4 · Edit on GitHub