社内の文書で RAG を作ると、最初に必ずぶつかるのが権限です。人事評価の資料や経営会議の議事録が入った AI を、そのまま全社員に開放するわけにはいきません。この記事では、よく見かける 2 つのやり方がなぜ権限を守れないのかを整理し、検索の段階で対象を絞る設計を説明します。
1. 「見せてから隠す」では守れない
よく見かけるのは、次の 2 つのやり方です。
- プロンプトで指示する ― 「人事部以外には評価の内容を答えないこと」とシステムプロンプトに書く。
- 回答のあとで検査する ― 生成された回答に機密の語が含まれていないかを確かめてから返す。
どちらも、機密の文書が一度 LLM のコンテキストに入っています。指示は質問の書き方しだいで破られますし、回答は複数の文書を混ぜて言い換えたものなので、どの文書のどの部分が混ざったかを後から判定するのは困難です。
原則: LLM のコンテキストに入った時点で、その内容は利用者に渡ったものとみなす。
2. 検索の段階で絞る
守るべき場所は LLM の手前、検索です。権限のない文書は、そもそも検索結果に入れません。
チャンクに権限の情報を持たせる
文書を分割したチャンクごとに、テナントと参照できる範囲をメタデータとして付けておきます。検索のときは、利用者の所属やロールからフィルタを組み立て、ベクトル検索の条件として渡します。
{
"must": [
{ "key": "tenant_id", "match": { "value": "t_8f2c" } },
{ "key": "acl.roles", "match": { "any": ["member", "sales"] } }
]
}絞ってから上位を取る
上位 k 件を取ってから権限で間引くと、残る件数が利用者によってばらつき、権限の少ない人ほど回答の精度が落ちます。フィルタは検索と同時にかけ、権限のある文書の中から上位 k 件を取るようにします。
3. 境界はデータベースにも持たせる
文書やポリシーの原本はリレーショナルデータベースにあります。ここでもアプリケーションの WHERE 句だけに頼らず、PostgreSQL の行レベルセキュリティ(RLS)でテナントの境界を持たせます。
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON documents
USING (tenant_id = current_setting('app.tenant_id')::uuid);
-- リクエストごとに、接続にテナントを設定してから問い合わせる
SET app.tenant_id = '3c1e…';こうしておけば、あるクエリで条件を書き忘れても、他のテナントの行は返りません。
4. 権限が変わったとき
ロールの割り当てや文書の公開範囲が変わったら、チャンクのメタデータも更新する必要があります。ベクトル自体は文書の内容から作られているので、作り直す必要はありません。メタデータだけを文書単位で更新すれば、次の検索から新しい権限で絞られます。
まとめ
- LLM のコンテキストに入った時点で、その内容は利用者に渡ったものとみなす。
- 権限は検索の段階で、検索と同時にかける。
- テナントの境界はデータベース(RLS)にも持たせる。
ZEMIX では、ここで説明した考え方を製品の基盤に組み込んでいます。文書ごとに「誰が参照できるか」を決めておくと、AI はその範囲の文書だけを使って答えます。
文書の取り込みから検索・エージェント・配信までを作っているチームです。実装から得た知見を書いています。