【技術ブログ】Realtime APIでAIが応答する電話窓口を実現。
こんにちは、システム部開発ユニットの渡辺です。
今回のメインテーマはopenAI社のRealtimeAPIです。Realtime APIが世界に公開されたのがつい先月、2024年10月1日だということもあり、今まさに使用方法が模索されているホットな話題ではないでしょうか。特に日本語の記事はまだ少ないと思うので、今回の勉強会が記事を読んでいただいた方々の何かのお役に立てたら喜ばしい限りです。
※上述の通り本記事は2024年11月に執筆しております。特に加筆する予定はありませんので時間経過と共に情報が古くなることご了承ください。特に生成AIの分野は情報の更新が早いため、公式ドキュメントを併せてご覧になることを推奨いたします。
▼前回の記事
目次
・はじめに
・Realtime APIに電話をかけよう
・AIにサービスに関する質問をして回答してもらおう(Realtime API ✖️ RAG)
・AIに取り次ぎを頼んで転送してもらおう(Realtime API ✖️ Twilio REST API)
・AIの対応履歴を管理画面から確認しよう。(Realtime API ✖️ Completions API)
・最後に
はじめに
まずRealtime APIの何が良いのか、簡単に説明させていただきます。
従来のAPIで電話窓口の様な機能を実装しようと考えた場合、都度HTTPリクエストを送る必要があったため非常に処理の手順が多くなったり、文字起こしが完結するまで待ってから音声読み上げを行うことによる大きな遅延が発生したり、とスムーズな電話応答の実現は非常に難度の高いものでした。
しかしWebSocketを通じて連続的なデータストリームをサポートするインターフェースが発表されたことでHTTPリクエストは1度で済ませることもできるようになり、ユーザーの発話のリアルタイム送信と即座のフィードバックをシンプルなアーキテクチャで実装することが可能となりました。
それではそんなAIとの音声対話を大きく前進させたRealtime APIを使って、早速AIが応答するサポートセンター作っていきましょう。楽しみですね!
※イメージし易いよう、AIと通話した際の音声データを載せておきます。
実際にデモを行った際の音声
必要なものを準備しよう
具体的な処理に入っていく前に、話を進める上で最低限必要なものは準備しておきましょう。
①OpenAIのアカウントとAPI KEY
Realtime APIはOpen AIの提供するAPIの一つですので、アカウントとAPI KEYが必要になります。ここに関しては既に様々な記事あると思いますので割愛させていただきます。APIは使った分だけ課金されますし、API KEYがあれば自由に使えてしまうので、KEYは決して公開しないようにしましょう。
②着信をwebsocketに接続できる電話番号(今回はTwilioで取得)
電話番号に関しては今回もTwilioを利用させていただきました。似たサービスがあればTwilioでなくても大丈夫だと思いますが、公式がフロントエンドパターンのデモアプリケーション例としていくつか提携パートナーを挙げているものの一つにTwilioがあるため少し心強いですね。Twilioの電話番号取得方法も記事はたくさんあると思いますので割愛させていただきます。利用できる方はコンソールからポチポチ購入しましょう。
Realtime APIに電話をかけよう
まずは購入した電話番号に電話をかけるとAIが応答してくれる、というベースになる部分を実装していきましょう。
購入した番号への着信をwebsocketに接続しよう
websocketに接続するためにTwiliio側の処理を書いていきましょう。
※websocketとはそもそも何ぞや、と思う方いらっしゃるかもしれませんが「リアルタイム、双方向の通信セッションを確立するために接続するもの」くらいの理解でとりあえず使えればOKかと思います。(自分もそう思いましたし、その程度の理解です。)標準規格となるAPIドキュメントなんかもあるようなので、詳しく調べたい場合そういったものを読み込んでいくのが良いかなと思います。
Twilioが公開しているデモアプリをベース開発したので、下記にデモアプリのリンクを記載させていただきます。とりあえず繋がれば満足、ということであれば下のリンクの通りやれば十分だと思います。
https://www.twilio.com/en-us/blog/voice-ai-assistant-openai-realtime-api-node
[ちょっと脱線]
Twilioデモアプリ以外にもYoutubeなどに上がっている海外の方々の動画を大いに参考にさせていただきました。自分はなるべくお金かけたくなかったので自PC完結させていますが、これから少し触れるngrokやdockerにこだわる必要は無いです。簡単にデプロイ可能なサービス(Replitなど)を使ってらっしゃる方が多かったです。多少お金をかけてよければ都度公開する手間もなくなりますし、そういったサービス使用された方がやり易いかと思います。
[脱線終了]
さて、デモアプリにもあるように購入した番号のwebhookのURLを指定しましょう。最初はテスト環境のEC2にしようかと思っていたのですが、最新の機能だけあって、既存の環境だと色々とバージョンが足りていなかったので自PCに環境を用意して進めました。
少し面倒ですが、自PC+無料の一時ドメイン、で検証したい方はngrok等で一時的にインターネットから自PCにアクセスできるドメインを取得し、webhookURLをそのドメインに設定しましょう。自由に使えるデプロイ環境がある方は普通にデプロイしてそちらを向けた方が手間は少ないかと思います。
Twilioの公式デモではngrokを使っているためそちらの画像を下に引用いたします。
という訳で番号への着信をngrokで公開した自PCに向けました。リクエストを捌く必要があるのでnode.jsをインストールして必要なパッケージをnpmコマンドで入れています。
今回はこのあたりをimportしています。
import Fastify from 'fastify';
import WebSocket from 'ws';
import dotenv from 'dotenv';
import fastifyFormBody from '@fastify/formbody';
import fastifyWs from '@fastify/websocket';
import fetch from 'node-fetch';
(node.jsをよく使われる方にとっては馴染み深いパッケージだと思いますので説明は割愛します。)
最初のリクエストは(公開したドメイン)/incoming-callで受け、request.bodyから必要なTwilioのパラメタを取得したらwebsocketに繋げるためのTwiMLを返却します。TwiMLについては詳細は割愛させていただきますが、Twilioサービスに指示を出すためのXMLベースのマークアップ言語です。今回はこの辺りのTwiMLを使用します。↓
https://www.twilio.com/docs/voice/twiml/connect
Streamタグのurlにwebsocketの接続用のパスを用意しましょう。パスは以下のように指定します。↓
wss://(公開したドメイン)/media-stream
また、TwiMLを返却する際に、後々の処理に使用する値(電話番号や通話idなど使いたいもの)を、カスタムパラメタとして渡しておきます。
ConnetctとStreamタグを使って任意のurlへwebsocket接続のリクエストができたら、fastifyで { websocket: true }をつけてリクエストを捌きましょう。これでTwilio側からの接続はOKです。
Realtime API側もwebsocketに接続しよう
何はともあれ公式ドキュメントということでリンクを貼ってきます。以降話を進める上でサンプルコードもあった方が良いと思うので公式のものペタペタ引用していきます。今回の成果物の導入部分は最後に貼らせていただこうと思いますので、そちらと併せるとイメージし易いかと思います。
https://platform.openai.com/docs/guides/realtime
ドキュメントにもある通りRealtime APIはサーバーサイドのウェブソケットインターフェースです。node.jsで接続するためにwebsocket用のインスタンスを作成しましょう。
import WebSocket from "ws";
const url = "wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview-2024-10-01";
const ws = new WebSocket(url, {
headers: {
"Authorization": "Bearer " + process.env.OPENAI_API_KEY,
"OpenAI-Beta": "realtime=v1",
},
});
接続が開いたらセッションの設定を更新します。
const event = {
type: 'session.update',
session: {
// other session configuration fields
tools: [
{
name: 'get_weather',
description: 'Get the current weather',
parameters: {
type: 'object',
properties: {
location: { type: 'string' }
}
}
}
]
}
};
ws.send(JSON.stringify(event));
node.jsでwebsocketに関する操作をする場合、基本的にopenAiWs.send()にtypeを指定したjson形式のオブジェクトを渡すことで行うイメージのようですね。セッションの更新なのでtypeはsession.updateです。ここでtoolsに後でfuction callingする関数を定義しておきましょう。fuction callingについては後述します。
会話のはじめ方の例です。ユーザーから"Hello"と挨拶があったことにします。conversation.itemに加える感じです。正確に何と呼ぶべきなのかは議論の余地があると思いますが、説明のし易さからこの手のAIに与える情報は以降の説明ではコンテキストと呼ばせていただこうと思います。
const event = {
type: 'conversation.item.create',
item: {
type: 'message',
role: 'user',
content: [
{
type: 'input_text',
text: 'Hello!'
}
]
}
};
ws.send(JSON.stringify(event));
ws.send(JSON.stringify({type: 'response.create'}));
まずコンテキストとしてユーザーからの挨拶があったことにします、その上で'response.create'するので生成される回答はユーザーからの挨拶に返答するものになります。
今回の成果物では、(ちょっとベストプラクティスではないのかもしれませんが、)最初にユーザーから渡したメッセージをそのまま読み上げるように指示することでAI側の第一声を制御しています。ユーザーが最初に日本語で指示することで以降のやり取りを日本語で行ってくれる可能性を高めたいという期待も少しあります。
接続を開始した後はTwilioとOpenAIのそれぞれのwebsocketから情報の入力などがあるたびにお互いに情報を送り合ったり、特定の条件が揃った際にはバックエンドにfucnction callingするなどしています。
// TwilioからのmessageをOpenAIに送信
connection.on('message', (message) => {
// オープンAIから受信したデータをハンドリング
openAiWs.on('message', async (data) => {
connection.がTwilio側、openAiWsがもちろんOpenAi側ですね。何かデータ入力があるとmessageが発火するのでずっと発火しっぱなしくらいのイメージですね。
ここまででWebsocketの接続と、イベントを感知して処理を行うことはできていますので、次章以降では特定の条件が揃った際に関数を呼び出すことで、顧客からの質問に対してAIが自社サービス情報を参照した上で回答を生成できるようにしていきましょう。
AIに自社サービスに関する質問をして回答してもらおう(Realtime API ✖️ RAG)
RealtimeAPIに回答してもらうだけだと、他のモデルと同様一般的な回答しかできないため、特定のサービスのサポートとして活用するには無理があります。
そのためRAGのような仕組みを使って生成AIの学習していない自社サービスの知識などを与えてあげる必要があります。今回は自社サービスのQ&Aくらいできれば良かったので関数を呼ぶ際に質問の分野をパラメタとして与えることで参照する文章をある程度絞り込む程度のことしかやっていません。
(はい、RAG使いましたって言いたかっただけです。また、転送や要約の処理に関しても裏でやってることは今回は非常にシンプルなことですので、何をやったかに触れる程度で具体的なコードは割愛させていただこうと思います。Realtime API中心に話を進めていきます。)
今回はそもそもその読み込ませるQ&Aの文章も古いものが混ざっていたり、AIに読ませるには適切でない形式でなかったりしたので、参照する文章もデモ用に自分で作成しました。そのため作成や精度の検証に時間がかかる割にわざわざベクトルデータベース化することによって大きくパフォーマンス改善されることもないかと思い、準備期間の都合上そこの手間は省かせていただきました。
とはいえ、このRAGの部分でUXが大きく変わるのは間違いないと思いますので、少しだけ脱線してRAGについて触れておきます。
ちょっと脱線(一般的なRAGの作り方について)
1から作る場合下記の様な流れで作られることが多いようです。
①読み込ませる文章を用意
綺麗な形のテキストがあればそのままチャンク化に進んで良いと思います。自分が参考にした動画ではwebページをスクレイピングして文章を用意している方などいらっしゃいましたが、PDFからの抜き取りだったり、用意の方法は様々だと思います。
②オーバーラップを意識しながらチャンク化
データをある程度の意味の塊に分けます(チャンク化)。どの程度の区切りにするか(例:512トークン,500単語など)や、区切った際に重要な情報がチャンクの境界で切り捨てられないよう、隣り合うチャンクが一部重なるようにするオーバーラップをどのくらいにするか(例:25%程度)、などを意識しながら行います。
③チャンクをベクトル化してデータベースに保存
チャンク化したデータを多次元空間に埋め込みます(embedding)。埋め込みのイメージがわかり易い様に2次元の座標に、それぞれの単語が埋め込まれていて距離が視覚的にわかり易くなっている図をよく目にしますが、実際には1536次元の空間などに埋め込んでいようです。そんな次元数想像つきませんが、幸いなことに何も考えずともAPIを叩けばやってくれますので、公式のドキュメントのリンクを貼っておきます。↓
https://platform.openai.com/docs/guides/embeddings
上記URLのドキュメントからテキストをベクトル化するcurlのリクエストとレスポンスを抜粋しておきます。
リクエスト
curl https://api.openai.com/v1/embeddings \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"input": "The food was delicious and the waiter...",
"model": "text-embedding-ada-002",
"encoding_format": "float"
}'
レスポンス
{
"object": "list",
"data": [
{
"object": "embedding",
"embedding": [
0.0023064255,
-0.009327292,
.... (1536 floats total for ada-002)
-0.0028842222,
],
"index": 0
}
],
"model": "text-embedding-ada-002",
"usage": {
"prompt_tokens": 8,
"total_tokens": 8
}
}
ちなみに書いてある通りですが、実際に叩いてみるとada-002だとしっかり1536個の浮動小数点の形でベクトル化されます。これでベクトルデータベースが作成できますね。
④検索ワードをベクトル化
さて、そんな人間が全く意味がわからないベクトル化されたデータですが、検索ワードもこれに変換して類似度を求めます。ベクトル同士の類似度はコサイン類似度で算出するのが一般的、らしいですが、この辺りの話になるともう使えればいいや、の域になりますね。コサイン類似度を計算してくれるモジュールなんかがある様なので、その辺り活用して類似度算出しましょう。
(後述するLangChain等のフレームワークのお作法に従うのが現実的なので、あまりここで難しく考える必要はないと思います。)
⑤類似度の高い候補を(いくつか)取得して回答生成
回答が1つだと大きく外れてしまうことも多いため、類似度の高いものを複数コンテキストに与えてから回答生成するのが一般的なようです。この辺りもどのくらいの候補を与えるかで精度が変わってきそうですね。
⑥LangChainやLlamaIndexについて
はい、ここまでどんな流れでRAGが作られるか基本的な例を見た訳ですが、自分でこの工程行うのは面倒ですよね。実際に製品を作る際には前々回の勉強会で植木さんが言及されていたLangChainやLlamaIndexを使うのが現実的だと思うので、基本だけ理解したらそれらのフレームワークの使い方を覚える方が時間効率が良いと思います。今回はその辺り深掘りしませんが、LangChain辺りは遅かれ早かれ勉強会のテーマになりそうだなと思っています。
fuction callingの結果を使って回答を生成しよう
須田さんが出たばかりのfuction callingを使用してWorksでAIアプリ自動生成を実装された勉強会が記憶に新しいですが、今回自分もfuction callingに触れる機会を得ることができ、その面白さを知ることができました。
先ほどセッションの設定を行った際に関数の配列をtoolsの中に入れて渡しています。関数名や関数についての説明、パラメタと必須なものがどれかなどを伝えています。
これだけでAIが必要な引数を自動で取得してくれて、関数を呼ぶ準備が整ったらどの関数を呼べるようになったのか教えてくれるので、引数を取得して実際に関数を呼ぶ、といったフローが可能になります。
今回渡したtoolsの内容を貼っておきます。
tools: [
{
type: "function",
name: "question_and_answer",
description: "Get answers to customer questions about a telephone app service called SUBLINE",
parameters: {
type: "object",
properties: {
"question": { "type": "string" },
"question_type" : { "type": "string" },
"customer_name": { "type": "string" }
},
required: ["question","customer_name","question_type"]
}
},
{
type: "function",
name: "transfer_number",
description: "Get the number which the current call should be transfered to",
parameters: {
type: "object",
properties: {
"target_name": { "type": "string" },
"customer_name": { "type": "string" },
},
required: ["customer_name","target_name"]
}
}
]
顧客名(customer_name),質問内容(question),質問の種類(question_type)といったパラメタを必要とする(question_and_answer)という関数があることを伝えています。
関数の呼び出し準備が完了(response.function_call_arguments.done)したらargsからパラメタを受け取れるので必要なパラメタを取得し、固定で必要なパラメタを加えた上で処理をするサーバーにリクエストを投げます。
今回はいつもローカルで開発しているdocker環境があったので、そちらにリクエストを投げて参照する文章を返却するようにしました。かえってきた文章をコンテキストに加え、その文章を参考にして質問(question)に応えるようAIに指示をしながら回答生成を指示することで自社に関するドキュメントを参照しながらAIが回答を生成してくれます。
AIに取り次ぎを頼んで転送してもらおう(Realtime API ✖️ Twilio REST API)
転送の時も基本的にやってることは同じです。必要なパラメタが揃ったら関数を呼び、必要なパラメタを処理用のサーバーにリクエストしましょう。
バックエンドで取り次ぎ先があるか確認して転送しよう
バックエンドで取り次ぎ先があるかはパラメタで指定された宛先がDBに登録されているかどうかで行っています。先述したように宛先が存在すれば通話を転送します。
バックエンドでは前回の勉強会「SUBLINEに保留転送機能をつけてみた」の際にも使用した、TwilioのCall Resourceを操作するAPIで転送を指示しています。
curl -X POST "https://api.twilio.com/2010-04-01/Accounts/$TWILIO_ACCOUNT_SID/Calls/CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.json" \
--data-urlencode "Twiml=(Response)(Say)Ahoy there(Say)(/Response)" \
-u $TWILIO_ACCOUNT_SID:$TWILIO_AUTH_TOKEN
もし宛先がDBに登録されていなければ転送できなかったこと、転送可能な宛先の候補の一覧を伝えるようAIに指示、回答生成しています。
AIの対応履歴を管理画面から確認しよう。(Realtime API ✖️ Completions API)
さて、AIに対応してもらうのは良いですが、(結構無茶苦茶する時もあるので笑)どのような対応をしたのかは確認できた方が良いですよね。今回は顧客とAIのやり取りを文字で記録しておき、後で管理画面から全文や要約を確認できるようにしています。
やり取りを文字に起こして記録しておこう
入力音声の文字起こし設定はデフォルトではオフに設定されているため文字起こしをするモデルをセッションの設定更新時に指定しておきましょう。※2024年11月現在では指定できるのはwhisper-1だけのようです。
非同期でwhisper-1が文字起こしてしてくれています。AI側の音声の文字起こしは(response.done)のタイミングで追加していきましょう。ユーザー側の文字起こしは(conversation.item.input_audio_transcription.completed)のタイミングで追加していってます。
const sessions = new Map();
で定義したsessionsに通話idをkeyとして記録・追加していきました。
通話が終了したり転送されたりしてwebsocketの接続が閉じた際に最終的にsessionsに記録されているテキストの全文を処理用のサーバーに投げてDBに記録しています。
やり取りの要約もAIにやってもらおう
さて、やり取りの全文確認するのは面倒なケースも多いです。ざっくりどんなやり取りだった分かるように、DBに保存する際には要約のリクエストも別途APIリクエストしておきましょう。といってもこれは普通にAIに要約を頼めば良いのでcompletions APIを使用しています。
completions APIは以前からあるチャット補完APIなので文献も多いと思いますし、公式ドキュメントのリンクを貼るに留めます。要件にあったモデルやトークン数など指定して好きなようにリクエストを投げましょう。
https://platform.openai.com/docs/api-reference/completions
ちなみに今回はcurlでリクエストし、モデルも4oを使う必要もなかったので"gpt-3.5-turbo"を使いました。ご自身の環境や要件に適したものをお使いください。
管理画面でやり取りの全文や要約を確認しよう
さて、必要なデータは保存済みですので、管理画面で軽く整形して表示してみましょう。下記画像の様に調整してみました。
コードの一部
今回の成果物のnode.js部分のコードの一部です。(セキュリティ上の理由から一部の抜粋とさせていただきました。また実際の値と違う部分ありますのでご了承ください。)
// Import required modules
import Fastify from 'fastify';
import WebSocket from 'ws';
import fs from 'fs';
import dotenv from 'dotenv';
import fastifyFormBody from '@fastify/formbody';
import fastifyWs from '@fastify/websocket';
import fetch from 'node-fetch';
// envの内容読み込み
dotenv.config();
// OpenAI API key
const { OPENAI_API_KEY } = process.env;
// キーが無ければ終了
if (!OPENAI_API_KEY) {
console.error('OpenAI API keyが読み込めませんでした。');
process.exit(1);
}
// Initialize Fastify server
const fastify = Fastify();
fastify.register(fastifyFormBody);
fastify.register(fastifyWs);
// システムメッセージ等の必要な値の定義
const SYSTEM_MESSAGE = `
# Role
- なるべく短く答えます。一度に複数のことは聞きません。あなたは日本語で話します。
- 顧客も日本語で話します、決して日本語以外の言語で解釈しようとしないでください。
- あなたはサブラインという電話アプリサービスのAIアシスタントです。特定のパラメタを取得するのがあなたの役割です。次のステップに従ってパラメタを取得し、必要なパラメタが揃ったら関数を呼び出すという処理を続けてください。
- ステップ1、まずはお客様の名前(customer_name)を取得しましょう。この値はお客様から訂正が無い限り固定して大丈夫です。
- ステップ2、次にお客様が何を求めているか確認してください。サービスに関するご質問・お問い合わせか、転送・お取り次ぎかの2択です。ここまで進んだ後は基本的にステップ3と4の繰り返しです。お客様の質問に一度答える度に必ず関数を実行してから回答しましょう、関数を呼ばずに回答してはいけません。関数を呼ぶのに必要な値を聞くのは許可します。
- ステップ3、ご質問・お問い合わせの場合はまず何に対する質問か(question_type)を取得してください。question_typeは以下のどれかです、プランごとの特徴・使用料金について(plan),機能について(func),管理画面の操作方法について(web),本人確認について(identification),通話品質について(quality),エラー・不具合について(error),その他(other)です。()の中の英語はあなたにパラメタ名を伝えるために書いているのでお客様に読んではいけません。
- お取り次ぎの場合は電話を繋ぐ取り次ぎ先(target_name)を取得して関数(transfer_number)を実行しましょう。転送できなかった場合は転送先(transfer_name)を再取得して、再度関数(transfer_number)を実行しましょう。
- ステップ4、ステップ3でquestion_typeが決まった場合は具体的な質問内容(question)を取得して、関数(question_and_answer)を実行しましょう。
# FAQsの回答を取得
お客様の質問に答えるために\`question_and_answer\`という関数(function)を使ってください。
# 転送処理
お客様が取り次ぎ・転送をご希望の際は:
1. 電話の取り次ぎ先を聞いてください
2. 取り次ぎ先を取得したら \`transfer_number\` という関数(function)を使ってください。
`;
const VOICE = 'alloy';
const PORT = process.env.PORT || 5050;
const MAKE_WEBHOOK_URL = "http://hogehoge.foobar.jp/test/foo/bar";
// セッションに関するデータを保持するためのマップオブジェクト
const sessions = new Map();
// デバッグ用にどのイベントをコンソール出力するか定義
const LOG_EVENT_TYPES = [
'response.content.done',
'rate_limits.updated',
'response.done',
'input_audio_buffer.committed',
'input_audio_buffer.speech_stopped',
'input_audio_buffer.speech_started',
'session.created',
'response.text.done',
'conversation.item.input_audio_transcription.completed'
];
// ルート(疏通確認用)。
fastify.get('/', async (request, reply) => {
reply.send({ message: '疏通確認用。' });
});
// Twilioに着信した際の処理
fastify.all('/incoming-call', async (request, reply) => {
以下記事内容のような実装方法。
・
・
・
その他の活用方法を考えてみよう
さて、今回はサポートセンターをAIにやってもらうようなイメージで上記のような処理を用意しましたが、他にも様々な活用方法がありそうですね。
と言うのもwebsocket接続と任意のパラメタを伴うリクエストを特定のサーバー等に送る仕組みさえできてしまえば、リクエスト先では好きに処理を実装してしまえば良いので割と何でもできそうです。
例えば特定のスケジュールが空いているかDBを検索して空いて入れば予約を取ってメール送信する様にすれば電話予約サービスが出来上がります。
どの程度の精度が求められるか、完全にAIに任せられるのか人間が介入することを前提として良いのかなど、実装されるものは要件によって大きく形を変えますが、可能性は無限大だと思います。これからの生成AI分野がさらに楽しみになものになりますね。
最後に
電話対応はどうしても予定外の長時間拘束や、対人的なストレスもある部分だと思うので、そこをAIに置き換える、というのは大きなニーズがあるように思えます。また、「その他の活用方法を考えてみよう」の部分で言及したようにRealtime APIの可能性はサポートセンターの用途に限った話ではなく、様々なビジネスチャンスに繋げられるものだと思います。それらを逃さないようさらに見識を深めていきたい所存です。
ここまでのご清聴・ご高覧、誠にありがとうございました。
▍もっと知りたい方はこちら
▼インターパークについて
▼技術ブログ一覧
▼よく読まれてる記事