UPSIDER Tech Blog

導入して実感したArrow-ktの良さ

こんにちは! 株式会社UPSIDERの支払い.comでバックエンドエンジニアを担当しているふっくです。

今回は、支払い.comバックエンドで導入した関数型にインスパイアされたKotlinライブラリ「Arrow-kt」を紹介します。

Arrow-ktとは?

Arrow-kt は、Kotlinに関数型プログラミングの強力な機能をもたらすライブラリセットです。特定の目的を持つ複数のライブラリとして提供されています (Overview of libraries)。

主なライブラリは以下の通りです。

  • arrow-core : 型安全なエラー処理を実現する EitherOptionRaise などを提供する中核的なライブラリです。 私たちのプロジェクトでは、現在この arrow-core を利用しています
  • arrow-fx-coroutines : Kotlin Coroutinesと連携し、高レベルな並行処理やサスペンドコンテキストでのリソース管理を提供します。
  • arrow-autoclose : arrow-fx-coroutines のリソース管理機能を、Kotlin Coroutinesに依存せずに利用可能にします。
  • arrow-optics : 不変データ構造に対するアクセスや変換を容易にするための抽象化( LensPrismTraversal など)を提供します。

Arrow-ktは、関数型プログラミングの概念をKotlinのイディオムに沿った形で提供することを目指しており、Kotlinプログラマーにとって自然に導入しやすいように設計されています。

💡私たちのプロジェクトでは当初エラー処理向けに kotlin-result を使っていましたが、更新作業(1系→2系)にて他ライブラリとの互換性の問題が発生しました 😢(issue) この問題に対処するために、同様の機能を提供するArrow-ktを導入しました。結果として、EitherはもちろんRaiseをはじめとするArrow-ktのエコシステムの拡張性はチーム内で好評であり、良い移行となったと感じています。

Arrow-ktを使ってみる

今回は簡単な「パスワード強度検証クラス」の実装を通して、arrow-core の中心的な機能を紹介します。

以下の PasswordStrengthValidator クラスは、パスワードが特定の要件(最低長、大文字・小文字・数字・特殊文字の有無)を満たしているかを検証します。 ここでは、エラーハンドリングのアプローチが異なる2つのメソッドを実装してみました。

  • validate : Kotlin標準のResultを使用した方法
  • validateArrowKt : Arrow-ktの Either を使用した方法

💡 Arrow-ktの Either<L, R> は、「L型またはR型のどちらかを持つ値である」ということを型レベルで保証するものです。慣習として、L(Left)をエラーケース、R(Right)を正常系の値を表すために用いることが多いです。

class PasswordStrengthValidator {
  // Arrow-kt使わないパターン
  fun validate(password: String): Result<String> {
    if (!hasMinLength(password)) {
        return Result.failure(ValidationException(ValidationError.TooShort))
    }
    if (!hasUpperCase(password)) {
        return Result.failure(ValidationException(ValidationError.NoUpperCase))
    }
    if (!hasLowerCase(password)) {
        return Result.failure(ValidationException(ValidationError.NoLowerCase))
    }
    if (!hasDigit(password)) {
        return Result.failure(ValidationException(ValidationError.NoDigit))
    }
    if (!hasSpecialChar(password)) {
        return Result.failure(ValidationException(ValidationError.NoSpecialChar))
    }
    return Result.success(password)
  }

  // Arrow-kt使うパターン
  fun validateArrowKt(password: String): Either<ValidationError, String> = either {
    ensure(hasMinLength(password)) { ValidationError.TooShort }
    ensure(hasUpperCase(password)) { ValidationError.NoUpperCase }
    ensure(hasLowerCase(password)) { ValidationError.NoLowerCase }
    ensure(hasDigit(password)) { ValidationError.NoDigit }
    ensure(hasSpecialChar(password)) { ValidationError.NoSpecialChar }
    // エラーを直接返すことも可能: raise(ValidationError.NoSpecialChar)
    password
  }

  private fun hasMinLength(password: String) = password.length >= 8
  private fun hasUpperCase(password: String) = password.any { it.isUpperCase() }
  private fun hasLowerCase(password: String) = password.any { it.isLowerCase() }
  private fun hasDigit(password: String) = password.any { it.isDigit() }
  private fun hasSpecialChar(password: String) = password.any { !it.isLetterOrDigit() }

  class ValidationException(val detail: ValidationError) : Exception(detail.message)
  sealed class ValidationError(val message: String) {
    data object TooShort : ValidationError("Too short message.")
    data object NoUpperCase : ValidationError("No uppercase message.")
    data object NoLowerCase : ValidationError("No lowercase message.")
    data object NoDigit : ValidationError("No digit message.")
    data object NoSpecialChar : ValidationError("No special char message.")
  }
}

次に、これらのメソッドをどのように呼び出し、結果を処理するかを見てみましょう。

Kotlin標準のResult

val response = validator.validate(password = "P@ssw0rd")
...
response.onSuccess { pwd -> /** 成功時の処理 */ } // onFailure { cause: Throwable -> }
response.fold(
  onSuccess = { pwd -> /** 成功時の処理 */ },
  onFailure = { cause: Throwable -> /** 失敗時の処理 */ },
)
response.map { pwd -> /** 成功時の処理 */ }

Arrow-ktのEither

val response = validator.validateArrowKt(password = "P@ssw0rd")
response.getOrNull() // leftOrNull()
response.onRight { pwd -> /** 成功時の処理 */ } // onLeft { cause: ValidationError -> }
response.fold(
  ifLeft = { cause: ValidationError -> /** 失敗時の処理 */ },
  ifRight = { pwd -> /** 成功時の処理 */ },
)
response.map { pwd -> /** 成功時の処理 */ } // mapLeft { cause: ValidationError -> }

// 👇 EitherをeitherのRaiseレシーバ付きラムダ内で利用する例 (ユーザ作成)
val userResponse: Either<RegistrationError, User> = either {
  ensureNotNull(param.email) { RegistrationError.InvalidEmail }

  val validatedPassword = validator.validateArrowKt(param.password)
    .mapLeft { cause -> RegistrationError.InvalidPassword(cause.message) }
    .bind()
  userRepository.insert(User.create(param.email, validatedPassword))
    .mapLeft { RegistrationError.SystemError }
    .bind()
}

Arrow-ktの Either を使うと、主に以下の点が嬉しいです 👌

  • エラーハンドリングの分離と可読性: either メソッドが引数に取る Raise レシーバ付きラムダ内では、ensurebind を使うことができます。これらを使うことで、連鎖的な処理の中でエラーを切り離し、ハッピーパスに焦点を当てた実装ができます 💪
  • 型安全なエラー表現: Either 型は任意の型をエラーとして扱えるため、ValidationError のような独自のエラー型を型安全に表現できます。また、foldmap などの関数型の操作も可能で、エラー処理の柔軟性が高いです 💪

このように、Either 型は型安全性と表現力の両方を兼ね備えているという点が大きなメリットです。

あとがき

今回は小さなサンプルコードを使ってArrow-ktを紹介しました。

Arrow-ktは関数型のライブラリですが、既存のコードベースにも段階的に導入しやすく、Kotlinの記述スタイルを損なうことなく型安全なエラーハンドリングのメリットを享受できます。もちろん、関数型に寄せたコーディングをするサポートとしても強力です。 私たちはKtorプロジェクトで導入しましたが、Androidやその他のKotlinプロジェクトでも同様の恩恵を受けられると思います。APIクライアントやデータベース操作など、エラー処理が重要な部分から段階的に取り入れると良いかもしれません。

まだArrow-ktはプロジェクトの一部のモジュールでしか導入できていませんが、いろいろな使い方を試しながら導入を進めていこうと思います 👍

We Are Hiring !!

支払い.comをはじめUPSIDERでは現在積極採用をしています。 ぜひお気軽にご応募ください。

herp.careers

カジュアル面談はこちら!

herp.careers

Culture Deckはこちら📣

speakerdeck.com

不正利用検知システムをリリースしました

こんにちは!不正利用対策チームのShoです。

3Dセキュア(3DS)導入の義務化、サイン決済の廃止が行われ、不正利用対策の重要性がますます高まっています。

UPSIDERでも2024年11月に不正利用対策チームを立ち上げ、より高精度な不正検知を目指した取り組みを開始しました。以下のブログでご紹介していますので、ぜひご覧ください。

tech.up-sider.com

そしてこの度、不正利用対策チームは新しく不正利用検知システムをリリースいたしました。

prtimes.jp

なぜ新しい不正利用検知システムを作ったのか

不正利用対策とUXのトレードオフ

