前提
私たちが運用しているあるクリニックの導入事例では、メッセージングチャネル上で患者対応を行っています。患者は自然言語で書き込みます——予約変更の依頼、施術に関する質問、「代わりに木曜の午後に行けますか」——そしてAIアシスタントが会話を担当します。クリニックのスケジュールは、クリニックが昔から使っている場所にそのままあります。受付が管理する共有スプレッドシートで、枠の表記ルールはスタッフが長年使い続けてきた慣習です。
すぐに思いつく作り方は、モデルにスケジュールを読ませて、空いて見える枠を自由に予約させることです。私たちはそれを作りませんでした。その理由こそが、この記事の価値のある部分です。
ルールその1:何が予約可能かをモデルは決めない
クリニックのスケジュールは、ポリシーを慣習の中に埋め込んでいます。特定の枠は再診とカウンセリング専用に確保されていて、それは視覚的なマークで示されているだけで、データベースのラベルではありません。新規患者の施術をそこに入れることは、決してあってはなりません。受付はそれを知っています。しかしグリッドを読む言語モデルは知りません——モデルには空き枠にしか見えないのです。
そこで最初にリリースしたのは、予約機能ではありませんでした。ハードなゲートです。確保枠は、アシスタントにとって構造的に立ち入り禁止。「避けるように指示する」のではなく——立ち入り禁止として、モデルの後、スケジュールに何かが触れる前に実行されるコードで強制します。プロンプトは言いくるめられますが、純粋関数は言いくるめられません。
アーキテクチャを一文で言えばこうです。LLMが抽出し、決定論的なコードが決定する。 モデルは「来週の木曜、3時以降がいいです。施術は前回と同じで」を読み取り、構造化された意図——希望時間帯、施術の種類、患者の特定——を出力することに長けています。その先はすべて、平易でテスト可能なロジックです。枠の分類、適格性ルール、所要時間の計算——データに対する純粋関数として書かれ、ユニットテストが付き、他のコードと同じようにレビューされます。
地味なディテールが安全を支える
エンジニアリング工数の大半は、デモでは決して映らないディテールに費やされました。
- 施術の所要時間はクリニック自身の対照表から取る。 予約可能なすべての施術に準備時間と施術時間があり、クリニックの紙の対照表と一行ずつ突き合わせました——その過程で19件の不一致を見つけて修正したパスも含みます。アシスタントは、実際の所要時間が収まらない枠を提示できません。
- 来院時刻は導出する。推測しない。 準備時間が患者の到着すべき時刻を決め、確認メッセージにそれを明記します。モデルの判断がひとつ減り、受付の不意打ちがひとつ減ります。
- 電話番号はテキストとして書き込む。 スプレッドシートは、数値として保存された電話番号の先頭のゼロを平気で食べてしまいます。小さなバグ、現実世界での影響、決定論的な修正。
- 曖昧さは人間にルーティングする。 リクエストがゲートをきれいに通らないとき——珍しい施術の組み合わせ、ルールでは解決できない枠の競合、ポリシー表がカバーしていない要望——アシスタントは即興で対応しません。コンテキストを添えて、会話を人間に引き継ぎます。この引き継ぎは機能であって、障害モードではありません。
このリストの判断ルールはどれも、クリニックの運営者が判断を下し、私たちがその判断をコードに落とし、そのコードがいまや検証可能になっている、という経緯で存在しています。運営者がポリシーを変えたら、私たちは関数とそのテストを変えます——プロンプトを変えて祈るのではなく。
なぜこのやり方なのか
このプロジェクトには、週末ひとつで出荷できるバージョンもあり得ました。有能なモデルにスケジュールとシステムプロンプトを与えて、予約させる。デモは見事に決まり、ほとんどの場合はうまく動くでしょう。問題は「ほとんどの場合」です。クリニックのスケジュールは、実在の患者と実在のスタッフへの約束です。失敗のかたちは、ダブルブッキングされた処置室と、2度目のトラブルの後にツールを信用しなくなった受付です。
私たちが採用した分割——理解はモデル、権限は決定論的なゲート、ゲートの外はすべて人間への引き継ぎ——は、先行するエンジニアリングコストが高くつく代わりに、プロンプトには返せないものを返してくれます。予約ロジックは、患者に触れる前にテストでき、事後に監査でき、ポリシーが変わったときには正確に一箇所だけ直せばよいのです。
このパターンはクリニックに限った話ではありません。AIアシスタントが、他の人々が依存しているリソース——カレンダー、在庫、台帳——に触れる場面ならどこでも、同じ線が引けます。モデルに読ませること。しかし、支配させないこと。