🤖

技術的リスクのあるAI統合プロジェクトを、引き継げる状態で完遂した話

に公開

はじめに

「自社のプロダクトにAI機能を組み込みたい」という要望が増えています。

しかしながら、本番稼働しているプロダクトにAI機能を組み込むのは思ったほど簡単なことではありません。

ユーザーの使用に耐えうるアウトプットが安定して出せるのか、ユーザー体験を損なうことはないか、そもそもどのサービスやライブラリを使えばいいのか。これらを一つずつ検証しながら進める必要があるため、AI機能のリリースまでには慎重なプロセスを踏む必要があります。

もちろんプロダクトはリリースして終わり、ではありません。継続的に運用し、必要に応じて機能を追加していく必要があります。私のように外部のエンジニアとして参画している場合には特に、後のことを考えて技術選定し、設計し実装していく必要があります。

今回は、ある事例を通じて技術選定から実装、引き継ぎまでのプロセスを紹介します。

1. 案件の背景

プロダクトと課題

あるWebメディア運営企業では、社内向けのSEO調査ツールを開発・運用していました。記事を作成する際に、キーワード調査や競合サイトのリサーチを行うツールです。

1つの記事を作成するためにはさまざまな調査を行う必要があり、多くの時間がかかっていました。

開発者の観点では、UIの優先度が相対的に低い社内ツールにも関わらず、Vueで作られたフロントエンドの開発コストが重い、という課題もありました。

クライアントの要望

AIによる効率化

記事作成フローにAIを組み込んで、SEO調査を効率化したい。

フロントエンド開発の簡略化

開発リソースが限られているため、できるだけコア(調査タスクを行う非同期ジョブ)の開発に専念できるよう、簡単に実装できるフロントエンドに乗り換えたい。

2. クライアントの構想とプロトタイプ戦略

クライアントの技術責任者には、既存のバックエンドは残しつつ、フロントエンドをNotionに移行したい、という構想がありました。Notionのデータベースにキーワードや競合のURLを入力すると自動で調査が行われ、結果がページとして保存されるイメージです。

そしてそこにAIを統合します。調査結果が出るとその内容をAIで要約する機能や、将来的には記事のドラフト作成までAIで行いたい、という構想もありました。

この構想をそのまま実現すると、キーワードや競合のURL、調査結果やAIのアウトプットをNotionのデータベースに保存する形となります。

しかし後述するように、プロダクトのデータベースとしてNotionを使うことには技術的な懸念がありました。

加えて、Notionに移行することでフロントエンドの開発工数が圧縮できるのは確かですが、社内ツールとはいえ使用に耐えうるUXを実現できるのかも検証が必要でした。

そこで、プロトタイプを作成して実際のユーザーに使ってもらい、検証が成功したらそのまま本番運用に移行する、というアプローチを取ることになりました。

私の役割

プロトタイプの作成を担当しました。具体的には、以下の工程です。

  • 技術選定
  • プロダクトのベース開発
  • 既存機能のいくつかを移植
  • 基本的なAI機能の実装
  • 社内エンジニアへの引き継ぎ

3. 技術選定

技術選定において重視したのは、「今の開発チームで無理なく運用できるか」です。

AIという新しいジャンルのため、ついつい最新技術を導入したくなります。しかし今回の目的の一つは「限られた社内リソースでコアの開発に専念できるようにする」ことです。

  • シンプルなアーキテクチャで実現できるか
  • 社内で使用実績があるか
  • あるいは業界内でデファクトで、情報が豊富にあるか

これらを重視しました。

Notionで本当に大丈夫か?

「Notionをフロントエンドにしたい」というのはクライアントの要望ですが、先述したように、正直言って懸念がありました。

Notionのデータベースにはトランザクションやユニーク制約の仕組みがありません。そのため、処理が途中で落ちた場合や、複数のユーザーが同時に同じキーワードを登録した場合に、データの重複や不整合が発生してしまいます。

