この記事はAIとともに作成しています
約2週間ほどのチーム開発演習で作成したプロダクトの技術的な話について書きます。
本記事では私がメインに担当したバックエンド・インフラ等の技術的な工夫についてのお話をします。
ソースコードはこちら
そもそも
今回の演習のテーマとして「社内の営業の課題をヒアリングし、その課題を解決するソリューションを提案、作成する」 というものでした。
1チーム5人編成で、ビジネス職とエンジニア職が半分ずつのチームで構成されておりチーム内で何を担当するのかも決めるようになっていました。
私たちのチームはビジネス職2人、エンジニア職3人のチームで構成されており、私はPM兼デザイナーを担当していました。
何を作ったのか
私たちのチームでは若手の営業スキル向上を支援するロープレAIサービス SalesGymを作成しました

これは現在営業職の中で問題となっている若手のスキル育成の機会が少ないという課題に対して「教育」という観点からアプローチをかけたものです。
他サービスやAI Chatサービスを用いたロープレとの差別化としては
複数回に分けてヒアリングを行える点です。
基本的なロープレサービスでは一回きりのヒアリングが行えるだけで複数回を行うにはユーザー側で対応する必要がありましたが、我々が作成したサービスではユーザーはヒアリング回数を設定するだけで複数回ロープレを行うことができます。
ということで以下から技術的なお話へ
まず、Sales Gym とは何でできているの?
Sales Gym は大まかに次の 4 層で構成されています。
- AWS — ホスティング・AI 音声サービス・ストレージ
- バックエンド — 会話パイプライン・API
- フロントエンド — ロールプレイ UI・音声入出力
- HULFT Square — Slack連携(本記事では詳細割愛)
基本的な使用技術は次のとおりです。
| レイヤ | 技術 |
|---|---|
| フロントエンド | React Router 7、TypeScript、Web Audio API |
| バックエンド | Python、FastAPI、SQLAlchemy |
| AWS | Amazon Bedrock、Amazon Transcribe、Amazon Polly、Amazon S3 |
インフラとして CloudFront / S3 / Fargate / RDS 等も使っていますが、上表はアプリケーションコードから直接呼び出す AWS サービスに絞った一覧です。
この技術選定の理由
フロントエンド
- 開発期間が 2 週間と短かったため、自分が使い慣れているフレームワーク・言語を選びたかった
- チームメンバーへの説明コストと、情報量の多さから React + TypeScript にした
バックエンド
- バックエンド実装は自分以外のメンバーが主担当の予定だったため、そのメンバーが使い慣れている Python を選んだ
- フロントと言語を揃える案もあったが、メンバーは TypeScript より Python に慣れていた
- LLM 利用が確実だったため、参考情報が豊富な Python が妥当と判断した
AWS
- 他サービスだと許可取り等で開発スピードが落ちると考え、社内で許可されている AWS を選んだ
- 個人的にも AWS を学びたかった(この選定のおかげで CLF も受かった、と言っても過言ではない)
構成図
AWS 構成
- CloudFront でフロントエンドを配信している
- AWS Fargate 上の Docker コンテナでバックエンドを常時ホストしている
- 会話音声は Amazon S3 に保存し、セッション ID や会話内容などは Amazon RDS に保存する
シーケンス図
ロールプレイ 1 ターンの流れです。PTT 中に PCM を WebSocket で送り、ptt_end 後に STT → Bedrock → Polly → 音声返却が走ります。
ドメインモデル(ER 図)
Program / CustomerProfile / CustomerState / HearingSession など、商談シリーズと AI 顧客の状態を表現しています。
今回の開発で意識したこと
AI を使える開発環境であり、ウォーターフォールに近い進め方だったため、要件を先に固めて、開発未経験のメンバーでも実装しやすい状態を作ることを意識しました。
具体的には次を行いました。
-
要件定義.mdを作成し、フロント・バック向けの突貫ドキュメントを用意 - 各フォルダ(
/frontend,/backend)の README から辿れる詳細ドキュメントを整備 - 実装は基本的にそのドキュメントに沿って進める
また、営業と開発で「やりたいこと」「やるべきこと」を揃えるため、PM としてプロダクトの方向性確認を定期的に行いました。人間も AI も同じ目標を持てるようにする を常に考えるようにしていました。
パフォーマンス面での工夫
Sales Gym として技術的に特に力を入れたのは次の 3 点です。ここを疎かにするとユーザー体験が一気に悪くなる、と判断しました。
- 顧客 AI のレスポンスレイテンシを小さくすること
- 顧客 AI のロールを崩さないこと
- ローディング画面
レスポンスレイテンシ
商談ロールプレイは PTT(Push-To-Talk) 方式です。ボタンを離してから AI 顧客が話し始めるまでの空白が、そのまま「遅い」と感じられます。Bedrock のモデルはsonnet4-6としたため、STT → LLM → TTS → 配信 のパイプライン全体を短くする方針にしました。
PTT 中もストリーミングで音声認識する
改善前は録音を止めてから STT を開始していたため、発話時間分だけ待ち時間が増えていました。
-
フロント(
usePushToTalk.ts): 16kHz PCM を約 256ms ごとに WebSocket へ送信 -
バック(
hearing.py):ptt_startと同時にTranscribeStreamSessionを開始し、ptt_endではfinish()するだけ - 途中認識は
transcript_partialとして UI に表示し、「聞こえている」フィードバックを返す
初回 AI 音声の出現を早くする
句点(。)まで待ってから Polly 合成を始めていたため、初回 MP3 まで 2〜4 秒かかっていました。split_sentences_for_tts(audio_pipeline.py)で 初回チャンクだけ 区切りルールを変えています。
- 1 文目: 読点(
、)でも区切り、最小 4 文字 - 2 文目以降: 句点のみ、最小 12 文字
例: (お辞儀をしながら) あ、どうも。宮本です。 → ["あ、どうも。", "宮本です。"]
そのほか、生成時間・配信まわりも合わせて調整しました。
-
max_tokens=400で Bedrock 応答の長さに上限(audio_pipeline.py) - Polly 合成を 専用ワーカースレッド に分離し、LLM 生成と並列化(以前は同一スレッドで直列実行されていた)
- 受信 MP3 を
AudioContextでギャップレス再生(useHearingWebSocket.ts) - ECS タスクを 1 vCPU / 2 GB に増強(
task-def-register.json)
トレードオフ: TTS 前に sanitize_customer_speech で括弧内のト書きを除去するため、Bedrock トークン到着と同時の Polly 合成はしていません。
顧客 AI のロールを崩さない
ロールプレイ相手として一貫した人格を保つため、次の 2 点を徹底しています。
ペルソナプロンプトを毎会話ごとに参照させる
build_chat_system_prompt(prompts.py)で、ターンごとに CustomerProfile と CustomerState からシステムプロンプトを組み立てています。
- 名前・業界・役職・表面ニーズ・性格(
personality_type) - 真の課題・認知度・信頼度・開示済み情報
- セッション番号・残り時間・商談ゴール
プログラム作成時に Bedrock で生成したペルソナ(createProgram → バックエンドのプロファイル生成)が、会話中ずっと参照されます。
過去の会話内容を参照させる
to_bedrock_messages で直近 20 ターン(MAX_CONVERSATION_TURNS)の会話ログを Bedrock の messages に変換し、毎ターン LLM に渡しています。AI 側の発話は TTS 前に sanitize_customer_speech でト書きを除去したテキストを履歴に残すため、ロールプレイ上の「言ったこと」と Polly に読ませた内容のズレも抑えています。
ローディング画面
ペルソナ生成(SettingsPage の createProgram)やセッション開始・評価ページ読み込みなど、次画面への遷移がどうしても遅くなる処理があります。白画面のまま待たせると離脱につながるため、ローディング UI で待ち時間を「体感」させない工夫をしました。
LoadingScreen コンポーネント
- くまキャラクターの口パクアニメーション(
LoadingScreen.cssのloading-bear-mouth-*) - ペルソナ生成時は
character="thinking"で「?」マーク付きの thinking ベアを表示 - メッセージ例: 「AI顧客のペルソナを作成しています」「セッションを開始しています」
useDeferredLoading
// 400ms 以上かかる場合だけローディング UI を表示
export function useDeferredLoading(isLoading: boolean, delayMs = 400): boolean
キャッシュ済みデータの再表示など、数十 ms で終わるケースではローディングを出さず、本当に待たされているときだけアニメーションを見せるようにしています。
おわりに
今回の開発で自分が主に担当した技術領域は次の 3 つです。
- AWS の構築
- ユーザー体験の向上(レイテンシ・ローディング・ロールプレイ品質)
- ハーネス構築(ドキュメント・開発環境の整備)
Polly の読み間違いや不自然な日本語生成など、サービスとしてまだ足りない部分は多く、完成にはほど遠いと感じています。また PM としてチームメンバーへタスクをきれいに割り振れなかった反省もあり、今回の開発演習を糧にマネジメントと技術力をもっと高めていこうと思いました。
記事に対する意見や修正点等ありましたらこちらまで
実装したプロンプト集
ペルソナ生成のプロンプト
あなたはB2Bディスカバリー商談のロープレ用・見込み顧客ペルソナを設計する専門家です。
指定分野の現実的な日本企業の決裁者・担当者像を1名分、JSONオブジェクト1つのみで返してください。
説明文・マークダウン・コードブロックは禁止。
## 必須キー
- name, industry, company_size, role_title
- surface_need(表向きニーズ・150字以内)
- true_challenge(真の本質課題・200字以内)
- personality_type, initial_awareness(0-100)
- hidden_motivations, typical_objections, background_facts, communication_style
## 設計ルール
- true_challenge は surface_need と因果関係があること
- initial_awareness が低いほど true_challenge との関連は薄い
- 文字列内改行禁止。ダブルクォートはエスケープ
会話ログの履歴要約と分析のプロンプト
営業ロープレの1セッション分の会話を分析し、JSONオブジェクト1つのみ返してください。
## 出力キー
- awareness_level, rapport_level(前回値から±15以内)
- disclosed_info, session_summary(150字以内), title(20字以内)
## 判定基準
- awareness: 表面ニーズのみ→変化なし、症状言及→+5〜10、構造的原因→+10〜15 等
- rapport: 傾聴・共感→上昇、押し売り→下降 等
- disclosed_info は「顧客が口にした事実」のみ