不正利用を防ぐ仕組みは、プロダクトの安全性を確保する上で不可欠ですが、行き過ぎた対策はユーザー体験(UX)の悪化につながります。そのため、セキュリティと利便性のバランスを考慮することが重要です。

既存のシステムでは不十分だった理由

UPSIDERではこれまで、Visaが提供する「VRM(Visa Risk Manager)」という不正検知ツールを活用してきました。VRMはとても強力なツールですが、以下のような課題がありました。

  • VRMは外部サービスのため、UPSIDERの社内データを直接活用できない
  • 法人カードの利用傾向は企業や業種によって大きく異なるため、個別最適なルール設定が困難

例えば、100万円という決済金額がその企業にとって大きいか、小さいかはUPSIDERの社内データを活用しないと判断できません。 また、海外で決済が発生した際にも、海外で業務を行っている企業と、国内のみで決済をしている企業ではリスクの大きさが異なります。

このような背景から、社内データを活用し、企業ごとに柔軟な検知ルールを適用できるシステムの必要性が高まりました。

既存のシステムの構成

  • VRMだけではUPSIDERの社内データを不正利用検知に活用することができなかった

新システムの構成

  • 新しい不正利用検知システムでは、UPSIDERの社内データを活用した検知を行う
  • VRMと併用することで、より精度の高い検知が可能になった

新不正利用検知システムのアーキテクチャ

新システムはUPSIDERのネットワーク内に構築されており、さまざまな社内データを取り込み、柔軟にルールを適用できます。主な構成要素は以下の通りです。

Rule Evaluator

不正利用をリアルタイムに判定するルール評価エンジン。複数のルールを実行し、疑わしいトランザクションを即時に検出します。

アーキテクチャの特徴

  • データベースには Firestore を採用しており、ルール定義の柔軟性とスケーラビリティを両立。
  • 決済基盤に求められる厳格なレスポンスタイム要件を満たすため、リアルタイムである必要がないDB書き込み処理等を非同期で実装。
  • 不正利用検知後の非同期アクションは、後続の Async Action Executer を利用。

Data Collector

社内のさまざまなデータソースからデータを収集・整形し、Rule Evaluatorに提供します。

データ収集とセキュリティ

  • 機密性の高い情報を扱う際には、元データが特定されないよう適切に加工・変換し、安全性を確保しつつルール評価の精度を維持。
  • 多様なデータソースに対応するために、高い拡張性を備えており、新たなソースの追加や変更にも容易に対応可能。

Async Action Executor

Rule Evaluatorの検知結果に応じて非同期にアクションを実行します。

非同期処理と拡張性

  • Pub/Sub を用いた疎結合・冪等性のある設計により、信頼性の高い非同期処理を実現。
  • Rule Evaluator以外のシステムからも利用可能な共通基盤として設計されており、拡張性に優れた構成。
  • アクションは自由に追加・変更が可能で、将来的な機能拡張にも対応。

この3つのアプリケーションを連携することで、UPSIDERならではのデータを活かした、高精度かつ柔軟な不正検知フローを実現しています。

全体システムの設計と運用

すべてのアプリケーションは Kubernetes 上にデプロイされており、高可用性・スケーラビリティ・継続運用性に優れた構成となっています。さらに、分散トレーシングにはCloud Trace を導入しており、各コンポーネントの動作や処理フローを可視化することで、迅速なトラブルシュートや性能監視を可能にしています。また、GKE における認証にはWorkload Identity Federation for GKEを利用し、安全なアクセス制御を実現しています。

今後の展望

新不正検知システムは既存のVRMと併用することで、相互に補完しあいながら、より多面的で強力な不正検知を実現しています。

今後は、

  • 独自ルールを検証できるシミュレーターの開発
  • 不正利用検知AIモデルの強化

に取り組み、検知精度の改善によるUXの向上を目指していく予定です。

UPSIDERは、セキュリティとUXのどちらも妥協しない、不正利用対策をこれからも追求していきます。

最後に

私たちの目指す理想の状態には、まだまだ道のりがあります。不正利用検知システム以外にも開発や改善したいことがたくさんあります。そんなUPSIDERの挑戦に興味を持っていただけた方は、ぜひカジュアル面談でお話ししましょう!

We Are Hiring !!

UPSIDERでは現在積極採用をしています。 ぜひお気軽にご応募ください。

herp.careers

カジュアル面談はこちら!

herp.careers

Culture Deckはこちら

speakerdeck.com

生成AI天下一武闘会:この世で一番速い仕事術 #ProductZineDay イベントレポート

こんにちは。Dev HRのNarisaです。
株式会社翔泳社 ProductZine編集部さん主催で開催されたプロダクトマネージャーのためのテックカンファレンス「ProductZine Day 2025」の基調講演に当社VPoPの森が登壇しましたので、イベントレポートをまとめます。

ProductZine Day 2025とは

ProductZine Day 2025とは、プロダクトマネージャーとしてのキャリア形成や人材育成、さらには生成AIの活用といったテーマを中心に、コミュニティを通じて得られた最前線の知見を持つ業界のキーパーソンの皆様と共に、リアルで濃密な学びと情報交換の場が提供されるカンファレンスです。今回は「生成AI天下一武闘会:この世で一番速い仕事術」をテーマに、パネルディスカッション形式で登壇させていただきました。

登壇者紹介(イベントページより引用)

【モデレーター】蜂須賀 大貴 / Newbee株式会社 代表 / プロダクトマネージャー

株式会社IMAGICA(現:IMAGICA Lab.)でエンジニアとしてキャリアをスタート。フリーランスPMとして複数企業の支援を経て、株式会社サイカにてプロダクト戦略の立案と実行をリードし、事業成長を牽引。その後、PIVOT株式会社にて、プロダクトマネージャー兼プロダクト組織の立ち上げを担当し、成長スタートアップ特有の課題解決やプロセス構築に携わる。現在は、2025年3月にNewbee株式会社を創業。テクノロジーメディア「Newbee」の運営、プロダクト支援、開発組織支援を行う。

森 大祐 / 株式会社UPSIDER VPoP

新卒で株式会社ワークスアプリケーションズに入社後、会計システムを中心として、大手企業のERP、業務システムの開発をリード。いくつかのキャリアを経て、PKSHAグループにて複数のAI SaaSを立ち上げ、それらのプロダクト企画統括を務める。主に、自然言語処理を活用した、人とAIとの協働型プロダクトの企画を得意とし、国内大手コールセンターの自動化プロダクトや、職場コミュニケーションのチャット化・自動応答などで数多くの成果を収める。2023年に株式会社UPSIDER入社。VP of Productを務める。

宮田 大督 / 株式会社エクスプラザ 生成AIエバンジェリスト・リードAIプロデューサー

株式会社エクスプラザで生成AIエバンジェリスト・リードAIプロデューサーを務める。生成AI技術の社会実装と普及に注力し、企業のAI導入支援やコミュニティ活動を推進。楽天やメルカリでのPdM経験を活かし、AIxPM領域での知見を発信。GaudiyではSNSエージェント実装や令和トラベルではノーコードツールでの大規模コンテンツ生成など、企画から実装までを手がける。

生成AI天下一武闘会 : この世で一番速い仕事術

生成AIが日常的に使われるようになってきた今、プロダクトマネージャーたちはどのようにAIと付き合い、業務や生活に活用しているのでしょうか?

「個人の仕事」「チームでの仕事」「趣味・プライベート」

という3つのテーマを軸に、それぞれのAI活用方法を対決形式で紹介しました。

各ラウンドは会場の拍手の大きさで勝敗が決まるという、まるで“生成AI天下一武道会”のような熱気あるセッションとなりました。

第1ラウンド:プライベートでのAI活用

森は、ChatGPTを活用したオーストラリア旅行の計画を披露しました。
2泊3日の短い旅程を“GPT縛り”で決めるというテーマで、音声入力による対話を中心に、旅行条件や希望(時差の少なさ、ジムの有無、非日常感、ひとり旅希望など)をAIに伝えながら旅先を選定していきます。ジム情報やパワーリフティングのトレーニング環境なども考慮された提案が返ってくるなど、AIとのインタラクションを重視した使い方が印象的でした。

結果的に、ウォンバットが「うんこが四角い」という理由で旅程に組み込まれ、誤って名前が似ている別の動物園のチケットを購入するという“GPTあるある”も共有されました。

一方、宮田氏は、検索特化型の生成AI「Perplexity」を使った韓国旅行のサポート体験を紹介しました。Perplexityは引用元が豊富で、事実確認のしやすさが特徴です。Googleマップでは分からないチケットの買い方や移動手段まで細かく教えてくれる点が旅行中に非常に有用であったことを語りました。

