ChatGPTと自社データを連携させたチャットボットを作る (なんちゃってAgent実装,コードあり)
こんにちは、@_mkazutaka と申します。
ChatGPT流行ってますね。とはいえいざ社内サービスで提供してみようとするとデータをどうやってChatGPTに渡すか苦労するかと思います(しています)。
今回は、弊社が提供しているサービスとChatGPTをうまく連携させ、自社のデータを使ってチャットボットのPoCを作ってみたのでその実装についての紹介です。最後にコードを載せています(なお会社からの許可はとっていない)
データ元
紹介する前にデータ元のサービスを紹介します。弊社は、ChatGPTを使ったPoCの他にEXPLAZAと呼ばれるアウトドアアイテムの口コミアプリを作っています。お客さまは、画像・レビュー本文等の口コミをEXPLAZAに投稿することができます。例えば以下はとあるテントに関する口コミです。今回は、このような口コミとChatGPTを連携させ、EXPLAZAというサービスで使用するチャットボットのPoCを作成します。
やりたいこと
作成するチャットボットは、アウトドアの口コミアプリに搭載するという想定です。 そのため「一般的なアウトドアに対する質問」に回答できるものが望ましいです。ただしこれだけだとEXPLAZAに搭載する意味はないので、EXPLAZAが持っているデータを使って「アウトドア商品に対する質問」にも回答できるようにします。以下の質問を想定します
アウトドアについての質問, 例: 「キャンプに必要なものを教えて」
アウトドア商品に対する質問, 例: 「ココペリヘキサライトについて教えて」(ココペリヘキサライトはおしゃ焚き火台のことです)
関係ない質問: 「ココペリってなんですか」
最終結果
最終的には、以下のような出力を得ることができました。
例えば、ココペリヘキサライトと呼ばれるおしゃれな焚き火台について質問すると以下のような回答が得られます。この回答には弊社のデータを使っており、最後にURLを付与して返しています。また、「ココペリ」のようなアウトドアに殆ど関係ない質問もしても回答を得ることができます。
実装
LangChainを使っていきます。embedding, fine-tuning, ChatGPT Plugins等試したのですが、最終的にはLangChainのAgentのような考えとChatGPT PluginsのJSONを文章化するという考えを取り入れることになりました。
説明していきます。要件定義から質問は以下の2つに分類されます。
1つ目の「一般的なアウトドアに対する質問」はそのままChatGPTが持っている知識で回答させます。2つ目の「アウトドア商品に対する質問」は、エクスプラザとデータ連携させます。つまり、質問によって回答の方法を分岐します。
分岐のために最初に「その質問が何に対しての質問なのか」を推定する必要があります(これをよしなにやってくれるのがLangChainのAgentなのですが、回答が安定しなかったので今回はなんちゃって実装でやってます)。
おもしろいのはこの推定にもChatGPTが使えるところです。具体的にはChatGPTに以下のようなプロンプトを投げて回答を得ます
取得したChatGPTの回答から情報を分岐させます。以下のようなコードでできます。
answer = ""
if "アウトドアについての質問" in judged:
# ChatGPTに聞いている
answer = conversation.predict(input=message)
if "アウトドア商品に関する質問" in judged:
# 質問文から商品の情報を得る
item_name = get_item_name(message)
# 商品の情報を使ってレビューを取得し、
answer = get_item_review(message, item_name)
「アウトドアについての質問」に関しては、ConversationChainを使って会話ごとChatGPTに渡します。
「アウトドア商品に関する質問」の場合、Explazaから情報を取得する必要があります。Explazaは商品名から検索する機能しか提供していないため、質問文からどの商品に対する質問なのかを推定します。この推定にもChatGPTを使います。具体的には以下のようなプロンプトを投げます。
得られた商品名をExplazaに投げます。GETリクエストでEXPLAZAのsearchエンドポイントを叩きます。
url = "https://api.explaza.jp/openai/search"
headers = {"Content-Type": "application/json"}
params = {"query": item, "num_results": 3}
response = requests.get(url, headers=headers, params=params)
if response.status_code != 200:
raise Exception
response = response.json()
この結果、Explazaからは以下のようなJSONデータが帰ります。
{"results":[{"description":"この時期の焚き火は、\n暖かくて気持ちいい…\nと同時に見てたくなる焚き火台🔥\n \n \nENRICH “ヘキサライト”\n \n \n組み立ても簡単な焚き火台。\n薪もガツガツ入れれるから、\n暖まりたい時は遠慮なく突っ込む😜\n \nそして、その炎が映し出す\nココペリの姿が、まるで踊ってるよう😳\n \nほんとに美しい🥺\n \n \nココペリも心も踊ります🥰\n \n \n \n#焚火台\n#焚き火台\n#焚き火","point":"見てても楽しい焚火台","url":"https://www.explaza.jp/oshi/bf16b519-ed88-4587-a34a-b727456a7cc7"},{"description":"再販されてもすぐに完売してしまう人気の焚き火台\n本当に素敵すぎて眺めてるだけでキャンプ時間が楽しくて幸せな時間になります。\n\n商品詳細\nサイズ:組立時 幅約40㎝奥行約34㎝高さ約30㎝\n重量:約6㎏\n素材:黒皮鉄(耐熱、耐久性に優れ、使い込むほどに経年変化という素材の『味』が出てきます。)\n\nココペリは笛を吹くことで豊作・子宝・幸運などをもたらす神様で、可愛いシルエットが無骨なキャンプを神秘的に演出してくれ、揺らめく焚火の炎の中に現れたココペリが、焚き火をしているキャンパー達に幸運を運んできてれるかも😌\n\n#焚き火\n#焚き火台\n#キャンプギア","point":"幻想的な焚き火台","url":"https://www.explaza.jp/oshi/a3372843-72c7-4a82-bcaa-6c6748516948"}]}
次にこのJSONをChatGPTに投げ、回答を作ってもらいます。今回は、以下のようなプロンプトを使用しました。
回答をよしなにお客さまに返してあげれば完了です。
最終的に
最終的に以下のような回答をえることができました。最後にURLもついていてExplazaのチャットボットだって感じがありますね。
課題
カテゴリ外の質問するとよくわからない答えが帰ってきます。アウトドアブランドについての質問をしているのに、質問が「アウトドア商品に関する質問」に分類され、回答がおかしくなります。分類するカテゴリを増やす、もしくは、必要な情報が聞き返すみたいな処理は都度必要になってくるかぁって感じです。「おすすめのテントを教えて」みたいなふわっとした質問も別でうまく制御しないといけなさそうだなとは思いました。このあたりは結局どれくらいのクオリティのチャットボットを組むか、要件定義の段階でつめないといけませんね。
2つ目は遅い。ChatGPTと3回ほどやり取りを行うため遅いですね。プロンプトを工夫すれば2回までには減らせそうだなとは思っていますが…このあたりはどの企業さんも困ってそう..
感想
今回は、アウトドドア口コミアプリにチャットボットのPoCを実装しました。
Agentの中でやってることを簡易的に実装することで回答を安定させることができたのは良かったです。Google検索の必要がない、動的な自社データを使いたい場合、本記事のような実装に落ち着きそうだなとは思いました。
実装後、ほんとにこの実装の考え方であっているかは考えていたのですが、結局やってることはがアプリのUXでやるか、質問文からChatGPTでやるかの違いなのでそんなに筋として間違ってないのかなとという結論にはなりました。(下記画像でいうと、アプリのUXからポチポチ押していってたのを、ユーザの質問から「はじめての方向けの質問なのか」等をChatGPTを使って推定していく形)
終わりに(宣伝)
Explazaでは、既存サービスへのChatGPT等の導入を検討されている企業さま向けのPoCの実施をしています。
試したみたい相談してみたい等があればお気軽にお問い合わせください!ぜひ一緒に作っていけたら!
コード
今回使ってコードです。実装について質問等があれば @_mkazutaka にお気軽にDMでもメンションでも送ってください。
import gradio as gr
import requests
from langchain.prompts import (
PromptTemplate,
MessagesPlaceholder,
SystemMessagePromptTemplate,
HumanMessagePromptTemplate,
ChatPromptTemplate,
)
from langchain.chains import ConversationChain, LLMChain
from langchain.chat_models import ChatOpenAI
from langchain.memory import ConversationBufferMemory
USER_QUESTION_TYPE_1 = "アウトドアについての質問"
USER_QUESTION_TYPE_2 = "アウトドア商品に関する質問"
AKS_QUESTION_TYPE_TEMPLATE ="""
次の質問は以下のどのカテゴリに当てはまる質問か教えて下さい。
1. アウトドアについての質問
2. アウトドア商品に関する質問
質問: {question}
"""
ASK_ITEM_NAME_TEMPLATE = """次の質問はどのアウトドア製品に関するものか製品名で教えて下さい。
質問: {question}
製品名:
"""
SUMMARIZE_API_RESPONSE_TEMPLATE = """あなたはアウトドアの専門家です。
あなたは、「{item}」のレビューについてユーザーに質問され、以下のJSONの情報を持っています。
以下のJSONには、「{item}」のレビューが含まれています。
レビュー箇所を特定し、以下の条件に従ってユーザーに対する回答文を作成してください。
・中学生にわかるような言葉で説明してください。
・「です」「ます」で答えてください。
・JSONに関する情報は、除いてください。
・最後にJSONから取得したURLを「参考にしたレビューのURLはこちらです:」の形式で追記してください。
```
{json}
```
"""
template = """あなたは、アウトドアアイテムの口コミアプリに搭載されている人間と友好的に会話するAIです。
AIはおしゃべりで、その文脈から多くの具体的な話を提供します。
また、アウトドアに関する知識が豊富でアウトドアの初心者から上級者まですべての人を楽しませることができます。
AIが質問に対する答えを知らない場合、正直に「知らない」と答えます。
"""
prompt = ChatPromptTemplate.from_messages([
SystemMessagePromptTemplate.from_template(template),
MessagesPlaceholder(variable_name="history"),
HumanMessagePromptTemplate.from_template("{input}")
])
llm = ChatOpenAI(temperature=0)
memory = ConversationBufferMemory(return_messages=True)
conversation = ConversationChain(memory=memory, prompt=prompt, llm=llm)
def judge_question_type(message: str) -> str:
prompt = PromptTemplate(
input_variables=["question"],
template=AKS_QUESTION_TYPE_TEMPLATE,
)
chain = LLMChain(llm=llm, prompt=prompt)
return chain.run(question=message).strip()
def get_item_name(message: str) -> str:
prompt = PromptTemplate(
input_variables=["question"],
template=ASK_ITEM_NAME_TEMPLATE,
)
chain = LLMChain(llm=llm, prompt=prompt)
return chain.run(question=message).strip()
def get_item_review(message:str, item: str) -> str:
url = "https://api.explaza.jp/openai/search"
headers = {"Content-Type": "application/json"}
params = {"query": item, "num_results": 3}
response = requests.get(url, headers=headers, params=params)
if response.status_code != 200:
raise Exception
response = response.json()
if len(response['results']) == 0:
return conversation.predict(input=message)
prompt = PromptTemplate(
input_variables=["json", "item"],
template=SUMMARIZE_API_RESPONSE_TEMPLATE,
)
chain = LLMChain(llm=llm, prompt=prompt)
return chain.run(json=response, item=item).strip()
def chat(message, history):
history = history or []
judged = judge_question_type(message)
answer = ""
if USER_QUESTION_TYPE_1 in judged:
answer = conversation.predict(input=message)
if USER_QUESTION_TYPE_2 in judged:
item_name = get_item_name(message)
answer = get_item_review(message, item_name)
history.append((message, answer))
return history, history
demo = gr.Interface(
fn=chat,
inputs=[gr.Textbox(lines=10, placeholder="Message here..."), 'state'],
outputs=[gr.Chatbot(), 'state'],
theme=gr.themes.Soft(
primary_hue="emerald",
),
allow_flagging='never',
)
demo.launch(inline=False, share=False)
最後に
犬かわいい