ChatGPT API ハック – EC営業AIの構築 – ベクトル検索によるRAGの導入

AI

フェーズ2:ベクトル検索によるRAGの導入

  1. 商品説明文から OpenAI Embedding を生成
  2. Pinecone / Weaviate / Supabase vector などでベクトル検索セットアップ
  3. ユーザーの質問 → 関連商品抽出 → messages に注入 → ChatGPT呼び出し
  4. 効果・価格・注意点などを含む「事実ベースのセールストーク」生成

Embeddingの生成

Embeddingとは?

Embeddingとは、文章や単語の「意味」をAIが理解できるように数値(ベクトル)で表したものです。たとえば「ニキビを治すクリーム」という文を入力すると、AIはその意味を数百〜数千次元の数値列に変換します。この数列は言葉そのものではなく、「スキンケア」「薬」「肌」「改善」といった概念的な特徴を含んでいます。

Embeddingの大きな特徴は、似た意味の文章は近い位置に配置されることです。たとえば「ニキビを治すクリーム」と「肌荒れを防ぐ薬」は、単語が違っても意味が似ているため、ベクトル空間では近くに並びます。逆に「ヒゲ剃りローション」は遠くに位置します。つまりAIは、単語の一致ではなく「意味の近さ」で判断できるようになります。

EmbeddingはChatGPTのように文章を生成する仕組みとは異なり、一方向の変換(不可逆)です。この数列から元の文章を再構成することはできません。その代わり、意味検索やレコメンド、RAG(検索+生成)といった仕組みで大きな力を発揮します。

長い文章をそのままEmbeddingにすると、複数の話題が混ざって意味が平均化されてしまうため、
通常は200〜500文字程度の短い文や段落に分けて(チャンク化して)ベクトル化します。こうすることで、AIはより正確に文脈を捉え、ユーザーの質問に近い情報を探し出せるようになります。

一言でいえば、Embeddingは「言葉を意味の地図に変える技術」です。AIはこの地図を使って、関連する情報や商品を見つけ出し、より人間的に理解された回答を返します。

1単語でも100単語でも、text-embedding-3-smallでは1,536次元、text-embedding-3-largeでは3,072次元の1ベクトルになる。

Python ライブラリの準備

pip install openai numpy pandas

Embedding生成

from openai import OpenAI
import json
import numpy as np
import pandas as pd

openai_api_key = "xxx"
client = OpenAI(api_key=openai_api_key)

with open("simplified_products.json", "r", encoding="utf-8") as f:
    products = json.load(f)

# 各商品に対してembeddingを生成
embeddings_data = []

for p in products:
    # 商品タイトル+説明を結合(descriptionがない場合はtitleのみ)
    text = f"{p['title']} - {p.get('description', '')}".strip()

    # 高速・低コスト版(精度を上げたいなら text-embedding-3-large)
    response = client.embeddings.create(
        model="text-embedding-3-small",  
        input=text
    )

    vector = response.data[0].embedding
    embeddings_data.append({
        "id": p.get("id", None),
        "title": p["title"],
        "tags": p.get("tags", ""),
        "embedding": vector
    })

# 結果をDataFrameにして確認
df = pd.DataFrame(embeddings_data)
print(df)

# 必要ならCSV保存(Pineconeなどにアップロード前段階)
df.to_csv("product_embeddings.csv", index=False)

Embeddingのイメージ

id                                                 title       tags  embedding
0  Aftershave - Bump Control 65ml Solution 2 packs  Skincare  [0.013, -0.024, 0.009, ...]
1  Benzoyl Peroxide - Pernex 20mg Cream            Skincare  [0.001, 0.027, -0.015, ...]

ベクトル検索のセットアップ

ベクトルデータベース

ChatGPT単体では「外部のデータ」を覚えていません。Embeddingで商品を数値化したあと、それを保存・検索できるベクトルデータベースが必要になります。例えば、以下のようなサービスが存在します。

  • 🟦 Pinecone(クラウド特化、高速・安定)
  • 🟪 Weaviate(オープンソース、自己ホスト可能)
  • 🟩 Supabase Vector(PostgreSQL拡張)
