TECH PLAY

株式会社AI Shift(株式会社サイバーエージェントグループ)

株式会社AI Shift(株式会社サイバーエージェントグループ) の技術ブログ

80

1. はじめに こんにちは。AIチームの栗原です。 2025年3月10日(月)〜3月14日(金)に 出島メッセ長崎 にて行われた 言語処理学会第31回年次大会 で、弊社からポスター発表で3件、口頭発表で1件の発表を行いました。 昨今の発表件数の増加の流れはさらに続いており、投稿件数は777件(昨年は599件)と過去最高をさらに記録した模様です。自然言語処理への熱はとどまるところを知らないといった様子です! 2. 各発表資料と発表内でのディスカッション 2.1 JHARS: RAG設定における日本語Hallucination評価ベンチマークの構築と分析 発表情報 Q2:ポスター   3月11日 (火) 10:20-11:50 Q2-17   ○亀井 遼平, 坂田 将樹 (東北大), 邊土名 朝飛 (サイバーエージェント/AI Shift), 栗原 健太郎 (AI Shift/サイバーエージェント), 乾 健太郎 (MBZUAI/東北大/理研) 発表資料 ディスカッション 研究のFutureWorkになり得るご意見を多数いただきました。ありがとうございます。その一部を紹介いたします。 ドメインが一般的すぎるような気がする。一番上のamebaの自社データの例のように、LLMの事前学習データに含まれないようなものを参考文献に入れたときや、もっと参考文献の量が多かった時にどうなるのか気になる hallucinationの自動検出に関して、推論過程を出力させたり、プロンプトをもう少し工夫したらうまく検出できるようになったりするか気になる。 2.2 タスク指向音声対話における大規模言語モデルを活用した柔軟な発話終了検知の検討 発表情報 P5:ポスター   3月11日 (火) 10:20-11:50 P5-15   ○大竹 真太, 東 佑樹, 杉山 雅和 (AI Shift) 発表資料 ディスカッション Q: 発話終了と判定するリッカート尺度の評価値の閾値を調整した場合の遅延時間と誤検知率がどうなるか知りたい A: LLMが出力するリッカート尺度の評価値には偏りがあります。3以下の予測値については3->2->1の順に計数が多くなっています。本実験では閾値を3に設定した場合のみの評価でしたが、これを2や1に変更すると誤検知率は下がり、遅延時間が長くなると推測できます。この閾値についても誤検知率と遅延時間のトレードオフを見ながら調整していく必要があると考えられます。 Q: 発話チャンク間の秒数を明示的にLLMに与えると性能が改善したりすることはないのか A: 入力が音声の場合は発話と発話の間の無音区間についても一応考慮されているとは思いますが、明示的に与えた方が性能が上がるということは十分に考えられます。今後のプロンプトエンジニアリングの参考とさせていただきます。 Q: VAPと比較はしないのか A: 今回の実験は沈黙時間ベースの手法をベースラインとして定めていたので特に比較していなかったですが、今後の課題としてVAPとの比較にも取り組みたいと考えています。 Q: どれだけユーザ発話が終了する前に発話終了のタイミングを予測するかが重要なのに遅延時間にLLMの処理時間が入ってきてしまうのは良いのか A: 予測にモデルの処理時間が入るのは自然なことだと考えています。本実験の場合、沈黙時間ベースの方法をベースラインとしているため、それよりも遅延時間を短縮できたことは成果だと考えられます。学術的には新規性に乏しかったり地味な結果となってしまっているかもしれませんが、実際のプロダクト運用を考えると、本研究の提案手法は十分に検討の余地があると考えています。 2.3 TEPPAY: ゲームのプレイ動画を入力とする実況AI Tuberシステムの提案 発表情報 P9:ポスター   3月13日(木) 10:20-11:50 P9-19   ○栗原健太郎 (AI Shift, サイバーエージェント), 吉野哲平, 高市暁広, 岩田伸治 (サイバーエージェント), 長澤春希 (AI Shift), 佐藤志貴, 岩崎祐貴 (サイバーエージェント) 発表資料 ディスカッション システムのクオリティ向上に向けたご意見を多数いただきました。その中から一部を抜粋して紹介いたします。 ゲームの画面の状態はある程度GameStateで保持できていそうだが、プレイ内容自体はStateに保存できていない、つまりそれを考慮した発話もできていなそう。そこで、Vision-Language-Model (VLM)の活用を提案したい。ただし、リアルタイム性を保持するという観点から、状況を表すラベルなどの単語を生成して、発話のヒントとして与えるのが良さそうに見える テキストの発話について、VTuberのようにユーモア混じりの面白さを実現していくのは少し難しいように感じる。一方で、状況説明などがある程度できそうに見えるので、スポーツ実況・将棋実況などの場況を説明しながらの実況などは向いているかも。 1人での実況はどうしても、単調になりがちかも。いっそLLM同士を対話させることで、2人実況のような枠組みにすると、より幅が広がるのではないでしょうか。 2.4 多面的なユーザ意欲を考慮したセールス対話データセットおよび対話システムの構築と評価 発表情報 D4:テーマセッション2: 人とAIの共生に向けた対話システム・言語使用の研究(3)   3月11日 (火) 14:50-16:20 D4-2  ○邊土名 朝飛, 馬場 惇, 佐藤 志貴 (サイバーエージェント), 赤間 怜奈 (東北大) 発表資料 ディスカッション Q: 対話の最初はユーザからされるのでしょうか?対話をしようとする段階で購買意欲は高い状態でスタートするというバイアスはないのかと気になりました。 A: 必ずセールス側から対話を開始する設定で行いました。事前購買意欲バイアスに関してですが、前提として「商品に多少興味はあるユーザ」を想定しておりまして、全く購買意欲がないユーザはセールストークの対象外としています。 Q: 詳細な説明を求めているとか対話を求めてるユーザにのみ対話をするみたいな戦略が必要なのかもしれないですね A: 確かにそのような戦略も必要かもしれません。ちなみにベテランの店頭販売員の方々にヒアリングすると、「相手の動き、視線、見ている商品、他にどの商品を見ていたかを考慮して話しかけるか否かや声掛け内容を変えている」とのことでした。テキストチャットの内容だけでこのような戦略を決定するのは難しいかと思いますので、発話(テキスト)だけでなくマルチモーダルな情報を考慮した対話システムが必要だと考えています。 Q: 意欲が高くなる発話の傾向があれば教えていただきたいです。 A: 明確にこうした傾向がある、とは言いにくいのですが、以下の2つの傾向があると思います。①質問は基本的にセールス側から行い、ユーザのニーズを把握する & ユーザ側から想定していない質問がされることを防ぐ②漠然とした質問など、ユーザの回答コストが高い質問を行わない(②の回答コストが高い質問の例:どんな商品をお探しですか?、②の回答コストが低い質問の例:仕事と趣味(映画鑑賞や音楽鑑賞など)のどちらの用途で使用されることが多いでしょうか?) 3. おわりに NLP2024, YANS2024に続いて、生成AIの台頭に起因した評価やデータセット構築の動きは今年も盛んなように感じました。たとえば、事業者として関心が高いRAG一つをとっても、表の上手い扱い方などその幅は多岐にわたっていました。 また、横井先生らのチュートリアルにもあるとおり、モデルの振る舞いに着目する流れもかなり盛んになりつつあると感じました!出力結果に着目している事例や、モデルのレイヤーに着目するなど多く見られたように感じます。(受賞論文もそういったものが多かったでしょうか?) LLMいずれにせよ、一見してなんでもできてしまいそうに見えるLLMを解き明かすという動きがより加速しているように実感しました! 最後に、このような素晴らしい学会を開催頂いた運営の皆様に感謝申し上げます。 来年は宇都宮開催が発表されました。宇都宮と言いますと、栗原は小学校の修学旅行で行ったのが最後になりますので、15年ほど前の記憶を掘り起こしながらの参加になるかなと思います!(尚、ほぼ記憶なし) 来年もあわよくば尖った研究発表ができれば!とも考えておりますので皆様よろしくお願いいたします。 投稿 言語処理学会第31回年次大会(NLP2025) 発表報告 は 株式会社AI Shift に最初に表示されました。
アバター
こんにちはAI Shiftの栗原です。3月10日(月)から3月14日(金)に 出島メッセ長崎 で 言語処理学会年次大会(NLP2025) が実施されます。AI Shiftからは口頭発表1件とポスター発表3件の合計4件の発表を行います。(口頭発表1件、ポスター発表1件は東北大NLPグループとの共同研究。)栗原の発表については、社内のゼミ制度による組織横断活動の成果発表になります。 本記事では各発表の概要と、議論したいポイントについて取り上げたいと思います。(AI Shift メンバー関与分のみ記載) 1. AI Shiftからの発表 ポスター発表 3月11日 (火) 10:20-11:50 Q2-17 JHARS: RAG設定における日本語Hallucination評価ベンチマークの構築と分析 亀井 遼平, 坂田 将樹 (東北大), 邊土名 朝飛 (サイバーエージェント/AI Shift), 栗原 健太郎 (AI Shift/サイバーエージェント), 乾 健太郎 (MBZUAI/東北大/理研) 3/12 (水) 8:30 ~ 10:00 P5-15 タスク指向音声対話における大規模言語モデルを活用した柔軟な発話終了検知の検討 大竹 真太, 東 佑樹, 杉山 雅和 (AI Shift) 3月13日(木) 10:20-11:50 P9-19 TEPPAY: ゲームのプレイ動画を入力とする実況AI Tuberシステムの提案 栗原健太郎 (AI Shift, サイバーエージェント), 吉野哲平, 高市暁広, 岩田伸治 (サイバーエージェント), 長澤春希 (AI Shift), 佐藤志貴, 岩崎祐貴 (サイバーエージェント) 口頭発表 3月11日 (火) 14:50-16:20 D4-2 多面的なユーザ意欲を考慮したセールス対話データセットおよび対話システムの構築と評価 邊土名 朝飛, 馬場 惇, 佐藤 志貴 (サイバーエージェント), 赤間 怜奈 (東北大) 2. 各発表の概要と議論したいポイント ポスター発表 2.1 JHARS: RAG設定における日本語Hallucination評価ベンチマークの構築と分析 概要 本研究では、日本語RAG設定におけるhallucination評価ベンチマークJHARSを構築した。3種類のLLMで実験を行った結果、(1) hallucinationの発生率は低い、(2) 事実確認が必要なhallucinationが発生しやすい、(3) GPT-4oは高い再現率でhallucinationを検出できることが示唆された。 議論したいポイント アノテーションのフローに関する議論 適切なドキュメントを取得できている状況下におけるHallucinationの少なさに関する議論 Hallucination検出モデル構築の方法に関する議論 スパン単位でのよりきめ細かいアノテーションの議論 LLMを用いたアノテーション補助の方法論について 2.2 タスク指向音声対話における大規模言語モデルを活用した柔軟な発話終了検知の検討 概要 本研究ではタスク指向対話において, 大規模言語モデル(LLM)の文脈理解能力を活用することで, 柔軟かつ高速な発話終了検知を実現する新しい手法を提案する. 社内で収集した電話音声データを用いて検知の遅延時間を評価し, ベースラインよりも約 37.8%短縮できた. 議論したいポイント 発話終了検知の誤検知を減らす方法について タスク指向対話における発話終了検知の評価指標について 2.3 TEPPAY: ゲームのプレイ動画を入力とする実況AI Tuberシステムの提案 概要 VTuberなどの実況配信への関心が高まる一方で、準備の複雑さや配信内容への不安が参入障壁を高くしている。本研究では、LLMを活用した実況配信システム「TEPPAY」を提案する。TEPPAYは発話生成など7つのモジュールで構成され、配信に必要な最低限の性能を担保しているが、魅力的な実況動画作成には課題が残ることを確認した。 議論したいポイント 各種モジュールに用いているツールの性能の妥当性・および他ツールの検討 TEPPAYのより良い構成の検討(etc Semantic Classifierは必要か、Avatarによる配信の振る舞いの実現方法 etc) 口頭発表 2.4 多面的なユーザ意欲を考慮したセールス対話データセットおよび対話システムの構築と評価 概要 本研究では、購買意欲を向上させるセールス対話システムの実現に向け、多面的なユーザの意欲を考慮した日本語セールス対話データセットを構築した。LLMと本データセットを用いたユーザ評価実験では、発話レベルでユーザの意欲を考慮し、分析から得られたセールス対話戦略の知見を組み込むことがユーザの購買意欲向上につながることが示唆された。 議論したいポイント ユーザの購買意欲をより効果的に向上させるには、どのようなデータや対話戦略が必要か 現状のセールス対話データセットを拡張・再構築する場合、どのようなデータや観点を追加するとより価値が高まりそうか 3. おわりに 今年もAI Shiftの他に,CyberAgentグループから多数の発表とスポンサーブースの出展があります。そちらの発表もぜひお越しください! 栗原個人としては昨年の神戸に引き続き、長崎も人生初上陸ですので、すでに非常に楽しみです。 ぜひ会場で研究に関する議論、雑談などできればと思います!よろしくお願いします! 投稿 言語処理学会第31回年次大会(NLP2025) でAI Shiftから4件の発表を行います は 株式会社AI Shift に最初に表示されました。
アバター
こんにちは!AIチームの戸田です。 近年、様々な作業の自動化を目的としたAI Agent(以下、Agent)が注目を集めています。特に、GUIを操作するAgentは、ユーザーインターフェースを介したタスクの自動化を可能にするため、大きな可能性を秘めています。 AnthropicのClaude 3.5 Computer Use や OpenAIのOperator 、オープンソースは browser-use などがその代表例です。研究分野でも、学習用データセット( GUI-World: A Dataset for GUI-Oriented Multimodal Large Language Models など)やベンチマーク( WebWalker: Benchmarking LLMs in Web Traversal など)が多数公開されており、GUI操作Agentの研究開発は活発化しています。 一方で、Agentにコンピューターの操作を委任することには注意が必要です。意図せず危険なコードを実行してしまう恐れがあるため、セキュリティリスクが非常に大きいと考えられます。 そこで注目したいのが、 E2B というオープンソースのインフラストラクチャです。E2BはVMWareやVirtualboxのような仮想環境を提供し、Agentの誤操作のリスクを回避する仕組みを整えています。 前回の記事 でも、CodeAgentの実行環境としてE2Bを活用できることを少し触れました。 今回は、E2Bが提供するGUI環境「 Desktop Sandbox 」を実際に試用し、その機能や使い方、そしてLLMを用いたGUI操作のサンプルアプリケーション「 Open Computer Use 」について解説します。 https://e2b.dev/ Desktop Sandboxの使い方 Desktop Sandboxの使い方や、Desktop Sandboxの環境をLLMを使って操作するサンプルアプリ Open Computer Use を紹介していきます。PythonとJavaScriptのSDKが提供されていますが、本記事ではPythonを使用します 初期設定 ライブラリはpipでインストールすることができます。 pip install e2b-desktop 加えて、E2Bにサインアップし、APIキーを取得する必要があります。 KeysのAdd API Keyから追加し、発行されたAPI Keyを環境変数 E2B_API_KEY に設定して下さい。 # .bashrcなどに設定 export E2B_API_KEY="xxx" 仮想環境の作成 以下のコードで仮想環境を構築できます。 from e2b_desktop import Sandbox desktop = Sandbox() import文を除けばたった一行で構築できます。お手軽ですね。 GUI操作の様子を映像で確認したい場合、 video_stream 引数に True を設定することで実現できます。 from e2b_desktop import Sandbox desktop = Sandbox(video_stream=True) stream_url = desktop.get_video_stream_url() print(stream_url) # https://e2b.dev/stream/sandbox/<sandbox-id>?token=<secret-token>の形式 最後の print 文で表示されたURLをブラウザで開くと、仮想マシンのデスクトップ画面がリアルタイムでストリーミング配信されます。 OSには高速で軽量なデスクトップ環境として知られる xfce が採用されており、実際に仮想環境を作成してみたところ、数秒で起動するほど軽量でした。 GUI操作 Desktop Sandboxでは、以下のGUI操作が可能です。 マウス操作(移動やクリック) スクリーンショットを撮る キーボード操作(テキストを打ち込む) ファイル操作 bashコマンドの実行 実際に右クリックを実行する例を見てみましょう。 from e2b_desktop import Sandbox desktop = Sandbox(video_stream=True) stream_url = desktop.get_video_stream_url() print(stream_url) desktop.right_click() 他の操作に関してはここでは紹介しませんが、 README にまとまっているので気になる方はそちらを参照していただければと思います。 GUI操作はデフォルトで提供されているメソッドに加え、 PyAutoGUI を使うこともできます。PyAutoGUIを使った右クリックの例を見てみましょう。 from e2b_desktop import Sandbox desktop = Sandbox(video_stream=True) stream_url = desktop.get_video_stream_url() print(stream_url) desktop.pyautogui("pyautogui.click(button='right')") # PyAutoGUI で右クリックする これによりプログラマティックなGUI操作が可能になります。 以下は私がテストで作ったデスクトップ上でマウスを円形に動かすコードを紹介します。 import time import math from e2b_desktop import Sandbox desktop = Sandbox(video_stream=True) stream_url = desktop.get_video_stream_url() print(stream_url) # 円の中心座標(画面の中央付近に設定) screen_width, screen_height = desktop.get_screen_size() cx, cy = screen_width // 2, screen_height // 2 r = 100 # 半径の設定 interval = 0.01 # 動作間隔 loops = 3 # 3周回す for _ in range(loops): for angle in range(0, 360, 15): # 15度刻みで移動 x = cx + r * math.cos(math.radians(angle)) y = cy + r * math.sin(math.radians(angle)) desktop.pyautogui(f"pyautogui.moveTo({x}, {y}, duration={interval})") このコードを実行し、出力されたURLにアクセスすると、以下のような映像が確認できます。 少しカクついてはいますが、マウスがぐるぐる円運動をしていることがわかります。 Open Computer Use Desktop Sandboxの基本的な使い方を紹介しましたが、LLMを用いて操作するには、さらに工夫が必要です。browser-useなどの既存のGUIを操作する Agentを参考にすると、LLMによるGUI操作Agentの一般的な処理フローは以下の通りです。 ユーザーからの指示を受け取る スクリーンショットを撮る マウスの座標など指示されたタスク遂行に必要な情報を得る 必要な操作を予測する 予測された操作を実行する 以下、指示が完了するまで2-5を繰り返す このフローを自力で実装することも可能ですが、少々手間がかかります。E2Bは、このフローを簡単に実現できるサンプルアプリケーション「Open Computer Use」を提供しています。以下はOpen Computer Useのアーキテクチャ図です。 以下に環境構築手順を示します。(macOSを想定しています) # 必要なライブラリのインストール brew install poetry ffmpeg # リポジトリのクローン git clone https://github.com/e2b-dev/open-computer-use/ # 依存するPythonライブラリのインストール cd open-computer-use && poetry install 次に、使用したいLLMプロバイダのAPIキーを環境変数に設定し、該当するプロバイダの設定を有効にします。そしてリポジトリ内の config.py の設定をプロバイダに合わせます。私はOpenAIのAPI Keyを設定したので、 vision_model = providers.OpenAIProvider("gpt-4o") と action_model = providers.OpenAIProvider("gpt-4o") のコメントを外しました。 以上で準備は完了です。実際にOpen Computer Useを実行してみましょう。今回はAgentに「Webブラウザを開いて、 UCIのiris のデータセットをダウンロードし、ダウンロードされたzipファイルを解凍して中身を確認する」といった指示を与えます。 poetry run start --prompt "Download the UCI iris dataset using a web browser, unzip the downloaded zip file, and check the contents." 上記のコマンドを実行すると、仮想環境内でブラウザが起動し、指示に従って操作が開始されます。操作ログの動画とAgentの行動ログを以下に示します。 ▼ Agentの行動ログ Log body { max-width: 1000px; margin: 0 auto; padding: 20px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; line-height: 1.6; background: #f5f5f5; } h1 { color: #333; border-bottom: 2px solid #ddd; padding-bottom: 10px; } p { margin: 12px 0; padding: 10px; border-radius: 4px; } Computer Use Log USER: Download the UCI iris dataset using a web browser, unzip the downloaded zip file, and check the contents. screenshot /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/screenshot_1.png THOUGHT: The objective is: Download the UCI iris dataset using a web browser, unzip the downloaded zip file, and check the contents. On the screen, I see: - Desktop environment with a blue background. - Icons for "Trash," "File System," and "Home." - A taskbar at the bottom with icons that include an internet browser symbol (looks like a globe). - Time and user information on the top right. This means the objective is: not complete. The next step is to click the internet browser icon in order to open the web browser. ACTION: click {'query': 'browser'} screenshot /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/screenshot_2.png click /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/location_3.png) OBSERVATION: The mouse has clicked. screenshot /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/screenshot_4.png THOUGHT: The objective is: Download the UCI iris dataset using a web browser, unzip the downloaded zip file, and check the contents. On the screen, I see: - Desktop background with blue color featuring a stylized mouse design. - Desktop icons: Trash, File System, and Home. - A panel at the bottom with some applications, including a web browser icon and a file manager. This means the objective is: not complete. The next step is to click the web browser icon in order to open the browser and navigate to the UCI Machine Learning Repository website. ACTION: click {'query': 'Web Browser'} screenshot /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/screenshot_5.png click /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/location_6.png) OBSERVATION: The mouse has clicked. screenshot /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/screenshot_7.png THOUGHT: The objective is: Download the UCI iris dataset using a web browser, unzip the downloaded zip file, and check the contents. On the screen, I see: - Applications menu at the top left. - Desktop icons: Trash, File System, Home. - A taskbar at the bottom with icons, including: - A globe icon (Web Browser). - A terminal icon. - A file manager icon. This means the objective is: not complete. The next step is to click the globe icon (Web Browser) in order to open the web browser. ACTION: click {'query': 'Web Browser'} screenshot /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/screenshot_8.png click /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/location_9.png) OBSERVATION: The mouse has clicked. screenshot /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/screenshot_10.png THOUGHT: The objective is: Download the UCI iris dataset using a web browser, unzip the downloaded zip file, and check the contents. On the screen, I see: - A web browser open, displaying a welcome screen for Firefox. - Options to "Save and continue" or "Skip this step." - A search or address bar at the top. This means the objective is: not complete. The next step is to click "Skip this step" in order to proceed to the main browser interface. ACTION: click {'query': 'Skip this step'} screenshot /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/screenshot_11.png click /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/location_12.png) OBSERVATION: The mouse has clicked. screenshot /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/screenshot_13.png THOUGHT: The objective is: Download the UCI iris dataset using a web browser, unzip the downloaded zip file, and check the contents. On the screen, I see: - Firefox web browser open with a welcome page. - A "Save and continue" button on the welcome page. - Tabs titled "Welcome to Firefox" and "Mozilla Firefox Privacy Notice". - A search/address bar at the top. This means the objective is: not complete. The next step is to type "UCI iris dataset" in the search/address bar in order to perform a web search to find and download the dataset. ACTION: click {'query': 'Search or enter address'} screenshot /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/screenshot_14.png click /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/location_15.png) OBSERVATION: The mouse has clicked. screenshot /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/screenshot_16.png THOUGHT: The objective is: Download the UCI iris dataset using a web browser, unzip the downloaded zip file, and check the contents. On the screen, I see: - A Firefox web browser tab open with a text box labeled "Search or enter address." - Suggestions and trending searches listed below. This means the objective is: not complete. The next step is to type "UCI Machine Learning Repository Iris dataset" in the search bar in order to find and navigate to the website where the dataset can be downloaded. ACTION: type_text {'text': 'UCI Machine Learning Repository Iris dataset'} OBSERVATION: The text has been typed. ACTION: send_key {'name': 'Return'} OBSERVATION: The key has been pressed. screenshot /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/screenshot_17.png THOUGHT: The objective is: Download the UCI iris dataset using a web browser, unzip the downloaded zip file, and check the contents. On the screen, I see: A web browser window displaying a Google search page for "UCI Machine Learning Repository Iris dataset." This means the objective is: not complete. The next step is to click on the search result link for the UCI Machine Learning Repository that leads to the Iris dataset in order to access and download the dataset. ACTION: click {'query': 'UCI Machine Learning Repository: Iris Data Set'} screenshot /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/screenshot_18.png click /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/location_19.png) OBSERVATION: The mouse has clicked. screenshot /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/screenshot_20.png THOUGHT: The objective is: Download the UCI iris dataset using a web browser, unzip the downloaded zip file, and check the contents. On the screen, I see: A Google search results page with the query "UCI Machine Learning Repository Iris dataset." The top result is "Iris - UCI Machine Learning Repository" with a link to the repository page. This means the objective is: not complete. The next step is to click "Iris - UCI Machine Learning Repository" in order to navigate to the dataset page. ACTION: click {'query': 'Iris - UCI Machine Learning Repository'} screenshot /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/screenshot_21.png click /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/location_22.png) OBSERVATION: The mouse has clicked. screenshot /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/screenshot_23.png THOUGHT: The objective is: Download the UCI iris dataset using a web browser, unzip the downloaded zip file, and check the contents. On the screen, I see: - A Google search result page. - A link titled "Iris - UCI Machine Learning Repository" with the URL `https://archive.ics.uci.edu/dataset/53/iris`. This means the objective is: not complete. The next step is to click the link titled "Iris - UCI Machine Learning Repository" in order to navigate to the page where the dataset can be downloaded. ACTION: click {'query': 'Iris - UCI Machine Learning Repository'} screenshot /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/screenshot_24.png click /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/location_25.png) OBSERVATION: The mouse has clicked. screenshot /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/screenshot_26.png THOUGHT: The objective is: Download the UCI iris dataset using a web browser, unzip the downloaded zip file, and check the contents. On the screen, I see: - UCI Machine Learning Repository homepage. - "Iris" dataset listed under Popular Datasets. - "VIEW DATASETS" button. - Navigation links for Datasets, Contribute Dataset, About Us. - Privacy notice banner at the bottom. This means the objective is: not complete. The next step is to click "Iris" under Popular Datasets in order to access the dataset page. ACTION: click {'query': 'Iris'} screenshot /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/screenshot_27.png click /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/location_28.png) OBSERVATION: The mouse has clicked. screenshot /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/screenshot_29.png THOUGHT: The objective is: Download the UCI iris dataset using a web browser, unzip the downloaded zip file, and check the contents. On the screen, I see: - UCI Machine Learning Repository website. - Information about the Iris dataset. - A "DOWNLOAD (3.7 KB)" button. - Navigation tabs and dataset details. This means the objective is: not complete. The next step is to click the "DOWNLOAD (3.7 KB)" button in order to download the Iris dataset. ACTION: click {'query': 'DOWNLOAD (3.7 KB)'} screenshot /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/screenshot_30.png click /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/location_31.png) OBSERVATION: The mouse has clicked. screenshot /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/screenshot_32.png THOUGHT: The objective is: Download the UCI iris dataset using a web browser, unzip the downloaded zip file, and check the contents. On the screen, I see: - A web page from the UCI Machine Learning Repository. - A notification indicating that "iris.zip" has been downloaded. - The "SHOW ALL DOWNLOADS" option in the browser. This means the objective is: not complete. The next step is to click "SHOW ALL DOWNLOADS" in order to locate the downloaded zip file. ACTION: click {'query': 'SHOW ALL DOWNLOADS'} screenshot /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/screenshot_33.png click /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/location_34.png) OBSERVATION: The mouse has clicked. screenshot /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/screenshot_35.png THOUGHT: The objective is: Download the UCI iris dataset using a web browser, unzip the downloaded zip file, and check the contents. On the screen, I see: A 'Library' window with "iris.zip" listed under downloads. The downloads window is open. This means the objective is: Not complete. The next step is to click "iris.zip" in order to open the file location and proceed to unzip it. ACTION: click {'query': 'iris.zip'} screenshot /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/screenshot_36.png click /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/location_37.png) OBSERVATION: The mouse has clicked. screenshot /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/screenshot_38.png THOUGHT: The objective is: Download the UCI iris dataset using a web browser, unzip the downloaded zip file, and check the contents. On the screen, I see: - A "Library" window displaying the download history. - An entry for `iris.zip`, downloaded from uci.edu and sized at 3.7 KB. - The Firefox browser window in the background. - Options for "Organize" and "Clear Downloads". This means the objective is: not complete. The next step is to click the `iris.zip` download entry in order to locate and unzip the downloaded file. ACTION: click {'query': 'iris.zip'} screenshot /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/screenshot_39.png click /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/location_40.png) OBSERVATION: The mouse has clicked. screenshot /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/screenshot_41.png THOUGHT: The objective is: Download the UCI iris dataset using a web browser, unzip the downloaded zip file, and check the contents. On the screen, I see: - A "Library" window showing downloads. - The file "iris.zip" downloaded with a size of 3.7 KB from uci.edu. - A web browser open with options for downloading and viewing datasets. This means the objective is: not complete. The next step is to click "iris.zip" in order to open or unzip the file and check its contents. ACTION: click {'query': 'iris.zip'} screenshot /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/screenshot_42.png click /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/location_43.png) OBSERVATION: The mouse has clicked. screenshot /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/screenshot_44.png THOUGHT: The objective is: Download the UCI iris dataset using a web browser, unzip the downloaded zip file, and check the contents. On the screen, I see: - A browser window showing download history with an entry for "iris.zip" from uci.edu. - A button to clear downloads. - An active Firefox browser window in the background. This means the objective is: not complete. The next step is to click "iris.zip" in order to open or unzip the file and check its contents. ACTION: run_command {'command': 'unzip iris.zip -d iris_unzipped'} OBSERVATION: Error executing function: Command exited with code 9 and error: unzip: cannot find or open iris.zip, iris.zip.zip or iris.zip.ZIP. screenshot /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/screenshot_45.png THOUGHT: The objective is: Download the UCI iris dataset using a web browser, unzip the downloaded zip file, and check the contents. On the screen, I see: - A Firefox window with the Downloads Library open. - A file named `iris.zip` listed in the Downloads Library. - An option to open in the file manager or perform other actions. This means the objective is: not complete. The next step is to **click** on `iris.zip` in order to locate the file in the file manager and proceed to unzip it. ACTION: click {'query': 'Show In Finder'} screenshot /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/screenshot_46.png click /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/location_47.png) OBSERVATION: The mouse has clicked. screenshot /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/screenshot_48.png THOUGHT: The objective is: Download the UCI iris dataset using a web browser, unzip the downloaded zip file, and check the contents. On the screen, I see: - A browser showing the Downloads Library with "iris.zip". - A file manager window showing the path `/root/Downloads/` with "iris.zip" highlighted. - A notice about using the root account. This means the objective is: not complete. The next step is to click on "iris.zip" to unzip it in order to extract the dataset contents. ACTION: click {'query': 'iris.zip'} screenshot /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/screenshot_49.png click /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/location_50.png) OBSERVATION: The mouse has clicked. screenshot /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/screenshot_51.png THOUGHT: The objective is: Download the UCI iris dataset using a web browser, unzip the downloaded zip file, and check the contents. On the screen, I see: - A file named "iris.zip" in the Downloads folder. - A warning message about using the root account. - A file manager window open to the Downloads directory. - A Firefox Downloads Library window showing "iris.zip". This means the objective is: not complete. The next step is to click on "iris.zip" in order to extract the contents of the zip file. ACTION: double_click {'query': 'iris.zip'} screenshot /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/screenshot_52.png double click /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/location_53.png) OBSERVATION: The mouse has double clicked. screenshot /var/folders/k9/0d1smgqj6f9_hkzf80qfbw80941m76/T/tmpswcple0c/screenshot_54.png THOUGHT: The objective is: Download the UCI iris dataset using a web browser, unzip the downloaded zip file, and check the contents. On the screen, I see: - The Firefox browser open with the UCI iris dataset download page. - The Downloads library showing `iris.zip` file. - The xarchiver application open displaying the contents of `iris.zip`. - Files listed: `Index`, `bezdekIris.data`, `iris.data`, `iris.names`. This means the objective is: complete. The iris dataset has been downloaded, unzipped, and the contents are visible. Agentの行動ログはAgentの思考であるTHOUGHTと実行したACTION、そして、観測した情報のOBSERVATIONに分けられています。 Agentの行動ログと操作ログの動画を合わせて見てみると、Webブラウザのアイコンを見つけられなかったり、UCIのサイトで迷子になったりと、目的のデータセットにたどり着くまでに時間がかかったりしていることがわかります。しかし、最終的には指示通りIrisデータセットのダウンロードと確認に成功しています。 終わりに 本記事では、E2Bが提供するGUI操作Agent開発のための安全な仮想環境「Desktop Sandbox」とその活用例として「Open Computer Use」を紹介しました。Desktop Sandboxを利用することで、非常に簡単に仮想環境を構築でき、Open Computer Useを使えば、GUI操作Agentへの指示をすぐに試すことが可能です。 しかし、現状のGUI操作Agentの精度はまだ発展途上であり、複雑なタスクや予期せぬ状況への対応には課題が残るようです。ベースとなるLLMの性能向上や、より洗練されたアーキテクチャの開発によりこれが解決できることを期待しています。 E2Bのような安全な開発環境は、こうした Agentの発展を後押しするとともに、身元不明の実行ファイルのテストや、マルウェア・スパイウェアの解析などにも使えるのではないでしょうか?私も活用方法を模索していきたいと思います。 最後までお読みいただきありがとうございました! 投稿 E2B Desktop Sandbox: GUI操作Agentのための安全な仮想環境 は 株式会社AI Shift に最初に表示されました。
アバター
こんにちは AIチームの戸田です 今回はHugging Faceが提供しているAgent Frameworkの smolagents が非常に興味深かったので、実際に触ってみた内容を共有します。 smolagentsは、Agentをシンプルに構築できることを目的としたライブラリで、従来のAgent Frameworkと比較して以下の2点で差別化されています。 シンプルな設計: 必要最小限の抽象化でAgentの構築が可能。公式のコードも約1,000行と軽量。 CodeAgent: Pythonコードを直接生成・実行することで、処理ステップを削減し、パフォーマンスを向上。 特に注目すべきは、後者の CodeAgent という仕組みです。CodeAgentは、タスクに応じたPythonコードを生成し、それを実行することで複雑なロジックや操作を簡潔に処理できる点が優れています。 なぜCodeAgentが必要とされるのか 従来のAgent Frameworkでは、LLMの出力を解析し、事前に定義されたJSONやXMLなどのフォーマットに従ってツールや関数を呼び出す方法が一般的ですが、このアプローチには以下のような課題があります。 解析の複雑さ :LLMの出力を正確に解析し、適切なツールや引数を特定するためのパーサーが複雑化します。 表現力の限界 :JSONなどの定型的なフォーマットでは、複雑なロジックや条件分岐、ループなどを表現するのが困難です。 LLMの訓練データとの不整合 :LLMは大量のコードデータで訓練されており、コードによる指示やアクションの生成には慣れていますが、カスタムフォーマットには最適化されていません。 これらの問題を解決するために、CodeAgentのアプローチが必要とされています。 CodeAgentのメリット 複数の研究( 参考論文1 , 参考論文2 , 参考論文3 )で従来のツールを呼び出すAgentに比べてCodeAgentの方が優れていると述べられています。 Executable Code Actions Elicit Better LLM Agents Figure 1より 簡単にどのような点で優れているのかをまとめました。 1. コードによる高度な表現力 CodeAgentは、LLMが直接Pythonコードを生成し、そのコードを実行することでタスクを達成します。これにより、複雑なアルゴリズム、条件分岐、ループ、例外処理など、Pythonコードで表現できるあらゆるロジックを活用できます。 2. LLMの能力を最大限に活用 LLMは既に大量のコードデータで訓練されており、コード生成や理解に優れています。CodeAgentはこの能力を直接活用し、より自然で効率的なエージェントの動作を実現します。 3. ツールやライブラリの柔軟な統合 Pythonコードを生成・実行するため、既存の豊富なPythonのライブラリやツールを簡単に統合できます。これにより、データ分析、機械学習、ウェブスクレイピングなど、多彩なタスクに対応可能です。 4. 開発とデバッグの容易さ 生成されたコードは通常のPythonコードであるため、開発者は既知のデバッグ手法やテストフレームワークを活用できます。これにより、問題の特定や解決が容易になります。 smolagentsの使い方 ここからはsmolagentsの基本的な使い方やCodeAgentの具体例を見ていきます。 インストール smolagentはpipで簡単にインストール可能です。 pip install smolagents シンプルなCodeAgentの構築と実行 以下は、smolagentsを用いた簡単なCodeAgentの構築例です。このコードではAgentにフィボナッチ数列の118番目の数字を答えさせます。 from smolagents import CodeAgent, LiteLLMModel model = LiteLLMModel(model_id="openai/gpt-4o") agent = CodeAgent( tools=[], model=model ) agent.run( "Could you give me the 118th number in the Fibonacci sequence?", ) このコードを実行すると以下のような出力が得られます。 1264937032042997393488322 という出力が得られました。 こちらのサイト で確認してみるとあってそうです。 今回の例ではモデルにOpenAIのGPT-4oを使用していますが、LiteLLMを通じて使用できるClaudeやGeminiなどの他のLLMや、transformersから使えるLocal LLMも設定することができます。 CodeAgentを定義する際の引数であるtoolsにはAgentが使用する関数(所謂Function Calling)を設定するのですが、smolagentsはデフォルトでPythonコード実行機能が備わっているので何も設定しなくても、以下のような関数をAgent自身で生成して実行することで回答を得ています。 def fibonacci(n): a, b = 0, 1 for _ in range(n-1): a, b = b, a + b return a fibonacci_118 = fibonacci(118) print(fibonacci_118) ちなみに生成したコードにバグが含まれていた場合、エラー出力を考慮してコードを再生成してくれます。何回まで再チャレンジするかはmax_stepsパラメータで設定します(デフォルト6)。 使用できるライブラリを追加する CodeAgentが生成するコードが利用できるモジュールは print などのPythonにデフォルトで設定されているもののみですが、CodeAgentを定義する際に additional_authorized_imports を設定することで、使用できるモジュールを追加できます。以下はrequestsとbeautifulsoupを設定することでスクレイピングを行えるCodeAgentの例です。 from smolagents import CodeAgent, LiteLLMModel model = LiteLLMModel(model_id="openai/gpt-4o") agent = CodeAgent( tools=[], model=model, additional_authorized_imports=['requests', 'bs4'], ) agent.run("Could you get me the title of the page at url 'https://www.ai-shift.co.jp/techblog/5333'?") スクレイピング対象は前回のTech Blogで、タイトルを取得します。 【AI Shift Advent Calendar 2024】2024年のTech Blog/対外発表の振り返り コードを実行すると以下のような出力が得られます。 どうやら適切なタイトルを取得することができたようです。 本来であれば自分でbeautifulsoupを使ってクローリング用の関数を作ったり、 Firecrawl のようなツールとの連携が必要ですが、CodeAgentではこういった手間がありません。 一方、意図を超えたコードを生成して実行できてしまう可能性がある点には気をつけて下さい。smolagentsは無限ループやリソースの肥大化を防ぐために操作数を制限しているのですが、セキュリティを最大限に高めるに E2B などのサンドボックス環境でコードを実行することが好ましいです。E2Bは、分離されたコンテナでコードを実行するリモート実行サービスで、smolagentsではCodeAgentを定義する際にuse_e2b_executorをTrueに設定することで使用できます。複雑な処理をさせるときは、基本的にこちらを使うのが良いでしょう。 Tools これまでの例でtoolsは空でしたが、生成したPythonコードだけでは処理しきれない指示に対応するために、OpenAIのFunction Callingのように独自ツールを関数として設定することができます。 smolgagentsにはデフォルトでWeb検索(DuckDuckGoSearchTool)や音声処理(SpeechToTextTool)などの様々なツールが用意されていますが、 smolagents.Tool クラスを継承する、もしくはデコレーター smolagents.tool を使用することで独自のツールを定義することができます 以下はツールを設定した Agentの例です。 from smolagents import CodeAgent, LiteLLMModel, DuckDuckGoSearchTool, model = LiteLLMModel(model_id="openai/gpt-4o") # デフォルトで用意されているWeb検索のツール search_tool = DuckDuckGoSearchTool() # Hugging Face Hub上で、指定されたタスクの最もダウンロードされたモデルを返すツールを作成 @tool def model_download_tool(task: str) -> str: """ This is a tool that returns the most downloaded model of a given task on the Hugging Face Hub. It returns the name of the checkpoint. Args: task: The task for which to get the download count. """ most_downloaded_model = next(iter(list_models(filter=task, sort="downloads", direction=-1))) return most_downloaded_model.id agent = CodeAgent( tools=[search_tool, model_download_tool], # Web検索と上記で作成したオリジナルツールを追加 model=model, ) なお、こちらはtoolsを設定する例を示すためのサンプルコードなので、これにより何かの問題を解決する、といったことは想定していないのでご了承ください。 おわりに smolagentsのCodeAgentは、LLMの持つコード生成能力を直接利用することで、従来の Agent Frameworkでは難しかった柔軟なタスクの実行を可能にします。Pythonコードを生成・実行するというアプローチは、LLMの訓練データとの親和性が高く、優れたパフォーマンスを実現します。 一方で、セキュリティやリソース管理といった実運用上の課題も存在します。安全な実行環境の構築や適切なモジュールの制限など、ガイドラインに従った運用が求められます。 smolagentsは、シンプルでありながら強力なエージェントを構築するための有用なツールだと思います。興味を持たれた方は、ぜひ一度試してみてください。 最後までお読みいただき、ありがとうございました! 投稿 Hugging Faceの新しいAgent Framework「smolagents」とCodeAgentの活用法 は 株式会社AI Shift に最初に表示されました。
アバター
DALLE-3で生成 1. はじめに こんにちは、AI Shiftの友松です。この記事は AI Shift Advent Calendar 2024  の最終日、25日目の記事です。 AI ShiftのTechBlogは 2019年の12月24日に開設 し、そして、昨日12月24日をもちまして5周年を迎えました。毎年恒例となっておりますが、アドベントカレンダーの最終日を使わせていただき、1年間の外部アウトプットの振り返りを実施します。 過去の振り返り記事は以下のとおりです。 2020年 2021年 2022年 2023年 2. Tech Blog まずはTech Blogに関して振り返ります。 2.1 投稿件数の推移 昨年までは主にAIチームが主体となり、AI Shiftのコーポレートサイト上でTech Blogを執筆していましたが、2023年後半からはDevチームもZennにてTech Blogを開始しました。 AI Shift コーポレートサイト TechBlog AI Shift Zenn TechBlog 今年は全体で84件と昨年から大幅に件数を伸ばすことができました。また、これまでAIチームでメインで書いていた機械学習やAIに関連する記事に加えて、Devチームによるソフトウェアエンジニアリングに関する話題も増えて内容に厚みが増しました。 2.2 2024年執筆の記事ランキング 今年掲載された記事の中から、コーポレートサイトではアクセス数が多かったもの、Zennではいいねによる反応が多かったもののトップ5をそれぞれ紹介します。 コーポレートサイト 順位 タイトル 1 高効率のLLM学習手法ReFTを試してみる 2 kotoba-whisper, ReazonSpeech, Nue-ASRの文字起こし性能を比較してみた 3 OpenAI ChatGPTの出力の尤もらしさをトークンレベルで可視化する 4 airllmを使ってT4で70B LLMの推論を実行してみる 5 spacy-llmで色々なNLPタスクをzero-shotで解いてみる Zenn 順位 タイトル 1 Honoを使い倒したい2024 2 [React] 新規作成画面と編集画面の実装で気をつけていること 3 Reactが初回マウントされるまでの仕組みを理解する 4 新規サービスのバックエンド開発で3ヶ月経ったので、試した技術や取り組みをまとめてみた 5 TanStack Router(& Query)はSPA開発で求めていたものだった【Reactのルーティングとデータ取得】 2.3 Advent Calendar 今年で4年目となる Advent Calendar 2024 も無事1日も途切れること無く完遂することができました。今年はDevチーム、AIチーム、そしてリスキリングのコンテンツチームも加わって開催できました。どの記事も読み応えのある記事なので、ぜひ改めてご一読いただけると嬉しいです。 過去開催のAdvent Calendarのリンクは以下のとおりです。 AI Shift Advent Calendar 2021 AI Shift Advent Calendar 2022 AI Shift Advent Calendar 2023 3. 学会 今年は事業的なフェーズもあり、投稿件数自体は昨年までと比べると5件と減っていますが、取り組み内容のシャープ化や、国際学会への投稿へのチャレンジもできました。(まだ未発表ですが、国際学会の採択リリースも近々出せればと思います。) 2025年もすでに言語処理学会に向けた論文執筆を進め、積極的に学会投稿もチャレンジできればと思います。 学会 タイトル 著者 言語処理学会第30回年次大会(NLP2024) RAGにおける自己認識的不確実性の評価 ◯二宮大空, 戸田隆道(AI Shift) 同上   RAGにおけるLLMの学習と評価:FAQタスクへの応用 ◯長澤春希, 戸田隆道(AI Shift) 同上 LCTG Bench: 日本語LLMの制御性ベンチマークの構築 ◯栗原健太郎(AI Shift), 三田雅人, 張培楠, 佐々木翔大, 石上亮介 (サイバーエージェント), 岡崎直観(東京工業大学) 第15回対話システムシンポジウム(2024) リアルタイム性と柔軟性を兼ね備えた音声対話システムのための軽量かつ高速な処理手法の検討 ◯大竹真太(AI Shift) 同上 模範的なクレーム対応のモデル化のための二話者間の通話分析 ◯東佑樹, 干飯啓太(AI Shift) 学会発表・参加に関するブログも執筆しておりますのでぜひご参照ください。 言語処理学会第30回年次大会(NLP2024) でAI Shiftから3件の発表を行います 言語処理学会第30回年次大会(NLP2024) 発表報告 日本音響学会 第151回(2024年春季)研究発表会 参加報告 YANS2024参加報告 第15回対話システムシンポジウムで発表します 【AI Shift Advent Calendar 2024】第15回対話システムシンポジウム参加報告 4. 登壇&冊子掲載 今年は外部登壇も積極的に行うことができました。弊社社員が登壇した内容をまとめました。一部資料や動画を公開しているものもありますので、ぜひ御覧ください。 発表日 登壇場所 タイトル 2月16日 React LT会/都内の会場決定!HRBrain 東京オフィスにて開催! TanStack RouterはSPA開発の選択肢になるか (+TanStack Query) 3月27日 フロントエンドTechCafe〜春のBun祭りLT会〜 BunがCommonJSをサポートする理由 4月18日 Muddy Web #8 ~Special Edition~ 新規開発と並走したリファクタリング戦略 5月11日 TSKaigi 2024 TanStack Routerで型安全かつ効率的なルーティングを実現 5月23-24日 Oracle Developer Days 2024 サイバーエージェントの生成AIへの取り組み 5月24日 初夏のJavaScript祭り @株式会社タイミー&オンラインハイブリッド TanStackで効率的なルーティングとデータ取得を実現 6月22日 Scrum Fest Osaka 2024 GPT - 振り返りフレームワークKPTをちょっとKAIZENしてちょうど良いフレームワークに。 6月25日 めぐろLT #16 「みんなのオーバーエンジニアリング供養」 マインクラフトのコマンド圧縮の効率化を考えたら、40年前の論文のアルゴリズムを実装することになった話 7月2日 W&Bファウンダー来日 & Nejumiリーダーボード3 - W&Bミートアップ #14 in 東京 LCTG Benchの設計思想と今後の期待 8月4日 若手エンジニアLT & 交流会 スプリントレトロスペクティブ 目的・やり方・勘所 同上 React19のAction APIによって変わる非同期状態管理のあり方 10月29-30日 CyberAgent Developer Conference 2024 サイバーエージェントにおける生成AIのリスキリング施策の取り組み 同上 新卒1年目が向き合う生成AI事業の開発を加速させる技術選定 同上 Argo Workflowsで構築するLLMを活用したコールセンターの自動要約プロダクトの立ち上げ 12月 月間コールセンタージャパン 2024年12月号 ACW省力化とCX/CS改善を一挙に実現?!生成AI活用の“一丁目一番地” 5. コンペ参加 2024年8月31日まで開催されたNishika株式会社様によるコンペ「【音声認識コンペ】文学作品の音声を 文字起こししよう!」に弊社チームで参加し、2位にランクインすることができました。コンペ結果の詳細に関してTech Blogにまとめておりますのでぜひ御覧ください。 Nishika音声認識コンペに参加して2位になりました!🎉 6. 終わりに 今回は、AI Shift Tech Blogが5周年を迎え、2024年のTech Blogおよび学会発表について振り返ってみました。また、本日でAI Shift Advent Calendar 2024の最終日を完走することができました。ありがとうございました。 2024年も弊社Tech Blogをお読みいただきありがとうございました。この記事が年内の最終記事となります。 AI ShiftのTechBlogの投稿件数は累積で308件となりました。年末年始でお時間有りましたらぜひ人気記事を中心にご覧いただけますと幸いです。 AI ShiftのTechBlogや対外発表に関する告知は 弊社の公式X で発信しておりますので、ぜひフォローお願いいたします。 皆さんよいクリスマス/年末をお過ごしください。 投稿 【AI Shift Advent Calendar 2024】2024年のTech Blog/対外発表の振り返り は 株式会社AI Shift に最初に表示されました。
アバター
はじめに この記事は AI Shift Advent Calendar 2024 の24日目の記事です。 初めまして! 早稲田大学修士2年生の村田栄樹です。 11月と12月の2ヶ月間、株式会社AI Shiftで ML/DS として内定者バイトをしています。 大学では普段、自然言語処理の研究をしていて、今回のバイト期間中も自然言語処理周りのタスク(特にRAG関連)に取り組みました。 せっかくバイト期間中にアドベントカレンダーがあるので、バイト中に取り組んだタスクや感想をまとめさせていただきます! タスクの内容について技術的な内容を含めて書いた後に、内定者バイトを通じての感想をまとめたいと思います。 クリスマスイブ担当という大役( タスクが終わらないことを恐れて後半担当になりました )ですが、RAGおよびAI Shiftに興味がある人にとって読み応えのある記事となっていることを祈っています。 タスク: クエリ書き換え 背景 RAG(Retrieval Augmented Generation)自体の詳しい解説は他の記事に譲りますが、再学習をせずに望んだ知識をLLMに与えることができるため、注目度の高い技術となっています。 AI ShiftでもRAGを用いたプロダクトを開発しており、その精度を改善することが私のタスクでした。 一般的なRAGの問題点として、以下のようなことが挙げられます。 ユーザクエリと検索対象のドキュメントが意味空間として近くない BM25などの表層検索の場合、タイポや類語に弱い 検索された結果やそれを基にした生成に対する評価が難しい 特に1つ目と2つ目の問題点に対して取り組むことにしました。 具体的には、 クエリを書き換えることによりドキュメントとの意味的な距離を近づけ、検索精度を改善する 手法の検証および実装です。 関連研究 クエリとドキュメントの意味的な距離を近づける手法は、 埋め込みモデルの訓練時に、クエリとドキュメントのようなペアを正例として入れることで、対応するクエリの埋め込みをドキュメントの埋め込みに近づける クエリもしくはドキュメントをテキストとして書き換え、表層的に意味を近づける の2つのタイプに大別されます。 1つ目のタイプに分類されるようなモデルとしては、以下のようなものが知られています。 Multilingual-E5 多言語に対応した事前学習済み埋め込みモデル 学習時に"query:"と"passage:"という接頭辞を使用して、意味空間をアライメントする Ruri 日本語に特化した事前学習済み埋め込みモデル E5と同様に、学習時に"クエリ:"と"文章:"という接頭辞を使用する Vertex AI エンベディングモデル タスクタイプと呼ばれるパラメータを指定することにより、タスクに合わせて埋め込みベクトルの品質を向上させる 2024年12月現在、4つの問題設定に対して7種類のタスクタイプが用意されている 図引用: https://cloud.google.com/vertex-ai/generative-ai/docs/embeddings/task-types?hl=ja 2つ目の書き換えの例としては、以下のようなものがあります。 Rewrite-Retrieve-Read RAG従来のRetrieve-then-Readではなく、クエリを書き換えたのちに検索をすることを提案 以下のようなIn-Context Learningで実現 Think step by step to answer this question, and provide search engine queries for knowledge that you need. Split the queries with ’;’ and end the queries with ’**’. Question: What profession does Nicholas Ray and Elia Kazan have in common? Answer: Nicholas Ray profession;Elia Kazan profession** ... Question: {x} Answer: 前述した問題点の中では1つ目(ユーザクエリと検索対象のドキュメントが意味空間として近くない)というより2つ目の問題点(BM25などの表層検索の場合、タイポや類語に弱い)に対する手法ですが、書き換えの手法ということでここで紹介しました HyDE (Hypothetical Document Embeddings) LLMでクエリに対する擬似的な回答を生成することで表層的な意味を近づける 図引用: https://arxiv.org/abs/2212.10496 改めて私はクエリ書き換えによってRAGの精度を改善する2つ目のタイプについて取り組むことにしました。理由としては、(1)クエリの書き換えはさまざまなユースケースに対して汎用性が高いと考えたことと、(2)プロダクトではAPI経由で埋め込みモデルを使用しているため介入の余地がないことがありました。 (1)は、対象ドキュメント内の製品名の変更(例:「Tポイント」→「Vポイント」)や異なる言語のクエリなど個別のユースケースに同じ枠組みで対応できるという利点です。 精度検証実験(通常データ) タスク決定後、クエリの書き換えが実際の業務で使用されるデータでどの程度有効かを確かめるために精度検証実験を実施しました。 手法 関連研究のサーベイを参考に以下の3つの手法を検証します。いくつかのモデルを事前実験で検証しましたが、費用面と性能を鑑みてすべて Open AI の GPT-4o-mini を用いて書き換えを実行します。また、各手法に対して、 ABEMAの料金はいくらですか? というクエリに対する書き換え例を示します。 paraphrase ( Para ) 質問を異なる単語を含むように言い換える プロンプト: 与えられた質問を意味的に同じ内容で使用されている単語が異なるものに書き換えてください。ただし、書き換えた質問のみを出力してください。 書き換え例: ABEMA TVの月額は? keyword ( Key ) 質問からキーワードを取り出して検索エンジン-likeなクエリにする プロンプト: 全文検索のためのキーワードを抽出してください。ただし、キーワードのみを半角空白区切りで出力してください。 書き換え例: ABEMA 料金 pseudo_answer ( HyDE ) 疑似的な回答を生成する。クエリ空間からドキュメント空間に近づけることでヒットしやすくする。 プロンプト: 与えられた質問への疑似的な回答を生成してください。回答はFAQのページのコンテンツのように書いて、回答部分のみを100字程度で簡潔に出力してください。 書き換え例: ABEMA TVは月額580円で利用できます。また、無料で利用できるコンテンツも用意されています。 また、リトリーブの設定は以下の通りです。 入力: 元のクエリと言い換えられたクエリ(1つ以上) それぞれのクエリについてリトリーブ 指定されたドキュメント数(10件)に50を加えた個数を検索 検索手法 vector : 埋め込みベクトルのコサイン類似度 surface : BM25 それぞれの検索結果の逆順位の和でソート 指定された文書数(10件)分のドキュメントを検索結果とする データセット 以下のような設定のデータセットを使用しました。 検索対象のドキュメント: 社内の実データ 精度検証用の3つ組 LLMで検索対象の実データを入力として3つ組{Query, Gold Answer, Gold Document}を生成 Query: RAGシステムに入力する質問文 Gold Answer: 対象ドキュメントを入力した状態でLLMが生成したQueryに対する回答 Gold Document: 上記2つを作成するときに入力したドキュメント 評価指標 RAGシステムのうち、検索パートと生成パートそれぞれを以下のように評価します。 検索パート HR@k (Hit Rate at k) : k位以内にGold Documentがある割合 MRR (Mean Recipirocal Rank) : 順位の逆数の平均 追加費用 : クエリ書き換えにかかったコスト(米ドル/クエリ) 生成パート LLMEval : GPT-4oによる5段階評価 (Query, Gold Answer, システムの回答を入力) 結果 検索手法ごとの結果を表1,2に示します。 書き換えを行わないベースラインと、各手法をそれぞれ用いた場合、3つの手法を同時に用いた場合の5つの設定について表示しています。 表1. vector検索での結果 Para Key HyDE HR@1 HR@3 HR@5 HR@10 MRR 追加費用 LLMEval ❌ ❌ ❌ 0.444 0.622 0.697 0.763 0.545 0 4.145 ✅ ❌ ❌ 0.419 0.602 0.693 0.759 0.527 3.17E-05 4.174 ❌ ✅ ❌ 0.444 0.651 0.714 0.776 0.556 2.40E-05 4.141 ❌ ❌ ✅ 0.444 0.639 0.705 0.780 0.555 6.87E-05 4.178 ✅ ✅ ✅ 0.452 0.656 0.710 0.772 0.561 9.71E-05 4.220 vector 検索では Keyword手法 および HyDE を使用した場合に検索精度が向上し、すべての手法を同時に実行した時に最も良い結果を得ました。 LLMEval でもすべての手法を同時に実行した場合が最も高得点です。 高性能なLLMをAPI経由で簡単に使える近年では、やはりRAGのボトルネックは検索パートにあるな、と感じています。 一方で、Paraphrase 手法では精度が低下しています。これはクエリが Gold Document から作成されており、一字一句同じ文言がクエリと Gold Document に含まれているため書き換えによって精度が低下したと考えられます。実クエリでは曖昧なものやタイポ、類義語などが含まれるため改善する可能性があると考えています。 またコスト面からも、1クエリあたり最大で約$0.0001(~¥0.015)と十分安価に精度向上が実現できたと言えます。 さらに結果には載せていませんが、精度検証段階では並列化していなかったため、書き換え手法を増やすにつれて線形に生成が始まるまでのレイテンシが増加するという問題がありました。本番実装の段階では非同期処理により、書き換え手法の個数によらず一定のレイテンシを実現することができました。 表2. surface検索での結果 Para Key HyDE HR@1 HR@3 HR@5 HR@10 MRR 追加費用 LLMEval ❌ ❌ ❌ 0.315 0.481 0.581 0.664 0.419 0 4.054 ✅ ❌ ❌ 0.295 0.423 0.531 0.631 0.387 3.17E-05 4.058 ❌ ✅ ❌ 0.332 0.498 0.602 0.689 0.440 2.41E-05 4.141 ❌ ❌ ✅ 0.224 0.407 0.502 0.643 0.340 6.87E-05 4.058 ✅ ✅ ✅ 0.295 0.461 0.535 0.685 0.405 9.71E-05 4.071 surface 検索では Keyword 手法のみで改善が見られました。 これは(stop wordなどがあるにしても)、クエリ側で余分な情報を減らすことで表層検索によってヒットしやすい状況を作ることができたためだと考えています。 ただし、surface 検索ではベースラインを含め全体的に精度が低く、文埋め込みの性能の高さを痛感しました。 また、Keyword 以外の手法は精度が悪化しており、書き換え手法と検索手法の組み合わせは慎重に選択する必要も感じています。 精度検証実験(低品質データ) 通常データでの精度検証実験では疑似データにおいて、クエリ書き換えによって検索精度がやや改善することを確かめました。 一方で実データを眺める中で、実際のプロダクトが受け付けるユーザのクエリはタイポが含まれたり完全な文でなかったりと疑似データとは性質が異なることに気づきました。 そこで質の低いクエリを含むデータに対して、クエリ書き換えがベースラインと比較してどのような挙動をするかを検証します。 実験設定 後述するクエリを除いて、精度検証実験(通常データ)と同様の設定で実験します。 また、検索手法によって傾向に差が見られなかったため、vector 検索の結果のみを示します。 質の低いクエリの準備 以下の3つのタイプのミスが含まれるようなクエリを元のデータセットをベースとして作成します。 作成にはLLMを使用しました。 タイポ : 製品の名前などの固有名詞にタイポが含まれるもの 文法ミス : 助詞や語順が誤っているもの キーワード羅列 : 完全な文ではなく、検索エンジンに入力するようなキーワードの羅列 結果 上記の3つのタイプのミスが含まれるようなクエリに対する結果を表3~5にそれぞれ示します。 表3. タイポを含むクエリに対する結果 Para Key HyDE HR@1 HR@3 HR@5 HR@10 MRR 追加費用 LLMEval ❌ ❌ ❌ 0.390 0.598 0.660 0.734 0.503 0 4.112 ✅ ❌ ❌ 0.398 0.593 0.651 0.734 0.506 3.22E-05 4.095 ❌ ✅ ❌ 0.423 0.614 0.685 0.755 0.528 2.49E-05 4.129 ❌ ❌ ✅ 0.411 0.610 0.664 0.739 0.520 6.84E-05 4.137 ✅ ✅ ✅ 0.407 0.631 0.672 0.759 0.526 9.70E-05 4.154 表4. 文法ミスを含むクエリに対する結果 Para Key HyDE HR@1 HR@3 HR@5 HR@10 MRR 追加費用 LLMEval ❌ ❌ ❌ 0.427 0.618 0.705 0.763 0.536 0 4.174 ✅ ❌ ❌ 0.419 0.614 0.697 0.759 0.532 3.23E-05 4.162 ❌ ✅ ❌ 0.436 0.647 0.714 0.772 0.551 2.44E-05 4.191 ❌ ❌ ✅ 0.440 0.656 0.693 0.772 0.553 6.89E-05 4.203 ✅ ✅ ✅ 0.440 0.656 0.685 0.780 0.556 9.76E-05 4.278 表5. キーワード羅列クエリに対する結果 Para Key HyDE HR@1 HR@3 HR@5 HR@10 MRR 追加費用 LLMEval ❌ ❌ ❌ 0.465 0.672 0.730 0.788 0.575 0 4.225 ✅ ❌ ❌ 0.440 0.651 0.722 0.780 0.555 2.35E-05 4.216 ❌ ✅ ❌ 0.469 0.668 0.730 0.788 0.577 1.97E-05 4.212 ❌ ❌ ✅ 0.456 0.656 0.714 0.784 0.563 6.55E-05 4.245 ✅ ✅ ✅ 0.461 0.664 0.722 0.793 0.573 8.88E-05 4.232 まず通常のデータを使用した場合(表2)と比較して、ベースラインの検索精度が低下していることがわかります(キーワード羅列クエリを除く)。 MRRについてみると、タイポを含む場合が0.042ポイント、文法ミスを含む場合が0.009ポイント減少しています。 クエリ書き換えでも、通常のデータを使用した場合と比較すると検索精度が下がっているものの、同じデータに対してはMRRやHR@10においてベースラインをおよそ上回っています。 クエリ書き換え特有の頑健性のようなものは観測できませんでしたが、クエリ書き換えはミスを含むデータでも同様に有効であることを確認できました。 キーワード羅列クエリでは、通常のデータを使用した場合(表2)と比較して、全体的に検索精度が上がるという結果になりました。ベクトル検索を使用する場合でも、検索エンジンに入れるようなキーワードの羅列の方が高精度だというのは個人的には意外でした。この傾向は表2で、Keywordへのクエリ書き換えがベースラインより良い結果であることとも一致しています。 タスクまとめ 以上の精度検証実験をもって、クエリ書き換えがある程度有効な手法であることを確認できました。 精度検証後は本番プロダクトへ実装し、このタスクは完了となりました。 感想 ここからは技術的な話ではなく、個人としてこのバイト期間中に感じたこと・学んだことを書き連ねたいと思います。 配属→タスク決定 今回内定者バイトとして AI Shift を志望した動機として、実際のプロダクトの中でのRAGを触ってみたいというものがありました。 RAGが今後欠かせない技術であるともてはやされる中で、普段の研究ではRAGを扱う機会は少なく、たまに読むRAGに関する論文での知識のみがある状態でした。 実際に配属された後は、私の方でかなり自由にタスク案の選定をさせていただきました。 その後、社員の方とタスク案の中からプロダクトの現状などと照らし合わせて実際に取り組むタスクを決定しました。 このプロセスの中で、(わかっていたつもりではあったのですが)実際の課題を考慮してタスクの優先度をつけることや、 そのタスクによって顧客にどんなメリットを与えることができるか、といった視点が足りていないことを感じました。 この辺りはエンジニアとして(社会人として)今後身につけたいです。 精度検証→実装 精度検証については、普段の研究でやっていることとそこまで遠くなく、比較的順調に進めることができました。 一方で本番実装では、開発経験の浅さから学ぶべきことがたくさん見えたように感じます。 内定者バイトの立場としてプロダクト実装をする上で、そもそもレポジトリのキャッチアップから始めなければならないという部分が大変なポイントかなと思います。 Github Copilotでそこまで時間は取られなかったものの、自らのコードリーディングを高めたいと感じています。 また、複数リトリーブするための非同期処理やテストコードの実装など、今まで触れてこなかった (避けてきた) 部分も大変なポイントでした。 その中でも社員の方の手厚いサポートもあり、本番実装をやり切ることができました。 メンターの方はもちろん、チーム全体としてかなり質問をしやすい雰囲気があるように感じました。 そのほか クエリ書き換えのタスクの他にも、チーム全体で取り組んでいるサーベイタスクに参加させていただきました。 サーベイタスクでは全体の中で担当分の調査を進める中で、自分が理解したことを他のメンバーにどのように伝えるか、どこを切り出して伝えるかという普段の研究では経験できないサーベイの難しさを感じました。 AI Shiftでは新メンバーへの会社全体の概要のキャッチアップの機会が用意されており、会社としてどのような方向で何をしているのか(自分の配属されたチーム以外も)理解が深まったことはありがたかったです。 また、期間中はランチやイベントなど幅広く経験させていただき、内定者バイトならではの理解もできたように感じます。 おわりに 本記事では、RAGの改善に取り組んだ内定者バイトの話を読んでいただきました。 技術的だけではなく、内定者として考えていたこともまとめたつもりです。 ここまで読んでいただきありがとうございました! 明日はアドベントカレンダーの最終日となります。 ぜひご覧ください!! 投稿 【AI Shift Advent Calendar 2024】クエリ書き換えでRAGの精度を改善してみた は 株式会社AI Shift に最初に表示されました。
アバター
はじめに こんにちは、AI Shift のAIチームに在籍している長澤 ( @sp_1999N ) です! この記事は AI Shift Advent Calendar 2024 の20日目の記事になります。 今回は比較的新しく登場した PydanticAI を使って、LLM の実運用におけるDependency Injection (DI, 依存性の注入) を検討してみたいと思います。 DI とは何かというトピックについても簡単に解説していますので、LLM を使用したアプリケーションを開発している方はどなたでもお読みいただけるものになっているかと思います! (この記事の内容は2024/12時点のものになります。PydanticAI は現時点で開発が盛んなようなので、最新の情報は公式ドキュメントをご参照いただければと思います。) PydanticAI とは PydanticAI は Python で利用できる Agent Framework です。Python で型安全な実装を行おうとした際に利用される Pydantic の系譜を受け継いだものになっています。 Similarly, virtually every agent framework and LLM library in Python uses Pydantic, yet when we began to use LLMs in  Pydantic Logfire , we couldn't find anything that gave us the same feeling. We built PydanticAI with one simple aim: to bring that FastAPI feeling to GenAI app development. こちらのコメントにもあるように、「FastAPI での直感的で快適な使用感を生成AIアプリケーションにもたらすこと」を目的として開発されています。API で利用可能な LLM をプロダクションレベルで運用する際の利用が想定されています。 PydanticAI の特徴としては以下のものが挙げられています。 Built by the team behind Pydantic (the validation layer of the OpenAI SDK, the Anthropic SDK, LangChain, LlamaIndex, AutoGPT, Transformers, CrewAI, Instructor and many more) Model-agnostic  — currently OpenAI, Anthropic, Gemini, Ollama, Groq, and Mistral are supported, and there is a simple interface to implement support for other models. Type-safe Control flow and agent composition is done with vanilla Python, allowing you to make use of the same Python development best practices you'd use in any other (non-AI) project Structured response  validation with Pydantic Streamed responses , including validation of streamed structured responses with Pydantic Novel, type-safe  dependency injection system , useful for testing and eval-driven iterative development Logfire integration  for debugging and monitoring the performance and general behavior of your LLM-powered application 紹介されている特徴を見ると、Pydantic の恩恵を受けた型安全な開発を LLM provider に依存せずに実装できることが伺えます。 また従来の Pydantic と比較して、 dependency injection system が新しい機能として紹介されています。 今回の記事ではこの DI 機能を紐解きながら、LLM の実運用における DI の実装例をご紹介します。 Dependency Injection とは 本題に突入する前に、DI についても簡単に触れておきます。 ソフトウェア開発の経験が豊富な場合は聞き馴染みのある言葉かも知れませんが、そうでない場合はなかなかにとっつきにくいトピックだと思います。(既に馴染みある方は次のセクションに進んでいただいて問題ありません。) Dependency Injection とは文字通り「プログラミングにおける依存性を (外部から) 注入する分離可能な形での管理」を実現するためのものです。うまく依存性を切り離せると、単体機能の管理・テストが容易になるなどの嬉しさがあります。ただこれだけでは分かりにくいので、具体例を交えながら解説します。 例えば、LLM でのメール校正・自動返信サービスを作るような場合を考えます。 全ての処理を分離せずに書くことによる課題 この時、全ての処理を1つの関数で実装したプログラムを作成しても良いかも知れませんが、どの機能がきちんと動いているのかのテストが大変になります。 また「校正処理を LLM ではなくルールベースのロジックに置き換えたい」や「送信処理で使用していたライブラリなどを大きく変更したい」などの場面を考えても、上記のような場合では改修作業は骨が折れそうな予感がします。 モジュールへの分割 そこで次に出てくるのがモジュールベースでの実装になります。ここで考えるサービスは例えば「メールの受信・前処理」「LLM によるメールの校正」「校正内容を反映したメールの自動返信」などに分けることができます。 分離したモジュールごとにそれぞれの責任範囲を定義することで、全体の構造がより理解しやすく、保守しやすい設計になります。このような設計は 単一責任原則 (Single Responsibility Principle) に基づいています。つまり、各モジュールやコンポーネントはその役割を1つのことに限定し、それぞれが具体的なタスクを担当します。 モジュール分割した後に残る課題 しかし、モジュールベースで実装した場合でも、各モジュールの「依存関係」の管理が課題として残ります。たとえば、「LLM によるメール校正」モジュールが特定のAPIクライアントに直接依存していると、将来的に校正ロジックを別のコンポーネントに置き換えたい場合や、異なるAPIに切り替えたい場合に、依存コードを大幅に書き直す必要が出てきます。 このような課題に対して、Dependency Injection が役に立ちます。 DI の簡単な例 例えば上記の例について DI の思想に基づいた実装例とそうでない例を簡単に比較してみます。 まずは全ての処理をまとめた例になります。上述していたモジュールが全て1つの関数に集約されてしまっています。 class EmailProcessingService: def process_email(self, email): prompt = "Please proofread the given email." # 特定の LLM ライブラリに依存 llm = SpecificLLMLibrary() corrected_email = llm.generate_response(prompt, email) # 特定のSMTPライブラリに依存 smtp_client = SpecificSMTPClient() smtp_client.send(corrected_email) メタ的に process_email 関数を見ると、この関数は「メールを入力したら自動で校正されて送信されるサービス」としての責務を果たしてくれれば十分であり、どのロジックでメールが校正されているかは関心の外側 (また別の話) になります。 しかしこのままだと、例えば校正処理をルールベースのロジックに変更しようとすると process_email の関数を大きく改修する必要があります。 ここで、依存性を簡単に注入した例を見てみます。 class EmailProcessingService: def __init__(self, llm, prompt, mail_client): # 依存性を外部から注入 self.llm = llm self.prompt = prompt self.mail_client = mail_client def process_email(self, email): corrected_email = self.llm.generate_response(self.prompt, email) self.mail_client.send(corrected_email) 各モジュールを引数レベルに引き上げただけに見えるかも知れませんが、 process_email の関数内で LLM が何のモデルであるか、メール送信に使っているライブラリが何であるかを意識せずに良くなっていることが分かります。(ただし、それぞれのモジュールにおける特定の処理の呼び出し方法 = インターフェースは他で定義しておく必要はあります。) そしてさらにこの関数のテストケースを考えてみます。 # Mock の定義 class MockLLM: def generate_response(self, prompt, email): return f"Prompt: {prompt}, Corrected: {email}" class MockMailClient: def send(self, email): print(f"Mock send: {email}") # テスト用の依存性注入 mock_llm = MockLLM() mock_mail_client = MockMailClient() email_service = EmailProcessingService(mock_llm, mock_mail_client) # テストの実行 email_service.process_email("Test email") LLM やメール送信部分を簡単にモックとして定義しているだけですが process_email 関数が動くかどうかのテストができるようになっています。一番最初の例ではそれぞれのモジュール全てが完成しないとテストできない (依存度が高い) 状態でしたが、うまく依存性を切り離すことで単体機能の管理やテスト等がしやすくなっていることが分かります。 前置きがかなり長くなってしまいましたが、以上が Dependency Injection の簡単なご紹介になります。 PydanticAI の基本概念 PydanticAI における DI の説明のため、まずは PydanticAI における基本を押さえておきます。 PydanticAI では Agent が LLM とのインタラクションにおける主要なインターフェースになります。 そしてこの Agent を中心に以下のコンポーネントを使用します。(言い換えると Agent は以下の要素のコンテナとして機能します。) system prompt LLM に対するいわゆるシステムプロンプト インスタンス化の際や、デコレータとして後からの引き渡しが可能です function tool LLM が外部情報等にアクセスするために呼び出すツール (関数) インスタンス化の際や、デコレータとして後からの引き渡しが可能です result type LLM のレスポンス型を定義したもので、インスタンス化の時のみ指定可能 dependencies 上記の system_prompt, tool そして result validators (result type に対する validation) のそれぞれについて Agent からのアクセスを提供します 上記のシステムプロンプトやツールは DI を利用して動的に設定することも可能ですが、インスタンス化の際に指定することも可能です。 Agent のコンストラクタ は以下のとおりです。 __init__( model: Model | KnownModelName | None = None, *, result_type: type[ResultData] = str, system_prompt: str | Sequence[str] = (), deps_type: type[AgentDeps] = NoneType, name: str | None = None, model_settings: ModelSettings | None = None, retries: int = 1, result_tool_name: str = "final_result", result_tool_description: str | None = None, result_retries: int | None = None, tools: Sequence[ Tool[AgentDeps] | ToolFuncEither[AgentDeps, ...] ] = (), defer_model_check: bool = False, end_strategy: EndStrategy = "early" ) PydanticAI での DI それでは本題に入っていきます。繰り返しになりますが、PydanticAI では dependency injection system として system prompts , tools そして result validators の大きく3つが提供されています。 つまり、システムプロンプトやツール、返り値の型設定などを依存関係があるものとして外部から注入できるようになっています。 PydanticAI において依存関係にあるものは、 RunContext 型を通して Agent からアクセスされます。 この解説のため、ドキュメントに掲載されている例 ( dice_game.py ) を少し改変して説明します。(改変した部分はコメントとして明記します。) import os import random import argparse from dotenv import load_dotenv from pydantic_ai import Agent, RunContext load_dotenv() # 改変: 環境変数の設定 # Define the AI agent with the game logic agent = Agent( 'openai:gpt-4o', deps_type=str, # DIの型を指定(今回はstr) system_prompt=( "You're a dice game, you should roll the die and see if the number " "you get back matches the user's guess. If so, tell them they're a winner. " "Use the player's name in the response." ), ) @agent.tool_plain def roll_dice() -> str: """Roll a six-sided die and return the result.""" return str(random.randint(1, 6)) @agent.tool def get_player_name(ctx: RunContext[str]) -> str: """Get the player's name.""" return ctx.deps def main(): parser = argparse.ArgumentParser(description="Dice Guessing Game") parser.add_argument("--guess", type=int, help="Your guess (number between 1 and 6)") parser.add_argument("--name", type=str, required=True, help="Your name") args = parser.parse_args() # Agentの実行: DIとしてargsで受け取った値を注入 dice_result = agent.run_sync(f'My guess is {args.guess}', deps=args.name) print(dice_result.data) if __name__ == "__main__": main() > python dice_game.py --guess 3 --name "Ben" The roll came up 5. Sorry, Ben, your guess was 3. Better luck next time! この例は、ダイスロールの出目を予想するプログラムになります。ここで注目して頂きたいのは2つの関数 roll_dice() と get_player_name(ctx: RunContext[str]) です。 Agent を定義した後で2つのツールを提供しているのですが、前者は @agent.tool_plain (依存性の注入が不要なツールの引き渡し)、後者は @agent.tool (依存性の注入が必要なツールの引き渡し) のデコレータを使用しており、かつ後者は引数に ctx: RunContext[str] を設定しています。 roll_dice() の関数はランダムな数値を返却するのみになっており、特定の外部情報は必要としていません。これと比べ get_player_name(ctx: RunContext[str]) はプログラム引数で渡される name の情報が必要になります。このため、引数に RunContext を設定し依存性を注入しています。 混乱を避けるため、ここまでの内容を整理します。 @agent.hoge のデコレータを使用することで Agent が利用する hoge (tool や prompt) を設定することができます。 もしこの設定において動的な処理 (依存性の注入が必要な場合) は RunContext を使って処理する形になります。逆に静的な処理 (依存性の注入が不要な場合) であれば RunContext を使用する必要はありません。 Agent 自体を DI する もう1つ面白い例として、Agent 自体の DI をご紹介します。 PydanticAI の dependencies では python の任意の型を指定できます。つまり、 Agent 型も DI として設定できる値になります。 実際に Agents as dependencies of other Agents のセクションでこの具体例が紹介されています。 ここではこのプログラムを参考に、独自の "冗談-1 グランプリ" を開催してみようと思います。 審査員として定義する judge_agent が主導してプログラムを実行します。 具体的には、ユーザーから冗談-1グランプリの開催の依頼を受け、審査員 Agent ( judge_agent )がお笑い芸人 Agent ( factory_agent )に冗談を生成させます。 そして芸人 Agent が生成した冗談に対して、審査員 Agent に評価をしてもらおうと思います。 from dotenv import load_dotenv from dataclasses import dataclass from pydantic_ai import Agent, RunContext load_dotenv() # AgentをDIするための型を定義 @dataclass class MultiAgentDeps: factory_agent: Agent[None, list[str]] judge_agent = Agent( 'openai:gpt-4o', deps_type=MultiAgentDeps, system_prompt=( '"joke_factory" を使用して複数の冗談を生成してください。' 'その後で冗談-1グランプリの審査員として、それぞれの冗談に対して順位とコメントを付けてください。' ), ) # 仮想のお笑い芸人として振る舞うAgentを作成 factory_agent = Agent('gemini-1.5-pro', result_type=list[str], ) # お笑い芸人に冗談を生成させるための関数 @judge_agent.tool async def joke_factory(ctx: RunContext[MultiAgentDeps], count: int) -> str: print(f"{count=}") r = await ctx.deps.factory_agent.run(f'冗談を{count}個生成してください。') return '\n'.join(r.data) # 審査員は、グランプリの開催依頼をもとに、芸人に冗談を生成させる result = judge_agent.run_sync('冗談-1グランプリの開催をお願いします。', deps=MultiAgentDeps(factory_agent)) print(result.data) > python depends_agent.py count=3 冗談を以下に示しますので、それぞれに順位とコメントを付けます。 1位: 「なぜ海賊はカレンダーを信用できないのですか? 彼らはいつも日付を決めようとしているからです。」 - コメント: 海賊と「日付を決める」ことの二重の意味を使ったワードプレイがうまいですね。シンプルながらもクスッと笑える冗談です。 2位: 「スプーンとフォークのどちらが寂しいですか? フォークです。スプーンは全部のスープにありえるからです。」 - コメント: スプーンとフォークという日常アイテムを使い、さりげない観察からくるユーモアが魅力的です。スプーンの人気を引き合いに出したアイデアが面白いです。 3位: 「ドアノブを壊して投獄されました。それは変えられないと思いました。」 - コメント: ドアノブを壊すことで「変えられない」という状況に対するアイロニーが少し分かりにくく、他の冗談に比べてややインパクトに欠けた印象です。 以上です。楽しんでいただけましたでしょうか? 実行結果を見ると、 factory_agent の生成結果が DI され、それに対する審査結果が無事に生成されていることが分かります。(まだまだお笑いは難しいようですね。) ちなみに count の値を明に指定してはいませんが、今回は 3 として実行されています。この挙動が自明ではないので念のため説明します。 プログラムでは judge_agent が主体となっています。ツールとして joke_factory 関数をいわゆる function calling 的に呼び出します。そしてこの関数の中で、 factory_agent が呼び出され、冗談を生成結果として関数の返り値に設定します。この時、 joke_factory 関数では、冗談をいくつ生成するかの指定も可能です。この count の指定は joke_factory の関数を呼び出している judge_agent が指定しています。 試しに judge_agent のプロンプトに「冗談は4つでお願いします」と指定の文言を入れると以下のような挙動が観察されました。 > python depends_agent.py count=1 count=1 count=1 count=1 冗談-1グランプリのランキングとコメントは以下の通りです。 ### 第1位 **「なぜ海賊はそんなに悪い成績だったの? 彼らはいつもCを得ていたからです。」** コメント: 「C」と「海(Sea)」をかけたユーモアが秀逸です。簡潔で理解しやすく、爆笑を生む最高の冗談です。 ### 第2位 **「なぜ海賊は自分の船を停泊できないのですか? 彼らは停泊する方法を知らないからです!」** コメント: 停泊(停める)という動作にちなんだシンプルなジョークがツボにはまります。若干わかりづらい印象もありますが、考えると面白いです。 ### 第3位 **「なぜ海賊はそんなに悪い成績だったの? 彼らはいつも略奪していたから!」** コメント: 海賊の「略奪」を成績に絡めたアイデアが斬新ですが、1位の冗談と似ているため、新鮮さに欠けます。 ### 第4位 **「なぜ海賊はそんなに下手なカードプレイヤーなのですか?」** コメント: やや曖昧で特にオチが感じられないため、順位が上がりませんでした。カードプレイの部分にユーモアを盛り込むとさらに面白いかもしれません。 挙動を観察する限り、今回のケースでは指定した冗談の数になるように複数回 joke_factory 関数を呼び出していることが分かります。( count=1 が複数回標準出力されていることから、1回ずつ冗談を生成させていることが分かります。) DI により、function calling がシームレスに実行されているとも考えられるため、よりシンプルな実装が実現できています。 まとめると、上記のように DI をうまく使用することで、複数の LLM を組み合わせた、いわゆるマルチエージェントについてもメンテナンスしやすい形で実装することができました。 おわりに Pydantic ひいては PydanticAI は型安全な実装を提供してくれることが分かります。 一方で RAG などをはじめとした応用事例からも分かるように、どのような情報を LLM に提供するか / アクセス可能な手段を LLM に提供するかは重要なトピックです。 しかしこれらの情報アクセスの設定はユースケースによって様々です。つまり適切に依存関係を切り分けることが保守・運用において重要な取り組みとなります。 このような現場における課題に対し、PydanticAI の dependency injection system は有用なアプローチになる気がしました。 今回は触れませんでしたが、PydanticAI ではチャット履歴の管理や Agent のテストについても effort less な実装を提供してくれています。 またストリーミング出力に構造を持たせた streaming structured response などの魅力的な提供もあります。 LLM をプロダクトに最適な形で組み込めるよう、AI チームとしても引き続きこの辺りの情報にキャッチアップしていければと考えております。 ここまでお読み頂きありがとうございました。 投稿 【AI Shift Advent Calendar 2024】PydanticAI で実現する Dependency Injection は 株式会社AI Shift に最初に表示されました。
アバター
こんにちは、AIチームの邊土名( @yaelanya_ml )です。 本記事は AI Shift Advent Calendar 2024 の18日目の記事です。 私はサイバーエージェントの研究組織である AI Lab の 完全自動対話研究センター という部署に所属していまして、現在は兼務という形でAI Shiftに関わっています。 せっかく(?)ですので、今回の記事では、この1年くらいの間 AI Lab で取り組んでいた セールス対話データセット構築 の研究紹介をできればと思います。 はじめに 突然ですが、皆さんはセールストークや店頭販売員などのセールスパーソンに対してどのような印象を持っていますか? 「商品をじっくり見たいのにセールストークをされて邪魔だった」というネガティブな思い出を持っている方も少なからずいらっしゃるかと思いますが、実際にセールストークを聞いて商品を購入した経験がある方も多いのではないでしょうか。 優秀なセールスパーソンは、買い手の表情や会話内容を注意深く観察し、買い手自身がうまく言語化できないような潜在的なニーズ・欲求を読み取る能力を持っています。 そして、買い手が明確に認識していなかった「欲しいもの」を具体的に言語化し、提案することで、相手の購買意欲を高めることができるのです。 このような「優れたセールストーク」を実行できるセールス対話システムを構築することは、売り手・買い手双方にとって多くのメリットがあります。 売り手側のメリットの例 システムが稼働している限り24時間365日対応可能(機会損失の防止) 人間と比較してスケールさせやすい 広告のランディングページやECサイト上など、これまで人間のセールスパーソンが入りにくかった領域にも対応可能 買い手側のメリットの例 システムが稼働している限り24時間365日アクセス可能 膨大なデータに基づく正確な情報提供 (e.g., RAG) 個々のニーズに合わせて提案内容をパーソナライズ 複数言語対応 今回は、セールス対話システム構築の第一歩として、何らかの基準となるセールス対話データセットを構築することを試みました。 セールス対話収集プロセス 今回のセールス対話収集プロセスの概要図を以下に示します。 今回は、Web上の商品販売ページに訪れたユーザと、そのWebページ上に設置されたセールス対話システムがテキストチャットを行う実験シナリオで対話データを収集しました。 データセットの特徴 本データセットの特徴として、以下の3つの設定で対話データを収集したことが挙げられます。 ユーザが対話途中で離脱することを許可 人間と対話システムとの間で行われる対話設定 対話に参加したユーザによる発話レベルの対話評価 ユーザが対話途中で離脱することを許可 対話データ収集の研究では、十分なデータ量を確保するために、実験参加者に対して「◯ターン会話してください」といった指示を与えているケースが少なくありません。 しかし、セールストークが実施される状況を考えると、商品を購入する意欲が低い、対話を続ける意欲が低いといった、ユーザが対話システムに対して協力的でない状況も想定されます。 そこで今回は、このようなシビアな状況を可能な限り再現してより現実に近いユーザ意欲を測定するために、ユーザ役の実験参加者に対して任意のタイミングで対話から離脱できることを説明しました。 人間と対話システムとの間で行われる対話データを収集する 先行研究( Hiraoka+ 2016 ; Tiwari+ 2023 )では、主に人間同士の対話データが収集されていますが、対話相手が人間かシステムかによって対話の特徴が変化することが指摘されています( Serban+ 2018 )。 私たちの最終目標はセールス対話システムを構築することなので、人間-システム間のセールス対話データが欲しい気持ちがあります。 しかし、いきなりセールス対話システムを用意するのも中々難しいところです。 そこで今回は、セールス対話システムのふりをしたセールス経験者(セールス役)がユーザ役の対話相手となるWizard of Oz (WOZ) 法( Kelley 1984 )の設定で対話データを収集しました。 対話に参加したユーザによる発話レベルの対話評価 本データセットには、発話レベルでの多面的なユーザ意欲評価のアノテーションが収録されています。 これにより、ユーザの購買意欲を高めるセールス対話戦略のきめ細やかな分析や、ユーザの反応に応じて柔軟に対話戦略を切り替える効果的なセールス対話システムの開発につながることが期待されます。 対話設定 実験参加者 セールス役、ユーザ役となる実験参加者はクラウドソーシングサービス上で募集しました。 セールス役は、実際に営業経験のある方々を合計5名採用しました。 さらに対話品質を向上させるため、対話収集の前に説明会を実施し、その上で対話練習を計2回、合計2時間のガイダンスを実施しました。 ユーザ役は、日本語で円滑にテキスト対話ができる者を対象として募集を行いました。 WOZ法を用いて対話データを収集する関係上、ユーザ役には「セールス対話システムとテキストチャットを行う実験である」と説明し、実験終了後に対話相手がシステムではなく人間であることを通知しました。 また、ユーザ役の自然な対話継続意欲を観測するために、任意のタイミングで対話から離脱してもよいことを説明しました。 ユーザ役による対話評価 ユーザ役は、対話終了後に発話レベル・対話レベルの評価を行います。 発話レベル評価では、ユーザ役がセールス役の発話に対して感じた印象を3段階(ポジティブ、ニュートラル、ネガティブ)で評価してもらいます。 評価観点は以下の3種類です。 対話継続意欲: ユーザ役は、「もっとBotと会話を続けたくなった」と少しでも感じた発話に対して”ポジティブ”の評価を行います(ネガティブ評価はその逆) 情報提供意欲: ユーザ役は、「自分の持っている情報をBotに話したくなった」と少しでも感じた発話に対して”ポジティブ”の評価を行います(ネガティブ評価はその逆) 目標受容意欲: ユーザ役は、「対話目標を受け入れたくなった(商品を購入したくなった)」と少しでも感じた発話に対して”ポジティブ”の評価を行います(ネガティブ評価はその逆) 対話レベル評価では、商品に対するユーザ役の購買意欲を、対話実験の前後に実施するアンケート調査を通じて測定します。 ユーザ役は、商品に対する購買意欲を7段階評価で回答します。 ユーザ役の購買意欲が対話開始前と比較して向上した場合、セールストークが成功したと判定します。 商品 セールス役が販売する商品は、架空の3種類のワイヤレスイヤホンの製品情報を作成しました。3つのワイヤレスイヤホンの価格は以下の通りです。 ハイエンドモデル:33,000円 ミドルエンドモデル:22,000円 ローエンドモデル: 11,000円 商品価格については、高価格もしくは低価格すぎない価格帯になるよう設定しました。 低価格過ぎる商品の場合、セールストークでユーザの意欲を高めなくても商品が購入される可能性がある一方で、高価格過ぎる商品の場合はそもそも購買する意欲が無くなってしまう可能性があるためです。 ワイヤレスイヤホンを商品に選択した理由としては、ある程度専門知識が必要な商品であることが挙げられます。 セールストークにおいては、セールスパーソンが商品に対する専門知識が深ければ深いほど顧客から信用されやすくなり、結果的に購買につながりやすくなります。 つまり、セールストークの効果が観測しやすくなると考えたため、ワイヤレスイヤホンを商品として選択しました。 対話環境 対話環境として、Slurkをベースとしたチャットツールをクラウドサーバ上に構築しました。 ちなみに、2年前のAdvent Calendarにて Slurkの紹介記事 を書いているので、ご興味のある方はそちらも併せてご覧ください。 実験参加者がチャットツールのURLにアクセスすると、実験参加者同士のマッチングが行われ、ユーザ役とセールス役のペアが揃った時点でチャットルームが自動的に作成されて対話実験が開始されます。 このチャットツールには、基本的なテキストチャット機能の他、商品情報の表示および共有、対話終了ボタンなどの機能が実装されています。 収集結果 上記のプロセスで構築したセールス対話データセットの統計量は以下の通りです。 109対話のうち、半数以上となる63対話でユーザの購買意欲を向上させることに成功しています。 終わりに 以上、ざっくりとしたセールス対話データセットの紹介でした。 私事になりますが、ちょうど先日、このデータセットに関する論文が自然言語処理の国際学会である COLING 2025 に採択されました! User Willingness-aware Sales Talk Dataset Authors:邊土名朝飛 (サイバーエージェント AI Lab)、馬場惇 (サイバーエージェント AI Lab)、佐藤志貴 (サイバーエージェント AI Lab)、赤間怜奈 (東北大学) COLING 2025は来年1月19日から24日にかけてアラブ首長国連邦のアブダビで開催されます。 私も現地で発表を行う予定ですので、現地参加される方はぜひお立ち寄りください。 論文では、本記事に書かれている内容に加えて、データセットの分析結果、データセットを用いてLLMをFine-tuningした際のユーザ評価実験の結果なども報告しています。 ご興味のある方はぜひ論文の方もご覧いただけますと幸いです(論文は近々公開される予定です) また、COLING2025の開催に合わせて、今回紹介したセールス対話データセットの公開の準備も進めています。 論文やデータセットが公開された際は私のXアカウント( @yaelanya_ml )で告知しますので、今しばらくお待ちください。 明日のAdvent Calendar 19日目の記事は、開発チームの木村による記事を公開予定です。ぜひそちらもご覧ください! AILabでもAdvent Calendar を開催していますので、こちらもよろしくお願いいたします! 投稿 【AI Shift Advent Calendar 2024】セールス経験ゼロの人間がセールス対話データセットを作ってみた話 は 株式会社AI Shift に最初に表示されました。
アバター
こんにちは!AI Shift 生成AIビジネス事業部で研修講師を担当している及川 信太郎( @cyber_oikawa )です。 この記事は AI Shift Advent Calendar 2024 の17日目の記事です。 今回は、サービスとして提供している生成AIを使いこなすための研修について、約半年間研修を提供する中で考えた「面白い研修コンテンツの作り方」についてまとめます。 AI Shiftが提供している研修について AI Shiftでは生成AIに関する研修を複数展開しています。 生成AI基礎 / 応用研修 Dify研修 Copilot研修 経営者向け研修 営業 / 人事 / マーケティング職向け研修 サイバーエージェントの99.6%が受講した「 生成AI徹底理解リスキリング for Everyone 」のエッセンスを元に、基礎的な研修からツール別 / 職種別で研修の社外向け展開を行っており、総受講者は数百名を超えています。 また、目まぐるしく変わる生成AI市場についての外部向け オンラインセミナー を随時展開しています。 研修全体の満足度平均は97%で、高い水準をキープしています。 今回は、研修満足度を高くキープし続ける研修コンテンツを作成するためのこだわりをご紹介したいと思います。 成功する研修の秘訣は「事前情報収集」にあり 研修の開催方法はいくつかのパターンがあります。 選抜者向け 特定の職種 / 部署向け 全社員向け 選抜者向けに募集する場合は、大体の場合がAIリテラシーが似通っているため、どのような内容を話せばいいのかも定められます。 しかし、職種 / 部署で募集する、全社員で募集する場合はAIリテラシーにバラつきがあります。 例えば、エンジニア職では生成AIツールを既に業務で使用しているケースが多い一方、営業職では活用方法が模索段階である場合が多いです。 「プロンプト」という言葉を聞いて完全に理解している方もいれば、聞いたことのない横文字にアレルギーが出てしまう方もいます。 そこで重要なのが、研修を作成する前に「事前情報収集をする」ということが非常に重要です。 生成AIの活用頻度はどれくらいか? どんな業務に生成AIを活用しているか? 活用において感じている課題は何か? どんなことを研修で知りたいか? このような内容をアンケートで回収することで、参加者の方々の生成AIに関するリテラシーを把握することができます。 リテラシーはおおむね『上級者』『初心者』『混合』の3パターンに分かれます。 それぞれのリテラシーで意識しているポイントをお伝えします。 ①上級者が多い場合 専門用語に関する説明は極力省く 既知の言葉である可能性が高く、自身で検索する手法も理解している場合が多い。 実践(手を動かしてプロンプトを作成したり、実装事例に触れる)の時間を多く設ける より自身の業務での活用イメージが沸く内容(プロンプト作成のワークショップや、自社データを使った具体的なAI活用シミュレーションなど)を取り入れる。 質疑応答の時間を多く取る 実践を通して、さらに自身の業務の身になる質問をしたい参加者が多いケースが多い。 ②初心者が多い場合 専門用語には必ず説明と、分かりやすい例え話を入れる 例)「プロンプト」とは、生成AIにどのようなタスクを遂行してもらいたいかを決める「指示文」です。 プロンプトに入れる文言では曖昧な表現は避けて「新卒社員にイチから説明する」イメージで丁寧に記載しましょう。 実例の紹介時間を多く設ける 「これは自分の業務に近い使い方だ!」という気づきを多く取り入れる。 ③混合の場合 内容は「初心者に合わせる」が鉄則 リテラシーが高い参加者は「そんなこと知ってるよ」という内容があっても、高度な活用事例や質疑応答があれば自身の知りたい情報を取り入れることができます。 しかし、初心者にとっては、知らない言葉が1つ出てくるだけで理解が追いつかなくなり、その後の理解が難しくなるケースが往々にしてあります。 そのため「内容は初心者に合わせる」を徹底して行うことで、結果的に全体の満足度を上げることができます。 期待値を合わせる 例えば初心者向けの内容にする場合「今回は初心者向けの内容になっています。上級者の方には復習のつもりで、周りの方もサポートしてあげてください」と事前に伝えたり、ワークショップなどを実施する場合は上級者向けにより高いお題を伝える、応用的な活用方法を個別でお伝えすることで期待値を合わせつつ、初心者・上級者両方に満足度が高くなるような内容設計が効果的です。 質疑応答のハードルを下げる 質疑応答の例を出す 「そもそもプロンプトってなんですか?という質問でも構いません!」というような、質問のハードルを下げる事例を出すことが有効的です。 ファーストフックを作る 参加者が多い場合、一番最初に質問をする方はとても緊張すると思います。そのため、運営側のメンバーに1問目の質問を振ったりする「ファーストフック」を作ることで、その後の質問が続きやすくなります。 座学と実践の黄金比 研修は概ね座学が中心で、3~4時間のケースが多いと思います。 しかし、一般的に大人の集中力の持続時間は50分と言われています。50分と言えど、人の話を50分聞き続けるというのは、よっぽど興味のあるコンテンツでなければ持続することは難しいです。 研修においても同じことが言えます。その全てが講師の話を聞きっぱなしでは、その後自身の業務で生成AIを使いこなすレベルまで到達するのは難しいです。 ここで研修時には 「座学:実践=3:1」 を意識しています。 例えばプロンプトを紹介したら試す時間を取る、どんな活用を行いたいか資料にアウトプットしてもらうなど、座学の後に実践を必ず加えることで、より知識が定着しやすくなります。 この比率を重視し始めてから、高満足度をキープし続けることができています。 また、午後一番(13時ごろ)から研修を開始する場合、昼食後で集中力が長く保たない場合が多いため、最初にグループディスカッションを行うなどのインタラクティブな実践を行うことも効果的です。 勉強においては授業の後の小テストを実施することで知識が定着するように、座学の後には実践を組み込むことで集中力を高く保って研修に参加いただくことができると思います。 満足度97%を実現するフィードバックループ 研修では振り返りが非常に重要で、研修後のアンケートにご協力いただくことを重視しています。 勉強になった内容はどこか 既知の情報はどこだったか 社内に同内容をお勧めしたいと思ったか どんな内容を実践したいと思ったか 他にどんなことを知りたいか このような内容を元に、例えば勉強になった内容で多かったものは事例等を加えて深く説明する、既知の情報が多いものは削除するなどのコンテンツのブラッシュアップをすることができます。 また、研修内容を録画してチームメンバー / 自身で見直すことも有効的です。 時間配分は問題ないか 話すスピードは早すぎないか 「えーと」「あのー」などのフィラーワードを多用していないか 休憩のタイミングは適切か(参加者が疲れてきていないか) こういった講師の話し方などは、アンケートでは見えにくいため自身で見直すことで、次回以降の研修に活かすことができます。 最後に 生成AIを取り巻く市場は日々変化しており、研修実施者もその変化に対応していく必要があります。 私自身まだまだ勉強不足ではありますが、来年以降も質の高い研修を提供していきたいと思います。 最後までお読みいただき、ありがとうございました! 投稿 【AI Shift Advent Calendar 2024】面白い研修コンテンツの作り方 は 株式会社AI Shift に最初に表示されました。
アバター
こんにちは AI Shift CTOの 青野 です AI Shift AdventCalendar 2024 16日目です 今回はAIエージェントを開発する時の勘所について書こうと思います お付き合いお願いします〜 はじめに 昨今、生成系大規模言語モデル(LLM: Large Language Model)を活用したプロダクトの開発機会が急速に増えている。ChatGPTやGPT-4など、自然言語による高度な応答を可能とするLLMは、従来困難だった自然言語処理タスクを容易にし、多くの領域で新たなアプリケーションを生み出す可能性を秘めているが、その一方で、LLMを用いたプロダクト開発は、急速に進化するLLMや市場の期待値を受けてより複雑化している 「解決したい課題は何か」「LLMが適切な手段なのか」「データ基盤やセキュリティの要件は満たされているか」「どのようなアーキテクチャを採用すべきか」といった問いは必ず発生する この記事では、LLMプロダクト開発時に考慮すべき前提・要求・要件定義、設計思想に関する視点を包括的に整理する AIエージェント開発の前提条件 課題とエージェントの必要性 まず、解決すべきビジネス課題やユーザーニーズを明確化する必要がある。顧客が本当に自然言語で柔軟な問題解決を求めているのか、既存のドキュメントやFAQを活用した単純な問い合わせ自動化で十分なのかを再考すべきである。必ずしもLLMが必須でないケースも多いため、安易な導入は避けるべきだろう とくに要件定義は必ずしっかりとした時間をとって実施すべき事の一つだ 旧来のプロダクト開発でももちろん非常に重要な項目には違いないのだが、未知への挑戦・研究領域の社会実装という意味合いが強い現状を考えると、プロトタイプの実装を含めた慎重な舵取りが求められる場面は非常に多いだろう 観点としては LLMによる解決可能性 その課題はLLMに適しているか 法的問題のクリア 人間以外がその課題の解決を実施しても問題ないか ラストワンマイルに人間が介在すべきか 精度の問題 LLMによる出力が100%正しくない前提にたった業務に求められる精度の認識合わせ 既存システムとの組み合わせ LLMが介在するときにどのような組み合わせが最も上手く協調できるか リプレイスが必要かどうか ユーザーニーズとモデル選定 これらの観点からLLMが介在するポイントを切り出して単体のシステムとして精度検証を行うことが望ましい AIエージェントによる解決可能性 LLMは万能ではない 確率的な応答に起因する不確実性や、ドメイン固有の厳密な正確性が求められる領域(金融、医療、法律など)では、LLM単独で要件を満たすことが難しい場合がある ユーザーが期待するのは正確性か、ヒント提供か、対話的な柔軟性か LLMで本当に課題が解決できるかを事前に検証することが重要となる 全てをLLMに任せる必要は無く、人間のコストを大幅に減らすことでも満たせる要求はあるだろう このあたりの見極めと期待値の調整が今のフェーズでは求められる もちろん今後のLLMの進化によって解決可能性の幅は広がっていくことは間違いない そのためLLMに関わるプロダクトを設計する人間は 最新情報のキャッチアップをおろそかにする と致命的な設計ミスをする可能性がある(自戒) ユーザーニーズとモデル選定 顧客が求める自然言語応答が簡易なFAQ対応なのか、詳細なドメイン知識に基づくアドバイスなのかによって適切なモデルは異なる。モデルサイズ、性能、コスト、オンプレミス運用の可否など、要件に合ったモデル選定が必要となる 多くの場合、専用のLLMが必要なケースは少ない Gemini1.5やGPT-4oで満たせないほどの性能的な要求は、課題の整理か実現方法が間違えている可能性が高いと思われる 評価額23兆円の規模の会社 が作るモデルですら解決できない課題は課題設定が間違えていると考えるのが妥当だろう ただし以下の場面では一定専用モデルを考慮する価値があるかもしれない 低性能で問題がなくコスト面の要求がある場合 セキュリティ的な制約(オンプレ等) 特定のタスクに特化しつつコスト面で圧縮したい場合 VLM等の学習データに特化する価値がある場合 AIエージェントの要求定義 既存ツールの活用可能性 すでに運用中のSaaSやツールで同等の機能が実現可能な場合、新たにLLMを導入する必然性は薄い LLM導入にはコストや運用負荷が伴うため、他の選択肢との比較検討が欠かせない また既存ツールで解決できない場合でも既存ツールとの組み合わせは必須となることが多い そのツールが API連携可能かどうか 実現したいことに対して必要なRead/Writeが可能かどうか データは適切な形式で構造化されているか 前処理やクリーニング、品質保証のプロセスは確立可能か といったことも検討する必要がある さらに、個人情報や機密情報を扱う場合には、データガバナンスやコンプライアンス(GDPRや各国法令)への対応も不可欠となる とくに社内での機密情報を扱うケースでは、データが外部を経由することが許可されるかどうかも検討が必要だろう(この考慮の中で初めてセルフホスティング型LLMの考慮をすれば良い) UI・UX AIエージェントは会話を通じた問題解決が主流ではあるが、そのタスクに対してチャット型が適しているかどうかはきちんと考える必要がある 多くの場合チャット型はユーザーに責務を投げすぎている 特に業務に特化する場合テキスト入力は煩雑そのものであり、無駄極まりない ユーザーはチャットがしたいのではなく、問題解決がしたいのだ という点は忘れないようにしたい AIエージェントの要件定義 上記はあくまで要求定義の段階となり、実際に要件を決めていく場合の勘所を整理する 特に、LLMを中心とした設計なのか、補助に使うのかという部分は非常に重要かつ難易度の高い設計ポイントとなる LLM依存度の調整 まず前提として昨今のLLMは様々な知識を事前のトレーニングで持っている一方、その知識の正確さ、範囲までは何も提示されていない つまり動かしてみて検証する必要がある ただし動いたとしても、ベンダー側で何も保証していない点は注意が必要だろう そのため、LLMが内包する知識にどこまで依拠するかを明確にし、アプリケーション特有のビジネスロジックやルールとの切り分けを行い、LLMの役割範囲を定義することが必要となる OpenAI、Google、Anthropicといったベンダーに関して言えば、ある程度LLMにまかせても良い範囲としてはJSON周りが挙げられるだろう 上記ベンダーのLLMは組み込み機能としてJSON Schemaを介する機能があり、JSON Schemaに関して言えば一定信頼をおいても問題ないと考えている その他のライブラリ・フレームワークに関して言えば非常にリスクが高い(認識しているバージョンの違い等)ためオススメしない セキュリティ・プライバシー要件 LLM導入には新たな攻撃ベクトル(プロンプトインジェクションなど)が存在する アクセス制御や認証プロセス、出力フィルタやモデレーション層の導入により、有害なコンテンツや不適切な応答を防ぐ ユーザーと組織をリスクから守るため、脅威モデリングを含めたセキュリティ設計が必須となる 特に、LLMにDBへ直接クエリを書かせるようなユースケースである場合、通常のアプリケーション設計と同じように、LLMに対するサニタイズのようなものが必要となる 少なくともユーザーの入力によって恣意的にSQLクエリを発行できるような状況は必ず避けなければならないアンチパターンとなる しかし、まだまだ実稼働しているAI エージェントのようなプロダクトが少ない現状だと、このあたりのセキュリティ的な留意事項に関しては発展途上なのが現状である AIエージェント時代のアプリケーションと設計 LLMを中心に置いたWebアプリケーションの設計と従来型のWebアプリケーション設計において決定的に異なるのが、主体をシステムに置くかLLMの意思決定に置くかという点にある 意思決定の委譲範囲 従来はシステム側で明確に定義された制御フローが前提だったが、LLM活用によって一部の意思決定をモデルに任せることが可能となる ただし、その裁量範囲は慎重に設定する必要がある 範囲を狭めすぎると実現可能なユースケースの減少、あるいは利便性の減少というUXの問題にぶつかる また裁量を与えすぎた場合、ハルシネーションを含めた不確定要素がシステムに致命的な問題を起こす可能性を常に考慮する必要性に迫られる しかし少なくともLLMに意思決定を委ねる選択をした場合、従来のWebアプリケーション設計と異なる点が少なからずシステム設計に出現する 従来型Webアプリケーションとの違い 今までのWebアプリケーションではユーザーの入力は、システムに適したフォーマットに制限され、入力値が正しい場合のみシステムが決まったフローで処理を行っていた そのため、設計としては意思決定は既になされている状態でユーザーにオプションを委ねる形で実装されることになる だが、AIエージェントの開発においては、多かれ少なかれ意思決定がオンデマンドで実行される可能性が高い それを踏まえてLLMが意思決定に関わる実装パターンを大きく4つに分類してみた これ以外にもパターンは存在すると思うが、大枠これに収まるのではないだろうか システムがユーザー入力からデータベースからデータを取得、LLMにて結果を生成 LLMがユーザーの入力をシステムに合わせて整形し、システムで結果を取得、LLMにて結果を生成 ユーザーの入力からLLMが直接必要なAPIを選び入力を整形する ユーザーの入力からLLMが直接DBを操作して必要なクエリを発行する 上から順番にLLMの介入度が上がりシステムの安定性も下がっていく 1. 最小関与パターン これらはシンプルなRAGのシステム等が該当する DBからのデータ取得は定型のクエリが決まっており、SQL Binding等で値を渡す 入力値もシステムから受け取るだけ 2. Function Callingパターン 入力値から必要な値を取得しAPI向けに整形するパターン Vector Searchではなく構造化されたクエリのためにユーザー入力を抜き出す いわゆるFunction CallingでLLMを活用するパターン 3. AIエージェントパターン いわゆるAIエージェントという領域 API定義を事前にLLMに提供し、必要なAPIの選択、足りないパラメータの保管を対話で行わせる 最終的に呼び出すAPI、パラメータはLLMによって選択される ただし、APIを実際に呼び出し、結果を整形する責任はシステムにある ModelContextProtocol (MCP) と外部知識活用 Model Context Protocol(MCP)は、Anthropic社が開発した、AIアシスタントや大規模言語モデル(LLM)を外部データソースやツールと接続するためのオープンスタン ダードである これにより、AIシステムはコンテンツリポジトリ、ビジネスツール、開発環境など、多様なデータソースとシームレスに連携できる MCPに関しては当社のテックブログでも何回か言及されているので、詳細はそちらに譲るとして 【AI Shift Advent Calendar 2024】MCP ClientをOpenAIモデルで実装する 【AI Shift Advent Calendar 2024】MCP(Model Context Protocol)を用いた予約対話AIエージェントの構築と動作のトレース MCPがなぜ提唱されたのか、どういった利便性があるのかをパターン3から解説していこう MCPの価値 「MCPは上記のパターン3を提供するためのプロトコルである」というのはやや間違えている パターン3を実現するだけであれば、AIフロントエンドがAPI定義を単純にLLMに渡せば実現自体は可能であり、難易度としてもさして高いものではない MCPが提供する価値は「関心の集約」これに尽きる 従来型の欠点 パターン3でのAIエージェント実装には非常に大きな問題が存在しており、それは呼び出すシステムが増えたときに顕著となる API定義と実装の分離 実装とは別にAPI定義を管理する必要があるため、変更コスト・実装コストが高くなる API定義に適したプロンプトの変更 API定義だけではなく、プロンプトの変更も管理を別で行う必要がある MCPの解決方法 MCPではLLMに提供される機能をServerという仕組みで集約して管理を行う 各集約にはresources/prompts/toolsを取得する仕組みが定義されており、各集約のDiscoveryを提供している この集約のアーキテクチャによって、パターン3の問題を解決することが可能となる つまり、 サーバが実装とLLM向けの定義、プロンプトが集約して管理されるため、実装・定義・プロンプトこの3つをサーバ側にて提供する事が可能となる この集約の仕組みをオープンプロトコルにしたおかげで、Server側でMCPに則ったDiscoveryを提供すると、AI エージェントは個別の実装の必要なく機能を増やしていくことが可能になる MCPの現実とこれから MCPはあくまで定義であり、アーキテクチャのプロトコルではあるものの https://github.com/modelcontextprotocol こちらにTypeScriptやPythonの実装が定義されており、コミュニティ側での様々なServer実装が用意されている 問題点 対応LLMがAnthropicのClaudeのみ Claude Desktopのみがネイティブで対応しているが、それ以外のLLMだとアプリケーション側の介入がそこそこ必要なので、まだ本領発揮は難しいだろう OpenAIやその他ベンダーがどれだけ参加するか次第 Server実装の質 あくまでコミュニティ実装であり、実装の質はまばらなので使う前によく内部を確認する必要がある 特にDBに対するWriteアクセスには慎重になった方が良い 実装と定義の分離 コミュニティによるServer実装はMCPのあり方との矛盾が生じてしまう MCPにて最も重要な点は実装・定義・プロンプトの集約にあるが、少なくともコミュニティ実装の場合実装以外とその他が分離してしまう 本来これらは実装ベンダーが提供することで本当の価値が生まれる とはいえこれまでもコミュニティがベンダーの補完をしてきた歴史があるのは事実で、コミュニティとベンダーのLLM向けのアーキテクチャで協調する未来がまた生まれるとよいなぁと思う次第 このような共通仕様は最終的には賛同ベンダーの数とパワーバランスが全てなので今後に期待しつつ変なことにならないといいなぁと思っている 理想的なパターン MCPかどうかは置いておいて、特定のプロトコルにLLMベンダー、クラウドベンダー、SaaSベンダーが参画しAIエージェントの世界に向けて動く 駄目なパターン 各社バラバラなプロトコルを提唱し始めて、10年後くらいに統一する委員会を発足し標準化作業が始まる 個人的にはJavascriptやDOMの標準化までの長い道のりを見てきたので、駄目なパターンのある程度マシなやつに落ち着くのかなぁとは思っている まとめ AIエージェント時代のプロダクト開発は、モデル選定やデータガバナンス、セキュリティ、アーキテクチャ、そしてMCP(ModelContextProtocol)による集約の管理など、多面的な視点を要求する 鍵となるのは「なぜAIエージェントを使うのか」という問いへの明確な答えであり、その上で適切なデータ、アーキテクチャ、セキュリティ戦略を整えることが不可欠となる AI Shiftではこのような観点でより高品質で「ユーザーにとって価値のある」AIエージェント実装を目指すべくプロダクト開発を行っていますのでご興味がある方は是非〜 採用 AI Shiftではエンジニアの採用に力を入れています! 少しでも興味を持っていただけましたら、カジュアル面談でお話しませんか? (オンライン・19時以降の面談も可能です!) 面談フォーム 投稿 【AI Shift Advent Calendar 2024】AIエージェントの設計とその勘所 は 株式会社AI Shift に最初に表示されました。
アバター
こんにちは、AIチームの二宮です。 この記事は  AI Shift Advent Calendar 2024 の14日目の記事です。 はじめに Model Context Protocol (MCP) とは、Anthropicから公開されたOSSで、LLMがさまざまなツールやリソースを活用するための標準プロトコルです。MCPはClaude以外のモデルでも利用できるように設計されています。そこで、今回はOpenAIのモデルを用いたMCP Clientを実装してみます。 MCPの実装は、Anthropicが公開しているTutorialが非常にわかりやすいので、そちらを進めていただくことをおすすめします。特に今回は Quickstart でのMCP Serverの実装と、 Building MCP clients でのMCP Clientの実装を終えた状態から始めます。ディレクトリ構成は以下のようになっています。 . ├── mcp-client │ ├── README.md │ ├── client.py │ ├── pyproject.toml │ └── uv.lock └── weather ├── README.md ├── pyproject.toml ├── src │ └── weather │ ├── __init__.py │ └── server.py └── uv.lock OpenAIモデルでのMCP Clientの実装 公式Tutorialで紹介されているMCP Clientの実装ではClaudeのFunction Callingを使っているので、ここをOpenAIのFunction Callingの記述に変更します。Claude Desktop Appも同様の実装なのかは不明ですが、その挙動を見る限り同様のことが実現できます。また、AnthropicとOpenAIのFunction Callingの実装方法はかなり類似しており、以下のclient.pyの実装ではMCPClientクラスの process_query の変更がほとんどです。 import asyncio import json from contextlib import AsyncExitStack from typing import Optional from dotenv import load_dotenv from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client from openai import OpenAI load_dotenv() # load environment variables from .env class MCPClient: def __init__(self): # Initialize session and client objects self.session: Optional[ClientSession] = None self.exit_stack = AsyncExitStack() self.openai = OpenAI() async def connect_to_server(self, server_script_path: str): """Connect to an MCP server Args: server_script_path: Path to the server script (.py or .js) """ is_python = server_script_path.endswith('.py') is_js = server_script_path.endswith('.js') if not (is_python or is_js): raise ValueError("Server script must be a .py or .js file") command = "python" if is_python else "node" server_params = StdioServerParameters( command=command, args=[server_script_path], env=None ) stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params)) self.stdio, self.write = stdio_transport self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write)) await self.session.initialize() # List available tools response = await self.session.list_tools() tools = response.tools print("\nConnected to server with tools:", [tool.name for tool in tools]) async def process_query(self, query: str) -> str: """Process a query using Claude and available tools""" messages = [ { "role": "user", "content": query } ] response = await self.session.list_tools() available_tools = [ { "type": "function", "function": { "name": tool.name, "description": tool.description, "parameters": tool.inputSchema } } for tool in response.tools ] response = self.openai.chat.completions.create( model="gpt-4o", messages=messages, tools=available_tools, ) message = response.choices[0].message # ツールを使わず回答できる場合はそのまま返す if not message.tool_calls: return message.content # ツールを呼び出していれば実行して結果をmessagesに追加する messages.append(message) for tool_call in message.tool_calls: tool_name = tool_call.function.name tool_call_id = tool_call.id # Execute tool call tool_args = json.loads(tool_call.function.arguments) tool_result = await self.session.call_tool(tool_name, tool_args) tool_result_contents = [content.model_dump() for content in tool_result.content] print( "=================\n" f"Use Tool: {tool_name}\n" f"- Tool Arguments: {tool_args}\n" f"- Tool Result: {tool_result_contents}\n" "=================" ) messages.append( { "tool_call_id": tool_call_id, "role": "tool", "name": tool_name, "content": tool_result_contents, } ) response = self.openai.chat.completions.create( model="gpt-4o", messages=messages, ) return response.choices[0].message.content async def chat_loop(self): """Run an interactive chat loop""" print("\nMCP Client Started!") print("Type your queries or 'quit' to exit.") while True: try: query = input("\nQuery: ").strip() if query.lower() == 'quit': break response = await self.process_query(query) print("\n" + response) except Exception as e: print(f"\nError: {str(e)}") async def cleanup(self): """Clean up resources""" await self.exit_stack.aclose() async def main(): if len(sys.argv) < 2: print("Usage: python client.py <path_to_server_script>") sys.exit(1) client = MCPClient() try: await client.connect_to_server(sys.argv[1]) await client.chat_loop() finally: await client.cleanup() if __name__ == "__main__": import sys asyncio.run(main()) 事前にOPENAI_API_KEYを環境変数に設定する必要がありますのでご注意ください。 ここでMCP Serverへリクエストを送っている箇所は2つあります。 まず、58行目の以下では利用可能なツールの情報をServerから取得しています。 response = await self.session.list_tools() 以下は実行結果です。 { "nextCursor": null, "tools": [ { "name": "get-alerts", "description": "Get weather alerts for a state", "inputSchema": { "type": "object", "properties": { "state": { "type": "string", "description": "Two-letter state code (e.g. CA, NY)" } }, "required": [ "state" ] } }, { "name": "get-forecast", "description": "Get weather forecast for a location", "inputSchema": { "type": "object", "properties": { "latitude": { "type": "number", "description": "Latitude of the location" }, "longitude": { "type": "number", "description": "Longitude of the location" } }, "required": [ "latitude", "longitude" ] } } ] } ツールごとに name 、 description 、 inputSchema が定義されています。これはOpenAIと類似しており、 inputSchema を parameters に変更すればそのまま利用できます。 次に、90行目の以下ではツールを実行しています。 tool_result = await self.session.call_tool(tool_name, tool_args) tool_result_contents = [content.model_dump() for content in tool_result.content] 戻り値の型は list[types.TextContent | types.ImageContent | types.EmbeddedResource] であり、それぞれ pydantic.BaseModel を継承しています。このままではOpenAIのFunction Callingでは利用できないのでmodel_dumpメソッドで dict 型に変換しています。 それでは実際にMCP Clientを実行してみます。 cd mcp-client uv run client.py ../weather/src/weather/server.py これでOpenAIのモデルを用いてMCPを利用することができました。 MCP Clientの位置付け MCPの公式ドキュメントには以下の図が記載されています。 MCP Hosts : Programs like Claude Desktop, IDEs, or AI tools that want to access data through MCP MCP Clients : Protocol clients that maintain 1:1 connections with servers MCP Servers : Lightweight programs that each expose specific capabilities through the standardized Model Context Protocol Local Data Sources : Your computer’s files, databases, and services that MCP servers can securely access Remote Services : External systems available over the internet (e.g., through APIs) that MCP servers can connect to Model Context Protocol: Introduction 現在の実装だとMCP Serverは必ず自身のローカルPCに存在する必要があります。インターネット上に公開されたサービスにアクセスする場合は、ローカルPC上のMCP Serverから接続する必要があります。ただし、この図にはMCP Clientが含まれない点に疑問を感じ、 Core Architecture の以下の図を見てみました。 Core architecture - Model Context Protocol どうやらMCP Host内にMCP Clientが含まれるようです。MCP ClientはMCP Serverと1対1接続し、MCP Hostが複数のMCP Clientを管理しています。 Claude Desktop App The Claude desktop application provides comprehensive support for MCP, enabling deep integration with local tools and data sources. Clients - Model Context Protocol こちらの表は各MCP Clientがサポートしている特性を表しています。 先ほどの図ではClaude Desktop AppはHostであると説明されていたため解釈が難しいですが、Claude Desktop Appで動作するClientがサポートする機能として捉えることができます。 表からわかるようにMCPはResources、Prompts、Tools、Sampling、Rootsをサポートしており、Claude Desktop Appはその内3つをサポートしています。今回はFunction Callingを用いることでToolsをMCP Clientで利用しましたが、その他の機能についても今後触れてみたいと思います。 最後に 今回はMCP ClientをOpenAIモデルで実装しました。 MCP Serverは様々な機能をMCP Clientに提供しますが、今回扱ったToolsにおいて実現できることはFunction Callingとそれほど変わりません。しかし、MCPはサーバーとクライアントの責任を明確に分離することで、開発のスピードを一層加速させるように設計されており、これによってLLMが利用できるツールやリソースの選択肢が増えたことは非常に大きなインパクトがあります。今後はMCPを活用して様々なツールと連携するAIエージェントを作成してみたいと考えています。 ここまでお読みいただきありがとうございました。 MCPについて以前投稿した記事( 【AI Shift Advent Calendar 2024】MCP(Model Context Protocol)を用いた予約対話AIエージェントの構築と動作のトレース )もぜひご覧ください。 投稿 【AI Shift Advent Calendar 2024】MCP ClientをOpenAIモデルで実装する は 株式会社AI Shift に最初に表示されました。
アバター
こんにちは、AI Shift AIチームの大竹です。 本記事は AI Shift Advent Calendar 2024 の11日目の記事です。 今回の記事では弊社の AI Messenger Voicebot をはじめとした音声対話システムにおける主要な課題である発話終了検知について扱います。 発話終了検知とは 発話終了検知は、音声対話システムにおいて2つの重要な役割を担っています。 まず、スムーズなターンテイキングの実現に不可欠です。対話というのは話者がターンを取り合いながら進行していきます。1ターンの中に休止があったり、聞き手の相槌が含まれていたりします。ターンを取ることをターンテイキングと言って、このターンテイキングを実行するために相手の発話が終了したことを検知する発話終了検知というロジックが重要になります。 また、適切なタイミングでの発話終了検知はユーザの意図を正確に理解するために重要です。高速なターンテイキングの実現に特化しようとして、早めに発話終了検知をしてしまうと発話が途切れてしまい、意図理解に失敗してしまいます。 このように発話終了検知はシステムがユーザとの対話を円滑に進めるために重要なタスクですが、話し手の発話が終了したかどうかをどのように判定しているかを言語化することは難しく、人間でも何となく話し終わっただろうという推論をもとにターンテイキングを行っている気がします。このように単純なルールでは書くことが難しい部分は機械学習的なアプローチを採用していい感じにできたら嬉しいです。 VADとVAP 発話終了検知の処理を実装するために重要になるのがVAP(Voice Activity Projection)、VAD(Voice Acitivity Detection)というタスクです。 VADは、与えられた音声データに対して、発話している区間を特定するタスクであり、内部では音声信号が有声音か無声音かを判定しています。 一方、VAPは、発話のターンテイキング、つまり話者交替のタイミングを予測するタスクで、対話における発話の切れ目や次に話し始めるタイミングを推測することに焦点を当てています。 本記事では、VADモデルやVAPモデルを用いてスムーズで頑健な発話終了検知がどの程度実現可能かを検証します。VAPモデルは商用利用可能のライセンスで公開されているモデルが存在しないので、モデルの作成も検証の範囲内とします。VADモデルは公開モデルで商用利用可能なものがあるので、モデルの作成は検証のスコープ外とします。 したがって、本記事で扱う内容は以下のものとします。 VAPモデルの作成 VAPを用いた発話終了検知 VADを用いた発話終了検知 VAPモデルの作成 使用したコード VAP-Realtime リポジトリ に含まれるコードをそのまま使用しました。 使用したデータ 日本語日常会話コーパス(CEJC) CEJCは、日常生活における自然な会話を収録した200時間規模のコーパスです。40名の協力者による「個人密着法」と、不足分を補う「特定場面法」で収集されています。映像・音声データから形態論情報まで、多様なデータを含む日本語会話のデータベースです。 訓練データの準備 CEJCのディレクトリ構造は以下のようになっています。 data ディレクトリ内に各セッションの音声データとそのアノテーションが含まれています。 CEJC/ ├── 00readme.html ├── data ├── metaInfo ├── rdb └── tool CEJC/data/C001/C001_001 ├── C001_001-luu.TextGrid ├── C001_001-luu.csv ├── C001_001-luu.eaf ├── C001_001-morphLUW.csv ├── C001_001-morphSUW.csv ├── C001_001-transUnit.TextGrid ├── C001_001-transUnit.csv ├── C001_001-transUnit.eaf ├── C001_001_IC01.wav ├── C001_001_IC02.wav ├── C001_001_IC03.wav ├── C001_001_IC04.wav ├── C001_001_IC05.wav └── C001_001_IC0A.wav *-luu.csv には長発話単位(Long Utterance Unit)に関する以下のようなアノテーションが含まれています。 luuID startTime endTime speakerID text 1 11.563 12.444 IC01_杉田 (R けん)ちゃん寝るんだ。@店に出入りの人が店先のベンチで昼寝 2 12.668 12.817 IC02_中沢 あ。 3 12.937 13.866 IC01_杉田 (L) 4 12.96 13.275 IC02_中沢 寝る。 startTime と endTime は発話に対する音声区間のアノテーションです。 このアノテーション情報をVAPの訓練に用いるために、以下のような形式のCSVファイルに変換します。 audio_path start end vad_list session dataset CEJC/data/K002/K002_003/K002_003b_IC0A.wav 0 20.0 [[[11.563, 12.444], [12.937, 13.866], [17.948, 18.595]], [[13.71, 14.782], [19.773, 20.273], [20.75, 22.44]]] K002_003a-luu CEJC CEJC/data/K002/K002_003/K002_003b_IC0A.wav 20 40.0 [[[0.75, 2.44], [4.437, 5.087], [11.687, 12.319], [12.645, 13.775], [18.575, 24.443]], [[6.892, 7.883], [10.078, 11.033], [12.904, 13.423]]] K002_003a-luu CEJC CEJC/data/K002/K002_003/K002_003b_IC0A.wav 40 60.0 [[[11.722, 12.475], [15.938, 23.122]], [[6.315, 8.495], [8.916, 13.78], [14.314, 15.324], [15.949, 16.579]]] K002_003a-luu CEJC 会話音声ファイルから20秒間のセグメントごとに有声区間を抽出し、 vad_list にまとめます。 vad_list は有声区間のリストのリストで、各話者ごとに有声区間がまとめられています。 上記のCSVファイルを訓練用、検証用、テスト用に8:1:1の割合で分割し、それぞれ train.csv 、 valid.csv 、 test.csv として保存します。 モデルの訓練 以下のスクリプトを用いて訓練を実行しました。他のハイパーパラメータはデフォルトのものを使用しています。訓練は、Vertex AIのT4 GPUを4枚搭載したインスタンスで行いました。 python train.py \ --data_train_path train.csv \ --data_val_path valid.csv \ --data_test_path test.csv \ --vap_encoder_type cpc \ --vap_cpc_model_pt ../asset/cpc/60k_epoch4-d0f474de.pt \ --vap_freeze_encoder 1 \ --vap_channel_layers 1 \ --vap_cross_layers 3 \ --vap_context_limit -1 \ --vap_context_limit_cpc_sec -1 \ --vap_frame_hz 5 \ --event_frame_hz 5 \ --opt_early_stopping 0 \ --opt_save_top_k 5 \ --opt_max_epochs 25 \ --opt_saved_dir ./trained_model/ \ --data_batch_size 8 \ --devices "0,1,2,3" \ --seed 0 訓練の結果、11エポック目以降でトレーニングデータに対する損失は減少していきましたが、検証データに対する損失が増加し始め、過学習の傾向が見られました。以下はTrainとValidationの損失関数の推移です。 発話終了検知の性能検証 VAPおよびVADを用いて、発話終了検知の性能を比較・検証しました。 評価時の推論はVAP、VADともにM2 macのCPUで行いました。 方法 検証データ 「えっと、来週の水曜日の午後5時に5人で予約できますか?」という発話内容で音響条件を変えて2種類の音声データを用意して使用しました。 audio_48kHz :スマートフォンで録音した48kHzの音声を16kHzにダウンサンプリング audio_8kHz :電話音声として録音された8kHzの音声を16kHzにアップサンプリング (注)2つの音声は完全に個別のものであるため、音声長が異なっています。 評価 以下の指標をプロットして3段組みのグラフを作成し、モデルの性能を視覚的に評価しました。 音声波形(Amplitude) 音声活動(Voice Activity) ターン判定(User/Bot Turn) VAPを用いた発話終了検知 VAPはマルチチャネルの音声ストリームを入力として受け取り、それぞれの音響特徴量を抽出します。この特徴量を基に、現在話している話者が引き続き発話を続ける確率(p_now)と、近い将来の話者確率(p_future)を出力します。用いた推論コードは以下の通りです。推論コードを動かすには VAPのリポジトリ をクローンしておく必要があります。 from rvap.vap_main.vap_main import VAPRealTime from pathlib import Path import matplotlib.pyplot as plt from matplotlib.gridspec import GridSpec import soundfile as sf import numpy as np def process_audio_file_with_plot(single_channel_audio_path, score_threshold=0.5): device = 'cpu' vap = VAPRealTime(vap_model, cpc_model, device, frame_rate, context_len_sec) # 音声データの読み込み audio_data, sample_rate = sf.read(single_channel_audio_path) print(audio_data.shape, audio_data.ndim) if audio_data.ndim == 2 and audio_data.shape[1] != 1: import librosa audio_data = librosa.to_mono(audio_data.T) assert sample_rate == 16000, "The sample rate of the input audio file must be 16kHz." x1 = audio_data x2 = np.zeros_like(x1) # 結果を格納するリスト p_now_list = [] p_future_list = [] turn_now_list = [] # p_nowに基づくターン turn_future_list = [] # p_futureに基づくターン frame_indices = [] vap_processor = VAPRealTime(vap_model, cpc_model, device, frame_rate, context_len_sec) current_x1 = np.zeros(vap_processor.frame_contxt_padding) current_x2 = np.zeros(vap_processor.frame_contxt_padding) FRAME_SIZE_INPUT = 160 # プロット用のfigureを作成 fig = plt.figure(figsize=(15, 10)) gs = GridSpec(3, 1, height_ratios=[1, 1, 0.5]) # 波形プロット用のサブプロット ax1 = fig.add_subplot(gs[0]) ax1.grid(True) # グリッドを表示 # VAP結果プロット用のサブプロット ax2 = fig.add_subplot(gs[1]) ax2.grid(True) # グリッドを表示 # ターン表示用のサブプロット ax3 = fig.add_subplot(gs[2]) ax3.grid(True) # グリッドを表示 for i in range(0, len(x1), FRAME_SIZE_INPUT): x1_frame = x1[i:i + FRAME_SIZE_INPUT] x2_frame = x2[i:i + FRAME_SIZE_INPUT] if len(x1_frame) < FRAME_SIZE_INPUT: break current_x1 = np.concatenate([current_x1, x1_frame]) current_x2 = np.concatenate([current_x2, x2_frame]) if len(current_x1) < vap_processor.audio_frame_size: continue vap_processor.process_vap(current_x1, current_x2) # 結果を保存 frame_indices.append(i / sample_rate) # 時間(秒)に変換 p_now_list.append(vap_processor.result_p_now[0]) p_future_list.append(vap_processor.result_p_future[0]) # ターン情報を決定 (閾値を0.5とする) turn_now = 1 if vap_processor.result_p_now[0] > score_threshold else 0 turn_future = 1 if vap_processor.result_p_future[0] > score_threshold else 0 turn_now_list.append(turn_now) turn_future_list.append(turn_future) current_x1 = current_x1[-vap_processor.frame_contxt_padding:] current_x2 = current_x2[-vap_processor.frame_contxt_padding:] # 波形のプロット time = np.arange(len(x1)) / sample_rate ax1.plot(time, x1) ax1.set_title('Audio Waveform') ax1.set_ylabel('Amplitude') ax1.set_xticklabels([]) # x軸のラベルを非表示 # VAP結果のプロット ax2.plot(frame_indices, p_now_list, label='p_now', color='blue') ax2.plot(frame_indices, p_future_list, label='p_future', color='red') ax2.set_title('Voice Activity Projection (VAP) Result') ax2.set_ylabel('Probability') ax2.legend() ax2.grid(True) ax2.set_ylim(0, 1) ax2.set_xticklabels([]) # x軸のラベルを非表示 # ターンの表示 ax3.plot(frame_indices, turn_now_list, label='Turn (p_now)', color='blue') ax3.plot(frame_indices, turn_future_list, label='Turn (p_future)', color='red', linestyle='--') ax3.set_ylim(-0.5, 1.5) ax3.set_yticks([0, 1]) ax3.set_yticklabels(['Bot', 'User']) ax3.set_title('Turn State (Score Threshold: {})'.format(score_threshold)) ax3.set_xlabel('Time (s)') ax3.legend() ax3.grid(True) # x軸の範囲を統一 xlim = (0, time[-1]) ax1.set_xlim(xlim) ax2.set_xlim(xlim) ax3.set_xlim(xlim) plt.tight_layout() plt.show() audio_48kHz audio_8kHz audio_48kHzとaudio_8kHzともにVAPの出力確率が高止まりしてしまっています。これはユーザが常にターンをとっていると予測していることを示しています。本来であれば、ユーザの発話が終わって無音が続くタイミングで確率が低くなってほしいです。 これは、おそらくCEJCのデータは日常会話であり、予約対話のようなタスク指向対話とはドメインが違うことにより本来の性能が発揮できていないことによるものだと思われます。また、VAPモデルの訓練時に過学習傾向が見られたため、訓練がうまくいっていない可能性があります。 VADを用いた発話終了検知 今回の検証ではVADを行うモデルとして silero-VAD を用います。 silero-VADは、入力された音声チャンクに対して有声か無声かを判定するモデルです。このモデルを用いて、20msごとの音声チャンクを連続的に処理し、各チャンクの有声判定を行います。この有声判定結果を基にして、一定時間以上の無声区間を検知したら発話終了検知を行うという ルール で発話終了検知を行います。用いた推論コードは以下の通りです。 from silero_vad import load_silero_vad import matplotlib.pyplot as plt from matplotlib.gridspec import GridSpec import numpy as np import soundfile as sf import torch class AudioBuffer: def __init__(self, window_size_samples, sample_rate): self.window_size_samples = window_size_samples self.buffer = torch.zeros(window_size_samples) self.sample_rate = sample_rate def update(self, new_chunk): # バッファを左にシフトして新しいチャンクを追加 shift_size = len(new_chunk) self.buffer = torch.cat([self.buffer[shift_size:], new_chunk]) return self.buffer def plot_realtime_vad_analysis(wav_path, threshold=0.5, chunk_size_ms=20, silence_threshold_ms=500): # モデルのロード model = load_silero_vad() # 音声データの読み込み audio_data, sample_rate = sf.read(wav_path) wav = torch.from_numpy(audio_data).float() # パラメータ設定 window_size_samples = 512 if sample_rate == 16000 else 256 chunk_size_samples = int(chunk_size_ms * sample_rate / 1000) silence_threshold_samples = int(silence_threshold_ms * sample_rate / 1000) # バッファの初期化 buffer = AudioBuffer(window_size_samples, sample_rate) # 結果格納用の配列 vad_results = np.zeros(len(audio_data)) turn_states = np.zeros(len(audio_data)) # 状態の追跡用変数 is_speaking = False silence_counter = 0 current_turn = 0 # chunk_size_samplesごとの処理 for i in range(0, len(audio_data), chunk_size_samples): # 現在のチャンクを取得 chunk_end = min(i + chunk_size_samples, len(audio_data)) current_chunk = wav[i:chunk_end] # チャンクが小さい場合はパディング if len(current_chunk) < chunk_size_samples: padding = torch.zeros(chunk_size_samples - len(current_chunk)) current_chunk = torch.cat([current_chunk, padding]) # バッファを更新 current_window = buffer.update(current_chunk) # VADの判定 speech_prob = model(current_window, sample_rate).item() is_speech = speech_prob >= threshold # VAD結果の保存 vad_results[i:chunk_end] = 1 if is_speech else 0 # ターン状態の更新 if is_speech: is_speaking = True silence_counter = 0 current_turn = 1 else: if is_speaking: silence_counter += chunk_size_samples if silence_counter >= silence_threshold_samples: is_speaking = False current_turn = 0 # ターン状態の保存 turn_states[i:chunk_end] = current_turn # プロット fig = plt.figure(figsize=(15, 10)) gs = GridSpec(3, 1, height_ratios=[1, 1, 0.5]) # 波形プロット ax1 = fig.add_subplot(gs[0]) time = np.arange(len(audio_data)) / sample_rate ax1.plot(time, audio_data) ax1.set_title('Audio Waveform') ax1.set_ylabel('Amplitude') ax1.set_xticklabels([]) ax1.grid(True) # VAD結果プロット ax2 = fig.add_subplot(gs[1]) ax2.plot(time, vad_results) ax2.set_ylim(-0.5, 1.5) ax2.set_yticks([0, 1]) ax2.set_title(f'Voice Activity Detection (VAD) Result') ax2.set_yticklabels(['Silent', 'Speech']) ax2.grid(True) # ターン状態プロット ax3 = fig.add_subplot(gs[2]) ax3.plot(time, turn_states, 'r-', label='Turn') ax3.set_ylim(-0.5, 1.5) ax3.set_yticks([0, 1]) ax3.set_title(f'Turn State (Silence Threshold: {silence_threshold_ms}ms)') ax3.set_yticklabels(['Bot', 'User']) ax3.set_xlabel('Time (s)') ax3.grid(True) ax3.legend() # x軸の範囲を統一 xlim = (0, time[-1]) ax1.set_xlim(xlim) ax2.set_xlim(xlim) ax3.set_xlim(xlim) plt.tight_layout() plt.show() audio_48kHz audio_8kHz VADを用いた発話終了検知はルールベースなので制御性が高いです。上記の例では無音継続長の閾値を調整することで、検知は遅れてしまうものの、途中の言い淀みにも頑健に処理できています。運用する際には、言い淀みに対する頑健さと検知の遅延とのトレードオフが最適になるように閾値を調整する必要がありそうです。 まとめ 本記事では、VAPやVADを用いた発話終了検知について扱いました。 VAPは柔軟な発話終了検知を行うための有望なアプローチだと思いますが、今回の検証ではそもそも学習がうまくいかず期待するような結果とはなりませんでした。また、学習がうまくいったとしても音響条件によって性能が左右されやすいので、性能面で改善の余地がまだまだ残されているなと感じました。 一方で、VADを用いた方法は、一定程度の発話終了検知の遅延が避けられないため、柔軟なターンテイキングを実現する方法としてベストな方法ではなさそうですが、制御性の面で機械学習的なアプローチよりも優れているため、システムには導入しやすいと思います。また、ハイパーパラメータを最適化することで、ある程度柔軟なターンテイキングに近づくのではないかと感じました。 今後、音声対話において会話の「間」というのは顧客体験を向上させるために、ますます重要になってくると思います。弊社AIチームとしても引き続きこの分野の動向を注視し、プロダクトへの応用可能性を探っていきたいと考えています。 明日は、インフラチームの大長から記事が上がる予定です。 ご精読ありがとうございました。 投稿 【AI Shift Advent Calendar 2024】VAD・VAPを用いた発話終了検知 は 株式会社AI Shift に最初に表示されました。
アバター
こんにちは、AIチームの干飯( @hosimesi11_ )です。 この記事は AI Shift Advent Calendar 10日目の記事になります。本記事では、FastAPIを使ったサーバのプロファイリングについて扱います。 本記事で用いたコードは こちら で公開しています。 はじめに 機械学習モデルのプロダクト組み込みが進むに連れて、データサイエンティストや機械学習エンジニアがモデルのプロダクト組み込みまでやることが増えてきていると感じています。ただ、GoogleのMLOpsの 記事 にある通り、これらの職種の方は本番環境クラスのサービスを構築できる経験豊富なソフトウェア エンジニアではない可能性が高いです。 Team skills: In an ML project, the team usually includes data scientists or ML researchers, who focus on exploratory data analysis, model development, and experimentation. These members might not be experienced software engineers who can build production-class services. さらに、一般的に機械学習システムの推論は時間がかかることが多く、その際にパフォーマンスチューニングが必要になることも多いと思います。 しかし、ソフトウェアに関する知識が十分でないため、どのように調査を進めればよいのか分からないという課題に直面することがあります。 このような状況では、推測に頼ってチューニングを行うのではなく、まずはボトルネックとなっている処理を正確に計測し、特定することが重要です。 Rule 3. Measure. Don't tune for speed until you've measured, and even then don't unless one part of the code overwhelms the rest. Rob Pike's 5 Rules of Programming そこで、 本記事ではPythonスクリプトとFastAPIを使ったシステムのプロファイリングについて簡単なチャットシステムを題材にまとめていきたいと思います。 システムの外観 簡単なチャットシステムとして、ローカルに gemma-2-2b-jpn-it を載せてFastAPIを介してレスポンスを返すシステムを使用します。通常のPythonスクリプトでのプロファイリングとFastAPIでのプロファイリングの明確化のため以下の2つのコードをベースに利用します。Mac上で動作させるためmpsを使用し、パッケージマネージャーにはuvを使用しています。 通常のPythonスクリプト import logging import os import torch from transformers import pipeline logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) def main(query: str): device = "mps" if torch.backends.mps.is_available() else "cpu" logger.info(f"デバイス: {device}. Starting to load the model...") pipe = pipeline( "text-generation", model="google/gemma-2-2b-jpn-it", model_kwargs={"torch_dtype": torch.bfloat16}, token=os.environ.get("HUGGINGFACE_HUB_TOKEN"), device=device, ) logger.info("モデルのロードに成功しました")   outputs = pipe( query, return_full_text=False, max_new_tokens=256, ) assistant_response = outputs[0]["generated_text"].strip() return {"assistant_response": assistant_response} if __name__ == "__main__": query = "こんにちは" response = main(query=query) logger.info(response) FastAPI import os import torch from fastapi import Depends, FastAPI, HTTPException from pydantic import BaseModel class QueryRequest(BaseModel): query: str def get_pipeline(): try: device = "mps" if torch.backends.mps.is_available() else "cpu" logger.info(f"デバイス: {device}. Starting to load the model...") pipe = pipeline( "text-generation", model="google/gemma-2-2b-jpn-it", model_kwargs={"torch_dtype": torch.bfloat16}, token=os.environ.get("HUGGINGFACE_HUB_TOKEN"), device=device, ) logger.info("モデルのロードに成功しました") return pipe except Exception as e: raise RuntimeError(f"モデルのロードに失敗しました: {e}") app = FastAPI() @app.post("/generate/") async def generate(query: QueryRequest, pipe=Depends(get_pipeline)): try: outputs = pipe( query.query, return_full_text=False, max_new_tokens=1024, ) assistant_response = outputs[0]["generated_text"].strip() return {"assistant_response": assistant_response} except Exception as e: raise HTTPException(status_code=500, detail=f"応答の生成に失敗しました: {e}") @app.get("/") async def root(): return {"message": "Hello World"} if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000) 上記のFastAPIサーバは、リクエストを投げると以下のようにレスポンスが返ってくる構成です。 .PHONY: generate API_URL = http://localhost:8000/generate/ QUERY = こんにちは、自己紹介をお願いします。 generate: @curl -X POST "$(API_URL)" \ -H "Content-Type: application/json" \ -d '{"query": "$(QUERY)"}' {"assistant_response":"私は、**[あなたの名前]**です。\n[あなたの職業や専門分野]として活動しています。\n[あなたの興味や趣味]が好きです。\n\nよろしくお願いします!\n\n\n\n**補足**\n\n* 上記はあくまで例です。あなたの名前、職業、興味などを自由に記入してください。\n* 自己紹介は、相手に印象を与えるための重要な要素です。\n* 簡潔で分かりやすい文章で、相手に伝えることを心がけてください。"} 前提知識 FastAPIとは FastAPI は、 型ヒントを活用したPython製のASGIベースのWebフレームワークです。非同期処理もサポートした高性能フレームワークで、Web APIの開発に適しています。 プロファイリングとは プロファイリングとは、プログラムの実行時における挙動を分析する方法です。プログラムの各部分がどれだけ頻繁に呼ばれたかやCPU使用率や関数ごとの実行時間などを計測するCPUのプロファイルと、メモリ使用量などを計測するメモリプロファイルがよく用いられます。これらのプロファイリング手法は大きく2種類に分類できます。 決定論的プロファイリング(Deterministic Profiling) : 全ての関数の呼び出し回数や実行時間を正確に測定します。詳細な情報が得られますが、オーバーヘッドが発生しやすいのが特徴です。 統計的プロファイリング(Statistical Profiling) : 一定間隔でプログラムの実行状態をサンプリングし、統計的に性能を推測します。オーバーヘッドが少ない反面、相対的な指標のみを表示します。 uvとは uv はAstralが提供している Rust製のPythonのパッケージマネージャーです。Pythonバージョンとライブラリのバージョンをまとめて管理することができる上、高速に動作します。 プロファイラツール一覧 Pythonでプロファイリングを行いたい場合、以下のようなツールがあります。それぞれのツールについて、種類や手法、GitHubのスター数をまとめました。 CPUプロファイラ ツール名 種類 手法 GitHub Stars cProfile CPUプロファイラ 決定論的 標準ライブラリ profile CPUプロファイラ 決定論的 標準ライブラリ py-spy CPUプロファイラ 統計的 12.9k   stars Yappi CPUプロファイラ 決定論的 1.5k   stars Scalene CPU+メモリプロファイラ 統計的 12.2k   stars aiomonitor 非同期処理特化のCPUプロファイラ 統計的 663   stars メモリプロファイラ ツール名 種類 手法 GitHub Stars Memray メモリプロファイラ 決定論的 13.4k   stars Scalene メモリプロファイラ 統計的 12.2k   stars memory-profiler メモリプロファイラ 統計的 4.4k   stars ※ GitHubスター数は2024年12月現在の数です。 ヒストリーを見ると、Memrayが近年一気に人気になったことがわかります。 各ツールの使用方法 profile/cProfile profileとcProfileはPython標準ライブラリの決定論的プロファイラです。profileはピュアPython実装になっており、実際に計測するとオーバーヘッドが生じやすいです。cProfileはその名の通り、profileのインターフェイスを真似たC拡張のモジュールで、profileに比べて高速に動作するためcProfileを使うことが推奨されています。 公式ドキュメント: https://docs.python.org/ja/3/library/profile.html GitHub: profile: https://github.com/python/cpython/blob/main/Lib/profile.py cProfile: https://github.com/python/cpython/blob/main/Lib/cProfile.py 一般的な使用方法 cProfileでプロファイルしたい場合、アプリケーションの中でcProfileを呼び出す方法と、モジュールとして実行する方法の2種類があります。アプリケーションの中で呼び出す際は、以下のようにcProfileで測りたい関数をwrapして実行することで計測することができます。 import cProfile response = cProfile.run("main(query=query)") モジュールとして実行したい場合はコマンドにモジュールを指定します。 uv run python -m cProfile -o output.pstats src/script.py これにより、スクリプトがプロファイルされ、 output.pstats に出力されます。これを gprof2dot を使用して可視化し、pngとして保存することでより詳細な情報を見ることができます。 gprof2dot -f pstats output.pstats | dot -Tpng -o output.png この図は関数呼び出しごとにどこに何%かかっているかがノードとエッジからわかるようになっています。青系は比較的軽い処理になっており、緑や赤になるにつれて時間がかかっている処理になります。 今回の例だとforwardを含めた module::1732::_wrapped_call_impl とそれに紐づく一連の関数呼び出し ( module: :740: :_call_impl 、 modeling_gemm: :752: :forward 、 linear::forward 、 modeling_gemm: :208: :forward )が全体の計54.86%の時間を消費しています。その中でスレッドの待機時間も約42.1%を占めているため、スレッドの非同期化などの最適化が検討できそうです。 FastAPIでの使用方法 FastAPIで使いたい場合はrouterの中でcProfile.runを呼び出したり、モジュールとして呼び出してもいいですが、ミドルウェアを自作することも可能です。 公式ドキュメント によると、FastAPIのミドルウェアには以下の特徴があります。 ミドルウェアはアプリケーションに届いたそれぞれの リクエスト を受け取る リクエスト に対して必要なコードを実行可能 実行後、アプリケーションの残りの部分に リクエスト を渡して処理させる ミドルウェアはアプリケーションによって生成された レスポンス を受け取る レスポンス に対して必要なコードを実行可能 レスポンス を返却 つまり、以下のような順序で処理が行われます。 このミドルウェアを自作してみます。 /generate/ に来たリクエストにのみrequest_idを生成して、プロファイラを実行した後に f"profile_{request_id}.pstats"  という名前で保存します。これによってリクエストごとに詳細なプロファイリングが可能になります。 @app.middleware("http") async def cprofile_middleware(request: Request, call_next): profile_target_paths = ["/generate/"] if request.url.path in profile_target_paths: request_id = uuid.uuid4() profile_filename = f"profile_{request_id}.pstats" profiler = cProfile.Profile() profiler.enable() response = await call_next(request) profiler.disable() profiler.dump_stats(profile_filename)  return response else: response = await call_next(request) return response あとは通常のPythonスクリプトと同様にgprof2dotで描画します。実際に描画した結果は以下のようになります。 py-spy py-spy は、2019年7月に公開されたRustで書かれた統計的プロファイラです。2024年12月現在、v0.4.0がリリースされており、Python 3.13にも対応しています。 GitHub: https://github.com/benfred/py-spy まず、ライブラリをインストールします。 $ uv add py-spy 一般的な使用方法 まず、計測したいPythonスクリプトを実行します。そして、実行中のPythonスクリプトのプロセスIDを指定することでプロファイラを行います。実行中のPythonプロセスのプロファイルを取得するには、以下のようにPIDを指定します。 $ uv run py-spy top --pid <PID> また、スクリプトを py-spy 経由で実行し、プロファイル結果をSVG形式で保存することもできます。 $ uv run py-spy record -o profile.svg -- python src/script.py 生成された profile.svg をブラウザで開くと、フレームグラフとして視覚化されたプロファイル情報が確認できます。 ※ Macを使っている場合、セキュリティの観点からpy-spyをそのまま使えないので、Dockerコンテナ内で使うことをお勧めします。 FastAPIでの使用方法 py-spyではそのままFastAPIを立ち上げ、別のプロセスから以下のようにプロファイルすることができます。 ps aux | grep uvicorn py-spy record -o fastapi_profile.svg --pid <PID> ただし、毎回uvicornのプロセスを指定するのは大変です。その場合、通常のスクリプトと同じようにuvicornもpythonモジュールとpy-spy経由で実行することができます。生成されるファイルも通常のスクリプトと同じになります。 py-spy record -o profile.svg -- python -m uvicorn src.main:app --host 0.0.0.0 --port 8000 Yappi Yappiは2022年8月に公開されたマルチスレッド、asyncio、geventに対応した決定論的プロファイラです。近年のPythonに搭載されている非同期プログラミングの機能を網羅的に扱っているプロファイラです。 GitHub: https://github.com/sumerc/yappi READMEではYappiの特徴を以下のように記述しています。 高速: 完全に C で書かれており、高速 ユニーク: マルチスレッド、asyncio、geventプロファイリングをサポート 直感的: プロファイラはいつでも、どのスレッドからでも開始/停止でき、結果を取得可能 標準準拠: 結果はcallgrind または pstat 形式で保存可能 豊富な機能セット: 実際のCPU 時間が表示され、さまざまなセッションから集計・並べ替え可能 堅牢性: プロジェクトが成熟 まず、ライブラリをインストールします。 $ uv add yappi 一般的な使用方法 Yappiでプロファイルしたい場合、アプリケーションの中で以下のように呼び出します。Yappiはclock typeにCPU TimeとWALL Timeを用意しています。CPU Timeは実際に CPUを実際に使用して計算を行っていた総時間です。一方でWALL Timeは I/O待ちやスリープ時間なども含めた、プログラムの総実行時間です。 ユーザーが測りたいものに合わせて適切なclock typeを指定する必要があります。 import yappi if __name__ == "__main__": query = "こんにちは" yappi.set_clock_type("cpu") yappi.start() response = main(query=query)  yappi.get_func_stats().print_all() yappi.get_thread_stats().print_all() logger.info(response) すると以下のようにCLI上に結果が表示されます。スレッドごとに結果が表示され、各スレッドの合計実行時間や、スケジュールされた回数などを確認できます。 ※ ttot: How much time this thread has spent in total. ※ scnt: How many times this thread is scheduled. name id tid ttot scnt _MainThread 0 8387219264 24.66898 4 TMonitor 1 6119763968 0.000551 3 Yappiの強みは非同期処理にあります。非同期処理でも先ほどと同じようなコードで実際のWALL Timeを測ることができます。結果は上記と同じ形式で表示されます。 async def main_async(query: str): return main(query=query) if __name__ == "__main__": query = "こんにちは" yappi. set_clock_type ( "WALL" ) with yappi. run (): asyncio. run (main_async(query=query)) yappi. get_func_stats (). print_all () JetBrains製品を使っている方は、PyCharmのデフォルトプロファイラとして搭載されているので、簡単に使用することができます。 FastAPIでの使用方法 FastAPIアプリケーションでYappiを使用する場合もミドルウェアとしてプロファイリングを行うことが有効です。 @app.middleware("http") async def yappi_middleware(request: Request, call_next): profile_target_paths = ["/generate/"]  if request.url.path in profile_target_paths: request_id = uuid.uuid4() func_profile_filename = f"func_profile_{request_id}.pstats" thread_profile_filename = f"thread_profile_{request_id}.pstats"      yappi.set_clock_type("cpu")  yappi.clear_stats() yappi.start() response = await call_next(request)  yappi.stop() func_stats = yappi.get_func_stats() func_stats.print_all() func_stats.save(func_profile_filename, type="pstat")  thread_stats = yappi.get_thread_stats() thread_stats.print_all() with open(thread_profile_filename, 'w') as f: thread_stats.print_all(out=f) return response else: response = await call_next(request) return response 上記のように実行することで、スレッド単位と関数単位のプロファイリングができ、CLIに結果が表示されます。また、statsファイルとしても保存されているので、cProfileなどと同様にgprof2dotを使うことで画像で描画できます。 Scalene Scaleneは 2021年3月に公開された CPU、GPU、メモリ全てを含んだオールインワンのプロファイラです。さらにAIを標準で組み込んでおり、AI を活用した最適化の提案を組み込んでいるのも特徴的です。行もしくは関数ごとにプロファイルを行います。 GitHub: https://github.com/plasma-umass/scalene まず、ライブラリをインストールします。 $ uv add scalene 一般的な使用方法 Scaleneでプロファイルしたい場合、スクリプトを scalene コマンドで実行します。 uv run scalene src/scalene_script.py 実行後、ローカルにHTMLファイルが吐き出され、CPUおよびメモリ使用量の詳細なレポートが出力されます。行単位でクリックするとLLMが最適化の提案をしてくれるので、オールインワンで修正可能になります。行単位で確認しつつ、LLMからの提案も加味しながら最適化していくプロセスがいいと思います。NVIDIAのGPUを使用している場合は、GPUのプロファイルも同時に行うことが可能です。 FastAPIでの使用方法 FastAPIをScaleneでプロファイルする場合、scaleneをmoduleで呼び出します。 .venv/bin/python -m scalene --html --outfile scalene.html -m uvicorn src.main:app --host 0.0.0.0 --port 8000 プロファイル結果をHTMLファイルとして出力していますが、Json出力もコンソール出力も可能です。 他にもcProfileなどと同様にアプリケーション内で呼び出す方法もありますが、コマンドラインから呼び出すことで、プロセス全体の監視にも役立ちます。 モジュールで呼び出した場合は以下のような結果が表示されます。 Memray Memrayは2022年4月にBloombergが公開したPythonのメモリプロファイラです。 公式ドキュメント: https://bloomberg.github.io/memray/ GitHub: https://github.com/bloomberg/memray 公式ドキュメントではMemrayの特徴を以下のように記述しています。 高速に動作 収集されたメモリ使用量データに関するフレームグラフなどのさまざまなレポートを生成可能 ネイティブ スレッドで動作 まず、ライブラリをインストールします。 $ uv add memray  一般的な使用方法 Memrayでプロファイルしたい場合、計測したいスクリプトを memray run コマンドで実行し、メモリプロファイルを取得します。 uv run memray run --output memray.bin src/memray_script.py プロファイル結果のバイナリをHTMLファイルとして吐き出し、フレームグラフを描画します。フレームグラフはアプリケーションのスタックフレームを階層ごとに視覚的に表現したものです。 uv run memray flamegraph memray.bin 生成された memray-flamegraph.html をブラウザで開くと、メモリ割り当てのフレームグラフが確認できます。他にもsummaryやlive、statsなど複数コマンドがあり、CLI上でリアルタイムに結果が確認できるので自身の使いやすいものを選びながらメモリ使用量の多い箇所を特定していくプロセスが良いと思います。 FastAPIでの使用方法 FastAPIでNemrayを使用するには、コード内で memray.Tracker を使用するか、Memray経由でuvicornを起動します。 memray.Tracker を使いたい場合、メモリをプロファイルしたい処理をwithブロックで囲みます。 from memray import Tracker @app.post("/generate/") async def generate(query: QueryRequest, pipe=Depends(get_pipeline)): try: with Tracker("memray.bin") outputs = pipe( query.query, return_full_text=False, max_new_tokens=256, ) assistant_response = outputs[0]["generated_text"].strip() return {"assistant_response": assistant_response} except Exception as e: raise HTTPException(status_code=500, detail=f"応答の生成に失敗しました: {e}") with ブロック内のメモリ割り当てが追跡され、 memray.bin として保存されます。その後は先ほどと同様にコマンドラインでフレームグラフを生成します。ただ、基本的にこちらも以下のようにモジュール経由で呼び出す方がコードの変更も少なくお手軽かと思います。 以下はlive形式で実行した結果になります。 .venv/bin/python -m memray run --live -m uvicorn src.main:app --host 0.0.0.0 --port 8000 .venv/bin/python -m memray run --live -m uvicorn src.main:app --host 0.0.0.0 --port 8000 memory-profiler memory-profilerは2012年4月に公開された行ごとのメモリ使用量を計測できるプロファイラです。 GitHub: https://github.com/pythonprofilers/memory_profiler まず、ライブラリをインストールします。 $ uv add memory-profiler 一般的な使用方法 memory-profilerでプロファイルしたい場合、アプリケーションの中でmemory-profilerを呼び出す方法と、mprof経由で実行する方法の2種類があります。アプリケーションの中で呼び出す際は、lineprofilerと同じように測りたい関数にデコレータをつけます。一方、mprof経由だと以下のようにmprof経由で計測したいスクリプトを実行します。そうすると、datファイルが吐き出されるので、mprof経由でplotをします。 $ uv run mprof run src/mprof_script.py $ uv run mprof plot そうすると時間ごとのメモリ使用量が描画されるのでメモリがどのように使用されているかが確認できます。 FastAPIでの使用方法 FastAPIで特定のエンドポイントのメモリ使用量を測定するには、 @profile デコレータを使用します。以下のように memory_profiler から profile をimportし、FastAPIのrouterと同じようにデコレータとしてつけます。先ほどと同様にdataファイルが吐き出されるので適切に描画することで、メモリの使われ方が確認できます。 from memory_profiler import profile @profile @app.post("/generate/") def generate(query: QueryRequest, pipe=Depends(get_pipeline)): try: outputs = pipe( query.query, return_full_text=False, max_new_tokens=256, ) assistant_response = outputs[0]["generated_text"].strip() return {"assistant_response": assistant_response}  except Exception as e: raise HTTPException(status_code=500, detail=f"応答の生成に失敗しました: {e}") memory-profiler はオーバーヘッドが大きいので、開発環境での使用を推奨します。また、モジュール経由でも呼び出すことができるので、そちらの使い方もおすすめです。 おわりに 今回はPython(特にFastAPI)でプロファイリングする方法を紹介しました。 他にもfastapi_profilerというツールも存在しますが、 これらのツールを使うことでサーバ のボトルネックを特定し、効率的なパフォーマンスチューニングが可能になります。パフォーマンスチューニングしたい場合は、まずプロファイラを入れて計測してみることをお勧めします。 個人的には、非同期処理などを使っておらず、ざっくり処理のボトルネックを追いたい場合はcProfile、メモリリークなどメモリをプロファイリングしたい場合はMemrayを使うのがいいのかなと思います。また、Scaleneもコミュニティが活発なので、今後のアップデートをウォッチしたいです。 明日のAdvent Calendar 11日目の記事は、同じチームの大竹による VAD・VAPを用いた発話終了検知 の記事の予定です。こちらもよろしくお願いいたします。 参考 https://cloud.google.com/architecture/mlops-continuous-delivery-and-automation-pipelines-in-machine-learning https://fastapi.tiangolo.com/ https://github.com/plasma-umass/scalene https://github.com/sumerc/yappi https://github.com/bloomberg/memray https://github.com/benfred/py-spy 投稿 【AI Shift Advent Calendar 2024】FastAPIのプロファイリング は 株式会社AI Shift に最初に表示されました。
アバター
こんにちは、AIチームの杉山です。 本記事は AI Shift Advent Calendar 2024 の6日目の記事です。 今回の記事では音声自動応答サービスにおけるエンティティ抽出の課題をOpenAI gpt-4o-audioを用いて解決できるか検証します。 音声自動応答サービスにおけるエンティティ抽出の課題 ちょうど 3年前の今日、弊社のAdvent Calendar で音声自動応答サービスにおけるエンティティ抽出の課題の紹介と解決に向けたアプローチの記事を私が執筆していました。 課題の詳細に関しては繰り返しになるため上記記事を参照していただければと思いますが、簡単に再掲します。 音声認識結果を用いたエンティティ抽出の例 電話自動応答サービスによる店舗予約の例を考えます。 いわゆるタスク指向対話のユーザーとボットのやりとりの中で、予約の完了に必要な情報(ex. 日時、人数、店舗名など)をヒアリングします。この時、ユーザーの発話音声の中からそのタスク完了に必要な用語(以降エンティティ)の抽出を精度高く実現できる必要があります。 しかし、発話内容を音声認識したテキストには音声認識誤りが含まれる可能性があり、その場合正解エンティティと大きく表層が離れるため、単純な編集距離やトークン埋め込みでは上手く行かないケースが頻発します。 先述の記事では、音声認識誤りが含まれるかもしれないテキストに対して、音声としての近さを考慮してエンティティ抽出を行うために音素を用いたアプローチを実施していましたが、音声という連続的な情報を一度テキストという離散的な情報に変換したことでさまざまな情報が落ちていると考えられるため、できれば音声のまま行いたいと考えています。 事前に用意しているエンティティ群を全てMFCCなどの音響特徴に変換しておいて、発話音声との類似度を測る方式なども検証しましたが、電話音声に含まれるノイズや、言い淀み・「xxです」のような余分な語尾など音響特徴に影響を与える因子によりなかなか上手くいきませんでした。 当時はLLMですら一般的ではなかったですが、現在ではテキストだけでなくマルチモーダルでの生成AIサービスが広まっています。 そこで、当時から3年経った今それらの技術を用いると提示した課題の解決がどれくらいできるのか検証したいと思います。 OpenAI gpt-4o-audioを用いた音声データからのエンティティ抽出の検証 今回の検証では音声データを入力として、プロンプトに抽出対象のエンティティ一覧を与えておき、入力音声に読み方が近いと考えられるもの選択させます。音声マルチモーダルのモデルとしてOpenAIのgpt-4o-audio[1]を使用します。記事執筆時点ではgpt-4o-audioはプレビュー版のみの提供なのでコード内でもpreviewを指定しています。 以下にサンプルコードを示します。 なお今回の検証では先述したモチベーションである、エンティティが含まれると思われるユーザー発話を音声のまま、テキストでプロンプトに与えたエンティティ集合との音の近さを考慮して抽出できるのか、を確認するものであるため、エンティティのタイプや個数は単純にしてあります。また簡単のためにパラメーターやプロンプトのチューニングは行っていません。 import base64 from openai import OpenAI client = OpenAI(api_key=YOUR_API_KEY) with open(YOUR_AUDIO_FILE_PATH, "rb") as wav_file: wav_data = wav_file.read() encoded_string = base64.b64encode(wav_data).decode('utf-8') completion = client.chat.completions.create( model="gpt-4o-audio-preview", modalities=["text"], # 出力のモーダル。音声も出力したい場合は["text", "audio"]とする。 messages=[ { "role": "user", "content": [ { "type": "text", "text": """ 入力音声の認識結果から、読み方の近さを考慮して一番尤もらしいものを 次の候補から選んでください。候補:['代々木', '渋谷', '豊洲', '銀座', '品川'] """ }, { "type": "input_audio", "input_audio": { # 入力に音声を与える場合はinput_audioとしてbase64エンコードした文字列を指定 "data": encoded_string, "format": "wav" } } ] }, ] ) print(completion.choices[0].message.content) 今回、はっきり「豊洲です」と発話した(つもりの)音声(clear_toyosu.wav)と、曖昧に「豊洲です」と発話した音声(ambiguous_toyosu.wav)の2種類で検証しました。 これまでの記事同様、QuickTime Playerで録音した音声を電話環境に近づけるために8kHzにダウンサンプリングし、wav形式に変換しています。 clear_toyosu.wav ambiguous_toyosu.wav それぞれの音声をGoogle音声認識にかけてみると前者は正しく「豊洲です」(confidence=0.97)、後者は「ナースです」(confidence=0.95)と認識されました。前者の場合であれば音声認識をしてNLP的なアプローチでも十分エンティティ抽出することができますが、後者の場合はそうはいかないため音の近さを考慮して抽出してくれることを期待します。 まず前者のパターンで実行した場合の結果を示します。実行ごとに結果が異なるため、3回分の結果を示します。 ・読み方が一番近いものは「豊洲」です。 ・豊洲 ・'豊洲'が読み方として一番近いです。 出力の形式は異なりましたが、このようにプロンプトにはテキストしか与えずともその読み方の近さを考慮して抽出できているようです。 こちらの音声は音声認識結果が[豊洲です]になるため、その結果とプロンプトの候補をテキスト上で比較している可能性も考えられるので、念の為与える候補を全てカタカナにして試してもみましたが同様の結果が得られたためその可能性は低いと思われます。 次に、後者の音声に対して実行した結果を、ブレが大きかったので多めに5回分示します。 ・申し訳ありませんが、私は音声を聞くことができません。音声の認識結果について詳しく教えていただければ、候補の中から選ぶお手伝いをさせていただきます。 ・私は話者を特定することはできません。 ・いただいたサンプルからスピーカーや話者を特定する機能はありません。 ・選択された候補は「代々木」です。 ・申し訳ありませんが、私は音声認識には対応しておりません。 音声認識を誤るような曖昧な発話音声に対しては、そもそも選択肢の中から選ぶことすら多くのケースでしてくれない、という結果になりました。一部選択できたケースに関しても、[豊洲です]と発話したつもりですが音声認識させると[ナースです]、となってしまうこちらの曖昧な音声では期待した選択結果は得ることができませんでした。 実際のプロダクトへの適用可能性については、もう少し多様なパターンの音声をログから選定して定量的に評価していきたいと思いますが、やはり音声認識結果と抽出エンティティの候補に近いものが存在しないような発話に対しては、ロジック単体でなく、再度明瞭に発話してもらうようインタラクションを行うなど音声対話全体として解決に取り組む必要があると感じました。 終わりに 今回の記事では、OpenAI gpt-4o-audioを用いて音声データからのエンティティ抽出の検証を行いました。音声認識が正しく行われるような明瞭な音声に対してはgpt-4o-audioを用いてaudio-to-textとして対応することができそうだということがわかりました。一方で、曖昧な音声に対してはそれ単体では困難であることは以前変わらず、対話であるという特性を活かしてインタラクティブに解決を目指していく必要がありそうです。 最後に余談ですが、先日 こちらの記事 で検証したRealtime APIはこのモデルを低レイテンシで動作するようホスティングされているモデルを利用しているようです。[2] イチゴを買って会場に届ける電話音声対話のデモ映像は大きな反響を呼びましたが、今回のような課題は当面まだまだ見つかると思いますので、AI Shiftとして引き続き日本語電話音声自動応答の研究開発を継続し、より良い体験のプロダクトを提供していきたいと思います。 こういった取り組みにチャレンジしていきたいという方がおられましたら、ぜひお声がけください! 明日はフロントチームの水野による記事を公開予定です。ぜひそちらもご覧ください。 参考 https://platform.openai.com/docs/guides/audio https://platform.openai.com/docs/guides/audio?audio-generation-quickstart-example=audio-in#how-is-audio-in-chat-completions-different-from-the-realtime-api 投稿 【AI Shift Advent Calendar 2024】OpenAI gpt-4o-audioを用いた音声データからのエンティティ抽出の検証 は 株式会社AI Shift に最初に表示されました。
アバター
こんにちは!生成AIビジネス事業部で研修開発の責任者を務めている伊藤 優( @yuuito1995 )です。 AdventCalendar の5日目を担当させていただきます。(今日が誕生日なので、12/5を担当することにしました) 我々は今、生成AIを使いこなすための研修をサービスとして提供しています。 様々な企業に研修を提供する中で感じた、現場のリアルや普及までの道のりを今回はまとめさせていただきます。 この1年の動きについて ちょうど1年前の2023/12/5、当時別子会社だった株式会社CAリスキリングパートナーズの2023年12月5日の合宿にて、生成AIの研修に特化していこうと決めました。サイバーエージェント社内でもちょうど 生成AI徹底理解リスキリング for Everyone が完了し、全社員の99.6%が受講完了したという結果が出てきたタイミングでした。2月末でのAI Shiftへの統合を経て、そこからちょうど1年間、様々なお客様からご相談を頂き生成AIの研修をやり続けてきました。 ※ https://www.cyberagent.co.jp/way/list/detail/id=30434 研修をやっていくうえでのスタンスとして、「研修を受けてくれた人が受けて終わり」ではなく、「しっかりその後使えるようになってもらいたい」というところもあったため、お客様の課題を伺いながらカスタマイズして研修を行い、かつできるだけリアルで実施して、その場で様々な疑問に答えていくというところを基本としていました。 その結果、この1年でリアルでの実施だけでも数百名の方に研修を受けていただくことができました。(おそらく、サイバーエージェントグループ内で2番目(CHOの曽山の次)に登壇・講演した1年だったんじゃないかと思います) そのため、実際に生成AIを使ったユーザーが「実際どこで困っているのか?」「実際に触ったとき、どこで躓いてしまうのか?」を受講者の方々からの声としてリアルに感じることができました。 企業の中で生成AIが使われるようになっていく過程を間近で見ていた身として、改めて言語化しようと思います。 この1年で感じた生成AI活用のリアル まず前提として、研修を提供するなかで一番多く聞かれたのが「 生成AIツールを導入したけど全然使われていない」 という声です。研修を行っている企業で「今週生成AI使いましたか?」と聞くと、手が上がるのはだいたい2割くらいです。それ以外の方は月数回使うかどうか?という状況です。 生成AIは世の中の様々なツールに比べると遥かに使い始めやすいツールだと思いますし、このブログを読まれている方からすれば「生成AIなんてそんなに難しいツールでもない」と思われる方もいるかもしれませんが、実際には普及に向けた道半ばだと思います。 これまでの研修から、受講者の状態は大きく4つに分類されるのではないかと感じています。 ①生成AI使い始め(社員の30% 生成AIを思いついたら使う) 珍しさもあって一回生成AIに触れてみる 触れてはみるものの、「実業務に近いテーマ」では触っていない なんか色々期待できそう、というので試してみるが、あまり満足いく精度ではない ②幻滅期(社員の50% 月数回触ってみる) 試してみて「こういうとき使えるかも?」というヒントは感じても、「自分でやったほうがいい」と感じて使わなくなってしまう 「回答内容があまり良くない」という理由で使わなくなってしまう ③活用加速期(社員の10% 週数回ほど定期的に利用) 「このとき使えるぞ」という実感が身につく 特定の業務に取り組むとき、基本的には生成AIを使うようになる イメージが付くようになり、次々に活用アイディアが浮かぶ ④定着期(社員の10% ほぼ毎日利用している) あらかた思いつくところではCopilot等の生成AIは使うようになっている。 生成AIの技術的な限界を感じ、その進化を待っている状態。 Dify等の新しいツールを試したり、開発を伴うプロジェクトを推進していきたい 生成AIが普及するための一番のポイントは、どの程度の人が③のフェーズにいるのか?というところだと思います。 一気に活用が進められるきっかけはどこにあるのか?を考えると、一番大きな理由は 「結局いつ使えばいいの?」というシーン(ユースケース)が明確にイメージできているか? という点だと思います。 自分自身を思い返しても生成AIをいつ使うかというシーンが明確に見つからないころは「これから生成AIを使うぞ!」と意気込んで使う必要があるため、「利用するハードル」は低いものの、「ページ(サービス)を開くハードル」がいつの間にか上がってしまっていて、「URL探して開くくらいなら自分でやったほうが早いか」となっていました。 明確に僕が生成AIを使うようになったのは、「プレスリリースの骨子を作成する時に生成AIを使うと、素案作成が半分くらいの時間で進められる」と気がついたときです。 <参考:プレスリリースの骨子を作る際に利用したプロンプトの具体例> # 役割 こんにちは、あなたは広報担当者で、企業のプレスリリースの制作を担当しています。 あなたの所属する企業は研修を通してリスキリングを支援する企業です、 下記の内容をもとに#プレスリリース草案を記載し、その後#フィードバックサイクルを実行してください # プレスリリース草案 ①タイトル ②概要 ③背景 ④カリキュラムの構成と詳細 ⑤今後の展望 # フィードバックサイクル 制作したプレスリリースの草案に対して、記者の目線からニュースバリューがどこにあるかをまとめてください。 また、ニュースバリューが十分でない場合、プレスリリースの内容に対して追記すべき項目をフィードバックしてください # プレスリリースのタイトル <タイトル> # プレスリリースに含めたい内容 <内容> その結果、プレスリリースを書くときの業務フローが ①リリースに必要な要素を洗い出す ②プロンプトを活用して骨子を作成 ③肉付けや、不要部分の調整 ④生成AIで誤字脱字等の校閲 ⑤社内確認フローの実施 という流れに変わりました。 このように、 生成AIありきの業務フロー が自分の中で出来上がったときに一気に活用する頻度が上がりました。 その後、「戦略やビジネス課題の壁打ちにも使えるのでは?」「プロンプトの改善点を洗い出せないか?」など、「この瞬間に使えるかも」と考えるようになり、さらに生成AI活用の幅を広げることができました。 このように、 「生成AIを使うユースケースがある」「それを使うための業務フローができているか?」 が生成AIを一気に活用できる様になるために重要なことだと思います。 そのため、我々の研修ではこの2つを考えていただける講義・ワークを準備しています。 このような経験を踏まえ、僕の中では企業において生成AIを活用していくためのステップを4つに分けて考えています。 まずは生成AIを利用することのできる環境を用意していくことがステップ1だと思います。社内でツールが導入される・ガイドラインが整備されるなどして、社内で生成AIを使うことのできる環境を整えることがステップ1です。 次のステップとして、各企業の中で生成AIを主体的に使い、「こういうときは生成AIが使える」というユースケースを生み出す事のできる社員を育てていく必要があります。生成AI活用プロジェクトを立ち上げたり、生成AI活用リーダーに向けた研修などを行っていくフェーズです。 生成AIの素晴らしいところは「プロンプトを共有すれば、誰でも自分の環境で生成AIを試すことができる」という点だと思っています。そのため、次のステップでは「〇〇のときに使えるプロンプト」が社内の中で蓄積され、それが社内に展開される仕組みを作っていく必要があります。 生成AIを使う社員が増えれば増えるほど、加速度的にユースケースも増え、それを目にして生成AIを使える人が増えていくはずです。 最後のステップ4で重要になるのは生成AIを使ってもらううえでのUX(EX)です。いかに業務のツールやプロセスの中で当たり前に生成AIを使える状態になっているか?です。 もしかするとSlackやTeamsなどのコミュニケーションツールの中に組み込まれている必要があるのかもしれませんし、多くの従業員がよく使うプロンプトは、もはや業務アプリケーションなどに組み込まれている方が良いのかもしれません。 「いつも通りボタンを押していたら自然に生成AIを使っていた」というような形で使うハードルを下げるとともに、業務アプリケーションや業務フローを生成AIありきに組み替えていくことが最終的には必要になると思います。 最後に 長々と書いてしまいましたが、この1年、生成AIを活用しようとする意欲のある方々に向けて本当に多くの研修を実施させていただきました。 研修を実施するなかで「こんなに前のめりにチャレンジしたいと思う人がいるのであれば日本の未来は明るい。もっと日本のビジネスマンの仕事を楽に、かつ効果的に成果を出せるようにしていかないと。」と思う様になりました。 微力ではありますが、来年以降も更に質の高い研修を提供し、「AIを民主化する」というAI Shiftのビジョンを実現することで、日本の未来に貢献していきます! 最後まで御覧いただきありがとうございました! 投稿 【AI Shift Advent Calendar 2024】生成AIの研修を1年やって感じた、生成AIが企業で普及するまで は 株式会社AI Shift に最初に表示されました。
アバター
(サムネイルはDALLE-3によって生成) こんにちは、AI Shiftの友松です。 この記事は AI Shift Advent Calendar の4日目の記事です。 今回はAnthropic社から発表されたMCP(Model Context Protocol)について取り上げたいと思います。 MCP はLLMと外部データソース、ツールのシームレスな連携を行うためのプロトコルです。LLMは人間で言えば脳みそに当たる部分です。その能力を引き出すためには外部のデータとの連携が不可欠です。人間も頭で考えるだけではなく、必要な情報にアクセスしながらあらゆる問題解決を行っています。 これまでもLLMと外部データを組み合わせる取り組みはされてきており、 Function Calling と呼ばれる機構を用いることでツールの選択、外部情報に問い合わせるための情報抽出などを定義して実行することができました。一方でFunction Callingでは都度外部情報にアクセスするために関数の定義を詳細にする必要がありました。 MCPでは、各情報ソースに対して実行可能な処理を"ツール"という形で定義し、そのツールをどのように使うかというのはLLMに判断させて実行を進行します。 "AIエージェント"を構築するにあたって、自分自身で判断し、行動選択をし、自ら外部の情報にアクセスし、次の意思決定につなげていく。といった一連のアプローチをMCPを用いることで可能になります。 具体的なセットアップについては他の記事でも既に参考になるものが多数出てきているため、そこに関しては簡潔にまとめます。 本記事ではMCPの動作を理解するためにSQLiteとの接続を行い、予約対話を行うAIエージェントの構築とトレースを行います。 今回の記事では以下の内容に触れます。 MCPを用いてローカルに構築したSQLiteにアクセス 予約管理を行うための最小限のデータベースの構築 上記による予約処理を自律的に実行するようなAIエージェントの構築 MCPが具体的にどんな処理を行っているかをトレース 1. 準備 MCPの Quickstart に準拠します。 差分としては、予約対話システムを構築するためにSQLiteで予約管理DBの構築を行います。 Macによるセットアップをしていますが、Windowsでやる場合は公式情報を参考にしつつ、予約DBの構築部分だけ本記事を参照ください。 1.1 環境 MacBook Pro 16インチ, 2021 Apple M1 Pro MacOS Ventura(13.6.8) 1.2 前提条件 Claude Desktop の最新版のインストール uv  0.4.18以上 Git SQLite ( to check) $ brew install uv git sqlite3 1.3 予約DBの構築 予約管理を行うためのDBを設定します。DB設計に関してはLLMとのやり取りによって作成しました。 1.3.1 DBへのアクセス sqlite3 ~/test.db <<EOF 1.3.2 予約枠テーブル 予約枠を取り扱うテーブルです。◯月◯日(date)の何時(start_time)から何時(end_time)までの予約枠が何枠(capacity)あるかという情報を管理します。 CREATE TABLE reservation_frames ( id INTEGER PRIMARY KEY AUTOINCREMENT, date DATE NOT NULL, start_time TIME NOT NULL, end_time TIME NOT NULL, capacity INTEGER NOT NULL ); 1.3.3 予約テーブル 予約情報を扱います。 どの予約枠(reservation_frame_id)に対して誰(customer_name, customer_contact)が予約をしているか、そして予約のステータス(confirmed, canceled, pending)を扱います。 CREATE TABLE reservations ( id INTEGER PRIMARY KEY AUTOINCREMENT, reservation_frame_id INTEGER NOT NULL, customer_name TEXT NOT NULL, customer_contact TEXT NOT NULL, status TEXT NOT NULL CHECK(status IN ('confirmed', 'cancelled', 'pending')), FOREIGN KEY (reservation_frame_id) REFERENCES reservation_frames(id) ); 1.3.4 予約枠情報の追加 12月5日から12月11日の10:00(start_time)から16:00(end_time)まで1時間毎の枠をそれぞれ空き枠(capacity)1で追加します。 INSERT INTO reservation_frames (date, start_time, end_time, capacity) VALUES ('2024-12-05', '10:00:00', '11:00:00', 1), ('2024-12-05', '11:00:00', '12:00:00', 1), ('2024-12-05', '12:00:00', '13:00:00', 1), ('2024-12-05', '13:00:00', '14:00:00', 1), ('2024-12-05', '14:00:00', '15:00:00', 1), ('2024-12-05', '15:00:00', '16:00:00', 1), ('2024-12-05', '16:00:00', '17:00:00', 1), ('2024-12-06', '10:00:00', '11:00:00', 1), ('2024-12-06', '11:00:00', '12:00:00', 1), ('2024-12-06', '12:00:00', '13:00:00', 1), ('2024-12-06', '13:00:00', '14:00:00', 1), ('2024-12-06', '14:00:00', '15:00:00', 1), ('2024-12-06', '15:00:00', '16:00:00', 1), ('2024-12-06', '16:00:00', '17:00:00', 1), ('2024-12-07', '10:00:00', '11:00:00', 1), ('2024-12-07', '11:00:00', '12:00:00', 1), ('2024-12-07', '12:00:00', '13:00:00', 1), ('2024-12-07', '13:00:00', '14:00:00', 1), ('2024-12-07', '14:00:00', '15:00:00', 1), ('2024-12-07', '15:00:00', '16:00:00', 1), ('2024-12-07', '16:00:00', '17:00:00', 1), ('2024-12-08', '10:00:00', '11:00:00', 1), ('2024-12-08', '11:00:00', '12:00:00', 1), ('2024-12-08', '12:00:00', '13:00:00', 1), ('2024-12-08', '13:00:00', '14:00:00', 1), ('2024-12-08', '14:00:00', '15:00:00', 1), ('2024-12-08', '15:00:00', '16:00:00', 1), ('2024-12-08', '16:00:00', '17:00:00', 1), ('2024-12-09', '10:00:00', '11:00:00', 1), ('2024-12-09', '11:00:00', '12:00:00', 1), ('2024-12-09', '12:00:00', '13:00:00', 1), ('2024-12-09', '13:00:00', '14:00:00', 1), ('2024-12-09', '14:00:00', '15:00:00', 1), ('2024-12-09', '15:00:00', '16:00:00', 1), ('2024-12-09', '16:00:00', '17:00:00', 1), ('2024-12-10', '10:00:00', '11:00:00', 1), ('2024-12-10', '11:00:00', '12:00:00', 1), ('2024-12-10', '12:00:00', '13:00:00', 1), ('2024-12-10', '13:00:00', '14:00:00', 1), ('2024-12-10', '14:00:00', '15:00:00', 1), ('2024-12-10', '15:00:00', '16:00:00', 1), ('2024-12-10', '16:00:00', '17:00:00', 1), ('2024-12-11', '10:00:00', '11:00:00', 1), ('2024-12-11', '11:00:00', '12:00:00', 1), ('2024-12-11', '12:00:00', '13:00:00', 1), ('2024-12-11', '13:00:00', '14:00:00', 1), ('2024-12-11', '14:00:00', '15:00:00', 1), ('2024-12-11', '15:00:00', '16:00:00', 1), ('2024-12-11', '16:00:00', '17:00:00', 1); 1.3.5 予約情報の追加 既に全体の7割が埋まっている状況を作るために予約枠情報をランダムでLLMに作ってもらいました。 INSERT INTO reservations (id, reservation_frame_id, customer_name, customer_contact, status) VALUES (1, 1, 'Customer_1', 'customer_1@example.com', 'confirmed'), (2, 3, 'Customer_2', 'customer_2@example.com', 'confirmed'), (3, 4, 'Customer_3', 'customer_3@example.com', 'confirmed'), (4, 5, 'Customer_4', 'customer_4@example.com', 'confirmed'), (5, 6, 'Customer_5', 'customer_5@example.com', 'confirmed'), (6, 8, 'Customer_6', 'customer_6@example.com', 'confirmed'), (7, 10, 'Customer_7', 'customer_7@example.com', 'confirmed'), (8, 11, 'Customer_8', 'customer_8@example.com', 'confirmed'), (9, 12, 'Customer_9', 'customer_9@example.com', 'confirmed'), (10, 14, 'Customer_10', 'customer_10@example.com', 'confirmed'), (11, 15, 'Customer_11', 'customer_11@example.com', 'confirmed'), (12, 17, 'Customer_12', 'customer_12@example.com', 'confirmed'), (13, 18, 'Customer_13', 'customer_13@example.com', 'confirmed'), (14, 19, 'Customer_14', 'customer_14@example.com', 'confirmed'), (15, 21, 'Customer_15', 'customer_15@example.com', 'confirmed'), (16, 22, 'Customer_16', 'customer_16@example.com', 'confirmed'), (17, 23, 'Customer_17', 'customer_17@example.com', 'confirmed'), (18, 25, 'Customer_18', 'customer_18@example.com', 'confirmed'), (19, 26, 'Customer_19', 'customer_19@example.com', 'confirmed'), (20, 27, 'Customer_20', 'customer_20@example.com', 'confirmed'), (21, 29, 'Customer_21', 'customer_21@example.com', 'confirmed'), (22, 30, 'Customer_22', 'customer_22@example.com', 'confirmed'), (23, 31, 'Customer_23', 'customer_23@example.com', 'confirmed'), (24, 33, 'Customer_24', 'customer_24@example.com', 'confirmed'), (25, 34, 'Customer_25', 'customer_25@example.com', 'confirmed'), (26, 35, 'Customer_26', 'customer_26@example.com', 'confirmed'), (27, 37, 'Customer_27', 'customer_27@example.com', 'confirmed'), (28, 38, 'Customer_28', 'customer_28@example.com', 'confirmed'), (29, 39, 'Customer_29', 'customer_29@example.com', 'confirmed'), (30, 41, 'Customer_30', 'customer_30@example.com', 'confirmed'), (31, 42, 'Customer_31', 'customer_31@example.com', 'confirmed'), (32, 43, 'Customer_32', 'customer_32@example.com', 'confirmed'), (33, 45, 'Customer_33', 'customer_33@example.com', 'confirmed'), (34, 46, 'Customer_34', 'customer_34@example.com', 'confirmed'), (35, 47, 'Customer_35', 'customer_35@example.com', 'confirmed'), (36, 49, 'Customer_36', 'customer_36@example.com', 'confirmed'); 1.4 configファイルの確立 ~/Library/Application Support/Claude/claude_desktop_config.json を編集。 { "mcpServers": { "sqlite": { "command": "uvx", "args": ["mcp-server-sqlite", "--db-path", "/Users/<YOUR_USERNAME>/test.db"] } } } 1.5 MCPサーバーの起動 $ uvx mcp-server-sqlite MCPの設定が完了し、Claudeの Start new chat をクリックするとチャット入力欄の右下に金槌のマークと6という数字が表示されています。クリックするとMCPが操作可能なツールの一覧が出てきます。今回はSQLiteに関する設定を行ったので、以下の操作が許可されています。詳しくは こちら を御覧ください Query Tools read-query: SELECTクエリを実行してデータベースからデータを読み取ります write-query: INSERT、UPDATE、またはDELETEクエリを実行する create-table: データベースに新しいテーブルを作成する Schema Tools list-table: データベース内のすべてのテーブルのリストを取得する describe-table: 特定のテーブルのスキーマ情報を表示する Analysis Tools append-insight: メモリソースに新しいビジネスインサイトを追加する 2. 予約対話の実行 まずは構築した予約対話AIエージェントとのやり取りを動画で御覧ください。このあとそれぞれのやり取りに関して細かくトレースを行います。 3. 動作のトレース これらのツールをユーザーからのQueryに合わせてToolを使い分け、タスクの実行を行います。実際に動画の中から一部のやり取りを切り出しながらMCPによる内部動作のトレースを行っていきます。 ① 「SQLiteのテーブルを参照して予約を取りたいです。」 list-table 質問に対してテーブルの一覧を参照しています。ここでは3つのテーブル( reservations , sqlite_sequence , reservation_frames )があることがわかります。 Request {} Response [{'name': 'reservations'}, {'name': 'sqlite_sequence'}, {'name': 'reservation_frames'}] describe-data テーブルの一覧に対して、関連しそうなreservationsテーブルとreservation_framesテーブルのスキーマ定義を見に行っています。 reservationsテーブルに対するRequest { `table_name`: `reservations` } reservationsテーブルに対するResponse [{'cid': 0, 'name': 'id', 'type': 'INTEGER', 'notnull': 0, 'dflt_value': None, 'pk': 1}, {'cid': 1, 'name': 'reservation_frame_id', 'type': 'INTEGER', 'notnull': 1, 'dflt_value': None, 'pk': 0}, {'cid': 2, 'name': 'customer_name', 'type': 'TEXT', 'notnull': 1, 'dflt_value': None, 'pk': 0}, {'cid': 3, 'name': 'customer_contact', 'type': 'TEXT', 'notnull': 1, 'dflt_value': None, 'pk': 0}, {'cid': 4, 'name': 'status', 'type': 'TEXT', 'notnull': 1, 'dflt_value': None, 'pk': 0}] reservation_framesに対するRequest { `table_name`: `reservation_frames` } reservation_framesに対するResponse [{'cid': 0, 'name': 'id', 'type': 'INTEGER', 'notnull': 0, 'dflt_value': None, 'pk': 1}, {'cid': 1, 'name': 'date', 'type': 'DATE', 'notnull': 1, 'dflt_value': None, 'pk': 0}, {'cid': 2, 'name': 'start_time', 'type': 'TIME', 'notnull': 1, 'dflt_value': None, 'pk': 0}, {'cid': 3, 'name': 'end_time', 'type': 'TIME', 'notnull': 1, 'dflt_value': None, 'pk': 0}, {'cid': 4, 'name': 'capacity', 'type': 'INTEGER', 'notnull': 1, 'dflt_value': None, 'pk': 0}] 上記のツール操作の結果をもとに2つのテーブルがあることとそのテーブル定義についての回答。またreservationsテーブルにcustomer_nameとcustomer_contactというフィールドがあることから予約には名前と連絡先が必要であることを理解して、情報提供をユーザーにリクエストしています。 ② 直近で予約可能な場所を4つ教えて。 read-query: 予約の空き枠情報を取得するためにクエリを発行しています。 Request { `query`: ` WITH reservation_counts AS ( SELECT reservation_frame_id, COUNT(*) as reserved_count FROM reservations WHERE status != 'cancelled' GROUP BY reservation_frame_id ) SELECT rf.id, rf.date, rf.start_time, rf.end_time, rf.capacity, COALESCE(rc.reserved_count, 0) as current_reservations, (rf.capacity - COALESCE(rc.reserved_count, 0)) as available_seats FROM reservation_frames rf LEFT JOIN reservation_counts rc ON rf.id = rc.reservation_frame_id WHERE rf.date >= DATE('now') AND (rf.capacity - COALESCE(rc.reserved_count, 0)) > 0 ORDER BY rf.date, rf.start_time LIMIT 4; ` } Response Error: Only SELECT queries are allowed for read-query read-query: ①で発行したSQLを実行するとエラーが発生することがわかりエラー内容をもとにSQLを修正して再トライしています。 Request { `query`: ` SELECT rf.id, rf.date, rf.start_time, rf.end_time, rf.capacity, (SELECT COUNT(*) FROM reservations r WHERE r.reservation_frame_id = rf.id AND r.status != 'cancelled') as reserved_count FROM reservation_frames rf WHERE rf.date >= DATE('now') ORDER BY rf.date, rf.start_time LIMIT 4; ` } Response [{'id': 1, 'date': '2024-12-05', 'start_time': '10:00:00', 'end_time': '11:00:00', 'capacity': 1, 'reserved_count': 1}, {'id': 2, 'date': '2024-12-05', 'start_time': '11:00:00', 'end_time': '12:00:00', 'capacity': 1, 'reserved_count': 0}, {'id': 3, 'date': '2024-12-05', 'start_time': '12:00:00', 'end_time': '13:00:00', 'capacity': 1, 'reserved_count': 1}, {'id': 4, 'date': '2024-12-05', 'start_time': '13:00:00', 'end_time': '14:00:00', 'capacity': 1, 'reserved_count': 1}] 再トライの結果、予約枠の情報と予約の空きの有無がわかるようなデータが返ってきています。その内容をもとにユーザーに予約枠情報をユーザーに伝えています。(ただし、ここでは、空きが無い予約枠も返しているので、SQLが不完全であることがわかります。) ③ 空きありのものを4つ教えて read-query: ユーザーの要件に合わせて、件数を4件にしたり、Where句でstatusを考慮していることがわかります。(先程のクエリでstatusが考慮されていなかったことをLLM側で修正しています。) Request { `query`: ` SELECT rf.id, rf.date, rf.start_time, rf.end_time, rf.capacity, (SELECT COUNT(*) FROM reservations r WHERE r.reservation_frame_id = rf.id AND r.status != 'cancelled') as reserved_count FROM reservation_frames rf WHERE rf.date >= DATE('now') AND (SELECT COUNT(*) FROM reservations r WHERE r.reservation_frame_id = rf.id AND r.status != 'cancelled') < rf.capacity ORDER BY rf.date, rf.start_time LIMIT 4; ` } Response [{'id': 2, 'date': '2024-12-05', 'start_time': '11:00:00', 'end_time': '12:00:00', 'capacity': 1, 'reserved_count': 0}, {'id': 7, 'date': '2024-12-05', 'start_time': '16:00:00', 'end_time': '17:00:00', 'capacity': 1, 'reserved_count': 0}, {'id': 9, 'date': '2024-12-06', 'start_time': '11:00:00', 'end_time': '12:00:00', 'capacity': 1, 'reserved_count': 0}, {'id': 13, 'date': '2024-12-06', 'start_time': '15:00:00', 'end_time': '16:00:00', 'capacity': 1, 'reserved_count': 0}] 上記の情報をもとにユーザーに予約枠の提示と追加の情報を求めるような応答をしています。 ④ 12月6日の11時から予約したいです read-sql: 指定した時間に対して改めてまだ予約枠の空きがあるかをチェックしています。 Request { `query`: ` SELECT rf.id, rf.date, rf.start_time, rf.end_time, rf.capacity, (SELECT COUNT(*) FROM reservations r WHERE r.reservation_frame_id = rf.id AND r.status != 'cancelled') as reserved_count FROM reservation_frames rf WHERE rf.id = 9; ` } Response [{'id': 9, 'date': '2024-12-06', 'start_time': '11:00:00', 'end_time': '12:00:00', 'capacity': 1, 'reserved_count': 0}] 予約枠の空きが確認できたため、実際の予約実行処理に進みます。 ⑤ <名前>です ユーザーが名前を話した際、ここではツールの選択は行われませんでした。次に予約を実行するためには連絡先を聞く必要があると判断し、ここでは追加のヒアリングにとどまっています。 ⑥ 連絡先は<メールアドレス>です 予約に必要な情報がすべて揃ったので、予約情報をreservationsテーブルに書き込みを行います。 write-query Request { `query`: ` INSERT INTO reservations (reservation_frame_id, customer_name, customer_contact, status) VALUES (9, '友松祐太', 'tomomatsu@example.com', 'confirmed'); ` } Response [{'affected_rows': 1}] DBからのレスポンスが正常だったので、予約が完了したことと、予約に使用した情報をユーザーに伝えています。また一通り予約実行全体のタスクが完了したのでクロージングのメッセージを伝えています。 このときのreservationsのテーブルを見てみると、最後の行に予約が追加されたことがわかります。 ⑦ やっぱり予約変更したいです。 read-query 予約の変更が必要ということで予約枠の提示が必要と判断し、予約枠検索用のQueryを発行します。 Request { `query`: ` SELECT rf.id, rf.date, rf.start_time, rf.end_time, rf.capacity, (SELECT COUNT(*) FROM reservations r WHERE r.reservation_frame_id = rf.id AND r.status != 'cancelled') as reserved_count FROM reservation_frames rf WHERE rf.date >= DATE('now') AND (SELECT COUNT(*) FROM reservations r WHERE r.reservation_frame_id = rf.id AND r.status != 'cancelled') < rf.capacity AND rf.id != 9 ORDER BY rf.date, rf.start_time LIMIT 4; ` } Response [{'id': 2, 'date': '2024-12-05', 'start_time': '11:00:00', 'end_time': '12:00:00', 'capacity': 1, 'reserved_count': 0}, {'id': 7, 'date': '2024-12-05', 'start_time': '16:00:00', 'end_time': '17:00:00', 'capacity': 1, 'reserved_count': 0}, {'id': 13, 'date': '2024-12-06', 'start_time': '15:00:00', 'end_time': '16:00:00', 'capacity': 1, 'reserved_count': 0}, {'id': 16, 'date': '2024-12-07', 'start_time': '11:00:00', 'end_time': '12:00:00', 'capacity': 1, 'reserved_count': 0}] Responseの予約枠情報をもとに、ユーザーに提示しています。先程予約実行した12月6日の11:00からの枠は空き枠が無くなったため、提示されていないことがわかります。また、これまでのやり取りから4件表示するのが適切と判断しqueryにも反映されています。 ⑧ 12月8日以降は? read-query 先ほどの条件だとユーザー側の条件に合わなかったため、条件の変更を求めています。QueryのWhere句の条件に2024-12-08以降という条件が付与されていることがわかります。 Request { `query`: ` SELECT rf.id, rf.date, rf.start_time, rf.end_time, rf.capacity, (SELECT COUNT(*) FROM reservations r WHERE r.reservation_frame_id = rf.id AND r.status != 'cancelled') as reserved_count FROM reservation_frames rf WHERE rf.date >= '2024-12-08' AND (SELECT COUNT(*) FROM reservations r WHERE r.reservation_frame_id = rf.id AND r.status != 'cancelled') < rf.capacity ORDER BY rf.date, rf.start_time LIMIT 4; ` } Response [{'id': 24, 'date': '2024-12-08', 'start_time': '12:00:00', 'end_time': '13:00:00', 'capacity': 1, 'reserved_count': 0}, {'id': 28, 'date': '2024-12-08', 'start_time': '16:00:00', 'end_time': '17:00:00', 'capacity': 1, 'reserved_count': 0}, {'id': 32, 'date': '2024-12-09', 'start_time': '13:00:00', 'end_time': '14:00:00', 'capacity': 1, 'reserved_count': 0}, {'id': 36, 'date': '2024-12-10', 'start_time': '10:00:00', 'end_time': '11:00:00', 'capacity': 1, 'reserved_count': 0}] Responseの予約枠情報をもとに、ユーザーに提示しています。 ⑨ 12月9日のやつでお願いします write-query① まずは、既に入っている予約に対してキャンセル処理をかけます。 Request { `query`: ` UPDATE reservations SET status = 'cancelled' WHERE reservation_frame_id = 9 AND customer_name = '友松祐太' AND customer_contact = 'tomomatsu@example.com' AND status = 'confirmed'; ` } Response [{'affected_rows': 1}] write-query② 続いて予約の実行処理を行います。ユーザーは12月9日のやつという曖昧な表現をしていますが、提示した候補は一つしかないので、12月9日の13:00-14:00ということを理解しています。また、すでに名前や連絡先の情報は聞けているため、ヒアリングがスキップされてreservationsテーブルへの書き込みを実行します。 Request { `query`: ` INSERT INTO reservations (reservation_frame_id, customer_name, customer_contact, status) VALUES (32, '友松祐太', 'tomomatsu@example.com', 'confirmed'); ` } Response [{'affected_rows': 1}] 予約が成功したので、新しい予約情報をユーザーに返却しています。ただし、ここでの一連のキャンセルと予約処理は、予約が成立しなかった場合に予約の削除だけが行われてしまうため、トランザクション管理は一部課題がありそうです。 一連の処理によって、DBの情報も更新されています。id=39の予約はcancelledになり、新たにid=40がconfirmedで予約がされていることがわかります。 ⑩ ありがとうございます ここでは特にツール選択の必要性がないため、クロージングの回答を返すのみとなっています。 ⑪ 予約照会をしてください。以前にキャンセルした日付も合わせて教えて下さい。 read-sql 予約照会をするために、reservationsテーブルの照会を行います。指示通り、予約キャンセルしたものも対象になっています。 Request { `query`: ` SELECT r.id, rf.date, rf.start_time, rf.end_time, r.status, r.customer_name, r.customer_contact FROM reservations r JOIN reservation_frames rf ON r.reservation_frame_id = rf.id WHERE r.customer_name = '友松祐太' AND r.customer_contact = 'tomomatsu@example.com' ORDER BY rf.date, rf.start_time; ` } Response [{'id': 39, 'date': '2024-12-06', 'start_time': '11:00:00', 'end_time': '12:00:00', 'status': 'cancelled', 'customer_name': '友松祐太', 'customer_contact': 'tomomatsu@example.com'}, {'id': 40, 'date': '2024-12-09', 'start_time': '13:00:00', 'end_time': '14:00:00', 'status': 'confirmed', 'customer_name': '友松祐太', 'customer_contact': 'tomomatsu@example.com'}] レスポンス内容をもとにユーザーにキャンセル済み、現在の予約をそれぞれ返却しています。 4. まとめ MCPを使うことでこんなにも簡単に予約対話エージェントを作ることができました。また、LLM側が事前に知っている内容はSQLiteというDBがあり、そのDBに対して6つの操作ができるという設定しか知りません。LLMはユーザーとの対話が始まると自ら判断し、データベースのテーブルの一覧を取得したり、テーブルのスキーマ定義を取得したり、そのテーブル構造を理解したうえで、テーブルに対する読み込み処理、書き込み処理を柔軟に実施することができるということの一連をトレースすることができました。 外部情報へのアクセスはSQLiteだけでなく既にGitやローカルファイル、PostgreSQLなど実装があったり、自前でも構築することが可能です。これらを組み合わせることによってより高度なタスクを実行するエージェントの構築が可能になります。 今後のAIエージェントの発展にMCPが大きく関わっていくことはおそらく間違いないと思っています。AI Shift社としても周辺技術を引き続き調査・検証を進めていきたいと思います。 明日は、コンテンツチームの伊藤からの記事が上がる予定です。 ご精読いただきありがとうございました。 投稿 【AI Shift Advent Calendar 2024】MCP(Model Context Protocol)を用いた予約対話AIエージェントの構築と動作のトレース は 株式会社AI Shift に最初に表示されました。
アバター
こんにちは、AI Shiftの東( @naist_usamarer )です。 この記事は AI Shift Advent Calendar 2024 の2日目の記事になります。 本記事では、2024年11月28日(木)から29日(金)にかけて開催された第15回対話システムシンポジウムの内容について紹介を行います。 また、 AI Shiftから2件の発表 を行ったので、発表資料といただいた質疑の内容も併せて紹介していきたいと思います。 対話システムシンポジウム 対話システムシンポジウムは、 人工知能学会 言語・音声理解と対話処理研究会 (SIG-SLUD) が年に1回開催しているシンポジウムです。 今回は、2022年、2023年に引き続き現地&オンライン(ポスター以外の聴講のみ)のハイブリッド形式で実施されました。 現地会場は前回、前々回と同じく国立国語研究所となりました。 今年の発表件数は以下の通りになりました。 発表件数:62件 招待講演:1件 口頭発表:15件 デモセッション:19件 ポスターセッション:23件 うちデモ有り:3件 国際会議報告:4件 一般発表のセッションは 去年 と比較して増え、全体として約1.5倍の発表件数となりました。 今年はこれまで開催されてきたインダストリーセッションがデモセッションという形に刷新されたことで、企業からの発表件数が増えたように感じました。 開催されたセッションの中から何点か抜粋してご紹介できればと思います。 招待講演 慶應義塾大学の 杉浦孔明先生 から 「 マルチモーダル言語処理は世界を変えるのか? 」 という題目で発表がありました。 講演では、マルチモーダル言語処理の基礎や研究の動向、ロボティクスへの応用、評価指標、および取り組み事例について紹介されていました。 個人的には基盤モデルのロボティクスへの応用や取り組み事例の紹介が非常に印象的でした。マルチモーダルLLMの進歩により、「棚の中に入っているものを持ってくる」「会場内の消火器を探す」のような複雑なステップを必要とするタスクの処理が可能になっている等最新の動向を知り非常に勉強になりました。 弊社のプロダクトは基本的にはコールセンターのドメインを対象にしていますが、音声・自然言語処理の範囲に閉じず様々な分野のキャッチアップを今後も続けていこうと思います。 AI Shiftからの発表 AI Shiftからは2件の発表を行いました。各発表の概要は 以前の記事 をご参照ください。 リアルタイム性と柔軟性を兼ね備えた音声対話システムのための軽量かつ高速な処理手法の検討 質疑応答 Q. 割り込み発話機能は実際使われるのか? A. 正常なシナリオでは割り込み発話機能が使用される機会は少ないと考えられるが、スロット訂正の際には利用されることがあると予想される。例えば、ユーザが「19時」と発話したにもかかわらず、システムが誤って「9時」と解釈した場合、その誤りを訂正するためにユーザがシステムに割り込んで発話する状況が考えられる。また、スロット訂正の機会を提供するために、逐次確認発話が有効に機能していると評価されている。 Q. 応答速度ストレスの評価結果について、発話終了検知が適切に機能していたとはどういうことか? A. ユーザが発話を終えたことをシステムが適切に検知するため、応答までの時間はユーザの発話終了を検知するまでの時間に大きく依存している。したがって、応答速度はおおむね発話終了を検知するまでの時間で決まると言える。このため、応答速度に対するストレスがないということは、ユーザの発話終了をシステムが適切に検知できていることを示す。つまり、応答速度が良好な場合、多くの場合で発話終了検知がうまく機能していたと考えられる。 Q. 定型応答フィラーが不自然になってしまったのはなぜか? A. 定型応答が各ターンで繰り返されることが鬱陶しく感じられ、さらに音声合成で生成されたフィラーが人間らしくなく不自然に聞こえるため。ここは各ターンにおいて適切なタイミングで人間のようなフィラーを差し込めると自然な対話を実現できるのではないかと考えている。 Q. 4つのモデルのうちどのモデルの性能が最も良かったか? A. 全てのモデルに課題は残ったが、スロットフィリングについては、LLMを使用しないルールベースの方法で十分対応できることが判明した。ルールベースの方法は、制御性と処理速度の両面でLLMを用いた方法より優位性がある。一方で、発話終了検知に関しては、stabilityを用いた方法と音量ベースVADを用いた方法の両方に改善の余地があることが分かった。 また、当日は実際に電話での飲食店予約対話のデモを用意しました。ポスター発表と同時に行うデモであることとデモに用いる端末が一つしか確保できていなかったということもあり、デモの実施自体が難しかったのですが、一人の方に体験していただけました。デモを体験していただいて感じた課題などの感想を以下に書き下します。 ユーザーはbotの機能(ヒアリング項目の複数同時聴取や割り込み発話など)を事前に知らないため、このbotの対応可能な機能を示すようなインタラクションがあると良いと感じました。 ヒアリング項目の聴取中に「やっぱり予約やめます」という発話があったものの、このシナリオを想定していなかったため対応できませんでした。このような発話への対応方針については議論の余地がありますが、現状のシナリオでは最終確認の段階以外で予約を取りやめる手段が電話を切ることしかなく、ユーザーにとって不便な点だと考えられます。 周りでポスター発表が行われている環境でも、スピーカー出力での対話が問題なく進行していました。バックグラウンドノイズが比較的小さい音量であれば、システムが正常に動作することが確認できて良かったです。 模範的なクレーム対応のモデル化のための二話者間の通話分析 質疑応答 Q. 今回の研究の活用イメージが湧かないが、どのようなものが考えられるか? A. 現時点でのタグセットの種類では大まかな傾向を把握する以上のことは難しい。今後オペレーターが顧客対応をする際の発話スタイルや戦略を反映させたタグセットを整備することで、良い対応をした際の具体的な行動の定量化・言語化を行いオペレーター教育支援に繋げていきたい。 Q. コールセンターの会話と日常会話だと対話の性質がかなり変わりそうだが、どのような基準でCEJCを選択したか? A. おっしゃる通り、かなりドメインの異なる対話であるが、CEJCに含まれる対話はさまざまなシチュエーションが含まれており、それらの多様な対話についてつけられる汎用的なタグセットとして設計されているため、コールセンターというドメインに対しても適用できるのではと考えCEJCを選択した。 Q. 今後タグ付与の精度を上げていくためにどのような改善が考えられるか? A. 今回のタグセットは音声以外の情報(視線・ジェスチャー表情等)も参照した上でつけているアノテーションであったため、音声情報だけでは分類が困難な発話も多く含まれていた。まずは音声情報だけで明確に判断できる分類基準をコールセンターのドメイン用に作る必要があると考えている。 Q. 「その他」のタグに分類された発話に「苛立ち等を表す言葉が入った」とのことだったが、具体的にどのような発話が分類されたのか? A. 「その他」のタグに分類された発話の多くは認識誤りを含むか、比較的短い長さの発話が多かった。判断がつきにくい発話も多かったが、オペレーターの発話に対する聞き手反応としての感情の表出を書き起こしたものが多い印象を受けた。 おわりに 対話システムシンポジウムは毎年参加していますが、今回は私、干飯、大竹が発表者として初の参加となりました。発表者として様々な研究者の方と議論する貴重な経験ができました。このような機会を設けてくださった運営委員の皆様、会場を提供してくださった国立国語研究所の皆様、そしてご参加いただいた皆様に心より感謝申し上げます。 今後もこのような場で発表できるよう、日々の研究開発に一層力を入れてまいります。 最後までお読みいただきありがとうございました。 明日は開発チームの由利による記事が公開される予定です。そちらもお読みいただけると幸いです。 投稿 【AI Shift Advent Calendar 2024】第15回対話システムシンポジウム参加報告 は 株式会社AI Shift に最初に表示されました。
アバター
はじめに 今年も始まりました、Advent Calendar。 こんにちは!AI ShiftのAIチーム所属の栗原健太郎 ( @kkurihara_cs )です! この記事は AI Shift Advent Calendar 2024 の1日目の記事です! Advent Calendarが始まったということは、もう年末なわけですが、とにかくOpenAI系のリリースが今年も多かったなと感じる1年でしたね(GPT-4o, GPT-4o-mini, o1など)。我々に関して言えば、LLMの事業応用に昨年以上に向き合った1年だったなぁと感じます。 記念すべきAdvent Calendar1日目では、多岐に渡るLLMに関する領域の中でも、とりわけ向き合う機会の多かったRAG (Retrieval-Augmented Generation)の性能改善についての記事をお届けします。 本記事の執筆を契機に(事業応用の観点も交えつつ)RAG改善周りの知識を整理できればと思います。 RAGの基本的な処理を簡単なコードと共に振り返りつつ、本書では触れられていないRAGの性能改善のための考え方や策を整理できればと思います。(大変恐縮ですが、改善項目の網羅性などは保証できておりません) 今回RAGを振り返るにあたっては、「 大規模言語モデル入門Ⅱ 」を活用させていただきました!親会社サイバーエージェントの研究組織AI Lab所属の山田康輔さんから献本いただいたものになります!(山田さん、ありがとうございます。) 本書 13-2章のサンプルコード をベースに、RAGにおいて改善すべきポイントについて整理していきます。 RAGの基本的な処理の流れに学ぶ改善ポイント はじめに、RAGは、情報検索技術とLLMの生成技術を組み合わせたLLMの生成に関する拡張の手法で、LLMが外部知識をもとにテキストを生成することを可能とします。 RAGのシステムは、大きく以下の二つのモジュールで構成されています。(細かくは、Graph RAG, SQL RAGなどの存在しますが、今回は一般的なドキュメント検索のRAGについて記述します。) Retriever 入力クエリに関連した外部知識を獲得するためのモジュールです。典型的には、外部知識は、検索元となるドキュメントの集合をデータストアに検索可能な状態で置いておくことが一般的です。 Generator Retrieverで獲得した情報をもとに、入力クエリに対する応答を返すモジュールです。基本的には一般的にLLMを用いて生成を行う事と大きな違いはありません。 では、早速、 13-2章のサンプルコード をベースに記事を展開します。適宜リンク先のサンプルコードを触りながらお読みください。(本記事ではコードを一部抜粋しております。) RAGの性能改善は、大きくRetrieverとGeneratorのそれぞれの改善に大別できそうです。ポイントごとに改善の考え方を整理できればと思います。 Retrieverの改善 1. クエリの前処理 from langchain_core.messages import HumanMessage, SystemMessage # Chat Modelに入力する会話データ chat_messages = [HumanMessage(content="四国地方で一番高い山は?")] # Chat Modelによるチャットテンプレート適用後の入力文字列を確認 chat_prompt = chat_model._to_chat_prompt(chat_messages) print(chat_prompt) # 出力 <s> ユーザ:四国地方で一番高い山は? </s > <s> アシスタント: 上記コードで、LLMに入力するクエリを定義しています。 RAGの性能を上げるための手段として、まずRetireverの精度を上げる(つまり適切なドキュメントを獲得できるようにする)ことが挙げられます。 適切なドキュメントの獲得を実現するための考え方の一つに、入力クエリを事前に前処理するという考え方があります。具体的には、入力クエリを別の文字列などに変換してRetrieverに入力をするという処理になります。 クエリの変換にはいくつか方法があり、どの変換方法が有効かは、適用するシチュエーションによって変わると考えております。 「四国 高い山」→「四国で高い山は?」のような、雑なクエリの自然な文章への変換 「四国で高い山は?」→ [検索用の擬似的な文章]を生成する変換 「四国で山登りをしたいので、おすすめの高い山を教えて。」→「Q1: 四国で山登りにおすすめの山は? Q2: 四国の高い山は?」など、複数の意図を整理する変換 など 参考: QueryRewriting , HyDE , MultiQueryRetriever , StepBackPrompt 2. 検索手法の検討 from langchain_huggingface.embeddings import HuggingFaceEmbeddings # Hugging Face Hubにおけるモデル名を指定 embedding_model_name = "BAAI/bge-m3" # モデル名からEmbedding Modelを初期化 embedding_model = HuggingFaceEmbeddings( model_name=embedding_model_name, model_kwargs={"model_kwargs": {"torch_dtype": torch.float16}}, ) # 二つのテキストの文埋め込みから類似度を計算 sample_texts = [ "日本で一番高い山は何ですか?", "日本で一番高い山は富士山です。", ] # 二つのテキストに対して文埋め込みを実行し、結果を確認 sample_embeddings = embedding_model.embed_documents(sample_texts) print(sample_embeddings) similarity = torch.nn.functional.cosine_similarity( torch.tensor([sample_embeddings[0]]), torch.tensor([sample_embeddings[1]]), ) print(similarity) # 出力 tensor([0.7743]) 上記コードでは、RAGにおける「入力クエリ」と「ドキュメント」の関連度合いを計算するための考え方として、「Embedding」を用いた二つの文字列の類似度の算出をしています。 「関連度合い」には異なる二つの考え方があり、コードに記載されている「Embedding」を含め、それぞれに基づいた検索手法が二つ存在します。それぞれの検索手法については、以下のように概説できます。 Embeddingを用いた意味的類似度による検索 Embeddingは文(文章)をベクトルに変換する技術です。このベクトルは文の意味を反映しているため、「二つのベクトルが類似している = 二つの文の意味が似ている」つまり関連しているとみなすことができます。一般的にEmbeddingを用いて表現される意味のベクトルは、汎用的な表現については適切に表現できる一方で、専門用語や固有名詞などの、世の中の文章頻出しない単語の意味をうまく表現できないと考えられています。ベクトルへの変換方法は多数存在します。 キーワードなどに着目した表層的な検索 表層的な検索は、二つの文(文章)それぞれを構成する文字や単語・フレーズの一致度合いの高さに基づいた検索技術です。文章の意味に着目するEmbeddingとは異なり、文字の一致度合いで関連度合いを決めていくため、固有名詞や専門用語がクエリやドキュメントに頻出するようなケースにおいては、Embeddingよりも高い性能を発揮する可能性があると考えられています。 また、これらをを組み合わせた「 ハイブリッド検索 」という手法も出ています。各検索手法の妥当性については、ドメインやユースケースに依存します。 参考: OpenAI-Vector Embeddings , BM25 3. ドキュメントのindexingの設定 from langchain_community.document_loaders import JSONLoader from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_community.vectorstores import FAISS # JSONファイルから文書を読み込むためのDocument Loaderを初期化 document_loader = JSONLoader( file_path="./docs.json", # 読み込みを行うファイル jq_schema=".text", # 読み込み対象のフィールド json_lines=True, # JSON Lines形式のファイルであることを指定 ) # 文書の読み込みを実行 documents = document_loader.load() # 文書を指定した文字数で分割するText Splitterを初期化 text_splitter = RecursiveCharacterTextSplitter( chunk_size=400, # 分割する最大文字数 chunk_overlap=100, # 分割された文書間で重複させる最大文字数 add_start_index=True, # 元の文書における開始位置の情報を付与 ) # 文書の分割を実行 split_documents = text_splitter.split_documents(documents) # 分割後の文書数を確認 print(len(split_documents)) # 分割後の文書の長さ(文字数)を確認 print(len(split_documents[0].page_content)) # 分割後の文書と文埋め込みモデルを用いて、Faissのベクトルインデックスを作成 vectorstore = FAISS.from_documents(split_documents, embedding_model) # ベクトルインデックスに登録された文書数を確認 print(vectorstore.index.ntotal) # ベクトルインデックスを元に文書の検索を行うRetrieverを初期化 retriever = vectorstore.as_retriever(search_kwargs={"k": 3}) # 文書の検索を実行 retrieved_documents = retriever.invoke("四国地方で一番高い山は?") # 検索された文書を確認 print(retrieved_documents) # 出力 >print(len(split_documents)) 1475 >print(len(split_documents[0].page_content)) 221 >print(vectorstore.index.ntotal) 1475 > print(retrieved_documents) Document(metadata={'source': '/content/docs.json', 'seq_num': 26, 'start_index': 0}, page_content='... 石鎚山(いしづちさん、いしづちやま)は、四国山地西部に位置する標高1,982 mの山で、...'), Document(metadata={'source': '/content/docs.json', 'seq_num': 1, 'start_index': 0}, page_content='富士山(ふじさん)は、静岡県(富士宮市、富士市、裾野市、御殿場市、駿東郡小山町)と山梨県(富士吉田市、南都留郡鳴沢村)に跨る活火山...'), Document(metadata={'source': '/content/docs.json', 'seq_num': 96, 'start_index': 0}, page_content='四阿山(あずまやさん)は、長野県と群馬県の県境に跨る山。標高2,354 m。...') 本コードでは、RAGの検索元となるドキュメント群をデータストアに登録(indexing)する処理を記述しています。indexingにおいて特に我々が検討するのは、「登録する各ドキュメントの長さ」です。 「登録する各ドキュメントの長さ」について、生成AIへの入力の長さを考慮して、基本的に長いドキュメントはある程度短く区切ってデータストアに登録します。区切られたドキュメントのことをチャンクと呼び、考慮する際には以下の二つについて留意する必要があります。 チャンクサイズ データストアに登録するドキュメントのチャンクの長さを表します。あまりに短すぎる場合ですと、各ドキュメントが持つ情報が少なくなり、関連性の高いドキュメントの発見が難しくなります。また、一般的にLLMの生成能力は、インプットの長さが長すぎる場合に制御性の低下などの性能低下を招くことが知られています。そのため、あまりにドキュメントのチャンクサイズが長いと、生成結果の品質悪化を招く可能性が高くなってしまいます。 チャンクオーバーラップ オーバーラップは連続したチャンクの前後の重なり具合を定義する値です。この数字があまりに小さい場合、各ドキュメントが前後の文脈をほとんど把握できず情報の欠損を招くリスクが上がります。数字が大きすぎる場合、ドキュメント数が不必要に大きくなり、検索の精度に影響が出てしまいます。 この他、単に固定された長さで区切るのではなく、文の意味に応じてチャンクを決めていくsemantic chunkingや、markdownやwebページなどの階層構造を考慮したchunkingも存在します。 参考: 各chunkingに関するAWSのdocument 4. 取得したドキュメント群へのリランキング処理 # 文書の検索を実行 retrieved_documents = retriever.invoke("四国地方で一番高い山は?") 上記コードによるドキュメントのretrieve後に工夫を施す方法の一つとして リランキング が知られています。 リランキングでは、最終的にk件のドキュメントを獲得したい場合に、初めにn (> k)件のドキュメントを取得します。その後n件のドキュメントと入力クエリとの類似度をリランカーモデルを用いて算出し、類似度の上位k件を最終的なドキュメントとして獲得します。 リランカーモデルを用いて算出した類似度算出には、以下のメリットとデメリットが存在します。 メリット リランカーモデルによって算出された類似度は「2. 検索手法の検討」にて紹介したEmbedding同士の類似度と比較して正確である。 デメリット リランカーモデルによる類似度計算にかかる時間はEmbedding同士の類似度の計算と比較してかなり長い。 本性質ゆえに、リランキングの適用を検討する上では、リランカーの計算時間分だけRAGの実行時間が伸びることについて議論する必要性があります。 参考: Cohere-reranker , bge-reranker-large Generatorの改善 5. より良い生成の獲得 from langchain_core.documents import Document from langchain_core.runnables import RunnablePassthrough # 任意のqueryからメッセージを構築するPrompt Templateを作成 rag_prompt_text = ( "以下の文書の内容を参考にして、質問に答えてください。nn" "---n{context}n---nn質問: {query}" ) rag_prompt_template = ChatPromptTemplate.from_messages( [("user", rag_prompt_text)] ) def format_documents_func(documents: list[Document]) -> str: """文書のリストを改行で連結した一つの文字列として返す""" return "nn".join( document.page_content for document in documents ) # 定義した関数の処理を行うRunnableを作成 format_documents = RunnableLambda(format_documents_func) from langchain_core.runnables import RunnablePassthrough # RAGの一連の処理を行うChainを作成 rag_chain = ( { "context": retriever | format_documents, "query": RunnablePassthrough(), } | rag_prompt_template | chat_model_resp_only ) # Chainを実行し、結果を確認 rag_chain_output = rag_chain.invoke("四国地方で一番高い山は?") # 出力 四国地方で一番高い山は、愛媛県と高知県の県境にある石鎚山です。標高は1,982メートルで、四国地方で最も高い山です。 上記コードは、ドキュメントのretrieveからGeneration(生成)まで実施するコードです。 Generation部分の改善の手段の一つとして、回答を生成するまでにLLMの推論処理を複数回実行する Self-RAG と呼ばれるフレームワークが提案されています。 しかし、Generation部分の改善にあたっては以下の難しさが挙げられます。 生成時間の長さ 複数回生成を行う場合には、その分だけ最終的な出力を得る時間も伸びます。RAGの主な適用先の一つであるchatbotなど様々なアプリケーションにおいて、出力の遅延はUXを大きく下げる懸念があります。 Streaming出力の取り扱い 生成AIを用いたテキスト生成の結果を出力させる方法として、「生成結果を全文一括で出力するか」「トークン単位(文字や単語などの小さい単位)で出力(Streaming出力)するか」の二つがあります。OpenAIのAPIやtransformersで提供されているモデルにおけるstreamerオプションの活用など、の場面でStreaming出力をさせることが可能です。 例えばchatbotなどのユースケースにおいては、生成されるまでじっと待つか、生成されている様子を視認できるかという違いが生まれます。基本的に後者の方がUXは良く、弊社でもstreaming出力の活用の場面は非常に多いです。一方で、streamingで出力した結果に対して(UXを落とさずに)修正・変更を実施するのは現状煩雑です。 以上の理由より、現状では事業応用観点でRAGの改善を検討する場合、Generation部分よりもRetriever部分の改善をまず考えることが多い印象を受けます。 しかし、GeneratorはLLMの大きな課題として挙げられるHallucinationをまさに引き起こす部分です。そのため、Generator部分の抜本的な課題解決に向けてアンテナを貼り続ける必要があります。 以上ここまででRAGの一連の流れを追うことができました! まとめ 今回は大規模言語モデル入門Ⅱのサンプルコードをベースに、RAGの改善ポイントについて整理しました。動くシンプルなコード等々のおかげで、RAGについて包括的に学習し直す良い機会となりました。 RAGの仕組み自体はシンプルである一方で、「Retrieverの改善」と「Generatorの改善」、特に「Retrieverの改善」は「クエリの前処理、検索手法の検討、ドキュメントのindexingの設定、取得したドキュメント群への処理」と多数の改善方針があることを改めて確認することができました。 今回紹介した多くの改善ポイントに対して、ドメインに応じて1つずつ対応し続けているうちは事業としてはなかなかスケールさせづらいのでは?この部分まで自動化できたりしたら面白いのでは?など想いを馳せている次第です。 今後もLLMや生成AIに関する最新情報にできるだけ追従していければと思います! 明日は同じくAIチームから東が第15回対話シンポジウムの参加報告記事をお届けする予定です。 投稿 【AI Shift Advent Calendar 2024】「大規模言語モデルⅡ」を読んで、RAG改善の勘所を整理してみた は 株式会社AI Shift に最初に表示されました。
アバター
こんにちは、AIチームの東( @naist_usamarer )です。 2024年11月28日(木)から11月29日(金)にかけて開催される第15回対話システムシンポジウムにて、AI Shiftから一般発表セッション(ポスター+デモ)で1件、一般発表セッション(ポスター)で1件の発表を行います。 本記事では発表内容の概要となりますので、お聞きになる際の参考にしていただけると幸いです。 一般発表セッション(ポスター+デモ) 発表日時:2024年11月29日 13:40-14:40 タイトル:リアルタイム性と柔軟性を兼ね備えた音声対話システムのための軽量かつ高速な処理手法の検討 著者:大竹 真太 AI Messenger Voicebot をはじめとするボイスボットの多くは、事前に定義されたシナリオに沿って対話を進める方式が主流となっており、ユーザーからの予期せぬ発話や多様な要求に対する柔軟な対応が難しいという課題があります。 また、近年大規模言語モデル(LLM)を活用した対話システムの研究が進んでいますが、音声対話システムではユーザー・bot間のインタラクションの円滑性がユーザー体験に直接影響を与えるため、応答速度の遅延の影響やAPI利用によるコスト増大などの懸念点が残されています。 本発表では軽量な手法とLLMを組み合わせたハイブリッドシステムを構築し、リアルタイム性と対話システムの柔軟性の実現とのトレードオフについて検証を行います。 会場では社内のエンジニアを対象にしたユーザー実験結果についてのポスター発表のほか、検証用の端末を用いて実際に架電を行う飲食店予約対話デモシステムを提供する予定です。 一般発表セッション(ポスター) 発表日時:2024年11月29日 13:40-14:40 タイトル:模範的なクレーム対応のモデル化のための二話者間の通話分析 著者:東 佑樹、干飯 啓太 コールセンター業務において、クレーム対応は心理的負荷が非常に高く、対応の効率化は重要な課題となっています。しかし、クレーム対応はオペレーター毎に属人化しており、応対品質の定量的な評価や質的向上のための効率的な従業員教育には課題が残されています。 本研究は、ユーザー・オペレーター間の音声対話の認識結果について、LLMにより対話行為タグを付与し、その結果を集計することで二話者間の通話内容や対応内容の傾向を定量化することを試みます。 今回の発表では、日本語の音声対話コーパスを用いたタグ付与の性能評価、およびAI Messenger Summaryを通じて収集した実環境の音声データを対象にした有用性の検証・分析の結果を発表します。 おわりに 以上の内容で当日は発表いたします。両発表とも同じ時間帯となりましたが、少しでも興味を持たれた方はぜひ両発表をお聞きいただき、さまざまな議論ができればと思います。 最後までお読みいただきありがとうございました。 投稿 第15回対話システムシンポジウムで発表します は 株式会社AI Shift に最初に表示されました。
アバター
はじめに こんにちは!奈良先端科学技術大学院大学 修士1年の夏見昂樹です。これまで研究ばかりで開発の経験がほとんどなかったので、大学院で学んでいる自然言語処理の技術を用いた社会実装を行いたい思いから、10月2日から10月31日までの約1ヶ月間、サイバーエージェントのインターンシップ-CA Tech JOBに参加させていただきました。 この記事では、私が本インターンシップ期間中に行なった開発や体験について紹介させていただきます。 配属部署について 今回私が配属されたAI Shiftは、「AIの民主化」をミッションとし、生成AI人材育成やコールセンター業務の自動化などのソリューションを提供しているサイバーエージェントの子会社です。その中で、私は生成AIを用いたソリューションの提案・開発を行う、生成AIビジネス事業部のAIチームにジョインしました。 取り組んだタスク 今回私が取り組んだタスクは「オペレーター研修支援ボットの開発」です。背景として、AI Shiftは沖縄対話センターを保有しており、電話やチャットの有人オペレーター対応を行っています。従来よりコールセンター業界の一般的な課題として、高品質なサービスを提供するため、研修に多くの時間を要する問題があります。また、オペレーターの入れ替わりが激しく、定着しないことが挙げられます。 具体的には、高度な知識とテキストコミュニケーションならではの難しい対応経験がオペレーターには求められます。これらを獲得するためにベテランのオペレーターが長期間、新人のオペレーターをサポートする必要があり、研修業務がベテラン層の負担となっているのが現状です。そこで、研修で実施するベテランと新人同士のロールプレイングを代替するボットを開発しました。本ボットの導入によって、ベテラン層の負荷を軽減しつつ「①新人の業務における基礎知識獲得、②チャット応対における適切な言葉遣いなどの習得」が期待されます。 一般的なボットはユーザーから質問をはじめ、ボットが回答する流れですが、今回の場合は、ボットがお客さまとして「〇〇をするにはどうすればいいですか?」といったようにボットからチャットが開始することが特徴です。 ボットとの対話を通じた新人オペレーターの成長を実現させる際、留意すべきポイントが3つあります。 1. ボット側が保有する知識 ボットはお客さまのように質問する機能と、ベテラン社員のように新人に一連の対話に対してフィードバックを示す機能の2つの機能を持つべきです。しかし、実際のシーンでは当然お客さまは答えを知らないので、ボットは知らないふりをしながらオペレーターと対話する必要があります。 2. オペレーターから提供される入力の正誤判定 オペレーターからの回答が本当に合っているのかをボットが判断する必要があります。このためには、何を正解とするのか定義する必要があります。 3. オペレーターへの追加の質問 オペレーターが一度の回答で十分な案内ができない可能性が存在するため、案内された内容が不足している場合は再度、これまでの対話を基にオペレーターに聞き返す必要があります。 システムについて 本システムのフローは次の図のようになります。上記留意事項に対応するモジュールについて簡単に説明していきます。 質問文の選択 ボットから提示する質問文に、今回の案件であるA社のヘルプページに存在するFAQ(Frequency Asked Question)の情報を用いました。ただ、既存の質問数には限りがあり、これらの質問文を暗記してしまうと学習効率が低下することを考え、LLMを用いて質問文のバリエーションを増やしました。具体的には、FAQの情報をLLMに入力し、基の質問文と同じ意味の質問文を10個ずつ作成しました。その際に用いたプロンプトは以下の通りです。 あなたの仕事は、与えられたFAQと実際の質問から、そのFAQに対して同じ意味で様々な想定質問を作成することです。以下に示すルールを必ず満たす必要があります。 1. 与えられたFAQを無視して読んでも意味のわかる質問であること。 2. 与えられたFAQから完全に答えられる問題であること。 3. 問題の難易度は中程度であること。 4. 質問は合理的で、人間が理解し回答できるものでなければならない。 5. 「ログインできない」「退会したい」のような実際のユーザーが問い合わせてくるような文言にすること。 6. 短く簡潔な言葉遣いを心がけること。ただし、与えられたFAQの長さを可能な限り尊重すること。 7. 10個の想定質問を作成すること。 応答はカンマで区切られた値のリストでなければなりません。 例: hogehoge, fugafuga 質問の意図は明確にしなければなりません。 例: 「hogehoge」の申し込み方法を教えてください。, とはなんですか? 質問は全て「?」で終わらなければなりません。 例外例: 申し込み方法を教えてください。, データ通信ができない。 実際の質問: {question} FAQ: {answer} 入力に対する正誤判定 オペレーターの理解度に応じた後続処理を振り分けるために、オペレーターの回答に対し、正誤判定をするモジュールを構築しました。モジュールの実装にはLLMを用いるとともに、判定結果は「正解」「不正解」「不十分」の3つの値で定義しました。また全ての場合で、回答内容に加え、言葉遣いに関するフィードバックもするよう指示しました。 各ラベルの定義は以下のようになります。 正解: ボットの最初の質問に対して、オペレーターのこれまでの回答が十分回答になっている 不正解:ボットの最初の質問に対して、オペレーターのこれまでの回答は全く回答になっていない、もしくは間違えている 不十分:ボットの最初の質問に対して、オペレーターのこれまでの回答は間違えてはいないが、情報が不足している。 ボットは「正解」「不正解」の場合は結果に応じたフィードバックを返し、「不十分」の場合は後述する「判定結果が「不十分」の場合の処理」に従って、会話を継続するよう実装しました。 各ラベルの正誤判定は、ボットの最初の質問文、オペレーターの過去の回答、既存のFAQページの内容を元に実施しています。 オペレーターの回答の正誤判定を、「オペレーターの回答は、質問文に紐づくFAQの内容を網羅するか」で判断することも検討しましたが、FAQページの情報量が実際にオペレーターが案内すべき以上の情報を網羅しているが故に、必要以上に「不十分」と判定してしまう事例が頻発しました。そのため「正解」ラベルの出力割合を増やすという意図で、「質問の回答になっているか」という尺度をベースにプロンプトを作成し直しました。その結果、質問文の内容を逸脱した過剰な追加質問の抑制に成功しました。最終的なプロンプトは以下の通りです。 あなたの役割は次の二つです。 1. ユーザーから提供される情報の「最初の質問」と「スタッフのこれまでの入力」と「答え」を比較して、「最初の質問」に「スタッフのこれまでの入力」が十分に答えられているのかを「答え」の情報をもとに「不十分」「正解」「不正解」の中から最も妥当なラベルを選択すること。 2. 「正解」「不正解」の場合は、これまでのスタッフの応答に対して、「最初の質問」と「答え」の情報をもとにスタッフにフィードバックすること。 以下のルールを忠実に守る必要があります。 1. 応答はカンマ区切りで行うこと。例:ラベル, フィードバック ※不十分の場合は、ラベル, なし と出力すること 2. 「不正解」ラベルは、本当に間違っている時のみ選択すること。基本的には「不十分」とすること。 3. 「答え」は情報が多すぎるところがあるので、答えの全てをスタッフが答えられている必要はありません。要点を押さえられていれば「正解」としてください。 4. 「正解」の場合は、どう良かったのかについて、「不正解」の場合は、何がダメだったのかをフィードバックすること。 5. フィードバックの際には、スタッフが正しく敬語を用いられていたかについても言及すること。言及の際は、どこの敬語が良くて、どこが悪いのか、またどう直すべきなのかんについて明確に示すこと。 6. わからなさそうだったり、答えを教えてほしい旨を回答された時は、「不正解」としてください。 判定結果が「不十分」の場合の処理 「オペレーターの回答が不十分だった場合に、インタラクティブにボットに対して追加の情報を提供する」という振る舞いの実現のため、上述の正誤判定で「不十分」となった場合には、ボットがオペレーターに不足している情報について追加で質問するようにLLMを用いて実装しました。追加の質問を生成する上で、「既存のFAQ(理想的な回答)」と「これまでのオペレータの回答」を参照しました。最終的なプロンプトは以下の通りです。 あなたは、ある〇〇を利用中もしくは契約しようとしている「お客様」です。 「質問文」を解決したいのですが、スタッフとのこれまでの会話では情報が不足していてまだ解決できません。 「理想的な回答」内の「質問文」に関連する答えを得るために、次に何を聞きたいのか考えて質問することがあななたの役割です。 以下に示すルールを忠実に守る必要があります。 1. 「理想的な回答」の情報をもとに、追加の質問を作成すること。 2. お客様になったつもりで質問すること。 3. 「理想的な回答」を知っていますが、詳しすぎてはいけません。あなたはお客様です。 4. お客様が質問するような文章を心がけること。 5. 質問の意図は明確にしなければなりません。 例: 「hogehogeの申し込み方法を教えてください。, hogehogeとはなんですか? 6. 「質問文」に含まれない内容を必要でなければ行ってはいけません。   ###質問文### #質問文# ###理想的な回答### #理想的な回答# ###スタッフとのこれまでの会話### #スタッフとのこれまでの会話# 類似する内容の研修資料の提示 対話へのフィードバック時に、不足している知識を学習できる資料を提示することで、効率的に学習が進むのではないのかという思いから、オペレーター研修に用いる研修資料の名前とページ数を3件提示する機能の開発を行いました。この機能の開発にあたり、研修資料の各ページをOpenAIのtext-embedding-3-smallを用いて埋め込み、ローカルでベクターストアを作成可能な Chroma を用いて事前に ストアしました。これに対して、対象のオペレーターの回答に紐づいている正解回答の文章を埋め込み、ベクターストア内からコサイン類似度が高い順に20件抽出します。この20件に対して、これまでの対話内容と近しい3件をLLMで絞り込むことで達成しました。 プロンプトと最終成果物のデモの外観は以下の通りです。 あなたの役割は、これまでの「staffとcustomerの会話内容」と「模範解答」に対して、提示される複数の候補を比較し、どの候補が最もこれまでの会話内容と模範解答の両方の内容に近いのかを3つ選択することです。 ただし、次のルールに忠実に従う必要があります。 1. 出力は選択した候補に存在する slide_name と page を slide_name@page のように必ず @ 区切りでのみ3つ出力すること。例: "hogehoge@4, hogihogi@12, higihigi@3" 2. slide_name と page 以外の情報を出力しないこと ###これまでのstaffとcustomerの会話内容### #history# ###模範解答### #best_answer# 学び 沖縄対話センターチームと週2回程度ミーティングを行い、その中でシステムについて相談しながら進めることができました。その際、ユーザーが真に求めているものを提供するため、ヒアリングの重要性を学びました。また、質問文生成、正誤判定、フィードバックの3つの役割でLLMを用いましたが、些細なプロンプト差異でモデルの出力が大きく変動することを身に染みて体感することができ、プロンプトエンジニアリングの重要性を痛感しました。この際の知見として、 こちら を参考にして、LLMの役割を明示することやルールを明記しました。加えて、Few-shot時に示した具体例に出力が引っ張られる問題を観測した際、例を意味のない文字(hogehoge)などに変更することで解決しました。また、開発初期の段階では、単一のLLMで正誤判定、判定結果別の対応をまとめて行っていましたが、満足する出力が得られなかったため、1つのタスクに1つのLLMを用いることで出力を安定させることに成功しました。この経験から、1つのLLMに対して役割やルールの詰め込みすぎには注意が必要だと学びました。 インターンシップに対する感想 AI Shiftの社員さんだけでなく、他の部署の社員さんともランチなどで交流する機会があり、社内の雰囲気を存分に感じることができるインターンシップだったと思います。 開発の経験がほとんどなかった私ですが、本インターンシップを通してDockerの扱い方からクラウドへのデプロイまで行うことができました。ここまで達成することができたのはサポートしていただいたトレーナーさんやチームの皆様のおかげです。本当にありがとうございました。 また、技術力の向上だけでなく開発のやりがいを身に染みて感じることができ、非常に実りの多い1ヶ月間を過ごすことができました。今回の経験をもとに今後さらに自分自身の成長に繋げたいと考えています。 改めまして、1ヶ月間ありがとうございました! 投稿 オペレーター研修支援ボットの開発@インターンシップ は 株式会社AI Shift に最初に表示されました。
アバター