SalesNow Tech Blog

『SaaS+Database』の領域で新たなセールスインフラを創ることにチャレンジしている株式会社SalesNowのテックブログです。

SalesNowのデータ基盤とLLM活用

はじめに

株式会社SalesNowでデータ部門の技術顧問をしている島田(@smdmts)です。

担当領域はデータ処理基盤、Webサービス開発、データ組織作りなどで、SalesNowでは1年半にわたり、データ処理基盤の構築/運用、データを活かした新機能の提案/実装、開発メンバーの採用/教育/チーム運営まで、幅広く業務サポートを行っています。

SalesNowでは、BtoBセールスの負の体験を解消し、社会の生産性を劇的に向上させるべく、「SaaS + Database」を軸とした企業データベース『SalesNow(セールスナウ)』を展開しています。

昨年(2023年)12月14日に、Databricks様、Forkwell様、Timee様のご厚意で、「データ基盤 x LLM」勉強会の場で弊社のLLM活用事例をLTさせていただきました。

この記事では、当日の登壇資料をもとに、SalesNowのデータ基盤とLLM活用について簡単にご紹介いたします。

SalesNowのデータ基盤

SalesNowのデータ基盤は、クローラにScrapy、処理基盤にDatabricksを利用しています。 法的・技術的に認められる範囲で、公開情報となるWebページをクローリングし、乱雑な文字列データを構造化してデータベースに格納しています。

次の図に示すとおり、クローリングしたデータをAWSのAuroraに一次領域として格納し、Delta Lakeに変換して、アプリケーション側のAuroraやElasticsearchに転送する構成になっています。

システム構成

Auroraは、データ量が増えても128TiBまで自動拡張されるスケーラブルなSQLエンジンで、トランザクション処理も可能なため、取得量が変動するデータの一次保管領域に適しています。 この構成はクローラとデータ処理を疎結合とし、AuroraからDelta Lakeに変換することで、分散計算機による大量データ処理を可能にする設計です。

データ品質とメダリオンアーキテクチャ

クローラには、HTMLを構造解析し、表形式の構造化データに変換する処理を施しています。 クローリングしたデータは必ずしも構造化されていないため、システムから利用しやすい形式に変換する必要があります。 たとえば年収の表記には、「n万円」、「nnn,nnn円」、「n万円〜」「n万円以上」などの表記の揺れがあり、これらを標準化(ノーマライズ)する必要があります。

データの標準化には、メダリオンアーキテクチャの利用が有効であると考え、データ基盤を構築しました。 メダリオンアーキテクチャは、Bronze(生ログ)、Silver(クレンジングした中間テーブル)、Gold(目的別に特化したテーブル)の3層構造で、データを抽象度毎に管理するアーキテクチャです。 本来メダリオンアーキテクチャは、ログの解析処理に用いられますが、乱雑なデータをシステムで利用可能な形に加工する場合にも有効です。 年収データをシステムで利用するためには、数値型でデータベースに格納する必要がありますが、その加工処理はBronzeからSilverへのデータ移行時に行われます。

しかし、日本語には同意語・類語が無数にあり、事前に全ての表記法が分かっている訳ではないため、変換パターンを完全に網羅はできません。 そこでDatabricksが提供する機能を用いて、Silverステージにあるデータの何パーセントが適正値であるかを分析・可視化して、その数値改善によるデータ品質の向上を行っています。 年収データの場合、数値型への変換に失敗したデータや、相場から大幅に乖離した(入力ミスと思われる)データなどが非適正値と判断されます。

データに特化したプロダクト開発では、データ品質の数値化は重要であり、可視化された数値をもとに改善状況の進捗確認をしています。

データ基盤とLLM活用

SalesNowでは、自動化されたクローリングシステムと人の手による入力という2つの方法でデータを収集し、データ処理基盤を通して利用者にとって価値の高い形に加工することで、即時性が高く正確な情報提供を実現しています。 このデータ蓄積作業は非常にコストが掛かるため、LLMを活用した代替機能の検討を行いました。

一般的に、データプロダクトの開発には、ふるまい要件(アプリケーションで達成したいふるまい)とデータ要件(ふるまい要件で必要となるデータ)の2つの要件があります。

ふるまい要件とデータ要件

今回のLLM活用でのふるまい要件は、「日本の全企業の会社概要をSalesNow上に表示したい」ことです。 日本全国には約550万社の企業があり、これらの会社概要文を人の手で作成するのはコスト的に現実的ではありません。 企業情報を見ながら概要文を作成し、正しいかを別人がダブルチェックするオペレーションをLLMで代替えできれば、データ作成を自動化できます。