特徴PineconeWeaviateSupabase Vector
運用形態クラウドSaaSOSS or CloudPostgreSQL拡張
セットアップ最も簡単中級(Docker等)開発者フレンドリー
スケーラビリティ◎ 高速スケーラブル◎ 構造化・関係型対応○ 小規模〜中規模
検索性能高精度(Annoy/ScaNNなど)高精度(HNSW)良好(pgvector)
コスト有料(無料枠あり)無料 or 有料Cloud安価 or 自社DB内完結
向き本番運用向け自前構築・制御したい人中小規模PoC

ベクトル検索の流れ

① 商品説明文をEmbedding化

② ベクトルDB(Pineconeなど)に保存

③ ユーザー質問をEmbedding化

④ 類似ベクトル検索(=意味が近い商品を抽出)

⑤ 抽出結果をChatGPTに渡して回答生成

pineconeのインストール

pip install pinecone

Indexの作成とベクトルの追加

index.upsertの実行

from openai import OpenAI
from pinecone import Pinecone, ServerlessSpec
import pandas as pd
import ast
import math

openai_api_key = "xxx"
pinecorn_key = "yyy"

client = OpenAI(api_key=openai_api_key)
pc = Pinecone(api_key=pinecorn_key)

index_name = "products"

if index_name not in [i["name"] for i in pc.list_indexes()]:
    # Index(保存先)を作成
    pc.create_index(
        name=index_name,
        dimension=1536,  # text-embedding-3-small の次元数
        metric="cosine",
        spec=ServerlessSpec(cloud="aws", region="us-east-1")
    )

index = pc.Index(index_name)

df = pd.read_csv("product_embeddings.csv")

vectors = []
for _, row in df.iterrows():
    try:
        emb = ast.literal_eval(row["embedding"])
        tag = row.get("tags", "")
        if isinstance(tag, float) and math.isnan(tag):
            tag = ""  # NaN → 空文字に変換

        vectors.append({
            "id": str(row["id"]),
            "values": emb,
            "metadata": {
                "title": row["title"],
                "tags": tag
            }
        })
    except Exception as e:
        print(f"Skipped row {row['id']}: {e}")

index.upsert(vectors=vectors)
print(f"Uploaded {len(vectors)} vectors to Pinecone index '{index_name}'.")

Indexの状態確認

index.describe_index_statsの実行

from pinecone import Pinecone, ServerlessSpec

pinecorn_key = "yyy"
pc = Pinecone(api_key=pinecorn_key)
index_name = "products"
index = pc.Index(index_name)

stats = index.describe_index_stats()
print(stats)
{'dimension': 1536,
 'index_fullness': 0.0,
 'metric': 'cosine',
 'namespaces': {'': {'vector_count': 41}},
 'total_vector_count': 41,
 'vector_type': 'dense'}

Indexの検索

index.queryの実行
client.embeddings.createの実行

rom openai import OpenAI
from pinecone import Pinecone, ServerlessSpec

openai_api_key = "xxx"
pinecorn_key = "yyy"

client = OpenAI(api_key=openai_api_key)
pc = Pinecone(api_key=pinecorn_key)

index_name = "products"

index = pc.Index(index_name)

query = "ニキビに効果のある薬"
query_emb = client.embeddings.create(
    model="text-embedding-3-small",
    input=query
).data[0].embedding

results = index.query(vector=query_emb, top_k=3, include_metadata=True)
for match in results["matches"]:
    print(f"{match['score']:.2f} - {match['metadata']['title']}")
0.30 - Betamethasone dipropionate cream - Xtraderm 20mg Cream
0.29 - Benzoyl Peroxide - Pernex 20mg Cream
0.26 - Minoxidil - Noxidil 5% Topical Solution 60ml

RAG最小構成完成版

① ユーザー質問を受け取る
② OpenAIで質問をEmbedding化
③ Pineconeで類似商品を検索
④ 類似商品をまとめてChatGPTに渡す
⑤ ChatGPTが自然な説明・提案を生成

from openai import OpenAI
from pinecone import Pinecone
import os

