先週, 別件のブログ用に project_assets の再利用率を集計しようとしたら, 明らかに実在しない「PoC汎用モジュール」がトップ3に入っていた。DBを覗いてみると, 作った覚えのない relation が3件挿さっている。犯人は誰でもなく, 最近走らせ始めたブログ用エージェントだった。
1人で受託をやっている, と前回まで書いていた。ただ, 書き手は1人でも「DBに書き込む主体」は増えていく。ブログ生成, X投稿の下書き, 営業リサーチ。今のところ3種類のエージェントが同じ Supabase に触っている。今回は, その状況で「LLMの推測」と「確定した事実」をどう分けたか, の話。

結論: 2つの列を足すだけ
先に着地点を書く。project_assets テーブルに written_by と status を足した。それだけで, 集計クエリの汚染は止まった。
-- 前回 (第2回) で作った project_assets を拡張する
ALTER TABLE project_assets
ADD COLUMN written_by TEXT NOT NULL DEFAULT 'human',
ADD COLUMN status TEXT NOT NULL DEFAULT 'confirmed',
ADD COLUMN confidence REAL,
ADD COLUMN proposed_at TIMESTAMPTZ,
ADD COLUMN confirmed_at TIMESTAMPTZ,
ADD CONSTRAINT status_check
CHECK (status IN ('proposed', 'confirmed', 'rejected'));
-- 集計はほぼ confirmed しか見ないので, partial index で軽くする
CREATE INDEX assets_confirmed_idx
ON project_assets (asset_id)
WHERE status = 'confirmed';
written_by は 'human', 'blog_agent', 'x_agent', 'sales_agent' のいずれか。status は 'proposed' か 'confirmed'。人が入れたものはデフォルトで confirmed, エージェントが入れたものは常に proposed から始まる。ここで一段抽象化を挟まないのは意図的で, プロダクトが小さいうちは列を足す方が壊れにくい。
初手では別テーブル (project_assets_proposed) に分けようとした。テーブルが物理的に分かれていた方が汚染しないはずだ, と思ったからだ。結果的にはやめた。エージェントが提案した relation を人が承認するとき「別テーブルから移す」INSERT + DELETE のトランザクションを毎回書くのが億劫で, 実際にはレビュー画面を開かなくなる。同じテーブルに status 列を足す方が, レビューUI側で UPDATE ... SET status = 'confirmed' 一発で済む。承認コストが下がると, 承認が回る。
集計は confirmed のみを見る
前回書いた再利用率のクエリはこう変わった。
-- 再利用率 = 2件以上の source_project に登場する asset の割合
SELECT
COUNT() FILTER (WHERE reuse_count >= 2)::float
/ NULLIF(COUNT(), 0) AS reuse_rate
FROM (
SELECT asset_id, COUNT(DISTINCT source_project) AS reuse_count
FROM project_assets
WHERE status = 'confirmed' -- ★ 足したのはこの1行だけ
GROUP BY asset_id
) t;
足したのは WHERE status = 'confirmed' の1行。ただ, これがないとブログ用エージェントが「たぶんこの案件, 前の認証モジュールを使い回してそう」と推測して書き込んだ relation が全部混ざる。数字は勝手に上がって, 意思決定が壊れる。partial index を張っておくと proposed が増えても集計は遅くならない。
エージェント同士は呼ばない, DBだけを読む
2026年のこのあたりの潮流として, LangGraph のように「共有 State を最初に定義してからエージェントを組む」型のフレームワークが主流になってきた。よくできていて, 精密な制御が要る場面では強い。ただ, 1人スタジオの規模だと, フレームワークを1本据えて全エージェントをそこに押し込むのは過剰な気がしていた。
今の構成では, フレームワークを挟まず, エージェント同士は互いを呼ばない。全員, DBだけを読む。
// blog_agent が新しい記事を書く前に「文脈」を取りに行く
const { data: reusable } = await supabase
.from('project_assets')
.select('asset_id, source_project, notes')
.eq('status', 'confirmed') // 推測は読まない
.in('source_project', relatedProjectIds)
.order('confirmed_at', { ascending: false })
.limit(20);
// この結果をそのまま prompt に混ぜる
const prompt = buildPrompt({
topic: input.topic,
reusableAssets: reusable ?? [],
});
これだけで, ブログエージェントは「うちが実際に使い回している資産の一覧」を毎回見て記事を書く。X用のエージェントも同じテーブルを別の切り口で読む。エージェント間の contract は「テーブルのスキーマ」だけだ。片方が壊れても, もう片方は動き続ける。
正直, これはフレームワーク信仰から見ればナイーブな構成だと思う。エラー時のリトライも, 並行制御も, 全部 Postgres 側の制約 (unique 制約, transaction, advisory lock) に任せている。ただ, 「共有 State = DB」と割り切ると, 新しいエージェントを1匹足すコストが劇的に下がる。既存の Supabase クライアントを渡して SELECT ... WHERE status = 'confirmed' と書かせるだけで, そいつはその時点までの確定した文脈を持って動き始める。
エージェントが提案 → 人が承認, の書き方
提案側と承認側は, こう分けた。
// 1. blog_agent が「この案件でこの asset を使ったのでは」と推測
await supabase.from('project_assets').insert({
source_project: 'client-x-2026Q3',
asset_id: 'auth-module-v2',
written_by: 'blog_agent',
status: 'proposed',
confidence: 0.62,
proposed_at: new Date().toISOString(),
notes: 'README に「既存の認証を流用」と記載あり',
});
レビュー画面では status = 'proposed' の行だけ拾って表示する。承認は次の1行。
UPDATE project_assets
SET status = 'confirmed',
written_by = 'human', -- 承認した瞬間から「人が保証」
confirmed_at = NOW()
WHERE id = $1;
ここで written_by を 'human' に上書きしているのは意図的だ。誰が最初に書いたかは proposed_at と別の audit テーブルに残しているが, 「今この relation を保証している主体」は承認した瞬間から人になる。この列は「起源」ではなく「今の保証者」を表す, と決めた。設計時に一度混乱したので念のため書いておく。
小さいNと, 自分の見積もりバイアス
ここまで書いておいて何だが, 手元の project_assets テーブルは行数がまだ2桁台だ。統計というより方向性を見ている, というのが正直なところ。「再利用率が上がった」と胸を張って言えるNには, たぶんまだ半年はかかる。
もう1つ, 前回の記事で「関連付けに毎回30秒」と書いたが, あれは自己申告の見積もりで, 実測ではない。ストップウォッチを持って何回か測ってみると, 集中しているときは10秒, 別作業から思い出して切り替えるコストが乗ると2分, と分散した。第1回で「オントロジーを作らないオントロジー」と書いたときの前提の一部が, ここで少し崩れた。設計に着手する前に平均を測っておけばよかった, と後から思う話でもある。
まとめ
written_by と status の2列だけで, LLMの推測は集計から隔離できる。エージェント同士は互いを呼ばず, DBだけを読み書きする。新しいエージェントを増やすときは, テーブルへの権限を渡して WHERE status = 'confirmed' と書かせれば, そいつはその時点までの確定した文脈を持てる。フレームワークを1本据えるより, スキーマ1枚で握るほうが, この規模には合っていた。
これで S7 の3本目, 「1人スタジオの軽量オントロジー」の連載はひとまず区切り。この上に LLM 側の推論を積む話 — proposed の自動生成をどこまで任せるか, confidence の較正をどう回すか — は, また別の連載でいずれ書く予定。
筆者は 5years+ で韓国・日本向けの AI 実装をやっている。