SalesNowのデータ要件では、「高品質であること」が絶対的に求められます。高品質データとは次のようなものです。

  • 最新のデータ、かつ、誤りが無い
  • 完全性(インテグラリティ)がある(重複や欠損が無い)
  • 文章においては、主語や目的語の欠損が無い、意味が明瞭、誤字の少なさなど

以上をふまえ、「データ品質の担保」と「コスト抑制」 の観点から、LLMで代替可能な作業範囲を検証します。

  • データ品質の担保:LLMの成果物が正しいかを確認し歩留まり率を算出する → 品質の数値化
  • コスト抑制:人件費とAPI利用費用を比較する

今回の検証では、文書生成をChatGPT、品質評価をGoogle PaLM2に置き換えました。

LLM-as-a-Judge

この手法は、LLM-as-a-Judgeと呼ばれており、LLMが出力した結果をLLMに評価させるアプローチです。 データベースに格納済みの企業データからLLMを用いて会社概要文を生成し、その品質をLLMに自動評価させます。

LLMによる文書生成と評価

LLMは原理上、ハルシネーションが発生することが知られており、生成時のみならず評価時でも誤った情報が出力されることを前提としたシステム構築が必要です。 LLMによる成果物が本当に利用者にとって高品質であるかを判断するためには、最終的には人による確認が必要です。 SalesNowではデータリサーチャと呼ばれるデータの調査を担当する専門職種が設置されており、本当に高品質であるかを人力評価していただきました。 (以下のプロンプト例は、実際に使用したものではなく概略です。)

  • 生成プロンプト:以下の文章から第三者視点で会社概要文を生成してください。提供サービスや企業説明に重点を置き、本文中に無いものは一切使用しないでください。
  • 評価プロンプト:企業の事業・商品・サービスが明確な場合に1点、企業情報が第三者視点で書かれている場合に1点、入力文書に会社概要と無関係な物が含まれていない場合に1点、合計3点で採点してください。また、日本語として適切ではない場合は0点としてください。
  • リサーチャへの依頼:出力された文章と採点を鑑みて、顧客に提示可能な情報であるか判断してください。

このような指示で会社概要文の生成、評価、人による判断を行った結果、次のような結果になりました。

  • 2点:「xx社は、プレスリリースや求人情報も掲載しています。」
    • どの企業でも行っている一般的内容のため、会社概要としては不適切
  • 2点:「xx社は、札幌で老人ホームと介護サービスを展開している会社です。」
    • 調査の結果、札幌以外にも事業所があるため、説明不足
  • 0点:「xx社は、金融機関として行動力みなぎるコミュニティバンクを実現します!」
    • 企業理念としては適切だが、会社概要としては説明不足
  • 3点:「愛知県に本社を置くxxxのトップシェア企業です。」
    • 調査の結果、トップシェア企業では無かった

ここから生成プロンプトや評価プロンプトをチューニングすることにより、文書生成や評価の精度向上が期待できるため、今後への課題として絶賛検証中です。

まとめ

SalesNowのデータ基盤とLLM活用について簡単に紹介させていただきました。 検証結果の続きを知りたい方は、こちらのスライドをご覧ください。

speakerdeck.com

データ基盤の構築や、データを軸としたプロダクトづくりに興味がある方は、ご意見、ご感想等、コメントにて頂けると嬉しいです。

SalesNowの開発組織は、最新のデータベース技術を積極的に取り入れ、技術を通じてBtoBセールスの負の体験を解消し、社会の生産性を劇的に向上させることに取り組んでいます。

プロダクトの成長に伴い、データベースの中長期的な競合優位性構築や、開発プロセスの持続的な改善や組織力の向上を目指し、ソフトウェアエンジニア・データエンジニア・データサイエンティストなどを積極採用しています。

技術選定、アーキテクチャ設計、開発プロセスの改善、開発組織の強化などを担当していただき、CTO/VPoE/テックリードなどの上位ポジションに挑戦することも歓迎しています。

カジュアルにまずはお話しましょう!

open.talentio.com

FastAPIでAPIを複数のSwagger UIに分割して管理する方法

概要

株式会社SalesNowではFastAPIを使ってバックエンドを構築しています。

FastAPIを使うことでSwagger UIを自動的に生成することができ、APIのドキュメント化や検証に非常に有用ですが、既定では1つのSwagger UIに全てのWebAPIが羅列されるため、規模が大きくなるにつれて、管理が辛くなることが想定されます。