両者から旅行中の“ハルシネーション”(誤情報)には注意が必要だとも指摘がありました。生成AIの提案を鵜呑みにするのではなく、判断力と距離感を持って活用すべきという姿勢が印象に残りました。

第2ラウンド:個人でのAI活用

森は「15分お時間いただけませんか」という打ち合わせ依頼が実はうまくいかないことが多い、と切り出しました。その対策として、話したいことを音声入力でAIに話しかけて要約してもらい、事前に関係者へテキストで共有するという手法を紹介。
特に、相手が“最も話が通じにくいペルソナ”であると仮定して文面を整えることで、ミーティングそのものが不要になるケースもあるとのこと。コミュニケーションコストを大幅に削減できる実践知として、多くの共感を集めました。

一方、宮田氏はAI連携型エディタ「Cursor」を活用した、日常タスクの自己管理術を紹介しました。朝「おはよう」と入力すると、タスクが一覧表示され、夜「おやすみ」と言えば、その日の反省会が始まるという“生活に溶け込んだAI活用”です。
特にKPTに基づく振り返りや、タスクが進まなかった日に「あなたのためにアプリを作りました」とAIが勝手にネイティブアプリを生成するなど、驚きと笑いが入り混じったエピソードが披露されました。

このラウンドでは「ゼロ距離感」がキーワードとなり、自分にとって“どれだけ自然にAIを日常へ取り入れられるか”が活用の鍵であるというメッセージが共有されました。

第3ラウンド:チームでのAI活用

森は、UPSIDERにおける業務マニュアルの整備プロジェクトを紹介しました。 従来はNotionでマニュアルを管理していたものの、誰でも編集可能な柔軟さが逆にオペレーション品質の低下や混乱を招くことがありました。これを受けて、Markdown形式で出力しGitHub上で管理し、独自サイトにマニュアルをデプロイして参照する仕組みへと移行。
AIはこの運用の中で、文書構成ルールや表現のトーンを標準化する役割を果たしており、非エンジニアのメンバーでもマニュアルの作成・更新ができる環境を実現しています。

一方、宮田氏はCursorをベースにしたプロジェクトマネジメントの仕組みを紹介。 Jiraなどの管理ツールではなく、Markdownベースのテキストファイルでスプリントレビューを実施し、AIが差分や履歴を把握してレビュー支援をしてくれる体制を構築。会議の10分前に生成されたレビュー内容がそのまま使えるなど、実用性とスピード感が際立っていました。

鍵は「ゼロ距離感」と「始める力」

森は「自分にとってAIとゼロ距離でいるには、音声入力が最適だった」と語り、AIを“迷わずすぐ使える状態にすること”の重要性を強調しました。 宮田氏は、「身近な問題や“ちょっと面倒”を即座にAIに相談する工夫が、AIを日常に溶け込ませるコツ」と語り、特別な技術がなくても使い始められることの大切さを示しました。 どちらのアプローチも、「AIをどう使うか」ではなく、「自分にとって自然に使える方法は何か?」を問い直す視点にあふれていました。セッションの最後には 「もしAIでなにかをやりたいと思ったら、ミヤッチさんに声をかけてください。もしAIでエージェントを作りたいと思ったらUPSIDERに入社してください!」
という冗談も飛び出し、笑いに包まれながら幕を閉じました。

おわりに

生成AIの進化は目覚ましく、その活用法も十人十色です。
本セッションは、日常からチーム運営に至るまでAIを自分らしく使うためのリアルな工夫と試行錯誤に満ちた内容で、これからAIを取り入れてみようと考えている方にとってヒントが満載の1時間となりました。
ご視聴いただきましたみなさま、ありがとうございました!
セッションをご視聴いただき、ご興味をもっていただきました方はぜひ株式会社UPSIDERのカジュアル面談にぜひお越しください! herp.careers

UPSIDER Engineering Deckはこちら📣

speakerdeck.com

AIによるコード生成を活かすためのタスク分割

こんにちは、UPSIDERでエンジニアをしている太田です(@Hide55832241)。
AIによるコード生成が進化する中で、私たちはそれを開発現場でどのように実用的に活かせるかを模索しています。
本記事では既存コード資産を活かしたAIコード生成における、効果的なタスク分割のアプローチを紹介します。

タスク分割せずにコード生成する

以下のFormコンポーネントを例としてAIにコード生成させてみます。
※この記事ではUIについて細かく書きませんが、既存のコンポーネントを使用し修正の必要がないコードが生成されることが正しいものという前提とします。

意図に近いコードが生成されることもありますが、品質にばらつきがあり、ランダム性の影響も見られます。
一度の指示で完全に期待したとおりのコードが生成される確率は低いです。
また追加で指示を与えて修正できるかと言われると必ずしもそうではないように感じます。
指示して修正がうまくいくこともある一方で、いっそ削除して再生成した方が早い場合も多いと感じます。
このような背景から、AIが生成した間違いを含むコードをAIに修正させたり、人が引き継いで修正するのは必ずしも効率的ではないように感じます。

コード生成のランダム性例

上振れした場合(理想のコード)

// Formスキーマ
// ✅️ 共通定義した汎用スキーマを使用し、アプリケーション内で統一されたふるまいが考慮されている
export const FORM_SCHEMA = z.object({
  name: OPTIONAL_STRING_SCHEMA({
    columnName: FORM_FIELD_LABEL.name,
    additionalSchema: z.string().maxLength(VALIDATION.name.maxLength)
  }),
  monthly_limit_amount: REQUIRED_NUMBER_SCHEMA({
    columnName: FORM_FIELD_LABEL.monthly_limit_amount,
    additionalSchema: z.number().min(VALIDATION.monthly_limit_amount.min).max(VALIDATION.monthly_limit_amount.max)
  }),
  enabled: REQUIRED_BOOLEAN_SCHEMA({
    columnName: FORM_FIELD_LABEL.enabled,
  }),
})

// Form型
// ✅️ スキーマから推論したinputとOutputの型を定義する
export type FormInputSchema = z.input<typeof FORM_SCHEMA>
export type FormOutputSchema = z.output<typeof FORM_SCHEMA>

// FormContext Hook
// ✅️ 定義した型を使用し、InputとOutputの型をそれぞれ指定する
export const useUpdateCreditCardFormContext =
  useFormContext<typeof FormInputSchema, unknown, FormOutputSchema>

下振れした場合

// Formスキーマ
// ❌ zodを生で使用する
// ❌ エラーメッセージが定義されない(独自に定義されることも)
// ❌ 数値入力はIMEやカンマ除去などが考慮されず、コンポーネントとふるまいが不一致となり正常に動作しない
export const FORM_SCHEMA = z.object({
  name: z.string().optional(),
  monthly_limit_amount: z.number().min(1000).max(100000000)
  enabled: z.boolean()
})

// Form型
// ❌ outputの型しか定義しない
export type FormSchema = z.infer<typeof FORM_SCHEMA>

// FormContext Hook
// ❌ 定義した型を使用しない
// ❌ inputの型を指定しない(検証後の型しか得られず、Formの入力に関連するデフォルト値の考慮などができない)
export const useUpdateCreditCardFormContext =
  useFormContext<z.infer<typeof FORM_SCHEMA>>

なぜ期待するコード生成が難しいのか?

その原因のひとつは、コードの依存関係にあるのではないかと考えました。
たとえばFormコンポーネントでは、多くのコードがFormスキーマに直接または間接的に依存します。
Formスキーマが間違っていると関連するコードに波及する確率が高くなります。
Formスキーマから得られる型の定義やAPI hookが間違っている場合にも影響は大きくなります。
逆に正しく定義された汎用コンポーネントやFormスキーマを使用するだけのコンポーネントでは、それらを正しく使用できれば問題が起こりにくいです。
コード生成の結果を見ていると、そうではない場合もありますが、参照されることが多いコードが間違っていることも多いことに気がつきました。
人による実装ではまずそこを作成しある程度確定させた後に残りのコードに着手しますが、AIは間違ったコードを参照したまま残りのコードも作り続けてしまいます。 (人間のように依存関係の核となる部分から順に構築するわけではなく、AIは文脈を考慮せずに並列的・部分的に出力を始める傾向があるように思えます)

Formコンポーネントの依存関係

支払い.com で採用されているFormコンポーネントの構造)

タスク分割してコード生成する

次にタスク分割してコード生成をします。
上記の画像のように細かくモジュールが分割され、その単位でタスク分割することができますが、まずは参照が多いFormスキーマの実装に関する詳細な指示をとコードの依存関係を指示し、残りのコードも生成させます。

※ 以下の既存モジュールはあらかじめ定義済みであることを前提とします。

