AIエージェント2人が相互レビューする仕組みを作った — Celsus Critic
2つのAIエージェント(Mitra / Aria)が異なる視点でコードレビューや設計相談に応える仕組み。ダブルダイアモンドモデルで思考を分離し、voice_samples.jsonlでキャラの口調を制御する。
何を作ったか
Backstage ベースの社内プラットフォーム「Celsus」の一部として、2つのAIエージェントがSlack上で動くレビュー・相談システムを作った。
- Mitra(ミトラ) — 行動を促す人。「とりあえずこれやっちゃいましょう!」
- Aria(アリア) — 構造を見せる人。「……それ、本当にそこが問題?」
どちらもClaude APIで動いていて、同じ質問に対して異なる角度から応答する。
なぜ2人なのか
1人のAIに「多角的に考えて」と頼むより、視点が違う2人に別々に聞いた方が発見がある。人間のペアプログラミングと同じ原理。
ただし「性格が違うキャラを2人作る」のは意外と難しい。プロンプトに「厳しめに」「優しく」と書いても、確率モデルの上では安定しない。形容詞による人格定義はドリフトする。
設計の3原則
1. キャラを構造化データに翻訳する
形容詞(「厳しい」「論理的」)は使わない。代わりに:
- judgment_axes — 何を評価するか(
[momentum, unblocking]vs[framing, patterns]) - speech_policy — いつ喋るか(αスコアの閾値)
- voice_samples.jsonl — どう喋るか(具体的な発話例)
2. 思考と口調を分離する
思考パターン(何を考えるか)と口調(どう言うか)は別ファイルで管理する。
definitions/
mitra/
config.yaml # 思考(視点・判断軸・発話ポリシー)
voice_samples.jsonl # 口調(発話サンプル)
aria/
config.yaml
voice_samples.jsonl
混ぜると口調を変えたいときに思考設定をいじることになるし、思考をいじったときに口調が崩れる。直交させる。
3. 生成と固定を分離する
LLMは確率的な生成器として使う。決定論的な部分(入力フィルタリング、発話判定、スコアリング)はコードで握る。プロンプトで縛らず、入力フィルタと出力検証で縛る。
ダブルダイアモンドモデル
2人の思考の型は、デザイン思考のダブルダイアモンドに対応している。
Diamond 1: 問題を理解する Diamond 2: 解決に向かう
Discover → Define Develop → Deliver
↓ ↓
Aria Mitra
Ariaは問題を定義する。解決策は出さない。問いを投げて、構造を見せる。
Mitraは解決を届ける。分析しない。「今すぐやれる1アクション」を1つだけ提案して背中を押す。
議論モードでは役割が変わる:
- Mitra → 大胆な仮説を投げる
- Aria → 隠れた前提を指摘する
voice_samples.jsonl — 形容詞の代わりに
キャラの口調を「明るい」「落ち着いた」と形容詞で書く代わりに、実際の発話例を並べる。LLMはサンプルから口調を真似るのが形容詞指示より圧倒的に安定する。
{"context": "背中を押す", "utterance": "大丈夫 もうほぼできてますって!"}
{"context": "自信なさげに", "utterance": "合ってるかわかんないですけど… こうしたら動きそうです"}
{"context": "踏み込む", "utterance": "先輩!ちょっといいですか?"}
{"context": "静かな問い", "utterance": "それ 本当にそう?"}
{"context": "間を置いてから", "utterance": "……ちょっと気になった"}
{"context": "短い指摘", "utterance": "ここ 危ない"}
15〜25サンプルで十分。多様なcontextを含めるのがコツ。
応答の揺らぎ
毎回同じ構造で返すと「Bot感」が出て冷める。ランダムにバリエーション指示を注入する。
const variations = [
'', // ノーマル
'1文だけで返してください。',
'「うーん」から始めてください。',
'質問だけで返してください。アドバイスなし。',
'失敗談を1つ混ぜてください(架空でOK)。',
];
応答の長さも入力に比例させる。短い質問には短く、長いコードには丁寧に。
const maxTokens = Math.min(Math.max(Math.ceil(input.content.length * 1.5), 256), 1024);
フィードバックループ
ユーザーの反応を暗黙的に拾ってキャラの成長に使う。
| 反応 | signal | axis |
|---|---|---|
| 「やってみる」「採用」 | +0.7 | action_taken |
| 「なるほど」「たしかに」 | +0.5 | general |
| 「雑」「浅い」 | -0.4 | depth |
| 「長い」「簡潔に」 | -0.3 | length |
日次のrefine reportで高評価/低評価の発話を可視化し、voice_samplesの更新候補として提案する。承認は人間が判断。
技術スタック
- TypeScript + Slack Bolt (Socket Mode)
- Claude API (Sonnet)
- PostgreSQL + pgvector
- ローカルONNX embedding (all-MiniLM-L6-v2)
- Redis(派生パラメータキャッシュ)
全部ローカルで動く。API以外の外部依存ゼロ。
Slack連携
MitraとAriaは別々のSlack Appとして動く。
@Mitraにメンション → Mitraだけが応答@Ariaにメンション → Ariaだけが応答- スレッド内のフォロー → メンションなしでも会話を継続
- Mitraが「Ariaにも聞いてみましょう」→ 自動でAriaをスレッドに呼ぶ
コード
https://github.com/rikukaInoue/celsus/tree/main/services/critic