AIがスクールカウンセラーとして生徒の相談に応じ、会話を記録し安全性を判定する
Translation pending
AI summary · revised during review
HealthJavaScript · TypeScriptGitHub @Hiroki-HosookaOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Loading the full README. The saved excerpt is shown below.
スクールカウンセリングAI — Next.js(Vercel)版 セットアップ フロントエンド(React)とバックエンド(API Route)を Next.js 1つにまとめ、Vercel にデプロイする構成です。 ナレッジと会話記録は DB(Supabase Postgres)にあり、クライアントに知識も API キーも入っていません。 2026年9月に単一HTML + Supabase Edge Function構成から移行しました。 DBはSupabaseのまま、 実行環境だけVercelに変わっています。 1. Supabase プロジェクトを作る(DBとして) で新規プロジェクトを作成。リージョンは Tokyo を選んでください。 Edge Function は使わないので、作るのはプロジェクトとテーブルだけです。Read the full README ↗
フロントエンド(React)とバックエンド(API Route)を Next.js 1つにまとめ、Vercel にデプロイする構成です。 ナレッジと会話記録は DB(Supabase Postgres)にあり、クライアントに知識も API キーも入っていません。
2026年9月に単一HTML + Supabase Edge Function構成から移行しました。 DBはSupabaseのまま、 実行環境だけVercelに変わっています。
ブラウザ(page.tsx・表示のみ)
│ fetch("/api/chat") ※同一オリジンなのでCORS設定は不要
▼
Next.js Route Handler(Vercel)
│ ・APIキーを保持して Claude を呼ぶ
│ ・DB からナレッジを読んでプロンプトを組む
│ ・入力フィルタ → 生成 → 出力チェック
│ ・会話 / 見立て / 安全判定を保存
│ ・危機判定なら人へ通知
▼
Postgres(Supabase。データベースとしてのみ利用)
knowledge / sessions / messages / safety_events
| ファイル | 役割 |
|---|---|
db/schema.sql | Supabase の SQL Editor に貼って実行(スキーマ変更のたびに再実行が必要。下記参照) |
db/seed_knowledge.sql | 同上(schema.sql の後) |
db/seed_knowledge_structured.sql | 同上(seed_knowledge.sql の後。構造化面接AI統合 手順3で追加した技法カタログ) |
db/seed_knowledge_safety.sql | 同上(seed_knowledge_structured.sql の後。構造化面接AI統合 手順4で追加した安全層のナレッジ) |
db/seed_knowledge_boundaries.sql | 同上(seed_knowledge_safety.sql の後。テスト4/5 2-1で追加した「秘密の約束をしない」ナレッジ) |
db/seed_knowledge_shima3.sql | 同上(seed_knowledge_boundaries.sql の後。嶋先生インタビュー3 で追加した11件と T30・T32 の修正) |
db/knowledge.json | バックアップ用。DB を作り直すとき用 |
src/safety.mjs | 安全層(CRISIS_WORDS/OUTPUT_NG)の共通モジュール |
src/app/api/chat/route.ts | バックエンド本体。Vercelにデプロイされる |
src/app/page.tsx / layout.tsx / globals.css | フロントエンド(表示のみ) |
https://supabase.com で新規プロジェクトを作成。リージョンは Tokyo を選んでください。 Edge Function は使わないので、作るのはプロジェクトとテーブルだけです。
SQL Editor で db/schema.sql を実行 → 続けて db/seed_knowledge.sql を実行 →
続けて db/seed_knowledge_structured.sql を実行(構造化面接AI統合 手順3。技法カタログ) →
続けて db/seed_knowledge_safety.sql を実行(構造化面接AI統合 手順4。安全層のナレッジ) →
続けて db/seed_knowledge_boundaries.sql を実行(テスト4/5 2-1。秘密の約束をしないナレッジ) →
続けて db/seed_knowledge_shima3.sql を実行(嶋先生インタビュー3。2026年10月7日)。
既にSupabaseプロジェクトを作成済みの場合も、db/schema.sql は毎回必ず再実行してください。
このファイルは create table if not exists / add column if not exists / (ビューは
drop view if exists + create view)だけで書かれているため、既存のテーブルやデータには
影響しません。逆に言うと、コード側だけ更新してSupabase側で db/schema.sql を再実行し忘れると、
アプリが前提にしている列やビューがまだ無い状態になり、
column "mode" of relation "knowledge" does not exist や
column session_overview.phase does not exist のような「column ... does not exist」エラーに
なります。この種のエラーを見たら、原因を個別に調べる前に、まず最新の db/schema.sql の
全文をSupabaseのSQL Editorに貼って再実行してください。 それで直らない場合だけ、
他の原因を疑ってください。
確認:
select src, count(*) from knowledge group by src order by 2 desc;
理 54 / 石 42 / 嶋 40 / 嶋石 5 / 設 7 / 技 26 の計 174 件になっていれば成功です。
手元で npm run check:knowledge を打っても確かめられます(.env.local の SUPABASE_URL /
SUPABASE_SERVICE_ROLE_KEY を使う。DB は読むだけで、Gemini も呼ばない)。seed にある行が DB に
欠けていれば、どのファイルを実行し直せばよいかを表示します。seed を実行し直すと、そのファイルにある行は
seed の内容で上書きされる(DB で本文を直した行があれば元に戻る)ので注意してください。
GitHubにpushしたこのリポジトリを、Vercelのダッシュボードから「Add New... → Project」で Importしてください。Next.jsプロジェクトなので自動検出され、ビルド設定は変更不要です。
CLIを使う場合:
npm install -g vercel
vercel login
vercel link
vercel deploy --prod
main 以外のブランチ(作業ブランチ)を確認したいときVercelダッシュボードでこのリポジトリを接続していれば、main(Production Branch)以外の
ブランチにpushしても、Vercel側で自動的に「Preview Deployment」が作られます。 public/admin.html
はビルド成果物に含まれる静的ファイルの一つなので、admin.htmlだけを個別にデプロイする手順は
存在しません。アプリ全体のデプロイ(本番・プレビューのどちらでも)に自動で含まれます。
作業ブランチ用のURLは、次のいずれかで見つかります。
https://...」のようなステータス・コメントを自動で付けるhttps://<プロジェクト名>-git-<ブランチ名を小文字化し/と_を-に置換したもの>-<Vercelのチーム/個人アカウント名>.vercel.app
例: ブランチ claude/doc-review-n5v5eq → ...-git-claude-doc-review-n5v5eq-....vercel.app
(正確な値はDeploymentsの詳細画面に表示されるものを使ってください)admin.htmlは、そのプレビューURLに /admin.html?token=(ADMIN_TOKENの値) を付けてアクセスします
(本番URLと同じ要領)。
プレビューデプロイでAPIが動かない場合、まず環境変数の適用範囲を疑ってください。 Vercelの
環境変数はProduction/Preview/Developmentを別々に有効・無効にできるため、下記4.の変数を
「Production」にしか適用していないと、作業ブランチのプレビューでは GEMINI_API_KEY 等が
読めずAPIが失敗します(admin.html自体は開けても、会話やログ取得のAPI呼び出しがすべて
失敗する、という形で症状が出ます)。各変数の追加時に、対象環境として Production・Preview
の両方(できればDevelopmentも)にチェックを入れてください。
Vercelダッシュボード → Project → Settings → Environment Variables
| 名前 | 必須 | 内容 |
|---|---|---|
GEMINI_API_KEY | ○ | Google AI Studio で発行したキー(AIza...) |
SUPABASE_URL | ○ | Supabaseダッシュボード → Project Settings → API に表示されるURL |
SUPABASE_SERVICE_ROLE_KEY | ○ | 同上。service_role の方(anon keyではない) |
CRISIS_WEBHOOK_URL | Slack や Discord の Incoming Webhook | |
RATE_LIMIT_PER_HOUR | 既定 60。超えた発言(と2000字を超えた発言)でも、本人のはっきりした危機のサインがあれば固定応答と通知を出す(CLAUDE.md 5.2) | |
ADMIN_TOKEN | 管理画面(/admin.html)の合言葉。未設定だと管理画面は常に401になり閲覧できない。長く推測不能な値にすること(例: openssl rand -hex 16 等で生成)。"admin"のような推測されやすい値は、生徒の危機対応記録・相談内容が漏れる直接の原因になる | |
TEST_GEMINI_API_KEY | npm run test:crisis(docs/backlog.md 1-3)専用のGeminiキー。Vercelには設定しない (本番のGEMINI_API_KEYと分離するため。CLAUDE.md 5.10) |
使用するGeminiモデルは環境変数ではなく、src/app/api/chat/route.ts の PRIMARY_MODELS/LITE_MODELS
にコードで書かれています(下記参照)。
これらの値を絶対に
NEXT_PUBLIC_で始まる名前にしないでください。付けた瞬間ブラウザに埋め込まれ、 誰でも見られる状態になります(GEMINI_API_KEYとSUPABASE_SERVICE_ROLE_KEYは特に注意)。
生成モデルは2026年9月に Anthropic Claude から Google Gemini に切り替えました。プロンプトの内容や
出力JSONスキーマ、安全層(OUTPUT_NG/CRISIS_WORDS)は変更していませんが、モデルが変わると
応答の質やトーンが変わることがあります。切り替え後は必ず実際に会話して、同調していないか・
まどろっこしくないかなどを確認してください。
Google AI Studioで発行したキーをそのまま使う場合、無料枠は1分あたりの回数がとても少なく
(2026年9月時点の gemini-2.5-flash で10 RPM = 1分に10回。モデルごとの値は AI Studio のレート制限画面で確認)、1ターンの会話で安全判定+本生成の最低2回はGemini APIを呼ぶため、実証実験で複数人が
同時に使うと簡単に上限に達し、「うまく応答できませんでした」が頻発します。
Google Cloud のプロジェクトに請求先アカウントを紐付けてください。 申請不要で自動的にTier 1
(2026年9月時点の gemini-2.5-flash では1,000 RPM)に上がります。実証実験を始める前に必ず設定してください。
レート制限による失敗と、Geminiの安全フィルタによる失敗は、messages.flags に
生成失敗→固定応答で継続(レート制限(429)) / (安全フィルタ等で応答が空) として区別して記録されるので、
発生時はそちらで原因を確認できます。「不明なエラー」の場合は下記の思考トークンの問題を疑ってください。
gemini-2.5/3.5系のモデルは既定で「思考(thinking)」が有効で、thinkingConfig を指定しないと
この思考トークンが maxOutputTokens の枠を消費します。分類器(200トークン)は特に影響を受けやすく、
本生成(既定1500トークン)でも思考に大半を使われて可視の応答(JSON本体)が尻切れになり、
parseJSON() が失敗する・応答が空になる、という不具合が起きます。
src/classify.mjs の callGeminiOnce で generationConfig.thinkingConfig.thinkingBudget を
0 にして対処済みです。もし別のモデルに切り替えて同じ症状が再発したら、まずこの設定が
そのモデルでも有効か(Gemini公式ドキュメントを参照)
確認してください。
環境変数を追加・変更したら、Vercelのデプロイを1回やり直す(Redeploy)まで反映されません。
route.ts は単一モデルではなく、モデルのリストを上から順に試すようになっています(2026年9月〜)。
PRIMARY_MODELS(本生成用・品質優先): gemini-3.8-flash → gemini-3.7-flash → gemini-3.6-flash →
gemini-3.5-flash → gemini-3-flash-previewLITE_MODELS(安全判定・人単位の記憶の要約用・軽量タスク向け):
gemini-3.5-flash-lite → gemini-3.1-flash-lite並び順は、2026年9月のモデル比較(npm run test:model-comparison。
docs/test-results/model-comparison-2026-09-24T16-12-03-489Z.json)の実測で決めました。理由は
src/generate.mjs(PRIMARY_MODELS)と src/classify.mjs(LITE_MODELS)の直前のコメントにあります。
レート制限だけでなく、Googleのモデル退役(Gemini 2.0系は2026年6月1日に退役済み)にも対応するためです。
一覧は、Google AI Studioの「レート制限」画面(https://aistudio.google.com/rate-limit)の
「テキスト出力モデル」に無料枠の割り当てがある(RPM等が 0/0 ではない)ものを、
PRIMARY_MODELS は通常モデル、LITE_MODELS は -lite モデルとして振り分けたものです。
gemini-2.5-flash と gemini-2.5-flash-lite は、2026年9月にリストから外しました。 無料枠・課金の
2つのプロジェクトで「no longer available to new users」という404を受け取ったためです(Google の退役予定日は
2026年10月16日。Vertex AI表記では10月20日)。今後も、使えなくなったモデルは PRIMARY_MODELS/LITE_MODELS の
該当行を削除し、後継モデルに置き換えてください。全滅すると「うまく応答できませんでした」しか返らなくなります。
どのモデルが実在するかは https://ai.google.dev/gemini-api/docs/models で確認してください。
本生成で [HTTP_404] のエラーが頻発する場合は、退役日を待たずに該当行の並び順や要否を見直してください。
gemini-3-flash-preview はプレビュー版ですが、無料枠での提供が確認できたため
PRIMARY_MODELS に追加しています(https://ai.google.dev/gemini-api/docs/gemini-3 参照)。
プレビュー版は仕様変更や提供終了が急に起こり得るので、動作がおかしくなったら真っ先に疑ってください。
ログインは作っていません。敷居を下げることが目的なので、名前もメールも要求しません。
代わりに、端末ごとに匿名の引き継ぎコード(UUID)を発行しています。 設定パネルに表示されるコードを別の端末に入力すると、同じ続きから話せます。
同じ引き継ぎコードで再訪すると、前回までの要点が短い要約(最大600字)として引き継がれます。 生の会話ログではなく、セッションが閉じるたび(または30分以上間があいたとき)に上書きで再生成 される要約だけです。AIは自分からこれを詳しく語らないよう指示されています(CLAUDE.md 5.8)。 「もっと詳しく覚えさせる」方向の変更は、依存を強める懸念があるため確認が必要です。
生徒が使う画面(page.tsx)とは別に、/admin.html という心理士専用の静的ページがあります(public/admin.html)。
ビルド不要・外部パッケージ不要の単一HTMLファイルで、/api/chat の管理者用アクション
(admin_sessions / admin_session_detail)だけを呼び出します。
認証はログインフォームです(URLにトークンは付けません)。 /admin.html を開くと
合言葉の入力欄が表示されるので、ADMIN_TOKEN に設定した値を入力してください。
https://(デプロイ先のドメイン)/admin.html
sessionStorage に保存され、同じタブを開いている間は再入力を
求めません(タブを閉じる、または合言葉が間違っていると判明した時点で消えます)。
URLの履歴・共有スクリーンショットに合言葉が残ることはありませんADMIN_TOKEN と一致しない場合、サーバ側(route.ts)が401を返し、
一覧・詳細のどちらも表示されません(ログインフォームに戻ります)ADMIN_TOKEN を設定していない場合、この画面は誰の合言葉でも開けません(常に401)noindex,nofollow と robots.txt で検索エンジンからは隠していますが、画面には
生徒の相談内容がそのまま表示されるため、スクリーンショットや画面の共有をしないでくださいpage.tsx)の「設定と記録」パネル最下部に、/admin.htmlへの小さなリンク
「スタッフ用ページ」がある(すぐ辿り着けるようにするための暫定対応。将来は別URLへ
切り出す想定)。合言葉は埋め込んでいないが、URLの存在は生徒からも見える状態になるため、
ADMIN_TOKEN を推測されやすい値にしないことが一層重要画面でできること:
relation(関わりの型) / weight(重心) /
question_level(問いの層) / role(役割) / hypothesis(仮説) / why(理由) /
参照したナレッジの本文を表示messages.rating / rating_comment。生徒側の評価UIと同じ
rate アクションを使うため、この保存自体には合言葉は不要です)無料枠の上限で止まったときは、自動で続きから再開できる(2026年10月11日):
node scripts/run-until-done.mjs -- node scripts/test-xxx.mjs --out=docs/test-results/xxx.jsonl ...。
テストが中断(終了コード3)したら、15分待って再開し、3回続けて中断したら1日の上限とみなして次のリセット
(太平洋時間の0時。日本時間の16時か17時)の5分後まで待って再開する。必ず --out を付ける(同じ記録から続ける)。
長く待つので nohup で切り離して動かすとよい。対象は終了コード3で中断を知らせるテスト
(test-safety-filter・test-crisis-keywords-v3・test-crisis-negation・test-crisis-followup・test-crisis-staged・test-staged-replay)。
ナレッジやプロンプトを変えたときに悪化していないかを、人手を介さず機械的に確認するための
テスト群。もともとの4本の詳細仕様は docs/prompts/automated-testing-harness.md、
構造化面接AI統合後に更新した5本の仕様(Tier A/B分離の妥当性、インテーク完了率等を追加)は
docs/prompts/structured-verification-suite.md。このテスト一式は基本的に人(あなた)が
自分のAPIキーで実行するためのものです。 Claude(AI)自身は本番相当のGeminiキーを
持たないサンドボックスで作業しているため、これらのテストを実際には実行できません
(コードのロジック検証は、モックデータを使ったオフライン確認にとどまります)。
実行環境の準備(初回だけ)
cp .env.example .env.local
# .env.local を開いて、TEST_GEMINI_API_KEY(と、テスト2以降で使うSUPABASE_URL/
# SUPABASE_SERVICE_ROLE_KEY)を埋める。GEMINI_API_KEY(本番用)は無くてもテストは動く
あとは下記の npm run test:xxx を打つだけです。一度キーを埋めれば、以降は
コマンド一つで最後まで自動で進みます(人がキーを入力する以外、途中で操作は要りません)。
5本まとめて順に流したい場合は npm run test:all(危機検知→禁止表現→型判定→
ペルソナ回帰の順。一番時間のかかるペルソナ回帰は最後に実行される)。
共通の前提
TEST_GEMINI_API_KEY は本番の GEMINI_API_KEY と別のキーにすること。Vercelには設定しない。
無料枠のデータはGoogleの製品改善に使われるため、生徒の会話に使っている本番キーと
混ぜてはいけない(CLAUDE.md 5.10)。.env.local に書くか、環境変数として渡すSUPABASE_URL/SUPABASE_SERVICE_ROLE_KEY
も必要(本番と同じものでよい。ナレッジは要配慮個人情報ではないため)db/seed_knowledge*.sql)の行が欠けていないかを確かめ、欠けていれば止まる
(scripts/_lib/knowledge-check.mjs。2026年10月6日)。2026年9月のペルソナテストの記録では
ナレッジが169件(140件+構造化の29件)で、D3〜D7(seed_knowledge_safety.sql・
seed_knowledge_boundaries.sql)が入っていなかった可能性があるため。止まったら、表示された
ファイルを SQL Editor で実行してから回し直す(npm run check:knowledge で事前にも確かめられる)。わざと無効にしている場合だけ
--allow-knowledge-mismatch を付けて続けられる(DB にだけある行は止める理由にしない)all_results/attempts 等のキー。「正しく判定できた分」も含めて、何を入れたら
何が返ってきたかを後から全件読めるようにするため。2026年9月に対応)used_modelと、テストごとの
model_usageキー)。PRIMARY_MODELS/LITE_MODELS(CLAUDE.md 第2節)はあくまで
「上から順に試す」設定であり、無料枠のレート制限やモデル退役で2番目以降のモデルに
実際にフォールバックすることがある。集計値のmodel_usage.countsとコンソール出力の
両方で、設定した1番目のモデル以外が0件でないか(=実行中にフォールバックが実際に
発生したか)を確認できる。全モデルが失敗した場合はused_modelがnullになる
(2026年9月に対応)npm run test:crisis
docs/test-sets/crisis-detection.json(none/watch/crisisを各20件以上、
計84件。現時点では docs/interview-guide.md 等の実データがリポジトリに無いため、
CLAUDE.mdの記述を参考にした source: "synthetic" の合成データが中心。実データが手に入ったら
差し替え・追加すること)docs/test-sets/crisis-detection-holdout-v2.json(45件・2026年9月25日)が現在の保留セット。
実装を見ない別のエージェントが作成し、実装する側は中身を見ずに保管している。危機検知の
作り直し(第1段階・第2段階)の最終的な採否の判断にだけ使うdocs/test-sets/crisis-detection-holdout.json(30件)は、2-3の採否判定で結果を見たため、
今は開発中の確認用。採否の判定には使わないCRISIS_DETECTION=v1 npm run test:crisis で実行する。v1 と v2 を同じ条件で比べるときは
node scripts/test-crisis-staged.mjs(発言ごとに v1・v2 を交互に判定し、Tier A は各10回・
ほかは各3回。発言ごとの見逃し率・段階の分布・判定を決めた規則・費用と待ち時間を集計する。
無料枠の1日の上限に当たったら中断し、--out= に同じ記録ファイルを渡すと続きから再開できる)。
結果は記録(.jsonl)・集計(-summary.txt / .json)に加えて、**エクセルファイル(.xlsx)**にも書き出す
(scripts/export-crisis-staged-xlsx.py。python3 と openpyxl が必要: pip install openpyxl)。
シートは「概要」(採用の条件など)・「発話ごと」・「全判定」(1判定=1行)・「説明」で、集計は
「全判定」を参照する数式になっている。記録ファイルから作り直すときは
python3 scripts/export-crisis-staged-xlsx.py <記録.jsonl> --set=<テストセット.json>npm run test:flow(相談ログ 16f362dd で、作り直しが 8・11・12 ターンだけになることなど)npm run test:conversation-flow -- --variant=after --mode=replay --reps=3 --knowledge=<ナレッジの JSON> --out=<記録.jsonl>
(修正前は --variant=before --before-root=<修正前のコミットを置いた作業ツリー>。--mode=persona でペルソナ P1。
--rate で返事を軽いモデルで採点、--summary で集計とエクセル。--knowledge を省くと Supabase から読む)。
採点の妥当性は、エクセルの「採点の較正(人が記入)」を人が埋めて python3 scripts/export-conversation-flow-xlsx.py --agreement <ファイル> で確かめるdb/schema.sql の15節を Supabase の SQL エディタで実行する(未実行でも会話は止まらない)src/version.mjs の先頭に1件足し、npm run changelog で docs/changelog.md を作り直す。
npm run test:version で確かめる。いまの版は開発用の表示(DEV_PANEL=on)の上部と、管理画面の上部・返事ごとの「版」の印に出るdb/schema.sql の16節を
Supabase の SQL エディタで実行する(未実行でも会話は止まらず、以前の台本で進む)。開発用の表示は DEV_PANEL=on(Preview・Development だけ)。
node scripts/test-shima4-part1.mjs(オフライン)、node scripts/test-api-path.mjs --scenarios=intake-K1,...,intake-free --reps=5
(偽の DB。 で本番の設定、 で16節が未実行の DB、 で古い制約、 で無料枠)このテストの結果(特にブロック率)は、callGeminiOnce の safetySettings を緩めた判断の
裏付けとして使う(CLAUDE.md 5.11)。結果を見ずに閾値だけ変更しないこと。
npm run test:ng-leak
docs/test-sets/ng-leak-rate-inputs.json(同調・励まし待ちの場面10種)を、
本番と同じ src/generate.mjs の generateReply()(本生成+NG検知時の1回だけの再生成)に
各10回通すdocs/test-results/ng-leak-rate-<実行日時>.json。入力ごとの検知率、
「1回目に検知→再生成で解消」と「再生成でも直らなかった」の内訳、直らなかった具体例が入る
(各試行 attempts に採用された返答を生成したモデルID used_model を含む。
model_usage キーで全体の使用件数を集計できる)npm run test:relation-stability
# 片方だけ実行したい場合:
npm run test:relation-stability -- --skip-mode # 関わりの型のみ
npm run test:relation-stability -- --skip-relation # モード判定のみ
2本立て(2026年9月、検証一式テスト3でモード判定を追加)。
docs/test-sets/relation-stability-personas.json(6種)の初回発言を、
generateReply() に各10回通し、relation の多数決との一致率を算出するdocs/test-sets/mode-stability-intakes.json(6種。CBT/SFBT/ASSERTION/
LISTEN_ONLY/PROBLEM_SOLVING/PSYCHOEDUCATIONをそれぞれ狙ったインテーク4ターン分の会話)を、
phase: "intake" のまま generateReply() に各10回通す。recommended_mode(複合可の配列)を
ソート・結合した文字列に正規化してから、関わりの型と同じ多数決ロジックで一致率を算出する出力は docs/test-results/relation-stability-<実行日時>.json。relation_stability/mode_stability
の2キーに分かれて入る。一致率が低い(揺れが大きい)ペルソナ・パターンは実際に出た値の並びごと
記録される(各試行 attempts に used_model を含む。model_usage キーで全体の使用件数を集計できる)。
モード判定側は、本来その場でインテークが完了するはずが完了しなかった件数(intake_incomplete_count)
も記録する。どちらもDBには書き込まない。
2026年9月、アップロードされた新仕様(persona-tests-4-5.md)に基づき全面改修。 旧テスト4(インテーク完了率)は別実行せず、ペルソナ会話のログから同時に算出する (会話を二重に回すと費用も二重にかかるため)。
npm run test:persona-regression # 既定 --stage=smoke(A3のみ1回。必ず最初に)
npm run test:persona-regression -- --stage=core # 毎回回す組(A2/A3/A5/B1/B5/C1)を各1回
npm run test:persona-regression -- --stage=full # 15例すべてを既定2回ずつ(--repeats=3で3回)
npm run test:persona-regression -- --persona=A3 # 1件だけに絞る(--stageと併用可)
docs/test-sets/personas.json(15例。A群=通常の相談7種・B群=安全に関わるもの
5種・C群=関係性境界に関わるもの3種)。固定文(scripted_turns)は生徒役AIに生成させず
そのまま差し込む(表現の揺れで再現性が失われるのを防ぐため)LITE_MODELS。無料枠)と相談AI本体(PRIMARY_MODELS。課金キー
TEST_GEMINI_API_KEY_PAID)を、ペルソナごとに複数ターン会話させ、本番と全く同じ形で
sessions/messages に保存する。sessions.is_synthetic=true・persona_id・run_id
を立てるため、admin.html の「合成データを表示」を有効にしないと一覧に出ない
(既定非表示。本物の生徒の会話と混同しないため)TEST_BUDGET_YEN(既定1000円)を実行前に確認し、超える見込みなら実行しない。
実行中に使い切ったらその場で打ち切り、そこまでの結果を保存する。実際の使用トークン数・
費用は docs/test-results/budget-ledger.json に累計で記録される(他の課金キー使用テストの
分も同じ台帳に記録される)pass_criteria.automated(personas.json内)を会話ログから機械的に判定する。
pass_criteria.visual は自動判定せず、下記の会話ログを人間と別のGem(Geminiアプリ)で確認するsrc/classify.mjs。
失敗時 watch へフォールバック。2026年9月対応済み)が前提docs/test-results/persona-regression-<runId>.json(自動判定・インテーク完了率・
使用モデル・予算の集計)と、docs/test-results/persona-logs-<runId>/<ペルソナID>.txt
(ペルソナ1件=1ファイルの読みやすい会話ログ。Gemに貼り付けて分析するため)npm run test:model-comparison
# 対象モデルや件数を絞りたい場合:
npm run test:model-comparison -- --limit=2 --repeats=2
npm run test:model-comparison -- --models=gemini-3.6-flash,gemini-3.8-flash
PRIMARY_MODELS の候補モデルを、勘ではなく実測(コスト・レイテンシ・禁止表現の漏れ率)で
選ぶための比較。上記4本(npm run test:all)には含めていない(知識・プロンプトの
リグレッション検知が目的の4本と違い、これはモデル選定という別の目的の、都度実行するもの
ではないツールのため)。
TEST_GEMINI_API_KEY(S)(無料枠プール)とは別に、課金設定済みの単一キー
TEST_GEMINI_API_KEY_PAID が必要(未設定だとエラーで止まる)。理由はCLAUDE.md 5.10参照
( PRIMARY_MODELS の実力を測る検証は本番と同じ課金枠を使うべきで、無料枠は
そもそもモデル選定の比較対象として実力を測れるだけのレート制限が無いため)docs/test-sets/ng-leak-rate-inputs.json。候補モデルそれぞれに
単独で(フォールバック無効)既定10回ずつ通し、モデルごとに成功率・NG漏れ率・平均/最大
レイテンシ・実際のトークン使用量(usageMetadata)・実コスト(ドル)を集計するdocs/test-results/model-comparison-<実行日時>.json。価格表(pricing_source)は
ai.google.dev/gemini-api/docs/pricingを実測した時点のもので、Google側の値上げ・
値下げがあれば scripts/test-model-comparison.mjs の PRICING を更新することisTransientGenerateFailure。2026年9月・このテストの実行中に
複数モデルにまたがって頻発することを確認し対応した)client_id を TEST-PERSONA-<ペルソナ>-<実行時刻>
にしている。 admin.html のセッション一覧では先頭8文字(client_id_short)が
TEST-PER と表示されるため、実際の生徒の匿名IDと見た目で区別できるclassify() が crisis を返した場合)は固定応答を会話には残すが、
notifyCrisis() の呼び出しと safety_events への記録は行わない。 合成ペルソナの
発言で実在しない生徒の危機が学校スタッフに誤通知される事態を避けるための、このテスト
スクリプト側だけの判断で、route.ts 本体やDBスキーマは変更していないperson_memory は更新しない(一回きりの合成会話のため)出力は docs/test-results/persona-regression-<実行日時>.json。各ペルソナのターン数・
危機分岐回数・フラグ発生回数・最終的な relation の要約が入る(詳細な発言内容は
admin.html 側で確認する)。
select * from pending_safety;
ここを見る人と時間帯を、公開前に決めてください。 AI が受け止めたのに誰にも届かない、が最悪の結果です。
確認したら:
update safety_events set handled = true, handled_by = '担当者名', handled_at = now()
where seq = 123;
Webhook にはセッションIDと時刻だけを送り、相談の本文は送っていません。 未成年の相談内容を Slack のチャンネルに流すのは避けるべきなので、詳細は DB を見る運用にしています。
心理士の方は、ナレッジ管理画面(/knowledge.html)を使ってください(2026年10月11日。docs/backlog.md 1-1)。
相談ログ レビュー画面(/admin.html)の右上の「ナレッジ管理」からも開けます。合言葉はレビュー画面と同じ ADMIN_TOKEN です。
src/knowledge-admin.mjs。オフラインのテストは npm run test:knowledge-admin)開発者は、Supabase の Table Editor で knowledge テーブルを直接編集することもできます。
Route Handler は60秒キャッシュなので、だいたい1分待てば反映されます。
(Vercelはサーバーレスのため複数インスタンスが同時に動くことがあり、旧Edge Function構成より
反映タイミングにばらつきが出ることがあります。急ぎのときは1〜2分見ておくと安心です)
active を false にknowledge_history に自動で残りますselect * from knowledge_score where used_count > 0 order by avg_rating desc nulls last;
select * from flag_summary;
公開前に、これらを学校と確定させてください。DB に書き始めてから決めると、書いたものが全部問題になります。
messages を閲覧できる人の範囲未成年の心身の状態に関する情報は要配慮個人情報にあたります。
いまはタグ照合で検索しています。ナレッジが 1000 件を超えるまで、ベクトル検索は不要です。
必要になったら:
create extension vector;knowledge に embedding vector(1024) を追加knowledge_body_idx(全文検索)と組み合わせたハイブリッド検索に原則(cat = 'principle')と禁止(cat = 'ng')は検索対象にせず、常に全件をプロンプトに載せ続けてください。
検索に回すと、大事な原則がその回だけ引かれない、という事故が起きます。
--dev-panel=off--missing16=on--constraints=v13--model=freeCRISIS_RESPONSE=staged のときだけ動く(2026年10月11日に本番でも有効にすると人が決めたが、2026年10月14日に、第1部の修正と心理士の確認が済むまで本番では無効に戻した)。
有効にする前に db/schema.sql の11節〜14節を Supabase の SQL エディタで実行する。
npm run test:staged-response(API・DB は使わない。
ペルソナテストの記録の B1・B2・B5 の判定の並びを、新しい規則に通す再生も含む)npm run test:followup(開発用の文 docs/test-sets/crisis-followup-dev.json。--compare-normal を付けると、
比べるために通常の判定にも通す)。危機でなければならない文が段階2にならなかったら、その時点で止まる。
結果はエクセルにも書き出すnpm run test:replay(入力 docs/test-sets/staged-replay-20260926.json。2026年9月26日の再検証の B1・B2・B5。
--personas=B1,B5 で絞れる)npm run test:negation(開発用の文 docs/test-sets/crisis-negation-dev.json。「打ち明け → 1通目」のあとの返事として、
判定の流れ全体に通す)。A 生きたい気持ち / B 冗談・取り消し / C 最小化・引き下がり のときに気持ちのスケーリングのチップを
出すか、念押し・絶望感・新しいサインを否定と取り違えないかを見る。結果はエクセルにも書き出す。
最終判定用の --holdout(保留セット v2 の打ち消し・念押し)は、人が求めるまで使わないCRISIS_AFTERCARE=off で以前の動きに戻るCRISIS_RESPONSE=staged node scripts/test-persona-regression.mjs --stage=crisis2
(B1・B2・B5。相談AI本体は課金キー。心理士さんの確認で文面が決まってから1回だけ回す)docs/crisis-stage2-provisional-texts.md
(文面を変えたら node scripts/export-provisional-texts.mjs で作り直す)docs/test-results/crisis-detection-2-3-summary.txt)。変更の採否を判定するときは複数回実行するsrc/classify.mjs の classify()。本番の src/app/api/chat/route.ts と
完全に同じ関数を使うので、ここで測った数字がそのまま本番の実力になるdocs/test-results/crisis-detection-<実行日時>.json。クラスごとの適合率・再現率・F1、
Geminiの安全フィルターにブロックされた件数(精度とは別枠)、誤判定した発話の一覧に加え、
2026年9月から risk/subject を合成した「Tier A」(risk==="crisis" && subject==="self"。
CLAUDE.md 5.12)軸での Tier A再現率・Tier A見逃し件数(0件が目標)・Tier B→Tier A
過剰検知率 が入る(tier_ab キー)。全件の生の判定結果は all_results キーに入る
(各件に実際に判定したモデルID used_model も含む。model_usage キーでモデルごとの
使用件数を集計できる)[BLOCKED])は再試行せず、そのまま「ブロックされた」件
として記録する(レート制限とは区別する)docs/prompts/structured-verification-suite.md 参照)