すると生成されるコードの品質が向上し、ランダム性が減少します。 (実装に関する詳細な指示を与えたので当然かもしれませんが)
参照されるFormスキーマの品質が向上することにより、Formスキーマ以外のコードの質も向上することが多くなったように感じます。
Formスキーマに限らず参照されることが多いコードに対し指示を与え、それを正確に使用することで生成されるコードの全体的な質の向上を見込めるように感じます。

分割してタスクを進める

先程はFormスキーマに詳細な指示を与えるもののForm全体を一度に作成する方法を取りましたが、一部のコードを先に生成・修正・確定させ、残りのコードを後から生成も可能です。
理想的には一度の指示ですべてのコードを生成できれば良いのですが、現時点ではそのようなケースは多くありません。
Formの生成の例ではまずFormスキーマのみをAIに生成させます。
このスキーマはフォーム全体の型やバリデーション、デフォルト値などに影響するため、特に重要な部分として単体でレビューや修正するアプローチをとります。
ときには人が介入して修正することも検討します。
AIを活用しながらも特に重要なコードを確実に作成した上で残りのコードを生成させます。

Formスキーマに依存するコード

このアプローチにより、次のようなメリットがあります。

  • 間違いのないコードを基盤にすることで、それに依存するコード生成の質が向上する
  • AI出力のランダム性を減らせる
  • コンテキストウィンドウを破棄しても残りのコード生成への影響が少ない

一度にコード生成する場合は詳細な指示を与えたとはいえ、レビューを行っていないため間違ったコードを参照したまま残りのコードを生成することもありました。
分割してタスクを進め確実に進めていくことで残りのコードの品質が向上することが多くなります。
一度の指示のみでAIに実装を任せることができなくなるので、非同期でAIに任せづらくなる大きなトレードオフがありますが、結果として分割してタスクを進めた方が早く品質が高くなることもあると思います。

段階的に実装を進めることができる

AIによるコード生成を行っていると何度コード生成し直しても期待したコードが得られず、進捗がまったくなく、はじめから自分で実装しておけばよかったと疲弊することがあります。
タスク分割するアプローチでは、タスク単位で確実に実装を確定させることで少しずつ確実に実装を進めていくことができます。
AIがすべてのコードを正確に実装してくれるにこしたことはないのですが、以下のようにAIによるコード生成に頼りながらも少しずつ実装を進めることができます。

  • まずはFormスキーマを確定させてから残りのコードをAIに生成させる
  • うまくいかなければFormの型を確定させてから残りのAIにコードを生成させる
  • それでもうまくいかなければFormやAPIのhookも確定させてから残りのAIにコードを生成させる

このように失敗の度に「確定させる範囲」を一段階広げることで、AIにとっての「正しい前提」を明確にし、生成精度の向上を狙います。

このアプローチにより以下の恩恵が受けられます

  • 少しずつコードを確定させていくことで、残りのコード生成の質を高めていく
  • AIにコード生成させる範囲を確実に狭めていく

あとがき

今後の展望として、AIによるコード生成をするためにルールやプロンプトのテンプレートを整備している中で、ルールベースでコードを生成をする余地が残っていることに気づきました。
ルールベースによるコード生成には柔軟性はありませんが、ランダム性もありません。
ルールベースによるコード生成とAIを併用することで、AIによるランダム性の伴うコードを生成させる範囲を狭めることができないかと模索を始めています。
またAIにコード生成させるための整備は、人間のエンジニアに対するオンボーディングにも役に立ちそうなものが多いと改めて感じています。

We Are Hiring !!

UPSIDERでは現在積極採用をしています。 ぜひお気軽にご応募ください。

herp.careers

カジュアル面談はこちら!

herp.careers

Culture Deckはこちら

speakerdeck.com

緊急生配信「MCP、A2Aで何が変わる?」イベントレポート

こんにちは!UPSIDERでDev HRをしていますNarisaです。

今回は、弊社VPoP 森 大祐が登壇したYouTube Liveイベント『緊急生配信「MCP、A2Aで何が変わる?」』の様子を、登壇内容にもふれつつレポートします!

今回の配信は、AI・エージェント領域の最前線を走る3名が、話題のMCP(Model Context Protocol)とA2A(Agent-to-Agent)についてディスカッション形式で深掘りするYouTube Liveイベントです。企画・配信はテクノロジーメディア「Newbee」。モデレーターの蜂須賀大貴さん(Newbee代表)を中心に、MCP/A2Aの基本から、今後のプロダクトやビジネスへの影響まで、居酒屋トークのようなラフな空気感で繰り広げられました。

アーカイブ動画

www.youtube.com

イベントページ:https://newbee.connpass.com/event/352211/

登壇者プロフィール

モデレーター 蜂須賀 大貴(@PassionateHachi)| Newbee株式会社 代表 / プロダクトマネージャー

エンジニア、プロジェクトマネジメント、新規事業開発などを経て、メディア業界一筋のプロダクトマネージャーとして従事。株式会社IMAGICA(現・株式会社IMAGICA Lab.)、フリーランス(複業)、株式会社サイカを経て現職。キー局、映画会社、VOD事業者をはじめとする多くのメディア企業のプロジェクトを担当。新卒から一貫した経験から、メディア業界の人脈と知見を持つ。また、主にアジャイル開発やプロダクトマネージメントの領域で年間10件超の講演、ワークショップの実施、書籍や記事への寄稿を行う。

パネラー 宮田 大督(@miyatti) | 株式会社エクスプラザ 生成AIエバンジェリスト・リードAIプロデューサー

株式会社エクスプラザで生成AIエバンジェリスト・リードAIプロデューサーを務める。生成AI技術の社会実装と普及に注力し、企業のAI導入支援やコミュニティ活動を推進。楽天やメルカリでのPdM経験を活かし、AIxPM領域での知見を発信。GaudiyではSNSエージェント実装や令和トラベルではノーコードツールでの大規模コンテンツ生成など、企画から実装までを手がける。

森 大祐(@diceK66)| 株式会社UPSIDER VPoP

新卒で株式会社ワークスアプリケーションズに入社後、会計システムを中心として、大手企業のERP、業務システムの開発をリード。いくつかのキャリアを経て、PKSHAグループにて複数のAI SaaSを立ち上げ、それらのプロダクト企画統括を務める。主に、自然言語処理を活用した、人とAIとの協働型プロダクトの企画を得意とし、国内大手コールセンターの自動化プロダクトや、職場コミュニケーションのチャット化・自動応答などで数多くの成果を収める。2023年に株式会社UPSIDER入社。VP of Productを務める。

斉藤 知明(@tomosooon)| 株株式会社ログラス 執行役員CBDO

1992年大阪生まれ。東京大学工学部卒。東京大学在学時にAI研究に従事、動画像を対象としたDeepLearningの研究で国際学会ICMEに論文が採択される。在学中に英単語アプリmikanを運営する株式会社mikanを協同創業しCTOに従事。その後Fringe81株式会社(現Unipos株式会社)に入社、ピアボーナスサービスUniposを立ち上げ子会社化、代表に就任。2023年5月、株式会社ログラスに入社。執行役員 CBDOに従事。「すべての挑戦が報われる社会に」を個人ミッションとする。

今回のテーマ:「MCP、A2Aで何が変わる?」

「生成AIをどうプロダクトに活かしていくのか?」をテーマに、MCPとA2Aという2つの新しいオープンプロトコルがもたらす変化を掘り下げました。 エージェントとエージェントの連携、自然言語とツールの橋渡し、そしてセキュリティや今後の社会実装の可能性まで、幅広い視点で語られた1時間でした。 配信冒頭、ざっくばらんな自己紹介とともに、会場には穏やかな空気が流れました。しかし、トピックが本題に入るにつれて、議論は一気に熱を帯びていきます。

A2AとMCP:対立ではなく、異なる役割で連携する未来へ

MCPは、AIエージェントが外部のツールやサービスと標準的に連携するための「使い方マニュアル」のような役割を果たします。一方でA2Aは、エージェント同士が自然な対話を通じてタスクを引き継いだり協力するための「共通言語」のようなものだと整理されました。

ログラスの斎藤氏は、「A2AはMCPを内包する」と整理し、エージェント間通信(A2A)とツール接続プロトコルMCP)が対立するものではなく、それぞれ異なる役割を持ちながら連携していく未来になると予測しました。

これに対し、森も「まさにその通り」と強く共感。両者は、互いに排他的ではなく、オープンな連携によって、エージェント社会の発展を支えていくべきだと話しました。

変化の本質:エージェント社会への第一歩

ここから話題は一気に未来に向かって広がっていきます。エージェントとツール、エージェント同士がシームレスにつながる世界。そこでは人間の作業をAIが自然に代替し、さらに人間の想像を超える創発的な動きが生まれていく可能性があると、宮田氏は語りました。