SalesNowの提供するWebシステムでは100以上のDBテーブル、300以上のWebAPIがあるため、機能ドメイン毎にSwaggerUIを分割して管理しています。

本記事では、この機能ドメイン等の単位でSwagger UIを分割して管理する方法を紹介します。

FastAPIで複数のSwagger UIを管理する方法

FastAPIはFastAPIクラスを使いappを作成し、このappに任意のPath関数を紐付けていくことでバックエンドを構築することができます。このapp毎にSwagger UIが作成されるため、機能ドメイン等の任意の単位で複数のFastAPIのappを作成し、ベースとなるapp(uvicornで起動するapp)に対して他のappをmountすることで、複数のSwagger UIを作成することができます。

ただし、この状態では、Swagger UI間のリンクが無く不便であるため、以下のコード例ではFastAPIのappのdescriptionに対して、Swagger UIへのリンクを生成するためのHTMLをセットして、Swagger UI上部に他のSwagger UIへのリンクを生成するようにしています。

コード例

# main.py
from fastapi import FastAPI

# appを定義
app = FastAPI(title="App")
feature_app = FastAPI(title="App(/feature)")
admin_app = FastAPI(title="App(/admin)")

# Path関数を定義
@app.get("/")
def base_app_root():
    return {"message": "Hello World"}

@feature_app.get("/")
def feature_app_root():
    return {"message": "Hello World"}

@admin_app.get("/")
def admin_app_root():
    return {"message": "Hello World"}

# BaseとなるAppに他のappをmountする
app.mount("/feature", feature_app)
app.mount("/admin", admin_app)

APP_PATH_LIST = ["", "/feature", "/admin"]

# Swagger間のリンクを作成
def _make_app_docs_link_html(app_path: str, app_path_list: list[str]) -> str:
    """swaggerの上部に表示する各Appへのリンクを生成する"""
    descriptions = [
        f"<a href='{path}/docs'>{path}/docs</a>" if path != app_path else f"{path}/docs"
        for path in app_path_list
    ]
    descriptions.insert(0, "Apps link")
    return "<br>".join(descriptions)

app.description = _make_app_docs_link_html("", APP_PATH_LIST)
feature_app.description = _make_app_docs_link_html("/feature", APP_PATH_LIST)
admin_app.description = _make_app_docs_link_html("/admin", APP_PATH_LIST)

コマンド実行例

# venv作成
$ python -m venv .venv

# venv有効化
$ . .venv/bin/activate

# packageインストール
$ pip install fastapi uvicorn

# uvicornでfastapiを実行
$ python -m uvicorn main:app --port 8080 --reload

http://localhost:8080/docsにアクセスすると、以下のような画面が表示されます。

App毎に別のSwagger UIが作成され、その間の相互リンクが画面上部に生成して、ページ間の移動を容易にしています。

baseのAppのSwagger UIです。画面上部にURLリンクが表示されており、これをクリックすることで複数のSwagger UI間を相互に移動することができます。

featureのAppのSwaggerUIです。

複数のAppを管理する独自クラスを作る

先程の実装でも動作はしますが、Appが多くなった場合に管理が煩雑になる恐れがあるため、FastAPIAppManagerクラスを作成して、管理を容易にしたものは以下の通りです。

複数のappを管理するためのFastAPIAppManagerクラスを作成して、app設定のコピーやSwagger UI間リンクの生成を行っています。

# main2.py (main.pyの改良版)
from fastapi import FastAPI

from app_manager import FastAPIAppManager

# appを定義
app = FastAPI(title="App")
feature_app = FastAPI()
admin_app = FastAPI()
app_manager = FastAPIAppManager(root_app=app)

# Path関数を定義
@app.get("/")
def base_app_root():
    return {"message": "Hello World"}

@feature_app.get("/")
def feature_app_root():
    return {"message": "Hello World"}

@admin_app.get("/")
def admin_app_root():
    return {"message": "Hello World"}

# FastAPIAppManagerにappを追加することで、Linkの自動生成を行う
app_manager.add_app(path="/feature", app=feature_app)
app_manager.add_app(path="/admin", app=admin_app)
app_manager.setup_apps_docs_link()
# app_manager.py
from fastapi import FastAPI