OPENAI_API_KEY = "xxx"
PINECONE_API_KEY = "yyy"
INDEX_NAME = "products"

client = OpenAI(api_key=OPENAI_API_KEY)
pc = Pinecone(api_key=PINECONE_API_KEY)

# === Indexの取得 ===
index = pc.Index(INDEX_NAME)

# === 質問のEmbedding生成 ===
user_query = input("質問を入力してください: ")
embedding_response = client.embeddings.create(
    model="text-embedding-3-small",
    input=user_query
)
query_vec = embedding_response.data[0].embedding

# ===  Pinecone検索 ===
results = index.query(vector=query_vec, top_k=3, include_metadata=True)

# スコアが低いものを除外(例:0.5未満)
matches = [m for m in results["matches"] if m["score"] >= 0.3]

if not matches:
    print("関連商品が見つかりませんでした。")
    exit()

# === 検索結果をまとめてChatGPTに渡す ===
context = "\n".join([
    f"- {m['metadata']['title']} (タグ: {m['metadata'].get('tags', 'N/A')})"
    for m in matches
])

# === ChatGPTで自然な回答生成 ===
system_prompt = """
あなたはオンライン薬局の営業アシスタントです。
次のルールを守ってください:
- 回答は提供された商品情報のみに基づく。
- 人気・おすすめなどの主観的表現は禁止。
- 効果効能は誇張せず、客観的に説明する。
- 不明点は「情報がありません」と答える。
"""

response = client.chat.completions.create(
    model="gpt-4o",
    temperature=0.7,
    messages=[
        {"role": "system", "content": system_prompt},
        {"role": "user", "content": f"以下の商品データを参考にしてください:\n{context}\n\n質問: {user_query}"}
    ]
)

print("\n AIの回答:")
print(response.choices[0].message.content)
質問を入力してください: AGAの薬はある?

 AIの回答:
はい、AGA(男性型脱毛症)の薬として「Dutasteride - Avodart 0.5mg Tablets 30 tabs」があります。

効果・価格・注意点などを含む「事実ベースのセールストーク」生成

  1. ベクトル検索で関連商品を top_k 取得
  2. 取得した商品の 事実(metadata)だけを整形して文脈に注入
  3. ChatGPTに 厳格ルール+JSON形式 で生成させる
  4. JSONをパースしてUIや次処理へ
from openai import OpenAI
from pinecone import Pinecone
import os, json

OPENAI_API_KEY = "xxx"
PINECONE_API_KEY = "yyy"
INDEX_NAME = "products"
SIM_THRESHOLD = 0.30   # 類似度のしきい値(0.5未満は捨てる)
TOP_K = 5              # 候補数

client = OpenAI(api_key=OPENAI_API_KEY)
pc = Pinecone(api_key=PINECONE_API_KEY)
index = pc.Index(INDEX_NAME)

def embed(text: str):
    return client.embeddings.create(
        model="text-embedding-3-small", input=text
    ).data[0].embedding

def search_products(query: str):
    qvec = embed(query)
    res = index.query(vector=qvec, top_k=TOP_K, include_metadata=True)
    matches = [m for m in res["matches"] if m["score"] >= SIM_THRESHOLD]
    return matches

def to_context(matches):
    # ChatGPTに渡す“事実セット”。モデルが読みやすい行構造にする
    lines = []
    for m in matches:
        md = m["metadata"] or {}
        line = {
            "id": m["id"],
            "title": md.get("title") or "",
            "price": md.get("price") or "",
            "description": md.get("description") or "",
            "tags": md.get("tags") or "",
            "image": md.get("image") or "",
            "similarity": round(m["score"], 3)
        }
        lines.append(line)
    return lines