技術者としては、信頼性を重視してRDBを使うべきでは、という思いは当然ありました。しかし、この懸念を率直にクライアントに伝えた上で議論した結果、構想通りNotionを採用することにしました。以下の理由からです。

  • 実現できればフロントエンド開発のコスト削減効果がとても大きいこと
  • 社内ツールで、かつミッションクリティカルなものではないので、データの不整合が発生しても都度修正で運用上問題ないこと[1]
  • 一人で作り始めるプロトタイプなので、撤退が容易であること

採用した技術スタック

TypeScript
Notionの公式SDKはNode版しかないため一択です。社内エンジニアは全員フロントエンド開発で使っているため、選定にあたって特に支障はありませんでした。

Lambda
定期的に起動し、Notionのデータベースをポーリングして新規レコードがあれば調査タスクを走らせる、というシンプルな構成にしました。

AWS Bedrock
LLMはAWS BedrockをSDK経由で使うことにしました。社内でAWSをメインで使っていたため、特に導入に支障はありませんでした。

4. 設計

プロトタイプとして、プロダクトのベースを作成し、既存プロダクトの機能をいくつか移植し、そこにAI機能を組み込んだものを作成します。

ワークフローツールやエージェントフレームワークは使用せず、シンプルな「バケツリレー型ポーリング構成」としました。

バケツリレー型ポーリング構成

以下のステップをLambdaがポーリングで監視し、処理を繋いでいきます。

  1. キーワード登録(ユーザー)
  2. 調査ジョブのキック(Lambda → 既存バックエンド)
  3. 調査結果の保存(既存バックエンドからLambdaが結果を取得し、Notionへ)
  4. AI要約の実行(Lambda + Bedrockで結果をNotionに保存)

これは最もシンプルな場合のフローです。実際には調査結果がさらに別の調査ジョブをキックしたり[2]、複数の調査結果をインプットとしてAIが起動することもあります。

最終的なアウトプットまで一気に行うワークフロー、あるいはエージェントとすることもできそうです。そうしなかった理由として、AIフレームワークが当時まだ成熟していなかったというのはあります。しかしそれより大事だったのは「人が介在する余白」を作るためという理由です。

中間の結果を確認し、OKだったら次の処理に渡す。必要に応じて中間結果を人が修正する。プロンプトを変えて試行錯誤し、最も良いものを次のインプットとする。

こうした、全部をAIに任せるのではない「人間とAIの共同作業」が現時点で最適だというのは、クライアントも私も議論の余地なく合意するところでした。

5. 引き継ぎを見据えた実装

続いて実装です。作って終わりではなく、社内エンジニアにスムーズに引き継ぐことが私の大事な役割です。そのためにさまざまな工夫を凝らしました。

Adapter パターンによる外部依存の隔離

これには2つの目的があります。

テストの容易化

プロトタイプとはいえ本番移行を見据えている以上、テストがないのは無責任です。主要な処理にはユニットテストを記述しました。

しかしこのシステムは、NotionやBedrock、既存のバックエンドなど外部との連携がとても多く、何も考えずに実装するとテストが困難になってしまいます。そのためインターフェイスと実装を分離し、テストが容易に書けるようにしました。

外部環境の変化への備え

Notionは機能追加や仕様変更が頻繁で、APIやSDKに大きな仕様変更が行われる可能性があります。

LLMの動向についても、当時は今よりさらに不確実性が大きい状況でした。Bedrockで使えないモデルが圧倒的に性能が高くなり、そちらを使わざるを得なくなる可能性や、使いやすいフレームワークやサービスが登場してデファクトとなる可能性もあります。

あるいは、Notionから通常のWebアプリケーションに乗り換えたいという要望が将来あるかもしれません。

Adapterパターンの採用により、依存先を乗り換えたい場合にコアロジックへの影響を最小限に抑えることができます。

ドキュメントに頼らない仕組みの構築

当然、引き継ぎにはドキュメントが必要です。必要十分な資料は作成しますが、ドキュメントそれ自体もメンテナンスが必要です。書かずに済むなら済ませたいところです。

