TECH PLAY

セーフィー株式会社

セーフィー株式会社 の技術ブログ

227

はじめに こんにちは。エンジニアリングオフィスの山崎(@ymzaki_m4)です! 開発組織の進化を支える幅広い活動をしている私たちですが、ふと気づくことがありました。 「私たち自身も、一つのチームとして成長し続ける必要があるのでは?」 そこで着目したのが「振り返り」の手法です。2024年から現在まで、私たちは振り返りの手法を進化させ続けてきました。特に2025年は 「感情」 を振り返りに組み込むことで、チームの心理的安全性が高まり、より建設的な議論ができるようになった実感があります。 この記事では、エンジニアリングオフィスが実践してきた振り返りの進化の過程と、そこから得た学びをお伝
アバター
はじめに こんにちは! セーフィーの開発本部エンジニアリングオフィスで26卒内定者インターンをしている吉田・水野・小石川です。 今回はインターン活動の一環で2025/11/15(土)に開催された「BTCONJP2025」というカンファレンスに私たちインターン生がゼロからWebアプリを作成し展示させていただいたので、その技術的挑戦と当日のトラブル対応について振り返りたいと思います。 当日の写真 導入・背景 自己紹介 吉田🧖)開発本部で内定者インターンをしている吉田和司です!結構多趣味で最近はドローンに興味を持って資格を取ったりしました。他にもサウナが大好きで、この前は名古屋の
アバター
はじめに こんにちは!セーフィー株式会社でサーバーサイドエンジニアをしている坂上(@Bobtaroh)です。 セーフィー開発本部では2025年9月に「生成AI利用推進プロジェクト」を立ち上げ、20名以上のメンバーが自発的に参加し、以下の5つの軸で活動しています。 情報収集活性化 技術検証・導入 ナレッジDB管理 生成AI利用活性化 ←今回はここ! 社外発信 プロジェクトの詳細はこちらの記事をご覧ください。 今回は「生成AI利用活性化」の一環として実施したClaude Codeライブコーディングについてご紹介します。 ライブコーディングイベントについて フロントエンドエンジニア
アバター
はじめに こんにちは。情報システムグループの後藤です。 今回は、社内で長らく課題となっていた「オフィス無線LANの接続不安定問題」に対し、内製での調査・切り分けと、外部ベンダーによるサイトサーベイを組み合わせて改善まで導いたプロジェクト全貌をご紹介します。 「Web会議が頻繁に切れる」「特定のエリアだけ通信が遅い」……。 多くの企業で聞かれる悩みと思いますが、弊社も例外ではありませんでした。 今回、どのような調査・切り分けを実施し、解決に至ったのか。 具体的なデータとともに振り返ります。 ネットワーク設計・運用に関心のある方やコーポレートインフラで同様の悩みを抱えている方にとって参考
アバター
はじめに こんにちは!セーフィー株式会社でデバイス開発をしている杉本です。 前回はコンセプトの立案からプロトタイプのユーザー検証まで行うプロト開発WGの活動を紹介しましたが、今回は自分の職務であるハードウェア開発プロジェクトマネージャー(以下、HW開発PM)について紹介させていただきます! ハードウェア開発プロジェクトマネージャーとは セーフィーにおけるHW開発PMという職種は、ハードウェアエンジニアと開発PMの両方の役割を担っています。 ハードウェアエンジニア 開発パートナーと連携して、設計、性能評価、信頼性試験、法規制対応、量産品質の確保までハードウェア開発を推進する
アバター
こんにちは、セーフィー株式会社 開発本部 テックブログ編集長の土井です。 これまで「Safie Engineers' Blog」として、はてなブログにて技術発信を行ってきましたが、この度、 テックブログのプラットフォームを「Zenn Publication」へ移行いたしました。 新しいブログはこちらになります。ぜひフォローをお願いします! zenn.dev なぜZennへ移行するのか? 1. ブログコンテンツを「エンジニア個人の集まり場」へ 2. 運用負荷を下げ、中身に集中できる体制へ 過去の記事とURLについて これからの「Safie Engineers' Blog」 なぜZennへ移行するのか? 今回の移行には、大きく2つの理由があります。 1. ブログコンテンツを「エンジニア個人の集まり場」へ これまで、記事はあくまで「会社の発信」としての側面が強かったのですが、今後は 「エンジニア個人のアウトプット」 の集合体としての側面もより大切にしたいと考えています。 Zenn Publication機能では、執筆した記事が会社のページだけでなく、執筆者個人のZennアカウントにも紐付きます。これにより、社外への発信がそのままエンジニア自身のポートフォリオや実績(資産)として蓄積されるようになります。 エンジニアが自分の言葉で語り、それが個人のキャリアやブランディングにも繋がる。そんなWin-Winな関係を目指しています。 2. 運用負荷を下げ、中身に集中できる体制へ テックブログの運営は、社内の有志メンバーによる少人数体制で行っています。 はてなブログはカスタマイズ性が高く、表現の自由度が非常に高い素晴らしいプラットフォームです。その反面、現在の私たちの体制では、記事ごとのトンマナ(統一感)を保つためのデザイン調整やサムネ、URL、カテゴリ、概要など様々な確認作業に多くのコストがかかっていました。 特にアドベントカレンダーなど、記事の公開ペースが速いタイミングでは、運営メンバーが1記事ずつ手作業でレイアウト調整や装飾を行う負荷が高まっていました。 Zennは、記事のレイアウトや装飾があらかじめ統一されています。 「誰が書いても、読みやすい記事になる」 ため、運営側での見栄えの調整作業が不要になります。 また、今回はあえてGitHub連携などは行わず、ZennのWebエディタ上で完結する運用を選びました。 Zennには標準で強力な「レビュー機能」が備わっており、プレビューを見ながらコメントや修正提案が可能です。エンジニアだけでなく、デザイナーや広報など、Git操作に慣れていないメンバーも含め、誰でも簡単に執筆・レビュー・公開ができる「シンプルで使いやすい体験」を重視しています。 過去の記事とURLについて これまで、はてなブログで公開してきた記事については、引き続きアーカイブとして公開を継続します。 旧ブログ: Safie Engineers' Blog (Hatena) (※ https://safie.hatenablog.com/ ) ただし、執筆者本人が自身のZennアカウントでの公開を希望する場合や、現在も参照頻度が高い人気記事などについては、一部Zennへの移行(転載やアップデート)も検討しております。 なお、旧ブログへのアクセスについてはリンク切れが起きないようリダイレクト処理等の準備を進めておりますが、今後はZennでの発信がメインとなります。 これからの「Safie Engineers' Blog」 新しいZenn Publicationでの 記事公開は、近日中に開始する予定です。 運用コストを下げ、記事作成のハードルを下げた新しい環境で、セーフィーのエンジニアたちが日々直面している課題や、その解決策をリアルタイムにお届けしていきます。 映像データ処理の技術 エッジAI・IoTデバイス開発 開発組織のカルチャー など、多岐にわたるトピックを発信していきますので、楽しみにお待ちください。 今後とも、セーフィーのエンジニアリングチームをよろしくお願いいたします。 zenn.dev safie.co.jp はじめまして!このアカウントは、セーフィーの技術情報やエンジニアの取り組みを発信するアカウントです🎉 エンジニアの皆さんが気になる情報をお届けします。 ぜひお気軽にフォローしてください! #セーフィー #エンジニア #開発者 #技術広報 — セーフィー技術広報 (@SafieDev) 2025年11月7日
アバター
こんにちは、サーバサイドエンジニアの伊林です。 この記事では、直近にセーフィーで行ったOSS活動について共有します。 セーフィーでのOSS活動について セーフィーのバックエンドは、主にPython、Go、Javaで実装されており、多くのOSSの力を借りてシステムが成り立っています。OSS開発者の方々には日々、感謝の正拳突きが止まりません。 自分はOSS活動の経験は浅いですが、チーム内でOSS活動を積極的に取り組んでいこうという目標設定をたてたこともあり、いくらか取り組んでみました。 また、自分以外の方が取り組んだ内容についてもせっかくなので合わせて紹介しようかと思います。 ※ OSS
アバター
こんにちは!エンジニアリングオフィスの横道( @m_yokomichi )です。 セーフィーでは、AI活用を開発生産性向上の重要な戦略と位置づけ、組織全体で活用を推進しています。その第一歩として、Claude Code座学勉強会を開催しました。基本的な使い方から今後の展開まで、取り組みのご紹介させていただきます。 はじめに:勉強会開催の背景 利用方法がわからないという課題 勉強会の概要 勉強会の内容:3つのポイントと一部機能紹介 ポイント1: AI開発ツールの使い分け ポイント2: 5分で始める導入方法 ポイント3: 基本的な使い方とTips 一部機能紹介: Claude Code Skills 参加者の反応:アンケート結果 満足度: 平均4.2点 / 5点 今後知りたいテーマ 参加者の声 今後の展開:ハンズオンと活用促進 次のステップ:実践の場を増やす 継続的な情報共有 まとめ:組織全体で取り組む生成AI活用 はじめに:勉強会開催の背景 利用方法がわからないという課題 セーフィーでは、Claude Maxプランを配布しており、Claude Codeの利用が可能になっています。誰でも使える状態ですが、「使ってみたいけど、どう使えばいいかわからない」という声が多く聞かれました。 GitHub Copilotは利用が進んでいる一方で、Claude Codeについて調査してみると、以下の理由で利用が進んでいない状態ということが分かりました。 他のAIツールとの違いがわかりにくい どんな場面で使うべきかわからない 基本的な設定方法が不明 こうした課題を解決し、利用のハードルを下げるため、座学形式の勉強会を企画しました。Claude Codeの特徴やメリットを理解してもらい、まずは「使い始める」きっかけを作ることが目的です。 なお、この勉強会の企画は、生成AI利用促進プロジェクトの一環で実施しており、そのプロジェクトの全体像については別記事で紹介させていただきます。 勉強会の概要 形式 : オンライン座学形式(30分) 対象 : 開発者全員 目的 : Claude Codeの基本理解と導入支援 「まだ使えていない人」が安心して第一歩を踏み出せるよう、基礎から丁寧に解説する内容としました。 勉強会の内容:3つのポイントと一部機能紹介 勉強会では、以下の3つのポイントを中心に解説しました。 ポイント1: AI開発ツールの使い分け 開発現場には様々なAIツールが登場していますが、それぞれに得意・不得意があります。勉強会では、 4つの開発手法 (コード支援型、Vibe Coding、Agentic Coding、スペック駆動)を整理し、それぞれの特徴と使い分けを解説しました。 Claude Codeは「Agentic Coding」型のツールで、コードベース全体を理解して複数ファイルを横断した作業が得意です。「細かいコード補完」ではなく、「複雑なタスクを自律的に処理する」のが特徴で、テスト実行とエラー修正を自動で繰り返すこともできます。 ポイント2: 5分で始める導入方法 「すぐに始められる」ことを重視し、導入手順を簡潔に紹介しました。 3ステップで始める Claude Maxプランの利用申請 社内ガイドラインに従って申請 インストール Homebrew:  brew install anthropics/claude/claude curl: 公式サイトからスクリプト実行 NPM:  npm install -g @anthropic-ai/claude-code 初期設定 claude コマンドでログイン API認証を完了 /init コマンドで CLAUDE.md を作成 このステップを進めるだけで、プロジェクトの構造を分析して CLAUDE.md ファイルを自動生成してくれます。このファイルがあることで、Claude Codeの応答精度が大きく向上します。 始める手間が少ないことで利用ハードルが低いことを伝えています。 ポイント3: 基本的な使い方とTips 実際にどんなタスクができるのか、具体例を示しました。 できること プロジェクト概要の把握(「このプロジェクトは何?」) ディレクトリ構造の理解(「構造を説明して」) コード変更(「○○機能を追加して」) Git操作(「コミットメッセージを考えて」) より良い回答を得るためのTips 具体的に指示する : 「バグを修正して」→「ログイン時に空白画面になるバグを修正して」 段階的に指示する : 複雑なタスクは分割する Planモードを活用 : 探索・計画してから実装 注意点 AIに全てを任せず、最終判断は人間が行う コマンド実行は都度確認する 機密情報の取り扱いに注意 Claude Codeの基本的な使い方を説明することで、何ができるか知ってもらい、利用イメージを膨らませていただけるようにしました。 一部機能紹介: Claude Code Skills 勉強会では、Claude Codeをより活用できる Skills についても紹介しました。 基本情報だけではなく、活用事例を紹介することによって色々できることを知ってもらい、より興味を持ってもらうことが狙いです。 参加者の反応:アンケート結果 勉強会後のアンケートから、参加者の反応を紹介します。 満足度: 平均4.2点 / 5点 高い満足度をいただき、「まだ使えていない人」に寄り添った内容が評価されました。 今後知りたいテーマ 知りたいという声が多かったものをいくつか紹介します。 Claude Code 使い方の深掘り Claude Codeの機能 GitHub Copilotの機能 プロンプトの書き方 チームでの利用方法 開発本部での活用事例 参加者の声 「Claude Code Skills は興味深かったです」 「すごくわかりやすかったです!とりあえずClaude 利用申請を今から開始しようと思います」 「開発における生成AIの使い分けがわかりやすかったです!複数組み合わせることもテクニックとして紹介されていたので今後活用してみようと思いました」 「勉強会がこれから導入する人向けの話になっていたので、全然使えてなかった私からしたらとても分かりやすくて助かりました」 特に印象的だったのは、「これなら自分にもできそう」という前向きな反応でした。 一方で「座学だけでは物足りない」といった声もあったので、ハンズオン形式で体験しながら理解するコンテンツも企画しようと思います。 今後の展開:ハンズオンと活用促進 次のステップ:実践の場を増やす 今回の座学勉強会で「基本的な理解」は深まりましたが、「実際に使ってみないとわからない」という課題も浮き彫りになりました。 そこで、今後は以下のような活動を計画しています: 1. ライトニングトーク会 Claude Code を活用できている人による実例紹介 具体的なユースケースの共有 2. ハンズオン形式の勉強会 実際にClaude Codeを操作しながら学ぶ 困ったときのトラブルシューティング 4. ナレッジデータベースの構築 活用事例の蓄積 Tips・プロンプト集の整備 よくある質問(FAQ)の作成 継続的な情報共有 勉強会だけではなく、Slackでの情報発信なども発信し、継続的な学びと実践の場を提供していきます。 まとめ:組織全体で取り組む生成AI活用 今回の勉強会は、セーフィーが進める生成AI活用推進の第一歩でした。 生成AIは「使える人だけが使う」のではなく、「組織全体で戦略的に活用する」ことが重要です。 座学形式で基本を学び、アンケートで今後のニーズを把握し、ハンズオン形式へと発展させる。こうした段階的なアプローチで、全員が生成AIを活用できる環境を整えていきます。 「わからない」から「使える」へ。 そして「使える」から「使いこなす」へ。その最初の一歩を、一緒に踏み出せたことを嬉しく思います。
アバター
はじめに こんにちは。セーフィーでプロダクト開発をしています大町です。 最近は、ChatGPT → Gemini → Claude →(たまにGrok)のように、利用するLLMサービスを気分に応じて使い回しています。 最近、次々とLLMの新しいモデルが出ていますが、以下のようなことを思う時があります。 もともとChatGPTを使っていたけどGeminiで性能のいいモデルができたので乗り換えたい アイデア出しはChatGPTで、調べ物はGeminiでといったように使い分けたい → でもサービスを変えるとコンテキストがなくなるので使いづらい そんなエンジン(LLM)だけ変えたいけどデータは変わらないままにしたい!という要望を解決するためのシステムをプロトタイプとして作って社内の勉強会で発表しました。このブログではその内容を共有したいと思います。 はじめに 今回紹介するシステムの概要 DB設計 MCPツールの実装 デモ 課題とその解決策 終わりに 今回紹介するシステムの概要 システムを一言で説明すると、自前でデータ基盤(DB)を用意して、そのデータ基盤に対して各LLMがMCPツールを使ってチャットのデータを入れたり、または必要に応じてとってきたりする、というものです。 MCPは、LLMが外部ツールを共通のインターフェースで呼び出すための仕組みで、今回は複数のLLMから同じデータ基盤を扱うための接点として利用しています。 以下がシステムのアーキテクチャ図です。 登場人物は次の3つです LLMサービス : ChatGPTやClaudeなど。MCPに対応していれば何でもOKです。 DB : 今回は手軽なSQLiteを採用しました。 MCPサーバー : 今回のシステムの核となる部分です。 DB設計 今回のシステムでは、LLMがMCPツールを使ってユーザーの回答に応じてDBから情報をとってきます。その時に、文章の意味を考慮する必要があるので、生のテキストに加えて文章の埋め込みベクトルもDBに保存するようにしています。 erDiagram entries { INTEGER id PK "自動増分" TEXT text "保存するメモ内容" TEXT source "発言者 (デフォルト: unknown)" TIMESTAMP created_at "作成日時" INTEGER dim "ベクトルの次元数" BLOB embedding "ベクトルデータ" } MCPツールの実装 ここが今回のシステムの中心になります。 今回はミニマム実装ということで、MCPのツールとしてingestとrecallというツールを定義しました。 ingest … データを保存する ユーザー入力時にLLMがデータをDBに入れるためのツールです。 以下がシーケンス図です。ユーザーの発言をベクトル化してDBに保存します。 sequenceDiagram autonumber participant User as ユーザー participant LLM as LLM (Claudeなど) participant API as 共通API (MCP) participant DB as データベース Note over User, DB: 【Ingest】記憶を保存する User->>LLM: 「昨日の夕飯はカレーだった」と発言 Note right of LLM: ユーザーの意図を汲み<br/>保存ツールを選択 LLM->>API: ingest(text="昨日の夕飯は...", source="you") activate API Note right of API: テキストをEmbedding<br/>(ベクトル化) API->>DB: INSERT (テキスト, ベクトル, 作成日時) DB-->>API: 保存完了 (ID返却) deactivate API LLM-->>User: 「保存しました」と応答 コード @ mcp.tool () async def ingest (text: str , source: str = "you" ) -> dict : """ メモを保存するツール。 ユーザーが「覚えて」「メモして」などと依頼したときに利用する。 入力テキストをベクトル化してデータベースに登録する。 """ vec = embed([text])[ 0 ] # 正規化済み db.insert_entry(text=text, source=source or "unknown" , vec=vec) return { "text" : text, "source" : source} vec = embed([text])[0] の部分で与えられたテキストに対応する埋め込みベクトルを作っています。埋め込みベクトルとは、単語・文や画像などのデータを意味や関係性を捉えた数値のリスト(ベクトル・座標)です。 意味や関係性の情報を保持するので、例えば、犬と猫は近くて犬とコンピュータは遠くなります。 embedの中身は以下です。学習済みのモデルを使ってテキストをベクトル化し、正規化までしています。 import os import numpy as np from sentence_transformers import SentenceTransformer MODEL_NAME = os.getenv( "EMBED_MODEL" , "sentence-transformers/all-MiniLM-L6-v2" ) _model = SentenceTransformer(MODEL_NAME) def embed (texts): vecs = _model.encode(texts, normalize_embeddings= True ) return np.asarray(vecs, dtype=np.float32) recall … データを検索する ユーザーが質問した際に、DBにあるデータを検索し、レスポンスするツールです。 シーケンス図:ユーザーの質問文をベクトル化するところまでは ingest と同じです。質問文のベクトルと、DBに格納されたデータのベクトルとの類似度を計算し、検索を行います。 sequenceDiagram autonumber participant User as ユーザー participant LLM as LLM (ChatGPTなど) participant API as 共通API (MCP) participant DB as データベース Note over User, DB: 【Recall】記憶を検索・共有する User->>LLM: 「昨日の夕飯なに?」と質問 Note right of LLM: 質問に答えるために<br/>検索ツールを選択 LLM->>API: recall(query="昨日の夕飯なに?") activate API Note right of API: クエリをベクトル化し<br/>全データと類似度計算 API->>DB: 全エントリのベクトル取得 DB-->>API: データ返却 API->>API: 類似度が高い順に抽出<br/>(閾値以下は除外) API-->>LLM: 検索結果 (JSON: "昨日の夕飯はカレー...") deactivate API LLM-->>User: 「昨日の夕飯はカレーでした」と回答 コード @ mcp.tool () async def recall (query: str ) -> dict : # 設定値 limit = 5 # 最大5件返す min_score = 0.35 # 類似度の下限値。これを下回った場合は返さない。 rows = db.fetch_all_entries() if not rows: return { "query" : query, "hits" : []} q = embed([query])[ 0 ] # DBの全ベクトルを展開 vecs = np.vstack([db.from_blob(r[ "embedding" ], r[ "dim" ]) for r in rows]) # 内積計算(正規化済みなのでコサイン類似度) scores = (vecs @ q).astype( float ) idx = np.argsort(-scores) hits = [] for i in idx[:limit]: s = float (scores[i]) if s < min_score: break r = rows[i] hits.append({ "id" : r[ "id" ], "text" : r[ "text" ], "source" : r[ "source" ], "created_at" : str (r[ "created_at" ]), "score" : s, }) return { "query" : query, "hits" : hits} ingestより処理が長くなっていますが、処理の内容としては質問文とDBにあるデータの類似度を計算し、類似度の高いものをレスポンスしています。 scores = (vecs @ q) で、質問文のベクトル q と全データのベクトル行列 vecs の積を取ることで、一度に内積(コサイン類似度)を計算しています。 今回はプロトタイプなのでNumpyで全件計算していますが、プロダクトとしてスケールさせるなら sqlite-vss などのベクトル検索拡張を使うのが良いでしょう。 デモ 実際に動かしてみました。Claudeでingestして、ChatGPTでrecallしています。 ingest by Claude recall by ChatGPT 課題とその解決策 実際に使ってみて感じたのですが、検索のところが難しいということがわかりました。 いくつか課題とその解決策を挙げてみました。 いつの「昨日の夕飯」かわからない問題 課題: ベクトル検索は意味の近さを探すため、「日時」などのメタ情報を考慮しません。そのため、「昨日の夕飯」と聞いているのに、文脈が似ていれば1年前のデータを取ってくる可能性があります。 解決策: recall ツールの引数に date_range などのフィルタ条件を追加し、LLM側で「昨日=2025-10-17」のように具体的な日付に変換して渡してもらうことで解決できそうです。 「質問」と「答え」の形が違う問題 課題: ユーザーの質問「昨日の夕飯は何?(疑問形)」と、DB内の記録「昨日の夕飯はカレー(平叙文)」では、文章のベクトルが必ずしも近くならない場合があります。 解決策: 単純な類似度検索だけでなく、LLMに「この質問に対する答えとしてありそうな文章」を仮想的に生成させてから検索にかける(HyDE: Hypothetical Document Embeddingsのようなアプローチ)か、データ保存時に「どんな質問に対する答えか」も一緒に保存するなどの工夫が考えられます。 終わりに 今回のシステムは、LLMのサービスを変えてもコンテキスト(データ)を維持できるようにするためのシステムでした。LLMは素晴らしい頭脳(モデル)を持っていますが、あくまでも一般的な知識に基づいたものでよりよく使うためには個別のコンテキストを与える必要があります。LLMのサービス側でコンテキストを持つことでその問題を解決していますが、そのことによりベンダーロックインも発生します。 今回のシステムではLLMサービスが提供しているモデルとデータを切り離すことで、モデルに関しては置き換え可能にし、データは自分の元で一元管理することで自由に閲覧・操作できる構成を考えました。 今回作ったのはプロトタイプでそのままでは使い物になりませんが、MCPやRAGを使ったシステムの1つのアイデアとしてみていただければ幸いです。
アバター
セーフィーで情報システムを担当している松尾です。 先日開催された「 BTCONJP 2025 (Business Technology Conference Japan 2025)」に参加してきました。 今回は、当日の現地の様子や、実際にセッションを聞いて感じた、これからの情報システムの役割についてレポートしたいと思います。 また、今回は個人的な事情で託児サービスを利用しての参加となったのですが、そこで自社のプロダクトを実際に体験することになり、非常に感慨深い1日となりました。そのあたりも含めてお伝えできればと思います。 会場の熱気と「人数カウント」 託児サービスで感じた「映像の力」とユーザー体験 セッションレポート AI前提の業務「再構築」と現場と共にあること 情シスは「橋渡し役」へ 情報システムの未来に向けて 会場の熱気と「人数カウント」 当日はセーフィーもスポンサーとしてブース出展を行いました。 今回は単なる展示だけでなく、セミナールームや会場内にセーフィーのカメラを設置させていただき、 リアルタイム人数カウント を実施しました。 会場での配信の様子 「今、どのエリアにどれくらいの人がいるのか?」 「このセッションには何人集まっているのか?」 これらを映像データから可視化することで、カンファレンスの熱量を定量的に見ることが出来ます。自社の技術がイベント運営の裏側でどう活きるのかを実際に見ることが出来て、とても興味深い体験でした。 また、会場内はブースを回ると記念品が貰える「スポンサーブース スタンプラリー」、参加者同士の交流を促す「情シスビンゴチャレンジ」、お悩みを相談できる「情シスカフェ&お悩み相談」など、参加型のコンテンツが沢山あり、コミュニティとしての一体感を強く感じるイベントでした。 託児サービスで感じた「映像の力」とユーザー体験 今回の個人的なハイライトの一つが「託児サービス」です。 預け先の都合がつかず、今回は子供と一緒に会場に入り、WAKE Career様がスポンサーをされている託児サービスを利用させていただきました。 この託児サービスではセーフィーのカメラが利用されており、保護者は自分のスマホから託児の様子を確認できるようになっています。子供たちの顔には自動的に 動物のスタンプ が追従して表示されるため、プライバシーを守る事ができます。 これにより、自分の子供や他のお子さんのプライバシーはしっかり守られつつ、 「あ、楽しそうに遊んでるな」 「保育士さんと一緒にお弁当を食べているな」 といった様子をリアルタイムで確認することが出来ました。 当日の託児の様子 初めての場所、初めての保育士さんで不安もありましたが、セミナーの合間にスマホから子供の状況を確認できたことで、安心してイベントに参加することができました。 自社のプロダクトの価値をいちユーザーとして感じることができたことも貴重な経験でした。 今回の出展は26年新卒のメンバーも参加していました! セッションレポート ここからは、聴講したセッションについての学びをまとめます。 BTCONJPのテーマは名前の通り、「ビジネステクノロジー(BT)」であり、ITを単なるツールとして扱うのではなく、「ビジネスを加速させ、競争力を生み出す源泉」とするものだと考えています。 皆様のセッションはどれも素晴らしく非常に学びになるものでしたが、その中でも「ビジネステクノロジー」の観点から、自分たち情報システムの立ち位置や今後を考えるうえで特に印象に残ったセッションを中心にレポートします。 AI前提の業務「再構築」と現場と共にあること AIの導入については現在多くの企業が進め方を検討していたり、すでに導入を進めている状態であると思います。 株式会社IVRyの植田 裕介さん、NOT A HOTEL株式会社の遠藤 祐介さんのAIに関連するセッションで感じたのは、 「今の業務フローのままAIを足すのではなく、AIを前提にプロセス自体を再構築せよ」 というメッセージでした。 今後は、情シスが自ら現場のビジネスプロセスに入り込み、 「そもそもこの業務は必要なのか?」「AIに任せるならどういうフローが最適か?」 を現場と一緒に考え、ゼロから作り直す動きが必要だと改めて感じました。 セッションの中では、AI を使って課題を解決するために、 社内を横断するチーム を立ち上げることが必要になるともお話されていました。 このような動きは、NTTドコモビジネス株式会社の小林 泰大さんと、株式会社マキタの高山 百合子さんのセッションで述べられていた、情報システムの中に留まるのではなく、積極的に外に出ていき現場の声を聞く、ということに繋がっていると感じました。 実際に上記のセッションでは、 直接現場と話しに行くことが重要であり、課題を共有し、共に手を動かすことで信頼関係が築ける と述べていました。 お互いに相手のせいにせず、情シス側も「自分事」として現場と一緒にビジネスを作り、加速していくことがと必要であると感じました。 情シスは「橋渡し役」へ 📢 情シスカンファレンス BTCONJP 2025 11/15(土) 開催 ✨ セッションのご紹介です! ──────────────────── SaaSを買わない未来 ― 作り出す情シスの挑戦 ──────────────────── 株式会社クラウドネイティブ 代表取締役社長 文部科学省… pic.twitter.com/9QXhIMYdOm — #BTCONJP Business Technology Conference Japan (@btconjp) 2025年10月8日 株式会社クラウドネイティブの齊藤 愼仁さんのセッションでは、情報システムの更に未来の話についても言及がありました。 SaaSの値上げが進む中、 「市民開発」 が進む未来においては、 今まではSaaSとして提供されていたアプリを従業員自身が作っていく流れが加速していく と思われます。 その中で、情報システムは SaaSを提供するだけの管理者ではなく、いかに現場の理解力を高めていけるか、伴走者になれるが重要であると 語られていました。 情報システムの未来に向けて 今回、様々な企業のセッションを聴講したなかで、登壇者の皆さんが語る結論は、立場や状況は異なりますが、同じ方向を向いていると感じました。 これからの情報システムは、 エンジニアリングを武器に、現場のビジネス課題を深く理解し、現場と共に事業を加速させるパートナーになる ことが求められていると改めて感じました。 独立行政法人 情報処理推進機構(IPA)産業サイバーセキュリティセンターの登 大遊さんのセッションでは、日本の労働人口減少とそれに伴う競争力の強化といったマクロな課題についても触れられました。 IPA 登大遊氏 ( @dnobori ) による基調講演のセッション内容が公開となりました! https://t.co/51GoG35Fre 情シスカンファレンス #BTCONJP 2025【11/15(土) 開催まであと11日!】 — #BTCONJP Business Technology Conference Japan (@btconjp) 2025年11月3日 セッションでは、現在の日本のデジタル産業を以下の3つに分類しており、日本は労働集約的で利益率の低い「(c) 活用支援(SIer等)」に偏っており、(a) と (b)が不足している状況であると述べられていました。 (a) デジタル基盤製品・サービス: OS・インターネット基盤・セキュリティ基盤・クラウド基盤等 (b) デジタル応用汎用製品・サービス: 汎用アプリ・汎用プラットフォームサービス・クラウド型 SaaS サービス等 (c) デジタル活用支援サービス: IT コンサル・受託ソフト開発・SIer 系 今後の日本で競争力を上げるためには、 (a) デジタル基盤製品・サービス と (b) デジタル応用汎用製品・サービス のような人数によってスケールしないビジネスや会社を作る必要があります。 セーフィーが目指す 「映像プラットフォーム」 は、この(a)と(b)のようなプラットフォームに位置づけられるような会社を目指しており、労働人口減少という社会課題の解決に対しても遠隔から現場をサポートするようなサービスを提供しています。 私たちがこの事業を強くし、世界で戦えるプラットフォームへと成長させることで、 「日本を強くする」 ことにも繋がっていけばいいなと、改めて思いました。 今回の学びを活かし、ビジネスを加速させる情報システムとして、既存の枠にとらわれずに今後もチャレンジしていきたいと思います。 運営の皆様、登壇者の皆様、会場でお会いした皆様、子供を預かってくださった保育士の皆様、本当にありがとうございました。
アバター
メリー・クリスマス、セーフィーCTOの森本です。 この記事は Safie Engineers' Blog! Advent Calendar 25日目の記事です。 一昨年 創業以来10年の開発組織の振り返り について掲載し、2年が経ちます。 ありがたい事に会社の事業、開発組織も順調に新しい取り組みを交えつつ大きく成長している事を実感している今日この頃です。 これもメンバーの皆さんの頑張りの賜物であると感謝しています。 そのおかげもあって、自分の時間を手を動かす事に使うことも少しずつ出来るようになってきましたので、今回は少し技術よりの内容について書く事にします。 はじめに MCPサーバー活用例 w/ Claude Desktop LLMとMCPサーバーのやり取り MCPサーバーを使用しない、従来のやり取り MCPサーバーを介する場合のやり取り MCPツールをカスケードで活用する場合 MCPツールの情報をLLMに設定するには まとめ 最期に はじめに 生成AI周りでは様々なモデルやそれにまつわるツールの急速な発展により、業務効率化、創造性の向上、情報アクセスの簡易化などの点に於いて、驚くべきスピードで多種多様な恩恵がもたらされているのは今更言うまでもありません。 もちろん当社でも早い段階から、一部メンバーが様々なツールの開発業務への活用やプロダクトへの応用検討を進めてくれていましたが、これを更に加速する為チームを組成し、 組織としての活用 を進めています。 また、これも言うまでもないですが、MCP(Model Context Protocol)により多種多様な外部サービスと柔軟に連携ができるようになり、ACP(Agent Communication Protocol)、A2A(Agent-to-Agent)などによりエージェント間のコミュニケーションも広がりを見せています。 一例として 当社社員が以下で紹介 してくれていますが、柔軟なテキストベースのインプットによりサービスの制御が可能となっています。 MCPを活用すれば、LLMを経由して当社サービスを柔軟に制御する事が出来るだけでなく、他のサービスとの連携も非常に簡単に行えます。 上記記事にある通り、当社カメラから画像を取得しその要約をすることも簡単に出来ますし、要約した内容をインカムで通知すると言った他のサービスとの連携も簡単です。 例)インカムーSafie連携 今更ですが、一気にサービスの拡張性を伸ばしてくれる技術という事で強く可能性を感じています。 ただ、カスタムのMCPサーバーを活用する為にはClaude Desktopが必要なの(他にも候補はありますが)がちょっと勿体ないのと、内部的にLLMとMCPサーバーがどのようにやり取りしているのか知りたかったので、MCPサーバーを自前のサーバー上で制御できる簡易なサンプルを作り中身を見てみました。 MCPサーバー活用例 w/ Claude Desktop Claude Desktopでは入力されたプロンプトによって、柔軟にMCPサーバーが提供するツールが使えるかどうか判断し、必要に応じ適切にパラメータをセットし実行してくれます。 例えば以下のようなカスタムのMCPサーバーを準備して、Claude Desktopに組み込み(※このあたりの手順は省きます)、「こんばんはとあいさつしてね」とClaude Desktopのプロンプトを入力すると、MCPサーバーの提供する機能を適切に実行し、そのレスポンスを返してくれます。 from mcp.server.fastmcp import FastMCP from pydantic import BaseModel, Field class GreetingMessage (BaseModel): message: str = Field( None , description= "あいさつのメッセージ" ) # Create an MCP server mcp = FastMCP( "Greeting" ) @ mcp.tool () def greeting (message: str | None = Field( description= "あいさつのメッセージ" , default= None )) -> GreetingMessage: """ あいさつメッセージを返します """ return GreetingMessage(message=f "Hello, MCP! {message}" ) def run (): mcp.run() よく知られた挙動ですが、内部でどうやっているか見てみました。 LLMとMCPサーバーのやり取り MCPサーバーを使用しない、従来のやり取り ※自分なりの解釈が相当入っていますが、そこはご容赦ください。 ※llmプロキシとしてLiteLLMを利用し、モデルはclaude-sonnet-4 を使用しています。 最初に従来のLLMへのチャットメッセージは以下のようなリクエストとレスポンスで成立しています。 response = client.chat.completions.create( model=request.model, messages=messages, tools=mcp_manager.tools if request.use_mcp else None , max_tokens=request.max_tokens, temperature=request.temperature, ) messages = [ { "role" : "assistant" , "content" : "あなたは柔軟に入力内容を類推できるAIアシスタントです。質問への回答は出来る限り完結に回答してくれます。" }, { "role" : "user" , "content" : "こんばんはとあいさつしてね" } ] tools = [] ※LLMに出来る限り簡潔にレスポンスを返して欲しかったので、上記のようなアシスタントロールのメッセージを最初にセットしています。 すると以下のようなレスポンスを返してくれます。 ChatCompletion( id = 'chatcmpl-b49350d7-d24a-43c4-a2a3-77343982f33d' , choices=[ Choice( finish_reason= 'stop' , index= 0 , logprobs= None , message=ChatCompletionMessage( content= 'こんばんは!' , refusal= None , role= 'assistant' , annotations= None , audio= None , function_call= None , tool_calls= None ) ) ], created= 1766406680 , model= 'apac.anthropic.claude-sonnet-4-20250514-v1:0' , object = 'chat.completion' , service_tier= None , system_fingerprint= None , usage=CompletionUsage( completion_tokens= 9 , prompt_tokens= 83 , total_tokens= 92 , completion_tokens_details= None , prompt_tokens_details=PromptTokensDetails( audio_tokens= None , cached_tokens= 0 , cache_creation_tokens= 0 ), cache_creation_input_tokens= 0 , cache_read_input_tokens= 0 ) ) LLMがchatAPIで渡されたプロンプトメッセージ「こんばんはとあいさつしてね」に対して、「こんばんは!」というメッセージを返しています。 MCPサーバーを介する場合のやり取り 上記に対し、LLMが入力されたプロンプトを元に最適なMCPツールを選択し必要に応じてパラメータをセットし実行するためには、サポートしているMCPツールの情報をインプットしてやる必要があります。 messages = { "role" : "assistant" , "content" : "あなたは柔軟に入力内容を類推できるAIアシスタントです。質問への回答は出来る限り完結に回答してくれます。" }, tools = [ { "type" : "function" , "function" : { "name" : "greeting" , "description" : "あいさつメッセージを返します" , "parameters" : { "type" : "object" , "properties" : { "message" : { "type" : "string" , "description" : "message parameter" , "default" : null } }, "required" : [] } } } ] 先程との違いは明確で、toolsパラメーターにMCPサーバーが提供するツールの情報をセットして、chatAPIを実行しています。 すると以下のようなレスポンスを返してくれます。 ChatCompletion( id = 'chatcmpl-63876e3e-fb1d-4910-81a9-8fea53190dff' , choices=[ Choice( finish_reason= 'tool_calls' , index= 0 , logprobs= None , message=ChatCompletionMessage( content= '' , refusal= None , role= 'assistant' , annotations= None , audio= None , function_call= None , tool_calls=[ ChatCompletionMessageFunctionToolCall( id = 'tooluse_rSY9i9tdRKmTZewG_-lfqg' , function=Function( arguments= '{"message": "こんばんは"}' , name= 'greeting' ), type = 'function' , index= 0 ) ] ) ) ], XXXXXXXXXXXXXXXX ) レスポンスも先程とは異なり、tool_callsパラメータがセットされています。 パラメータにはchatAPIで渡したプロンプトとtoolsパラメータの内容を元にLLMが選択した最適な実行すべきMCPツールが設定されています。 LLMはあくまで実行すべきツールを提示してくれるだけなので、実際のツール実行部分はLLMの範疇外なので対応が必要です。 上記ではgreetingというツールをmessageに記載されているパラメータで実行すべきとなっているので、実行します。 {   'message' : 'Hello, MCP! こんばんは' } 実行結果は自身で準備したMCPツールの通りとなります。 今回はレスポンスとして「Hello, MCP! こんばんは」が返ってきていますが、 プロンプトから入力したテキストが正しくパラメータとして渡され、ツールも想定通りに実行されています。 ※あくまでLLMは実行すべきと判断した内容を返してくれるだけで、ツールの実行は自身でやっています。 次にLLMにMCPツールの実行結果をチャットAPIで渡してやります。 messages = [ { "role" : "assistant" , "content" : "あなたは柔軟に入力内容を類推できるAIアシスタントです。質問への回答は出来る限り完結に回答してくれます。" }, { "role" : "user" , "content" : "こんばんはとあいさつしてね" }, { "role" : "assistant" , "content" : "" , "tool_calls" : [ { "id" : "tooluse_rSY9i9tdRKmTZewG_-lfqg" , "type" : "function" , "function" : { "name" : "greeting" , "arguments" : "{ \" message \" : \" こんばんは \" }" } } ] }, { "role" : "tool" , "tool_call_id" : "tooluse_rSY9i9tdRKmTZewG_-lfqg" , "content" : "{ \" message \" : \" Hello, MCP! こんばんは \" }" } ] ※toolsは重複するので記載を省きます。 LLMが前のステップで返してきたtool_call_idに合わせてツールのレスポンスをセットしチャットメッセージを実行します。これによりLLMがMCPツールが実行された事と、その結果を受け取る事ができます。 ChatCompletion( id = 'chatcmpl-32ae0203-9b39-4b33-9a71-6ba9c9ac1cdb' , choices=[ Choice( finish_reason= 'stop' , index= 0 , logprobs= None , message=ChatCompletionMessage( content= 'こんばんは!いかがお過ごしですか?' , refusal= None , role= 'assistant' , annotations= None , audio= None , function_call= None , tool_calls= None ) ) ], XXXXXXXXXXXXXXXX ) するとLLMが今までのやり取りとMCPツールの実行結果をベースにレスポンスを返してくれます。 今回は「こんばんは!いかがお過ごしですか?」です。若干例が分かりにくいですね。笑。 更に連続でMCPツールが実行出来る場合はtool_callsに次に実行すべきMCPツールの情報がセットされるのですが、今回は特に設定されて無いのでこれでチャットプロセスは完了となります。 MCPツールをカスケードで活用する場合 では冒頭でご紹介した、当社メンバーが作成したSafie API MCPツールを使って、カスケードでMCPツールを実行する例を見てみましょう。 先程のMCPツールも読み込みつつ、「昨日の10時の画像に人が映っているかどうか調べて」というプロンプトを入力します。 尚、時間情報が含まれる場合はタイムゾーンを解釈するようなプロンプトメッセージを付加しています。 いきなり長くなりますが、LLMへ投げるプロンプトは以下の通りとなります。 messages = [ { "role" : "assistant" , "content" : "あなたは柔軟に入力内容を類推できるAIアシスタントです。質問への回答は出来る限り完結に回答してくれます。 \n\n ## 日時情報 \n 現在の日時: 2025-12-22T13:51:54.528550+00:00 \n 現在の日付: 2025-12-22 \n\n 相対的な日付表現(今 日、明日、昨日など)は上記の現在日時を基準として \n 言語から類推できるタイムゾーンに変換の上 \n ISO 8601拡張形式(タイムゾーン付き)で解釈してください。" }, { "role" : "user" , "content" : "昨日の10時の画像に人が映っているかどうか調べて" } ] tools = [ { "type" : "function" , "function" : { "name" : "list_devices" , "description" : " アクセス権限のあるデバイスの一覧を取得します " , "parameters" : { "type" : "object" , "properties" : { "item_id" : { "type" : "integer" , "description" : "item_id parameter" , "default" : null } }, "required" : [] } } }, { "type" : "function" , "function" : { "name" : "get_device_image" , "description" : " 指定されたデバイスから画像を取得します timestampを指定しない場合、API実行時点の最新画像が取得できます " , "parameters" : { "type" : "object" , "properties" : { "device_id" : { "type" : "string" , "description" : "device_id parameter" }, "timestamp" : { "type" : "string" , "description" : "timestamp parameter" , "default" : null } }, "required" : [ "device_id" ] } } }, { "type" : "function" , "function" : { "name" : "list_device_media" , "description" : " 指定されたデバイスで録画されている映像(メディア)の一覧を取得します timestampを指定しない場合、API実行時点の最新画像が取得できます 制限: - start/endの取得最大範囲は 1日(86400秒)です " , "parameters" : { "type" : "object" , "properties" : { "device_id" : { "type" : "string" , "description" : "device_id parameter" }, "start" : { "type" : "string" , "description" : "start parameter" }, "end" : { "type" : "string" , "description" : "end parameter" } }, "required" : [ "device_id" , "start" , "end" ] } } }, { "type" : "function" , "function" : { "name" : "get_device_location" , "description" : " 指定されたデバイスの現在のGPS位置情報を取得します。GPSに対応していないデバイスでは利用できません デバイスに手動設定された位置情報を取得することはできません " , "parameters" : { "type" : "object" , "properties" : { "device_id" : { "type" : "string" , "description" : "device_id parameter" } }, "required" : [ "device_id" ] } } }, { "type" : "function" , "function" : { "name" : "get_device_thumbnail" , "description" : " 指定されたデバイスの最新サムネイルを取得します " , "parameters" : { "type" : "object" , "properties" : { "device_id" : { "type" : "string" , "description" : "device_id parameter" } }, "required" : [ "device_id" ] } } }, { "type" : "function" , "function" : { "name" : "list_device_standard_events" , "description" : " 指定されたデバイスの標準イベント情報一覧を取得します 「標準イベント」とは以下の5つのイベントの総称です - 接続検知 - 切断検知 - モーション検知 - サウンド検知 - 人検知 制限: - start/endの取得最大範囲は 1日(86400秒)です " , "parameters" : { "type" : "object" , "properties" : { "device_id" : { "type" : "string" , "description" : "device_id parameter" }, "start" : { "type" : "string" , "description" : "start parameter" }, "end" : { "type" : "string" , "description" : "end parameter" }, "event_types" : { "type" : "array" , "enum" : [ "connect" , "disconnect" , "motion" , "sound" , "person" ], "description" : "event_types parameter connect, disconnect, motion, sound, person" , "default" : null } }, "required" : [ "device_id" , "start" , "end" ] } } }, { "type" : "function" , "function" : { "name" : "greeting" , "description" : " あいさつメッセージを返します " , "parameters" : { "type" : "object" , "properties" : { "message" : { "type" : "string" , "description" : "message parameter" , "default" : null } }, "required" : [] } } } ] 入力が時間情報を含むプロンプトの場合は、タイムゾーンやフォーマットを指定するメッセージを補完しています。 toolsパラメータが長くなっていますが、こちらはサポートするMCPツールをリスト化して設定しています。 こちらのメッセージに対するレスポンスは以下となります。 ChatCompletion( id = 'chatcmpl-211b219f-f4af-4e42-8ac1-c6195b2ed713' , choices=[ Choice( finish_reason= 'tool_calls' , index= 0 , logprobs= None , message=ChatCompletionMessage( content= '昨日の10時の画像を調べるために、まずアクセス可能なデバイスの一覧を取得します。' , refusal= None , role= 'assistant' , annotations= None , audio= None , function_call= None , tool_calls=[ ChatCompletionMessageFunctionToolCall( id = 'tooluse_QDchcs-OTn60bSHhwYflUw' , function=Function( arguments= '{}' , name= 'list_devices' ), type = 'function' , index= 1 ) ] ) ) ], XXXXXXXXXXXXXXXX ) LLMは指定されたプロンプトを実行するために、まずはアクセス可能なカメラの一覧取得を行うべきと判断し、list_deviceAPIを実行するようレスポンスを返してくれます。素晴らしく柔軟ですね!! 先程の例と同じで、実行すべきMCPツールのメソッドが指定されているので、そのとおりに実行します。 { 'result' : [ { 'device_id' : 'xxxxxxxx' , 'serial' : 'xxxxxxxx' , 'setting' : { 'name' : 'XXXXXXXX' }, 'status' : { 'video_streaming' : True }, 'model' : { 'description' : 'One (SF-1)' } } ] } 指定された通りにMCPツールのメソッドを実行し上記のレスポンスを得ます。 同様にこちらのレスポンスを含めてLLMにチャットを送信します。 messages = [ { 'role' : 'assistant' , 'content' : 'あなたは柔軟に入力内容を類推できるAIアシスタントです。質問への回答は出来る限り完結に回答してくれます。 \n\n ## 日時情報 \n 現在の日時: 2025-12-22T14:05:27.941094+00:00 \n 現在の日付: 2025-12-22 \n\n 相対的な日付表現(今日、明日、昨日など)は上記の現在日時を基準として \n 言語から類推できるタイムゾーンに変換の上 \n ISO 8601拡張形式(タイムゾーン付き)で解釈してください。' }, { 'role' : 'user' , 'content' : '昨日の10時の画像に人が映っているかどうか調べて' }, { 'role' : 'assistant' , 'content' : '昨日の10時の画像を調べるために、まずアクセス可能なデバイスの一覧を取得させていただきます。' , 'tool_calls' : [ { 'id' : 'tooluse_zVu7PAOFSHqr5GYrKxGM7A' , 'type' : 'function' , 'function' : { 'name' : 'list_devices' , 'arguments' : '{"item_id": null}' } } ] }, { 'role' : 'tool' , 'tool_call_id' : 'tooluse_zVu7PAOFSHqr5GYrKxGM7A' , 'content' : '{"result": [{"device_id": "xxxxxxxx", "serial": "xxxxxxxx", "setting": {"name": "XXXXXXXX"}, "status": {"video_streaming": true}, "model": {"description": "One (SF-1)"}}]}' } ] ※toolsは重複するので記載を省きます。 上記メッセージへのレスポンスは以下となります。 ChatCompletion( id = 'chatcmpl-be9de40c-a119-4548-930a-b75abff8f898' , choices=[ Choice( finish_reason= 'tool_calls' , index= 0 , logprobs= None , message=ChatCompletionMessage( content= 'デバイスが1台見つかりました。昨日(2025-12-21)の10時の画像を取得して確認します。' , refusal= None , role= 'assistant' , annotations= None , audio= None , function_call= None , tool_calls=[ ChatCompletionMessageFunctionToolCall( id = 'tooluse_dGHdMM4PQt-fAdNBmMTEhg' , function=Function( arguments= '{"device_id": "xxxxxxxx", "timestamp": "2025-12-21T10:00:00+09:00"}' , name= 'get_device_image' ), type = 'function' , index= 1 ) ] ) ) ], XXXXXXXXXXXXXXXX ) すると今度はtool_callsに次に実行すべきMCPツールが指定されています。 get_device_imageを指定のパラメータ付きで実行しろと言うことですね。 言われたとおりにツールを実行します。 get_device_imageは指定されたパラメータを元に、サムネイルデータを取得します。 取得したサムネイルデータをBase64形式に変換し、先程の例と同様にLLMにメッセージとして入力します。(※画像データは重いので記載しません) messages = [ { 'role' : 'assistant' , 'content' : 'あなたは柔軟に入力内容を類推できるAIアシスタントです。質問への回答は出来る限り完結に回答してくれます。 \n\n ## 日時情報 \n 現在の日時: 2025-12-22T14:05:27.941094+00:00 \n 現在の日付: 2025-12-22 \n\n 相対的な日付表現(今日、明日、昨日など)は上記の現在日時を基準として \n 言語から類推できるタイムゾーンに変換の上 \n ISO 8601拡張形式(タイムゾーン付き)で解釈してください。' }, { 'role' : 'user' , 'content' : '昨日の10時の画像に人が映っているかどうか調べて' }, { 'role' : 'assistant' , 'content' : '昨日の10時の画像を調べるために、まずアクセス可能なデバイスの一覧を取得させていただきます。' , 'tool_calls' : [ { 'id' : 'tooluse_zVu7PAOFSHqr5GYrKxGM7A' , 'type' : 'function' , 'function' : { 'name' : 'list_devices' , 'arguments' : '{"item_id": null}' } } ] }, { 'role' : 'tool' , 'tool_call_id' : 'tooluse_zVu7PAOFSHqr5GYrKxGM7A' , 'content' : '{"result": [{"device_id": "xxxxxxxx", "serial": "xxxxxxxx", "setting": {"name": "XXXXXXXX"}, "status": {"video_streaming": true}, "model": {"description": "One (SF-1)"}}]}' }, { 'role' : 'assistant' , 'content' : 'デバイスが1台見つかりました。昨日(2025-12-21)の10時の画像を取得して確認します。' , 'tool_calls' : [ { 'id' : 'tooluse_dGHdMM4PQt-fAdNBmMTEhg' , 'type' : 'function' , 'function' : { 'name' : 'get_device_image' , 'arguments' : '{"device_id": "xxxxxxxx", "timestamp": "2025-12-21T10:00:00+09:00"}' } } ] }, { 'role' : 'tool' , 'tool_call_id' : 'tooluse_dGHdMM4PQt-fAdNBmMTEhg' , 'content' : 'get_device_image' }, { 'role' : 'user' , 'content' : [ { 'type' : 'image_url' , 'image_url' : { 'url' : 'data:image/jpeg;base64,XXXXXXXX' } }, { 'type' : 'text' , 'text' : '以降のMCPツール呼び出しで画像が必要な場合は、画像データをMCPツールのargumentsに設定してください。' } ] } ] ※toolsは重複するので記載を省きます レスポンスは以下の通りです。 ChatCompletion( id = 'chatcmpl-57a6d457-306a-4f27-b14e-a4696f11172d' , choices=[ Choice( finish_reason= 'stop' , index= 0 , logprobs= None , message=ChatCompletionMessage( content= '昨日(2025-12-21)の10時頃の画像を確認しました。 \n\n 画像を見る限り、**人は映っていません**。 \n\n 画像には以下が写っています: \n - 赤い自動販売機 \n - 商品が陳列された木製の棚 \n - 白いタイル張りの床 \n - 右下に白い機器(エアコンの室外機のようなもの) \n\n 店舗や事務所のような場所の監視カメラの映像のようですが、この時点では人の姿は確認できませんでした。' , refusal= None , role= 'assistant' , annotations= None , audio= None , function_call= None , tool_calls= None ) ) ], XXXXXXXXXXXXXXXX ) これ以上MCPツールを実行する必要が無いので、tool_callsはNoneとなっています。 また、LLMは設定した画像データをベースに適切にプロンプトで指定された内容を処理したレスポンスを返しています。 入力されたプロンプトに基づいて、MCPサーバーを設定しつつ最適なレスポンスを導き出す手法は上記の繰り返しです。 こうやってみると適切な情報をセットしてやる必要はありますが、それらの情報を元にLLMが如何に柔軟にテキスト情報を処理し、必要に応じ最適なMCPツールを選択しレスポンスを生成しているかがよく分かります。 同様に様々なサービスのMCPツールを組み込むだけで、容易にLLMを介したサービス間自動連携が実現できます。 MCPツールの情報をLLMに設定するには 各メソッドの情報を抽出し、登録する必要があります。 参考程度ですが、軽く書いてみたので貼っておきます。 複数のMCPサーバーを統合管理できるようにもしてみました。 import json import logging from mcp.server.fastmcp import FastMCP class MCP_Manager (): def __init__ (self): #self._mcps = {} self._tools = {} self.logger = logging.getLogger(__name__) async def register_tool (self, mcp: FastMCP): """指定されたMCPサーバーをMcpManagerに登録する""" for tool in await mcp.list_tools(): doc = tool.description description = doc.replace( ' \n ' , '' ) if doc else f "{tool.name}" properties = {} # 簡単なパラメータ推論(実際にはより詳細な実装が必要) required = tool.inputSchema.get( "required" , []) for param_name, param in tool.inputSchema[ "properties" ].items(): self.logger.info(f "Param : {param}" ) if param_name != 'self' : param_type = "string" # デフォルト # 一旦地道に実装する(もっとよいやり方があると思うが) _any = param.get( "anyOf" ) if _any is not None : param = _any[ 0 ] if "items" in param.keys(): def_name = param[ "items" ][ "$ref" ].split( "/" )[- 1 ] properties[param_name] = { "type" : param[ "type" ], "enum" : tool.inputSchema[ "$defs" ][def_name][ "enum" ], "description" : f "{param_name} parameter {', '.join(tool.inputSchema[" $defs "][def_name][" enum "])}" } else : properties[param_name] = { "type" : param[ "type" ], "description" : param[ "description" ] if description in param.keys() else f "{param_name} parameter" , } else : if param[ "type" ] == int : param_type = "integer" elif param[ "type" ] == float : param_type = "number" elif param[ "type" ] == bool : param_type = "boolean" properties[param_name] = { "type" : param_type, "description" : param[ "description" ] if description in param.keys() else f "{param_name} parameter" , } if param_name not in required: # Default値は現状は参考程度にしかならないが一応セット properties[param_name][ "default" ] = param.get( "default" , None ) self._tools[tool.name] = { "mcp" : mcp, "name" : tool.name, "description" : description, "properties" : properties, "required" : required } self.logger.info(f "Registered tool: {tool.name} with properties: {properties} and required: {required}" ) @ property def tools (self): """登録されたツールの一覧をBedrock API仕様に準拠する形で返却する""" return [{ "type" : "function" , "function" : { "name" : v[ "name" ], "description" : v[ "description" ], "parameters" : { "type" : "object" , "properties" : v[ "properties" ], "required" : v[ "required" ] } }} for k,v in self._tools.items()] async def call_tool ( self, tool_name: str , tool_args: dict ): """指定されたツールを実行し、結果を返す""" self.logger.info(f "Function call detected: {tool_name}" ) # FastMCPツールを実行 if tool_name in self._tools.keys(): try : # パラメーターの補完を行う (default値はclaudeでは補完されない) for k,v in self._tools[tool_name][ "properties" ].items(): if k not in tool_args.keys(): if "default" in v: tool_args[k] = v[ "default" ] self.logger.info(f "Executing tool: {tool_name} with args {tool_args}" ) # 後で必要性に応じ修正する tool_result = await self._tools[tool_name][ "mcp" ].call_tool( tool_name, tool_args ) self.logger.info(f "Tool Result: {tool_result} Type: {type(tool_result)} Len: {len(tool_result)}" ) return tool_result except Exception as e: self.logger.error(f "Tool execution error: {e}" ) error_message = f "ツール '{tool_name}' の実行中にエラーが発生しました: {str(e)}" #return error_message raise Exception (error_message) return {} まとめ MCPの活用により、様々なツール、サービスを柔軟且つ統一的に活用する事が可能となります。 更に最近ではより上位のレイヤーでのコミュニケーションが可能となるエージェント間コミュニケーション・プロトコルも流行ってきている。 主要な候補としてACP、A2Aが存在していますが、今後A2Aに統合されるような話もあるようです。 これらの活用により、生成AIの活用の幅が更に広がる事が期待されます。 尚、上記の実装例も生成AIを活用して作成しています。(本当に便利です) 一方でそのままでは活用できず、細かな修正を加える必要はありました。 ※やはり盲目的に使うのではなく、内容を理解しつつ適切に活用すべきだと改めて感じています。 最期に セーフィーでは開発効率向上やプロダクトの価値向上に向けて、積極的に生成AI活用の検討も進めています。 最新の技術も組み合わせつつ、更なる成長へ向けて様々な開発に関わる取り組みを行っています。それらに一緒に関わってくれるエンジニアさんを絶賛募集しています!!!! safie.co.jp
アバター
この記事は Safie Engineers' Blog! Advent Calendar 24日目の記事です。 はじめに こんにちは。開発本部 システム基盤開発部 サーバー第1グループの尹です。 私たちサーバー第1グループは、複数のプロダクトを横断して担当するため、チケット一つひとつに「要件」や「完了の定義」を契約書のように明記し、手戻りのない開発を心がけています。この「曖昧さを排除し、仕様を定義する力」こそが、実はプロンプトエンジニアリングの正体です。AIからの回答が安定しないのは、AIの能力不足ではなく、こちらの「仕様定義(プロンプト)」がバグっているからかもしれません。 今回は、Anthropic公式が発表している「Claudeのプロンプトガイド」から、実務での生産性を爆上げするための10のベストプラクティスをご紹介します。これらをマスターすれば、AIのアウトプット品質は劇的に向上します。 はじめに 1. 契約書のようにプロンプトを書く 2. 「なぜ」やるのか背景を伝える 3. 「例」こそが正義(Few-Shot プロンプティング) 4. 大きなプロジェクトは「小分け」にする 5. Agentワークフローであることを宣言する 6. 提案ではなく「行動」させる(Say "Do It") 7. 「〜しないで」ではなく「〜して」と肯定形で書く 8. XMLタグで挙動を制御する 9. ツール利用を強要しすぎない(大文字で叫ばない) 10. 「Think(考えて)」という言葉を避ける まとめ 1. 契約書のようにプロンプトを書く フォーマット、スタイル、長さ、ターゲット読者を明確に指定しましょう。Claudeは指示を文字通りに解釈します。指示が曖昧であれば、解釈も曖昧になります。 ❌ Bad Prompt React Hooksについての記事を書いて。 ✅ Good Prompt React Hooksについての技術ブログ記事を書いてください: フォーマット : Markdown形式、コード例を含むこと 長さ : 1500〜2000文字 スタイル : 専門的だが分かりやすく ターゲット : 経験1〜2年のフロントエンド開発者 必須項目 : useState, useEffect, カスタムフックの実践的な使用例を含めること 2. 「なぜ」やるのか背景を伝える そのタスクの用途を一言添えるだけで、Claudeは出力のスタイルや深さを適切に調整してくれます。 ❌ Bad Prompt この四半期レポートの要点をまとめて。 ✅ Good Prompt この四半期レポートの要点をまとめてください。 これは経営層向けの報告資料として使用するため、重要な数値データと意思決定に必要な提言を強調してください。 3. 「例」こそが正義(Few-Shot プロンプティング) サンプルを提供すれば、Claudeはその構造を忠実に再現します。逆に言えば、悪い例を与えれば悪い出力になります。 ❌ Bad Prompt 商品説明を書いて。これ参考に:「この商品はすごくいいよ、買ってね!」 ✅ Good Prompt 以下のフォーマットを参考に、商品説明を書いてください。 【参考フォーマット】 【製品名】 : スマートノイズキャンセリングイヤホン Pro 【コアな強み】 : 40dBアクティブノイキャン | 30時間再生 | ハイレゾ認証 【利用シーン】 : 通勤時の静寂、集中作業、没入感のある音楽体験 【一言まとめ】 : 喧騒の世界を、あなたの聴きたい音だけに。 では、上記を参考に「ポータブルプロジェクター」の説明文を作成してください。 4. 大きなプロジェクトは「小分け」にする Claudeは長いタスクもこなせますが、「一気に完了させる」のではなく「ステップごとに進める」よう強制することで安定性が増します。 ❌ Bad Prompt ログイン、登録、パスワードリセット、OAuth、権限管理を含むユーザー認証システム全体をリファクタリングして。 ✅ Good Prompt ユーザー認証システムのリファクタリングを行いたいので、以下のステップで進めてください: 第一歩 : まず既存のコード構造を分析し、修正が必要なファイルリストを挙げる 第二歩 : 新しいアーキテクチャ案を設計し、私が確認するまで待機する 第三歩 : 各モジュールを実装する。一つ完了するごとに私がレビューを行う まずは「第一歩」から実行してください。 ※補足:OpenSpecやZCFなどのオープンソースツールを活用すると、大きなプロジェクトを構造化されたタスクリストに自動分解してくれるため便利です。 5. Agentワークフローであることを宣言する 長い対話や開発を行う場合、コンテキスト(文脈)が圧縮されたり忘れ去られたりするのを防ぐため、状態を保存するよう指示します。 ❌ Bad Prompt この機能開発を手伝って。 ✅ Good Prompt 現在、マルチターン会話で機能開発を行っています。コンテキストが圧縮される可能性があるため、以下の対応をお願いします。 重要なステップが完了するたびに: 現在の進捗まとめを progress.md という形式で出力・保存する 次のToDoリストを列挙する 重要な設計上の決定事項を記録する これにより、会話が中断してもスムーズに再開できるようにしてください。 6. 提案ではなく「行動」させる(Say "Do It") 単に「どうすればいい?」と聞くと、Claudeはアドバイスだけを返す傾向があります。コードを書いてほしいなら、明確にそう伝えましょう。 ❌ Bad Prompt このコード、どこか改善できる? ✅ Good Prompt このコードを直接リファクタリングしてください。要件は以下の通りです: 重複ロジックを独立した関数に抽出する エラーハンドリングを追加する 変数名を最適化する アドバイスだけではなく、修正済みのコードを直接出力してください。 7. 「〜しないで」ではなく「〜して」と肯定形で書く 否定形(〜しないで)よりも、肯定形(〜して)の指示のほうがAIは遵守しやすい傾向にあります。 ❌ Bad Prompt 回答にはMarkdownを使わないで。長すぎないように。専門用語は使わないで。 ✅ Good Prompt 質問には3つの短い段落で回答してください。各段落は50文字以内でお願いします。 プレーンテキスト形式を使用し、小学生でも理解できる言葉で説明してください。 8. XMLタグで挙動を制御する 軽量なXMLタグを使用して、AIのデフォルトの振る舞いを設定すると効果的です。 ❌ Bad Prompt 主体的であってほしいけど、気をつけて。危険なことはしないで、でも保守的になりすぎないで…… ✅ Good Prompt XML <behavior> <mode>proactive</mode> <risk_tolerance>medium</risk_tolerance> <explanation_level>detailed</explanation_level> </behavior> 上記の設定に基づき、この投資案の実現可能性を分析してください。 9. ツール利用を強要しすぎない(大文字で叫ばない) 最近のモデル(Claude 3.5/Opusなど)に対し、「絶対に!」「必ず!」と強い言葉でツール利用(検索など)を強制すると、過剰反応してしまうことがあります。穏やかなトーンで誘導しましょう。 ❌ Bad Prompt 毎回回答する前に絶対に検索ツールを使え!!!検索しないのは間違いだ!!! ✅ Good Prompt ユーザーがリアルタイム情報(ニュース、株価、天気など)を求めている場合は、検索ツールを使用して最新データを取得することを推奨します。 常識的な質問やコードの問題については、検索せずに直接回答してください。 10. 「Think(考えて)」という言葉を避ける 特定のモデル(Opus等)では、「Think」という単語に敏感で、不要な内部推論(思考プロセス)が走りすぎてしまう場合があります。「Consider(検討して)」や「Evaluate(評価して)」といった言葉で代用するのがコツです。 ❌ Bad Prompt Think about the best approach to solve this problem. (この問題を解決する最善の方法を考えて) Think step by step. ✅ Good Prompt Consider the best approach to solve this problem. (この問題を解決する最善の方法を検討してください) Evaluate each option and explain your reasoning. (各選択肢を評価し、その理由を説明してください) まとめ 今回の内容をクイックリファレンスとしてまとめました。 ベストプラクティス 核心となるポイント 1. 契約書のように書く フォーマット、スタイル、長さ、対象読者を明確にする 2. 「なぜ」を伝える 用途を一言添えて、出力の深さを調整させる 3. 例こそが正義 良いサンプル(Few-shot)が良い出力を生む 4. 小分けに進める 一気にやらせず、ステップごとに確認する 5. Agentモード宣言 状態や進捗を保存させ、文脈切れを防ぐ 6. 行動させる(Do It) 提案だけでなく「実行」を明確に要求する 7. 肯定形で指示する 「〜しないで」ではなく「〜して」と言う 8. タグで誘導する XMLタグで振る舞いを定義する 9. 叫ばない ツール利用は穏やかに誘導する 10. Thinkを避ける Consider/Evaluateを使い、過剰な推論を防ぐ これらのテクニックを活用して、日々の開発やドキュメント作成の効率をさらに高めていきましょう! 【出典】Anthropic, Claude Official Prompt Engineering Guide, https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/overview
アバター
この記事は Safie Engineers' Blog! Advent Calendar の23日目の記事です。 こんにちは開発本部AI開発部で開発マネージャーをしているおにきと申します。 この記事ではセーフィーにおける映像解析AI開発におけるプロセスを紹介したいと思います。 読者としては主に映像に関わるAIの開発をしている方およびAI開発のマネジメントに関わる方を想定しています。 1. セーフィーにおけるAI開発とは 2. はじめに:AI開発における「プロセス設計」の重要性 3. AI開発プロセス全体の構造 4. モデル開発の5つフェーズ 4.1. フェーズ1:技術調査(メトリクスと技術選定の確定) ゴール やること 4.2. フェーズ2:フィジビリティスタディ ゴール やること 4.3. フェーズ3:PoC ゴール やること 4.4. フェーズ4:製品開発 ゴール やること 4.5. フェーズ5:改善 ゴール やること おわりに 1. セーフィーにおけるAI開発とは セーフィーではクラウド録画プラットフォームの価値をさらに高めるべく、映像解析AIの開発に注力しています。 私たちが開発する映像解析AIとは、動画や静止画を入力として、AIが物体の位置や数、特定のイベントを検出する機能です。例えば、カメラ内のエッジコンピューティングを活用して人の動きの分析を行う「AI-App(アイアップ) 人数カウント」や、クラウド側で解析を行う「AI People Count」などがあります。 現在、セーフィーでは 映像解析AIのプラットフォーム構想 を掲げています。今後、さらに多様なアプリを迅速に開発していくうえで、「仕組みづくり」が重要になっています。 ※なお、これらの開発にあたっては、弊社の「データ憲章」に基づき、プライバシー保護と倫理的配慮を最優先事項として取り組んでいます。 2. はじめに:AI開発における「プロセス設計」の重要性 AIの開発においては、モデルの認識精度、必要なデータ量、計算コストを事前に予測することが難しく、加えて顧客の要望やユースケースを事前に十分に把握することもできない場合が多いため、リスク(=不確実性)の高い開発と言えます。AI開発ではいきなり製品開発を開始すると開発の途中で所望の性能やコストを実現できずに開発が長引いたり、最悪開発が中止したりといった事態につながります。 リスクを下げるためには、開発プロセスを複数のフェーズに分けて、各フェーズにおいてリスクを管理することが大切です。 AI開発プロセスを導入することで以下のことが期待できます。 各開発プロセスで検討する内容とアウトプットの起き方がわかる フェーズを分けることで、フェーズ間でのリスクの判断をはっきりと行うことができる 3. AI開発プロセス全体の構造 AIを利用したシステムの開発プロセス全体像を、モデル開発とシステム開発の二軸で整理したものが以下の図です。 AI開発においては、モデル開発とシステム開発を分けて管理することを推奨します。これは、モデル開発においては、認識性能や計算コストといった非機能要件の実現・改善が開発内容になるのに対して、システム開発ではUIなど機能要件を実装することが主な開発内容になり、管理の仕方と時間軸が大きく異なるためです。一般的にもモデル開発はMLOps、システム開発はDevOpsとして分けて扱うことが多いようです。 次の章ではモデル開発のプロセスにおける分解したフェーズを説明していきます。実際のAI開発ではテーマによってどこから開始するかなどは異なっているため、テーマごとに柔軟な運用が必要です。 4. モデル開発の5つフェーズ 4.1. フェーズ1:技術調査(メトリクスと技術選定の確定) まずはどのような技術および評価方法があるのかを把握します。 ゴール 既存のモデルの調査を行い、技術選定と評価方法の大枠を定める やること 論文調査を行い既存の手法を理解し、採用する技術を定める 物体検出か画像分類かなどのタスクを定め、採用するモデルを決めます 評価手法を把握する 選定したタスクに対してどのような評価手法があるかを把握します 4.2. フェーズ2:フィジビリティスタディ アルゴリズムを実際に動かしてみて大まかな性能と課題を把握します。把握した課題の改善の見込みの度合いに応じて、課題改善を続けるか、場合によっては開発を中断するかを判断します。 ゴール モデルを用いて製品として実現可能であるかを検証する 課題を把握する ※課題把握(+改善)に関しては今後のフェーズで継続的に行います やること データの収集 オープンデータが利用できるかを調べます (利用可能であれば)製品が使われる環境に近いデータを集め、アノテーションを行います 評価の実施 評価メトリクスを定めて、性能評価を行います モデルの計算時間を計測します エッジコンピューティングの場合は、実際のカメラデバイス上で目標の処理速度が出るかも検証します 課題の把握 評価の結果どのような課題があるかを把握します それらの課題が改善しそうかを判断します 4.3. フェーズ3:PoC 顧客が実際に運用する環境を想定した検証を行います。検証もいきなり実地で行うのではなく、最初はオフラインでの検証から始めるなどステップを分けることも勧められます。 ゴール 実運用状況で顧客が満足できるモデル性能を実現する 評価メトリクスに対して顧客が満足するための達成基準を定める やること モデルの改善の実施 モデルの追加学習が必要な場合はデータの収集・アノテーションを行います パラメタのチューニングを行います 評価メトリクスの達成基準を定める 顧客のヒアリングや反応を見て、評価メトリクスにおいて達成すべき基準値を定めます 評価メトリクスは必ずしも学術的な手法でなくとも正解率など製品の顧客が直接理解できる手法にしても良いです 課題の把握・改善 フィジビリティスタディに引き続き課題の把握を行いますが、このフェーズからは改善も行います 4.4. フェーズ4:製品開発 製品としてリリースするための最後のフェーズです。モデル開発の場合このフェーズまで来ると時間の限り性能改善やパラメタチューニングをし続けるということも少なくありません。 ゴール 製品としてリリースできるレベルにモデルを仕上げる やること 評価メトリクスの達成基準を達成する 追加学習やパラメタチューニングを行います 性能・計算量の改善 達成基準をクリアした場合はリリースまでの時間で改善を続けます 課題の把握・改善 リリースまでに解決すべき課題を改善し続けます クリティカルでない課題であれば、改善はリリース後に先延ばしすることも検討します 4.5. フェーズ5:改善 製品がリリースしたあとであっても製品価値を向上するためにモデルの改善は継続的に行うことが重要です。 ゴール より良いモデルをリリースして改善を行う やること 課題の把握・改善 既知の課題、リリース後の運用中に見つかった課題を改善します おわりに 本記事では、セーフィーの映像解析AI開発で採用しているプロセスについて記載しました。実際の開発ではこのプロセスをベースとしつつ状況によって柔軟にフェーズの内容を変えながら開発を行っています。 AI開発はリスクとの戦いですが、こうしたプロセスを開発組織全体で共有しながら、プロダクトとして実現させていくことがやりがいのあるところだと思います。 セーフィーでは一緒に働く仲間を募集中です! 少しでもご興味を持っていただけたら、ぜひ以下の採用サイトを覗いてみてください。 safie.co.jp
アバター
この記事は Safie Engineers' Blog! Advent Calendar  22日目の記事です。 こんにちは、セーフィー 企画本部 デザインセンターの木村です。デザインOpsグループのグループリーダーをしています。 デザインシステムを用いたSafie Viewer(セーフィー ビューアー)のUIリニューアルを行い、無事2025年8月にリリースすることができました! この記事は、「デザイナーとエンジニアで作るデザインシステム 」シリーズ第2回目の記事です。 デザイナー3名がエンジニア・PdMと協力し、Safieのメインプロダクトである「Safie Viewer」にデザインシステムを導入したプロジェクトを振り返ります。 今までの記事 #1: デザインシステム「Pantograph」の課題解決 #2: 「どう進めるか」から考えた、0からの挑戦!1年4ヶ月でデザイナーとエンジニアが「共創」し、デザインシステムを導入した話 ← 今回の記事はこちら 【はじめに】Safie Viewerとは 【導入編】デザインシステム反映の目的、そして0からのスタート 【決断編】エンジニアに「デザイン」を迷わせない 〜全画面作成の決断〜 想定外の効果 【実践編】エンジニアとの接続 〜デザインを伝える〜 ①デザインの意図や方針を、言葉で丁寧に説明し共有する ②デザインの意図や仕様を、デザインデータの横に記載する ③実装しやすいFigmaデータを作成する 【確認編】作って終わりじゃない 〜デザイナーが実装まで責任を持つ〜 628件のチケットでデザインと開発の「隙間」を埋める 自分のデザインに最後まで責任を持つ 【完結編】「チーム力」が、プロダクトを強くする 正解がないからこそ、自分たちで創る さいごに 【はじめに】Safie Viewerとは プロジェクトを振り返る前に、Safie Viewerについて簡単にご紹介します。 Safie Viewerとは、「Safie」のカメラで撮影しクラウド上に録画された映像を、リアルタイムで視聴したり、過去の録画を確認したりするためのWebアプリケーションです。録画された映像を、ダッシュボードで自由自在にレイアウトやカスタマイズしたり、切り取りたい映像のスナップショットやタイムラプスを作成し、ダウンロードすることもできます。 デザインリニューアル後のSafie Viewer 【導入編】デザインシステム反映の目的、そして0からのスタート 私はSafie ViewerのUIリニューアルプロジェクトに、ディレクション兼デザイン担当で参画しました。任されたミッションは、「 機能や開発要件を変えずにデザインシステムを適用し、UIを刷新すること 」。これは以前から「デザイン」と「開発」の間にあった、以下の課題を解決するためです。 品質基準の揺れ デザインの再現性の低さ 個別実装による開発コストの増加 「既存のデザインをデザインシステムに置き換える。」 言葉にすると簡単そうに聞こえるかもしれませんが、いざプロジェクトに参画してみると、UIを刷新するために必要な検討事項や解決すべき様々な課題があることが分かりました。 仕様書がない(一部の詳細仕様は立ち上げメンバーの記憶の中に存在) 機能拡張により複雑化したUI/UXの見直しや再整理が必要な箇所がある どう進めるかの具体はこれから(ここは事業会社の醍醐味かもしれません) デザインプロセスで、明確に決まっていたのはほぼ「納期」のみ デザインの置き換えといっても、単なる表層の変更ではありません。当時のデザインになった経緯、機能仕様、そしてUIの刷新によってUXが最適化されるかどうかも含め、多角的に考慮しながら進める必要があります。 それらの情報や、既存のデザインデータもほぼない状況からのスタートでしたが、私たちのミッションは単にUIを新しくするだけでなく、「どう進めるか」というプロセスを考えるところから始まりました。 しかし、進め方が決まっていない不安よりも、自由に検討できることへの期待の方が強かったです。仕様書がないならみんなで認識合わせすれば良い。進め方が決まっていないなら、自ら提案し、みんなで模索しながら最善策を検討していけば良い。 「どう進めるか」から考え、自分たちの手でプロセスを築き上げていくことが、私にとっても「ワクワクする仕事」でした。 【決断編】エンジニアに「デザイン」を迷わせない 〜全画面作成の決断〜 Safie Viewerのデザインシステム反映プロセス:画面の洗い出し プロジェクトに参画した当時、私は入社5ヶ月目で会社には慣れてきた頃でしたが、Safie Viewerに関する知識はまだ乏しく、どんな機能があるのか、どのようなユーザーがどのように使っているのか、十分に把握できていない状態でした。 デザインシステムのオーナーとしての役割も引き継いだ後でしたが、すべての仕様や操作など完全に網羅できておらず、エンジニアチームのメンバーとも、ほとんどコミュニケーションを取ったことがない状態からのスタートでした。 「Safie Viewerの理解度を上げるためにも、仕様書もデザインデータもないので、実際に操作してみて、画面を起こすところからはじめよう。」 プロジェクトを進めるにあたって、私がまず着手したのは、既存画面の「洗い出し」と「可視化」による現状把握でした。Safie Viewerの全画面のスクリーンショットを撮り、Figmaにひたすら貼るという作業を行いました。その数なんと、ライトモードのみで約680枚。 当初は、主要画面のテンプレートとデザインパーツのコンポーネントをエンジニアに提供し、実装していただく方針で進んでいました。しかし、対象となる画面とパターンの膨大さを目の当たりにすると、テンプレートのみを渡して「似たようなページはよしなに実装してほしい」という進め方では、エンジニアの負荷が高まり、デザインの再現性も担保しづらく、プロジェクトが破綻すると思いました。 エンジニアとデザイナーでは、専門領域が違うため、注力するポイントや見る視点が異なります。 「ここの余白はどうしたら良いんだろう」「ここはこの色で合ってる?」と、エンジニアの手を止めさせてしまうことは、余計なストレスをかけてしまい、本来注力すべき開発作業の工数を圧迫しかねません。 何が正解か分からないまま進めるより、デザイナーが画面を作り視覚的に正解を示すことで、エンジニアがデザイン理解にかける負荷を減らし、同時に意図した形でUIの再現性を高めることができる。 「デザインに迷うエンジニアの工数」と、「デザイナーが手を動かす工数」を比較すると、デザイナーが工数をかける方が、圧倒的にメリットが大きいと感じ、できるだけエンジニアが実装に集中できる環境を作ろうと考えました。 そして私は、「スクリーンショットを撮った画面すべてをデザインデータとして起こす」という決断をしたのでした。 プロジェクト開始時のSafie Viewer画面スクリーンショット(約680枚) 想定外の効果 この判断は、結果としてチーム間のコミュニケーションに良い影響をもたらします。 具体的に画面を作成し並べることで、「この画面にはこのパターンが抜けているから追加してほしい」「こういう実装方法なので、デザインデータをこのように変更してほしい」「このデザインだと、次の操作はどうなりますか?」など、エンジニア・PdM・デザイナー間で、実際の操作や他の画面パターンを想像しながら、積極的でかつ建設的な議論ができるようになりました。 さらに、「実はこんなレアケースの仕様があって⋯」「実は裏機能があって⋯」など、そんな使い方があったんだ!と、今まで一部の人の頭の中にしかなかった「隠れた仕様」の発見にもつながったのです。 画面全体の可視化は、チームメンバーそれぞれの頭の中にあった仕様や思考も可視化し、結果的に、デザインシステムの実現性とUIの最適化、また共通認識のすり合わせに大きく貢献しました。 【実践編】エンジニアとの接続 〜デザインを伝える〜 Safie Viewerのデザインシステム反映プロセス:UIデザイン作成/認識合わせ/仕様整理 「全画面をデザインする」と決めた後、エンジニアへデザインを伝えるプロセスとして特に徹底したことが3つあります。 ①デザインの意図や方針を、言葉で丁寧に説明し共有する 他のデザイナーが作成したデザインの意図を読み解くのは、デザイナー同士でも難しい場合があります。職能が異なればなおさら、デザインデータだけで意図を正確に伝えるのは、さらにハードルが高いことだと思います。 そこで、単に完成した画面を渡すのではなく、週3回の定例ミーティングで、担当デザイナーが1ページずつデザインの意図や方針を説明し、不明点を解消しながら進めることにしました。これにより、作成したデザインの意図を正しく伝えることができ、認識齟齬による手戻りを防ぐきっかけになったと思います。 また、PdMとエンジニア側の専門的な視点を交えることで、技術的な実現性やプロダクトの整合性、そしてユーザーニーズとの落とし所などを探りながら進行することができました。メンバーが意見を出し合いながら認識をあわせることで、チームの共通資産を協力して作る場にもなったと思います。 ②デザインの意図や仕様を、デザインデータの横に記載する デザインの意図、仕様、旧デザインとの変更点などをデザインのすぐ横に記載し、開発時にエンジニアが参照しやすい状態にしました。あわせて、定例の中で話した変更点や決定事項なども明記し、後から経緯を振り返れるようにしています。 「書くのを忘れていた!ここどういう仕様だったっけ?」となることもありましたが、細かく面倒なこの作業こそが、後々チームを助けることになりました。 デザインデータの仕様記載 ③実装しやすいFigmaデータを作成する FigmaのDevモードで、エンジニアが参照しやすく、実装しやすいデザインデータの作成を心がけました。オートレイアウトの活用や、コンポーネント・余白の共通化により、可能な限り実装に近い形でデータを作成しています。 細かい点では、Frameの名前も適切に変更しました。Devモードで確認する際、データ構造を素早く理解し、参照箇所に迷わずスムーズに実装できるよう配慮しています。 Figmaの構造上どうしてもFrameが多重になってしまうなど、完全に実装を考慮したデータ作成が難しいケースや、エンジニアチームと微調整を重ねた箇所もありましたが、ある定例で「デザインデータで困っていることはありますか?」と尋ねた際に、「特にありません」という回答をいただけたのは、こうした日々の努力があったからだと自負しています。 また、デザインデータをきれいに作成することは、デザイナー側へのメリットにもつながりました。デザインの更新や修正も容易になり、「この画面を作ってほしい」などの要望にも、スピード感を持って応えられるようになりました。 FigmaのFrame構造 【確認編】作って終わりじゃない 〜デザイナーが実装まで責任を持つ〜 Safie Viewerのデザインシステム反映プロセス:デザインQA エンジニアとの連携において、私が最も大切にしていることの一つが、実装後の「デザインQA」です。 前述の通り、デザイナーとエンジニアでは専門領域が違うため、着眼点が異なります。いくらデザインデータが完璧であったとしても、実装後に細かいズレが生じてしまいます。 そのため、実装後に機能的な挙動を確認するQAチームとは別に、「意図したデザインになっているか」「想定通りの操作感になっているか」などデザイナー自身の目で細かく確認しました。 628件のチケットでデザインと開発の「隙間」を埋める 今回、デザイン修正依頼で作成したチケットは、合計「628件」に及びました。一見多く感じるかもしれませんが、この数は今回のエンジニアチームによる実装のデザイン再現性が非常に高く、デザイナーが「ここをもっと良くしたい」と細部までこだわることができた結果です。 チケットを起票する際は、単に文章だけで伝えるのではなく、該当ページのリンク、修正箇所の画像(必要に応じて目印をつける)、発生条件など、可能な限り詳細を記載するように心がけました。 作成には手間がかかりますが、その分修正内容が直感的に伝わりやすくなります。結果的に「どこを直せばいいか分からない」というストレスを軽減したり、デザイン意図の把握や該当箇所を探す手間が省けるため、修正対応が圧倒的にスムーズになると実感しているからです。 最初は「細かすぎる」と思われたかもしれません。しかし、チケットを通じたやりとりが、デザイン意図への理解を深めるきっかけとなり、そこで得た知見が別のページの実装にも活かされるといった好循環が生まれていれば嬉しいなと思います。 自分のデザインに最後まで責任を持つ 「デザインだけ渡してあとはお願いします」と開発側に依頼するのは、私個人としてはあまり好きではありません。世の中に出るプロダクトは、最終的にエンジニアの力があってこそ初めて「形」になると思っています。だからこそ、自分が作ったデザインに対して最後まで責任を持ちたいですし、意図を正確に伝え、対話を重ねることは、結果として「自分が作ったデザインのため」でもあり、「エンジニアへのリスペクト」でもあり、「プロダクトの品質担保」にもつながると考えています。 将来的にはAIの力でデザインの再現度が高くなり、この工程はもっと楽になるかもしれませんが、複雑な機能を持つ業務システムの場合、細部の調整にはまだまだデザイナーの目が必要だと感じています。 デザインQAは私の永遠の課題です。品質を担保しながら、スピード感を持ち、より効率的な進め方を、これからも模索していきたいと思います。 【完結編】「チーム力」が、プロダクトを強くする 私はもともと制作会社の出身です。以前の仕事は「クライアントの要望を形にすること」であり、向き合う先は常にクライアントでした。しかし、事業会社に所属する今は違います。エンドユーザーであることはもちろん、一緒に働くチームメンバーや社員もデザインを届けるべき重要な相手だと実感しています。 私には、「チームの良い空気感が、良いプロダクトを生み出す」というポリシーがあります。心理的安全性が担保されていれば、それだけ遠慮なく意見を言い合うことができ、結果としてプロダクトの質を高めることにつながると信じているからです。 正直に言えば、時には厳しい意見が飛び交ったり、私自身の言い方がきつくなってしまった場面もあったと思います。しかし、それをお互いに受け止めながら、建設的な議論ができたのは、風通しが良く、本音で向き合えるチームだったからだと思います。 お互いを尊重し、相互理解を深めることで目線を合わせ、納得のいく意思決定につなげていくプロセスこそが、セーフィーではとても重要だと感じています。 正解がないからこそ、自分たちで創る セーフィーのプロジェクトには、「こう進めればうまくいく」という正解はありません。 「ここはまず仕様を整理してからUIに落とそう」「この機能は複雑だから、個別にMTGを設けよう」「ここの意思決定には、UXリサーチャーの視点を借りよう」 その都度、自分たちで最適な進め方を考え、会話しながら進めることは、セーフィーで働く醍醐味の一つです。 1年4ヶ月にも及ぶ「Safie Viewerのデザインシステム反映プロジェクト」は一旦完了しましたが、課題はまだまだ山積みです。Safie Viewer自体のUX改善はこれからも続きますし、デザインシステムの社内浸透もまだまだこれからです。 それでもここまで走り続けることができたのは、エンジニア・PdM・デザイナーといった職種の垣根を超えて、お互いの分からない部分を補完し合い、一緒に良いものを創ろうとするみんなの想いと協力があったからこそだと思います。 最後まで読んでいただき、ありがとうございました! さいごに デザインセンターでは、Safieのデザインシステムに関するその他の記事も公開しています。 ぜひ、あわせてご覧ください。 engineers.safie.link note.com note.com
アバター
この記事は Safie Engineers' Blog! Advent Calendar 21日目の記事です。 はじめに こんにちは。セーフィー株式会社に25新卒として入社した中です。 昨日の記事 に引き続き、本日は、新卒研修で開発した備品管理システム「Treasure Collection」の開発を通して我々「宝舟」が学んだこと、そして今後のロードマップについてご紹介します。 はじめに 開発を通して学んだこと 配属後に開発したもの 現状の課題と対策 Treasure Collection をどう育てていくのか 将来的なチャレンジ 終わりに 開発を通して学んだこと このプロダクト開発では、 こちらの記事 で触れたタスク管理の難しさや、プロダクトの全体像を常に把握しておくことの重要性など、多くの実務的な学びを得ました。 研修の延長線上にあるこの環境では、PMが軸となってタスク量をコントロールしつつ、メンバーは各自の得意なスタックを飛び越えて、さまざまな領域に積極的にチャレンジしています。通常のプロジェクトでは難しい、大胆な方針変更や開発中のロールチェンジなども経験し、失敗を繰り返しながらチームとして大きく成長しました。 私自身、このプロジェクトでPMを経験しましたが、「プロジェクトをどう進めるべきか」「何を軸に判断すべきか」が分からず大きく失敗しました。しかし、その経験を深く内省し、現在ではその学びを配属先の業務に活かすという良い流れを作れています。 配属後に開発したもの 8月にそれぞれ配属された後も、私たちは「Treasure Collection」の改善を続けてきました。ユーザーの声を反映し、プロダクトを実用的なものにするために実装した機能の一部をご紹介します。 CSVインポート機能で移行を効率化 もともとスプレッドシートで管理されていた備品の移行を容易にするため、CSVファイルによる一括登録機能を実装しました。これにより、初期の登録作業にかかる労力を大幅に削減しました ステータスに「紛失」「廃棄」を追加 今後の棚卸し機能の実装を見据え、備品の現状を正確に把握するために「紛失」と「廃棄」ステータスを追加。 特に「紛失」ステータスは異常値であるため、他よりも彩度を上げて視認性を高める工夫を施しています。 [実装途中] カード形式の表示オプション 利用者からの「備品とその写真を紐付けて管理したい」という要望を反映し、写真と備品情報を関連付けて表示するカード形式の追加を進めています デザインの大幅アップデート(こだわりポイント) 特に私が担当したデザインアップデートについて、詳しくご説明させてください アップデート後のデザイン アップデート前のデザイン アップデート後のデザインの根幹にあるのは、「情報を色、サイズ、太さといった視覚的な要素で明確に区別する」という考え方です。この考えに基づき、以下の3点に特にこだわりました 一覧性・情報量の最大化 リスト表示の中で、ユーザーが必要とする情報(備品名、ステータスなど)を漏れなく、かつ整理して表示しました ステータス認識の迅速化 「貸出可」「貸出中」などのステータスを分かりやすい色で表現し、一覧画面で最も目に入りやすい左側に配置しました 情報の優先度の表現 情報には必ず優先度があるため、太字やフォントサイズを使い分け、ユーザーの視線誘導を意識した設計を行いました これらは全て「ユーザー視点に立って、何がユーザーにとって使いやすいのか」を追求した結果です。この一連のプロセスこそが、研修の本来の目的である「プロダクト視点を身につける」ことに繋がっていると実感しています 現状の課題と対策 チームの課題:本業との両立 課題:各メンバーの配属先での業務が最優先となるため、Treasure Collectionの開発タスクに十分な時間を割くことが難しい状況です 解決策:誰が担当しても同じ品質で実装できることを目指し、チケットの起票時に背景や目的、具体的な仕様をより詳しく記載するように改善しています ↓チケットの一例 私個人の課題:デザイン実装での連携不足 課題: デザイン作成時、フォントサイズやピクセル単位の配置など、細かい仕様を詰め切らないまま実装担当者に渡してしまい、コミュニケーションコストが発生しました 解決策: 現在は、デザインファイル内に必要な仕様をメモとして明記することで、この課題の解消を進めています。 ↓ヘッダーのデザインをした際のデザインとその細かい仕様メモ Treasure Collection をどう育てていくのか 本プロジェクトの今後のロードマップは以下の通りです 機能拡張の最優先として、棚卸し機能の実装を考えています 背景: ユーザーから「備品管理には棚卸し(備品の存在・保管場所の確認)が不可欠」との強い要望をいただきました。 意義: 棚卸しは大変な労力がかかる作業であり、現状ユーザーはTreasure Collectionと別に棚卸し用のスプレッドシートを運用しています。「棚卸し」機能を実装することで、スプレッドシートからの脱却と作業の簡略化を実現できるため、優先的に対応を進めています。 将来的なチャレンジ 現在のロードマップには含まれていませんが、将来的にこのプロダクトをより大きくするための長期的なビジョンがあります。 Challenge 1:全社展開を見据えたプロダクトへ 「とりあえず動けばいい」というフェーズは既に終わりました DBの設計を抜本的に見直し、より効率的で拡張性の高いプロダクトを目指します UI/UXの改善にも力を入れ、全社員が直感的に使える「迷わせない」デザインを目指します Challenge 2:デバイス地獄を逆手に取った「最強の安定性」 弊社は管理するデバイスの数が非常に多いため、逆に言えば、この環境下で安定稼働を目指せることは最高のテストフィールドであると言えます 「自分たちで社内の課題を発見し、ほしいと思ったもの」からスタートしたこのプロダクトを、将来的には様々な企業の課題を解決するソリューションへと育てていきたいと考えています。道のりは険しいですが、最高にワクワクする挑戦です 終わりに 新卒研修で生まれた「Treasure Collection」は、今も我々新卒エンジニアにとって実践的な学びの場であり続けています。本業と両立しながら、今後もユーザーに寄り添った開発を続け、より良いプロダクトへと成長させていきたいと思います!
アバター
はじめに この記事は Safie Engineers' Blog! Advent Calendar 20日目の記事です。 こんにちは、セーフィー株式会社に25新卒として入社した緑川です。 我々新卒エンジニア7人「宝舟」は、今年2025年の4月から7月にかけて新卒研修として社内の課題を解決するプロダクトの開発を行いました。 その研修で開発した「Treasure Collection」について、この記事と明日公開の後編にて説明します。 はじめに 注目した課題 どう解決するか? アプリのイメージ 備品一覧画面 カテゴリ管理画面 備品の貸し出し そのほか機能 技術説明 技術選定 まとめ 注目した課題 セーフィーはクラウドカメラサービスの会社なので、オフィスでは多くのカメラや周辺機器が使われています。特に品質保証を行うクオリティマネジメントオフィスでは、検証用カメラや関連機器の管理、貸出を行っています。その記録はスプレッドシートを使って行われていました。 各備品の所在を把握したり、必要な人が借りるためには記録が重要です。しかし備品の数が増えるにつれ、スプレッドシートのメンテナンスは大きな負担になっています。 どう解決するか? ヒアリングによって上記の課題を認識した結果、 備品の所在や貸し借りを正確かつ簡単に記録できるシステム を研修で作ることになりました。 そうして作ったのが、「 Treasure Collection 」です。 主な機能は、以下の3つです。 備品管理者が備品の情報を登録できる 検索機能によって任意の条件に合った備品を検索できる 貸出申請・承認を通じて貸し借りを管理できる このアプリによって、ユーザーは使いたい備品を簡単に見つけて、貸出を申請することができます。 備品の管理者は申請を承認するだけで貸出情報を記録することができ、今誰が持っているのか常に把握することができます。 アプリのイメージ Treasure Collectionがどんなアプリなのか、各画面の紹介を通して説明していきます。 備品一覧画面 備品の検索 Treasure Collectionのトップページには、登録されている備品が一覧表示されます。 中心に備品の情報や現在の状態を示すカードが縦に並んでいます。備品はカテゴリごとにグルーピングされて表示されます。 左側にあるのが、表示する備品を絞り込むための検索バーです。備品情報にテキストで検索をかけられるほか、カテゴリや貸出状況、自分が借りているかどうかなどで検索を行うことができます。 この検索機能を使うことでユーザーは任意の条件の備品を簡単に見つけることができます。 備品の登録 各カテゴリの見出し部分にある、「備品追加」ボタンから新しく備品を追加することができます。 こちらが備品の登録画面です。備品に紐づく情報は、以下の通りです。 カテゴリ 「カメラ」、「ルーター」など、備品の大まかな種別 管理者 その備品を管理していて、貸出を承認する権限をもつユーザー 保管場所 その備品が普段保管されている場所 備考 プロパティ カテゴリに紐づく備品の詳細情報 カメラの場合、「メーカー」、「シリアルナンバー」、「機種名」など 管理者はこれらの情報を入力して備品を新規登録します。この備品の登録は依然として負担のかかる作業となっていますが、少しでも簡略化するためにCSVによる一括登録もサポートしています。 貸出申請 備品一覧画面から貸出申請も行うことができます。備品カードの一番右か、備品詳細モーダルにある貸出申請ボタンをクリックすることで貸出申請モーダルを開くことができます。 備品を借りたいユーザーは、ここに借りる目的や返却予定日を入力して貸出申請します。ここで入力した目的や予定日は貸出申請メッセージに含まれるため、管理者が可否を判断しやすくなっています。 カテゴリ管理画面 カテゴリ管理機能は、Treasure Collectionの中でも工夫したポイントでした。 カテゴリは運用開始後も新しく増えることが予想されるので、備品の管理権限を持つユーザーが柔軟に追加できれば便利になります。 また、備品に紐づく情報はカテゴリによって異なることが予想されます。例えば、ケーブルであれば「長さ」の情報が欲しく、ディスプレイであれば「解像度」が欲しいです。このようなカテゴリによって異なる備品情報も備品管理者から編集できたほうがいいと考え、できたのがカテゴリ管理機能です。 画像の表は、カテゴリの名前と、それに紐づく情報が並んだものです。備品管理者の権限を持つユーザーに限り、これを編集することができます。 このカテゴリに紐づく情報のことをTreasure Collectionでは「プロパティ」と呼びます。プロパティは名前と入力が必須かどうかを指定することができます。上の画像は「カメラ」カテゴリに指定されたプロパティを編集する画面で、シリアルナンバー、メーカー、付属品などの情報が紐づけられるように指定されています。 備品の貸し出し このシステムでも最も重要な貸出処理について説明します。 以下の手順を踏むことで、備品の貸し出しがシステムに記録されます。 ユーザーが借りたい備品に対して、貸出申請を出す 専用Slackチャンネルに、管理者にメンションされた申請メッセージが送信される 管理者は申請を承認または拒否する 承認された場合、ユーザーは管理者から備品を受け取ってから受け取り完了ボタンを押す 貸し借りの申請・承認は普段社内で使われているSlackからできるので、メッセージに気づきやすく、関連するやり取りもそのまま行えます。 上の画像は貸出申請メッセージのイメージです。管理者が承認または拒否のボタンを押すことで手続きが終了します。それだけであれば、アプリを開く必要もありません。 そのほか機能 返却リマインダー 人力で返却の催促をするのが大変だという話は初期から聞いていて、その解決策を形にしたのがこの機能です。期限が過ぎた備品の返却を促すメッセージがSlackで毎朝送信されます。メッセージにはアプリの備品詳細画面へのリンクがあるので、すぐに返却処理を行うことができます。 CSVバックアップ 毎日システムに登録された全備品とその貸し借りの情報をCSVファイルとして出力します。人間が読み書きしやすいように体裁が整えられて出力されるので、もしシステムが止まってもGoogle スプレッドシートにアップロードするだけで暫定的な備品管理体制が用意できます。 技術説明 Treasure Collectionの開発のために使ったものや、技術的な工夫点について説明します。 技術選定 以下のような言語やツールを使用しています。全体として課題解決のための開発スピードを重視し、メンバーが触ったことのあるものを中心に採用しました。 フロントエンド フレームワークとしてNext.jsを使用しています。メンバーにReactの経験がある人がいたこと、開発しやすさのために素のReactよりNextの方がいいという意見が出たことから採用しました。複数のページがあるアプリケーションのため特にApp Routerの機能が役立ちました。 バックエンド セーフィーで開発しているサービスで実際に使用されていること、Pythonの使用経験がある人が多かったことからバックエンドはFastAPIで開発しています。Pythonの充実したライブラリを活かして比較的簡単に開発できること、APIドキュメントが自動的に作成されることなどが便利でした。 データベースにはPostgreSQLを採用しました。これは、カテゴリとプロパティの機能の実現のためにJSONのカラムをサポートしているのが魅力的だったからです。 インフラ アプリケーションはAWS上にデプロイされ、Terraformで管理しています。 インフラの構成図がこちらです。システムが止まって貸し借りができない、ということがないように耐障害性を重視し冗長化しています。 備品とその貸出情報は定期的にCSVとしてバックアップされるので、万が一の場合でもスプレッドシートを使った貸出管理に戻すことができるようになっています。 そのほか、Dockerを使って環境構築やデプロイを単純化したり、GitHub Actionsを使ってテストやフォーマットチェックを確実に行えるようにするなど、様々なツールを使って開発手順の効率化にも取り組みました。7人での開発だったこともあり、大きく効果を感じられました。 まとめ 研修で開発した備品の貸し出し管理アプリであるTreasure Collectionについて説明してきました。多くの研修メンバーにとってユーザーとやり取りをしながら大がかりなアプリケーションを開発するのは初の試みで、大きな経験を得ることができました。このTreasure Collectionは執筆時現在も継続的に開発中で、議論しながら改善や新機能開発を行っています。 より詳しい振り返りや今後の展望については翌日公開の後編にて、メンバーの中さんより詳しく説明します。お楽しみに!
アバター
この記事は Safie Engineers' Blog! Advent Calendar 19日目の記事です。 序文 チームの紹介 メンバーの構成 開発内容 当初の開発手法 どこかしっくりこないスクラム開発 肥大化する振り返り メンバー間のタスクの理解度の差 改善の取り組み スクラムを辞める タスク内容を議論する時間を多く確保 振り返りは継続も簡素化 デイリーミーティングの司会の輪番化 成果と課題 序文  セーフィーのクラウド録画サービスのサーバーサイド開発を担う、サーバー第一グループというチームでグループリーダーをしている三村です。自分はセーフィー入社前まではプレイヤーとしてサーバーサイドの開発を行うことが多かったですが、入社後はたまたま機会がありマネジメントの仕事も任されるようになり、その一環としてチームの開発手法を改善しようと活動してきました。アドベントカレンダーの季節なので、その一部をブログで共有できればと思います。 チームの紹介 メンバーの構成  セーフィーの開発組織は人数が多く、サーバーサイドエンジニアだけでも人数が30人以上がいます。そのため、サーバーサイドエンジニアのチームも複数に分かれています。映像配信に関する開発チーム、AI機能に関する開発チーム、社内や代理店向けの管理ツールの開発チームなどがある中、私のチームはユーザー向けの新規機能を開発するチームとなっています。  チームのメンバー数は異動などから変動しつつもの、だいたい6〜8人くらいで開発をしています。メンバーは比較的若手が多く、新卒数年目の人から30代前半くらいの人で構成されていて、社歴は長くても3年くらいまでです。 開発内容  開発する内容としては、社内で走っている複数の開発プロジェクトに対応する形で開発をしています。プロジェクトの例としては、 カメラを用いた侵入者検知プロダクトの「Safie Security Alert(セーフィー セキュリティ アラート)」 の開発 既設のIPカメラをセーフィークラウドで視聴可能にする「Safie Trail Station(セーフィー トレール ステーション)」 の開発 高速道路業界向けの「キロポスト表示」機能 の開発 クラウド録画サービスをベトナムなど海外で利用できるようにする 開発 など、種類は多岐に渡っていてそれらを並行で開発をしています。  弊社はカメラやサイレン、ライトなどのモノを扱う開発をしているため、新機能追加時にはソフトウェアエンジニアやPdMだけでなく、デバイスエンジニアから機器の調達や配送、カスタマーサポートの立て付けなど、一般的なWebサービスの開発と比べてより多くの人が関わります。そのため新機能の開発の際にはそれら関係者を一堂に集めたプロジェクトが作られて、私たちサーバーサイドエンジニアもそれらに参加をし、開発内容をチームに持ち帰ってタスク化しています。  そのためチームの開発の仕方も、一つのプロダクトをじっくり腰を据えて開発しているというよりは、複数の文脈の開発を並行して行っているような体制になっています。加えチームとしてはサーバーサイドエンジニアのみを抱え機能開発に必要な他のエンジニアがいないことから、一般的なスクラム開発がしにくい体制となっています。 当初の開発手法 どこかしっくりこないスクラム開発  私がこのチームにジョインした頃は、チームはスクラム形式で開発を行っていました。プランニングで決めた内容を2週間のスプリント期間でこなし、スプリント終わりの振り返りでベロシティ等を確認する形式でした。  ただし上述の通り一つのプロダクトのみを開発するような体制ではなく明確なスプリントゴールが決めにくいことや、スプリント期間中に臨機応変に差し込みで消化しなければならないタスクも多く、プランニングでの計画通りにはスプリントが進まないことが多かったです。結果的に、スクラム開発のセレモニーは一通りこなしてはいるものの、「スプリント期間で動く機能を実装して少しずつプロダクトを良くしていく」というスクラム開発の本質的な部分をあまり実践できていなかったです。 肥大化する振り返り  スプリント期間の終わりに振り返りを実施していました。そこで行う内容も段々と肥大化していき、時間がかかるようになっていきました。  例えば、フレームワークとして一般的なKPTを使っていたところに、 Fun/Done/Learn というものも面白そうだという話になり、その二つを組み合わせた形で振り返りで行っていました。また振り返りの際に指標として取るメトリクスも、一般的なベロシティに加えてメンバーの自己申告の「幸福度」などさまざまなものをとるようになり、数が増えていきました。 メンバー間のタスクの理解度の差  複数のプロジェクトの開発を一つのチームで開発している関係で、メンバー間でタスクへの理解の差が生まれていました。スプリントに積むタスクの内容説明は2週間に一回のリファインメントの時間のみであり、時間が足りていなかったです。 改善の取り組み  根本的には、チームで一つのプロダクト開発というよりは複数のプロジェクト形式での開発をしていることによる負が多かったですが、そこの改善には別途 職能横断型チーム 化に移行しようという動きなどもあったため、一旦は現状を受け入れる形で改善に取り組みました。 スクラムを辞める  まず最初に、スクラム形式で開発をすることを辞めました。臨機応変に複数のPJの要請でタスクを差し込みで詰む必要のあるチームの現状と、スプリント期間中の開発内容を固定して機能を作り上げていくスクラム形式が合わないという現状を受け入れたための判断でした。  その代わりにチームで取り入れた方式が、 カンバン でした。カンバン開発は、スクラムのような厳格な期間設定なしに、継続的なフローを重視するアジャイル手法です。視覚的なボードを活用して作業状況を共有し、チームがタスク消化のリードタイムを縮めるようプロセスを改善することを目指しています。  個人的には、 ゾンビスクラムサバイバルガイド という本を読んでチームのスクラム開発の現状への問題意識を改めて持つようになりました。この本ではセレモニーはこなすものの、本質的にスクラム的な思想の実践はできていない状態で、どのようにスクラム的な開発に近づけるかのケースワークの紹介がされていますが、私のチームにおいては根本的な原因のチーム開発の体制の改善は一旦置いておいて、最終的にスクラムをやめる判断となりました。 タスク内容を議論する時間を多く確保  カンバンに移行してからも、スクラム時代に行っていたセレモニーを全て辞めたわけではありません。例えばスクラム時代に行っていたリファインメント *1 の時間は、これまでスプリント期間中一回しか行っていなかったものを、2日に一回程度まで時間を確保するように増やしました(話題にするタスクがなければ流会にしているので、最大限これらの時間を使っているわけではないです)。  上述のようにチームでは様々なプロジェクトでの開発を行なっているため、スクラム時代にはチームメンバーは自分のあまり深く理解できてない文脈のタスクを取って時間がかかってしまうという課題がありました。そのためカンバンに移行した後は、タスクについてなぜそれをやる必要があるのか、それがユーザーにどのような価値を提供するものなのか、また実装上どのような不確実性があるのかなどをチームメンバーで議論する時間をより長く設けました。  また日々のリファインメントに加えて、大きな機能開発の始めの際などは別途時間を取りチームメンバー全員が集まり設計について議論をする時間(特に大きな機能の場合は合計で数時間程度まで)を設けるようにしました。これまでは担当者一人が設計をしてそれを元にタスク化してチームで消化するような形であった中、メンバー全員が設計段階から少なくとも話は聞いているような状況にすることで、理解度の差を埋めようとしました。 振り返りは継続も簡素化  リファインメントだけでなく、振り返りの時間もチームの開発手法改善のための話し合いだけでなく、単純にコミュニケーションの機会としても意義を感じていたため引き続き2週間に一度実施しています。ただしその際に採る指標は、消化したタスクのリードタイムやタスク数ベースでのチームのスループットなど、チームの生産性の関するものに限定するようにしました。また方式はシンプルにKPTに戻しました。 デイリーミーティングの司会の輪番化  元々スクラムを行っていた時は、デイリーミーティングの司会はスクラムマスターを兼任するエンジニアメンバーによって行われていました。スクラムを辞めたことを契機に、この司会の役割をメンバー間で輪番としました。目的としては、デイリーミーティングの場を自分の出番が来たら喋るだけの場でなく、各メンバーが主体的に情報共有する場としての意識を持てるようにすることでした。 成果と課題  一番大きな問題であった、チームの現状とチーム開発のプラクティスのズレの意識は解消することができました。複数文脈の開発の中で突発的に発生する差し込みタスクを、スクラム開発時代は例外として扱わなければならなかったものを、カンバン開発では想定内のこととして振り返りなどでも扱うことができるようになりました。  また振り返りを生産性に関するものに限りシンプルにしたこともあり、チームでタスクのリードタイムを削減することに関して集中して話し合うことができるようになりました。そのため振り返りで出てくるTryも現実的に実施しやすいものが増え、継続的に意義のあるアクションを取る流れは作られ始めています。例えば、カンバンのボードとしてタスクの歩留まり箇所を可視化したことにより、スループット向上のためにはチームのレビューの速度に問題があるという仮説が出て、各メンバーのレビューの反応速度やレビュアーのアサイン方法などについて改善を試みるなどのアクションなどがありました。  チームでこなすタスクの内容の理解度をメンバー間で平準化しようとする取り組みは、多少の進歩はありつつもまだ改善の余地があるような状態です。以前よりはタスクについて情報を共有し議論する時間が増え、既存メンバー間でのヘルプも行われやすい状況になったものの、特に新規ジョインしたメンバーに取ってはコンテクストが複雑でタスク内容が分かり難いような状態が依然と続いています。こちらについては来年以降チームで引き続き改善したいと感じています。。。 *1 : スクラムの文脈での「リファインメント」とは違い、カンバンに移行してからは、単純にタスクについて話し合って理解を深める場程度の意味でこの語を使っています
アバター
この記事は Safie Engineers' Blog! Advent Calendar  18日目の記事です。 はじめに こんにちは!セーフィーでサーバーサイドの開発を担当している石塚です! セーフィーのサーバーサイドの開発では Python を使って実装することがかなり多いです。Python のプロジェクトでは当然のことながらパッケージ管理ツールを使って、プロジェクトに必要なパッケージを管理しています。普段何気なく使っているパッケージ管理ツールですが、普段は仕組みを意識して使うことがなかったので今年のアドベントカレンダーにちょうど良いかなと思い、今回のテーマとしました。 この記事ではセーフィーの Python プロジェクトでよく使われる uv を取り上げ、uv の紹介と依存関係を解決する仕組みについて書いています。(N番煎じですが…) はじめに uv について uv とは uv の使い方 依存関係の解決方法 依存関係とは? 例 uv が速い理由 まとめ uv について uv とは uv は、Astral 社によって開発されている、Rust 製の Python パッケージ管理ツールです。 Python のパッケージといえば pip や poetry などもありますが uv はこれらに比べて非常に高速です。 出典:UV 紹介ページ https://docs.astral.sh/uv/ (2025年12月09日アクセス) Python の標準的な設定ファイルである pyproject.toml にも対応しているため、一般的な Python プロジェクトで導入可能です。 必要に応じて venv で仮想環境を作成することもできます。 uv の使い方 インストール方法は 公式ドキュメント を参照するのがよいと思います。Windows, MacOS, Linux の各 OS に対するインストール方法が書いてあります。インストールはとても簡単です。 Mac や Linux であれば以下のコマンドでインストールできます。 curl -LsSf https://astral.sh/uv/install.sh | sh Windows の場合でも PowerShell で簡単にインストール可能です。 powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" uv は Python 自体のバージョンも管理することができます。 詳しくは https://docs.astral.sh/uv/guides/install-python/#getting-started を参照ください。 基本的な使い方は以下の通りです。 # プロジェクトの作成 uv init <プロジェクト名> README.md や pyproject.toml などのファイルが作成されます。 # 実行 uv run hello.py run コマンドで実行することができます。 # package の追加(例: boto3) uv add boto3 add コマンドでパッケージを追加することができます。 ここでなぜパッケージの依存関係が管理できるのかと疑問に思いました。 次の章で uv における依存関係を解決する仕組みについて紹介します。 依存関係の解決方法 uv の公式ページ ( https://docs.astral.sh/uv/concepts/resolution/ ) に解説がありました。 以下で詳しくみていきます。 依存関係とは? そもそも依存関係とは何でしょうか。普段何気なく使っている用語ですがここで改めて確認してみましょう。 依存関係とはパッケージが動作するために必要としている他のパッケージのことです。一般的にパッケージはバージョン管理されており、依存しているパッケージの許容しているバージョンも決まっています。 例 あるプロジェクトで「パッケージ A」と「パッケージ B」の両方をインストールしたいとします。 パッケージ A は、パッケージ C に依存しており「パッケージ C の バージョン 1.0 以上」が必要 ( package_C>=1.0.0 ) パッケージ B は、パッケージ C に依存しており「パッケージ C の バージョン 2.0 未満」が必要 ( package_C<2.0.0 ) この時、パッケージ管理ツールは以下のように考えます。 A のためには C が 1.0 以上であること しかし、B のためには C が 2.0 未満であること 結果として C はこの範囲内にあるバージョンの中から最新のものを選ぶ このようにして依存関係を解決し、互換性を保ったまま適切なバージョンをインストールすることができます。 uv では pyproject.toml に依存関係が定義されています。 PEP 508 で指定子の構文が定義されています。各依存関係は「パッケージ名」「バージョン制約」「環境マーカー」で構成されます。(例: boto3>=1.42.3 ) uv が速い理由 uv が速い理由を見てみましょう。 1. Rust による実装 Python 自体は素晴らしい言語ですが、パッケージ管理のような大量の計算(依存関係の解決)やファイル操作が必要な処理においては、コンパイル言語である Rust のパフォーマンスが圧倒的です。 uv はこの恩恵を最大限に受けています。 2. グローバルキャッシュとハードリンク これが uv の最大の特徴かもしれません。従来の pip や venv は、プロジェクトごとにパッケージの実体をコピーしていました。これではディスク容量も食いますし、I/O待ち時間も発生します。 一方 uv は、PC内の共通領域にパッケージを保存(グローバルキャッシュ)し、各プロジェクトの仮想環境にはそのファイルへの 「リンク(ハードリンクやリフリンク)」 を作成します。 物理的なファイルコピーが発生しない ため、インストールが一瞬で完了します。 3. 並列処理 パッケージのダウンロードや解凍を並列に行うことで、ネットワークやディスク I/O の待ち時間を最小限に抑えています。 多くのパッケージのインストールが必要な大規模なプロジェクトほど時間短縮効果が多く得られます。 PubGrub アルゴリズム 詳しくは https://nex3.medium.com/pubgrub-2fb6470504f をお読みください。 Rust で実装された pubgrub-rs が公開されており、 uv はこれを採用することで高速な動作を実現しています。PubGrub は依存関係の解決に失敗した場合には人が理解しやすい形で原因を教えてくれます。 上記の理由から依存関係のインストールはすぐに終わり、体感的にもかなり早くなったことを実感しています。パッケージのバージョンの更新が頻繁に入る場合でも uv sync がすぐに終わるため素早くコーディングに取り掛かることができます! まとめ 今回は、Python の新しいパッケージ管理ツール uv について、その特徴と裏側の仕組みを紹介しました。 セーフィーの開発環境でも、こうした新しいツールを積極的に検証・導入することで、開発サイクルの高速化を目指しています。 まだ pip install の待ち時間にコーヒーを淹れている方は、ぜひ一度 uv を試してみてください。 poetry からの移行でも十分に恩恵は得られると思います! 最後まで読んでいただきありがとうございました!
アバター
この記事は Safie Engineers' Blog! Advent Calendar 17日目の記事です。 はじめに セーフィー株式会社 AI開発部でテックリードを務める橋本です。 本記事では、私が2025年の1年間を通じて 設計・実施してきたAI開発部の技術ミーティングの取り組みについてご紹介します。 これらのミーティングは、テックリードとして掲げた 「継続的な学びと成長の機会を提供し、組織全体の技術力を底上げする」 という目的のもと企画しました。 具体的には、以下の4つの活動を軸として運用しています。 論文読み (隔週 30分) コード読み (隔週 30分) 勉強会 (隔週 1時間) 成果発表会 (隔週 1時間) それぞれのミーティングに込めた意図と、2025年の実施状況について詳しくお話しします。 はじめに 論文読み ① エッジでのリアルタイム処理 ② 映像解析 ③ Multimodal & Reasoning コード読み 勉強会 ① エンジニアリングプラクティス・ドキュメンテーション ② ソフトウェア設計 ③ テスト・品質保証 ④ Pythonエコシステム・技術スタック ⑤ 組織論・キャリア 成果発表会 まとめ 論文読み AI開発部では、日進月歩で進化するAI技術やトレンドを素早くキャッチアップし、それを製品開発へ還元することを目指しています。したがって、業務課題に関連した論文を調査し、常に最新の動向を把握し続ける姿勢は不可欠です。 また、読んだ内容をチームにアウトプットすることは、発表者自身の理解を深めるだけでなく、聞き手となるメンバーの知見拡大にも寄与します。 2025年は、チーム全体で計31本の論文を取り上げ、議論を行いました。今年の選定テーマを振り返ると、当社の映像プラットフォーム事業に直結する以下の3分野が中心となりました。 ① エッジでのリアルタイム処理 リアルタイム性が求められるエッジデバイス(カメラ)内において、推論精度と処理効率を両立させる技術です。 最新の物体検出: YOLOv12, YOLOv13, YOLOE など、高速かつ高精度な検出モデル 軽量化・高速化: MobileMambaや量子化技術など、限られたリソースで高性能なモデルを稼働させる手法 高度なトラッキング: 混雑環境下での人物追跡や、IDスイッチの抑制技術 ② 映像解析 映像からオブジェクトや異常を「数値・構造化データ」として正確に抽出する技術です。 異常検知・安全管理: 建設現場の安全確認や、防犯用途での危険察知能力の向上 3D復元・深度推定: CameraHMR, Depth Anything V2など。単眼カメラの2D映像から、人物の姿勢や空間の奥行きを復元し、高度な判断につなげる技術 ③ Multimodal & Reasoning 数値データの抽出に留まらず、LLM/VLMを用いて映像の「文脈や意味」を理解・推論する技術です。 映像理解の深化: InternVideo2, MM-VIDなど。長時間映像から「何が起きているか」を言語化し、検索や要約を可能にする基盤技術。 推論能力の強化: DeepSeek-R1, Chain of Draftなど。AIが文脈を深く読み解き、ハルシネーションを抑えて確からしい判断を下すための基礎研究 コード読み プロダクトエンジニアにとって、設計力および実装力の向上は極めて重要です。 日常業務の進捗管理だけでは、コードレベルまで踏み込んだ品質や工夫を共有しきることは困難です。また、Pull Request上のレビューだけでは、レビュア以外のメンバーへのナレッジ共有が不足しがちであるという課題感がありました。 そこで、他者の優れた設計や実装に触れることで「自分も採用してより良いコードを書きたい」という意識を高め、自然とベストプラクティスを共有・実践したくなる文化を目指しました。 2025年は、 計22回 のコード読みを実施し、実装のベストプラクティスについて議論を深めました。 勉強会 論文読みで得た最先端の知識を、実用的なプロダクトとして落とし込むための「エンジニアリング力」を高めるべく、多岐にわたるテーマに取り組みました。 ① エンジニアリングプラクティス・ドキュメンテーション コードを書く以前の「どう伝えるか」「どうレビューするか」という基礎スキルに焦点を当てました。 Google Engineering Practice Google Technical Writing Course (One / Two / Error Message) 考える技術・書く技術 ② ソフトウェア設計 堅牢で保守性の高いソフトウェアを作るための設計理論について学びました。 クリーンアーキテクチャ ドメイン駆動設計 デザインパターン ③ テスト・品質保証 コードの信頼性を担保するためのテスト手法や、品質に対する考え方を深めました。 テストケース設計 コード品質の指標と管理 ④ Pythonエコシステム・技術スタック Pythonを中心とした、より実装に近い技術スタックや言語仕様を共有しました。 astral-sh/uv の活用 PyO3とmaturinによるPythonとRustの連携 Pythonコードのプロファイリング手法 最新のPython言語仕様 カラーマネジメントの基礎 ⑤ 組織論・キャリア マネージャーにも参加いただき、組織のあり方やキャリア構築についても視点を広げました。 高校野球から考える組織の進化 ~ティール組織とは~ 「量産型エンジニア」からの脱却:面接官が語る、個性を活かす採用面接の心得 成果発表会 2025年12月現在、AI開発部は13名体制となり、組織規模が拡大しました。 人数が増えるにつれ、開発業務において「お互いが何をしているのか詳細まで掴みにくい」という状態が生じがちです。 このミーティングは、開発の成果がある程度まとまった段階で、各メンバーが実務における成果を発表する場として設けました。発表者にとっては、プロジェクトの意義・目的・成果(事業貢献)を改めて言語化する機会となります。 仲間の成果を詳しく知ることは、チーム全体のモチベーションを高めるとともに、相互理解と連携の強化につながっていると感じています。 まとめ 2025年の活動を振り返ると、これらのミーティングは単なる知識のインプットにとどまらず、「論文ベースの最先端技術を、確かなエンジニアリング力で、着実に製品へ落とし込んでいく」という、AI開発部らしい文化の醸成につながりました。 今後もこのサイクルを定着させ、技術を楽しみながら、真摯にプロダクトと向き合える組織へと成長していければと思っています。
アバター
この記事は Safie Engineers' Blog! Advent Calendar 16日目の記事です。 みなさま初めまして、AI開発部の菅井です。 昨今、AI開発の現場では、単に精度の高いモデルを作るだけでなく、 「いかにしてAIをプロダクトとして使える形にするか」 というエンジニアリング力が強く求められていると感じます。 我々AI開発部においても例外ではありません。 素晴らしいモデルができても、それを動かすためのサーバー費用が莫大であれば、ビジネスとして成立しないからです。特にカメラ映像を扱う場合、データ量は膨大で、処理の遅延は許されません。 「いかに推論の実行速度を上げるか」 「どうすればクラウド(EC2)のインスタンスコストを極限まで安くできるか」 これらは、モデルのパラメータチューニングと同じくらい、私たちにとって重要な「試行錯誤」のテーマです。 今回は、gRPCを使ったAI推論サーバーにおいて実装方法によりどれくらいコスト差が生じるのかについて紹介します。 1. なぜ、gRPCの実装方法にこだわるのか? 2. 想定ケース 3. 実装 共通:Proto定義 共通:擬似的な推論負荷 (Heavy Task) 共通:Clientの実装 ① 同期Pythonの実装 ② 非同期Pythonの実装 ③ 非同期C++の実装 4. ベンチマーク結果 測定環境 測定結果サマリ 考察 ① 同期Python vs ② 非同期Python ②同期Python vs ③非同期C++ まとめ 残された課題と今後の展望 PoCからプロダクトへ 1. なぜ、gRPCの実装方法にこだわるのか? AI推論サーバーのコスト構造を考えた時、最も高価なリソースは「GPU」や「CPU」です。 しかし、一般的なWebサーバーの実装(同期処理)では、通信待ちやI/O待ちの間、この高価なリソースが「待ちぼうけ」をしてしまいます。 同期処理の無駄: 「データ受信待ち」の間、CPUは遊んでいる(のに課金される)。 Pythonの限界: 非同期にしても、GIL(Global Interpreter Lock)の制約で、マルチコアCPUを積んだ高価なインスタンスを使っても、1コアしか使い切れない。 これでは、いくら高性能なEC2インスタンスを契約しても、スループット(処理できるカメラ台数)は伸びず、コストばかりが膨れ上がってしまいます。 「高価なコンピュートリソースを、1ミリ秒たりとも遊ばせず、極限まで使い倒すにはどうすればいいか?」 その答えを見つけるために、今回は ①同期Python 、 ②非同期Python 、 ③非同期C++ の3パターンを実装し、それぞれのパフォーマンスとコスト効率を徹底的に比較しました。 なぜREST APIやWebSocketではなく、gRPCなのか? 今回の「リアルタイム映像解析」において、gRPCを採用した理由は主に2点あります。 Bidirectional Streaming(双方向ストリーミング): HTTP/1.1(REST)のような「1リクエスト1レスポンス」では、30FPSの映像を送り続ける際のオーバーヘッドが大きすぎます。gRPCなら、接続を維持したまま双方向にデータを流し続けられるため、低遅延な推論が可能です。 Schema Driven(スキーマ駆動): PythonとC++という異なる言語間で高速に連携するためには、厳密な型定義(Protobuf)によるコード自動生成が不可欠でした。これにより、手動でのデータパース実装によるバグや性能劣化を防げます。 (※gRPC自体の基礎については、@S4nTo様から素敵な記事が投稿されておりますので、そちらも併せてご参照ください。 https://qiita.com/S4nTo/items/0ff0445542538ef49a05 ) よくある「カメラ映像を渡すから、あとの処理はAIエンジニアに任せた!」というパスに対して、我々がサーバーサイドの技術選定(エンジニアリング)でどこまで応えられるか。それはサービスの運用コストを 数分の一、あるいは十分の一 に削減できる可能性を秘めているのです。 2. 想定ケース 今回の記事で想定するシステム構成は以下の通りです。 クライアント: 多数のネットワークカメラ(またはゲートウェイ)。映像フレームを絶え間なく送信し続ける。 サーバー: AI推論サーバー(EC2インスタンス)。受け取った映像に対して物体検出を行い、BBOX(座標)などのメタデータを返す。 通信要件: プロトコル: gRPC (Bidirectional Streaming) 同時接続数: 1台のサーバーで可能な限り多く 遅延: リアルタイム性が求められるため、キュー詰まりによる遅延はNG。 この構成において、「Pythonの同期サーバー」「Pythonの非同期サーバー」、そして「C++の非同期サーバー」で実装した場合に、パフォーマンスにどれだけの差が出るのかを見ていきます。 3. 実装 共通:Proto定義 それぞれの実装パターンで用いるProtocolBufferの定義になります。 ObjectDetection というサービスが DetectStream というRPCメソッドを持つというものです。実際の画像ストリームを意識して書いていますが、今回の実装では処理自体はダミーのTaskを実行するため中身はほとんど使われません。 syntax = "proto3"; package camera_ai; // サービス定義 service ObjectDetection { // 双方向ストリーミング: // クライアントは映像フレームを送り続け、サーバーは推論結果を返し続ける rpc DetectStream (stream DetectRequest) returns (stream DetectResponse) {} } // リクエスト: カメラからの1フレーム分のデータ message DetectRequest { // 非同期処理ではリクエストとレスポンスの順序が保証されない場合や、 // クライアント側で結果をマッチングさせるためにIDが必須です。 uint64 frame_id = 1; // 画像データ (JPEG/PNG等のエンコード済みデータ、またはRawデータ) bytes image_data = 2; // 必要に応じて画像サイズなどを入れることもありますが、 // シンプルにするため今回は省いてもOKです。 // int32 width = 3; // int32 height = 4; } // レスポンス: 1フレームに対する推論結果 message DetectResponse { // リクエストの frame_id をそのまま返すことで、 // クライアントはどの画像の推論結果かを特定できます。 uint64 frame_id = 1; // 1枚の画像に複数の物体が写っているため repeated にします repeated Object objects = 2; } // 検出された個々の物体 message Object { string label = 1; // クラス名 (例: "person", "car") float confidence = 2; // 確信度 (0.0 - 1.0) BoundingBox bbox = 3; // 座標情報 } // バウンディングボックス (OpenCVのcv::Rectに合わせておくと楽です) message BoundingBox { int32 x = 1; int32 y = 2; int32 width = 3; int32 height = 4; } 共通:擬似的な推論負荷 (Heavy Task) GPU推論は非同期ですが、実際には画像の前処理(デコード・リサイズ)や後処理(NMS等)でPythonのCPUリソースが大量に消費されます。この「隠れたボトルネック」を再現するため、 sleep ではなく while ループでCPUを回します。 def heavy_inference_simulation (target_ms= 30 ): start = time.perf_counter() # 指定時間、CPUを空回しして負荷をかける while (time.perf_counter() - start) < (target_ms / 1000.0 ): pass return "Result BBOX" 共通:Clientの実装 まずはサーバーにいじめ抜くほどの負荷をかけるクライアントです。 Pythonの asyncio を使い、1台のマシンから複数のカメラ接続を擬似的に生成します。 async def main (): print (f "Starting Load Test with {NUM_CLIENTS} clients for {TEST_DURATION} seconds..." ) # クライアントタスクを全て起動 tasks = [] for i in range (NUM_CLIENTS): tasks.append(run_single_client(i)) # 全員同時にアクセスするとエラーになりやすいので、わずかに開始時間をずらす if i % 10 == 0 : await asyncio.sleep( 0.05 ) # 全タスクの完了を待つ await asyncio.gather(*tasks) ① 同期Pythonの実装 まずは比較対象となる、標準的な同期実装です。 protobufで定義された ObjectDetectionServicer.DetectStream にストリームを受けとった後の処理を書けばいいだけのシンプルな実装になります。 concurrent.futures.ThreadPoolExecutor を使い、1接続につき1スレッドを割り当てます。 特徴: 実装は一番簡単。 弱点: max_workers (スレッド数)が接続上限になる。それ以上の接続はキュー待ちになり、遅延が激増する。 class ObjectDetectionServicer (pb_grpc.ObjectDetectionServicer): def DetectStream (self, request_iterator, context): """ 双方向ストリーミングの実装。 request_iterator: クライアントから送られてくるリクエストのイテレータ """ print (f "[{time.ctime()}] New connection established." ) # クライアントから stream で送られてくるデータを1つずつ処理 for request in request_iterator: frame_id = request.frame_id img_data = request.image_data # ここに画像データ(bytes)が入る # --- ここで本来は推論を行う --- # 今回はダミーの推論時間を設ける(例: 30ms) heavy_inference_simulation(target_ms= 30 ) # ダミーの結果を作成 print (f "Sync processing frame_id: {frame_id}" ) # レスポンスを作成 response = pb.DetectResponse( frame_id=frame_id, objects=[ pb.Object( label= "person" , confidence=random.uniform( 0.8 , 0.99 ), bbox=pb.BoundingBox(x= 100 , y= 100 , width= 50 , height= 200 ) ) ] ) # 結果を返す yield response ② 非同期Pythonの実装 次に、 asyncio 版です。非同期版もPythonでは簡単に実装することができます。 ObjectDetectionServicer.DetectStream にasyncをつけて非同期に対応させる形になります。 推論部分を run_in_executor でスレッドプールに逃がし、イベントループを止めないように工夫します。 特徴: 接続自体は大量にさばける(C10k対応)。 現実: Pythonの GIL (Global Interpreter Lock) の制約により、複数のスレッドで計算しようとしても、同時に動けるCPUコアは実質1つだけ。CPUバウンドな処理(前処理等)が重なると詰まる。 class ObjectDetectionServicer (object_detection_pb2_grpc.ObjectDetectionServicer): async def DetectStream (self, request_iterator, context): loop = asyncio.get_running_loop() async for request in request_iterator: # 【工夫】重い処理を別スレッドに逃がして、イベントループを守る # しかし、Pythonコードである以上、GILの呪縛からは逃れられない... await loop.run_in_executor( None , heavy_inference_simulation, 30 ) yield object_detection_pb2.DetectResponse(...) ③ 非同期C++の実装 最後に、今回の主役であるC++実装です。 Pythonとは異なり、 CallDataやCompletionQueue について 理解する必要があるため、実装方法は少し複雑になります。 CallDataクラス Clientとの1つ接続ごとの状態とデータをを管理するクラスになります。 非同期処理では、処理がいったん中断されてスレッドが別の仕事に移るため、「関数のローカル変数」にデータを残しておくことができません。そのため、以下の情報をヒープメモリ上のオブジェクト(CallData)として保持し続けます コンテキスト: ServerContext 、 Request 、 Response などのデータ実体。 ステート(状態): 現在その接続が「接続待ち」なのか、「受信中」なのか、「送信中」なのかを示す状態遷移(ステートマシン)。 ロジック: イベントを受け取った際に実行すべき処理内容( Proceed メソッド)。 CompletionQueueクラス CompletionQueue は、gRPCライブラリ内部(カーネルやネットワーク層)と、アプリケーション(ユーザーコード)をつなぐ 連絡通路のようなものです。 非同期で行われた操作(データの読み込み、書き込み、接続確立など)が完了した際、その通知(イベント)がすべてこのキューに格納されます。 完全にスレッドセーフであり、複数のスレッドから同時にアクセスしても安全に動作するように設計されています。 ワーカースレッドは、このキューに対して Next() メソッドを呼び出し、「何か完了した仕事はないか?」と問い合わせます。イベントがあれば即座に取り出し、なければイベントが来るまで待機(ブロック)します。 CallDataクラスの実装例 class CallData { public : CallData (ObjectDetection::AsyncService* service, ServerCompletionQueue* cq) : service_ (service), cq_ (cq), stream_ (&ctx_), status_ (CREATE) { // インスタンス生成と同時に「接続待ち」状態に入る Proceed (); } void Proceed () { if (status_ == CREATE) { // 1. CREATE状態: // gRPCシステムに対して「次の接続要求が来たら、このCallDataを使ってね」と依頼する。 // タグとして `this` (自分自身のポインタ) を渡すのが定石。 status_ = PROCESS; service_-> RequestDetectStream (&ctx_, &stream_, cq_, cq_, this ); } else if (status_ == PROCESS) { // 2. PROCESS状態 (初期接続完了): // 新しい接続が確立された瞬間にここに来る。 // 重要: 次のクライアントのために、新しい CallData を即座に作って待機させる(Factoryパターン) new CallData (service_, cq_); // 最初の読み込みを開始する // 読み込みが終わったら、また `this` (自分) が呼ばれるようにタグ付けする status_ = READING; stream_. Read (&request_, this ); } else if (status_ == READING) { // 3. READING状態 (Read完了): // クライアントから request_ にデータが入った状態でここに来る。 // --- 推論処理 (Mock) --- // request_.image_data() を cv::Mat に変換したりする場所 HeavyInferenceSimulation ( 5 ); // 5msの重い処理をシミュレート response_. set_frame_id (request_. frame_id ()); auto * obj = response_. add_objects (); // objに結果をセットする // 結果を書き込む status_ = WRITING; stream_. Write (response_, this ); } else if (status_ == WRITING) { // 4. WRITING状態 (Write完了): // クライアントへの送信完了後にここに来る。 std :: cout << "Processed frame_id: " << response_. frame_id () << std :: endl ; // レスポンスをクリアして次のデータを読む response_. Clear (); status_ = READING; stream_. Read (&request_, this ); } else { // FINISH状態 assert (status_ == FINISH); delete this ; // 自分の役目は終わったので自決する } } // クライアント切断時などの後処理用 void Finish () { status_ = FINISH; stream_. Finish (Status::OK, this ); } // 外部からアクセスするタグ用のアクセサ // 処理が成功したか(ok)によって挙動を変える制御が必要 void OnEvent ( bool ok) { if (!ok) { std :: cout << "Client disconnected or finished reading/writing." << std :: endl ; Finish (); return ; } Proceed (); } ワーカースレッドの立ち上げとCompletionQueueの呼び出し例 // サーバーのメインクラス class ServerImpl { public : ~ ServerImpl () { server_-> Shutdown (); cq_-> Shutdown (); } void Run () { std :: string server_address ( "0.0.0.0:50051" ); ServerBuilder builder; builder. AddListeningPort (server_address, grpc:: InsecureServerCredentials ()); builder. RegisterService (&service_); // CompletionQueue (イベントキュー) の作成 cq_ = builder. AddCompletionQueue (); server_ = builder. BuildAndStart (); std :: cout << "Server listening on " << server_address << std :: endl ; // 最初の1つ目の CallData を投入して待ち受け開始 new CallData (&service_, cq_. get ()); // イベントループ void * tag; // イベントに紐付いたタグ (CallDataのポインタが入る) bool ok; std :: vector < std :: thread > threads; int thread_count = 15 ; // サーバーのCPUコア数に合わせるのが一般的 for ( int i = 0 ; i < thread_count; i++) { threads. emplace_back ([ this , i](){ std :: cout << "Worker Thread " << i << " started." << std :: endl ; void * tag; bool ok; // 複数のスレッドが、同じ cq_ (キュー) を同時に監視する。 // gRPCのCompletionQueueはスレッドセーフなので、競合は内部で解決してくれる。 while (cq_-> Next (&tag, &ok)) { // 空いているスレッドがイベントを拾って処理を実行 static_cast <CallData*>(tag)-> OnEvent (ok); } }); } // 全スレッドの終了を待つ(実際はCtrl+Cされるまで帰ってこない) for ( auto & t : threads) { if (t. joinable ()) t. join (); } } 今回の実装のイメージを以下に図示しています。 C++実装のイメージ Clientが DetectStream を呼び出すと ComletionQueue にイベント通知がされ、空いているWorker Threadが CallData を呼び出します。 CallData は Proceed により自身の状態をみながら処理を進めます。画像を渡されているのであれば、推論を実施して結果を Clinet に返します。 重要なポイントはCompletion Queueがスレッドセーフであり、各スレッドをまたいで使われている点であり、この仕組みが非同期実行を実現しているといえます。 各Worker Threadは空いている順からイベント通知のあった CallData を処理していきます。そのため、 CallData 1 が Worker Thread 1 で呼ばれることがあれば、 Worker Thread N で呼ばれることもあります。同期処理と異なるのは、このようにスレッドが特定のClientのストリームを占有せずに、各Worker Threadが協力して多くのClientのストリームを処理することにあります。 4. ベンチマーク結果 実際にそれぞれの実装方法でどれくらいの差が出るのかを確認していきます。 測定環境 項目 内容 マシン Intel(R) Core(TM) Ultra 7 155H (1.40 GHz) 16コア Python 3.10.18 CXX 17 推論負荷 5ms間のループ処理 クライアント 30台 ストリーム時間 10秒 ストリームFPS 10 測定結果サマリ 各々がRequestを受け取りResponseを返すまでの時間を計測した結果が以下になります。 実装パターン 平均時間 (Avg) 最速 (Min) 最遅 (Max) 評価 ① 同期 Python 63823.44ms 25.03ms 127814.19ms ❌ 破綻 ② 非同期 Python 45678.62ms 15.19ms 90461.10ms ❌ 破綻 ③ 非同期 C++ 13.157ms 5.611ms 71.566ms 🏆 成立 10FPSを遅延なく処理するためには 最遅Max が100ms以下である必要があります。 ③非同期 C++ は上記の条件を満たすので本マシンで 30台のクライアント をさばくことができることがわかります。 Client数のみを変えていき、 最遅Max を観測した結果を以下のグラフにしました。 処理時間(Max)の比較 ①同期Python 、 ②非同期Python が 1 00msを下回るClient数は2までなので、本マシンで 2台のクライアント しかさばけないことがわかります。 実装方法によってコストに 15倍の差 がでるという結果になりました。 考察 ① 同期Python vs ② 非同期Python 同じPython、同じGILの制約下にありながら、同期版だけが Max 127秒 (非同期版より30秒以上悪化)という遅延を記録しました。 その決定的な差は、 「スレッドを占有する時間の長さ」 にあります。 1. 20個の椅子と30人の客 今回のサーバー(16コア)では、Pythonのデフォルト設定により 約20個のスレッド が稼働していました。 ここに 30台のカメラ が同時に接続すると、何が起きるでしょうか。 2. 同期版の敗因:接続=占有 同期実装では、「ストリーミング中の10秒間、スレッドをずっと占有」します。 開始直後: 20台がつながった瞬間、全スレッドが埋まります。 待ちぼうけ: 残り10台は、前の人の配信(10秒)が終わるまで、 接続することすら許されず待機 させられます。 結果: 「待ち時間10秒」+「処理時間」が確定し、遅延が跳ね上がりました。 3. 非同期版の勝因:計算=占有 非同期実装では、スレッドを占有するのは「推論している一瞬(5ms)」だけです。 高回転: 5msで席を立つため、20個のスレッドで30台を次々とさばくことができます。 結果: 「入り口での門前払い」が起きず、少なくとも接続とデータ受信はスムーズに行われました。 ②同期Python vs ③非同期C++ 続いて、同じ「非同期アーキテクチャ」を採用している両者の比較です。結果は、C++がPythonを 1200倍以上 の速度差で圧倒しました。 両者とも「スレッドプール」を使って計算を並列化しようとしています。しかし、実際のCPUコアの使われ方は対照的です。 Pythonの限界 (GIL): コード上でスレッドを15個立てても、Pythonの GIL (Global Interpreter Lock) の制約により、同時にPythonコードを実行できるのは 常に1スレッドだけ です。 15車線の高速道路があるのに、 工事規制で「たった1車線」しか通行できない 状態と同じです。30台のカメラから来るデータが、この細い1車線に殺到したため、大渋滞(90秒の遅延)が発生しました。 C++の真価 : GILが存在しません。15個のスレッドが、 15個のCPUコアすべてを同時に使用 して計算します。 15車線をフルに使って車がビュンビュン流れるため、30台程度の交通量では渋滞など起きようがありません。 まとめ 本記事では、同期・非同期のアーキテクチャの違い、そしてPython・C++という言語特性の違いが、システムのパフォーマンスにどれほど差をもたらすかを検証しました。 実験の結果、同じハードウェアを使っていても、実装方法を変えるだけで 「1台のサーバーに収容できるカメラ台数」が15倍以上変わり得る ことが確認できました。これは、クラウドインフラ費用が1/15になる可能性を示唆しており、エンジニアリングの工夫が事業の収益性に直結することを証明しています。 残された課題と今後の展望 今回は「非同期C++の可能性」にフォーカスしましたが、検証しきれていないテーマも残されています。 Python MultiProcessing: PythonのGILという障害は、マルチプロセス化によって回避する手段が存在します。メモリ消費量とのトレードオフにはなりますが、Pythonの生産性を維持しつつ性能を上げるアプローチとして検証の価値があります。 同期C++との比較: 「C++だから速い」のか「非同期だから速い」のかをより厳密に切り分けるため、同期C++実装との比較も興味深いテーマです。 これらを含めたさらなる検証は、次回の記事でお届けできればと思います。 PoCからプロダクトへ 実際のプロダクト開発は、今回のようなCPUループ処理よりも遥かに複雑です。 映像のデコード、リサイズなどの前処理、そしてGPUを用いた推論処理。これらを遅延なくパイプライン化するために、 NVIDIA DeepStream のような特化したツールキットを活用して、さらなる効率化を目指す道もあります。 しかし、どのような手段を選ぶにせよ、本質は変わりません。 AI開発者がモデルの精度だけでなく、 「実行速度」や「運用コスト」を意識した設計 を行うこと。 そのエンジニアリングへの意識こそが、AIを単なるPoC(概念実証)で終わらせず、持続可能なビジネス価値を生む「プロダクト」へと昇華させる鍵になるのです。
アバター