SYSTEM_PROMPT = """あなたは薬局ECの営業アシスタントです。以下を厳守してください。
- 出力は必ずJSONのみ。自然文は書かない。
- 与えられた商品データ以外の知識を使わない(一般知識での補完や想像禁止)。
- 「人気」「おすすめ」「効果的」「ベスト」等の主観表現は禁止。
- 効果・用途・注意点はデータに存在する内容だけで、事実ベースで簡潔に。
- 不明項目は空文字にせず「情報がありません」と記す。
- 価格は与えられた値をそのまま表示(通貨や税抜/税込の推測をしない)。
- 出力スキーマ:
{
  "query": string,
  "pitches": [
    {
      "product_id": string,
      "product_name": string,
      "price": string,
      "what_it_does": string,
      "suitable_for": string,
      "cautions": string,
      "concise_pitch": string,
      "evidence": string  // 「提供データのみ」と記す
    }
  ]
}
"""

def generate_pitch(user_query: str):
    matches = search_products(user_query)
    if not matches:
        return {
            "query": user_query,
            "pitches": []
        }

    context_items = to_context(matches)

    print(context_items)

    # userメッセージに“事実”をJSONで添付
    user_content = (
        "ユーザー質問:\n"
        f"{user_query}\n\n"
        "関連商品データ(JSON):\n"
        + json.dumps(context_items, ensure_ascii=False)
        + "\n\n上記のみを根拠に、スキーマに沿って応答を生成してください。"
    )

    resp = client.chat.completions.create(
        model="gpt-4o",
        temperature=0.3,  # 事実寄せで低め
        response_format={"type": "json_object"},
        messages=[
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": user_content}
        ],
    )

    content = resp.choices[0].message.content
    result = json.loads(content)
    return result

# ===== 実行例 =====
if __name__ == "__main__":
    q = "ニキビに使える外用薬が知りたい。刺激が強すぎないものが良い。"
    result = generate_pitch(q)
    print(json.dumps(result, ensure_ascii=False, indent=2))
{
  "query": "ニキビに使える外用薬が知りたい。刺激が強すぎないものが良い。",
  "pitches": [
    {
      "product_id": "8580179165338",
      "product_name": "Aftershave - Bump Control 65ml Solution 2 packs",
      "price": "950.0",
      "what_it_does": "It soothes and hydrates your skin, helping to reduce redness and leave it feeling calm and smooth after every shave.",
      "suitable_for": "情報がありません",
      "cautions": "情報がありません",
      "concise_pitch": "BUMP PATROLは、シェービング後の肌を落ち着かせ、潤いを与えるアフターシェーブトリートメントです。",
      "evidence": "提供データのみ"
    }
  ]
}

生成のポイント(設計意図)

  • 厳格ガード
    「提供データ以外は使わない」「主観語NG」「不明は“情報がありません”」を system で固定。
  • JSON固定
    response_format={"type":"json_object"}(新SDK)で、パース安定。
  • 低温度
    temperature=0.3 で事実寄せ&再現性UP。
  • スコア閾値
    0.5 未満は除外(“無理やりの紹介”を防止)。必要に応じて調整。
  • concise_pitch
    事実の再言だけで30〜60文字程度の一文にさせるとUIに載せやすい。

※ metadataに「description/cautions/suitable」等が薄いと、“情報がありません”が入ります。精度を上げるには 商品説明を少しだけリッチに(用途・注意・容量など)するのが効きます。

まとめ

フェーズ2では、商品データをベクトル化し、ユーザーの質問内容に応じて関連商品を検索・要約するRAGの仕組みを構築しました。これにより、事実ベースでセールストークを生成できるAI営業アシスタントの土台が完成しました。

一方で、以下の課題が残っています。

  • 応答が完了してからまとめて返るため、ユーザー体験がやや遅い
  • 会話から在庫確認やCRM登録などのアクションにつなげられていない
  • 英語・日本語が混在する環境での対応が限定的
  • 出力結果をCRMや分析に直接活用できる形に整備できていない

これらを踏まえ、次のフェーズ3では以下に取り組みます。

  • stream: true によるリアルタイム応答でUXを改善
  • Function Callingを用いた在庫・CRM連携の自動化
  • 日本語・英語のハイブリッド対応による多言語化
  • 購買確度スコアなどのJSON出力を通じたデータ活用強化

これにより、AIを「情報を答える存在」から
「実際に行動し、業務を支援する営業アシスタント」へと進化させます。

関連記事

カテゴリー

アーカイブ

Lang »