理想は「コードがドキュメントになる」ことです。そのために以下のようにしました。

  • インフラはTerraformで構築
  • GitHub ActionsでCI/CDを行う
  • 「テストがドキュメントになる」よう、ユニットテストの構造やdescribeを丁寧に設計し記述する

これによって、開発者は

  1. docker compose up で開発環境が立ち上がり
  2. 実装してPRを作成すると、lintやテストが自動で実行され
  3. PRをマージすると自動でデプロイされる

ので、そもそも手順を覚える必要がありません。

これらの仕組み自体をメンテナンスしたい場合でも、すべてがレポジトリにコードとして一元管理されています。把握しやすく管理しやすい仕組みと言えるのではないでしょうか。

6. 引き継ぎ

ここまでで、プロダクトのベースが出来上がり継続的に開発できる状態になりました。そのためプロトタイプの段階ではありますが、後の実装は社内のエンジニアに引き継ぎました。

先述した必要十分なドキュメントを用意し、30分程度の全員対象のレクチャーを行った上で実際に機能開発に取り組んでもらいました。必要に応じてペアプロを行う想定でしたが、なんと皆、ペアプロを実施せずとも自力で実装してリリースすることができました。

もちろん、社内のエンジニアの皆さんが優秀で、 Adapter パターンなどもコードを見ただけで意図も含めて理解してもらえたことが一番の理由です。しかしながら、先に挙げた様々な工夫も役に立ったのではと思います。

7. 期間・工数

参考までに、どのくらいの規模のプロジェクトだったか、引き継ぎまでにかかった期間・工数を載せておきます。

(この案件では週4日で稼働していたので、期間が1ヶ月の場合、工数としては0.8人月です)

調査・設計:1ヶ月(0.8人月)

  • 技術選定
  • アーキテクチャ設計
  • 実装方針の決定

実装:2.5ヶ月(2.0人月)

  • プロダクトのベース開発
  • 最初の数機能の実装
  • インフラ構築

引き継ぎ:0.5ヶ月(0.4人月)

  • CI/CD整備
  • ドキュメント整備

合計:4ヶ月(3.2人月)

まとめ

プロトタイプの作成とはいえ、以下の理由によりなかなか難易度の高い案件でした。

  • インターフェイスの刷新やAI統合など盛りだくさんの内容だった
  • Notionをフロントエンドおよびデータベースにするという、技術的にリスクのある構成だった
  • 検証が成功すれば本番運用に移る想定のため、作り切りではなくスムーズな引き継ぎが必要だった

以下の工夫により、開発の速度とプロダクトの質のどちらも犠牲にすることなく、スムーズな引き継ぎも実現できました。

技術選定

  • 懸念点を包み隠さず伝えた上で、率直な議論で皆が納得できる結論を得ること
  • むやみに最新技術に飛びつくのではなく、開発メンバーが使い慣れているか、実績があり情報が豊富か、などを考慮すること

AI統合

  • 全てをAIに任せるのではなく、人とAIが協働できるような仕組みを意識すること

引き継ぎ

  • 外部依存を隔離し、テストしやすい設計にすること
  • IaCやCI/CDなど「ドキュメントに頼らない仕組み」を作ること

最後に

今回は技術選定やアーキテクチャ設計を含む、実質的に新規開発に近い案件でした。

技術選定や設計においては「技術的に正しいか」だけでは成功しません。クライアントの状況を理解し、トレードオフを明確にし、引き継げる状態まで責任を持つ。この全体のプロセスが、外部エンジニアとしての価値だと考えています。

この経験が、同様の課題を抱える方々の一助となれば幸いです。ご意見やご質問、ご相談があれば、コメント欄でお気軽にお寄せください!

脚注
  1. もちろんデータの不整合が多発すると運用負荷が上がります。そのためアプリケーションレイヤーでの必要最小限のリトライ処理やエラーハンドリングは行っています。 ↩︎

  2. あるキーワードのGoogleランクが調査結果として出力され、各ランクのURLをインプットとして競合URLの調査が行われる、といったケースです。 ↩︎

Discussion