森も自身のプロダクト開発の経験を踏まえ、「これまで到達できなかったラストワンマイル──たとえば経費精算や業務システムへの自然なデータ入力──に、ようやく到達できる可能性が出てきた」と熱く語りました。

MCPやA2Aは、単なる技術トレンドではなく「仕事の成果を出すために必要な道具になり得る」ものだという認識が、全員の間で共有されていました。

現状の課題とこれからの挑戦

とはいえ、現時点でMCPやA2Aが完璧なソリューションかといえば、そうではありません。イベントの中では、以下のようなリアルな課題も挙げられました。

  • セキュリティリスクMCPサーバーはローカルにも立てられる一方、悪意あるサーバーが混在するリスクも増加。特に企業利用では、信頼できるハブ(例:VeyrAxなど)を介す運用が現実的になりつつある。
  • エージェント自体の未成熟:A2Aが実現しようとしているエージェント連携には、まだ基盤となる「賢いエージェント」が数少ないという課題も。標準化は急速に進みつつあるものの、本格的な社会実装はこれからが本番。
  • API供給の圧力MCP普及により、「もっとAPIを公開してほしい」という期待も急速に高まる可能性がある。これまでのような“閉じたシステム設計”では対応できず、プロダクト開発者側も大きな変革を迫られつつある。

プロダクトマネジメントの原点回帰

イベントではこの変化を「プロダクトマネジメントの原点に戻るチャンス」と話しています。「プロダクトマネージャーが本来やるべきなのは、どの技術を使うかではなく、“どう価値を生み出すか”を考え続けることだ」と。

MCPやA2Aといった新しい技術は、ツールに過ぎません。本当に重要なのは、自社のプロダクトがどの瞬間にどんな価値を提供するべきなのか、そのビジョンを描ききること
そのために必要な技術を、必要なだけ使えばいいのだと森は語りました。

最後に

セッションの最後には、斎藤氏と宮田氏と森から力強いメッセージが送られました。

斎藤氏は、A2A登場の意義について次のようにまとめました。
これまでの汎用的なエージェントは実用性に課題があったが、A2Aによって超特定業務に特化したスモールでスペシフィックなエージェントが次々に生まれる未来が開けたと指摘。小さな課題に対しても「120点を出せるエージェント」が生まれやすくなり、特定業務間で協働できる環境が整いつつあると展望を語りました。「これからは、すべてを自社で抱えるのではなく、特化したエージェント同士が連携し合う世界へ。そこに新たなワクワクが広がっている」と、未来への期待を寄せました。

宮田氏は「プロダクトマネージャーとしての原点を取り戻してほしい」と呼びかけました。エージェントやMCP/A2Aといった技術にとらわれ過ぎず、常に“お客様の価値創出”を中心に考えるべきだと強調。MCPやA2Aは、あくまでそのための道具であり、細部にこだわりすぎず柔軟に使いこなすべきだと語りました。そして、エージェントによって人間の「運動神経」が向上していく、まさに肉体派の時代が到来していると表現しました。

一方、森も「どの瞬間に価値が出るのかを見極めることが重要だ」と応じました。 AIを社会実装する際、特にラストワンマイル──成果に直結するアクション──を担うことが最大の課題だったと振り返り、MCPやA2Aによって、その課題を乗り越える可能性が現実味を帯びてきたと指摘。「出したい価値や成果を明確に持ち、それを実現するために技術を活用してほしい」と締めくくりました。

5月15日開催 ProductZine Day 2025 基調講演に登壇!

MCPとA2Aは、AIと人間、そしてAI同士の協働を劇的に加速させる可能性を秘めています。
しかし、それを活かせるかどうかは、技術そのものではなく、私たち自身が「やりたいこと」をどれだけ明確に描ききれるかにかかっています。

5月15日に開催される「ProductZine Day 2025」の基調講演では、今回のイベントに登壇した【モデレーター】蜂須賀 大貴氏(Newbee)、森 大祐氏(UPSIDER)、宮田 大督氏(エクスプラザ)の3名が、「生成AI天下一武闘会:この世で一番速い仕事術」と題し、生成AIの活用が広がる中で、プロダクトマネージャーはどのように日々の業務や生活にAIを取り入れているのかを語ります。

本セッションでは、「個人の仕事」「チームでの仕事」「趣味・プライベート」という3つのテーマをもとに、PM業界屈指のAI活用プロフェッショナル2名のユースケースを比較。
さらに、その中から1つを選び、具体的な使い方を実演形式で紹介します。AIによる業務変革のリアルを、豊富な実践知とともにお届けしますので、ぜひご参加ください。

event.shoeisha.jp

この度は貴重な機会をいただきまして、ありがとうございました!!!

We Are Hiring !!

株式会社UPSIDERは絶賛採用中です! herp.careers

UPSIDERが開発する人とAIとが協働する業務基盤、UPSIDER Coworking Platformでは、本生配信のトピックとなった、「MCP/A2A」を活用して、AI同士が協業し業務を駆動する野心的な仕組みの開発にチャレンジしています。

応募前にはカジュアル面談も実施可能ですので、ご希望の方は以下よりお申し込みください! herp.careers

UPSIDER Engineering Deckはこちら📣 speakerdeck.com

AIによるコーディングは本当に生産性を向上させるのか

こんにちは、UPSIDERでエンジニアをしている太田です(@Hide55832241)。
この記事では、エンジニアがAIエージェントを活用し、AIによるコードの自動生成によって実装工数を削減できるのかを考察します。
近年 GitHub CopilotCursorCline などのAIコーディングエージェントが登場しています。
こうしたツールの普及により、本当にコーディング業務の生産性は向上するのか?
日々開発するエンジニアとして、主観的な視点で考察します。
本記事では、エディタなどでAIコーディングエージェントを活用し、エンジニアが自らコードを自動生成させるケースに焦点を当てて考察します。
非エンジニアによる活用や、自律的にタスクを実行するようなツール(例: DevinOpenHandsGoose など)は対象外とします(ただし考察の中には共通する部分もあると思います)

AIを使用したコード生成

今回は、簡単なFormのフロントエンド実装を題材にします。
実際のプロダクトで検証した内容がベースとなっています。
以下のFormコンポーネントを例として取り上げます。

ワイヤーフレーム

Form仕様

入力項目

slug 項目名 種類 必須/任意 検証 選択肢 デフォルト値 備考
name カード名 text 任意 最大40文字 - コンポーネントから指定した任意の値 -
monthly_limit_amount 限度額(月) number 必須 整数、最小: 1,000、最大: 100,000,000 - コンポーネントから指定した任意の値 -
enabled 利用制限 boolean 必須 コンポーネントから指定した任意の値 - -

その他項目

項目名 種類 用途 備考
更新ボタン Form Submitボタン カード情報を更新する -

(参考)自分で実装した場合

  • 所要時間: 約25分
  • ファイル数: 約20
  • コード量: 約500行
  • クオリティ: 100%(自己評価)

タスクのみを指示しAIに生成させる

仕様を doc.md に記載し、AIに以下のように指示したケースを検証します。

doc.md

# **Form仕様**

## **入力項目**

| slug | 項目名 | 種類 | 必須/任意 | 検証 | 選択肢 | デフォルト値 | 備考 |
| --- | --- | --- | --- | --- | --- | --- | --- |
| name | カード名 | text | 任意 | 最大40文字 | - | 親コンポーネントから指定した任意の値 | - |
| monthly_limit_amount | 限度額(月) | number | 必須 | 整数、最小: 1,000、最大: 100,000,000 | - | 親コンポーネントから指定した任意の値 | - |
| enabled | 利用制限 | boolean | 必須 | 親コンポーネントから指定した任意の値 | - | - |  |

## その他項目

| 項目名 | 種類 | 用途 | 備考 |
| --- | --- | --- | --- |
| 更新ボタン | Form Submitボタン | カード情報を更新する | - |
@doc.md の仕様のFormコンポーネントを作成してください

※ CursorのAgentモード(Claude 3.7 Sonnet)の使用を想定します
SSRは行わず、React、React Hook Form、Zodを使用したプロダクトです

生成結果

生成されたコードには、以下のような多岐にわたる点で修正が必要でした

  • useControllerregister を使うべき箇所で、 watchsetValue を使用している
  • ラジオボタンを期待する箇所でチェックボックスを使用している
  • コンポーネントを不要に定義し直している
  • 未指定にすべき箇所に空文字を渡している
  • FormContextdisabled を参照せず常に false にしている
  • ディレクトリ構成が期待と異なる
  • 一部項目にエラーメッセージが設定されていない
  • 存在しないmutation keyを使用している
  • 汎用エラーメッセージを使わない
  • 汎用Formスキーマを使わない
  • 汎用コンポーネントが活用されていない