class FastAPIAppManager:
    """複数のFastAPI appを管理するためのManagerクラス"""

    def __init__(self, root_app: FastAPI) -> None:
        self.app_path_list: list[str] = [""]
        self.root_app: FastAPI = root_app
        self.apps: list[FastAPI] = [root_app]

    def add_app(self, app: FastAPI, path: str) -> None:
        self.apps.append(app)
        if not path.startswith("/"):
            path = f"/{path}"
        else:
            path = path
        self.app_path_list.append(path)
        # ベースとなるappのTitle等を引き継ぐ
        app.title = f"{self.root_app.title}({path})"
        app.version = self.root_app.version
        app.debug = self.root_app.debug
        self.root_app.mount(path=path, app=app)

    def setup_apps_docs_link(self) -> None:
        """他のAppへのリンクがopenapiに表示されるようにセットする"""
        for app, path in zip(self.apps, self.app_path_list, strict=True):
            app.description = self._make_app_docs_link_html(path)

    def _make_app_docs_link_html(self, current_path: str) -> str:
        """openapiの上部に表示する各Appへのリンクを生成する"""
        descriptions = [
            f"<a href='{path}/docs'>{path}/docs</a>"
            if path != current_path
            else f"{path}/docs"
            for path in self.app_path_list
        ]
        descriptions.insert(0, "Apps link")
        return "<br>".join(descriptions)

ソースコード

上記のソースコードは以下で公開しております。

github.com

まとめ

FastAPIでWebAPIを複数のSwagger UIに分割して管理する方法を紹介しました。

ご意見、ご感想等、コメントにて頂けると嬉しいです。

SalesNowの開発組織は、最新のデータベース技術を積極的に取り入れ、技術を通じてBtoBセールスの負の体験を解消し、社会の生産性を劇的に向上させることに取り組んでいます。

プロダクトの成長に伴い、データベースの中長期的な競合優位性構築や、開発プロセスの持続的な改善や組織力の向上を目指し、ソフトウェアエンジニア・データエンジニア・データサイエンティストなどを積極採用しています。

技術選定、アーキテクチャ設計、開発プロセスの改善、開発組織の強化などを担当していただき、CTO/VPoE/テックリードなどの上位ポジションに挑戦することも歓迎しています。

カジュアルにまずはお話しましょう!

open.talentio.com

SalesNow Tech Blogはじめました。

こんにちは。SalesNowの代表の(@Ats_mrk)です。

僕たちは、B2Bセールスの働き方を変える企業データベース『SalesNow(セールスナウ)を展開しています。

本日よりこのSalesNow Tech Blogで、社内で取り組んでいる開発業務やUI/UXデザイン、PM(プロジェクトマネジメント)/PdM(プロダクトマネジメント)など、プロダクト開発に関連することを中心に発信していきます。

最初の記事ということで、今回はTech Blogを開始した背景を書いていきます。

SalesNowのプロダクト開発を伝えるため

SalesNowでは、これまでにも弊社のサービスや企業文化に関する情報を、コーポレートサイトや各自のX(旧Twitter)アカウント、事業・組織に関する情報をまとめたCampany Deck、会社の雰囲気やカルチャーを伝えるnote、大事にしている価値観をまとめたCulture Deckなどを通じてお伝えしてきました。こちらに加え、Tech Blogを通して「プロダクトチームが取り組んでいる課題や、課題解決に向けてどのようなアプローチをしているのか」をより細かく伝えていきたいと思います。

情報の透明性を高め、SalesNowの理解を深められるように

SalesNowでは「言語化とナレッジシェア」を大事にしています。社内外問わず、情報をオープンにすることでSalesNowに関心を持ってくれた人が、SalesNowがどのようなプロダクト開発をしてくれたか簡単にキャッチアップできるようにしたいと考えています。

SalesNowの開発チームが率先して言語化したナレッジをオープンに発信していくことにより、より多くの人がスタートアップ/セールス業界全体を良くしていける雰囲気作りをしていきたいと考えています。

メンバーの声を届け、良いチームを創る

各メンバーの情報発信によって自身が取り組んできた業務へのFB(フィードバック)が社外から得られ、それらがメンバー個人やチームに新たな気づきを促し、より良い開発組織へと成長していけると考えています。

また、社内向けの発信としてもメンバー一人ひとりが日々の仕事やプロジェクトで学んだこと、直面した課題、そしてそれをどのように解決しているかを共有する場としても、このTech Blogを活用していきます。

最後に

本日はSalesNow Tech Blogの開始のご挨拶をさせていただきました。今後は社内の様々な声や、SalesNowプロダクトに関する情報を共有していく予定です。

どうぞご期待ください!

 

SalesNowのプロダクトや文化についてもっと知りたいと思われた方は、カジュアル面談でお話しましょう。

open.talentio.com