TECH PLAY

TECH Street

TECH Street の技術ブログ

18

はじめに 事前知識 スクラム Codex CLI AI-Scrumで何ができるのか 概要 1. 要件整理AI(プロダクトオーナー)が動く 2. 設計AI(アーキテクト)が設計書を作成 3. タスク分解AI(プランナー)が作業を細分化 4. 開発AI(デベロッパー)が実際のコードを生成 5. 品質管理AI(QA)がテストコードを作成・実行 6. ドキュメント作成AI(ドキュメンター)が説明書を生成 使い方 ステップ1:インストール ステップ2:やりたいことを書く ステップ3:コマンド実行 特定の工程から再開も可能 想定される活用シーン  1. プロトタイプ作成 2. アイデア検証 3. 学習目的 最後に 記事執筆者   はじめに こんにちは! Thinkings株式会社 でエンジニアとして働いている栗岡と申します。 2023年4月にThinkings株式会社へ新卒で入社し、現在はCREという、顧客課題の解決や既存機能のアップデートを通して、顧客信頼性を高めていくことを目的・役割としているチームでエンジニアとして働いています。 今回は、個人開発をやってみたい人を対象に、 AI-Scrum という、Codex CLI(コーデイングエージェント)を用いて作りたいものをスクラム形式で開発、改善できるシステムを作ってみたので紹介していきたいと思います。 非エンジニアの方・初学者の方でも、根気よく設定していただければ作りたいものが作れるようになると思うので是非一読していただければと思います! 事前知識 スクラム スクラムに関しては、私自身もまだまだ勉強中の身なのですが、基本的には スクラムガイド というものを参考にしています。 スクラムとは、ソフトウェア開発でよく使われる「チームワーク重視の開発手法」のことです。従来の「最初から最後まで一直線で進む」開発とは違い、「小さな単位に分けて、チームで協力しながら段階的に完成させる」アプローチになります。 通常のスクラムチームには以下の役割があります。  スクラムマスター: チームにスクラムの理論とプラクティスを全員に理解してもらえるよう支援する人  プロダクトオーナー(PO): 何を作るかを決める人  開発者: 設計やタスク分解、実際にコードを書き品質を作り込む人  AI-Scrumでは、POと開発者をAIが自動で担当してくれます。人間がスクラムマスターの立ち位置になります。  Codex CLI AI-Scrumは Codex CLI を使うこと前提になっています(私はChatGPT Proプランで利用しています)。 なので、他のAI Agentでの動作は確認していません。要望が多く集まればClaude CodeやGithub Copilot向けの設定を追記するかもしれませんが、基本的には皆様自身で使いたいAI Agentに沿って設定を変更してください。 AI-Scrumで何ができるのか AI-ScrumのコードはGithubで公開しているので、実際に手元で試してください。  GitHub - niya1123/ai-scrum 概要 例えば、「旅行計画を立てるアプリを作りたい」と思ったとします。従来なら、設計や実装に数日ほど時間を要しますが、AI-Scrumなら以下のようなプロセスで自動化されます。 1. 要件整理AI(プロダクトオーナー)が動く 入力:「旅行計画アプリを作りたい。仕様は以下のような感じでお願いします。~~~」 ↓ 出力: ・目的地検索機能が必要 ・スケジュール管理機能が必要   ・予算計算機能が必要 ・友人との共有機能が必要 このようにAIが必要なものを考えて、プロダクトバックログというやるべき作業リストを作成してくれます。 優先度1: ユーザーログイン機能 優先度2: データ入力画面 優先度3: レポート表示機能 これによって、各機能の重要度と開発順序が明確になります。また、人間がこの作業を引き継いだとしても何をやっていたのかが明確にわかります。 2. 設計AI(アーキテクト)が設計書を作成 ・データベース設計 ・画面設計 ・機能の関係図 ・技術選定理由書 3. タスク分解AI(プランナー)が作業を細分化 ・ログイン画面の作成 ・目的地検索画面の作成 ・スケジュール入力画面の作成 ・データベース接続処理の実装 (各タスクごとに優先度と工数も自動算出) 4. 開発AI(デベロッパー)が実際のコードを生成 // 実際に動くNext.jsのコードが生成される export default function TravelPlanner() {   const [destination, setDestination] = useState('');   const [schedule, setSchedule] = useState([]);   // ... 実際に動作するコード } 5. 品質管理AI(QA)がテストコードを作成・実行 // 自動生成されるE2Eテスト test('旅行計画が正常に保存される', async ({ page }) => {   await page.fill('[data-testid=destination]', '京都');   await page.click('[data-testid=save-button]');   await expect(page.locator('.success-message')).toBeVisible(); }); 6. ドキュメント作成AI(ドキュメンター)が説明書を生成 # 旅行計画アプリ使用マニュアル ## 基本的な使い方 1. 目的地を入力してください 2. 日程を選択してください 3. 保存ボタンを押してください 使い方 使い方はGithubのREADMEに詳しいことは書いていますが、一番簡単な使い方の例を示します。 ステップ1:インストール まずはコードをGithubからクローンしてきて、以下コマンドを実行します。 npm install (Node.js 18以上が必要です) ステップ2:やりたいことを書く domains/my-project/my-project-plan.mdというファイルに、作りたいアプリについて日本語で書きます。 my-projectというのは任意の名前です。マークダウンのファイル名も任意の名前です。 今回は家計簿アプリを作るという想定で以下のような計画書を作成します。 ファイルはdomains/kakei/kakei-plan.mdとしました。   # 家計簿アプリの計画書 ## 目的 毎月の支出を管理して、無駄遣いを減らしたい ## 主な機能 - 支出の記録 - カテゴリ別の集計 - 月次レポートの表示 - 予算設定と警告機能 ## 想定ユーザー - 家計管理に興味がある20-40代 - スマートフォンで手軽に記録したい人   ステップ3:コマンド実行 npm run domain domains/kakei/kakei-plan.md 先ほど作ったマークダウンファイルを対象として実行します。あとはAIチームが自動で作業してくれます。 実際の出力内容は以下のような感じです。今回は少しエラーが出ていました。実装段階でなんらかの原因でcodexが何も出力しなくなってしまったようで、それを検知してエラーを出しています。 > ai-scrum-template@0.1.0 domain > tsx scripts/run-domain.ts domains/kakei/kakei.md === 1) PO: Backlog 生成 === ✔ [PO] codex完了: 7 json行 (46.8s, exitCode=0, signal=none)ed:46.7s === 2) Architect: 技術選定/雛形適用 === ✔ [Architect] codex完了: 332 json行 (618.0s, exitCode=0, signal=none)d:618.0s === 3) Planner: タスク分解 + E2E雛形 === ✔ [Planner] codex完了: 162 json行 (218.6s, exitCode=0, signal=none)d:218.6s === 4) 実装イテレーション 1/3 === ✔ [Dev-FE#1] codex完了: 4654 json行 (268.3s, exitCode=0, signal=none)ed:268.3s ──────────────────────█───────────────── [Dev-BE#1] lines:10633 elapsed:540.2s ⚠️ STALL Dev-BE#1 306s 無アクティビティ (閾値 300s) → 終了要請 (調整: CODEX_STALL_TIMEOUT_MS) ✔ [Dev-BE#1] codex完了: 10633 json行 (540.2s, exitCode=null, signal=SIGTERM) Unexpected error: Error: codex terminated: no activity for >= 300s (stall) (exitCode=null, signal=SIGTERM). Tune CODEX_STALL_TIMEOUT_MS/CODEX_TIMEOUT_MS as needed.     at ChildProcess.<anonymous> (/Users/niya/Documents/ai-scrum/scripts/orchestrator.ts:242:21)     at ChildProcess.emit (node:events:507:28)     at maybeClose (node:internal/child_process:1101:16)     at Socket.<anonymous> (node:internal/child_process:457:11)     at Socket.emit (node:events:507:28)     at Pipe.<anonymous> (node:net:351:12) { 特定の工程から再開も可能 前述のように、どこかのタイミングで作業が止まってしまったとしてもリカバリー可能です。 概要で説明した、要件整理AI、設計AI、タスク分解AI、開発AI、品質管理AI、ドキュメント作成AIの各工程を任意の場所から作業を再開することができます。 PO段階から再開したい場合は、計画書のパスを渡してあげて、それ以外はPOが作成したbacklog.ymlへのパスを渡すようにしてください。 # プロダクトバックログ作成工程から再開 npm run domain:po domains/my-project/my-project-plan.md # 開発工程から再開 npm run domain:dev -- --backlog out/po/{RUNID}/backlog.yml # テスト工程から再開 npm run domain:qa -- --backlog out/po/{RUNID}/backlog.yml 今回であれば開発工程から再開させればうまくいきそうです。 実際に以下のように実行すると、作業を再開してくれました。 npm run domain:dev -- --backlog out/po/2025-10-16T02-01-30-883Z/backlog.yml > ai-scrum-template@0.1.0 domain:dev > START_FROM=dev tsx scripts/run-domain.ts --backlog out/po/2025-10-16T02-01-30-883Z/backlog.yml (from='dev' 以前のステージはスキップ) 既存 backlog を再利用します: /Users/niya/Documents/ai-scrum/out/po/2025-10-16T02-01-30-883Z/backlog.yml → out/po/2025-10-21T00-16-55-269Z/backlog.yml backlog から推測した tasks.yml を再利用します: /Users/niya/Documents/ai-scrum/out/planner/2025-10-16T02-01-30-883Z/tasks.yml → out/planner/2025-10-21T00-16-55-269Z/tasks.yml Architect スキップ (--from=dev). 既存 out/architect/2025-10-21T00-16-55-269Z/scaffold.log が無いので続行しますが問題が起きる可能性があります。 === 4) 実装イテレーション 1/3 === ✔ [Dev-BE#1] codex完了: 7 json行 (14.0s, exitCode=0, signal=none)ed:13.9s ✔ [Dev-FE#1] codex完了: 294 json行 (231.8s, exitCode=0, signal=none)d:231.7s .... 以降はQAを実行してドキュメント作成が行われます。 QAに関しても、E2Eを実行して失敗していれば、E2E失敗内容から実装フェーズをサイド実行してバグを修正します。このループは基本的にE2Eが通るまで実行され続けます。 今回であれば以下のように実行されていました。 === 4) 実装イテレーション 1/3 === === 5) QA: 受け入れテスト === ✔ [QA#1] codex完了: 45 json行 (51.8s, exitCode=0, signal=none)ed:51.7s QA#1 runner=unknown ⚠️  QA RED → 原因調査 → Plannerに差し戻し (再分解) ✔ [Investigate#1] codex完了: 7 json行 (34.3s, exitCode=0, signal=none)ed:34.2s ✔ [Replan#1] codex完了: 88 json行 (202.8s, exitCode=0, signal=none)d:202.7s === 5.3 Dev: 再分解タスクの適用 (RED後) === ✔ [Dev-FE#1-retry] codex完了: 243 json行 (340.0s, exitCode=0, signal=none)d:339.9s ✔ [Dev-BE#1-retry] codex完了: 365 json行 (451.2s, exitCode=0, signal=none)elapsed:111.2s ✔ [Dev-FE#1-retry(impl)] codex完了: 141 json行 (140.5s, exitCode=0, signal=none)d:140.5s === 5.4 QA: 再実行 (再分解+実装反映後) === ✔ [QA#1-retry] codex完了: 160 json行 (146.1s, exitCode=0, signal=none)d:146.1s === 4) 実装イテレーション 2/3 === === 5) QA: 受け入れテスト === ✔ [QA#2] codex完了: 180 json行 (121.9s, exitCode=0, signal=none)d:121.8s QA#2 runner=unknown ⚠️  QA RED → 原因調査 → Plannerに差し戻し (再分解) ✔ [Investigate#2] codex完了: 7 json行 (24.8s, exitCode=0, signal=none)ed:24.7s ✔ [Replan#2] codex完了: 51 json行 (305.1s, exitCode=0, signal=none)d:305.0s === 5.3 Dev: 再分解タスクの適用 (RED後) === ✔ [Dev-BE#2-retry] codex完了: 695 json行 (525.5s, exitCode=0, signal=none)d:525.4s ✔ [Dev-FE#2-retry] codex完了: 753 json行 (823.1s, exitCode=0, signal=none)d:823.0s === 5.4 QA: 再実行 (再分解+実装反映後) === ✔ [QA#2-retry] codex完了: 170 json行 (122.8s, exitCode=0, signal=none)d:122.8s ✅ QA GREEN (after replan+dev) → 受け入れ完了 === 6) Docs: ドキュメント生成 === ✔ [Docs#2-retry] codex完了: 256 json行 (210.0s, exitCode=0, signal=none)d:209.9s 完了: 成果物/ログは 'out/' を参照してください。 このようにして、特定の工程から作業を再開でき、プロダクトを完成させ、それに関するドキュメントも作成してくれます! 想定される活用シーン 以上のようにAI-Scrumでは、計画書を作れば要点を押さえつつプロダクトを完成させてくれます。これを使えば以下のようなシーンで効率よく開発ができると考えています。  1. プロトタイプ作成 「クライアントに提案するためのデモが必要」  → 2-3時間で動くプロトタイプが完成 2. アイデア検証 「このアイデア、実際に形にしたらどうなるだろう?」  → 複数のバリエーションを短時間で作成・比較 3. 学習目的 「React/Next.jsを学びたいけど、何を作ればいいかわからない」  → アイデアを入力するだけで学習素材が完成 最後に AI-Scrumは以下のような人にとって役立つシステムだと思っています。  ・アイデアはあるけど、プログラミング経験が浅い  ・設計やテストまで考えるのが大変  ・とりあえず動くものを素早く作りたい  ・複数のアイデアを試して、一番良いものを選びたい  ・ドキュメント作成が面倒だけど、ちゃんとしたものを作りたい  そして、AI-Scrumはこれからもアップデートしていく予定です。是非今後とも動向を確認していただければと思います。ありがとうございました!   Thinkingsエンジニアでは、テックブログを更新中です! こちらもぜひご覧ください。 Thinkingsテックブログ | Zenn   記事執筆者 栗岡 陽平 Thinkings株式会社 2023年4月にThinkings株式会社へ新卒として入社。主に外部連携系のバックエンド開発を担当。 現在はCREという顧客課題の解決や既存機能のアップデートを通じ、顧客信頼性を高めていくことを目的としたチームでエンジニアとして働いています。  
アバター
  なぜ「UXはUIの一部」という誤解は生まれたのか 「機能」ではなく「心地よい体験」こそが価値の源泉となる AIエージェントの罠を超え、真のユーザー価値を追求するために     こんにちは。株式会社HuX でCEOをしている、亀田です。 本記事では、「UXはUIの一部」という誤解がデジタルプロダクトの歴史の中でいかにして生まれたのかを解説し、AI時代においてこの誤認がもたらす致命的な足枷について論じます。また、ユーザーのインサイトを追求し、AIの真価で価値を増幅させる真の体験設計のあり方を考察します。 なぜ「UXはUIの一部」という誤解は生まれたのか そもそも、なぜこれほどまでにUX(ユーザーエクスペリエン-ス)がUI(ユーザーインターフェース)の一部、あるいは単なる「使いやすさ」の追求だと誤解されるようになったのでしょうか。その背景には、デジタルプロダクトが辿ってきた歴史が深く関係しています。   ウェブサイトやモバイルアプリが普及し始めた時代、私たちのデジタル体験のほとんどは「画面」を通じて行われてきました。ユーザーがサービスに触れる主要な接点が画面である以上、そのレイアウトやボタンの配置、情報の見せ方といったUIの改善が、ユーザー体験の向上に直結する最も効果的な手段だったのです。UIは目に見え、具体的な改善点を指摘しやすいため、企業や開発者は自然とそこに注力しました。結果として、「優れたUIを作ること」が「優れたUXを提供すること」とほぼ同義に扱われるようになり、「UX/UIデザイン」という言葉が一人歩きを始め、両者の境界は曖昧になっていきました。   また、UIが持つ「可視性」も誤解を助長しました。UIはデザインカンプやモックアップとして具体的に示すことができますが、UXはユーザーの感情や満足度といった目に見えない抽象的な概念です。そのため、議論や評価がしやすいUIに話が終始しがちになり、UXという言葉が内包する「体験の全体性」という本質が見過ごされてきたのです。しかし、AI技術が社会に浸透する今、この誤解はプロダクトの成否を分ける致命的な足枷となりつつあります。     「機能」ではなく「心地よい体験」こそが価値の源泉となる UI中心の考え方がなぜ足枷となるのか。それは、ユーザーが本当に価値を感じ、ファンになる瞬間が、必ずしも優れた機能や美しい画面によってもたらされるわけではないからです。サービス全体の体験こそが、その価値の源泉となります。   例えば、多くの人がNetflixのファンになるのは、再生ボタンのデザインが優れているからでも、検索機能が高速だからでもありません。 「自分の見たい、あるいは自分でも知らなかった好みのコンテンツに次々と出会える」 という、中核となる体験価値に魅了されるからです。洗練されたUIや高性能なレコメンドAIは、あくまでその中核的価値をスムーズに届けるための手段に過ぎません。もしNetflixに魅力的なコンテンツがなければ、どれだけUIを改善しても、誰も使い続けないでしょう。   この本質は、AI時代のサービス設計において決定的に重要です。もしサービスそのものが提供する体験価値が凡庸であれば、そこにAIを導入しても、できることは「ちょっと便利なアシスタント」止まりです。   ユーザーの行動を先読みして操作を少しだけショートカットしてくれる、といったレベルに留まり、真に「心地よい」「これなしではいられない」と感じる体験を生み出すことはできません。AIの真価は、サービスが持つ本来の価値を増幅させ、より深く、パーソナルな体験としてユーザーに届けることで初めて発揮されるのです。 AIエージェントの罠を超え、真のユーザー価値を追求するために 現在議論されているAIエージェントの多くは、タスクの自動化や効率化に焦点を当てています。しかし、ユーザーの行動を観察し、それをただ自動化するだけでは、本質的な価値の提供には至りません。それは単なる 「効率化」 で終わってしまい、ユーザーの心を動かす「心地よさ」には繋がらないのです。   この罠を避けるためには、プロダクト開発の原点に立ち返る必要があります。つまり、データやログを眺めるだけでなく、 ユーザーの心の奥底にあるインサイト(洞察)を追求するマネジメント手法 へと回帰することが極めて重要です。     例えば、私が提唱する「 コスプレUX 」のように、ペルソナになりきってその生活を追体験し、ユーザーが本当に求めているものは何か、どんな瞬間に喜びを感じるのかを肌で理解しようとするアプローチは、AI時代にこそ有効性を増します。   図 コスプレUXの手順   こうしたインサイトに基づいて「我々が提供すべき本当の価値は何か」という議論がなされないまま開発を進めてしまえば、できあがるAIは「多少便利なエージェント」で終わってしまいます。それは技術の可能性を大きく無駄にしていると言えるでしょう。   今まさに求められているのは、定義されたユーザー価値をさらに高めるためにAIをどう活用するかを考え抜き、 AIにさらなる心地よい体験を提供させるプロダクトマネジメント なのです。   そして、そのすべてを実現するための第一歩は、極めてシンプルです。それは、私たち自身が持つ「UX」という言葉の定義を、UIという狭い領域から解放し、サービスに関わる体験の総体として捉え直すこと。AI時代における優れたプロダクト開発は、その UXの再定義 から始まるのです。   記事執筆者 亀田重幸 株式会社HuX CEO / HCD-Net認定 人間中心設計専門家 / Goodpatch Anywhere PM / 日本大学 文理学部 情報科学科 大澤研究室 プロボノ HAI研究  