修正が必要なコードの例

// useControllerやregisterを使うべき箇所で、watchやsetValue を使用している
// NG
const FormFieldEnabled = () => {
  const { setValue, watch } = useFormContext()
  const value = watch('enabled')
  return <RadioGroup value={value} onChange={(e) => setValue('enabled', e)} />
}

// 期待するコード (簡略化しています)
const FormFieldEnabled = () => {
  const { control } = useFormContext()
  const { field } = useController({ control, name: 'enabled' })
  return <RadioGroup {...field} />
}

// 汎用Formスキーマを使わない/汎用エラーメッセージを使わない
// NG
export const FORM_SCHEMA = z.object({
  name: z.string().max("150文字以内で入力してください").optional(),
  ...
})

// 期待するコード
export const FORM_SCHEMA = z.object({
  name: OPTIONAL_STRING_SCHEMA({
    columnName: FORM_FIELD_LABEL.name,
    additionalSchema: z.string().max(NAME_VALIDATION.maxLength, {
      message: MESSAGE_MAX_LENGTH(
        FORM_FIELD_LABEL.name,
        NAME_VALIDATION.maxLength
      ),
    }),
  }),
  ...
})

何度か試行を重ねましたが、生成結果にはかなりのランダム性がありました。
初回の生成でそこそこ使えるコードが出力されれば運が良い方だと感じます。
正直、最初は「雑な指示でもここまで作れるのか」と感心しました。
しかし結果的には作り直しレベルで修正の必要なケースが多く見られました。
試行回数がまだ十分とは言えませんが、この規模のタスクに対して実装に関する具体的な指示をしなかった場合、「完全に納得のいくコード」が生成されたケースは、今のところ一度もありません。
これらのことから全く実装について指示せず、簡単なFormコンポーネントレベルのコードを生成させることは生産性の向上に繋がらない、むしろ生産性が悪化する可能性もあると感じます。

実装について指示する

次に実装について指示したケースを検証します。
今回はよりスコープを絞り、Formスキーマのみを生成させる例を具体的なコードとともに取り上げます。

期待するコード

import * as z from 'zod'

import {
  MESSAGE_MAX_LENGTH,
  MESSAGE_NUMBER_MIN,
  MESSAGE_NUMBER_MAX,
  MESSAGE_NUMBER_INT,
} from '@/consts/validationMessage'
import {
  OPTIONAL_STRING_SCHEMA,
  REQUIRED_NUMBER_SCHEMA,
  REQUIRED_BOOLEAN_SCHEMA,
} from '@/lib/zod/form'

const MONTHLY_LIMIT_AMOUNT_VALIDATION = {
  min: 1_000,
  max: 100_000,
} as const
const NAME_VALIDATION = {
  maxLength: 40,
} as const
export const FORM_FIELD_LABEL = {
  name: 'カード名',
  monthly_limit_amount: '限度額(月)',
  enabled: '利用制限',
} as const
export const FORM_SCHEMA = z.object({
  name: OPTIONAL_STRING_SCHEMA({
    columnName: FORM_FIELD_LABEL.name,
    additionalSchema: z.string().max(NAME_VALIDATION.maxLength, {
      message: MESSAGE_MAX_LENGTH(
        FORM_FIELD_LABEL.name,
        NAME_VALIDATION.maxLength
      ),
    }),
  }),
  monthly_limit_amount: REQUIRED_NUMBER_SCHEMA({
    columnName: FORM_FIELD_LABEL.monthly_limit_amount,
    additionalSchema: z
      .number()
      .int({
        message: MESSAGE_NUMBER_INT(FORM_FIELD_LABEL.monthly_limit_amount),
      })
      .min(MONTHLY_LIMIT_AMOUNT_VALIDATION.min, {
        message: MESSAGE_NUMBER_MIN(
          FORM_FIELD_LABEL.monthly_limit_amount,
          MONTHLY_LIMIT_AMOUNT_VALIDATION.min,
          '円'
        ),
      })
      .max(MONTHLY_LIMIT_AMOUNT_VALIDATION.max, {
        message: MESSAGE_NUMBER_MAX(
          FORM_FIELD_LABEL.monthly_limit_amount,
          MONTHLY_LIMIT_AMOUNT_VALIDATION.max,
          '円'
        ),
      }),
  }),
  enabled: REQUIRED_BOOLEAN_SCHEMA({
    columnName: FORM_FIELD_LABEL.enabled,
    additionalSchema: z.boolean(),
  }),
})

  • 各項目のスキーマ、エラーメッセージは共通定義されたものを使用する必要がある
  • additionalSchema で定義する条件には、明示的なエラーメッセージを設定する必要がある
  • 定数は直書きせず、使い回せるように定義する

なぜスキーマを直書きしないか

たとえば数値入力の場合、z.number() と書けば済むように見えるかもしれませんが、実際には多くの処理を組み込んだ汎用スキーマを定義して使っています。
以下は、実際のコードに近い一例です。

const REQUIRED_NUMBER_SCHEMA = <
  Input extends number,
  Def extends z.ZodTypeDef,
  Output extends number,
>({
  additionalSchema,
}: {
  additionalSchema: z.ZodSchema<Output, Def, Input>
}) =>
  z
    .string()
    .transform((v) => {
      // 全角->半角変換、カンマ除去、単位除去など
      return formattedValue
    })
    .refine((v) => !!v, { message: '入力してください' })
    .transform((v) => {
      if (v === '') return z.NEVER
      return Number(v)
    })
    .pipe(z.number({ invalid_type_error: '数値を入力してください' }))
    .pipe(additionalSchema)

実装に関する指示を与えない結果

スコープをFormスキーマに絞ったのでもう一度、タスクのみを指示しAIにコード生成させた結果を簡単に記載します。
Formスキーマに絞った場合、Form全体を生成させるより質が高いコードを生成されるケースが増えました。
ほぼ修正の必要のないコードが生成されることもありました。
しかし依然としてランダム性が高く、期待するコードが生成される確率は高くありません。
Formスキーマのような小さいタスクですら期待するコードが生成されないと、複数の実装工程でランダム性を受け入れる必要があるため生産性向上には繋がらない、もしくは非常に限定的であると判断します。

実装に関する指示を与えた結果

以下のドキュメントを追加します。

form_schema.md

# Formスキーマ

## ルール

### 1. `src/lib/zod/form.ts` に定義した共通スキーマを使用すること

共通スキーマの命名ルール: {REQUIRED/OPTIONAL}_{TYPE}_SCHEMA

**例**

- 必須入力の文字列用スキーマ: REQUIRED_STRING_SCHEMA
- 任意入力の数値入力用スキーマ: OPTIONAL_NUMBER_SCHEMA

### 2. additionalSchemaの各項目にはエラーメッセージを設定すること

**例**

```typescript
REQUIRED_STRING_SCHEMA({
  ...,
  additionalSchema: z.string().max(100, { message: MESSAGE_MAX_LENGTH(...) }),
})
```

エラーメッセージは `src/consts/validationMessage.ts` に定義したメッセージを使用すること

### 3. 文字数制限などの制約で使用する値は定数定義して使用すること

**例**

```typescript
const RANGE = { max: 100 }
REQUIRED_STRING_SCHEMA({
  ...,
  additionalSchema: z.string().max(RANGE.max, { message: MESSAGE_MAX_LENGTH(RANGE.max) }),
})
```

## 出力例

```typescript
const NAME_VALIDATION = { maxLength: 100 } as const
export const FORM_SCHEMA = z.object({
  name: REQUIRED_STRING_SCHEMA({
    additionalSchema: z.string().max(NAME_VALIDATION.maxLength, { message: MESSAGE_MAX_LENGTH(NAME_VALIDATION.maxLength)}),
  })
})

```

以下の指示でコード生成します。

@doc.md の仕様のFormコンポーネントを作成してください
@form_schema.md に従ってください

生成されたコードは、確実に質が上がっていました。
試行回数が十分ではありませんが検証した際には、修正の必要のないコードを生成する確率が7割、多少の修正は必要だがおおむね許容できるレベルのコードが3割、使えないコードが生成されたのはゼロです。
この結果から工夫をすれば質の高い結果を得られることがわかりました(得意なタスクとそうではないタスクで結果に差は出ると思いますが)
あらゆる工程でこのレベルのコード生成ができれば生産性向上が見込めそうです。

シミュレーション

