TECH PLAY

セーフィー株式会社

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

221

こんにちは、セーフィー株式会社 開発本部 テックブログ編集長の土井です。 これまで「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日
アバター
こんにちは!エンジニアリングオフィスの横道( @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(概念実証)で終わらせず、持続可能なビジネス価値を生む「プロダクト」へと昇華させる鍵になるのです。
アバター
この記事は  Safie Engineers' Blog! Advent Calendar  の15日目の記事です。 はじめに こんにちは、セーフィー でサーバーサイドエンジニアをしている金成といいます。 今回は AWS ECS Blue/Greenデプロイをサービスに導入してみたので、そちらについて共有させてください。 AWS ECS Blue/Green デプロイは今年になって有効化された機能で、今迄は ECS ではローリングリリースのみ、Blue/Green デプロイをやる場合は CodeDeployを使う必要がありました。 今回の機能追加で、ECSのサービス設定の変更と関連リソースを追加することで、ECSの設定のみで Blue/Greenデプロイができるようになりました。 今回、我々のチームで運用しているサーバーについて、適用してみました。 はじめに 背景とモチベーション リリースの手法 ローリングリリース Blue/Green デプロイ B/G デプロイのフロー ECS における B/G デプロイの選択肢 Lifecycle Hooks によるリリース安全性向上 制限事項とハマりポイント 1. B/G → ローリングへの切り替え時 2. ALBリスナールールの制限 現状の課題と今後の展望 おわりに 背景とモチベーション 現在、我々のチームでは、認証サーバーの運用をしています。 こちらは多くのサービスで利用されるため、リリースの際の障害のリスクを最小限に抑えることが課題でした。 特に、環境差分に起因する障害や考慮漏れなどはstaging環境でのチェックから漏れやすく、サービスの安定性を脅かすリスクとなっていました これらの課題を解決するため、我々は既存のデプロイ手法を見直し、より安全性の高いリリースプロセスへと改善を図ることにし、新たに導入された Blue/Green デプロイを利用することにしました。 リリースの手法 ローリングリリース 現在、ECSのデフォルトでよく採用されているのは ローリングリリース です。 一部のサーバーから新しいバージョンを段階的に適用していくデプロイ方法です。サービスを停止せずにデプロイすることができます。 Blue/Green デプロイと比べるとロールバックに時間がかかり、一時的に新旧サーバーが混在するデメリットはありますが、インフラの構成自体はシンプルで管理しやすいです。 Blue/Green デプロイ Blue(旧環境)と Green(新環境) を作成し、トラフィックを切り替えることでデプロイする手法で Blue環境は落とさずに、一定時間残すため、問題発生時のダウンタイムが少なくなります。ローリングデプロイの切り替えにはタスクの起動を待つ必要がありますが、トラフィックの切り替えで済むため、早くロールバックができます。一方、インフラの構成が増え、デプロイの複雑さは増します。また、管理するリソースが増えます。 認証サーバーのようにサービスレベルが高い、リスクを最小にしたい場合には、ロールバックを早く確実に実現できる Blue/Green デプロイは有効な選択肢になると思います。 B/G デプロイのフロー B/G デプロイは、以下の手順で進みます。問題発生時の 切り戻しポイントが明確 で、手順5〜6の間であれば、即座にBlue環境(旧環境)にトラフィックを戻すことが可能です。 初期状態 : Blue環境 (旧バージョン) にトラフィックが流れている。 Green環境 (新バージョン) を構築。 テスト用トラフィックをGreen環境に流し、 動作検証 を実施。 本番トラフィックをGreen環境へ一部または全体に 段階的に移行 。 トラフィックがGreen環境に 100%切り替え 。 一定期間の監視後、Blue環境を 停止 。 出典: Amazon ECS blue/green service deployments workflow Webページ https://docs.aws.amazon.com/AmazonECS/latest/developerguide/blue-green-deployment-how-it-works.html (2025年12月9日アクセス) ECS における B/G デプロイの選択肢 AWSでは、ECS環境でB/Gデプロイを実現する方法として、以下の2種類が提供されています。 CodeDeploy を使う (従来の方式) ECS B/G デプロイを使う (ECS サービス設定に統合された新しい方式) 比較項目 CodeDeploy 連携 ECS B/G デプロイ (NEW) トラフィック移行 豊富 (Canary, Linear, AllAtOnceなど比率を自由に設定可能) 現状は AllAtOnce のみ 。 設定管理 CodeDeploy側で設定。 ECSサービス定義に統合され、 管理がまとまる 。 戦略切り替え 複雑。 ローリング ↔ B/G の変更がサポート されており比較的楽。 検証フック Lifecycle Hooks (固定タイムアウト) Lifecycle Hooks (柔軟なタイムアウト設定が可能) 我々は、 設定管理の容易さ と、将来的な ローリングリリースへの切り戻しの容易さ を評価し、 ECS B/G デプロイ を選択しました。 Lifecycle Hooks によるリリース安全性向上 ECS B/G デプロイおよび CodeDeployには、 Lifecycle Hooks という非常に重要な機能があります。 これはデプロイの特定のステージ(例: トラフィック切り替え前)で、カスタムの 検証ステップ(AWS Lambda)を実行し、その検証が成功するまでデプロイの進行をブロックできる機能です。 フックの利用イメージ: 新しいGreen環境にトラフィックが流れる前に、Health Checkや簡単なSmoke Test用のLambdaを実行し、環境起因の設定ミスがないかを自動的に検証できます。 これにより、トラフィックを本番環境に切り替える前に、設定ミスなどによる障害リスクを大幅に低減し、より安全 にデプロイを進めることが可能になります。 Safieでは、簡単なSmoke Test用のLambdaを本番トラフィック切り替え前に実行し、チェックした上でデプロイできるように変更しました。 制限事項とハマりポイント 導入にあたり、いくつかの注意点と制限事項に直面しました。 1. B/G → ローリングへの切り替え時 B/Gデプロイ設定を解除しローリングに戻した直後のデプロイで、内部的にB/Gデプロイの設定(例: ロール)を参照し続けることがありました。この影響で、新規デプロイができなくなる事態が発生しました。(1敗) => 不必要なIAMロールの削除は慎重に行う 必要があります。 2. ALBリスナールールの制限 ALBの設定で、リスナールール数の上限があります(アカウントやリージョンにより異なる可能性あり)。B/Gデプロイではトラフィック切り替えにリスナールールを利用するため、多くのリスナールールを使用している既存のALBでは、B/Gデプロイの導入が難しい場合があります。 現状の課題と今後の展望 ローリングリリースから Blue/Greenデプロイに切り替えることで、素早いロールバック、デプロイ前の検証ができるようになりました。 ただし、検証はごく簡単なもので、特定のユースケースの考慮漏れをカバーできず、選択できるデプロイ戦略にも制限があります (例えば, CodeDeployと違いカナリアリリースはできない)*1 今後の展望として、E2E/統合テストとの組み合わせや、AWS ECS 今後の機能追加に合わせてより柔軟なデプロイができるように検討をすすめる予定です。 もし、似た課題をもつ方がいれば、導入を検討してみてはいかがでしょうか? おわりに 最後になりますが、セーフィーではエンジニアを積極的に募集しています。気になる方はこちらをご覧ください! https://safie.co.jp/teams/engineering/ カジュアル面談から受け付けておりますので、気軽に応募いただければと思います! 最後までお読みいただき、ありがとうございました。 *1 2025/10/30 ~ で AWS ECS は リニアデプロイ と カナリアデプロイのサポートを開始してます ( https://aws.amazon.com/jp/about-aws/whats-new/2025/10/amazon-ecs-built-in-linear-canary-deployments/ )
アバター
この記事は Safie Engineers' Blog! Advent Calendar  14日目の記事です はじめに はじめまして!エンジニアリングオフィスの山崎麻衣子( @ymzaki_m4 )と申します。 セーフィーには2025年3月に入社し、開発本部の組織開発全般に携わっています。 (エンジニアリングオフィスのお仕事については、昨年の記事も良ければご覧ください!) engineers.safie.link 今回は、私がこれまで取り組んだ 「組織の横連携強化」 に関する施策についてご紹介します。 組織の拡大によって生じ始めた課題と、その解消に向けた最初の一歩。組織マネジメントで同じような悩みを抱える方の、何かちょっとしたきっかけになれば幸いです。 はじめに 強力な組織の実現へ:課題を探る 本質的な課題を特定:浮き彫りになった2つの壁 「横のつながり」を生む:GL横断ミーティングの実行と設計 組織変革の第一歩と、次の目標 おわりに 強力な組織の実現へ:課題を探る 開発本部の今年度(FY25)の目標の1つとして、 「強力な組織」の実現 が掲げられていました。 セーフィーのさらなる事業成長を促進するためには、将来の成長を実現しうる仕込みや組織体制の整備を進める必要がありました。 そのためには、現状の成長速度に満足せず、組織文化の醸成、個人の成長支援、情報連携強化など、多面的な成長を強力に推し進めることが重要となります。 こうした状況下で入社した私がはじめに取り組んだことは、開発本部の部室長、グループリーダー(GL)との1on1で、 組織の現状を多角的に把握 することでした。 約3週間かけて計25名の部室長やGLに対してヒアリングを実施しました。 現在の組織の取り組みや課題などについて、採用、オンボーディング、育成、評価、文化醸成、開発プロセスなどの幅広い切り口でお伺いしました。 業務の合間を縫って快く応じてくださり、みなさんが抱いている想いや組織の現状を把握することができました。(ありがとうございました!) 本質的な課題を特定:浮き彫りになった2つの壁 ヒアリング内容は 構造化・抽象化 し、開発本部全体の本質的な課題を特定しました。 これらの課題を基に、解決のための道筋を考え、直近で実施すべき具体的な施策を策定しました。 まず、組織として課題感が大きいものを以下の4つのテーマに分類しました。 組織運営の基盤強化 人材育成・成長の仕組み化 業務効率と生産性向上 組織文化と帰属意識の醸成 さらに、短期・中長期に分類し、課題の関連性と施策の優先順位を探っていきました。 その結果、直近で取り組むべき課題が以下の2つであると特定するに至りました。 組織の明確な方向性・ビジョンが不足している 役割や権限が不明確で、横のつながりが薄い これらの2つの課題を解決していくにあたり、まずは「既存のリソースを活用してすぐに実施可能で、直接的に効果が見込めるもの」に取り組み、着実に成果へとつなげていきます。一方で、「根本的解決に時間を要し、新たな仕組みや文化の醸成が必要なもの」についてはバックキャスティングで計画を策定し、この両輪で課題解決を推進します。 そこで、まず着手したのが 「GLの横連携強化」 でした。 GLという組織の中核を担う存在が積極的に連携することで、組織の一体感を醸成し、グループを超えて成果や付加価値を創出し続けられる組織を目指したいと考えました。 また、GLの半数以上から共通課題として挙げられたのが「GL役割と権限の定義の曖昧さ」や「部門・チーム間の情報共有と連携の弱さ」であり、これらが組織の方向性の不明確さや横のつながりの希薄さにつながっているとも考えられました。 この短期的な基盤強化が、長期的な成長のための「土台」を固める上で必要だったのです。 開発本部FY25下期キックオフ(2025年7月開催)での説明資料より(一部改変) 「横のつながり」を生む:GL横断ミーティングの実行と設計 「GLの横連携強化」を進めるために、 「GL横断ミーティング」 の定期開催を企画しました。 この会議は「組織の中核を担うGLが、自身のチームを超えて組織全体を理解し、相互に協力しながら、より良い組織を共に創っていくための、 主体的な場 」と定義し、GLの中で有志を募って始めることとしました。 企画の検討にあたって特に意識した点は以下の4点です。 一方的な情報提供ではなく、双方向の対話やワークを中心とした設計にする 毎月の開発本部会で実施内容を全体共有することで、活動の透明性を高める 各回でテーマを明確に定め、明日からの活動につながるアウトプットを意識する アウトプットや実施後アンケートを踏まえ、会議自体の改善を繰り返す また、目指す状態とそのためのステップを意識しながら、段階的なロードマップを設定しました。 会議は8月から開始し、月1回以上の頻度で継続的に開催しました。 はじめは「相互理解と安心できる場の醸成」をテーマに、参加者同士の交流を重視しました。回を重ねるごとに「実践と改善のサイクル」を重視し、具体的な現場の課題解決のための議論や、マネジメントスキル向上を意識した学習を取り入れ、組織への貢献を視野に入れていきました。 11月末までで全6回開催しましたが、当初からGLのみなさんが意欲的に参加してくださり、会議への満足度が高い状態が維持できています。(ありがとうございます!!) また、要望も踏まえて会議の運営やコンテンツを調整することで、マネジメントスキル向上などへの貢献度も高められていることをアンケート結果を通じて感じることができました。 組織変革の第一歩と、次の目標 「GL横断ミーティング」は、GL同士が 部門やチームを超えて課題を共有し支え合う ための「横のつながり」を強化し、 ラインマネジメント能力の向上 という成果も出始めていると感じています。 今後はさらに、長期施策として 「組織ビジョンの策定と言語化」 を進めたいと考えています。 「実行力と創造力を兼ね備え、事業成長に必要な変化に迅速に対応できる組織」を目指し、その想いを言語化して社内外に示すことで、想いに共感したメンバーが集まる「異才一体」な強い組織となり、さらなる事業成長に貢献できるように取り組んでいきます! また、これまでの取り組みを通じて、セーフィーは 自ら考え、判断し、行動することで、自律的に価値を創造する メンバーのスタンスを大切にしてくれると強く感じることができました。 これからも組織課題に真摯に向き合い、改善プロセスを継続的に推進していきます。 おわりに 本記事では、私が取り組んだ組織変革の第一歩についてご紹介しました。 まだ道半ばでやりたいことも数多くあるので、いつか別の機会にご紹介できればと思います。 ついでに宣伝です!セーフィーでは 一緒に働く仲間を募集中 です! 少しでもご興味を持っていただけたら、ぜひ以下の採用サイトを覗いてみてください。 safie.co.jp 最後まで読んでいただき、ありがとうございました。
アバター
この記事は セーフィー株式会社 Advent Calendar 2025 の13日目の記事です!🎄 はじめに こんにちは! セーフィー株式会社 基幹システム開発部 戦略グループの佐々木です。 約1年前、セーフィーの中途採用の選考を受けようとしていた私は途方に暮れていました・・・ なぜなら 「セーフィーの基幹システム開発部とはなにか」 についての情報が、募集要項以外にほぼ見つけられなかったからです。 過去の私と同じように「基幹システム開発部に興味はあるけど、情報が全然見当たらないな・・」とお困りのみなさんへ、セーフィーの基幹システム開発部についてご紹介したいと思います! はじめに セーフィーにおける「基幹システム」 基幹システム開発部について 基幹システム開発部ってどんな雰囲気? おわりに セーフィーにおける「基幹システム」 みなさんは「基幹システム」と聞いてどんなシステムを思い浮かべるでしょうか? セーフィーにおいて、基幹システムは 「企業活動に必要不可欠な最小限の業務を取り扱うシステム」 と定義されています。 と言われても、ちょっとイメージがわきにくいですよね。 セーフィーは、クラウド録画サービスというSaaSビジネスを展開しつつ、カメラデバイスなどの「モノ(ハードウェア)」も取り扱っている点が特徴です。 そのため「企業活動に必要不可欠な最小限の業務」として、以下のような領域を定義しています。 「モノ」を管理する領域:SCM、サービス/製品保証管理 「サブスクリプション」を管理する領域:契約管理 「モノ」「サブスク」から請求を管理する領域:請求管理 営業活動を支える領域:SFA/CRM 企業活動の根幹を支える領域:会計管理、組織管理、データ分析/活用 では、これらの領域について、それぞれどのようなシステムを利用しているのでしょうか? 利用システムを領域ごとにグルーピングすると以下のような形となります。 このように様々なシステムを組み合わせながら、日々の業務、ひいては顧客のセーフィー利用を支えているのが、セーフィーにおける「基幹システム」です。 基幹システムについては過去のテックブログでも紹介していますので、興味を持たれた方はぜひこちらもご覧ください。 「モノ」×「サブスク」業務システム紹介! 基幹システム開発部について ここまでご紹介してきた基幹システムのうち、CRMシステム群、契約・請求・会計管理システム群を管理しているのが「基幹システム開発部」です。 (注:データ分析基盤はデータドリブン推進室が担当しており、そちらも過去記事で紹介されています。 What is データ分析基盤 ) 基幹システム開発部のMissionは 「持続可能で柔軟な基幹システムを提供し、企業の成長と経営効率化を支える」 であり、セーフィーのさらなる成長を支えるための基盤づくりに取り組んでいます。 基幹システム開発部は以下の3グループで構成されています。 開発第1グループ 開発第2グループ 戦略グループ それぞれのグループの役割は以下の通りです。 開発第1グループ 主にSalesforceに関する開発・保守を担当しています。 セーフィーではSalesforceを幅広い領域で活用しており、営業からカスタマーサポートまで、様々な部署の業務を支える大切な基盤となっています。 開発第1グループでは、社内の要望を元に開発・リリースを行っているほか、現在はSCM業務の改善を目的とした「山桜プロジェクト」(製品・在庫管理システムの強化)に内製で取り組んでおり、社内ユーザーであるCS本部のメンバーと密に連携しながら開発を進めています。 ユーザー部門と一体となってのプロジェクト運営は、セーフィーの基幹システム開発における大きな醍醐味のひとつです。 開発第2グループ 主にECサイトやZuora、請求書管理ロボに関する開発・保守を担当しています。 契約管理・請求管理という顧客影響が大きいシステムも担当しており、日々のデータの整合性確認、月次決済の確認といった重要な業務も多く担当しています。 ベトナム子会社の業務システム開発やECサイトの保守など、幅広い領域のプロジェクトを担当する開発第2グループですが、2026年からは会計領域へのSAP導入を行う「欅プロジェクト」も本格スタートし、セーフィーの成長を支えるための大きなプロジェクトが今後数年続く見込みです。 戦略グループ 主に基幹システム全体の戦略企画や、開発プロジェクトにおけるプロジェクトマネージャー(PM)としての役割を担当しています。 前述の山桜プロジェクト・欅プロジェクトのPMや、子会社の業務システム構築プロジェクトのPM、代理店向け自社開発システムのプロダクトマネージャー(PdM)といった形で、各メンバーがそれぞれのプロジェクトの推進役を担っており、部門横断的な課題の解決をリードしています。 基幹システム開発部ってどんな雰囲気? 私が入社後すぐに感じたのは、「落ち着いたおだやかな人が多い」雰囲気でした。 一方で、システムに関する議論ではそれぞれのバックグラウンドを基に様々な意見が飛び交い、よりよい基幹システム構築に向けた熱い思いを日々感じています。 また、育児中のメンバーも多く、裁量労働制をいかして柔軟に子育てと両立しているメンバーも多いです。 プロジェクト単位での活動が多い基幹システム開発部ですが、隔週で開催されている「基幹システム開発部会」では、全員が一言話す「チェックイン」が冒頭に設けられており、「子供の時の夏休みの思い出」「老後の夢、生き方」「便利なフリーソフト」など多岐にわたるテーマを元にお互いの人となりを知る良い機会となっています。 ボルダリング部に参加しているメンバーの報告や、お子さんとのトカゲの飼育エピソード、育休明けのパパメンバーと赤ちゃんの登場など、「異才一体」のセーフィーらしく、いろいろなジャンルのエピソードが飛び出す笑いが絶えない時間で、私も毎回とても楽しみにしています。 おわりに 基幹システム開発部についてのご紹介はいかがでしたでしょうか? この記事を読んだ皆さんの基幹システム開発部への解像度が少しでも上がっていたらうれしいです。 セーフィーのさらなる成長を支えるため、基幹システムは今まさに進化の時を迎えています・・・! さらなる成長を見据える 今のセーフィー「だからこそ」味わえるダイナミックな基幹システム再構築 に、あなたも一緒に取り組んでみませんか? 基幹システム開発部では、一緒に働いていただける仲間を絶賛募集中です! 興味を持っていただけた方は、ぜひ採用サイトもご覧ください。 safie.co.jp
アバター
この記事は Safie Engineers' Blog! Advent Calendar 12日目の記事です。 はじめに そのテストコード、まだ全部「手」で書いていますか? 今はどんな生成AIを使っているのか? なぜ「全自動」ではないのか? 実践!Claude Codeを使ったテスト半自動化4ステップ このワークフローがもたらした、時間以上の「価値」 おわりに:AIとの協業が当たり前になる未来へ はじめに こんにちは!セーフィー株式会社でサーバーサイドエンジニアをしている坂上( @Bobtaroh )です。新卒2年目となり、私を含め同期のエンジニアたちも、それぞれの配属先で一生懸命に業務をこなしています。 本記事では、同じく新卒2年目であるフロントエンド開発を担当している東條さんにインタビューを行い、開発現場で話題の「Claude Code」を活用した テスト半自動化のワークフロー について紹介します。 「AIに全部任せる」のではなく、あえて「半自動化」を選ぶその理由と、具体的な実践手法について深掘りしました。 そのテストコード、まだ全部「手」で書いていますか? テストコードは、アプリケーションの品質担保に不可欠ですが、その実装は時としてエンジニアの負担になることもあります。セーフィーのフロント開発チームでは、特に以下の課題がありました。 大規模なデザインシステム移行をやっていて、工数見積もり上、テストを後回しにしていた テストを実装する箇所多かった 配属されてから開発したコンポーネントが40~50ほどあり、1コンポーネントあたり1000行ほどのテストが必要だった テストの中には、作業に近い単純な実装が多く存在し、エンジニアのモチベーションがあまり上がりづらい側面も少々あった これらの課題を抱えていた時に、世間ではCoding Agentが注目を集め始め、弊社でも社内ツールとしてGeminiやClaude Codeの利用環境が整備されました。それを機に、今回のテストの半自動化ワークフローを組むことに至ったとのことでした。 今はどんな生成AIを使っているのか? 昨今、Claude Code以外のさまざまな生成AIが登場していますが、以下の4つの生成AIに関して、どのように使い分けているのか、東條さんに聞いてみました。 Gemini 相棒的な存在 Gemini単体でタスクをお願いすることはなく、調べるためのツールとして使うことが多い 会議中に出てきた分からないことについても聞くことがよくある Claude Code 部下に近い存在 以下の仕事を投げている テストの実装 新規開発の雛形作成 複雑な実装の解説依頼 Github Copilot 以前使っていたが、今は完全に使用していない Bedrock Chat Geminiが導入される前までは主流だったが、今は使用していない 2025年12月現在では、主にGeminiとClaude Codeを使用しているとのことでした。 なぜ「全自動」ではないのか? 現在のテスト半自動化に至るまでには様々な試行錯誤がありました。 弊社でClaude Codeが利用可能になる前までは、Github Copilot Agentを使って、One-shotのプロンプトでテストを生成させる手法を試みました。しかし、プロジェクト固有のルールが守られなかったり、意図しないコードが生成されたりと、結果として出力の6〜7割を人間が書き直すことになっていました。 Claude Codeが利用可能になった当初では、劇的な変化はありませんでした。Github CopilotでSonnet 4モデルを使用することができていたため、Github Copilotで培った仕組みをそのまま流用していたとのことです。hooksやmemoryの分散化の利用を試みましたが、管理が難しく、チーム開発になかなか向かないということがわかりました。 現在では、Subagentsを用いて、設計、実装、スタイルレビューと統合の4つに工程を分けることで、半自動化まで進めることができたとのことです。 実践!Claude Codeを使ったテスト半自動化4ステップ Claude Codeを使った半自動化の詳細説明をします。 この半自動化では、主に Subagents の機能を使っています。Subagentsとは、メインの会話に左右されずに特定のコンテキストを保持し、適切な権限のものと、特定の領域に特化して精度を向上させることができる再利用可能なエージェントのことです。公式サイト( https://code.claude.com/docs/ja/sub-agents )では、以下の4つの利点が挙げられています。 コンテキストの保持 特化した専門知識 再利用性 柔軟な権限 この機能とClaude Codeのファイル仕様に従って、以下のファイル構成で実現しています。 .claude ├── agents # Agentの定義が詰まったディレクトリ │ ├── consolidate-text-fixes.md │ ├── generate-test-spec.md │ ├── implement-test.md │ ├── investigate-test-pattern.md │ ├── review-test-first.md │ ├── review-test-second.md │ ├── review-test-third.md │ └── search-translation-key.md └── commands # ワークフロー(コマンド)を実行させる定義が詰まったディレクトリ └── test-workflow.md commandsディレクトリで定義したワークフロー(test-worklow.md)では、以下のような構成になっています。 🔽実際のtest-workflow.mdの中身を見る。(ここをクリック!) --- name: test-workflow description: テスト設計・実装・レビューの一連のワークフロー --- # テスト実装ワークフロー管理エージェント このエージェントは、テストの設計から実装、レビューまでの一連のワークフローを管理します。**各フェーズで適切なサブエージェントを呼び出し**、高品質なテストコードを作成します。 ## ワークフロー概要 1. **テスト設計** → 2. **テスト実装** → 3A. **並列テストレビュー** → 3B. **統合修正・最終調整** → 4. **最終確認** テスト設計後にユーザーレビューを求め、ユーザーの修正後承認を得てからテスト実装以降のフェーズに進みます。 ## 実行手順 ### Phase 1: テスト設計 📝 **使用エージェント**: `generate-test-spec` 1. 対象ファイルを分析 2. テスト設計書を作成(`<fileName>テスト設計書.md`) 3. **ユーザーに設計書のレビュー・修正を依頼** 4. 承認を得たら次フェーズへ ### Phase 2: テスト実装 💻 **使用エージェント**: `implement-test` 1. テスト設計書を基にテストコードを実装 2. テストを実行してカバレッジ確認 3. エラーがある場合は修正(最大3回試行) ### Phase 3A: 並列テストレビュー 🔍 3つのテストレビューエージェントを**並列実行**して、その結果を結合した問題の分析と改善方針を1つ作成します: #### Review 1: 基本修正分析 **使用エージェント**: `review-test-first` - 動的import、getState、非同期処理などの基本的な問題を分析 - 改善方針と具体的な修正内容を提案 - **ファイル修正やテスト実行は行わない** #### Review 2: DOM・マッチャー・設定分析 **使用エージェント**: `review-test-second` - detectChanges、translate関数、MockStoreValueなどの問題を分析 - 改善方針と具体的な修正内容を提案 - **ファイル修正やテスト実行は行わない** #### Review 3: spyOn・配列・追加ルール分析 **使用エージェント**: `review-test-third` - 専用マッチャー、spyOn配置、DOM要素取得などの問題を分析 - 改善方針と具体的な修正内容を提案 - **ファイル修正やテスト実行は行わない** #### レビュー結果ファイル生成 - レビュー結果ファイル名: `<fileName>レビュー結果.md` - 管理のためレビュー対象ファイルと同ディレクトリに生成 - 加工は行わず各レビューエージェントから返却された結果を結合する ### Phase 3B: 統合修正・最終調整 🛠️ **使用エージェント**: `consolidate-test-fixes` - **前フェーズで作成したレビュー結果ファイルを渡します** 1. **実際の修正実行**: 統合された修正計画に従ってファイルを修正 2. **テスト実行**: 修正後のテストを実行して動作確認 3. **最終調整**: エラーがあれば原因分析して再修正 4. **最終確認**: もう一度並列テストレビューを行い、修正が完了しているかを確認する 5. **レビューファイルの削除**: 不要になったレビューファイルを削除する ### Phase 4: 実装最終確認 ✅️ 実装内容が設計書から乖離した内容になっていないかを確認 ## 注意事項 ### 各フェーズ共通 - エラーや問題が発生した場合は詳細をユーザーに報告 ### テスト実行コマンド ```bash npm run test <project> --specs=<testFile> --agent # e.g. npm run test viewer --specs=hoge.component --agent ``` YOU MUST: **テスト実行時は必ず`npm run test <project> --specs=<testFile> --agent`を使用し、他のコマンド実行を試みないこと、特に`--agent`オプションを追加しないとテストが終了しません** - `<project>`: プロジェクト名(`viewer`, `myportal`, `core`, `sdk`のいずれか) - `<testFile>`: `.spec.ts`を除くテストファイルパス。例:`example.component` ### 品質基準 - カバレッジ: 95%以上 - すべてのテストケースが成功すること - コードスタイルがプロジェクトの規約に準拠していること ## トラブルシューティング ### テストが失敗する場合 1. エラーメッセージを確認 2. consolidate-test-fixesエージェントで再修正を試行 3. 3回試行しても解決しない場合は`investigate-test-pattern`エージェントを使用して同様の実装がないか調査してから再修正 ### レビューで問題が見つからない場合 1. 該当するレビューエージェント(review-test-\*)を再実行 2. 特定の問題に焦点を当ててレビューを指示 3. 新たなレビュー結果でconsolidate-test-fixesを再実行 全体の流れとしては、以下の4ステップです。 テスト設計 まず generate-test-spec エージェントが対象コードを分析し、テスト設計書を作成します。ここで重要なのが 「人間の承認」 を必須にしている点です。設計書がプロジェクトの意図と合致しているかをエンジニアが確認し、修正を経てOKが出て初めて実装フェーズへ進むようにしています。 テスト実装 承認された設計書を元に、 implement-test エージェントがコーディングを行います。ここではテストの実行とカバレッジ確認までを自律的に行い、エラーが出れば自己修正するように指示しています。 ここで工夫した点は、実装内容に直接関係しない弊社特有のルール(命名規則やスタイル)はあえてこの工程では指示しないことです。ルールを厳格に守らせようとすると、肝心な処理に関わるロジックの精度が落ちてしまう傾向があった経験があったためです。これによって、まずは純粋な処理のみに集中させることで、本来のコーディングスキルを存分に発揮してもらい、 Claude Codeが自信を持って 実装してもらえるようになったとのことです。 並列トリプルレビュー 実装されたコードに対し、3つの異なる専門家エージェントが 並列で レビューを行います。 Reviewer A: 基本的な構文や非同期処理のチェック Reviewer B: DOM操作やマッチャーの正確性チェック Reviewer C: SpyOnやモック定義など、高度なルールのチェック この過程では、2番目のステップの出力結果をもとに、処理内容には関係ない弊社特有のルールに沿っているかの確認のレビューを行い、提案のみを行います。コード編集は行いません。 これによって、以下の3つのメリットがあったとのことです。 複数の項目をAgentに投げると、細かい見落としが多くなる傾向にあったが、Subagentsで分けることで、特定の観点に絞ることができ、精度が上がった 並列実行することで、実行速度が向上した 処理内容のレビューは考慮せずに、スタイルレビューに限定させたことで、この過程で弊社特有のルールや表現を入れ込むことができた 統合修正・最終調整 3匹のレビュアーから上がってきた指摘事項を consolidate-test-fixes エージェントが集約し、実際のコード修正に適用します。最後に改めてテストを回し、設計書との乖離がないかを確認して完了となります。 このワークフローがもたらした、時間以上の「価値」 この「半自動化」を導入した結果、テスト実装にかかる時間は 体感で約20%短縮 されました。数字だけ見ると劇的な変化ではないように見えるかもしれません。しかし、東條さんは「時間短縮以上の価値がある」と語ります。 並行処理による生産性向上 AIがコードを書いている間、人間はSlackの返信やBacklogの整理、あるいは他の複雑な実装の考察など、別のタスクをこなせるようになりました。0から1の面倒な記述をAIが肩代わりしてくれることで、脳のメモリを他の業務に割けるようになったそうです。 品質の向上 AIとの対話の中で、「そのエッジケースは考慮しましたか?」と逆に指摘されることもあり、一人で書くよりもテストの網羅性が上がったといいます。 おわりに:AIとの協業が当たり前になる未来へ 今回は、新卒2年目が実践する、生成AIを用いたテスト半自動化ワークフローについて紹介しました。今回紹介したワークフローを導入したことで、2025年6月〜2025年12月までに完了するという目標だった予定が、2ヶ月早く終えることができたとのことです。 今後は、このノウハウをチーム全体から開発部署全体に広げつつ、さらなる精度の向上に取り組んでいきたいとのことです。 また、生成AIをテスト自動化以外のことで使っていきたい分野はあるかを聞いてみました。 「勉強に使いたいなと思っています!10時に出勤して、10時〜12時はエージェントに任せている間に、優雅にコーヒーを飲みながら最新技術のキャッチアップに費やしたいなと思ってます。そして、もっと使うエージェントを増やして、10や20匹のAgentに対して指示を出して、操ってみたいなという野望もあります。」 最後に、これからAIを使いこなしたいと考えている後輩や学生エンジニアへのアドバイスをもらいました。 「使いこなし方こそ、生成AIに聞いたら良いと思います!笑。バイブコーディングが流行っていますが、やはり、読む力は引き続き必要になっていくのではないかと思っています。大切なのは、AIに使われるのではなく、使いこなす側になること。AIが出してきた成果物に対して、自分の頭で考え、疑い、責任を持つ。エンジニアとしての『思考力』が、これからの時代こそ重要になると思います。」 AIはあくまでツールであり、最後に品質を保証するのはエンジニア自身です。この当たり前の原則を忘れずに、AIを「賢い部下」のような立ち位置で活用し、協力していくスタイルこそが、これからの開発のスタンダードになっていくのかもしれません。 いま改めて、AIとの向き合い方を意識的に考えることが最も重要だと気付かされました。 以上、新卒2年目が実践するテスト半自動化に関するご紹介でした。
アバター
この記事は  Safie Engineers' Blog! Advent Calendar 2025 11日目の記事です。 はじめに 今年も残すところあとわずかとなりましたね。AI開発部の田中です。 現在は、複数のカメラ映像を統合して解析するシステムのR&D(研究開発)に取り組んでいます。 画像認識AIの分野ではPythonが主流ですが、 「Pythonでロジックを書いたら、処理が遅すぎてリアルタイム性が確保できない」 という壁にぶつかったことはありませんか? 今回は、6台のカメラを使った リアルタイムマルチカメラトラッキング を実装する過程でまさにその問題に直面し、一部の処理を Rust に置き換えることで劇的な高速化を実現した話を紹介します。 「Rustは難しそう」と思っている方でも、Pythonと連携させるだけなら意外とハードルは高くありません。ぜひ最後までお付き合いください。 はじめに リアルタイムマルチカメラトラッキングとは システム構成と技術スタック 1. 推論部(映像のデコード・AI推論) 2. 統合部(トラッキング・ID統合) Pythonでの限界とRustへの転換 なぜRustを選んだか PyO3とmaturinを使った実装フロー 前提ツール 1. プロジェクトのセットアップ 2. Rustコードの実装 (src/lib.rs) 3. Pythonからの呼び出し (main.py) 4. ビルドと実行 導入結果:10倍以上の高速化を実現 まとめ リアルタイムマルチカメラトラッキングとは リアルタイムマルチカメラトラッキング(オンラインマルチカメラトラッキング)とは、RTSPなどで接続した複数のカメラから現在の映像を取得し、リアルタイムに以下の処理を行う技術です。 検出 : カメラごとに被写体の領域(バウンディングボックス)を検出 追跡 : カメラ内で被写体を追跡し、カメラ内のIDを割り当て 特徴抽出 : 被写体の見た目の特徴量(ReID特徴量など)を抽出 統合 : 全カメラの情報を統合し、エリア全体で「被写体がどこにいるか」を一意に特定 これらを 「次のフレームの映像が来る前」 に完了させる必要があります。 今回目指したスペックは、 「ネットワークカメラ6台、各20FPS」 です。つまり、全カメラの処理を 50ミリ秒(1000ms / 20fps)以内 に完了させなければ、遅延が発生してしまいます。 システム構成と技術スタック システムの大まかな構成図は以下の通りです。 処理は大きく「推論部」と「統合部」に分かれています。 1. 推論部(映像のデコード・AI推論) GPUリソースを最大限に活かすため、NVIDIAの技術を採用しています。 DeepStream SDK GStreamerベースのマルチメディアフレームワーク。動画のデコードやDNN推論をパイプライン処理で高速に実行可能です。 DeepStream Python Apps DeepStreamをPythonから制御するためのバインディングです。 【出典】NVIDIA-AI-IOT, deepstream_python_apps, GitHub https://github.com/NVIDIA-AI-IOT/deepstream_python_apps (2025年11月23日アクセス) 2. 統合部(トラッキング・ID統合) 推論結果を受け取り、ロジックベースで被写体の同一性判定を行います。 Python : 全体の制御・グルーコード Rust : 計算コストの高いマッチング処理を担当 PyO3 & maturin : PythonとRustを繋ぐためのツール群 【出典】PyO3 Project, PyO3 User Guide, Webページ https://pyo3.rs/main/index.html (2025年12月10日アクセス) 【出典】PyO3 Project, maturin, GitHub https://github.com/PyO3/maturin (2025年12月10日アクセス) Pythonでの限界とRustへの転換 推論部に関しては、DeepStreamの恩恵によりPythonバインディング経由でも十分高速でした。 問題が発生したのは、後段の「統合部」です。 複数のカメラから検出された被写体の位置や特徴量を総当たりで比較し、同一被写体判定を行う処理(マッチング処理)において、Pythonの処理速度がボトルネックになりました。 カメラが増え、被写体が増えれば増えるほど計算量は指数関数的に増加します。 純粋なPython実装でテストしたところ、被写体が多いシーンでは処理に 数百ミリ秒 かかってしまいました。目標の50ミリ秒を大幅に超えており、映像はカクつき、処理落ちは避けられない状態です。 なぜRustを選んだか NumPyなどを使ったベクトル化も検討しましたが、条件分岐を含む複雑なロジックだったため、コンパイル言語への置き換えを検討しました。そこで白羽の矢が立ったのが Rust です。 C++並みの高速性 : メモリ安全性を担保しながら爆速で動く。 PyO3のエコシステム : Pythonとの連携が非常に容易で、既存のPythonコードの一部だけをRust関数に置き換える「部分的な導入」がしやすい。 「全体を書き直すのは無理だが、重いループ処理だけRustに任せよう」という戦略です。 PyO3とmaturinを使った実装フロー ここからは、実際に今回採用した開発フローを紹介します。 パッケージ管理には、最近話題の高速なツール uv を使用しました。 前提ツール uv : Pythonのパッケージ管理・プロジェクト管理ツール Rust : コンパイラ言語(cargoなどが含まれます) 1. プロジェクトのセットアップ maturin を使えば、Rustのプロジェクト作成からPythonパッケージとしてのビルドまで一気通貫で行えます。 # 作業ディレクトリへ移動 cd ${WORK_DIR} # maturinのインストール(uv toolを使用) uv tool install maturin # PyO3バインディングを含む新規プロジェクト作成 # --mixed オプションでPythonとRustのハイブリッド構成を作成 uvx maturin new -b pyo3 --mixed study_maturin cd study_maturin # 仮想環境の作成(Python 3.12指定) uv venv -p 3 . 12 2. Rustコードの実装 (src/lib.rs) デフォルトで生成されるコードです。 use pyo3 :: prelude :: * ; /// 2つの数値を足して文字列として返す関数 #[pyfunction] fn sum_as_string (a: usize , b: usize ) -> PyResult < String > { Ok ((a + b). to_string ()) } /// Pythonモジュールとしての定義 #[pymodule] fn study_maturin (m: & Bound < '_ , PyModule > ) -> PyResult < () > { // Pythonから呼べる関数として登録 m. add_function ( wrap_pyfunction! (sum_as_string, m) ? ) ? ; Ok (()) } 3. Pythonからの呼び出し (main.py) ビルドされたRustの関数は、通常のPythonモジュールと同じようにimportして使用できます。 import study_maturin def main (): a = 2 b = 3 # Rustで実装された関数を呼び出し result = study_maturin.sum_as_string(a, b) print (f "The sum of {a} and {b} is: {result}" ) if __name__ == "__main__" : main() 4. ビルドと実行 開発モード( maturin develop )でビルドすると、現在の仮想環境にパッケージがインストールされます。本番性能を出すため --release を付けています。 # キャッシュクリア(念のため) uv cache clean # リリースビルドでインストール uvx maturin develop --release # Pythonスクリプト実行 uv run main.py 実行結果: The sum of 2 and 3 is: 5 このように、非常に少ない手順でRustの関数をPythonから呼び出すことができました。 今回は単純な足し算ですが、実際の業務ではここに「マルチカメラのID統合ロジック」を実装しました。 Rust側で構造体を定義してPythonのクラスとして扱ったり、型ヒント(.pyiファイル)を生成してVSCodeでの補完を効かせたりすることも可能です。 導入結果:10倍以上の高速化を実現 統合部のボトルネックとなっていた解析ロジックをRustに置き換えた結果、パフォーマンスは劇的に改善しました。 Before (Python) : 1フレームあたり 300ms超 (ピーク時) After (Rust) : 1フレームあたり 30ms以内 約10倍以上の高速化 を達成し、目標としていた「6台・20FPS(50ms以内)」の処理時間を余裕を持ってクリアすることができました。これにより、リアルタイムで滑らかなトラッキングが実現できています。 まとめ 「Pythonは遅い」と諦める前に、ボトルネック部分だけをRustにオフロードするアプローチは非常に有効でした。 特に PyO3 と maturin (そして uv )のエコシステムのおかげで、PythonエンジニアにとってもRust導入の敷居はかなり低くなっていると感じます。 今回はロジックの一部のみの置き換えでしたが、今後はRustで処理する領域を広げ、システム全体の堅牢性とパフォーマンス向上に挑戦していきたいと思います。
アバター
この記事は Safie Engineers' Blog! Advent Calendar 10日目の記事です こんにちは!Safie(セーフィー)でモバイルアプリの開発をしている河原( @rui_qma )です。 🚀 はじめに:なぜComposeでドラッグ&ドロップが必要なのか 従来のViewシステムとの壁 AndroidチームではAndroid ViewベースのUIから Jetpack Compose ベースのUIへの移行を進めています。 弊社のプロダクト「 Safie Viewer (セーフィー ビューアー)」のカメラ一覧画面には、カメラの順番をドラッグ&ドロップ(D&D)によって並び替える機能があります。 従来のAndroid Viewでは、 RecyclerView に ItemTouchHelper を組み合わせることで、D&Dによる並び替えが公式APIとして簡単に実現できていました。 しかし、Jetpack Composeでは、2025年11月現在、 LazyVerticalGrid (または LazyColumn )内で同様のD&D並び替え機能を「公式APIだけで」実現するための専用のコンポーネントはまだ提供されていません。( composeBom : 2025.11.01で確認) 本記事では、この「Composeでドラッグ&ドロップによる並び替えが可能なグリッド」を、公式API未提供の中でどのように作り込むか、その設計と実装手法について解説します。 🚀 はじめに:なぜComposeでドラッグ&ドロップが必要なのか 従来のViewシステムとの壁 ✨ 実現したい機能の要件定義 📦 Step0: 土台となるComposableを用意 👆Step1: 要素の長押し検出と触覚フィードバック 実装のポイント ↔️Step2: 要素をドラッグして移動する 実装のポイント 🔄Step3: ドラッグによる要素の入れ替え処理 実装のポイント 💫Step4: 入れ替わる際のアニメーションを実装 実装のポイント ⬇️Step5: リストの上下端到達時の自動スクロール 実装のポイント 🎉 まとめと今後の展望 👀将来的な見通し 💻 実装コード ✨ 実現したい機能の要件定義 今回の記事で実装を目指すD&D並び替えグリッドの要件は以下の5点です。 要素を 長押し したタイミングでD&D可能状態にし、 触覚フィードバック を実行する。 ドラッグに沿って要素を移動させる。 順番の入れ替わりが発生した場合、 アニメーション で視覚的に表現する。 ドラッグ位置がリストの上下端に達した場合、 リスト自体を自動でスクロール する。 ドラッグ終了時に要素を定位置に戻す。 これらの機能を、土台となるComposableから段階的に実装していきます。 📦 Step0: 土台となるComposableを用意 今回の設計方針としてLazyVerticalGridをラップするComposableを作成し(DraggableGridと命名)、このComposableの中で状態の管理及びD&D機能の拡張を進めていきます。 @Composable fun DraggableGrid( list: List < String >, modifier: Modifier = Modifier, itemContent: @Composable (item: Camera, modifier: Modifier) -> Unit , ) { val lazyGridState = rememberLazyGridState() LazyVerticalGrid( modifier = modifier, columns = GridCells.Fixed( 3 ), state = lazyGridState, horizontalArrangement = Arrangement.spacedBy( 12 .dp), verticalArrangement = Arrangement.spacedBy( 12 .dp), ) { items(list) { id, modifier -> itemContent(id, modifier) } } } 👆Step1: 要素の長押し検出と触覚フィードバック 要素の長押しを検知する処理を実装します。 実装のポイント ドラッグ対象のCompose要素に Modifier.pointerInput を追加 pointerInput内でジェスチャー検出関数 detectDragGesturesAfterLongPress で長押しを検出 onDragStart に長押し時の処理、 onDrag にドラッグ中の処理を実装する 触覚フィードバックは performHapticFeedback を呼び出すことで動作するため、以下のように実装することによってユーザーに長押し状態であることを知らせることができます。 Developers ... val haptic = LocalHapticFeedback.current LazyVerticalGrid( modifier = modifier.pointerInput( Unit ) { detectDragGesturesAfterLongPress( onDragStart = { offset -> // ドラッグ開始時に行いたい処理をここに実装する haptic.performHapticFeedback(HapticFeedbackType.LongPress) // 触覚フィードバック }, onDrag = { change, dragAmount -> // ドラッグ中に行いたい処理をここに実装する change.consume() // ドラッグイベントの消費 }, onDragEnd = { // ドラッグ終了時に行いたい処理をここに実装する }, onDragCancel = { // ドラッグがキャンセルされた時に行いたい処理をここに実装する } ) }, ... ↔️Step2: 要素をドラッグして移動する Step1で記述したdetectDragGesturesAfterLongPressのコールバックにドラッグに関する処理を実装していきます。 実装のポイント onDragStartでドラッグ対象のインデックスを取得 onDragでドラッグ量を加算して保持 ドラッグ対象要素に対して Modifier.offset でドラッグ量を指定して移動 onDragEnd, onDragCancelでドラッグ量を初期化 (今回は簡単に実装するためにComposable内でインデックスやオフセット等の状態を管理していますが、Stateクラスの作成などViewとは分離して管理することをオススメします) val density = LocalDensity.current var draggingIndex by remember { mutableIntStateOf( 0 ) } var dragOffset by remember { mutableStateOf(Offset.Zero) } LazyVerticalGrid( modifier = modifier.pointerInput( Unit ) { detectDragGesturesAfterLongPress( onDragStart = { offset -> // ドラッグ開始位置のインデックスを取得 draggingIndex = lazyGridState.layoutInfo.visibleItemsInfo.find { info -> val rect = Rect(info.offset.toOffset(), info.size.toSize()) rect.contains(offset) }?.index ?: - 1 haptic.performHapticFeedback(HapticFeedbackType.LongPress) // 触覚フィードバック }, onDrag = { change, dragAmount -> dragOffset + = dragAmount change.consume() // ドラッグイベントの消費 }, onDragEnd = { dragOffset = Offset.Zero }, onDragCancel = { dragOffset = Offset.Zero } ) }, ... ) { itemsIndexed( items = list, key = { index, item -> item }, ) { index: Int , item: String -> val dragging = draggingIndex == index val modifier = if (dragging) { Modifier .offset( x = with(density) { dragOffset.x.toDp() }, y = with(density) { dragOffset.y.toDp() }, ) .zIndex( 1f ) // ドラッグ中のアイテムを前面に表示 } else { Modifier } itemContent(item, modifier) } } 🔄Step3: ドラッグによる要素の入れ替え処理 ドラッグ中アイテムの 中心位置 が他要素の領域に入ったタイミングで、リストの順番を入れ替える処理を実行します。 実装のポイント 要素入れ替えコールバック(ex: onListChange)を定義し、並び替えの処理を実装する ドラッグ中アイテムの中心位置を算出する 中心位置が他要素の領域内に存在する場合、要素入れ替えコールバックを実行する 実際の入れ替えロジックは複雑になるため、ここでは詳細を割愛しますが、リスト変更を親に委ねるコールバック設計が重要です。詳細な実装は コチラ を参照ください。 💫Step4: 入れ替わる際のアニメーションを実装 要素が入れ替わったことをユーザーにわかりやすく伝えるため、アニメーションを追加します。 このStepに関しては標準で提供されている機能を利用することで、手軽に実装することができます。 実装のポイント アイテムにキーを設定する 入れ替えられる側の要素に Modifier.animateItem() を設定する itemsIndexed( items = list, key = { index, item -> item }, // Compose側でアイテムを特定するためのキー ) { index: Int , item: String -> val dragging = draggableGridState.draggingIndex == index val offset = draggableGridState.dragOffset val modifier = if (dragging) { Modifier .offset( x = with(density) { offset.x.toDp() }, y = with(density) { offset.y.toDp() }, ) .zIndex( 1f ) } else { Modifier.animateItem() // 入れ替えられる側にアニメーションを設定 } itemContent(item, modifier) } ⬇️Step5: リストの上下端到達時の自動スクロール 例えば「1番目から100番目に移動したい」といった大幅な移動を行いたい場合、現状は1回のドラッグにつき最大1画面分しか移動できないため、複数回のドラッグが必要になります。 この問題を解決するため、ドラッグによってリストの上下端まで持っていった場合はリスト自体のスクロールも行う機能を実装します。 実装のポイント ドラッグ中の要素がリストの上下端からどれだけはみ出しているかを算出する はみ出している場合は、はみ出した量に応じたY軸移動量を算出する リストと要素の両方を同時にY軸移動量分だけ移動する(同期する) この実装はパフォーマンスと競合回避の観点から複雑な部分であり、スクロールと要素移動を同時に行う同期処理に工夫が必要です。詳細な実装は コチラ を参照ください。 🎉 まとめと今後の展望 本記事では、Jetpack Composeにおいて標準で提供されていない機能を、 Modifier.pointerInput を活用してゼロから作り込む手法を解説しました。 タッチイベントの検出、オフセットによる位置調整、 Modifier.animateItem() によるアニメーション、そして自動スクロールを組み合わせることで、従来のAndroid Viewの RecyclerView + ItemTouchHelper に匹敵する、高度なUXを実現することができました。 この実装には手探りの部分が多く、リファクタリングやパフォーマンス改善の余地は十分にあると考えていますが、本記事が皆様のCompose実装の手助けとなれば幸いです。 👀将来的な見通し 執筆時点では自前での実装が必要でしたが、ロードマップにはドラッグ&ドロップに関する記載があるため、将来的には標準機能としてサポートされる可能性が高いです。 出典:Jetpack Compose のロードマップ https://developer.android.com/jetpack/androidx/compose-roadmap?hl=ja (2025年12月3日アクセス) それまでは今回解説したような自前の実装を行うか、あるいは、従来のAndroid Viewの ItemTouchHelper を ComposeView でホストして利用するという選択肢も一つの回避策となります。状況に応じて、最適なアプローチを選択してください。 💻 実装コード 執筆にあたって実装したコード全体は、以下のGitHubリポジトリで公開しています。是非参考にしてください。 https://github.com/ruiqma/DraggableGridSample モバイルチームは開発する仲間を募集しています! open.talentio.com open.talentio.com
アバター