アバター
デモアプリケーションの紹介 主な機能 技術構成 Denoとは Deno Deployとは 参考 Deno KVとは ローカル環境でのDeno KV Deno KV on Deno Deploy 参考 WebSocketとは 参考 デモアプリケーションを作りました アプリケーションの構成 主な機能 技術的な実装 まとめ 株式会社ゆめみでサーバーサイドエンジニアをしている、うーたんです。 2023 年 4 月に株式会社ゆめみへ新卒入社し、現在はサーバーサイドエンジニアとして案件を担当しています。 技術同人誌への寄稿やコンピューター雑誌への執筆活動も行っており、 ゆめみ大技林 '24 や I/O 2024年4月号 などに寄稿したことがあります。 本記事では、Deno の紹介と、Deno Deploy、Deno KV、WebSocket を使ったサンプルアプリケーションの開発体験について、デモアプリケーションを交えてご紹介します。 デモアプリケーションの紹介 今回開発したアプリケーションは「Google スライドのスライド送りを簡単に複数端末で同期できる君」です。 URL: https://sync-google-slide.vercel.app/ このアプリケーションでは、Google スライドの URL を入力するだけで、複数の端末でスライドのページ送りを同期できます。たとえ、プレゼンテーション中に発表者がスライドを進めると、聴衆の端末でも同じタイミングでスライドが進むようになります。 主な機能 Google スライドの URL を入力してスライドを登録 複数端末でのリアルタイム同期 ユーザーごとのスライド履歴管理 技術構成 バックエンド:Deno + Deno Deploy + Deno KV + WebSocket フロントエンド:Next.js + Vercel + Material-UI それでは、使用した技術について詳しく見ていきましょう。 Denoとは Deno(/ˈdiːnoʊ/、ディーノと発音)は、セキュアなデフォルト設定と優れた開発者体験を提供するオープンソースのランタイムです。JavaScript、TypeScript、WebAssembly をサポートし、V8、Rust、Tokio をベースに構築されています。 Deno公式ドキュメント によると、Deno の主な特徴は次のとおりです。 TypeScript-ready out of the box :ゼロ設定で TypeScript を直接実行できる Secure by default :他のランタイムがスクリプトに完全アクセスを与えるのに対し、Deno では細かい権限を強制できる たとえば、ファイルシステムへのアクセスが必要な場合は、明示的に権限を許可する必要がある。 権限を指定しない場合、Deno は安全にスクリプトを実行し、必要な権限がない場合はエラーを返す。 Robust built-in toolchain :Node やブラウザ JavaScript とは異なり、Deno には標準ライブラリ、リンター/フォーマッター、テストランナーなどが組み込まれている Fully compatible with Node and npm :Node と npm との完全な互換性を提供する Fast and reliable :高速で信頼性の高い実行環境である Open-source :オープンソースプロジェクトである Deno Deployとは Deno Deploy は、Deno の公式クラウドプラットフォームです。Deno で書かれたアプリケーションを簡単にデプロイし、グローバルに配信できます。 Deno Deploy の主な特徴は以下です。 WebSocketのサポート :リアルタイム通信が必要なアプリケーションでも設定なしで動作する Deno KVの利用可能 :Deno の組み込み key-value ストアをクラウド上で利用できる エッジコンピューティング :世界中のエッジロケーションで実行され、低レイテンシを実現する HTTPSの自動設定 :SSL 証明書の管理が不要で、自動的に HTTPS が有効になる GitHubとの連携 :GitHub リポジトリと連携して、プッシュするだけで自動デプロイが可能である 特に、WebSocket と Deno KV が簡単に利用できる点は、データの保存とリアルタイム通信を必要とするデモアプリケーションの開発において大きなメリットでした。 参考 Web Streams at the Edge | Deno KV on Deno Deploy The Future of the Web is on the Edge | Deno Deno Deploy Deno KVとは Deno KV は、Deno に組み込まれた key-value ストアです。RDB とは異なり、シンプルな key-value のペアでデータを保存・取得できます。 なお、Deno KV は現在開発中で、仕様変更の可能性があります。使用するには、Deno に --unstable-kv フラグを渡す必要があります。 ローカル環境でのDeno KV ローカル環境では、Deno KV は SQLite データベースを使用します。SQLite は設定不要のコンパクトかつ高速なデータベースであり、Deno KV も同様に設定不要で高速に動作します。 Deno KV on Deno Deploy Deno Deploy では、分散データベースである FoundationDB を使用します。FoundationDB は Apple がオープンソース化した NoSQL データベースで、分散環境で高速に動作しつつトランザクション操作により全体としてデータの強い一貫性を実現できます。 Deno KV には、次の特徴があります。 組み込みサポート :追加の依存関係なしで利用可能 シンプルなAPI :複雑なクエリ言語を覚える必要がない ローカル環境 :SQLite を使用し、設定不要で高速動作 Deno Deploy環境 :FoundationDB を使用し、分散環境でも強い一貫性を実現 このデモアプリケーションでは、スライドの情報やユーザーデータを Deno KV に保存し、複数の端末間でリアルタイムに同期しています。 参考 Deno KV Documentation Deno KV on Deno Deploy Deno、JavaScript用データストア「Deno KV on Deno Deploy」オープンベータに。分散環境でも強い一貫性、1GBストレージまで無料 - Publickey Deno、JavaScript/TypeScriptのためのデータストア「Deno KV」発表。Deno本体にSQLiteを統合、分散環境では強い一貫性も提供 - Publickey WebSocketとは WebSocket は、クライアントとサーバー間でリアルタイムの双方向通信を可能にするプロトコルです。HTTP とは異なり、一度接続を確立すると、サーバーからクライアントへのプッシュ通信が可能になります。 このデモアプリケーションでは、スライドのページ送りを複数の端末で同期するために WebSocket を使用しています。1 つの端末でスライドを進めると、他の端末でも同じタイミングでスライドが進むようになります。 参考 WebSocket API - MDN WebSocket on Deno Deploy デモアプリケーションを作りました 今回開発したデモアプリケーションは「Google スライドのスライド送りを簡単に複数端末で同期できる君」です。このアプリケーションは、Google スライドの URL を入力するだけで、複数の端末でスライドのページ送りを同期できるツールです。 アプリケーションの構成 バックエンド(Deno + Deno Deploy) Deno で開発された REST API と WebSocket サーバー Deno Deploy でホスティング Deno KV を使用してスライド情報とユーザーデータを管理 WebSocket を使用してリアルタイム同期 フロントエンド(Next.js + Vercel) Next.js で作成したアプリケーション Vercel でホスティング 主な機能 リアルタイム同期 :複数の端末でスライドのページ送りを同期 ユーザーのスライドを管理 :ユーザーごとにスライドの履歴を管理 技術的な実装 バックエンドでは、Deno の標準ライブラリと WebSocket API を使用して、リアルタイム通信を実現しています。Deno KV を使用することで、複雑なデータベース設定なしに、スライド情報を管理しています。 まとめ このプロジェクトを通じて、Deno と Deno Deploy の機能を知ることができました。特に次の点が印象的でした。 開発の簡素化 :TypeScript の標準サポートにより、型安全性を保ちながら開発を進められました デプロイの簡単さ :Deno Deploy と Vercel の組み合わせにより、フロントエンドとバックエンドを簡単にデプロイできました リアルタイム機能の実現 :WebSocket と Deno KV の組み合わせにより、複雑な設定なしにリアルタイム機能やスライドの管理機能を実装できました Deno の TypeScript の標準サポートとデフォルトでセキュアな設計は、アプリケーション開発においてメリットだと感じました。今後も Deno を使っていきたいです。 記事執筆者 うーたん 氏 株式会社ゆめみ 技術書同人誌博覧会のコアスタッフや若手ふんわり勉強部という若手エンジニア向け勉強会、エンジニアニメというをIT業界に関わる方が集まってアニメから得た学びについて語り合いながら交流する勉強会を主催しています。 勉強会や技術カンファレンスなどで登壇や技術同人誌を執筆も行っています。  
アバター
AI時代における技術力の重要性 初期アーキテクチャの重要性 アウトプットの習慣とナレッジシェア 個人と組織で取り組むべきナレッジシェア 記事執筆者   技術tipsへの初めての寄稿をさせていただきます。 古川( @yosuke_furukawa )です。私自身がデザインシステムの勉強会で登壇させていただきました。以下にそのリンクを貼らせていただきます。   デザインシステムが必須の時代に - Speaker Deck   「デザインシステムが必須の時代に」という話をさせていただきましたが、その裏側で私が伝えたかったことをお話します。 2025年のAI時代、皆様如何お過ごしでしょうか。私も皆様と同じくAIの恩恵を受けながら開発を行っております。AIをしばらく利用してて思うこととしては下記の2点があります。 AI時代における技術力の重要性: プロンプトの書き手がどれくらいの技術力と経験があるかでAI自身の問題解決能力が変わる 初期アーキテクチャの重要性: 逆に言うと誰が書き手でも一定以上のパフォーマンスを得ようとした場合、初期のアーキテクチャがどうなっているかが重要 AI時代における技術力の重要性 AI時代における技術力の重要性 に関しては特に異論はないと思います。プロンプトをどこまで正確に記述してもAI自身が間違うことはあります。その間違いに対してどんな指摘で軌道修正させるかを考える必要があります。そのときに複数の解決策が思いつくかどうか、複数の解決策から最もシンプルで迷いの少ない手を選べるかは技術力と経験に寄ります。   今までに、Claude CodeやCopilot、Cursorといったツールを活用して複数のアプリケーションを開発してきました。慣れていないネイティブアプリから、慣れているWebアプリまで、幅広く開発してきましたが、ネイティブアプリの開発では、不慣れな部分もあり、AIが複雑なコードを生成してミスをした際、すぐに解決策を見つけられず、そのまま進めてしまったことがありました。その結果、非効率な開発となり、品質の低下を招きました。一方、Webアプリでは、問題に直面しても解決策を提示し、スムーズに開発を進めることができました。 初期アーキテクチャの重要性 初期アーキテクチャの重要性 に関しても異論はないんじゃないでしょうか。初期に整理整頓されたアーキテクチャを作ることで、その後のミスを減らす効果につながると感じています。AIは事前にヒントとなる情報があるかどうかでその出力性能が大幅に変わります。   わかりやすく言えば、 「優秀なアーキテクトが設計した基盤の上でAIを動かす」 のと、 「何もない状態からAIにアーキテクチャを構築させる」 のとでは雲泥の差があります。   この2つの話をまとめると、「優秀なアーキテクトや優れた経験と技術力を持つ技術者がいれば、AIを活用した開発はスムーズに進む」という、ある意味当たり前の結論に行き着きます。しかし、現実には、そうした優れた人材はどこにでもいるわけではありません。 したがって、AI時代になっても、高い品質やパフォーマンスを追求するためには、私たち人間自身が設計・実装能力を高める努力を続け、学びながら開発を進めていく必要があります。 アウトプットの習慣とナレッジシェア これまで、私たちは学習しながら開発するときに、さまざまな方法を行ってきたと思います。本を読んだり、実際に小さなアプリケーションを作ってみたりする人もいるでしょう。 このとき、 学んだことをアウトプットする習慣があるかどうかが、これからのAI時代で最も大きな差を生むと考えます。 なぜなら、アウトプットした内容は、そのままAIの貴重なインプットになるからです。 たとえば、勉強会や開発ブログで共有した内容は、開発のヒントとして残り続けます。これまで組織は、知識を体系化して共有することで、開発の不確実性に対応してきました。AI時代になっても、この重要性は変わりません。むしろ、これまで以上に重要になってくるでしょう。 個人と組織で取り組むべきナレッジシェア ナレッジシェアが重要になる時代に、個人と組織がどう取り組むべきかについてお話しします。   まず、個人としては、これまで以上に学びとアウトプットを継続していく必要があります。幸い、AIを活用すれば、まったく経験のない分野でも最初の一歩を踏み出しやすくなりました。私自身も全くやったことのないインフラの設定やネイティブアプリの開発などをAIでスタートダッシュすることで最初の垣根を突破して学びの機会を得ています。 次に、組織としては、社員一人ひとりが学んでいるナレッジをうまく外に出させるように仕掛けを作る必要があります。例えば以下のような仕掛けでしょうか。   社内勉強会の開催でのナレッジシェアの機会の創出 ハッカソンやアイデアソンなどの新しいものを試す機会の創出 社内に閉じるだけではなく、社外に出てカンファレンスなどでも登壇していくことで、形式化する   学んだ機会をうまく活用することはAI時代も変わりません。デザインシステムも広く捉えれば社内のナレッジやノウハウをデザインやコンポーネントという形で形式化したものです。   「デザインシステムが必須の時代に」というテーマでお話ししましたが、本当に伝えたかったのは「 ナレッジシェアが必須の時代になった 」ということです。 みんなで開発をこれまで以上に盛り上げていけたら嬉しいです。 また、宣伝になってしまいますが、JSConf.jpというカンファレンスもあります。もし機会があれば、ぜひ登壇して知識を共有していただけると幸いです。   記事執筆者 古川陽介 JSConf.jp 開催主催者 Japan Node.js Association代表理事を務める。  
アバター
インタビュー業務 n8nとは? 従来の日程確定後の手動プロセスと課題 従来の手動プロセス n8nを使った自動化ワークフローの設計 実装の詳細とコードサンプル 1. ワークフロー全体の設定 2. Gmail監視の設定 3. OpenAIによる情報抽出 4. Notion書記シート自動作成 5. Slack承認ワークフロー 6. Google Calendar自動登録 7. 日時フォーマット処理 8. 最終完了通知 ワークフロー接続の構成 Human in the loopアプローチを組み込むと効果的 最後に 記事執筆者   こんにちは!スマートバンクでUXリサーチャーをしている、Harokaです。 現在、リサーチ業務でAI活用を進めており、各種ツールを使いながら、業務全体のワークフローや、進め方を見直しています。 今回、ユーザーインタビューで日程が決まってから書記シートの準備、予定表の登録等チームに連携する業務をn8nで自動化に挑戦しました! 本記事では、具体的な実装手順とコードサンプルを交えてご紹介します。 インタビュー業務 リサーチチームでは、インタビュー対象者とのやり取りにおいて、日程確定後に以下のような業務を行っています。 日程確定メールの内容確認と情報抽出  Notionでの書記シート作成と基本情報入力   Google Calendarへの予定登録と参加者招待 チームメンバーへのSlack通知 これらの作業はすべてリサーチOps(オペレーション)のメンバーが実施しているのですが、1件あたり約15分(月間換算すると5時間以上!)程度かかります。 特に日程確定メールから必要な情報を正確に抽出し、複数のツールに転記する作業は、注意力を要します。 そこで、オープンソースのワークフロー自動化ツール「n8n」を活用して、日程確定後の業務を大幅に自動化することにしました。 n8nとは? n8nは、ノーコード・ローコードでカスタムワークフローを作成できる自動化ツールです、SlackやNotion、Gmai、OpenAIなど200以上のサービスとの連携が可能です。ほか、JavaScriptでカスタムロジックを実装可能なので、何かしらワークフローを作りたい!と思ったら実現しやすいような印象です。 なんといっても、ビジュアルインターフェースなので非エンジニアの私でも直感的に使いやすいと感じています。 従来の日程確定後の手動プロセスと課題 従来の手動プロセス 日程確定後の業務フローは以下のような流れでした: 日程確定メール確認 :Gmailで日程確定メールを受信 情報抽出 :メール本文から対象者名、日時、場所等の情報を手動で確認 Notion書記シート作成 :手動でページ作成、基本情報入力 カレンダー登録 :Google Calendarに手動で予定追加、参加者招待 Slack通知 :チームメンバーに手動で実施予定を共有 これらを、n8nを中心とした進め方にガラッと変えられるか検証しました。 n8nを使った自動化ワークフローの設計 n8nを中心とした進め方として、以下を検討しました。インタビューメールが届いたことをトリガーに、書記シート、予定表を同時に作成・登録できるようにしています。   Gmail監視 → AIでメール情報抽出 → Notion書記シート作成 → Slack通知 →  カレンダー登録 → データ記録 → 完了通知   ワークフロー全体像   実装の詳細とコードサンプル 1. ワークフロー全体の設定 まず、ワークフロー全体の構成を定義します:   { " name ": " 1.インタビュー予約→書記シート、予定表作成 ", " nodes ": [ // 各ノードの設定(詳細は後述) ] , " connections ": { // ノード間の接続設定 } , " active ": true , " settings ": { " executionOrder ": " v1 " } }   2. Gmail監視の設定 日程確定メールを自動検知するGmailトリガーノードの設定:   { " parameters ": { " pollTimes ": { " item ": [ { " mode ": " custom ", " cronExpression ": " 0 6-22 * * * " } ] } , " simple ": false , " filters ": { " q ": " インタビュー決定 " } , " options ": {} } , " type ": " n8n-nodes-base.gmailTrigger ", " typeVersion ": 1.2 , " name ": " Gmail Trigger " }   3. OpenAIによる情報抽出 メール内容からインタビュー情報を自動抽出するOpenAIノードの設定:   { " parameters ": { " modelId ": { " __rl ": true , " value ": " gpt-3.5-turbo ", " mode ": " list " } , " messages ": { " values ": [ { " content ": " 件名: {{ $json.subject }} \n ``` \n\n ## 抽出情報 \n\n ・タイトル(タイトルの見出しの下にある部分を抽出) \n ・開始日時(dateのタイムスタンプから、JSTに変換して表示、年はメールの受信日時から割り出してください。日程と時間を書いてください。) \n ・終了日時(dateのタイムスタンプから、JSTに変換して表示、年はメールの受信日時から割り出してください。日程と時間を書いてください。) \n ・開始日(日付のみ、曜日含めてフォーマット化) \n ・開催時間(開始時間から終了時間まで) \n ・場所 \n ・インタビュアー(名前、メールアドレス) \n ・インタビュイー(名前、メールアドレス) \n ・会議室 \n ・メモ \n\n ## output sample \n\n ```json \n { \n \" title \" : \" TITLE \" , \n \" start_datetime \" : \" 2025-11-10T00:00:00.000+09:00 \" , \n \" end_datetime \" : \" 2025-11-10T01:00:00.000+09:00 \" , \n \" date \" : \" 1月10日(月) \" , \n \" time \" : \" 00:00~01:00 \" , \n \" place \" : \" https://example.com \" , \n \" interviewer \" : { \n \" name \" : \" インタビュアー名 \" , \n \" email \" : \" interviewer@your-company.com \"\n }, \n \" interviewee \" : { \n \" name \" : \" インタビュイー名 \" , \n \" email \" : \" interviewee@example.com \"\n }, \n \" room \" : \" https://meet.google.com/xxx-yyyy-zzz \" , \n \" memo \" : \" 備考欄 \"\n } \n ``` \n\n {{ $json.text }} " } ] } , " jsonOutput ": true , " options ": {} } , " type ": " @n8n/n8n-nodes-langchain.openAi ", " name ": " OpenAI " }   4. Notion書記シート自動作成 抽出した情報を基にNotionページを自動作成:   { " parameters ": { " resource ": " databasePage ", " databaseId ": { " __rl ": true , // 実際のNotionデータベースURLに置き換えてください " value ": " https://www.notion.so/your-workspace/YOUR_DATABASE_ID?v=YOUR_VIEW_ID ", " mode ": " url " } , " title ": " ={{ $json.message.content.interviewee.name }}様書記シート ", " simple ": false , " propertiesUi ": { " propertyValues ": [ { // Notionデータベースのプロパティ名に合わせて調整してください " key ": " 実施日|date ", " date ": " ={{ $json.message.content.start_datetime }} " } ] } , " options ": {} } , " type ": " n8n-nodes-base.notion ", " name ": " CreatePage " }   5. Slack承認ワークフロー 書記シート作成後、リサーチOpsメンバーに通知し、承認を待つ仕組み   { " parameters ": { " operation ": " sendAndWait ", " select ": " channel ", " channelId ": { " __rl ": true , // 実際のSlackチャンネル名に置き換えてください " value ": " #your-research-channel ", " mode ": " name " } , // メンション先やメッセージ内容は組織に合わせて調整してください " message ": " @research-team \n Notionに書記シートが作成されました。 \n 必要情報を入力し、入力を終えたらApprovalボタンを押してください。 \n\n {{ $json.url }} ", " options ": {} } , " type ": " n8n-nodes-base.slack ", " name ": " NotifyCreatedPage " }   6. Google Calendar自動登録 承認後、カレンダーへの予定登録を自動実行:   { " parameters ": { " calendar ": { " __rl ": true , // 実際のGoogleカレンダーのメールアドレスに置き換えてください " value ": " your-email@your-company.com ", " mode ": " list " } , " start ": " ={{ $('FetchPage').item.json.properties['実施日'].date.start }} ", " end ": " ={{ $json.interviewEndDateTime }} ", " additionalFields ": { " attendees ": [ // Notionの参加者プロパティ名は実際の設定に合わせて調整してください " ={{ $('FetchPage').item.json.properties['参加者'].people.map(item => item.person.email).join(',') }} " ] , // 説明文の内容は組織に合わせて調整してください " description ": " 書記シート: {{ $('FetchPage').item.json.url }} \n\n 会議室URL: {{ $('OpenAI').item.json.message.content.place }} ", " summary ": " ={{ $('OpenAI').item.json.message.content.title }} " } } , " type ": " n8n-nodes-base.googleCalendar ", " name ": " Google Calendar " }   7. 日時フォーマット処理 Slack通知用に日時を適切にフォーマット:   { " parameters ": { " operation ": " formatDate ", " date ": " ={{ $('Google Calendar').item.json.start.dateTime }} ", " format ": " custom ", " customFormat ": " yyyy/MM/dd (EEE) T ", " outputFieldName ": " formattedStartDate ", " options ": {} } , " type ": " n8n-nodes-base.dateTime ", " name ": " FormatStartDate " }   8. 最終完了通知 全ての処理完了後、チームに結果を通知:   { " parameters ": { " select ": " channel ", " channelId ": { " __rl ": true , // 完了通知用のSlackチャンネル名に置き換えてください " value ": " #your-research-report-channel ", " mode ": " name " } , " messageType ": " block ", // Slack Block Kitの構成は組織の通知フォーマットに合わせて調整してください " blocksUi ": " { \n\t\" blocks \" : [ \n\t\t { \n\t\t\t\" type \" : \" section \" , \n\t\t\t\" text \" : { \n\t\t\t\t\" type \" : \" plain_text \" , \n\t\t\t\t\" emoji \" : true, \n\t\t\t\t\" text \" : \" インタビューが予約されました \"\n\t\t\t } \n\t\t }, \n\t\t { \n\t\t\t\" type \" : \" divider \"\n\t\t }, \n\t\t { \n\t\t\t\" type \" : \" section \" , \n\t\t\t\" text \" : { \n\t\t\t\t\" type \" : \" mrkdwn \" , \n\t\t\t\t\" text \" : \" *<{{ $('Google Calendar').item.json.htmlLink }}|{{ $('Google Calendar').item.json.summary }}>* \\ n{{ $('FormatStartDate').item.json.formattedStartDate }}-{{ $json.formattedEndDate }} \\ n<{{ $('FetchPage').item.json.url }}|書記シート> \"\n\t\t\t }, \n\t\t\t\" accessory \" : { \n\t\t\t\t\" type \" : \" image \" , \n\t\t\t\t\" image_url \" : \" https://api.slack.com/img/blocks/bkb_template_images/notifications.png \" , \n\t\t\t\t\" alt_text \" : \" calendar thumbnail \"\n\t\t\t } \n\t\t } \n\t ] \n } ", " text ": " インタビューが予約されました " } , " type ": " n8n-nodes-base.slack ", " name ": " Slack1 " } ワークフロー接続の構成 各ノード間の接続関係は以下のように設定します。   { " connections ": { " Gmail Trigger ": { " main ": [ [{ " node ": " OpenAI ", " type ": " main ", " index ": 0 }] ] } , " OpenAI ": { " main ": [ [ { " node ": " CreatePage ", " type ": " main ", " index ": 0 } , { " node ": " Google Sheets ", " type ": " main ", " index ": 0 } ] ] } , " CreatePage ": { " main ": [ [{ " node ": " NotifyCreatedPage ", " type ": " main ", " index ": 0 }] ] } , " NotifyCreatedPage ": { " main ": [ [{ " node ": " FetchPage ", " type ": " main ", " index ": 0 }] ] } , " FetchPage ": { " main ": [ [{ " node ": " FormatInterviewDate ", " type ": " main ", " index ": 0 }] ] } , " FormatInterviewDate ": { " main ": [ [{ " node ": " Google Calendar ", " type ": " main ", " index ": 0 }] ] } } } Human in the loopアプローチを組み込むと効果的 今回、完全自動化ではなく、重要な判断ポイントでは人間が介入できる設計にしました。Human in the loopという考え方なのですが、これはAIや自動化システムの処理に人間の判断を適切なタイミングで組み込むことで、効率性と品質の両方を確保する手法です。 例えば、以下のようなアプローチです。 書記シート作成後の承認 :Slack通知で内容確認を促し、承認後に次の処理へ 品質保証 :AI抽出結果を目で見て確認 対象者へメールを送る等、最終的な責任は人間が持つ必要があります。 今回はチームメンバーが主な対象でしたが、目の届く範囲でAIを活用し、人間がミスをしやすい転記などは安心して任せられるような構造にしています。 この仕組みによって、定型対応で問題ないケースでは80%の作業が自動化される一方、インタビューに関するご質問がある等、例外的なケースでも柔軟に対応できる体制を作ることができました。 最後に この取り組みを通じて特に重要だと感じたのは、 AIを主語としたワークフロー設計 の考え方です。従来の「人間がAIを使う」から「AIが主体となって人間がサポートする」発想に転換するのが大事だなと感じました。 今後もこのアプローチを踏まえ、UXリサーチ業務全体の自動化を進めていきたいと考えています!皆様のお取り組みもぜひお聞かせください。   Harokaのnote 書籍 『UXリサーチの活かし方』 も発売中です   記事執筆者 瀧本 はろか 株式会社スマートバンク UXリサーチ部 部長 校正・校閲担当者を経て、ベンチャー企業で新規事業の立ち上げやインタビューの企画執筆を経験。その経験からUXリサーチャーに転身、人材会社の新規事業のUXリサーチやリサーチ組織立ち上げ、リサーチャー育成に携わる。2022年4月より株式会社スマートバンクに1人目のUXリサーチャーとして入社。N1インタビューの文化を受け継ぎ、年間100件を超えるインタビューを担当。メンバー全員が「Think N1」を身近に感じられるような働きや経営・事業に伴走するリサーチを推進。著作『UXリサーチの活かし方 ユーザーの声を意思決定につなげるためにできること(翔泳社)』。     .balloon5 { width: 100%; margin: 1.5em 0; overflow: hidden; } .balloon5 .faceicon { float: left; margin-right: -90px; width: 80px; } .balloon5 .faceicon img{ width: 100%; height: auto; border: solid 3px #d7ebfe; border-radius: 50%; } .balloon5 .chatting { width: 100%; } .says { display: inline-block; position: relative; margin: 5px 0 0 105px; padding: 17px 13px; border-radius: 12px; background: #d7ebfe; } .says:after { content: ""; display: inline-block; position: absolute; top: 18px; left: -24px; border: 12px solid transparent; border-right: 12px solid #d7ebfe; } .says p { margin: 0; padding: 0; }
アバター
はじめに スマートハウスの定義 構築したスマートハウスのシステムと、暮らす上で感じた課題 V2HとEV充電器Pulsar Plusの違いと併用方針 スマートハウスの運用で利用するアプリ 1日の流れを可視化してみた 1.    3時〜6時 2.    6時〜9時 3~4. 9時〜15時 5. 15時〜18時 6. 18時〜21時 7. 21時〜3時 GPTsにしてみた 入力内容 出力結果 まとめと今後の展望 記事執筆者 はじめに 先日、パーソルキャリアさんが主催する” エンジニアの自由研究発表会vol.10 〜業務外でエンジニアスキルを活かしてみた!〜 ”というイベントで、タイトルの通り登壇させていただきました。この記事では、登壇では語りきれなかったところまで詳細にお話したいと思います。 スマートハウスの定義 この記事では「スマートハウス」を、”創エネ、蓄エネ、省エネが可能で、電力利用を最適化した住宅”と定義します。具体的には、太陽光パネルで発電した電力を、蓄電池(もしくは電気自動車)に蓄電しつつ、HEMSを使って効率良く電力を使用する住宅となります。 構築したスマートハウスのシステムと、暮らす上で感じた課題 私は、2022年に一戸建ての住宅をリフォームしたタイミングで、自宅を完全にスマートハウス化しました。システム構成としては以下となります。 一般家庭としては大容量の10kW(システム容量9.5kW)の太陽光パネルを搭載し、また、一般的な家庭用蓄電池と比較するとはるかに大容量の電気自動車IONIQ5(アイオニック5、バッテリー容量72.6kWh)を導入し、発電量も蓄電量も多いスマートハウスが完成したため、かなり満足していました。   ですが、日々運用していく上で、気になる課題がありました。それは蓄電効率です。   電気自動車を蓄電池として使う(電気自動車から自宅に送電する)ためには”V2H”という機器が必要なのですが、このV2Hを通じて充放電をすると、気温や状況にもよりますが、1〜2kWほどの電力ロスが生じるのです。 その理由として主なものとは、この電気自動車が800V仕様のバッテリー電圧であり、400V仕様のV2Hを用いて充放電する際に変圧に電力を消費する点、バッテリー保護のためバッテリーの温度管理に電力を消費する点などがあります。いずれにしても、電気自動車を蓄電池として大事に使用するためには必要な電力であり、そもそも一消費者には改善しようがない部分です。   電気自動車から自宅への放電をする際にはV2Hを使うしかないですが、充電時(自宅から電気自動車への充電)には、V2Hではなく一般的なEV充電器でもできると思い、EV充電器としてWallbox社のPulsar Plusを追加で導入しました。 Pulsar Plus導入後のシステム構成は以下となります。   V2HとEV充電器Pulsar Plusの違いと併用方針 V2HとPlusar Plusの違いをまとめました。 V2Hは、Vehicle to Homeという名称が示す通り、クルマのバッテリーから自宅に放電できるという機能を持っています。我が家で導入したのは、ニチコン社のEVパワーステーションプレミアムPlusです。 そのV2Hの同じケーブルを使って自宅からクルマに充電することも可能ですが、出力が最大6kWまでとなります(EV充電の業界では、6kWの充電は普通充電と比較して”倍速充電”と呼ばれます)。   上記で説明した通りV2Hを使って充電した場合は電力ロスが生じますし、10kWの太陽光パネルを搭載している我が家の最大発電電力は、日中9kWを越えます。そのため、”充電”をする際には、電力ロスが比較的少なく、最大出力が8kWであるPulsar Plusを優先で使いたいのです。   まとめると、簡単そうにみえますが、以下を実現できれば良いわけです。 電気自動車から自宅へ「放電」する際はV2Hを利用 自宅から電気自動車へ「充電」する際はPulsar Plusを利用   IONIQ5には急速充電用と普通充電用の2つの充電口があり、 急速充電用(DC、CHAdeMO)の充電口にはV2H 普通充電用(AC、200V)の充電口にはPulsar Plus この通りケーブルをそれぞれ挿しておき、アプリ操作でV2HとPulsar Plusの利用を切り替えます。   一見簡単そうにみえる操作ですが、充放電の理想的な切り替えタイミングは、電力単価の変動、バッテリー残量や数時間後の発電電力(予測値)、外出有無などによって変わることから、一律のタイマー設定だけだと意図しない挙動となる日もでてきます。タイマー設定だけに頼らず、その都度状況に応じて判断し、手動で切り替えをする必要があります。 スマートハウスの運用で利用するアプリ エネルギーマネジメントで利用しているアプリの一部をご紹介します。 まず、我が家ではNature 社のNature Remo EというHEMSを愛用しており、Natureアプリでは、自宅の電力のトータルの可視化と記録を行っています。   次に、クルマメーカのHyundaiが提供するBluelinkアプリでは、クルマのバッテリー残量のリアルタイムでの把握が可能です。V2Hのメーカであるニチコンが提供するEVPSアプリ、Pulsar PlusのメーカであるWallboxが提供するWallboxアプリでは、それぞれの機器の充放電の制御やタイマー管理を行います。   1日の流れを可視化してみた さて、我が家で使っているガジェットやアプリをご紹介したところで、毎日私がどのように考え、充放電を切り替えているのか、1日の流れに沿ってお伝えしましょう。   前提条件は以下となります。 電気自動車IONIQ5のバッテリー容量は72.6kWhで、SoC(バッテリーの残量)は、30%<SoC<95%の範囲内で運用することを努力目標とする。バッテリーの劣化の原因である過充電・過放電を避けるためと、急な外出や停電などに備える目的である。 買電単価が時間帯によって変動する、まちエネの「昼ワッ得プラン」を採用。デイタイム(9:00~15:00):23.15円/kWh、ピークタイム(16:00~21:00):43.32円/kWh、ベースタイム(それ以外):36.43円/kWh 使用する充放電機器は、Pulsar Plus(図内では”Pulsar”と表記)、V2H(ニチコン社EVパワーステーションプレミアムPlus)の2種類 1.    3時〜6時 この時間帯は、天気や季節によらず、太陽光パネルの発電電力<自宅の消費電力となる。 3時時点でクルマのバッテリー残量が55%以上あれば、V2Hの放電を開始し、自宅の消費電力をクルマから供給する。クルマのバッテリー残量が55%未満であれば何もしない(買電したまま)。 2.    6時〜9時 この時間帯は、日の出から数時間経過し、晴れていれば太陽光の発電電力が自宅の消費電力を越え始める頃である。クルマのバッテリー残量が45%以上あり、その日の天候が晴れであればV2Hを放電モードでONにする。 そうすることで、 太陽光の発電電力>自宅の消費電力 の場合→発電電力を自宅で消費し、余った余剰電力をクルマに充電 太陽光の発電電力<自宅の消費電力 の場合→発電電力を使用するとともにクルマからも電力を供給し、買電はしない を実現する。 3~4. 9時〜15時 9時以降15時までは日がのぼっており、発電のゴールデンタイムである。太陽光の発電電力が3kW以上あれば、Pulsar PlusをONにして、クルマへの充電を開始する。 曇りや雨で発電出力が3kW未満の場合は何もせず、電力会社から買電して暮らす。 また、この時間帯は「昼ワッ得プラン」では買電単価が23.15円/kWhと比較的安く、多少の買電は許容できると考えて運用を行う。当日あるいは次の日に長距離のドライブ予定がある場合は、走る距離に応じて目標SoCを設定し、9時〜15時に充電が完了するようPulsar Plusの出力アンペア数を調整し、充電を行う。 5. 15時〜18時 特に16時〜21時は買電単価が高くなるため、「買電して電気自動車に充電する」ことがないよう、確実にPulsar Plusをオフにしておく。 15時時点で、太陽光の発電出力が4kW以上あるか、電気自動車のバッテリー残量が95%ある場合、V2Hを放電モードにする。 前者の場合は、日が沈むまで余剰電力を電気自動車に充電するため。後者の場合は、どのような天候であっても、次の日の朝日が昇るまで放電を継続しても電力が足りる(電気自動車のSoCが30%を下回ることはない)ためである。 6. 18時〜21時 18時時点で電気自動車のバッテリー残量が85%以上ある場合、V2Hを放電モードにする。 その後任意のタイミングで、平均の消費電力を2.7kW(自宅の消費電力1.2kW+V2Hの電力ロス1.5kW)とし、次の日の朝6時頃までの消費電力量が、電気自動車のバッテリーの使用可能電力量を下回れば、V2Hを放電モードにする。 7. 21時〜3時 この時間帯は、特にアクションは必要なし。電力マネジメントのことは忘れて十分に睡眠を取る。 GPTsにしてみた 1日の取り組み内容はいかがでしょうか。 私のように、毎日太陽光の発電電力や消費電力をモニタリングしている人であれば、季節や天気に応じてだいたいの発電量や消費電力が予測できるので、その時々で何をすべきかをすぐに判断できますが、スマートハウスの初心者にとってはかなり難易度が高いのではないでしょうか・・・。 ということで、誰もがすぐに合理的な判断が出来るようにするため、GPTsを作成しました。     入力内容 前提条件:太陽光パネル出力(kW)、電気自動車バッテリー容量(kWh)、V2H・EV充電器ありなし、契約している電力プラン 現在:太陽光出力(kW)、EVのSoC(%)(※必要に応じて、今日明日の天気、外出予定) 出力結果 このように、その時々でやるべきことを教えてくれます。   ユーザー こんにちは GPTs こんにちは! 「電気代ほぼ0円生活」を一緒に目指しましょう 😊 まず、以下の前提条件を教えてください: 1. 太陽光パネルの出力(例:5.5kW など)   2. EVのバッテリー容量(例:60kWh など)   3. V2HやEV充電器はお持ちですか?両方ある場合はそれも教えてください   4. 現在の電力プラン名    → わからなければ、「昼間安い・夜間安い・一日中同じ」のどれかでOKです。 そして、現在の状況もお願いします: 5. 現在の時刻(例:午後3時など)   6. 現在の太陽光発電出力(例:3.2kWなど)   7. EVのバッテリー残量(SoC)(例:55%など)   8. 今日の天気(例:晴れ・曇り・雨など)    ※現在が夜なら、明日の天気予報もお願いします! これらをもとに、今日これからどう充放電するのが一番お得かを定量的に提案します!   ユーザー 10kW、72.6kWh、両方あり、昼間安い GPTs ありがとうございます!以下の前提条件を確認しました: ✅ 太陽光パネル出力:10kW   ✅ EVバッテリー容量:72.6kWh   ✅ V2HとEV充電器:両方あり   ✅ 電力プラン:昼間が安い 次に、現在の状況を教えてください: 1. 現在の時刻(例:午前8時、午後4時など)   2. 太陽光の現在の発電出力(例:5.8kWなど)   3. EVのバッテリー残量(SoC)(例:55%など)   4. 今日の天気(晴れ・曇り・雨など)    ※夜であれば、明日の天気予報も教えてください。 この情報が揃えば、「今どうするのがベストか(EVを充電 or 放電 or 待機)」を数値根拠つきでご提案します!     ユーザー 現在は13時。現在8kW、SoCは65%、今日は晴れのち雨 GPTs ありがとうございます!現時点の条件を整理します: 🔸現在の時刻:13時(昼)   🔸太陽光発電出力:8kW   🔸EVのSoC:65%(= 約47.2kWh 残量)   🔸天気:晴れのち雨(午後~夕方にかけて曇り・雨になる見込み)   🔸電力プラン:昼間が安い(夜間は割高) ✅ 現在の最適な行動:   → EVをEV充電器で充電する(できれば今の太陽光余剰電力の範囲内で) 🔍 根拠(定量的分析) ☀ 太陽光の発電と消費   - 発電量:8kW   - 家庭の消費(仮定):平均1.5~2.5kW   - 余剰電力 ≒ 5.5〜6.5kW程度(=EV充電に使える) 🔋 EVの充電可能量   - 現在SoC:65%(≒ 47.2kWh)   - 上限95%まで充電可能 → 95%-65% = 30%(≒ 21.8kWh)   - このままいけば、余剰電力だけで3~4時間ほどで充電可能 🔌 充電手段の比較 | 方式 | 利点 | 損失(ロス) | |------|------|--------------| | EV充電器 | 高効率・電力ロス少 | 約0.1kWh程度/h | | V2H充電 | ロス大きい | 1.5kWh/h の損失(☠️非推奨) | → 今は EV充電器 を使うのがベストです。 ⚠️ 注意点   - 午後~夕方にかけて曇ってきて発電量が下がると予想されるため、今のうちに充電しておくのが得策。   - SoCが95%を超えないように注意(バッテリー劣化の抑制) 🔄 次のアクションタイミング   - 16時〜17時頃の天気と発電状況を再確認 → 発電低下+家の消費増ならV2Hで放電する可能性も視野に。 必要なら、自動制御プランの設計(時間帯別の充放電スケジュール)もできますよ!どうしますか?   GPTsに言われた通りに日々動けば、太陽光パネルで発電した電力の効率的な蓄電ができ、買電電力量が減るため、電気料金の削減が可能です。また、5月〜9月の時期は、売電による収入も大きく見込めますから、トータルで黒字が実現できます。 まとめと今後の展望 本記事では、創エネとしての太陽光パネル、蓄エネとしての蓄電池(EV)、省エネとしてのHEMSを導入してスマートハウスを構築したのち、”蓄エネ”部分をEV充電器とV2Hを併用することで効率化し、スマートハウス全体の電力効率を向上させた経験をお伝えしました。また、その時々の合理的なアクションを誰もが実施できるよう、GPTsを作成しました。 今後は更にGPTsを改良し、極力マジックナンバーを排除し、様々な環境で利用できるよう一般化できればと考えています。     記事執筆者 ななみん  通信会社に勤務。自転車シェアサービス立ち上げ、スマートフォンの開発管理(Samsung・SONY)、FinTechサービス(ポイント投資サービス、THEO+ docomo)の立ち上げ、越境日本酒販売サービスの企画開発を担当。入社から一貫して新規事業の企画・開発に取り組む。 2017年から副業を開始し、大学からの受託業務(ハードウェアのプロトタイプ製作等)、スタートアップ企業のWebマーケティング(SEO・Google広告・SNS運用等)等を実施。 趣味は、IoTを使ったおうちハック、一戸建て自宅のスマートハウス化。太陽光発電システム・V2H・電気自動車を導入し「電力の自給自足」を実現し、電力やエネルギーマネジメントに関しても知見を有する。 https://note.com/nanamin_kedama https://www.instagram.com/nanamiryu/     .balloon5 { width: 100%; margin: 1.5em 0; overflow: hidden; } .balloon5 .faceicon { float: left; margin-right: -90px; width: 80px; } .balloon5 .faceicon img{ width: 100%; height: auto; border: solid 3px #d7ebfe; border-radius: 50%; } .balloon5 .chatting { width: 100%; } .says { display: inline-block; position: relative; margin: 5px 0 0 105px; padding: 17px 13px; border-radius: 12px; background: #d7ebfe; } .says:after { content: ""; display: inline-block; position: absolute; top: 18px; left: -24px; border: 12px solid transparent; border-right: 12px solid #d7ebfe; } .says p { margin: 0; padding: 0; } .table-of-contents{ li:nth-child(6){ ul{ li{ display: none; } } } }
アバター
はじめに Devinからのギフト🎁 1. AIとの会話の始め方 2. どんなことを頼めるのか、まずは相談 3. エンジニアでなくても大丈夫? 4. コードの全体像を把握 5. 具体的な業務イメージで質問 6. AIの「信頼度」表示の工夫 7. 社内ツールのクエリもAIに相談 8. SQLクエリの内容をAIに解説してもらう 9. 追加でやりたいことを相談 10. 追加実装の相談とAIのサポート 11. AIがSQLクエリや実装プランを提案 12. AIの成果物をそのままエンジニアに共有 おわりに 記事執筆者 はじめに こんにちは! スマートバンク でUXリサーチャーをしている、 Harok a です。2025年6月、TECH Streetコミュニティの事例・知見共有勉強会である、 UXリサーチャー勉強会 に登壇させていただきました。   「チームとAIで取り組むリサーチ」というタイトルでリサーチ活動におけるAI活用事例についてご紹介させていただいたのですが、今回は発表でお伝えしきれなかった、AIアシスタントとどのようにやりとりをしているかTipsをご紹介します。   登壇資料はこちらをご覧ください note.com   Devinからのギフト🎁 スマートバンクでは、エンジニアを中心に完全自律型AIコーディングエージェントである Devin を活用しています。   私自身、行動ログなど定量分析をする場合、エンジニアに依頼し、細かな調整を行って進めていくことをメインとしています。 また、過去類似した調査で活用したredashが存在する場合で、別の分析軸を追加して再度見てみたいケースなど、都度エンジニア相談することに申し訳なさを感じていました。   せめてコードに何が書いているかわかれば業務がスムーズに進むと思っていたため、私が定量分析する際の状況理解を進める手段として、Devinを活用することにしました。   Devinをインストールし、手始めに一つお願いしたのですが、なんとDevinからギフトクレジットをもらいました。 Devinを使ってクエリ作成のサポートをしてもらいました💡 Devinくん、お母さんはGPT-3なんですね🤱 pic.twitter.com/mcQj0n0W2K — はろっぺ | Haroka『UXリサーチの活かし方』 (@mawarisuru) 2025年6月13日   長らくDevinを活用している弊社のエンジニアからも「こんなことが起きるのははじめて!」と驚いており、私のDevinとのやりとりが的確でわかりやすかったからでは?と言ってくれました。   そこで、今回は、自分の振り返りも兼ねて、AIアシスタント(Devin)と実際にやりとりした事例をもとに、  「エンジニアではない立場から、どのようにAIに依頼し、やりとりを進めたか」の工夫したポイントを紹介 します。   先に結論をお伝えしておくと、この2点を意識するとゴールまで導きやすくなるように感じます。   ・自分が何に困っていて、何をしてほしいか、何ができないのかを説明する ・わからないことがでてきたら具体的に尋ねる 1. AIとの会話の始め方   Devinとは、Slackのやりとり用のチャンネルが用意されているため、そこで@Devinから会話をはじめていきます。   こんなやりとりから始まりました。   英語のやり取りの方が的確に伝わるかもしれませんが、速さを出すため自分が得意な(?)日本語に切り替えてもらうように依頼します。 (つい、AIツールに向かってさん・くん付けで喋ってしまうのでした) Haroka 「Devinくん、日本語でお話ししてもらえますか?」 Devin 「わかりました!日本語でお話しします。何かお手伝いできることはありますか?」 2. どんなことを頼めるのか、まずは相談 Devinとやりとりするのがはじめてだったので、まずは 自分のセットアップに不足がないか確認 するため、簡単に確認できる方法を聞いてみました。   Haroka AIがうまく動くか試してみたいです。おすすめのコマンドや進め方はありますか? Devin いくつかおすすめの試し方があります:  - 簡単なコードレビュー  - 小さなバグ修正  - テストの追加  - ドキュメント更新  - リファクタリング  どのリポジトリで何か試してみたいことはありますか? 3. エンジニアでなくても大丈夫? さすがDevinです。コードを実際に操作することができるようですが、私が期待する使い方ではないので、 自分の立場を明らかにし、引き続き質問 してみます。 私が期待する使い方は、 プロダクトのコードについて何が書かれているか理解し、適切にエンジニアとコミュニケーションをとって必要なデータを取得するサポート だからです。   Haroka 私はエンジニアではなく、コードは書けないのです。でも、既存のコードに何が書いてあるのか知りたいと思っています。こういう時はどうするといいでしょうか? Devin それは素晴らしいアイデアです!コードを読んで、何をしているのかを分かりやすく説明できます。どのリポジトリのどの部分について知りたいですか?特定の機能や画面について気になることがあれば教えてください。 4. コードの全体像を把握 「どのリポジトリのどの部分」と聞かれたので、何かしら指定する必要がありそうです。ただ、Devinがどう格納しているか、 どの伝え方だとわかるのか不明だったので、具体的に 聞いてみました。   Haroka 「そもそも、どんなリポジトリがありますか?」 Devin 「利用可能なリポジトリは以下の通りです:  - 管理画面のWebアプリ  - Androidアプリ  - コアAPI  - iOSアプリ  - Webコンポーネントライブラリ  - Webサイト  どのリポジトリについて詳しく知りたいですか?」 5. 具体的な業務イメージで質問 残念ながら「このリポジトリから探して」と指定ができるか不安だったので、やりたいことを伝えて一緒に探してもらうことにします。 ちなみに、 他のAIツールでも、探す範囲を指定すると正確に返答してくれる可能性が高い 気がしています。 Haroka 例えば、特定の加盟店で決済しているユーザーを探したいときはどのリポジトリになりますか? Devin コードを調べてみますね!  特定の加盟店で決済しているユーザーを探すには、主にコアAPIのリポジトリを使用します。  - 決済情報や加盟店情報を保存するテーブル  - 加盟店の詳細情報を管理するモデル  - ユーザーの決済履歴と加盟店を紐付けるテーブル  などが関係しています。  管理画面のWebアプリで検索・表示機能も提供されています。  (信頼度: 中程度。実際の検索クエリやAPIエンドポイントの詳細な実装方法については、より詳しい調査が必要です。) 6. AIの「信頼度」表示の工夫 「信頼度」という言葉を初めて見ました。この「信頼度」の定義を私が知っておかないと、後にエンジニアに何か依頼するときに考慮すべき事情があるのかもしれないと思い、確認しておきます。 Haroka 信頼度はどうやって度合いを決めているんですか? Devin 信頼度は、コードを実際に確認した範囲と推測の部分を区別するために付けています。  - 高:実際のコードを直接確認し、動作が明確に分かる場合  - 中:一部のコードは確認したが、全体像や詳細が不明な場合  - 低:推測や一般的な知識に基づく場合  このように透明性を保つことで、どこまで確実で、どこから推測なのかを明確にしています。 7. 社内ツールのクエリもAIに相談 なるほど、信頼度の定義が分かった上でコミュニケーションが取れそうと感じたので、早速本題に移ります。 特定のクエリについて、見方を教えてもらい、追加のクエリ生成のコミュニケーションをDevinに手伝ってもらうのが狙いです。 Haroka このredashのリンクにある情報は読めますか? Devin そのリンクにはアクセスできませんでした。  代わりに、クエリの内容をコピー&ペーストで教えていただければ、分析して詳しく説明できます!   8. SQLクエリの内容をAIに解説してもらう Redashはログイン情報が必要であり、権限的に取得できないことがわかりました。クエリの内容をテキスト情報で伝えると良さそうなので、指示通りに対応します。 Haroka (SQLクエリを貼り付け) Devin このクエリは、特定の加盟店で高額利用しているユーザーのトップ10を抽出しています。  - 決済データをベースに  - ユーザー情報を結合  - 特定の加盟店名で絞り込み  - 利用額を合計し、ランキングを作成  - 各加盟店ごとにトップ10ユーザーを表示  という流れです。  (信頼度: 高。実際のクエリを確認済み)     9. 追加でやりたいことを相談 実際のクエリはお見せできないのですが、構文の行を指し示し、どの範囲から何の条件で抽出しているかをわかりやすく書いてくれました。 読み違えていてはいけないので、 追加で依頼したい内容が書かれていないかをチェック します。 Haroka このクエリは、 決済回数の変遷について はわからないという認識であっていますか? Devin はい、その認識で正しいです。  - 利用額TOP10は現在のクエリで取得可能  - 決済回数や時系列変遷の分析には追加の集計ロジックが必要です  追加の集計ロジックを作りましょうか? (信頼度: 中。決済回数の集計機能の全体的な実装状況について、より詳細な調査が必要です。)   10. 追加実装の相談とAIのサポート 追加の集計ロジックを作り、それをエンジニアに確認してもらう流れを試してみたいと思ったので、 結論から伝えます 。その上で、 私がエンジニアではないことを再度伝え、的した形に考慮してもらう ようにしました。 Haroka 追加実装について進めていきましょう。私はエンジニアではないので、AIくんに教えてもらった内容をエンジニアに伝えたいです。または、自分で出してみたいです。できますか? Devin はい、できます!  - 追加で必要なSQLクエリの例  - 実装アプローチ  - エンジニアへの共有方法  などをまとめてお伝えします。 11. AIがSQLクエリや実装プランを提案 Devin 必要な追加SQLクエリの例:  - 月間ごとの時系列分析  - 四半期ごとのTOP10ユーザー分析  これらのクエリをエンジニアに共有すれば、Redashで実行できます! (ここから数分経過) 数分待っていると、これまでのやりとりを整理し、成果物を出してくれました。 Devin 推し活決済分析の要件と実装プランが整理できました!  - 既存のクエリでできること  - 追加で必要な機能  - エンジニアへの共有内容  これで要件の全体像と実装方法が明確になりました。エンジニアの方にこの内容を共有していただければ、スムーズに実装を進められると思います。 12. AIの成果物をそのままエンジニアに共有 こんな感じで、3つのファイルを用意してくれました。テキストデータではなく、ファイルをダウンロードして使いやすい形にしてくれるのに驚きです。 Haroka 素晴らしい!添付ファイルまで作ってくれるんですね!共有しますね! Devin ありがとうございます!エンジニアの方との共有がうまくいくことを願っています。何か質問があれば、いつでも聞いてくださいね!   と、この後、記事冒頭のギフトクレジットの話に戻ったのです。 おわりに   このように、  - 「分からないことは素直に質問する」  - 「やりたいことを具体的に伝える」  - 「エンジニアでないことを表明し、SQL作成のサポートを依頼する」  という流れで、安心してAIとやりとりを進めることができました。   関連話題で、米スタンフォード大学教授でAIスペシャリストのJeremy Utley氏が『AI時代の7つの新習慣』で「AIに質問する際、質問の意図や背景をしっかり伝える 」を主張しています。   これは、AIのみならず、人間のチームメイトの場合は自然と考慮して進めているコミュニケーションではないでしょうか。   記事では、AIをチームメイトとして捉えるべきだとの主張が書かれており、AIにどれくらいの期待値を持ってどう接するとより良いか?を考えるヒントが詰まっている気がしますので合わせてお読みいただけると良いかと思います。 スタンフォード大学の“人気”教授が説く「AI時代の7つの新習慣」 - ZDNET Japan   私もまだ試行錯誤中ではありますが、自分がやりたいこととAIができることをすり合わせながら、AIとともにやりたいことを成し遂げられるようトライしていきたいと思っています。みなさまの、AIとの付き合い方のヒントにしていただけると幸いです。     Harokaのnote 書籍 『UXリサーチの活かし方』 も発売中です   記事執筆者 瀧本 はろか 株式会社スマートバンク UXリサーチ部 部長 校正・校閲担当者を経て、ベンチャー企業で新規事業の立ち上げやインタビューの企画執筆を経験。その経験からUXリサーチャーに転身、人材会社の新規事業のUXリサーチやリサーチ組織立ち上げ、リサーチャー育成に携わる。2022年4月より株式会社スマートバンクに1人目のUXリサーチャーとして入社。N1インタビューの文化を受け継ぎ、年間100件を超えるインタビューを担当。メンバー全員が「Think N1」を身近に感じられるような働きや経営・事業に伴走するリサーチを推進。著作『UXリサーチの活かし方 ユーザーの声を意思決定につなげるためにできること(翔泳社)』。 .balloon5 { width: 100%; margin: 1.5em 0; overflow: hidden; } .balloon5 .faceicon { float: left; margin-right: -90px; width: 80px; } .balloon5 .faceicon img{ width: 100%; height: auto; border: solid 3px #d7ebfe; border-radius: 50%; } .balloon5 .chatting { width: 100%; } .says { display: inline-block; position: relative; margin: 5px 0 0 105px; padding: 17px 13px; border-radius: 12px; background: #d7ebfe; } .says:after { content: ""; display: inline-block; position: absolute; top: 18px; left: -24px; border: 12px solid transparent; border-right: 12px solid #d7ebfe; } .says p { margin: 0; padding: 0; }
アバター
初めに 環境 問題設定 搬送最適化を考える 各問題の設定 定式化 実装 結果 計算時間の平均比較 各問題の各回数ごとの計算時間比較 最後に Appendix(定式化) 記事執筆者 初めに 前回の記事 にて、pythonの無料数理最適化ライブラリである、OR-ToolsとPuLPの性能比較を行ってみました。 前回は変数/制約式ともに数が小さい小規模な比較でした。そこで、 今回は変数/制約式が多く、 より複雑な題材を用いて性能比較を行います。 環境 Python Version 3.12.3 OS Windows 11 使用するライブラリ PuLP version: 2.8.0 OR-Tools version: 9.10.4067 問題設定 搬送最適化を考える 定式化手法 Efficient SAT Approach to Multi-Agent Path Finding Under the Sum of Costs Objective での定式化をアレンジして使用する 具体的には、SAT定式化->0-1整数計画法による定式化にアレンジする 詳しくは 別記事 を参照 前提 2次元のマップと複数のエージェントが存在する 2次元のマップは、下記の要素を持つ 各エージェントが滞留出来るノード 各エージェントが渡ることが出来るエッジ 各エージェントは、それぞれが初期位置と目的位置を持つ 評価方法 各エージェントが初期位置から目的位置へと移動するための道筋を計算させる 上記計算を各10回ずつ解かせ、時間を計測する 各問題の設定 問題1 問題設定 ノードは1,2,3の3つ エージェントはa1のみの1つ エージェントa1はノード1をスタートし、ノード3へ到着する 備考 一番シンプルな問題 合計2ステップで完了する 図1:問題1 問題2 問題設定 ノードは1,2,3,4の4つ エージェントはa1,a2の2つ エージェントa1はノード1をスタートし、ノード3へ到着する エージェントa2はノード4をスタートし、ノード1へ到着する 備考 シンプルだが、落とし穴がある問題 エージェントa2はa1がノード2を通過するのを待つ必要がある 2つのエージェントがお互いに干渉しなければ2ステップで終わるが、干渉を考慮すると4ステップ必要 図2: 問題2 問題3 問題設定 ノードは1~14 エージェントはa1,a2,a3の3つ 備考 先の論文に掲載されている問題 ノード数、エージェント数ともに多いため、問題1, 2と比較すると計算量が多い 図3: 問題3 定式化 以下に概略を記します。 定式化は整数計画法を用いた 元論文ではSATを使用しているが、今回はSATをアレンジした整数計画法を用いる 目的関数として、各エージェントの移動数を採用する(⇔全エージェントの移動総数を最小化する) 定式化の概要 各制約 C0: エージェントは各々、 $t=0$で初期位置、$t=\mu$で目的位置に存在する C1: エージェントがある時間ステップで頂点に存在する場合、次の時間ステップに隣接する辺を通過する必要がある C2: エージェントがある時間ステップ間で辺を渡った時、その辺の両端にある頂点に存在する必要がある C3: エージェントが頂点で待機する時以外, 頂点は空でなければならない C4: 同じ時間ステップ、同じ頂点に2つ以上のエージェントは存在しない 目的関数 全エージェントの移動総数を最小化する 定式化の結果、各問題の変数、制約条件の個数は下表の通り 問題 変数[個] 制約条件数[個] problem1 23 054 problem2 120 418 problem3 972 4848 実装 実装は複雑なので概要のみ記します。ディレクトリ構造は以下の通り route_searcher │ route_searcher.py │ tree.txt │ __init__.py │ ├─constructor │ var_constructor.py │ __init__.py │ ├─formulator │ formulator.py │ __init__.py │ └─problem_solver │ solver.py │ __init__.py │ └─each_processor base_processor.py processor_or_tools.py processor_pulp.py __init__.py 実装について constructor: 変数生成、ソルバ生成 formulator: 定式化処理 problem_solver: 問題を解き、規定の形式で出力する 結果 計算時間の平均比較 計算結果 問題 OR-Toolsの平均計算時間[msec] PuLPの平均計算時間[msec] 計算時間の比 problem1 0.67497 40.44169 59.9倍 problem2 2.6444 57.50438 21.7倍 problem3 22.70905 173.84964 7.7倍 各問題の各回数ごとの計算時間比較 OR-Toolsの計算結果 PuLPの計算結果 上記の結果より、OR-ToolsとPuLPの性能比較をした結果、これら問題においては、OR-Toolsの方が計算処理時間が短く、優れていることがわかりました。 最後に 個人的には、小規模な問題に対しては, PuLPもOR-Toolsも, コーディングレベルで最適化がなされていると思っていました。一方, OR-Toolsの方は”大規模で複雑な問題向け"のコーディングを強いてくる為, 大規模な問題で差がつくと考えていました。 しかし、実際はPuLPもOR-Toolsを比較したとき、小規模な問題の方が差が大きいことが分かりました。 やはり、 個人的にはOR-Toolsの方が好きですね。 Appendix(定式化) 定式化の数式を書きます。 定式化 記事執筆者 小澤 陽介 パーソルキャリア株式会社 テクノロジー本部 デジタルテクノロジー統括部 デジタルビジネス部 アナリティクスグループ 前々職は半導体製造会社にて、リソグラフィ工程のプロセスエンジニアリングに従事。前職では機械系メーカーR&Dにて数理最適化のアプリケーション開発及び工程可視化に従事。パーソルキャリアに入ってからはアナリストとして、主に生成系AIに関連するアプリケーション開発や検証に従事。意思決定の為の計算機開発にも携わっている。
アバター
搬送最適化とは 概要 実例 論文の内容 概要 数式の詳細 定式化 まとめ 記事執筆者 搬送最適化とは 概要 搬送最適化の前に、物流最適化について解説する。物流最適化とは、物流業務全体におけるコスト削減、効率向上、労働環境改善などを目的とした取り組みである。 物流最適化には、以下のような具体的な方法がある。 トラックの最適な配送ルートの計画 データ活用で在庫管理の効率化 自動倉庫システムによる作業効率の向上 上記の中でも、特に自動倉庫システムなど、倉庫内における貨物搬送を最適化する必要が出てくる。これを搬送最適化と呼ぶ。 実例 今回は実例として、株式会社Mujinの 記事 を取り上げる。 (動画リンクも含めて非常にイメージが湧きやすい。) グリッド式AGV(記事から引用) 上の図にあるとおり、複数の台車(AGV, Auto Guided Vehicle)が荷物を運ぶ為に通路を走っている。 この際, 台車同士が衝突しないように制御を行わなければならない。 この制御が上手く機能しない場合は渋滞が発生し、荷物の到着が大幅に遅れる可能性がある。 上記の制御は非常に難易度が高く、マルチエージェント経路探索(MAPF)という名前で、現在でも盛んに研究されている。 今回紹介する論文は、数理最適化を用いて台車が走行するルートを計算することが目的である。 論文の内容 概要 論文の情報 題名 Efficient SAT Approach to Multi-Agent Path Finding Under the Sum of Costs Objective 著者 Pavel Surynek Ariel Felner Roni Stern Eli Boyarski 発行年度 2016年 発表された場所 2016 European Conference on Artificial Intelligence 出版 IOS Press まずは論文の概略(日本語訳)を提示する マルチエージェント経路探索(MAPF)のタスクは複数のエージェントの競合しない経路を見つけることである。 本論文では, MAPFのコスト和変形のための最初のSATソルバを提案する。 コスト和の下界とメイクスパンの上界を用いることで、妥当な数の変数を持つことができる。 次に探索ベースのソルバであるICTSにならい、実験評価を行った。 その結果、多くのシナリオで新しいSATベースの手法が、従来のコスト和探索型ソルバーであるICTSとICBSアルゴリズムを上回ることが分かった。 前章でも言及したが、MAPFは現在でも盛んに研究されている。 今回の手法では、MAPFをSATの問題に変換し、ソルバにより厳密解を求める。 ※SATとは、 与えられた命題論理式を真にする値割り当てが存在するか否かを判定する問題である。 数式の詳細 本論文では、MAPFをグラフ理論の言葉に置き換えている。問題の定義は以下の通りである。 続いて、問題を解く方針について述べる。 まずはMAPFが持つグラフ情報から、各エージェントごとに時間発展グラフ(Time Expansion Graph, TEG)を生成する。 例として、シンプルなMAPFからのTEG生成を挙げる。 時間発展グラフの例 上図では、3つの頂点を1つのエージェントが渡る状況を描いている。 エージェントa0は頂点u1からスタートし、頂点u3を目指す。 時間ステップμは本来であれば3で充分であるが説明の為にμを4にしている。 MAPFを数学に落とし込む為、グラフが持つ各オブジェクトに対して変数を設定する 続いて, Booleanと整数を対応付けた求解用の変数を定義する 以降は、大文字のBoolean変数を小文字の0-1変数に書き換えながら定式化を追う。 定式化 制約式C0 制約式C1 制約式C2 制約式C3 制約式C4 制約式C5&C6->目的関数 定式化をまとめると, 以下のようになる. まとめ 以上の形で、グリッド上の経路探索を整数最適化で置き換えることが出来た。ただし, 以上の定式化は変数量/計算量が多く、実用的とは言い難い状況である。 今後はコンピュータの計算速度を上げることも重要だが、アルゴリズムの高速化を図ることも重要である。 記事執筆者 小澤 陽介 パーソルキャリア株式会社 テクノロジー本部 デジタルテクノロジー統括部 デジタルビジネス部 アナリティクスグループ 前々職は半導体製造会社にて、リソグラフィ工程のプロセスエンジニアリングに従事。前職では機械系メーカーR&Dにて数理最適化のアプリケーション開発及び工程可視化に従事。パーソルキャリアに入ってからはアナリストとして、主に生成系AIに関連するアプリケーション開発や検証に従事。意思決定の為の計算機開発にも携わっている。
アバター
はじめに こんにちは。株式会社タイミーでバックエンドエンジニアをしている廣江です。以前、TECH Streetさん主催の「 Ruby勉強会~各社の取り組みや課題から学ぶ会~ 」というイベントでRBSを手書きで利用しているという話をさせていただきましたが、今回はその後についてのお話を書かせていただこうと思います。 ここでの「手書きRBS」とは言葉の通り、手書きされたRBSを指しています。反対に手書きではないRBSとは、TypeProfやSordなどのRBS生成ツールを利用し、ソースコードやコメントから自動生成されたRBSを指しています。 はじめに RBSについて タイミーでのRBSの活用状況 rbs-inlineを使うかどうか RBSの恩恵 ケアレスミスの減少 インターフェースへの意識 苦労している点 外部ライブラリのRBS不足 RBSのキャッチアップ 動的なメソッド生成やメソッド呼び出し 最後に RBSについて RBS とは、Ruby3.0から追加されたRubyの型情報を記述するための言語です。RBSファイルと型検査機 「Steep」 を用いることにより、Rubyのソースコードの静的な型検査を行うことができるようになります。 タイミーでのRBSの活用状況 タイミーにはRuby on Rails製のアプリケーションがいくつか存在していますが、メインシステムともう一つ、小さめのサブシステムの開発でRBSを活用しています。以前の発表で取り上げたものはこのうち後者のサブシステムでの事例でしたが、メインシステムでも本格的にRBSを活用しており、直近ではドキュメンテーションツールのYARDからrbs-inlineに完全移行しています。この経緯については 弊社テックブログ にて掲載されています。 今回は、引き続きサブシステムにおける手書きRBSのその後について書かせていただこうと思います。前回のイベントでの発表内容については後ほどレポートが上がるようですので、そちらをご参照ください。 rbs-inlineを使うかどうか rbs-inlineは実装のすぐ近くにコメントで型情報が書け、そこからRBSファイルを生成してくれる便利なツールです。将来的にRBS本体に取り込まれる想定で開発が進んでおり、私たちもrbs-inlineを採用すること自体には肯定的でしたが、現状以下の理由から採用していません。 1.手動でRBSファイルを生成し忘れた場合、コミット時にrbs-inlineからRBSファイルを自動生成するような仕組みが現状用意できていない 2.自動生成の整備にかかるコストが現状の規模と効果に対して見合わないと判断した 3.RBSのGit管理をやめ、型検査時に都度生成するスタイルであれば上記の仕組みは不要だが、この方法だと現在感じているメリット(後述)をいくつか失ってしまう 4.開発者はエディタにVS Codeを利用しているため、rbs-inlineを導入せずとも拡張機能によって実装のすぐ近くに型情報が表示される 5.RBSをわざわざ別ファイルに書くことに対する面倒臭さが慣れによって薄くなっている ちなみに、私たちは「今後もrbs-inlineを使わない」という決定をしたわけではなく、使いたくなった場合は遠慮なく使っても良いということにしています。rbs-inlineのRBS生成はコメントが一切書かれていない実装であっても、型を untyped としてRBSの雛形を生成してくれるため、RBSファイルの生成サポートツールとしては非常に優秀なんじゃないかなと考えています。 ちなみにRBSファイルをGit管理するメリットには、Pullする度にRBSファイルを生成し直す手間が必要ないことや、後述するRBSファイルをレビューすることに価値があることが挙げられています。 RBSの恩恵 ケアレスミスの減少 RBSを導入したことで、エラーの発生率は格段に抑えられていると感じています。少なくともサブシステムの初回リリース以降 NoMethodError のようなケアレスミスによるバグは発生していません。 例えば、以下のようコードを書いた時、 + の部分で型エラーが起きます。 array = [1, 2, 3] array.first! + 1 Type `(::Integer | nil)` does not have method `+`(Ruby::NoMethod) これは Array#first が nil を返す可能性を含んでいるからです。今回の例では直前に array = [1, 2, 3] であることが分かっているため、 array.first が 1 であることは明らかですが、型チェックをパスするためには以下のように記述を変更する必要があります。 array = [1, 2, 3] array.fetch(0) + 1 些細な変化ですが、 array.fetch(0) が nil である可能性がなくなり、 array に要素が必ず1つ以上求められることがソースコードから明確になります。こうした小さな変化が積み重なることにより、バグの起きにくいコードになっているのだと感じています。 さらにVS Codeを利用していると、このような型エラーをリアルタイムでフィードバックしてもらえます。これによりテストを実行せずとも記述の誤りに気づくことができるため、開発速度にも貢献できています。 また引数の間違いやtypoなども同様にリアルタイムのフィードバックで気づけるため、普段の開発においてかなりの恩恵を受けていると言えます。 インターフェースへの意識 RBSではメソッドの型を書くとき中にロジックを書かないため、引数や返り値の型に集中できます。これを利用し、Pull Requestではインターフェースに集中したレビューが可能になっています。Rubyにはインターフェースを表現する機能はありませんが、それに近いことをするテクニックは存在します。 このインターフェースに集中したレビューが行えることにより、共通のインターフェースを見つけ、適切なクラス設計を促せる場合があります。また、引数に明らかに場違いなオブジェクトを受け取っていたり、返り値に全く関連のないオブジェクトが返っている場合にもレビューで気づきやすく、そういった場合に設計を見直すきっかけとなります。 苦労している点 恩恵については前回の発表時から感じていたものが引き続き感じられているという状態なのですが、苦労している点は新たに少し増えています。 外部ライブラリのRBS不足 まずは、前回の発表時にもあった外部ライブラリのRBS不足です。こちらは gem_rbs_collection にPRを出すことでなんとか解消しつつ進めていますが、バージョン更新頻度の高いRailsの新機能などの型情報が必要になった場合などは頭を抱えることがあります。 これについての対応としては、前回の発表時には外部ライブラリの型定義が不足していた場合はgem_rbs_collectionにPRを出すことを推奨していたのですが、そこそこ頻度が高いことと、それよりも優先すべきタスクがあるというケースが多くなったため、必ずしも実施していません。代わりに現在ではsig/gems/というディレクトリを掘り、ひとまずはそこに型検査を突破できる程度の必要最小限の型定義を書くという方法をとっています。手が空いた際にそれらを整理してgem_rbs_collectionにPRを出しています。 RBSのキャッチアップ 次に、新しいメンバーのRBSのキャッチアップについてです。当然RBSをガッツリ書いてきました!という人は稀だと思うので、RBSのキャッチアップはほぼ必ず発生すると考えています。このキャッチアップを考えた時に、rbs-inlineを使っていればYARDライクな記法もあるため誤魔化しが利くんですが、RBSをちゃんと書くとなるとキャッチアップもちゃんと行なってもらう必要があります。また書き方がわからずうまくいかないといった場合にきちんとフォローできる体制を取る必要があり、それがないといまくいかないからと steep:ignore 等で型検査を無効にされてしまうとRBSの恩恵を受けられなくなってしまいます。 こちらについては現状、RBSのSyntaxが確認できるドキュメントへのリンクをREADMEに用意するにとどまっており、それ以上の対応策が十分に打てていない状態です。用意したドキュメントはあまり参照されておらず、これを改善するために周知を行うであったり、勉強会やワークショップの開催などを検討しています。キャッチアップも最初から全て完璧に行うのは難しいと考えているため、既存のRBSを参考にしつつできるところまでやってもらい、どうしても難しいようであれば気軽に聞いてもらえるような環境をちゃんと整備していくことが重要かなと考えています。 動的なメソッド生成やメソッド呼び出し 最後に動的なメソッド生成やメソッド呼び出しでは恩恵を受けづらいという点があります。特にmethod_missingを利用した実装には全くの無力です。これについてはRBSの恩恵を受けるために動的なメソッド生成や呼び出しのような実装は避けるという、RBSフレンドリーな実装を心がけることでなんとか回避していますが、Rubyの武器の1つを失ったような気持ちになっています。もちろんこういった実装は多用すべきではないので、避けるという方針自体は間違っていないとは思いつつ、完全に封印されてしまうとそれはそれで不便だなと感じています。 これについての対応としては、上で書いた通り、基本的にはRBSフレンドリーな実装を心がけるという方針によって回避していきます。その上でどうしても動的な実装が必要になった場合の策として、steepの アノテーションコメント を書くという方法でなんとかしていこうと思っています。これはソースコード上にコメントで変数やメソッドなどの型を記述できるというもので、型検査を無効にするよりは遥かに良い状態を維持できるだろうと睨んでいます。 最後に 苦労している点はいくつかあるものの、現状圧倒的にメリットが多いと考えているRBSですが、すでに成熟仕切ったRailsアプリケーションに導入するのは大変かと思います。それを実施しているのが弊社テックブログで掲載されている「 YARD から rbs-inline に移行しました 」という記事になっており、こちらの内容も参考にしていただけると良いかと思います。この記事を通じて少しでもRubyでの静的型検査に興味を持っていただけたり、何かの助けとなれば幸いです。 記事執筆者 廣江 亮佑 株式会社タイミー 2023年6月に株式会社タイミーに入社。岡山からフルリモートで勤務しています。SpotworkSystemTribeに所属し、現在はサービス内でのお金の動きに関わるドメインを主に担当しています。最近IDEをRubyMineからVS Codeに乗り換えました。SNSではだいたいrhiroeという名前で活動しています。
アバター
はじめに こんにちは、オシロ株式会社でリードエンジニアとして働いているにっく( webuilder240 )と申します。オシロでは自社プロダクトとしてコミュニティ専用オウンドプラットフォーム「OSIRO」を提供していますが、私は2015年の開発開始から一人目エンジニアとして携わり、技術選定の意思決定を行ってきました。 今回は、そのなかでもRuby on Railsを選択した理由、 その学習に役立つOSSアプリケーションについて紹介したいと思います。この記事を読むことで、Railsの選定理由や実践的な学習方法について理解を深めていただければと思います。 はじめに Ruby on Railsの選択理由 開発に必要な便利機能がはじめからそろっていた 可読性・コードの美しさ 選び続けている理由 RailsのコードリーディングにおすすめなOSS Mastodon forem Writebook 最後に Ruby on Railsの選択理由 先日、 TECH Street主催のRuby勉強会 に登壇させていただいたときに、 『Ruby, Ruby on Railsを選んだ理由は何ですか?』という質問を受けました。 他の登壇者の方々は、既にRailsを採用した後に引き継がれた立場の方だったので、開発開始の技術選定で明確にRuby on Railsを選ぶ意思決定をしたのは私だけでした。 そんなこともあり、今回は選定理由について詳しくお話ししてみたいと思います。 開発に必要な便利機能がはじめからそろっていた Ruby on Railsを利用し始めたのはコミュニティ専用オウンドプラットフォーム「OSIRO」の開発を始めたときからなので、今年で9年目を迎えます。 その時にはすでに現在Railsで広く使われている機能やライブラリはリリースされていて、Webアプリケーション開発で必要なものは多く揃っていました。 当時、私が使っていたCakePHP2ではサポートされていなかった、DBマイグレーション機能やRails Consoleも当たり前に使えるのは魅力的でした。 外部ライブラリも当時から豊富で、今でもテストツールとして広く利用されるRSpecやActiveJobのバックエンドとしてよく使われるSidekiq、ページネーションライブラリであるKaminariと、Railsだけでできなくても定番ライブラリが揃っていました。 当時CakePHPには定番ライブラリが少なく、ひとりでプロダクト開発をしていた私にとっては、定番ライブラリや機能を使って開発を効率的に進めることができるのは大変魅力的だったのです。 可読性・コードの美しさ Railsの機能だけでなく、その可読性やコードの美しさも選定の大きな理由の一つでした。特に、CakePHP2と比較したときのコードのシンプルさは、私にとって大きな魅力でした。以下に具体的なCakePHP2のコードと比較した例を示します。 CakePHPでは以下のようなコードで投稿の編集を行います。このコードは条件チェックやフラッシュメッセージの設定が冗長でした。 public function edit($id = null) { if (!$this->Post->exists($id)) { throw new NotFoundException(__( 'Invalid post' )); } if ($this->request->is(array( 'post' , 'put' ))) { if ($this->Post->save($this->request->data)) { $this->Session->setFlash(__( 'The post has been saved.' )); return $this->redirect(array( 'action' => 'index' )); } else { $this->Session->setFlash(__( 'The post could not be saved. Please, try again.' )); } } else { $options = array( 'conditions' => array( 'Post.' . $this->Post->primaryKey => $id)); $this->request->data = $this->Post->find( 'first' , $options); } } 一方、Railsでは以下のようにシンプルに記述できます。 Railsのコードは文字量が少なく可読性が高いことが特徴です。 def edit @post = Post.find(params[:id]) end def update @post = Post.find(params[:id]) if @post.update(post_params) redirect posts_path, notice: "The post has been saved." else flash[:now] = "The post could not be saved. Please, try again." render :edit end end これは少し極端な例かもしれませんが、RailsのコードはCakePHPよりもシンプルで読みやすく、記述量が少なく保守性が高いです。当時の私はそこに感銘を受けたのと、CakePHP2がCakePHP3へのバージョンアップにより、大幅に構文が変わってしまうことがあって、CakePHPからの乗り換えを決めました。 選び続けている理由 昨今はTypeScript、GoやKotlinといった静的型付け言語がモダンな技術とされていますが、 私は依然としてRailsを選び続けています。長年の経験と愛着があるのもありますが、多くのライブラリは今でもサポートされていて、安定的に利用できるためです。DeviseやKaminari、Sidekiqなども安定して利用できています。安定したフレームワークとライブラリを使うことは、競争が激しいスタートアップにおいて、スピード感をもってプロダクト開発をする上でも非常に重要な要素です。 このように、Railsは長年利用されてきた便利なライブラリがサポートされている点や安定感の高さがあるため、オシロでは今後もRailsでWebアプリケーション開発を続けるつもりです。 RailsのコードリーディングにおすすめなOSS Mastodon 当時のTwitter(現: X)の代替サービスとして、一時的に流行していた分散型ソーシャルネットワークサービスのマストドンです。 mastdon GitHubリポジトリ これも実はRailsでできています。 簡単な構成を説明すると、バックエンド処理のほとんどをRailsでのWebAPIで実装していて、フロントエンドやView部分はReactで実装されています。 Railsの内部にReactでのSPAがあるという技術構成で、こういった構成は他の案件でも参考になるかもしれません。 その際、RailsとReactがどのようにして共存しているか?などの観点から見るとヒントになる点が多く、 バックエンド部分については良い意味で「普通のRailsアプリケーション」という感じで、すごくマジカルなコードも少なく、読みやすいと思っています。 forem 登壇でも紹介させていただいた、DataUpdateScriptのトピックで紹介した技術コミュティOSSで、 dev.toがこのForemで稼働しています。 QiitaやZennに近いようなサービスで、 私が近年特にRailsアプリケーションとして注目し、参考にしているOSSです。 dev.to GitHubリポジトリ 特徴としては先述のMastdonとは異なり、RailsがAPIに徹しているわけではなく、部分的にRailsでSSR(サーバーサイドレンダリング)もしています。 これはisland Architectureを採用してPreactとRailsでのSSRを組み合わせて実装されています。 foremのisland Architectureについては、 こちらの記事で詳しく解説しているので、良ければこちらもご覧ください。 Ruby on RailsのMPAでForem流の"Islands Architecture"を導入するメリット・デメリット バックエンドの処理も比較的ごく普通なRailsアプリケーションで、読みやすいかと思います。 他にもパフォーマンス改善に対する工夫が随所で行われているプロダクトです。 Writebook 現在Railsの作成者であるDHH(David Heinemeier Hansson)がCTOとして在籍している37Signalsが、積極的に進めているプロジェクトとして once というものがあります。 これは買い切りのアプリケーションをユーザーが購入し、サーバーにユーザーがインストールして利用するような仕組みが特徴です。 購入したアプリケーションのコードは読むことができ、実際にRuby on Railsの教材としても購入が推奨されていました。私もCampfireというチャットアプリケーションを購入してコードリーディングをしていました。 https://once.com/campfire その中に Writebook というWeb上で本の執筆ができるというアプリケーションがあります。 このプロダクトは無料で、Railsのメンテナーが数多く在籍する37signalsのRailsのプロダクションコードを読むことができます。 ただ残念なことに現時点でエディタのマルチバイト文字の対応がうまくできておらず、日本語の入力が満足にできない状況です。 それでも37signalsのプロダクトのコードはとても洗練されていて、内部品質がとても高く、Railsのお手本のようなコードを見れるのはやはり魅力的です。 特にHotwireをフロントエンドで利用しているOSSアプリケーションのコードはあまりないので、Hotwireを活用しているエンジニアにとっては「答え合わせ」ができるので、必読かと思っています。 最後に Ruby on Railsは今でも現役のWebアプリケーションフレームワークで、多くの開発者にとって強力なツールです。この記事を通じてRailsの選定理由や学習に役立つOSSアプリケーションについて少しでも理解を深めていただけたなら幸いです。 記事執筆者 西尾 拓也(にっく) オシロ株式会社 プロダクト開発部 リードエンジニア OSIROのプロダクトを作る1人目のエンジニアとして創業前からジョイン。OSIROのシステムの土台を担当しRuby on Railsで開発。 会社の拡大に伴い、別々だったシステムを1つのプラットフォームに統合するマルチテナント化など、様々な技術課題の解決や機能改善を行う。 2019年からリードエンジニアとして現在は6名のエンジニアを統括しており、自分もチームメンバーもエンジニアリングを楽しめるように日々工夫している。
アバター
はじめに 強化学習とは強化学習は、AIが処理した結果に対して「報酬(スコア)」を与え、報酬が最大になる処理方法を学習させる手法です。 引用元:強化学習とは?機械学習との違いなどの用語解説や活用事例などをご紹介 強化学習のゲームでの成功例としてDeepMindによって開発されたAlphaZeroがあげられます。AlphaZeroは人間の事前知識を必要とせず、自分自身と対局する自己対局のみで強くなりチェス、将棋、囲碁でそれぞれの世界チャンピオンプログラムに互角以上のレベルに達しました。AlphaZeroの学習方法は異なるゲームでも同じ学習方法を用いて強くなることができたという点が画期的でした。 今回はAlphaZeroの手法以外の強化学習法である elmo式学習 root learning descent の3つで実験し、どの程度正解の手を選べるのか調べてみました。 はじめに elmo式学習 root learningとdescent 実験 ○×ゲーム モデルの性能評価方法 モデルの出力をラベルに変換するルール 実験時の条件 各学習手法で共通条件 root learning elmo式学習 descent 実験結果 考察 まとめ elmo式学習 elmo式学習は瀧澤 誠氏により提案された強化学習の手法です。人間の知識を必要としない学習手法でelmoは第27回世界コンピュータ将棋選手権で優勝しました。 ほかの将棋AIソフトも同じ学習手法を用いることで強いプログラムが作成できることが確認できたことから有用性が証明されました。 詳しい手法は こちら:elmoがもたらしたオーパーツについて のブログを参照ください。 root learningとdescent descentはQuentin Cohen-Solalにより提案された強化学習の手法です。descentもAlphaZeroの学習手法と同様に、人間の知識を必要とせず、特定のゲームに依らない学習手法です。詳しい手法は こちら の論文を参照ください。こちらの論文では tree learning root learning desent について実験しています。 elmo式学習はroot learningと似た手法です。elmo式学習とroot learningの違いは、学習時の目的関数に深い探索結果だけでなく、自己対局の結果も考慮する点にあります。elmo式学習のほうが性能が良いことが将棋では示されています。 またこの論文では9つのゲームに対してそれぞれの学習手法で学習した結果を比較し、descentが最も強いモデルができることを確認しました。 実験 3種類の学習手法の有効性を試すため○×ゲームで実験を行いました。このゲームを選んだ理由はゲームの状態数が少なく、各局面での勝ち、負け、引き分けが容易に分かるためです。 実験に使用したソースは こちら で共有しています。 ○×ゲーム ○×ゲームとは3x3のマスを用意して、順番に○、×を書き込んでいくゲームです。○×のどちらかが縦横斜めのいずれかに3つ並べることができるか、またはすべてのマスに○×を書き込んでしまったらゲーム終了となります。縦横斜めのいずれかに○×を並べることができたら並んだ方の勝ちで、どちらも並べることができず、マスが埋まってしまった場合引き分けとなります。このゲームは双方が最善を尽くすと引き分けになることが知られています。 ゲームの詳細は こちら を参照ください。 ○×ゲームの状態数は5478通りあることが確認されています。 (参考) 今回作成したプログラムでも同様の検証を行い、結果が一致したので正しいそうだと考えられます。 図1:初期局面 図2:お互いに1回ずつ○×を書き込んだ後 図3:終了局面 モデルの性能評価方法 学習によってどの程度モデルの性能が上がっているのかを調べるため、モデルの更新が終わった各タイミングでテストデータによる評価を行いました。 テストデータはゲームで存在しうる全局面(5478局面)に対してモデルの評価を行い、その値に対して次に説明するルールにしたがいラベルに変換しました。テストデータのラベルとモデルが出力した結果を変換したラベルが一致しているかどうかの正解率にて評価を行いました。 モデルの出力をラベルに変換するルール [ -1.0, -0.2 ):負け [ -0.2, 0.2 ]:引き分け ( 0.2, 1.0 ]:勝ち 実験時の条件 学習時の条件は下記表のとおりです。 各学習手法で共通条件 最適化手法:AdamW 学習率:1e-4 エポック:30 ミニバッチサイズ:128 自己対局の際の着手の選択方法:ε-greedy法(ε=0.2) 自己対局→モデル更新の回数:32回 自己対局時の1着手を選択するためのシミュレーションの回数:25 root learning 自己対局の回数:1000 elmo式学習 自己対局の回数:1000 lambda:0.7 descent 自己対局の回数:10 実験結果 モデルの性能をプロットした結果は下記のとおりです。いずれの学習手法も○×ゲームは難易度が低くすぎたようで、1回目の学習が終わった時点で正解率が80%を超えており、その後学習を継続しても正解率はあまり変化しませんでした。 elmo式学習とroot learningはほぼ同じ正解率でしたが、若干elmo式学習のほうが正解率が高いことが確認できました。 descentは正解率が最大で98%となり他の学習手法と比較すると性能が良いことがわかりました。 図4:学習結果 考察 root learningとelmo式学習の正解率が80%前後にとどまり、descentのように90%以上の正解率に達しなかった原因について考察しました。 elmo式学習で判断を間違えてしまった局面の一例を確認してみます。 図5:間違いの例1 図6:間違いの例2 model_outputがモデルが出力したスコアでlabelが前述の条件に従ってラベル化した値、正解の値がanswerとなります。 いずれの例も勝ちまたは負けが簡単にわかる局面で引き分けと判定してしまっています。 原因として、自己対局時の棋譜に偏りが生じてしまったのが原因だと考えられます。自己対局は学習中のモデルを用いて棋譜を生成していきます。なので学習中のモデルは学習が進むに連れ、勝ち負けがはっきりわかった局面(=難易度低の局面)は学習が進むに連れ選ばれないようになっていきます。図7で示したとおり学習1回目では様々な難易度の局面が現れますが、学習2回目、3回目と進むごとに自己対局の棋譜の内容が難易度が高い局面が増えていくことが予想されます。 図7:自己対局時の棋譜の推移 一方モデルの更新は自己対局の棋譜を元に行われるため、モデルの更新に使用する局面が難易度高、中に偏っていくことになります。図7で示したように学習1回目では様々な難易度の局面を学習対象としていましたが、学習2、3回目と学習が進むにしたがって難易度高、中に偏っていくことが予想されます。そのためモデル更新を実施していく中で難易度低の局面の結果が変わってしまったことが原因だと考えられます。 まとめ 今回は以下の3種類の強化学習を実装し、○×ゲームで学習させ、どの学習手法が最も性能の良いモデルを作成できるか検証しました。 検証した結果、descentが○×ゲームの全局面に対する98%の局面で正解することができました。 またroot learning、elmo式学習がdescentよりも性能が悪かった原因について考察し、自己対局時の局面の偏りが原因ではないかと考察しました。 次回はどうぶつしょうぎや6x6のオセロなどもう少し難しいゲームで実験してどのような結果になるか試してみたいと思います。 記事執筆者 宇賀神 拓也 パーソルキャリア株式会社 テクノロジー本部 デジタルテクノロジー統括部 デジタルソリューション部 フロントアプリエンジニアグループ 前職ではベンチャー企業で音声を使ったAIのサービス開発を担当し、フロントエンド、バックエンドを広く浅く経験。パーソルキャリア入社後はデータエンジニアとして、AIを用いたアプリケーション開発に携わっている。
アバター
前書き デジタルテクノロジー統括部アナリストの安藤です。 最近マーケティングに関するプロジェクトに携わることになり、Marketing Mix Modeling(以下MMM)の門を叩くことになりました。 MMMでは、広告メディアがどれだけKPIに貢献したのかを回帰モデルによって推定し、それを元に予算の最適化を行います。 変数の値をそのまま使うのではなく、全体的なトレンドや季節性、繰越効果や飽和効果といった要因を扱っていることが特徴です。 MMMの代表的なOSSとしてGoogleの LightweightMMM やMetaの Robyn などが公表されていますが、2024年3月にGoogleから新たなMMMパッケージとして Meridian が発表されました。Meridianは現在アーリーアクセス中であり、私もまだ使ったことはないのですが、一般公開とともにLightweightMMMの代替になる予定である、とのことです。 私のプロジェクトでもPython環境での使用の容易さからLightweightMMMを使用しており、Meridianへの移行を見据えて本記事ではLightweightMMMと比べた時のMeridianの新機能について掘り下げていきます。 すでに日本語でのMeridianの解説記事として データ分析な日々 様の記事があるため、二番煎じにはなってしまいますが、可能な限りこちらで書かれていない部分について書いていきます。 data-everyday.com 全2回を予定しており、前半にあたる今回は時変ベースライン、Reach and Frequency データの取り扱い、ROIに事前分布への組み込みを取り扱います。 さらにMeridianには因果推論の考えが取り入れられており、こちらは次回触れる予定です(1-2ヶ月くらいで出せればいいな......)。 前書き モデルの概要 Reach and Frequency データの取り扱い 時変ベースライン ROIに関するドメイン知識や実験結果を事前分布に組み込む 感想 モデルの概要 全てのデータは事前にスケーリングされていることを前提としています。 各変数が指し示すものは以下の通りです。 - :地理単位のインデックス - :時間単位のインデックス - :制御変数(メディア変数以外の変数)のインデックス - :メディア変数(Reach and Frequencyデータ**でないもの**)のインデックス - :メディア変数(reach and frequencyデータ**であるもの**)のインデックス - :時変ベースライン。詳細は後述 - :地理単位ごとのベースライン。地理単位を考慮しない場合は - :制御変数に対する係数 - :制御変数の値 - :メディア変数(Reach and Frequencyデータ**でないもの**)に対する係数 - :メディア変数(Reach and Frequencyデータ**であるもの**)に対する係数 - :変数ベクトル を表す。 は最大何期前までを考慮するかを表す整数パラメータ - : と を組み合わせたもの。   `hill_before_adstock = False`(デフォルト)のとき   `hill_before_adstock = True`(デフォルト)のとき - :投資効果の幾何的減衰を表す関数。     ただし - :投資効果の飽和を表す関数。     ただし であり、   また、 であり、 の時凹型の、 の時S字の形状をとる - は地域 ・時間 ・チャンネル におけるスケーリングされたReach数 - は地域 ・時間 ・チャンネル における平均Frequency数 - :正規分布に従う誤差項 一般的なMMMのモデル構造に馴染んだ人であれば、Reach and Frequencyデータに関する部分が追加されていること、トレンドや季節性の部分が時変ベースラインとしてまとめられていることなどが特徴的だと感じるかもしれません。 以降、これらについてさらに詳しく見ていきます。 Reach and Frequency データの取り扱い 通常のMMMでは、インプレッションに関する変数はインプレッションの総数のみを扱い、広告に接触したユーザーのユニーク数については扱いません。 Meridianでは、インプレッションを - Reach(各期間における広告に接触したユニークユーザー数) - Frequency(各期間におけるインプレッションの総数をリーチ数で割ったもの。平均接触回数) に分けて扱うことが出来ます。 もう少し数式を詳しく見ていきましょう。 ある地域 、チャンネル におけるメディア広告の効果は次のように表されます。 最初に、ヒル関数をFrequency数 に適用しています。ヒル関数ではS字型の飽和効果を表現可能です。広告への平均接触回数には特定の適切な頻度があり、想起の強化のために一定以上の接触が必要であるものの、多すぎても意味のない接触になったりかえって広告疲れを起こしたりするおそれがあるという想定です。 一方、Frequency数を固定したもとでは広告の効果とReach数は概ね線形の関係にあると想定し、 にアドストック関数を適用して広告の効果を推定します。実際にはReachに関しても飽和効果が見られmROIが減少する可能性があるため、観測されたデータにおけるReach数の範囲外に外挿しないことが推奨されています。 Reach and Frequencyデータを用いて予算最適化を行うケースでは、Frequencyを過去の平均に固定するオプションとFrequencyを最適化しつつ予算の最適化を行うオプションが存在します。 Optimization for media channels with reach and frequency data のページにある通り、最適なFrequencyの推定は予算の割り当てに依存しないため、最適なFrequencyを推定した後その結果を組み込んで予算の配分を行うことが可能です。 時変ベースライン LightweightMMMでは、時間によって変動する部分は - トレンド項 - 季節項 の和で表していました(週次データの場合)。 一方、Meridianでは`knot_values`と呼ばれるパラメータに従って時変ベースライン を決定します。`knot`とは日本語で「結び目」を意味する単語です。 以下は Setting Knots のページに従って解説していきます。 ドキュメントの記述と平仄を合わせるため、少しわかりにくいですが時変ベースラインパラメータのベクトルを とします。 は です。 ここで という形で を表現します。重み行列 は 行列、`knot_values`のベクトル (と呼ばれます)は です。 ベイズ推定の対象になるのは です。 ここは自分の解釈が入ってきてしまうのですが、`knot`は`knot_values`とは別に位置パラメータを持っていると考えるとわかりやすいです。 ドキュメントにはありませんが、議論をわかりやすくするため、`knot` の位置を と書くこととします。 各`knot`の位置は を 等分するように決定されます。 、 であるとすると、 となります。 は と とが定まった時点で各 と とのL1距離によって自動的に決定されます。 ごとに 以下の最大の とのL1距離、そして 以上の最小の とのL1距離を取り、その距離に基づいて と の加重平均を取るようなイメージです。 例えば のような`knot`があり、 に興味がある場合のベースラインは と表現されます。 `knot`の数はバイアスとバリアンスのトレードオフを見ながら決定するべきである、と述べられています。`knot`の数が増えれば時間効果の推定値のバイアスが減少する一方でバリアンスは増加するという関係にあります。 これを踏まえた上で、 - 地理影響を考慮したモデルでは`knot`の数を`n_times`と一致させたところからスタートし、明らかなオーバーフィッティングや非現実的なメディア効果の推定が見られなくなるまで少しずつ`knot`の数を減らしていく - 地理影響を考慮しないモデルでは`knot`の数を1からスタートし、明らかなオーバーフィッティングや非現実的なメディア効果が見られるようになるまで少しずつ`knot`の数を増やしていく - `knot`の数を微増・微減させても(例:`knots=10`と`knots=11`)同様の結果が返ってくる可能性が高いため、`knot`の数は幅を持って試すことが有用である また、`knot`の位置は必ずしも一定ではなく、ユーザーが自由に設定することが可能であり、例えば例えば祝日やスポーツイベントなどの影響を組み込むことが出来るようです。 ROIに関するドメイン知識や実験結果を事前分布に組み込む すでに経験的な知識や特定のマーケティングに関する実験により、あるチャンネルの(大まかな)ROIがわかっている場合があります。 これまでのMMMツールではROIそのものに事前分布を設定することは出来ず、推定したROIとドメイン知識や実験によるROIが一致するように各種のパラメータや係数の事前分布を試行錯誤する必要がありました。 MeridianではROIを再パラメータ化すること、すなわちメディア効果の係数 をROIの関数として表すことでROIに事前分布を設定し、設計者の信念をモデルに反映させることが容易になりました。 以下、 ROI priors for calibration のページに従って、 をROIの関数として表すための式変形を追っていきます。 あるチャンネル による売り上げの増分を次のように表現します。 まず、この増分を に関連する部分とそうでない部分に分離します。 増分のうち に関連しない部分を とすると、次のように変形できます。 後の変形のため、 に関する総和記号を外しておきましょう。 `media_effects_dist = Normal`の場合、 を用いて は次のように書くことができます。(`media_effects_dist = Normal`の場合の議論はドキュメントを参照してください) 従って、 を主語にすると となり、 をROIに関する関数として書くことができました。 感想 ReachデータとFrequencyデータの活用について、観測範囲外のReach数の場合にモデルを外挿すべきではないというのは結構大きな制約ですが、それでも適切なFrequenctyを決定するための戦略決定に使えるのは嬉しいです。 また、今までトレンド+季節性で表現していた時変項目がMeridianのように特定の構造を持たないパラメータのベクトルで表現されるようになることでどう変わるのかというのは気になりますね。モデルの構造が複雑になればなるほど、解釈をするアナリストの数理的知識が試されるようになりそうです。 さらに、データ集めにも苦労することになりそうなのは言うまでもないですね......!Googleとしては Google Marketing Platform を使って欲しいという狙いがあるように見えます。こうしたプラットフォームを使わずにデータを蓄積していくのは大変ではありますが、「理論的にはこういうデータがあるとよい」というのがわかっていればその条件を満たすようにデータを集める仕組みを整えていけばよいわけですから、MMMを知ることでデータ収集やデータモデリング戦略にもプラスになると言えるでしょう。 また、これはLightweightMMMにもRobynにもないのですが、競合のデータを扱うフレームワークがあればさらに嬉しいなと感じます。例えば競合他社のTVCMのGRPや指名検索数などのデータが入手できているケースがあり、当然KPIに大きな影響をもたらしうる項目なのですが、残念ながらこれらをサポートするような機能はなさそうです(もちろんコントロール変数として組み入れることはできるのですが......)。 次回はMeridianがどのような形で因果推論の考え方を取り入れているのかについて解説していきます。 記事執筆者 安藤 有瑠聡 パーソルキャリア株式会社 デジタルテクノロジー統括部 デジタルビジネス部 アナリティクスグループ 2022年パーソルキャリア株式会社に入社。現在はアナリストとしてAzure MLを利用した社内アプリケーションのモニタリング、求人推薦アルゴリズムの開発、因果推論を活かした営業支援のための分析、社内データ基盤プロジェクトの横断調査など、幅広い分野の業務を行っている。
アバター
導入 本記事について 注意事項 実装 全体の流れ 前提条件 On Your Dataからサンプルアプリをデプロイ (参考)サンプルアプリのカスタマイズについて Prompt Flowからフローを推論エンドポイントとしてデプロイ/サンプルアプリの設定変更 Azure AI Studioの評価機能を使用したプロンプトチューニング 回答精度向上に向けた指針について 最後に 導入 本記事について 「Prompt Flow(Azure AI Studio)を使用したRAG入門 第1回 」の続きです。 まだお読みでない方は第1回目の記事を先にご確認いただけますと幸いです。 本記事ではPrompt Flow(Azure AI Studio)を使用したRAGのアプリケーション開発の入門として、Azure AI Studioのサンプルアプリや評価機能等の便利な機能についてご紹介できればと思います。 また、本記事が技術ブログへの初投稿のため、温かい目で見守っていただければ思います。 Azureの公式ドキュメントの敷居を下げることを意識して執筆しましたので、本記事をきっかけにPrompt FlowやAzure AI Studioに少しでも興味を持っていただければ幸いです。 【第1回】 RAGの概要、On Your Data(Azure AI Studio)の概要/設定、Prompt Flow(Azure AI Studio)の概要/設定 等 www.tech-street.jp 【第2回】 Prompt Flow(Azure AI Studio)の推論エンドポイントのデプロイ、サンプルアプリのデプロイ/カスタマイズ、Azure AI Studioの評価機能、回答精度向上に向けた指針 等 注意事項 ①    今回紹介する内容は記事執筆時点(2024/6/7)から更新されている可能性があるため、最新情報は公式ドキュメントのご確認をお願いいたします。 ②    今回紹介する内容は一部プレビュー版の内容を含むため、運用環境では推奨されていない機能も含むことに注意してください。 ③     ネットワークや認証等のセキュリティ設定については必要に応じて追加で対応をお願いいたします。 ④    本記事の内容に関連して発生するいかなる不利益、損害、またはその他の問題につきまして、一切の責任を負いかねます。読者の方々が本記事の情報を用いる際には、自己の責任において実施してください。 実装 全体の流れ ①    サンプルアプリをデプロイ Azure AI Studioのプレイグラウンドからサンプルアプリをデプロイします。 Azure AI Studio プレイグラウンドでエンタープライズ チャット Web アプリをデプロイする - Azure AI Studio | Microsoft Learn ②    Prompt Flowからフローを推論エンドポイントとしてデプロイ/サンプルアプリの設定変更 続いて、作成したフローを推論エンドポイントとしてデプロイします。その後、①にて作成したサンプルアプリの設定を編集し、推論エンドポイントで動作するようにします。 リアルタイム推論を行うためにマネージド オンライン エンドポイントとしてフローをデプロイする - Azure AI Studio | Microsoft Learn ③    Azure AI Studioの評価機能を使用したプロンプトチューニング 作成したフローでプロンプトを2パターン用意し、Azure AI Studioの評価機能を使用してそれぞれの出力結果の比較を行います。 バッチ実行を送信してフローを評価する - Azure AI Studio | Microsoft Learn 前提条件 「Prompt Flow(Azure AI Studio)を使用したRAG入門 第1回 」の各設定が完了していることを前提とします。  On Your Dataからサンプルアプリをデプロイ サンプルアプリのデプロイ手順 ①    [チャット セッション] ウィンドウの上にあるメニューから、[Deploy to a web app]を選択し、サンプルアプリをデプロイする。デプロイが完了すれば、[Launch]からアプリにアクセス可能です。 なお、サンプルアプリの認証設定は、下記公式ドキュメントをご参照ください。 Azure AI Studio プレイグラウンドでエンタープライズ チャット Web アプリをデプロイする - Azure AI Studio | Microsoft Learn   ②    デプロイしたサンプルアプリに[Launch]からアクセスし、チャットを投げてみると以下の回答が得られました。 以上でサンプルアプリのデプロイが完了しました。お疲れ様でした。 (参考)サンプルアプリのカスタマイズについて サンプルアプリは、Azure App Serviceの環境変数から一部カスタマイズ可能です。特にメッセージのフィードバック機能は、ユーザーからのフィードバックを簡易に回収することができるため、PoCの段階では本機能を有効にしたうえでチャットの履歴機能※と組み合わせてユーザーの質問や回答の良し悪しの情報を収集することも可能です。 詳細は下記公式ドキュメント若しくはサンプルアプリのGitHubリポジトリのREADMEを参照願してください。 公式ドキュメント Azure OpenAI Web アプリの使用 - Azure OpenAI | Microsoft Learn サンプルアプリのGitHubリポジトリ GitHub - microsoft/sample-app-aoai-chatGPT: Sample code for a simple web chat experience through Azure OpenAI, including Azure OpenAI On Your Data.   また、リポジトリが更新された場合はAzure App Serviceのデプロイセンターから同期をかけることでApp Service側も更新可能です。 ※デフォルトではUIからの履歴削除は物理削除となるため、履歴を削除しない運用にするかサンプルアプリの機能の修正が必要となります。 Prompt Flowからフローを推論エンドポイントとしてデプロイ/サンプルアプリの設定変更 Prompt Flowからフローを推論エンドポイントとしてデプロイ ①    フローエディターで[Start compute session]をクリックし、コンピューティングセッションを開始したうえで[deploy]をクリックします。その後、添付画像の通り、設定したうえで[Review+Create]をクリックします。 ②    デプロイ完了後に推論エンドポイントにアクセスし、[Test]から試しにチャットを投げてみると以下の回答が得られました。 以上で推論エンドポイントのデプロイが完了しました。お疲れ様でした。 サンプルアプリの設定変更 ①    サンプルアプリのAzure App Serviceの環境変数からプロンプトフローの設定を実施します。先ほどデプロイしたサンプルアプリにアクセスします。 ②    環境変数を以下の通り、追加します。その後、App Serviceを再起動([停止]→[開始])します。 USE_PROMPTFLOW        :True AZURE_OPENAI_STREAM    :False PROMPTFLOW_ENDPOINT    :(推論エンドポイントのRESTエンドポイント) PROMPTFLOW_API_KEY    :(エンドポイントのAPIキー) PROMPTFLOW_RESPONSE_TIMEOUT:500 PROMPTFLOW_REQUEST_FIELD_NAME:query PROMPTFLOW_RESPONSE_FIELD_NAME:reply    ③     デプロイしたサンプルアプリで試しにチャットを投げてみると以下の回答が得られました。エラーが発生した際はブラウザをリロードして再度試してみてください。 なお、プロンプトフローの履歴機能は、記事執筆時点(2024/6/7)では機能しないことにご注意ください。 以上でサンプルアプリの設定が完了しました。お疲れ様でした。 Azure AI Studioの評価機能を使用したプロンプトチューニング Azure AI Studioの評価機能とは Azure AI Studioでは、評価機能をノーコードで簡易に使用可能です。大きく「組み込みの評価機能」と「カスタムの評価機能」があります。 Azure AI Studio を使用して生成 AI アプリを評価する方法 - Azure AI Studio | Microsoft Learn    評価機能の設定手順 上記で作成したフローに対して評価機能を用いてプロンプトチューニングを実施します。具体的には、generateReplyノードに対して、バリアントを生成し、デフォルトのプロンプトとそれを日本語に翻訳したプロンプト(GPTに翻訳してもらったうえで整形しています)の出力結果を比較します。なお、使用するプロンプトは省略いたします。 バリアントを使用してプロンプトを調整する - Azure AI Studio | Microsoft Learn バッチ実行を送信してフローを評価する - Azure AI Studio | Microsoft Learn ①    generateReplyノードの右上にある[Show variants]を選択します。その後、デフォルトのvariant_0の[Clone]を選択し、variant_1として複製します。 ②    variant_1にアクセスし、Promptを日本語訳したものに編集します。  ③    編集が完了しましたらPrompt下部の[Validate and parse input]をクリックし、検証を完了させます。 ④    フローエディターの[Evaluate] >[Custom evaluation]の順に選択します。添付画像のとおり設定を進めます。   なお、テストデータは履歴機能を加味しない形で.jsonl形式で用意しました。あくまで動作確認用のデータとして用意しているため、内容については参考程度に留めていただけますと幸いです。   .jsonlファイル {"content":"ストレスチェックは年に何回程度実施する必要がありますか。20文字以内に回答してください。", "ground_truth":"1年に1回実施する必要があります。", "chat_history":"[ ]"} {"content":"退職金制度は必ず設置する必要がありますか?20文字以内で回答してください。", "ground_truth":"必ず設置する必要はありません。", "chat_history":"[ ]"} {"content":"割増賃金について深夜に労働させた場合の割増率について簡潔に教えてください。", "ground_truth":"2割5分以上。", "chat_history":"[ ]"} {"content":"慶弔休暇は必ず定める必要がありますか。", "ground_truth":"必ず定める必要はありません。", "chat_history":"[ ]"} {"content":"職場のパワーハラスメントを防止するために必要な措置を定めた法律を正式名称で教えてください。", "ground_truth":"労働施策の総合的な推進並びに労働者の雇用の安定及び職業生活の充実等に関する法律です。", "chat_history":"[]"} なお、今回使用する評価指標としては、「QnA Ada Similarity Evaluation(LLMを使用し、ユーザーが提供した正解データとモデルの回答の類似性を5段階で測定)」及び「QnA GPT Similarity Evaluation(Embeddingモデルを使用し、ユーザーが提供した正解データとモデルの回答の類似度をコサイン類似度で測定)」を使用します。詳細は以下のリンクをご参照ください。 バッチ実行を送信してフローを評価する - Azure AI Studio | Microsoft Learn  ⑤    [Prompt Flow]>[Runs]から評価結果を確認します。フローを選択するとより詳細な結果が確認できます。Metricsを比較する限りでは同じ結果ですが、Tokensでは日本語のプロンプトの方が若干多く消費しています。 こちらの結果からは、精度は同程度であったため、コスト(トークン量)観点からデフォルトのプロンプトの方が良さそうです。以上で評価が完了しました。お疲れ様でした。 コストが発生し続けてしまうため、必要に応じてコンピューティングの停止やリソースの削除を実施してください。   ここからはAzure AI Studioの便利な機能をご紹介します。 [Visualize outputs] から各フローのメトリックや出力結果を一覧で表示することが可能です。 また、精度向上に向けてボトルネックになっていそうな処理を発見するためにノード単位で入出力を一覧で詳細に確認できるTrace機能も便利です。Azure AI Studioのtracingタブからアクセス可能です。 Tracing — Prompt flow documentation (microsoft.github.io)   回答精度向上に向けた指針について 以下の内容を実践すればある程度の精度向上は図ることができると考えております。 検索精度向上(回答生成に必要なドキュメントを取得できるか?) ・ Azure AI Searchのセマンティックハイブリッド検索を導入する。 ・ Azure AI Searchのアナライザー等の言語設定をドキュメントに合わせて設定する。 ・ Azure AI Searchのチャンクサイズ、オーバーラップをドキュメントに合わせて設定する。 ・ メタデータをAzure AI Searchのインデックスに埋め込み検索時に活用する。 ・ ドキュメントのノイズを除去する。 ・ 検索クエリ作成時のプロンプトやモデルを工夫する。    ・ 高精度なモデルを利用する。    ・ ユースケースに合わせて、ドキュメントの種類や独自情報の定義等をプロンプトに記載する。    ・  CoTやFew-Shot等のプロンプトエンジニアリングの手法を使用し、ユースケースに応じたRAGのプロセスを提示する。 回答精度向上(取得したドキュメントから上手く回答を作成できるか?) ・ 回答生成時のプロンプトやモデルを工夫する。    ・ 高精度なモデルを利用する。    ・ ユースケースに合わせて、ドキュメントの種類や独自情報の定義等をプロンプトに記載する。    ・ CoTやFew-Shot等のプロンプトエンジニアリングの手法を使用し、ユースケースに応じたRAGのプロセスを提示する。 ・ 利用するドキュメント数の上限や検索スコアの閾値を最適化する。   また、前述の通り、サンプルアプリの機能やAzure AI Studioの評価機能を活用して、利用者の意見を吸い上げたり、細かいチューニングを実施していったりすると良いと思われます。 最後に 本記事では、Azureが提供するRAGを実現するサービスである「Prompt Flow」や「Azure AI Studio」を紹介しました。本記事で紹介させていただいた内容でPrompt FlowやAzure AI Studioの活用イメージを少しでも掴んでいただければと思います。 本記事の内容が少しでもRAGの導入やPrompt Flowの活用にお役に立てていただけましたら幸いです。 記事執筆者 雨谷 健司  パーソルキャリア株式会社 デジタルテクノロジー統括部 デジタルビジネス部 アナリティクスグループ 前職は鉄道会社で駅係員をはじめとした現業研修後にテクノロジー部門で社内DXの推進やデータ利活用組織の基盤作りに従事。パーソルキャリア入社後はアナリストとして、データ分析、機械学習モデルの開発、施策設計・評価、データマネジメント、生成系AIに関連するアプリケーション開発/検証に携わっている。
アバター
  導入 本記事について 皆様は、Prompt Flowをご存知でしょうか。Prompt Flowとは、Microsoftが提供するLLM開発のためのOSSのツールです。Azure Machine LearningやAzure AI Studio上でも提供されており、それらをバックエンドとして動作させることができます。加えて、VS Code(Visual Studio Code)の拡張機能も提供されています。一方で、公式ドキュメント以外の情報が少なく、実際に活用するにあたって個人的にハードルを感じていました。 そこで本記事ではPrompt Flow(Azure AI Studio)を使用したRAGのアプリケーション開発の入門について、2回に分けてご紹介し、その簡易さや便利さを実感いただけたらと思います。なお、Azure AI Studioの一部機能はプレビューのため、プロダクトして活用する際は注意が必要ですが、PoC段階で低コストかつ迅速にRAG等のLLMを使用したアプリケーションを試したいシーンでは非常に有用だと考えております。 また、本記事が技術ブログへの初投稿のため、温かい目で見守っていただければ思います。Azureの公式ドキュメントの敷居を下げることを意識して執筆しましたので、本記事をきっかけにPrompt FlowやAzure AI Studioに少しでも興味を持っていただければ幸いです。   【第1回】 RAGの概要、On Your Data(Azure AI Studio)の概要/設定、Prompt Flow(Azure AI Studio)の概要/設定 等 【第2回】 Prompt Flow(Azure AI Studio)の推論エンドポイントのデプロイ、サンプルアプリのデプロイ/カスタマイズ、Azure AI Studioの評価機能、回答精度向上に向けた指針 等 www.tech-street.jp 注意事項 ①    今回紹介する内容は記事執筆時点(2024/6/7)から更新されている可能性があるため、最新情報は公式ドキュメントのご確認をお願いいたします。 ②    今回紹介する内容は一部プレビュー版の内容を含むため、運用環境では推奨されていない機能も含むことに注意してください。 ③    ネットワークや認証等のセキュリティ設定については必要に応じて追加で対応をお願いいたします。 ④    本記事の内容に関連して発生するいかなる不利益、損害、またはその他の問題につきまして、一切の責任を負いかねます。読者の方々が本記事の情報を用いる際には、自己の責任において実施してください。   導入 本記事について 注意事項 RAGの概要 RAGとは Azureで実装するRAG 実装 全体の流れ 前提条件 On Your Dataの設定 On Your Dataとは On Your Dataの設定手順 On Your Dataの回答精度を向上させる方法 Prompt Flowでのフロー作成 Prompt Flowとは Prompt Flowのフロー作成手順 最後に 記事執筆者   RAGの概要 RAGとは RAG(Retrieval-Augmented Generation)とは、事前に用意したナレッジベースのデータをLLMへの指示(プロンプト)に埋め込むことで、LLMが学習していないデータも加味して回答を生成することができるアーキテクチャです。下記のRAGの処理の流れの一例を示します。ここで、オーケストレータは、RAG全体の処理を定義し、ナレッジベースやLLMとを仲介する役割を果たします。 Azureで実装するRAG 前述の通り、RAGの構成要素としては、大きく「LLM」「ナレッジベース」「オーケストレータ」があります。Azureでは、「LLM」に関しては「Azure OpenAI Service」、「ナレッジベース」に関しては「Azure AI Search」が基本的に選択され、「オーケストレータ」の実装方法でバリエーションが出てくると考えています。  個人的な観点で、各オーケストレータの選定した場合の柔軟性・実装コストの比較表を整理いたしました。使用するオーケストレータにより、柔軟性と実装コストでトレードオフの関係にあると考えています。   実装 全体の流れ 以下の公式ドキュメントを参考にステップバイステップで実装していきます。 Azure AI Studio でプロンプト フローを使用して質疑応答コパイロットを作成してデプロイする - Azure AI Studio | Microsoft Learn ①    On Your Dataの設定 まずAzure AI StudioのプレイグラウンドからOn Your Dataを設定します。 ②    Prompt Flowでのフロー作成 続いて、①にて設定したOn Your DataをPrompt Flowとして編集する機能を活用して、フローを作成します。  前提条件 以下の前提条件を満たすように準備をお願いいたします。 Azure AI Studio でプロンプト フローを使用して質疑応答コパイロットを作成してデプロイする - Azure AI Studio | Microsoft Learn ・ Azure OpenAIへのアクセス権の申請が完了していること。 https://aka.ms/oai/access ・ Azure AI Hub・Azure AI Projectが作成され、任意のChatモデル/ Embeddingモデルがデプロイされていること。 プロジェクトを作成して Azure AI Studio でチャット プレイグラウンドを使用する - Azure AI Studio | Microsoft Learn なお、本記事では、Chatモデルは「gpt-4-1106-preview」、Embeddingモデルは「text-embedding-ada-002」を使用しています。 ・ ストレージアカウントがデプロイされ、新たに作成したコンテナにRAGで使用するドキュメントがアップロードされていること。 ストレージ アカウントを作成する - Azure Storage | Microsoft Learn クイック スタート: BLOB のアップロード、ダウンロード、一覧表示 - Azure portal - Azure Storage | Microsoft Learn なお、本記事で使用するドキュメントは、経済産業省が公開しているモデル就業規則(Wordファイル)です。 モデル就業規則について |厚生労働省 (mhlw.go.jp) ・ Azure AI Search(Basicプラン以上)がデプロイされていること。 ポータルで Search サービスを作成する - Azure AI Search | Microsoft Learn ・ デプロイしたAzure AI services(若しくはAzure OpenAI Service)、Azure AI Search及びストレージアカウントに接続されていること。 Azure AI Studio で新しい接続を追加する方法 - Azure AI Studio | Microsoft Learn  On Your Dataの設定 On Your Dataとは On Your Dataとは、Azureが提供するRAGをノーコードで実装するPaaSサービスで、Azure OpenAI ServiceやAzure AI Studioのプレイグラウンドで提供されています。  Azure OpenAI Service で独自のデータを使用する - Azure OpenAI | Microsoft Learn On Your Dataの構成イメージは下図の通りです。 On Your Dataの設定手順 今回は後続のPrompt Flowへの接続を見据えて、Azure AI StudioのプレイグラウンドからOn Your Dataを設定します。なお、前述の前提条件が満たされているものとし、Azure AI Studioのプレイグラウンドからの設定を説明します。   ①    Azure AI Studioの作成したProjectのプレイグラウンドにアクセスし、[セットアップ] ペインで、[Add your data]>[+Add a new data source] の順に選択します。 ②    添付画像の通り、設定を実施し、完了するまでしばらく待ちます。   ③    システムメッセージに以下のプロンプトを入力します。 ## 役割 ユーザーが取得した就業規則から情報を抽出できるように設計されたAIアシスタントです。 ## 指示 ユーザーから就業規則に関連する日本語ドキュメントを受け取ります。 ドキュメントを丁寧に読み、意味を理解します。 ユーザーの日本語のクエリに対してドキュメントの内容を元に日本語で回答します。 ## 注意点 あくまでドキュメントのみを情報源とし、自身の知識は使用しません。 ドキュメントから答えられないクエリには、その理由を説明します。       ④    試しにチャットを投げてみると以下の回答が得られました。gpt3.5だと回答が返ってこなかったり、英語で返ってきたりすることがあります。 以上でOn Your Dataの設定が完了しました。お疲れ様でした。   On Your Dataの回答精度を向上させる方法 On Your Dataの回答精度を向上させる方法は下記公式ドキュメントに整理されています。 Azure OpenAI Service で独自のデータを使用する - Azure OpenAI | Microsoft Learn   On Your Dataでは回答精度向上に向けた試行錯誤に取り組む中で衝突する課題が大きく3つあると考えています。 ①    検索クエリ生成と回答生成のChatモデルが分けることができない。(On Your Dataの内部の機構は公開されていませんが、検索クエリ生成にChatモデルを使用している可能性が高いと筆者は考えています。) ②    検索クエリ生成と回答生成のプロンプトがブラックボックスのため、プロンプトのチューニングできない。 なお、回答生成のプロンプトはシステムメッセージから一部チューニング可能です。 ③    RAGの全体のアーキテクチャがブラックボックスでチューニングができない。 PoCとしてはOn Your Dataで十分という見方もできますが、これらの課題はもう一歩精度向上を図りたいときに大きな足かせになると感じています。特に自社のユースケースに特化させようとしたときに②は大きなボトルネックになり得ます。Prompt Flowではこれらの課題を解決することができます。    Prompt Flowでのフロー作成 Prompt Flowとは Prompt Flowは、LLMアプリケーションを開発するためのOSSのツールで、開発サイクルを効率化するために、アイデア出し、プロトタイピング、テスト、評価、本番環境へのデプロイやモニタリングといった用途をサポートする機能があります。 Azure Machine LearningやAzure AI Studioでも提供されており、それらをバックエンドとして動作させることが可能です。また、VS Codeの拡張機能も提供されています。 GitHub - microsoft/promptflow: Build high-quality LLM apps - from prototyping, testing to production deployment and monitoring. 筆者はLangChainやSemantic Kernel に近いサービスという印象を持っていましたが、以下の公式のQAにある通り、Microsoftとしては補完関係にあるサービスと位置付けているようです。 プロンプトフローはMicrosoftのLangChainに相当するものですか? プロンプト フローは、LangChain と Semantic Kernel を補完するものであり、どちらとも併用できます。プロンプト フローは、評価、デプロイ、明確に定義された資産の監視、フロー ロジックを提供し、アプリケーションのデバッグや大規模なオーケストレーションのテストに役立ちます。 Azure AI Studio - 生成 AI 開発ハブ | Microsoft Azure Prompt Flowのポイントを以下にまとめます。 ・ Toolと呼ばれる処理を定義したノードを組み合わせてフローとして全体の処理を定義します。 ・ Toolは組み込みで「LLM」「Prmopt」「Python」等が存在します。また、組み込みのToolに適したものが存在しない場合は、カスタムのToolを作成することも可能です。 今回使用するToolの役割は下記の通りです。 ・LLM:プロンプト及びLLMの設定を定義 Azure AI Studio 内のフロー用 LLM ツール - Azure AI Studio | Microsoft Learn ・Python:Pythonの処理を定義 Azure AI Studio 内のフロー用 Python ツール - Azure AI Studio | Microsoft Learn ・Index Lookup:ナレッジベースの検索処理を定義 Azure AI Studio 内のフロー用インデックス参照ツール - Azure AI Studio | Microsoft Learn Prompt Flowの詳細は、以下の公式ドキュメントを参照してください。 Azure AI Studio でのプロンプト フロー - Azure AI Studio | Microsoft Learn プロンプト フローのビルド方法 - Azure AI Studio | Microsoft Learn Prompt Flowのフロー作成手順 On Your Dataの設定の続きになります。 ①    [チャット セッション] ウィンドウの上にあるメニューから、[Prompt Flow] を選択し、添付画像の通り設定し、フローにアクセスします。 デフォルトで作成される各ノードの役割は以下の通りです。(記事執筆時点では公式ドキュメントが更新されていなかったため、想定を記載しています。) formatRewriteIntentInputs(python):ユーザーからのクエリと会話履歴を取り、指定したトークンの最大数に基づいて整形するためのする処理 rewriteIntent(LLM):ユーザーのクエリの意図を判断し、検索クエリを生成するためのプロンプトを定義 extractSearchIntent(python):rewriteIntentノードの出力の書式を設定するためのプログラム(文字列を引数として受け取り、その文字列に含まれる「意図」をリストとして返す) querySearchResource(Index Lookup):Azure AI Search等のナレッジベースから検索を行うツール chunkDocuments(python):検索結果として得られたドキュメントのリストをチャンク(区切り)に分割するプログラム selectChunks(python):チャンクを選択し、スコアに基づいてフィルタリングして返すプログラム shouldGenerateReply(python):条件に基づいて返信を生成すべきかどうかを判断するためのプログラム formatGenerateReplyInputs(python):selectChunksノード及びinputsの出力の書式を設定するためのプログラム(与えられたクエリ、会話履歴、文書のチャンク、およびトークンの最大数に基づいて、応答を生成するための入力をフォーマットする) generateReply(LLM):取得したドキュメントを使用して応答するためのプロンプトを定義 ②    [Start compute session]をクリックし、コンピューティングセッションを開始します。   ③    [Chat]を選択し、フローが正しく機能するかテストします。Azure AI Studioの言語設定が日本語になっている場合、フローの設定値が翻訳されて上手く動作しない場合があります。その際は該当箇所を修正してください。 以上でPrompt Flowの基本設定が完了しました。お疲れ様でした。 コストが発生し続けてしまうため、必要に応じてコンピューティングの停止やリソースの削除を実施してください。 お気づきの通り、プロンプトフローでは、各ノード単位で詳細な設定が可能であるため、On Your Dataの課題が解決可能です。各プロンプトの改善にあたっては、プロンプトフローの評価機能を駆使しながらデフォルトのものをベースに追記・編集する形が良いと思います。  最後に 本記事では、Azureが提供するLLMプロダクトを開発するサービスである「Prompt Flow」(及び「On Your Data」)を紹介しました。 本記事で紹介させていただいた内容で、Azure AI StudioでPrompt Flowを使用したRAGを簡単に実装できることを体感いただけたのではないかと思います。 第2回では、Azure AI Studioのサンプルアプリや評価機能といったもう一歩踏み込んだ内容をご紹介できればと思います。 本記事の内容が少しでもRAGの導入やPrompt Flowの活用にお役に立てていただけましたら幸いです。 記事執筆者 雨谷 健司  パーソルキャリア株式会社 デジタルテクノロジー統括部 デジタルビジネス部 アナリティクスグループ 前職は鉄道会社で駅係員をはじめとした現業研修後にテクノロジー部門で社内DXの推進やデータ利活用組織の基盤作りに従事。パーソルキャリア入社後はアナリストとして、データ分析、機械学習モデルの開発、施策設計・評価、データマネジメント、生成系AIに関連するアプリケーション開発/検証に携わっている。
アバター
はじめに OpenAI の Assistants API をそのまま使用することで、自前で LangChain のエージェントなどを使用して同様の処理を実装する手間を省け、非常に便利です。ただ、現状( 2024/05/18 )ではまだ β 版ということもあり、 API のインタフェースの改変も多く見られます。 Assitants APIを用いたcode-interpreterのUIをstreamlitで実装 においても、実装例が紹介されていますが、そのままでは動作しないこともあり、最新版での動作検証も兼ねてStreamlitでの実装例を紹介します。   また、本記事ではStreaming対応済みの実装を取り入れており、よりリアルタイムな対話が可能となっています。扱っているモデルは2024/05/14に発表されたGPT-4oを用いています。 はじめに 実装例 app.py openai_handler.py デモ 実行手順 ファイルの生成 1. PDFの生成 2. Wordの生成 3.Excelの生成 4.PowerPointの生成 5.CSVの生成 6.PNG画像の生成 ファイルの生成に関して ファイルの読み取り 1.PDFの読み取り 2.Wordの読み取り 3.エクセルの読み取り 4.PowerPointの読み取り 5.CSVの読み取り 6.PNG画像の読み取り ファイルの読み取りに関して まとめ 1.実装例 2.ファイル生成のデモ 3.ファイル読み取りのデモ 4.課題と改善点 記事執筆者 実装例 以下に、Streamlit(Python)を用いた実装例を示します。 Github Repositoryは こちら になります。 app.py import streamlit as st from openai import OpenAI import openai_handler st.title( "Assistant API Code Interpreter" ) client = OpenAI() with st.form( "form" , clear_on_submit= False ): user_question = st.text_area( "文章を入力" ) file = [st.file_uploader( "ファイルをアップロード" , accept_multiple_files= False )] or None submitted = st.form_submit_button( "送信" ) if submitted: st.session_state[ "thread" ], st.session_state[ "stream" ] = openai_handler.submit_message( user_question, file ) openai_handler.wait_on_stream(st.session_state[ "stream" ], st.session_state[ "thread" ]) openai_handler.py import time from os.path import dirname, join from typing import ( Any, Iterable, Literal, Optional, Tuple, TypedDict, ) import streamlit as st from dotenv import load_dotenv from openai import AssistantEventHandler, OpenAI from openai.lib.streaming._assistants import AssistantStreamManager from openai.pagination import SyncCursorPage from openai.types.beta.thread import Thread from openai.types.beta.thread_create_params import ( Message as CreateMessage, ) from openai.types.beta.thread_create_params import ( MessageAttachment, ) from openai.types.beta.threads import ( Message, Run, TextContentBlock, ) from openai.types.beta.threads.image_file import ImageFile from openai.types.beta.threads.text import Text from streamlit.runtime.uploaded_file_manager import ( UploadedFile, ) from typing_extensions import override class EventHandler (AssistantEventHandler): @ override def on_text_created (self, text: Text) -> None : print ( " \n assistant > " , end= "" , flush= True ) @ override def on_text_delta (self, delta: Any, snapshot: Any) -> None : print (delta.value, end= "" , flush= True ) @ override def on_image_file_done (self, image_file: ImageFile) -> None : print ( "on_image_file_done image_file id:" , image_file.file_id) st.image(get_file(image_file.file_id)) @ override def on_end (self) -> None : print ( "on_end" ) if "thread" in st.session_state: thread = st.session_state[ "thread" ] pretty_print(get_response(thread)) def on_tool_call_created (self, tool_call: Any) -> None : print (f " \n assistant > {tool_call.type} \n " , flush= True ) def on_tool_call_delta (self, delta: Any, snapshot: Any) -> None : if delta.type == "code_interpreter" : if delta.code_interpreter.input: print (delta.code_interpreter.input, end= "" , flush= True ) if delta.code_interpreter.outputs: print ( " \n\n output >" , flush= True ) for output in delta.code_interpreter.outputs: if output.type == "logs" : print (f " \n {output.logs}" , flush= True ) dotenv_path = join(dirname(__file__), ".env.local" ) load_dotenv(dotenv_path) client = OpenAI() # IF: https://platform.openai.com/docs/assistants/how-it-works/creating-assistants assistant = client.beta.assistants.create( name= "汎用アシスタント" , instructions= "あなたは汎用的なアシスタントです。質問には簡潔かつ正確に答えてください。" , tools=[{ "type" : "code_interpreter" }], model= "gpt-4o" , ) ASSISTANT_ID = assistant.id global_messages: list [Any] = [] class CustomMessage (TypedDict): role: Literal[ "user" , "assistant" ] content: str attachments: Optional[Iterable[MessageAttachment]] metadata: Optional[Any] # If no file is uploaded, the 'files' variable is assigned a list containing a single 'None' value. def submit_message ( user_message: str , files: Optional[ list [Optional[UploadedFile]]] = None , assistant_id: str = ASSISTANT_ID, ) -> Tuple[Thread, AssistantStreamManager[EventHandler]]: print ( "assistant_id:" , assistant_id) print ( "user_message:" , user_message) print ( "files:" , files) with st.chat_message( "user" ): st.write(user_message) if files is None : files = [ None ] file_ids = submit_file(files) if files[ 0 ] is not None else [] messages: list [CustomMessage] = [ { "role" : "user" , "content" : user_message, "attachments" : None , "metadata" : None } ] if len (file_ids) > 0 : messages[ 0 ][ "attachments" ] = [ { "file_id" : file_ids[ 0 ], "tools" : [{ "type" : "code_interpreter" }], } ] # IFは修正される可能性があるため、下のURLを確認する # https://platform.openai.com/docs/assistants/how-it-works/managing-threads-and-messages _messages: Iterable[CreateMessage] = [CreateMessage(**msg) for msg in messages] thread = client.beta.threads.create(messages=_messages) print ( "thread_id:" , thread.id) stream = client.beta.threads.runs.stream( thread_id=thread.id, assistant_id=assistant.id, instructions= "ユーザーのメッセージと同じ言語で回答してください。回答を生成する際はユーザーへの確認は不要です。" , event_handler=EventHandler(), ) return thread, stream def submit_file (files: list [Optional[UploadedFile]]) -> list [ str ]: if files: ids = [] for file in files: if file is not None : # IF: https://platform.openai.com/docs/assistants/how-it-works/creating-assistants _file = client.files.create( file = file .read(), purpose= "assistants" , ) ids.append(_file.id) return ids else : return [] def get_response (thread: Thread) -> SyncCursorPage[Message]: return client.beta.threads.messages.list(thread_id=thread.id, order= "asc" ) def pretty_print (messages: SyncCursorPage[Message]) -> None : for m in messages: print ( "role:" , m.role) print ( "content:" , m.content) if m.role == "assistant" : for content in m.content: cont_dict = content.model_dump() if cont_dict.get( "text" ) is not None and isinstance (content, TextContentBlock): message_content = content.text annotations = message_content.annotations files = [] for ( index, annotation, ) in enumerate (annotations): message_content.value = message_content.value.replace( annotation.text, f " [{index}]" , ) if file_path := getattr ( annotation, "file_path" , None , ): files.append( ( file_path.file_id, annotation.text.split( "/" )[- 1 ], ) ) for file in files: st.download_button( f "{file[1]} : ダウンロード" , get_file( file [ 0 ]), file_name= file [ 1 ], ) def wait_on_run (run: Run, thread: Thread) -> Run: while run.status == "queued" or run.status == "in_progress" : print ( "wait_on_run" , run.id, thread.id) run = client.beta.threads.runs.retrieve( thread_id=thread.id, run_id=run.id, ) print ( "run.status:" , run.status) time.sleep( 0.5 ) return run def wait_on_stream (stream: AssistantStreamManager[EventHandler], thread: Thread) -> None : with st.chat_message( "assistant" ): with stream as s: st.write_stream(s.text_deltas) s.until_done() def get_file (file_id: str ) -> bytes : retrieve_file = client.files.with_raw_response.content(file_id) content: bytes = retrieve_file.content return content   デモ 実行手順 上記のリポジトリで、'make run' を実行します。 下記の画面に遷移します。   ファイルの生成   1. PDFの生成 指示文:「任意のPDF資料を作成してください。マーケティング関連のビジネス企画書でお願いします。」  ・1回目生成失敗  ・補足:他にも稀に文字化けしたPDFが生成されてしまうことがある。(日本語の文字エンコードを正しく指定できていない。)   ・2回目: 生成されたPDFファイル   <結果> 生成に失敗する場合もあり不安定に感じる。生成されたファイル自体には特に問題はない。 2. Wordの生成 指示文:「任意のワード資料を作成してください。マーケティング関連のビジネス企画書でお願いします。」   ・ 生成されたワードファイル   <結果> 文字がメインでフォーマットも特に綺麗に整理されてはいないが、ワードファイル自体は問題なく生成されている。   3.Excelの生成 指示文:「任意のエクセル資料を作成してください。マーケティング関連の帳票でお願いします。」   ・ 生成されたエクセルファイル <結果> サンプルデータとして特に問題のないエクセルファイルが生成されました。   4.PowerPointの生成 指示文:「任意のパワーポイント資料を作成してください。マーケティング関連のビジネスプレゼンテーションでお願いします。」  ・ 生成されたパワーポイントファイル <結果> 文字がメインでフォーマットも特に綺麗に整理されてはいないが、パワーポイント自体は問題なく生成されました。   5.CSVの生成 指示文:「任意のCSV資料を作成してください。マーケティング関連の帳票でお願いします。」  ・ 生成されたCSVファイル <結果> ・サンプルデータとして特に問題のないCSVファイルが生成されました。   6.PNG画像の生成 指示文:「任意のPNG画像を作成してください。マーケティング関連のビジネスに関するものでお願いします。」  ・ 生成されたPNG画像 <結果> テキストが記載されたシンプルな画像。文字が若干はみ出している点が気になる。   ファイルの生成に関して 全体的にPDF、ワード、エクセル、パワーポイント、CSV、PNG画像全て生成に成功しています。ただ、PDFのみ一度生成に失敗しており、若干不安定なところは目立ちました(日本語の文字コードを正しく指定できず文字化けすることもありました。)。また、PNG画像は生成に成功したと言ってもテキストが画像化されているだけで、DALLE等で生成される写真のような画像は取得できませんでした。   結論としてテキストベースで資料を作成したり、ダミーのデータを作成するにはCode Interpreterは問題なく使えると思います。 ファイルの読み取り 上記で生成したファイルを元に読み取りのタスクを行なってみます。   1.PDFの読み取り 指示文:「こちらのファイルの内容を全て抽出して説明してください。」  <結果> 初め文字化けが指摘されたが二度目のトライで抽出に成功しました。   2.Wordの読み取り 指示文:「こちらのファイルの内容を全て抽出して説明してください。」  <結果> ファイルの中身は抽出できているが、中身を抽出するまでのプロセスが若干冗長に感じられる。   3.エクセルの読み取り 指示文:「こちらのファイルのデータはどういった内容が含まれているかを説明し、グラフで示してください。」  <結果> 注意点として、エクセルの中身に日本語が混ざっているとグラフの生成に失敗します。上記で生成したエクセルには日本語が入っていたので英語に直したもので読み取りを行っています。また、エクセルであることを判定するまでの工程が冗長に思われます。グラフ表示は問題ありませんでした。( 修正後エクセルファイル )   4.PowerPointの読み取り 指示文:「こちらのファイルの`全スライド`の内容を全て抽出して説明してください。」  <結果> 問題なく各スライドの中身を抽出できている。ただ、スライドという指示分を足さないとzipを解凍して、ディクレトリを探索しながらそれぞれのxmlファイルを詳しく解説し始めてしまう点がある。   5.CSVの読み取り こちらのファイルのデータはどういった内容が含まれているかを説明し、グラフで示してください。  <結果> 最初エクセルで解析しようとするが失敗し、その後CSVであることを特定できている。また、上記のエクセルと同様に日本語が混ざっていると抽出に失敗するため、上記でCSVとして生成したものは英語に直したもので読み取りを行っています。( 修正後CSVファイル )   6.PNG画像の読み取り 指示文:「こちらのファイルのデータはどういった内容が含まれているかを説明してください。」  <結果> 敢えてファイルのデータと指定しましたが、画像データであることを特定して中身の説明まで上手くできています。 ファイルの読み取りに関して 全体的にどのファイルのデータも若干プロンプトの調整が必要な箇所はありますが中身をうまく抽出できています。ただ、CSVやExcelでグラフの可視化をする際に日本語が混ざっていると表示に失敗し、英語に変換する必要があるのは残念でした。ただ以前よりも抽出の精度は上がっているので今後の改善に期待です。 まとめ この記事では、OpenAIのAssistants APIを用いたCode Interpreterの現状と課題について、Streamlitを使用して検証しました。以下が主要なポイントです。 1.実装例 Streamlitを使用して、OpenAI Assistants APIを統合したCode Interpreterの実装方法を紹介しました。 2.ファイル生成のデモ PDF、ワード、エクセル、パワーポイント、CSV、PNG画像の生成タスクを実施し、全体的に成功しました。ただし、PDFの生成に関しては一度失敗することがあり、特に日本語の文字エンコードの問題が見られました。 3.ファイル読み取りのデモ 生成したファイルを読み取り、その内容を抽出するタスクを実施しました。各形式のファイルについて適切に内容を抽出できましたが、CSVやExcelでのグラフの可視化には、日本語が含まれていると表示に失敗する場合がありました。 4.課題と改善点 PDFの生成において日本語の文字エンコードに問題がある点。 CSVやExcelのグラフ可視化において、日本語が混ざると表示に失敗する点。 ファイルの読み取りにおいてプロンプトの調整が必要な場合がある点。 全体として、OpenAIのAssistants APIは多様な形式のファイル生成と読み取りにおいて強力なツールであり、今後の改善によりさらに高い精度と安定性が期待されます。この記事が、OpenAI Assistants APIを用いた実装の参考になれば幸いです。 記事執筆者 梅本 誠也  パーソルキャリア株式会社 デジタルテクノロジー統括部 生成系AIエンジニアリング部 生成系AIエンジニアリンググループ リードエンジニア 韓国で5年間正規留学し、その間に業務委託で機械学習とデータエンジニアリング方面の開発を経験。新卒でアプリケーションエンジニアとしてフロントエンド、バックエンド、インフラを幅広く経験。パーソルキャリア入社後はデータエンジニアとして、社内のデータ分析基盤の構築と運用保守を担当。一方で、生成系AIを用いたアプリケーション開発にも携わっている。
アバター
今回のテーマ Pythonには無料の数理最適化ライブラリがあります。有名どころでは「OR-Tools」と「PuLP」が二大巨頭ではないでしょうか。 個人的にはOR-Tools推しですが、私の周囲ではPuLPを使用する人も多く、どちらが良いのか知りたくなりました。そこで、今回は「OR-Tools」と「PuLP」に数独を解かせて、その処理速度を比較して、どちらのライブラリがよいか検証してみます。 今回のテーマ 環境 問題設定 定式化 実装 結果 最後に Appendix1(定式化) Appendix2(実装) 記事執筆者 環境 Python Version 3.11.4 OS Windows 11 使用するライブラリ PuLP version: 2.8.0 OR-Tools version: 9.8.3296 問題設定 数独を用いる。 9x9の全マスに, 1~9の数字を必ず埋める。 どの1行、どの1列、どの3x3においても同じ数字は1回だけしか使えない。 数字が埋まっている場所では、その数字を使う。 問題は こちらのページ から借りてきました。 上級問題を10種類、各10回ずつ解かせ、時間を計測する。 定式化 定式化は整数計画法を用いた。 本来ならば目的関数を定義するが、今回の定式化では考慮しない。 今回の定式化はどちらかと言うとSATの考え方に近い。 9×9×9の箱を考える。 行方向×列方向×番号の3方向。 各セルに0か1の数値が入る。 変数は x(i, j, k)∈{0, 1}とする。 但し、i, j, k ∈ {0, 1, ..., 8}である。 インデックス(i, j, k)はそれぞれ行方向、列方向、番号を表す。 例として x(1,1,7)=1なら、2行目、2列目のセルには8が入るという意味である。 既に数字が埋まっているインデックスと数値を以下で定義する。 例として、1行目、 1列目のセルに5が入っていたら、以下とする。 制約条件 どの1行においても、同じ数字は1回だけしか使えない。 どの1列においても、同じ数字は1回だけしか使えない。 どの3x3においても、同じ数字は1回だけしか使えない。 数字が埋まっている場所では、その数字を使う。 実装 計算機のクラスを作成しています。重要なポイントは以下です。 計算機の抽象クラスには計算以外の機能を全て巻き取らせている。 時間計測 元になる配列のセッター、ゲッター 解のセッター、ゲッター PuLPの実装、 OR-Toolsの実装について 計算能力以外で差が出ないように、出来るだけ定式化のコードを同じにしている。 それぞれの計算機で求解する場所の前後で時間計測を行っている。 結果 全体の結果は上図(箱ひげ図)の通りです。 平均で7倍前後程度の差が出ました。 また、計算一回当たりの処理時間の代表値は以下の表の通りです。 代表値 OR-Tools PuLP 平均値[秒] 0.013 0.099 中央値[秒] 0.013 0.094 また、10回のトライアルの詳細な計算時間は上図の通りです。問題ごとに傾向はあまり見られない為、純粋なライブラリの実力が出ていると考えられます。 最後に 数独の求解に関して、7倍程度の差が出るとは予想外でした。 今回は変数が729個の整数計画問題として求解しましたので、 今後は線形計画法や混合整数計画法の求解でどの程度違うかを、 問題のスケールも含めて調べてみようかと思います。 Appendix1(定式化) 以下は定式化(数式)です。参考にしてください。 Appendix2(実装) 以下はクラスの全文です。 参考にしてください。 計算機の抽象クラスは以下です。 from abc import ABC, abstractmethod from time import perf_counter_ns from src.utils.enums.problem_types import ProblemTypes class AbstractCalculator(ABC): """ 計算機の抽象クラス """ @abstractmethod def __init__(self) -> None: """ 必要なら、イニシャライザは各サブクラスにて実装 """ pass @abstractmethod def formulate(self) -> None: """ 定式化処理は各サブクラスにて実装 """ raise NotImplementedError("Implement each subclasses") @abstractmethod def calculate(self) -> None: """ 求解処理は各サブクラスにて実装 """ raise NotImplementedError("Implement each subclasses") def set_array(self, array: list) -> None: """問題用の配列をセットする※初期化処理も兼ねる Args: array (list): 問題用の配列 """ self.__array = array self.__time_array = [] self.__calc_time = -1 self.set_answer_array([]) def set_answer_array(self, answer_array: list) -> None: """解答の配列をセットする Args: answer_array (list): 解答の配列 """ self.__answer_array = answer_array def set_problem_type(self, problem_type: ProblemTypes) -> None: """問題種別をセットする Args: problem_type (ProblemTypes): 問題種別 """ self.__problem_type = problem_type def append_time(self) -> None: """現在の時刻を配列に追加する """ self.__time_array.append(perf_counter_ns()) def set_calc_time(self) -> None: """時刻の配列から計算時間をセットする Raises: Exception: 時間計測していない """ time_array = self.__time_array if len(time_array) < 2: # 配列の長さが2より小さい->時間計測していない raise Exception("No records") else: # 時刻の最初と最後を取得する time0 = time_array[0] time1 = time_array[1] # ナノ秒で記録しているので, 秒単位に直す calc_time = (time1-time0)*(10**-9) # 0.1マイクロ秒より後の桁を切り捨てる calc_time = round(calc_time, 7) # 計算時間をセットする self.__calc_time = calc_time def get_array(self) -> list: """問題用の配列を出力する Returns: list: 問題用の配列 """ return self.__array def get_answer_array(self) -> list: """解答用の配列を出力する Returns: list: 解答用の配列 """ return self.__answer_array def get_problem_type(self) -> ProblemTypes: """問題種別を出力する Returns: ProblemTypes: 問題種別 """ return self.__problem_type def get_calc_time(self) -> float: """計算時間を出力する Returns: float: 計算時間 """ return self.__calc_time 以下がPuLPの実装です。 import pulp from src.calc.abstract_calculator import AbstractCalculator from src.utils.enums.problem_types import ProblemTypes class PulpCalculator(AbstractCalculator): """ PuLPを用いた計算機です。 """ # region constructor def __init__(self) -> None: super().__init__() # endregion constructor # region formulate def formulate(self) -> None: """ 定式化処理。問題種別次第で処理を変更する。 """ problem_type = self.get_problem_type() if problem_type == ProblemTypes.IP: raise NotImplementedError("Not implemented") elif problem_type == ProblemTypes.LP: raise NotImplementedError("Not implemented") elif problem_type == ProblemTypes.SUDOKU: self.__formulate_SUDOKU() else: raise ValueError(f"Invalid type: {problem_type}") def __formulate_SUDOKU(self) -> None: """数独の定式化処理 """ # モデルインスタンスを作成する prob = pulp.LpProblem('Sudoku', pulp.LpMinimize) # 目的関数はナシ prob += 0 # 領域作成 self.__numbers = list(range(1, 10)) self.__xs = list(range(1, 10)) self.__ys = list(range(1, 10)) numbers = self.__numbers xs = self.__xs ys = self.__ys choices = pulp.LpVariable.dicts( "Choice", (xs, ys, numbers), 0, 1, pulp.LpInteger ) # 制約(k方向にどれか一個1)(答えの1マスには一つの数値しか存在できない) for i in xs: for j in ys: prob += (sum(choices[i][j][k] for k in numbers) == 1) # 制約(i方向にどれか一個1)(答えの1行には各数値は一つずつしか存在できない) for j in ys: for k in numbers: prob += (sum(choices[i][j][k] for i in xs) == 1) # 制約(j方向にどれか一個1)(答えの1列には各数値は一つずつしか存在できない) for i in xs: for k in numbers: prob += (sum(choices[i][j][k] for j in ys) == 1) # 制約(3x3に区切られたマス内でも各数字は一つしか存在できない) for k in numbers: for i3 in range(3): prob += ( sum( choices[i3*3+i][j][k] for i in range(1, 4) for j in range(1, 4) ) == 1 ) prob += ( sum( choices[i3*3+i][j][k] for i in range(1, 4) for j in range(4, 7) ) == 1 ) prob += ( sum( choices[i3*3+i][j][k] for i in range(1, 4) for j in range(7, 10) ) == 1 ) # 制約(既に数値が置いてある場所に変数をセットしていく) array = self.get_array() for i, row in enumerate(array): for j, cell in enumerate(row): if cell == 0: # 数値が確定していないので次へ pass else: # 数値が1~9のどれかが入っている prob += (choices[i+1][j+1][cell] == 1) self.__choices = choices self.__prob = prob # endregion formulate # region calculate def calculate(self) -> None: """計算機処理. 問題種別次第で処理を変更する。 """ problem_type = self.get_problem_type() if problem_type == ProblemTypes.IP: raise NotImplementedError("Not implemented") elif problem_type == ProblemTypes.LP: raise NotImplementedError("Not implemented") elif problem_type == ProblemTypes.SUDOKU: self.__calculate_SUDOKU() else: raise ValueError(f"Invalid type: {problem_type}") def __calculate_SUDOKU(self): """数独の求解処理 """ # モデルインスタンスを取得する prob = self.__prob self.append_time() # 求解処理を行うが, メッセージが鬱陶しいので見せないことにする state = prob.solve(pulp.PULP_CBC_CMD(msg=False)) self.append_time() self.set_calc_time() # 結果を解答用配列にセットする if pulp.LpStatus[state] == "Optimal": numbers = self.__numbers xs = self.__xs ys = self.__ys choices = self.__choices answer_array = [] for x in xs: row = [] for y in ys: for v in numbers: if choices[x][y][v].value() == 1: row.append(v) answer_array.append(row) self.set_answer_array(answer_array) else: print("Couldn't optimize") # endregion calculate 以下がOR-Toolsの実装 from ortools.linear_solver import pywraplp from src.calc.abstract_calculator import AbstractCalculator from src.utils.enums.problem_types import ProblemTypes class OrToolsCalculator(AbstractCalculator): """ OR-Toolsを用いた計算機 """ # region constructor def __init__(self) -> None: super().__init__() # endregion constructor # region formulate def formulate(self) -> None: """ 定式化処理。問題種別次第で処理を変更する。 """ problem_type = self.get_problem_type() if problem_type == ProblemTypes.IP: raise NotImplementedError("Not implemented") elif problem_type == ProblemTypes.LP: raise NotImplementedError("Not implemented") elif problem_type == ProblemTypes.SUDOKU: self.__formulate_SUDOKU() else: raise ValueError(f"Invalid type: {problem_type}") def __formulate_SUDOKU(self) -> None: """数独の定式化処理 """ # モデルインスタンスを作成する solver: pywraplp.Solver = pywraplp.Solver.CreateSolver("SAT") # 領域作成 var_dict = {} for i in range(9): # 行方向 for j in range(9): # 列方向 for k in range(9): # 奥行方向 var_name = f"ans_{i}_{j}_{k}" var_dict[(i, j, k)] = solver.IntVar(0, 1, var_name) # 制約(k方向にどれか一個1)(答えの1マスには一つの数値しか存在できない) for i in range(9): for j in range(9): solver.Add(sum(var_dict[(i, j, k)] for k in range(9)) == 1) # 制約(i方向にどれか一個1)(答えの1行には各数値は一つずつしか存在できない) for j in range(9): for k in range(9): solver.Add(sum(var_dict[(i, j, k)] for i in range(9)) == 1) # 制約(j方向にどれか一個1)(答えの1列には各数値は一つずつしか存在できない) for i in range(9): for k in range(9): solver.Add(sum(var_dict[(i, j, k)] for j in range(9)) == 1) # 制約(3x3に区切られたマス内でも各数字は一つしか存在できない) for k in range(9): for i3 in range(3): solver.Add( sum( var_dict[(i3*3+i, j, k)] for i in range(3) for j in range(0, 3) ) == 1 ) solver.Add( sum( var_dict[(i3*3+i, j, k)] for i in range(3) for j in range(3, 6) ) == 1 ) solver.Add( sum( var_dict[(i3*3+i, j, k)] for i in range(3) for j in range(6, 9) ) == 1 ) # 制約(既に数値が置いてある場所に変数をセットしていく) array = self.get_array() for i, row in enumerate(array): for j, cell in enumerate(row): if cell == 0: # 数値が確定していないので次へ pass else: # 数値が1~9のどれかが入っている # 1を引いてインデックスを取る k = cell - 1 solver.Add(var_dict[(i, j, k)] == 1) # 変数のディクショナリとモデルインスタンスをセットする self.__var_dict = var_dict self.__solver = solver # endregion formulate # region calculate def calculate(self) -> None: """計算機処理。問題種別次第で処理を変更する。 """ problem_type = self.get_problem_type() if problem_type == ProblemTypes.IP: raise NotImplementedError("Not implemented") elif problem_type == ProblemTypes.LP: raise NotImplementedError("Not implemented") elif problem_type == ProblemTypes.SUDOKU: self.__calculate_SUDOKU() else: raise ValueError(f"Invalid type: {problem_type}") def __calculate_SUDOKU(self): """数独の求解処理 """ # ソルバを取得する solver: pywraplp.Solver = self.__solver # 求解する self.append_time() status = solver.Solve() self.append_time() self.set_calc_time() # 結果をセットする if ((status == pywraplp.Solver.OPTIMAL) or (status == pywraplp.Solver.FEASIBLE)): # 解答用の配列のひな形を作成する answer_array = [ [0 for _ in range(9)] for _ in range(9) ] var_dict = self.__var_dict for i in range(9): for j in range(9): for k in range(9): # 変数インスタンスを取り出し、インデックス+1をして解答用の配列にセットする variable: pywraplp.Variable = var_dict[(i, j, k)] val = variable.solution_value() if val != 0: answer_array[i][j] = k+1 break # 解答用の配列をセットする self.set_answer_array(answer_array) else: print("Couldn't optimize") # endregion calculate 記事執筆者 小澤 陽介 パーソルキャリア株式会社 テクノロジー本部 デジタルテクノロジー統括部 デジタルビジネス部 アナリティクスグループ 前々職は半導体製造会社にて、リソグラフィ工程のプロセスエンジニアリングに従事。前職では機械系メーカーR&Dにて数理最適化のアプリケーション開発及び工程可視化に従事。パーソルキャリアに入ってからはアナリストとして、主に生成系AIに関連するアプリケーション開発や検証に従事。意思決定の為の計算機開発にも携わっている。
アバター
今回のテーマ データベースの開発や運用において、サービスが成長していくのは嬉しいですが、同時にシステムがその成長に対応できるように常に改善していかなければなりません。特に、「本来起こってはいけないデッドロックが発生した場合の対処方法」を共有しておくことは、問題が起きた時に役立つと思います。 そこで、今回は「 MySQL(InnoDB)でバルクインサート時にデッドロックが発生した場合に検討するべきこと 」について、お届けします。 今回のテーマ 結論 デッドロックの原因 デッドロックとは ギャップロックとは 対応策1: バルクインサートの順番を整理する 対応策2: トランザクション分離レベルを緩和する 注意点 まとめ 記事執筆者 結論 ギャップロックが発生しないよう、バルクインサートの順番を整理する 上記でも解消しない場合、影響を考慮しつつトランザクション分離レベルを緩和することを検討する デッドロックの原因 デッドロックとは デッドロックの概念に関しては、以下の記事がわかりやすく解説してくださっているので、説明はこちらに譲ります。 デッドロックとは こちらにも書いてある通り、 お互いがロック解除待ち状態となり、どちらも処理が進行できなくなる状態 が発生した場合、デッドロックの状態となります。ロックにも種類があり、行ロック(レコードロック)やテーブルロックなどがありますが、今回取り上げたいのは ギャップロック です。 ギャップロックとは こちらも説明は以下の記事に任せます。 良く分かるMySQL Innodbのギャップロック - Qiita こちらにもある通り、ギャップロックとは インデックスとインデックスの間にかけられるロック のことです。なぜ数あるロックのうちギャップロックを特別に取り上げたのかというと、今回のテーマであるバルクインサートと関連が深いからです。 上記の記事では、SELECT FOR UPDATEをインデックスの間で行うとギャップロックが取られ、その範囲にはINSERTが行えなくなる事例を取り上げていました。SELECT FOR UPDATEで明示的にロックを取得しているので、こうした事象が起こるのは納得が行きますよね。ただ、これが バルクインサートのように一見ロックを取得しに行っていないように見える処理においてもギャップロックが起こりうる のが注意点です。 対応策1: バルクインサートの順番を整理する まずバルクインサートでギャップロックが起こるメカニズムを紹介している、下記の記事に目を通していただけたらと思います。 MySQLのBULK INSERTでデッドロックを回避する - Qiita 端的に言うと、バルクインサートの順番をあべこべにした結果、ギャップロックの取り合いが発生しているということです。ただ、逆に言えばインサート順をインデックスに沿う形にすることで、ギャップロックの発生を抑制することが可能です。もしかしたら今手元で発生している問題は、こちらの方法で解決するかもしれません。 Goで書くと、以下のようにバルクインサートに使うスライスの中身を昇順にする対応が可能です。 package main import "sort" type user struct { ID int Name string Age int } func main() { users := []user{ { ID: 1 , Name: "Tom" , Age: 20 , }, { ID: 2 , Name: "Alice" , Age: 23 , }, { ID: 3 , Name: "Bob" , Age: 19 , }, } // バルクインサート時にギャップロックによるデッドロックが発生しないよう昇順にソート sort.Slice(users, func (i, j int ) bool { a, b := users[i], users[j] return a.ID < b.ID }) } 下はPythonでの実装例です。 class User : def __init__ (self, id : int , name: str , age: int ) -> None : self.id = id self.name = name self.age = age users = [ User( 1 , 'Tom' , 20 ), User( 2 , 'Alice' , 23 ), User( 3 , 'Bob' , 19 ) ] ### バルクインサート時にギャップロックによるデッドロックが発生しないよう昇順にソート users.sort(key= lambda user: user.id) 対応策2: トランザクション分離レベルを緩和する 上記の対応でも解消しなかった場合、次の手段としてトランザクション分離レベルを緩和することが考えられます。トランザクション分離レベルに関しては下記の記事がわかりやすいと思います。 トランザクション分離レベルについてのまとめ - Qiita ダーティリード、リピータブルリード、ファントムリードをちゃんと理解してからトランザクション分離レベルを理解しよう - Qiita MySQL(InnoDB)のデフォルトのトランザクション分離レベルは REPEATABLE READ です。なぜトランザクション分離レベルの緩和を検討する必要があるかというと、 REPEATABLE READはインサート処理においてデフォルトでインデックス間のギャップロックを取得する仕様となっている からです(Oracle, PostgreSQL, SQL Server等はデフォルトのトランザクション分離レベルがREAD COMMITTEDとなっています。)。端的に言うと、 ギャップロックを取得しないようにしよう 、と言うわけです。今回はInnoDBを例に取って対応策を検討してみたいと思います。 デッドロックが発生したDB(MySQL)内のターミナル上で以下のコマンドを実行します。 SHOW ENGINE INNODB STATUS\\G 出力結果は以下の通りです( MySQLドキュメント からの抜粋です)。 ~~ RECORD LOCKS space id 31 page no 3 n bits 72 index `PRIMARY` of table `test`.`child` trx id 8731 lock_mode X locks gap before rec insert intention waiting Record lock , heap no 3 PHYSICAL RECORD: n_fields 3 ; compact format; info bits 0 0 : len 4 ; hex 80000066 ; asc f;; 1 : len 6 ; hex 000000002215 ; asc " ;; 2: len 7; hex 9000000172011c; asc r ;;... ~~ SHOW ENGINE INNODB STATUS に関しては、以下の記事が参考になると思います。 なぜあなたは SHOW ENGINE INNODB STATUS を読まないのか - そーだいなるらくがき帳 お気づきかもしれませんが、 RECORD LOCKS space id 31 page no 3 n bits 72 index PRIMARY of table test . child trx id 8731 lock_mode X locks gap before rec insert intention waiting と書かれているように、ギャップロックを取得しようとしてロック待ち状態になっているのがわかると思います。インデックスの順番通りにバルクインサートしているのにも関わらず、デッドロックが回避できない、となった場合は、繰り返しになりますが、トランザクション分離レベルの緩和を検討してみると良いと思います。 それでは、GoとPythonそれぞれを例に挙げて対応策を紹介したいと思います。 Go(Gorm) package main import ( "database/sql" "gorm.io/gorm" ) type SQLHandler struct { conn *gorm.DB } func (handler *SQLHandler) BeginTx(opts ...*sql.TxOptions) *gorm.DB { var opt *sql.TxOptions if len (opts) > 0 { opt = opts[ 0 ] } return handler.conn.Begin(opt) } func doSomething(tx *gorm.DB) error func (handler *SQLHandler) DoSomething() { // トランザクションの開始 // バルクインサート時のギャップロック発生を抑制するため、トランザクション分離レベルをデフォルトのREPEATABLE READからREAD COMMITTEDに変更 tx := handler.BeginTx(&sql.TxOptions{Isolation: sql.LevelReadCommitted}) // データベース上で何かしらの処理を実行 err := doSomething(tx) defer func () { if err != nil { tx.Rollback() } else { tx.Commit() } }() } 実装例を挙げると上記のようになると思います。 tx := handler.BeginTx(&sql.TxOptions{Isolation: sql.LevelReadCommitted}) のように、オプション用の構造体にトランザクション分離レベルをフィールドで指定したものを引数に渡せば良いです。ここで作られたトランザクションが影響する処理は、全てトランザクション分離レベルがREAD COMMITTEDとなり、バルクインサートによるギャップロックを抑制できます。 Python(SQLAlchemy) from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker connection_string = f "mysql://{DB_USERNAME}:{DB_PASSWORD}@{DB_ENDPOINT}:{DB_PORT}/{DB_DBNAME}?charset=utf8mb4" engine = create_engine( connection_string, isolation_level= "READ COMMITTED" , ) factory = sessionmaker(bind=engine) session = factory() create_engine関数の引数に isolation_level="READ COMMITTED" を追加するのみの対応で大丈夫です。Go(Gorm)の例と同様、ここで作られたセッションが影響する処理は全てトランザクション分離レベルがREAD COMMITTEDとなり、バルクインサートによるギャップロックを抑制できます。 注意点 では 最初からトランザクション分離レベルを緩和すれば良いのでは? と思うかもしれませんが、緩和する際には注意点があります。それは、 トランザクション分離レベルをREPEATABLE READからREAD COMMITTEDに緩和することによって、ファジーリード(ノンリピータブルリード)とファントムリードを許容している と言う点です(*1)。 例えば、ファジーリードによってトランザクション実行中でも他のトランザクションがコミットした特定のレコードに対する変更結果は即時反映されることになります。対象として扱うレコードが互いに独立している場合は問題ないですが、 例えば同じレコードの値を参照して集計などを行いたい場合は、他のトランザクションのコミット結果の影響を受けてしまう ことになります。 トランザクション分離レベルを緩和する際は、影響を受ける処理がその緩和により許容できるものかどうかの検討を慎重に行う必要があります。まずはバルクインサートの順番を整理し、それでもデッドロックの解消が見込めない場合の代替案として検討するのが良いと思います。 *1: REPEATABLE READにおいても、ファントムリードは許容されています。 まとめ 今回は「バルクインサートでデッドロックが発生した場合に検討するべきこと」という題目で記事を書かせていただきました。サービスがスケールするに従って予想だにしなかった箇所がボトルネックとなり障害が発生することは少なからずあると思います。その際にこちらの記事が対応策立案の一助になれば幸いです。 記事執筆者 梅本 誠也 パーソルキャリア株式会社 デジタルテクノロジー統括部 生成系AIエンジニアリング部 生成系AIエンジニアリンググループ リードエンジニア 韓国で5年間正規留学し、その間に業務委託で機械学習とデータエンジニアリング方面の開発を経験。新卒でアプリケーションエンジニアとしてフロントエンド、バックエンド、インフラを幅広く経験。パーソルキャリア入社後はデータエンジニアとして、社内のデータ分析基盤の構築と運用保守を担当。一方で、生成系AIを用いたアプリケーション開発にも携わっている。
アバター