実装に関する指示をAIに与えることでコードの質が向上し、その結果修正にかかる工数が減る可能性があることがわかってきました。
では、 どの程度の工数削減が見込めるのか? ざっくりとした前提ではありますが、シンプルな条件でシミュレーションしてみます。
以下は、 「似たような実装が何度も発生する場合に、AIの活用がどれほど工数削減に寄与するか」 を試算したものです。
AI導入による効果が出る条件を把握するために、工数比較しました。
数値はあくまで仮定に基づくものであり、実運用とは差異があることをご承知おきください。

前提

  • 初回実装は人間が行う
  • 類似実装がある場合、汎用的なAI指示を作成
  • 以降の実装はAIに任せる
  • 人間による実装工数
    • 人間による初回実装: 30
    • 人間による類似実装: 10 (初回実装の3分の1)

上記を前提として以下の組み合わせでシミュレーションをします

  • 初めてAIに指示する際に今後の類似実装でも使用可能な汎用的な指示を作成する工数を30 (人間による初回実装と同程度)、10 (人間による類似実装と同程度) とした場合の2パターン
  • 人間による類似実装の工数に対し、AIが実装した場合の工数を2 (5分の1)、1 (10分の1) とした場合の2パターン
汎用的な指示の作成工数 AIによる実装工数
仮定1 30 2
仮定2 30 1
仮定3 10 2
仮定4 10 1

それぞれAIを使用せずに実装した場合に比べて、どれほどの工数削減が見込めるのかシミュレーションします

人間による実装工数

まとめると以下のようになります

類似実装回数 仮定1 仮定2 仮定3 仮定4
1 工数が増加 工数が増加 工数が増加 工数が増加
2 - 減少 - 減少
3 - - - 2分の1
4 - 2分の1 減少
5 減少 - - 3分の1
10 2分の1 3分の1 5分の2 5分の1

人間が実装した場合と比較した類似実装回数ごとの実装工数

汎用的なAI指示の作成に大きな工数が必要な場合、AIによる類似実装の効果が出るのが遅く、類似実装が少ない場合は工数の削減は見込めなさそうです。
汎用的なAI指示の作成にあまり工数が必要ない場合、AIによる類似実装の効果が出るのが早く、類似実装が少なくても工数の削減を見込めそうです。
AIによる類似実装のパフォーマンスが高い場合、類似実装が増えるにつれ大きな工数の削減が見込めそうです。
しかしこれらはあくまで机上のシミュレーションです。
もっと生産性向上が見込めると考える場合以下のようなことも考えられるでしょう。

  • 初回実装もAIに行わせる
  • 指示の作成の工数を削減できる

逆に以下のような様々な懸念点も考えられるでしょう。

  • 類似実装といってもタスクごとに様々な要件や規模があり、すべてのタスクで質の高いコードが生成できるとは限らない
  • タスク全体のコードを生成できるとは限らないので、生産性の向上は限定的

所感と限界

様々な考察をしましたが実際どうなのか?
正直なところ、現時点ではまだ明確な答えは出せません。
今回の検証や考察はエンジニア業務の中でも「実装」に焦点を当てたごく一部の側面に過ぎません。
また実装に限っても、AIがすべてを代替できる状況とは言い難いです。
たとえば、UI実装のような視覚的・感覚的な要素は、AIにとっては依然として難易度が高い分野であると感じます。
デザインシステムや実装レベルのデザインファイル、レイアウトコンポーネントの整備などにより精度は向上が見込めると考えられます。
MCP等によるデザインファイルへのアクセスなどにより生産性の向上を見込めるかもしれません。
しかしそれらが整っていない環境でモデルの精度やツールの性能だけに頼るのはまだ難しいように感じます。
特に曖昧な指示に対しては今後モデルの性能が向上してもある程度のランダム性を伴うことが予想されます。
またAIが生成したランダム性の高いコードを確認して何度もやり直すことは、とても労力を伴うように感じています。
慣れの問題もあるかもしれませんが、AI縛りでコーディングをすると1日の終わりには想像以上の疲れを感じることがありました。

おわりに

AIによるコード生成には、現時点でも十分な可能性が感じられます。
しかし使い方によっては生産性を下げてしまう可能性もあると感じています。
実装について詳細に指示する方法はコードの質は高くなるかもしれませんが、それ自体の工数とメンテナンスが伴います。
とはいえ、使い方次第では確実に開発効率を引き上げられるポテンシャルがあるとも感じています。
AIによるコード生成には大きな可能性を感じながらも、今はまだ試行錯誤を重ねている段階です。
これらのことはあくまで現時点で感じていることです。
AIモデルや周辺の技術の進化は想像以上に早く、このような状況になることは数年前に全く予想もしていませんでした。
今後どのような状況になるかわかりませんが、AIを活用したエンジニアリングの可能性に期待し、今後も試行錯誤を重ねながら向き合っていきたいと思います。

We Are Hiring !!

UPSIDERでは現在積極採用をしています。 ぜひお気軽にご応募ください。

herp.careers

カジュアル面談はこちら!

herp.careers

Culture Deckはこちら

speakerdeck.com

TemporalによるDurableなワークフロー開発

こんにちは、UPSIDERで請求関連の機能の開発を担当しているチームです。

本記事では、Temporal というフレームワークを活用した Durableなワークフロー開発についてご紹介します。

私たちのチームでは、UPSIDERのさまざまなサービスにまたがる請求関連をスムーズかつ確実に処理するために、日々たくさんの課題に向き合っています。今回は、その中で出会った難しさと、それをどうTemporalを利用して乗り越えたかを技術的な観点からお届けします。

1. 「Durableな請求処理」が求められる背景

長期戦の請求処理との戦い

UPSIDERでは、月初の請求確定、請求日前日のリマインド通知、入金確認、そして万が一の引き落とし失敗時の督促対応など、1ヶ月以上に渡って連続的に発生する一連の請求イベントを管理しています。

単発の処理では終わらないーむしろ請求が確定したところから、長いマラソンが始まるのです。

これらを正確に、かつ確実に行う必要があります。なぜなら、請求に関わる処理での失敗はユーザーの信頼を失いかねないからです。

こうしたプロセスは、元々バッチ処理KubernetesのCronJobによって実装・運用されていました。が、ここには幾つかの問題が...。

この情報のサイロ化はまさに「魔女の鍋」。必要な情報をすべてのリソースからすくいあげて、ようやく全体像が見えてくる、そんな世界でした。

課題は「見えにくさ」と「わかりにくさ」

  • そもそも、どのバッチがいつ何をしているのかが見えない
  • 関連するAPI呼び出しの流れをたどるには、いくつものドキュメントを行き来する必要がある
  • 新しく入ってきた開発者にとっては、「えっ、請求周りが複雑すぎて無理😇」という洗礼

この状況を打破すべく私たち請求チームはもっと一貫性と透明性を持った請求処理基盤を作ろうと動き出しました。

2. バッチ処理に頼らない新たなアプローチ

“イベント×日時”の壁

請求処理は、スケジュールに基づいた処理実行が必要不可欠です。
となると、まず候補に上がるのが「バッチ処理」。ですが、先ほど紹介したような課題(見えない、つらい、カオス)を踏まえると、「もうちょっと違う方法をとりたいよね」というのがチームの共通認識でした。

—— じゃあどうする?

バッチ処理でスケジューリングしないといけない根本原因は、イベントAの後にイベントBを特定の日時まで待って実行することが難しいためです。。イベントの後に特定のイベントを実行する事自体は難しくありませんが、特定の日にならないと実行されないという機構を入れるにはそれなりに考えることがあります。

単純な例でいくと次のイベントを受け取ったらその時間までsleepをするなどが考えられますが以下のような問題があります。

  • sleep中にサーバーが落ちたらどうするの?
  • podやnodeのスケールアップ、ダウンでプロセスが強制終了されたら?
  • そもそも、1ヶ月sleepするプロセスって何者?笑

そんな疑問が次々に湧いてきて、「これは堅牢性もスケーラビリティも厳しいな...」という結論に。

あらわれた救世主「Temporal」

そこで私たちが選んだのが、Temporalというフレームワークです。
Temporalは、Durable Executionを実現するためのワークフローエンジンで、時間を跨いで実行される長期プロセスを安全に管理することができます。

—— Durable Executionとは?
実行状態や進行状況を失わずに保持できる仕組みのこと。

3. Durable Execution(耐久実行)とは?

イベントドリブンな世界の”つらみ”とその先にあるもの

さて、ここで少しだけ前提のお話しです。

請求のような長期間にわたるプロセスを、マイクロサービス環境で構築しようとすると、よく検討されるのが「イベント駆動アーキテクチャEDA」というアプローチです。

この構成自体はモダンかつ柔軟で、イベントを使って各サービス間を疎結合に保てるメリットがあります。
…が、イベントを”中心”にしてプロセス全体を組み立てようとすると、システムが複雑化してしまうことがあります

