Perplexity では、使っていて気持ちのよい製品をつくることを大切にしています。エージェント機能を備えたブラウザ Perplexity Comet と、幅広い仕事をこなせる強力なデジタルワーカー Perplexity Computer では、音声だけですべての機能を使えるようにすることを重視しました。やりたいことを口にして、あとは任せて仕事が進むのを見ていられるのは、ほかではなかなか味わえない心地よさです。私たちが音声インターフェースに期待を寄せるのは、実際のやり取りが少し魔法めいて感じられるからです。
Perplexity が毎月扱う数百万件の音声セッションでこの体験を届けるために、私たちは Realtime-1.5 を本番環境に導入しました。Computer で音声の利用が広がっていく様子には、私たち自身もわくわくしましたし、多くを学びました。ここでは、これまでに得た意外な発見をいくつか紹介します。ぜひ皆さんも Realtime-1.5 を試して、気づいたことを教えてください。
1. コンテキスト管理の方針
コンテキスト管理の良し悪しがよく表れたのが、長尺のコンテンツ、特に内容が濃く、数時間にわたるポッドキャストでした。目指したのは、ポッドキャストの文字起こしを音声で活用できるようにすることです。たとえば、ユーザーがいきなり「開始から 2 時間半のところでは何の話をしていた?」と尋ねても、話の流れを踏まえた答えが返ってくるようにしたいと考えました。
文字起こしの全文はコンテキストに収まりません。そこで最初は、大きな塊に分けて送っていました。ところがすぐに、一度の更新が大きいと、空き容量を超えた時点でそれまでの履歴が丸ごと失われるとわかりました。たとえば、残り 5,000 トークンのコンテキストウィンドウに 10,000 トークンの更新を送ろうとすると、モデルはそれまでの履歴をすべて失います。一度の大きすぎる更新で、少しずつ削られるはずのコンテキストがひとまとまり消えてしまうため、大きな塊を送るのは予想以上に危険でした。
そこで方針を変えました。一度に大きな更新を送るのをやめ、すべてを 2,000 トークンずつの小さな塊に分けて順に送るようにしました。処理の手間は増えますが、動作はずっと安定します。コンテキストの切り詰めが起きても、履歴の一部が削られるだけで済みます。
もう一つ、見落としやすい点に気づきました。コンテキストは内容に応じてモデルへの渡し方を変える必要があります。conversation.item.create でコンテキストを更新する際、item.type: "message" には system、user、assistant の 3 つのロールがあります。ロールによって、モデルはメッセージの性質を判断します。system は指示を与えたり振る舞いを定めたりするために使い、user はエンドユーザーの入力、assistant はモデルが生成した出力に使います。
ロールの割り当てを誤ると、やり取りが不自然になりました。user として渡すコンテキストが多すぎると、ユーザーは資料をもとに質問しただけなのに、モデルはウェブページの抜粋やコメントまで、一つひとつユーザーが読み上げたかのように応答しました。逆に system として渡しすぎると、モデルは、もともと「知っていた」ことと、コンテキストとして与えられた情報と、ユーザーが今まさに尋ねていることを区別できなくなりました。
ページを閲覧する場面がよい例です。ユーザーがスクロールするたびに、私たちは画面に表示されている内容をモデルに伝えています。これをすべてユーザー入力として扱うと、モデルはユーザーが各段落を声に出して読んだものとして振る舞い始めます。これでは実際の状況と合いません。目指しているのは、システムが背後でページの内容を把握していて、ユーザーが何か尋ねたときに自然に答えてくれる体験です。その実現には、コンテキストの量よりも、会話の中でそれぞれの情報が持つ意味を正しく伝えることが重要でした。
2. 製品間での音声処理の統一
Perplexity には Ask、Comet、Computer などの製品があり、それぞれのクライアントは異なる技術スタックで構築されています。Swift、TypeScript、Rust、C++ では、ネイティブの音声バッファの形式も異なる場合があります。当初は各クライアントが生の音声データやそれぞれのネイティブ形式の音声をそのまま Realtime API に送っていたため、性能にばらつきがありました。
最終的には Rust で SDK を開発し、プラットフォームごとの違いを吸収して、すべてのクライアントが同じ仕様で音声を API に送れるようにしました。具体的には、サーバーに届く前に音声波形を処理します。まず、Opus コーデックの推奨サンプリングレートと WebRTC の内部サンプリングレートに合わせて、48 kHz のモノラルにリサンプリングします。次に WebRTC APM でエコーキャンセル、自動ゲイン制御、ノイズ低減、ハイパスフィルター処理を行い、最後に伝送用にエンコードします。SDK の導入により、音声処理に使う定数を統一できました。リサンプリングを含む処理パイプライン全体も、クライアントごとに構成する手間が省け、SDK に集約できました。
3. 雑音や間を想定した調整
音声区間検出(VAD)は、ユーザーが実際に使う環境で調整することが重要です。実際に使われるマイクやスピーカーの音量、周囲の騒音に合わせて調整します。社内テストでは、サンフランシスコの騒がしいバーも試験環境に選びました。そこで製品を試す場面がいかにもありそうだったからです。誰かが「Perplexity の新しいアプリ、もう試した?」と言い、友人がスマートフォンを取り出します。そこで音声がうまく動かなければ、一度に 2 人のユーザーを失います。うまく動けば、「うわ、マジかよ!」という反応が返ってきます。この場面を想定したことで、どこまで調整すべきかが明確になりました。理想的な条件でうまく動くものも、実環境ではしばしば通用しません。最初から条件の悪い環境で調整する方がよいのです。
音声 UX で特に難しかったのは、発話の途中に生じる間の扱いでした。人は、考えたり、画面に資料を表示したり、何かを読み上げる準備をしたりするとき、自然に言葉を止めます。モデルはその間を発話の終わりと判断しやすく、早すぎるタイミングで口を挟んでしまいます。たとえば、ユーザーが数式の導出について質問しようとしたときです。式を探すために少し黙っただけで、質問を言い終える前にモデルが話し始めてしまいました。そこで生まれたのが音声ロックです。従来のプッシュ・トゥ・トーク方式では、音声は既定でオフになっており、ユーザーはボタンを押して話します。私たちはその操作を逆にしました。普段はボタン操作なしで自然に会話でき、自分の話がまだ終わっていないときだけ、音声をロックして発言を続けられます。この仕組みは、単発の機能にとどまらないと考えています。音声インターフェースがより複雑なワークフローで使われるようになれば、何らかの形でこのようなやり取りが標準になるでしょう。
4. 主要ツールへの絞り込みと学習時のデータ分布に沿った設計
ツールは、特に重要なものだけに絞ります。私たちの場合は 10 個未満でした。ユーザーにとって価値の高い操作をカバーできる、少数の主要ツールに集中しました。この割り切りは妥当でしたし、今後のモデルのスナップショットで性能が向上すれば、この構成でさらによい結果が得られると考えています。
システムプロンプトには、各ツールをいつ、どのように呼び出すかを明記しました。また、ツールのスキーマと出力の両方が、モデルの学習時のデータ分布から外れないよう注意しました。具体的には、ツールの出力をアシスタントのセリフのように書くのではなく、一般的な構造化ツールデータの形式にしました。読み上げる内容に指示を書き込むのをやめ、フィールドを明確に分けた構造化 JSON を返すようにしました。たとえば、ユーザーに読み上げる文は response_text に入れ、振る舞いは require_repeat_verbatim のようなフラグで指定します。これでツールの利用が安定し、やり取りの形式もモデルが学習中に目にしたと考えられるものに近づきました。
// Good:
{
"response_text": "I kicked-off the task to create a market research dashboard",
"require_repeat_verbatim": true
}
// Avoid:
I kicked-off the task to create a market research dashboard
# Response Instructions
Read the above instructions EXACTLY as they are
実用段階に入った Realtime
Realtime-1.5 は、業界にとっての転換点です。長いコンテキストの扱い、より多くのツールの活用、高度な判断を要するタスクへの対応には、確かに改善の余地があります。それでも、モデルの性能は今後さらに向上すると私たちは見込んでいます。Perplexity では、その未来を見据えて今から開発を進めています。今のシステムを使いやすくしながら、次世代の Realtime モデルと、それによって実現する新しい音声体験に備えたいと考えています。