GMOメイクショップ コアグループ エンジニアの森です。
最近業務効率化のためにOpenAIのAPIを利用して大量の文章データを自動整理するツールを開発をしました。
この記事ではAIを使った文章要約と、過去データとの重複チェックを実装した過程について紹介したいと思います。
開発の背景
課題
makeshopをご利用いただいているショップ運営者から日々様々なご意見・ご要望をカスタマーサポート(CS)にいただいています。その中でmakeshopの機能改善に関する要望については、CSがスプレッドシートに直接記載し、それを確認するという方法で管理していました。しかし1万を超えるショップ様から寄せられる要望は数が多く、さらに以下のような問題が発生していました。
- 要望の重複:複数のショップ様から似た内容の要望が何度も寄せられ、同じ要望が複数回記録されてしまうことがありました。
- 記録のばらつき:CSが要望を記録する際、書き方にばらつきがあり、要望内容の確認や議論が困難になることがありました。
要望の内容確認・精査に工数が増えることで、エンジニアが開発に着手するまでの時間がかかり、対応が遅れる一因となっていました。
解決策
この課題を解決するために、以下の2つの機能を備えたツールを開発することになりました。
1. 要望の要約処理
- 要望をAIが自動的に共通フォーマットに要約・整形することで、素早く概要を把握できるようにする。
- 複数の要望が一つにまとめられた場合でも、それぞれを個別の要望に分解することで、正確な件数を把握できるようにする。
2. 重複した要望の検出
- 重複する要望を検出し、同じ趣旨の要望を繰り返し確認する手間を削減する。
- 複数のショップ様から寄せられた内容が同じ要望の数を把握することで需要の高い要望を見極め、意思決定の最適化を図る。
ツールの概要
ここからは前段の問題を解決するために今回開発したツールについての説明です。
使用した技術
■Chat Completions API
自然な会話やテキスト生成が可能なOpenAIのAPIです。基本的に返答はテキストベースの文章ですが、レスポンス形式を指定することでjson等データで受け取ることも可能です。
■Embeddings API
テキストデータを数値ベクトルに変換できるOpenAIのAPIです。テキストのベクトルデータは自然言語処理によって様々な解析に利用可能です。
■Qdrant
オープンソースのベクトル検索エンジンで、テキストや画像の埋め込みベクトルを扱います。
全体像
以下は今回作成したツールの簡単な全体像です。
流れとしては以下です。
- CSがショップ様から要望を聞き取り
- CSがSlack Workflowの入力フォームから要望を入力
- 入力された要望をツールにかける(←本記事で紹介する部分)
- 整形後の要望をスプレッドシートに書き込む
今回紹介するのは3番の部分、主に上記図の青いエリアでの処理です。
ツールを置いたAWSの環境構築やSlack Workflow、Google Spreadsheetとの連携の実装についても、いずれどこかで紹介できればと思っています。
ツールでの処理内容
APIを呼び出すメソッド
まずAPIの呼び出し部分の紹介です。OpenAIのAPIにリクエストするGoで書かれたメソッドです。
// Chat CompletionAPIの呼び出し // @param ctx context.Context コンテキスト // @param apiKey string APIキー // @param prompts string プロンプト // @param responseFormat *openai.ChatCompletionResponseFormat レスポンスの形式指定 // @return string レスポンス文 func requestCompletionAPI(ctx context.Context, apiKey string, prompts string, responseFormat *openai.ChatCompletionResponseFormat) (string, error) { client := openai.NewClient(apiKey) request := openai.ChatCompletionRequest{ Model: openai.GPT4oMini, Messages: []openai.ChatCompletionMessage{ { Role: "user", Content: prompts, }, }, User: "system", ResponseFormat: responseFormat, } res, err := client.CreateChatCompletion(ctx, request) if err != nil { return "", err } return res.Choices[0].Message.Content, err }
// EmbeddingsAPIの呼び出し // @param ctx context.Context コンテキスト // @param apiKey string APIキー // @param texts []string テキスト // @return [][]float32 テキストのベクトル値 func requestEmbeddingsAPI(ctx context.Context, apiKey string, text string) ([]float32, error) { client := openai.NewClient(apiKey) res, err := client.CreateEmbeddings(ctx, openai.EmbeddingRequest{ Input: text, Model: openai.SmallEmbedding3, User: "system", }) if err != nil { fmt.Printf("errors requestEmbeddingsAPI: %v\n", err) return nil, err } fmt.Printf("success requestEmbeddingsAPI\n") return res.Data[0].Embedding, nil }
要約処理
処理内容
要約はChat Completions APIに本文、該当する機能種別を組み込んだプロンプトを渡すことで実現しました。
大きく以下の3つを満たす出力を得られるようにプロンプトを作成しました。
- 要旨だけを抜き出し文量を減らし箇条書きにする
- 不適切な文章の置換・削除
- 異なる内容の分割
プロンプト
以下が実際のプロンプト(一部)です。
※ %sは本文、機能種別にあたる変数です。
以下はECサイトの運営者から寄せられたmakeshopに対する要望です。 - %s この要望を読み、以下の{整形条件}に従って整形してください。 # 整形条件 - この要望は「%s」の機能に対する要望である前提で要約すること - 主題を明確にすること - 具体的な要望を箇条書きにすること - 細かな要望は省略しないこと - 固有名詞はそのまま残すこと - 出力結果が100文字を超えないようにすること - 全く異なる機能について書かれていた場合、それぞれの機能毎で配列に分割すること - 要望に複数の内容が含まれていた場合、内容毎に配列に分割すること # 出力形式 - 内容毎、機能毎に配列で分割して出力してください。 - 主題となる内容を{topic}、具体的な要望を箇条書きで{detail}に入れてください。 - {topic}は「〇〇について」のような形にしてください - {detail}は文字列の配列で、各要素は具体的な要望を表します makeshopとは何か、やmakeshopの機能やユースケースについては{makeshopの前提知識}を参照してください。
固有名詞など、無くなると要望の根源になりうるものは削除しないようにし、逆に要望に直結しないものについては削除するよう調整しました。容易に概要を把握できるよう共通フォーマットで出力されるようにしています。
{makeshopの前提知識}には、管理画面にある各ページとその機能について説明した文章を定義しています。(文量が多いため割愛)
出力結果
出力結果は jsonオブジェクトの形式で取得するようにしました。レスポンスの形式はAPIリクエストにResponseFormatを含めることで指定可能です。 以下出力結果と使用したResponseFormatです。
元データ(AIで生成したダミーデータ): 最近のリリースで、一部の注文管理機能が使いにくくなった。特に、注文履歴の検索機能が弱体化しており、過去の注文を見つけるのが非常に困難になっている。ユーザーが必要な情報を素早く見つけられるように、機能を改善してもらいたい。また、失敗した原因の追跡についてももっと詳細な情報が提供されることを求める。このような基本的な機能が失われているのは許せない。 出力結果: { "formattedRequests": [ { "topic": "注文管理について", "detail": [ "注文履歴の検索機能の改善を求める", "過去の注文情報を見つけやすくしてほしい", "失敗した原因の追跡について詳細情報を提供してほしい" ] } ] }
responseFormat
responseFormat := &openai.ChatCompletionResponseFormat{ Type: openai.ChatCompletionResponseFormatTypeJSONSchema, JSONSchema: &openai.ChatCompletionResponseFormatJSONSchema{ Name: "FormatRequest", Schema: jsonschema.Definition{ Type: "object", Properties: map[string]jsonschema.Definition{ "formattedRequests": { Type: "array", Description: "要望", Items: &jsonschema.Definition{ Type: "object", Properties: map[string]jsonschema.Definition{ "topic": { Type: "string", Description: "主題", }, "detail": { Type: "array", Description: "詳細", Items: &jsonschema.Definition{ Type: "string", Description: "詳細の要素", }, }, }, }, }, }, }, }, }
重複チェック
処理内容
重複チェックによって過去に同様の要望があった場合に、それを検知できるようにします。処理は以下の5ステップで行われます。
新しい要望をEmbeddings APIにリクエストして、ベクトル化した値を取得
- 先に紹介した
requestEmbeddingsAPI()
の引数に要望の本文を使うことで、ベクトルを取得します。
- 先に紹介した
Qdrantの類似検索によって、過去の類似した要望を取得
重複していた場合、重複IDを付与
- 重複していたと判断した場合は、対象となる過去要望に紐づくUIDを重複IDとして登録します。重複IDを持つかどうかで、過去にあった要望かを判別できます。
重複した要望数をカウント
- 重複IDをカウントすることで、重複が多い要望 = 需要の高い要望として扱うことで、改善対応の優先度決定に反映することができます。
プロンプト
以下はChat Completions APIに重複チェックをリクエストする際のプロンプトの一部です。
# 出力結果 - {類似候補}の中から類似している要望を見つけ、その類似度を0から100の間でスコアリングしてください。 - 類似度の高い順に、類似した要望の管理番号(id)、類似した要望の内容(contents)、類似度(score)、根拠(reason)を配列で出力してください。 - 出力結果は最大1件までとしてください。 - {類似候補}が存在しない場合は、空の配列を出力してください。 ##類似条件 ### 類似していない可能性が高い要素 類似していると判定した理由が以下に該当する場合は、類似していない可能性が高いです。 - 不便である、使いにくい、などの汎用的な表現が一致しているだけが理由の場合 - 使いにくさ、不満が表現されている点だけが理由の場合 - 「ECサイト」、「管理画面」という全体的なワードが一致していることだけが理由の場合 - 機能に関する要望が一致していることだけが理由の場合 - 旧画面からの変化への苦情という点が一致していることだけが理由の場合 - 新旧管理画面の移行に関する要望という点が一致していることだけが理由の場合 以下の要素が含まれている場合、類似していない可能性があります。 - 同じ固有名詞が一度も使用されていない - 対象にしているページ・機能が異なる
{出力結果}には、前述した通り内容の一致度をスコアリングして最もスコアが高い要望を出力するよう指示をしました。スコアの他に識別できるようUID、またAIの判断基準が適切かを検証できるよう類似している根拠も出力するようにしました。
{類似条件}は精度を上げるために検証しながら少しずつ追加していきました。
検証段階で、これらの類似条件が書かれていない状態だと、
「これはECサイトに対する要望という点が共通しているため類似しています。」
「使いにくいという意見が一致しているため類似しています。」
といった大雑把な根拠に基づいて高いスコアが設定されるようになっていました。
そのため、そういった汎用的な表現等は類似している根拠としないよう、プロンプトに追加しました。
出力結果
こちらの出力結果も要約処理と同様にjson形式で取得できるようResponseFormatを指定しました。
以下出力結果と使用したResponseFormatです。この例の場合はスコアが50のため重複とは判定しません。
"checkDuplicateResults": [ { "id": "01920edf-a155-71f7-a37a-caf99d79c132", "contents": "商品管理について\n - 新管理画面のナビゲーションを改善してほしい。\n - 旧管理画面に慣れたユーザーが迷子にならないように配慮してほしい。\n - 頻繁に使う機能へのアクセスを簡単にしてほしい。", "score": 50, "reason": "要望の内容は管理機能に関するものであり、ナビゲーション改善と機能へのアクセス簡素化が共通。しかし、注文履歴や注文情報という具体的機能に関する要求とは異なるため、直接の重複ではない。" } ]
responseFormat
responseFormat := &openai.ChatCompletionResponseFormat{ Type: openai.ChatCompletionResponseFormatTypeJSONSchema, JSONSchema: &openai.ChatCompletionResponseFormatJSONSchema{ Name: "CheckDuplicate", Schema: jsonschema.Definition{ Type: "object", Properties: map[string]jsonschema.Definition{ "checkDuplicateResults": { Type: "array", Description: "類似要望との重複チェック結果", Items: &jsonschema.Definition{ Type: "object", Properties: map[string]jsonschema.Definition{ "id": { Type: "string", Description: "ID", }, "contents": { Type: "string", Description: "要望本文", }, "score": { Type: "integer", Description: "重複度", }, "reason": { Type: "string", Description: "スコアの根拠", }, }, }, }, }, }, }, }
まとめ
今回は文章データを自動整理するツールについて紹介しました。 まだ実験段階のため今後の運用を通してまだまだ改善できるものだと思いますが、引き続き開発と検証を重ねてより高い精度で要望の整理ができるようなれば、その過程についてまた取り上げたいと思います。 このツールではサービスのユーザから要望の文章を対象にしましたが、OpenAIのAPIやQdrantの利用など今回の実践した手法は様々なものに応用できると思います。是非みなさんの抱えた課題の解決に役立ててもらえれると嬉しいです。