たとえば:

  • 処理の流れが複数のイベントに分かれてしまい、ビジネスロジックがいくつものサービスやキューに散在
  • 各サービスが独立してイベントを処理するため、「いま全体のプロセスはどこ?」がわかりにくい
  • 状態の管理やリトライ処理、補償トランザクションなどを自前で組み込む必要があり、ロジック以外の構築などに焦点を当てる必要が出てくる

これらは偶発的複雑性(Incidental Complexity)と呼ばれる種類の課題です。本質的ではない部分に時間を取られ、結果として機能追加も運用も大変になっていきます。

ワークフロー中心、という考え方

こうした課題に対して、Temporalがもたらしてくれたのは、ビジネスロジックを”プロセス(ワークフロー)”単位で捉えるという視点でした。

言い換えれば、分散されたイベントの連鎖ではなく、一つのまとまった物語としての処理を組み立てるイメージです。

この発想の転換こそが、Temporal導入の大きな転機でした。

4. Temporalが実現するDurable Executionの中身

「落ちても大丈夫」を支える仕組み

Temporalの凄さは「ずっと動き続けてくれるように見える」ということに尽きます。 たとえば、「3日後にリマインドメールを送って、さらにそこから20日後に入金確認する」みたいな流れを書いたときに、

  • その23日間ずっとプロセスを保持してくれる
  • サーバーが落ちたり、Podが再起動しても途中から再開できる
  • それらすべてが慣れ親しんだ言語でコード管理でき、可視化もできる

まるで「何年も落ちずに動き続ける理想的な単一サーバー」がいるかのようです。物理的にはそんなサーバーは存在しないかもしれませんが、Temporalはそれを履歴のリプレイによって実現してくれています。

リプレイの力: 失われない状態、そして制約

この「履歴のリプレイ」こそがTemporalの肝。
Temporalのワークフローは、「実行履歴(イベントログ)」を全て永続化しています。サーバーが落ちたとしても、過去のイベント履歴を使って状態を再構築することで、再起動後何事もなかったように動き続けるのです。
ただし、これには一つ重要な制約があります。

—— Workflowの中では、非決定的な処理を書いてはいけない

非決定的とは、リプレイのたびに結果が変わってしまう処理のこと。たとえば:

  • 乱数生成
  • 現在時刻の取得

このような処理はリプレイのたびに違う結果になってしまい、整合性が取れなくなります。
そのため、Temporalではこういった処理を「Activity」という別コンポーネントに切り出す、というルールがあるのです。

WorkflowとActivityの役割分担

Temporalでは処理を「Workflow」「Activity」に分けて設計します。

Workflow

  • Activityをどの順番で、どんな条件で実行するかを制御
  • タイムアウトやリトライの制御、条件分岐なども含む
  • 状態は全てリプレイ可能なように管理
  • スケジューリングや特定のsignalを待つなども簡単に実装可能

Activity

  • 「DBアクセス」「API呼び出し」など、副作用を持つ処理を担当
  • 非決定的な処理は基本全てこちらにまとめる
  • 実行中にPodが落ちても、実行のスナップショットが記録されていて、復旧後に途中から再開できる

5. Temporalの採用の背景: なぜ選んだのか?

請求処理のようなプロセスには、「数日待ってから実行」「障害が起きても復旧」「何かをトリガーに次の処理を始める」といった要件がついて周ります。
この複雑さに対応するために、他のワークフローツールも調査しましたが、私たちがTemporalを選んだ理由は以下の通りです:

  • Durable Executionによる高い信頼性
  • GoやJavaのような慣れ親しんだ言語でワークフローを記述できること
  • OSSかつKubernetes対応で、柔軟なデプロイも可能なこと
  • 長期間のワークフローにも対応できること

それぞれの選定ポイントをまとめた比較表はこちらです

6. Temporal導入による請求処理の変化

私たちのチームでは、これまでバッチやCronで分散していた請求処理を、1つのワークフローとしてTemporal上に集約しました。
たとえば、以下のようにWorkflowとActivityを実装しています。

Golangによるコードサンプル(Workflow)

func SimpleInvoiceWorkflow(ctx workflow.Context, invoiceID string) error {
    // Activity実行用オプション
    ao := workflow.ActivityOptions{
        StartToCloseTimeout: time.Minute,
        RetryPolicy: &temporal.RetryPolicy{
            InitialInterval:    time.Second * 5,
            BackoffCoefficient: 2,
            MaximumAttempts:    3,
        },
    }
    ctx = workflow.WithActivityOptions(ctx, ao)

    // 1) 請求メールを送信するアクティビティ
    if err := workflow.ExecuteActivity(ctx, "SendInvoiceEmailActivity", invoiceID).Get(ctx, nil); err != nil {
        return err
    }

    // 2) 翌日まで待機
    workflow.Sleep(ctx, 24*time.Hour)

    // 3) 支払いリマインドメールを送信するアクティビティ
    if err := workflow.ExecuteActivity(ctx, "SendReminderEmailActivity", invoiceID).Get(ctx, nil); err != nil {
        return err
    }

    return nil
}

Workflowでは一連の流れをコードでそのまま書けるようになりました。

workflow.Sleep(ctx, 24*time.Hour)

このコード一行で、KubernetesのCronJobや別バッチを用意せずに「1日待つ」という処理が可能になります。

  • スケジューリング専用の仕組みは不要
  • バッチ連携を頭で組み立てる必要はない

「このフローの状態っていまどこ?」という問いは、Workflowのどこまで進んでいるかは常にTemporal Cloudが状態を保持しているので簡単にわかります。

Golangによるコードサンプル(Activity)

アクティビティでは、実際に外部APIを呼び出したり、DBにアクセスしたりといった処理を行います。

func SendInvoiceEmailActivity(ctx context.Context, invoiceID string) error {
    subject := "【ご請求】請求書 " + invoiceID
    body := "請求書 " + invoiceID + " をお送りします。ご確認ください。"

    // 3) 実際のメール送信(ここではログ出力)
    log.Printf("To: customer@example.com\nSubject: %s\n\n%s\n", subject, body)
    return nil
}
  • 非決定的な処理(現在時刻の取得、外部API呼び出しなど)は全部アクティビティに押し込める。
  • アクティビティが失敗してもTemporalがリトライの仕組みを持っているため、自前でリトライのためのキューを用意する必要はなし

7. Durable Executionによって得られた本質的なメリット

Temporalの導入は、単に「バッチをやめた」という話ではありません。

私たちが本当に実感しているのは、ワークフロー中心の思考へのパラダイムシフトです。

ワークフローが”プロセスそのもの”になった

従来のバッチ処理では、「前のバッチがこれを処理して...次のバッチがあれをして...」というように、処理同士のつながりを頭の中で構築する必要がありました。
でもTemporalでは、ワークフローが状態とロジックを両方持ち続けてくれるため、
「〜日に何をやるか」だけを純粋に書けばよく、前後関係やリカバリまで含めてワークフローに”お任せ”できます。

リソースが一つにまとまる安心感

以前は:

という情報の分散っぷりが悩みの種でした。
今は:

設計が自然とシンプルになる

Temporalは「ワークフローで捉える」という設計思想をベースにしているため、バッチ的な複雑性を自然と避ける構造になっています。

  • 結果として、コードが読みやすく
  • 状態を把握しやすく
  • 障害にも強くなった

これは、「Durable Execution」の威力を身をもって体験している証拠です。

8. まとめ: ワークフローが世界をシンプルにする

本記事では、UPSIDERの請求チームにおける実例を通して、TemporalによるDurableなワークフロー開発をご紹介しました。

  • バッチ前提の設計では、処理が分散し、全体の把握や障害対応が困難に
  • Temporalによって、ワークフロー中心の設計へシフト
  • 長期間にわたる請求処理を、1つのリソースに集約
  • サーバーダウンやPodの再起動を気にせず、耐障害性のある状態管理が可能に

Temporalは魔法ではありませんが、「まるでサーバーが落ちないかのように動く世界」を実現してくれます。

次回: Temporalの「見える化」もすごい

今回のブログでは触れていませんが、Temporal CloudのUIで実際のイベント履歴を可視化できるのも導入後に気づいたポイントです。
「どこまで処理されたか」「どこで止まっているか」が一目でわかる便利さは、ぜひ次回のブログで詳しくお伝えしたいと思います!

以上、UPSIDER請求チームからのレポートでした!
最後まで読んでいただき、ありがとうございました🙌

We Are Hiring !!

UPSIDERでは現在積極採用をしています。 ぜひお気軽にご応募ください。 herp.careers

カジュアル面談はこちら! herp.careers

Culture Deckはこちら📣 speakerdeck.com