0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

生成AIを使ったチャットアプリを使って、生成AIが知らないはずのサービスを提供する

Last updated at Posted at 2025-12-20

はじめに

遅ればせながら、LLMエンジニアリングを勉強しながらRAG(MCP)を使えるようになりたいと思っています。

教材にはこれを使いましたが、1から学んでいくにあたって、本当にためになりました。

AI Engineer Core Track: LLM Engineering, RAG, QLoRA, Agents
更新頻度も高く、すでに18万人近くが受講していて、レビュースコアも高いので、かなりおすすめだと思います。

ここでは、ほとんど備忘録的なログ目的ですが自分が使えるようになった部分を残しておきたいと思います。
(ひとが見ている場所で書いたり話したりするのが、一番自分自身の理解を深めますね)

やりたいこと

これが使えるようになった場合、一番はやっぱりそのドメインで機密的に持っているサービスを生成AI経由で操作することです。
たとえば歯医者の予約、メールの自動分類、銘柄の約定、スマート家電のON/OFF、ゲームのマルチサーバー選別、いろいろあると思います。
ここでは例として、映画館サイトが提供するチケット発券を、座席一覧APIと発券APIを使って、RAGで生成AIに専用な知識を補完させて、最終的にAPIのパラメータを組み立てさせて機械的に発券させる方法を書いてみたいと思います。

サンプルコード

実際にはかなりボカしていますが、構造的には実際に動くコードを元にしています。
このコードはpythonで、gradioを使ったチャットアプリをWEBサービスとして構築し、これを本家ドメインのサイトに埋め込んで、自然言語のやりとりを経由して裏側で必要に応じてAPIを操作する、というものになります。

sample.py
import sys
import os
import json
import re
import urllib.parse as urllib
import requests
import gradio as gr
from openai import OpenAI

# OPENAI_API_KEY / GEMINI_API_KEY / ANTHROPIC_API_KEY ...
API_KEY = os.getenv("OPENAI_API_KEY")

# 使用する生成AIのモデル名
MODEL = "gpt-5-nano"

# システムプロンプトで発券ボタンを案内する静的メッセージ
# これを静的にしておくことで、発券ボタン押下で発火する予約APIに、条件となるパラメータをセットさせます
TRIGGER_MESSAGE = "発券の準備ができましたので、以下の発券ボタンを押してください。"

# サービスを提供するAIアシスタントに、APIから取得したリストを元に行わせる業務を定めます
RECEPTION_SYSTEM_PRONPT = f"""
あなたは映画館で発券可能な映画を案内する簡潔なアシスタントです。
以下のリストは発券可能な映画状況一覧です。
nameは映画名、dateは上映日、timeは上映開始時間です。
sheetは座席番号です。
statusが1になっているのが発券可能な空いている座席です。
nameとdate、timeが同じ場合に限り、一度に5名まで発券可能ですが、xyの重複はできません。
希望する映画の時間が決まっても人数や座席がまだ分かっていない場合には、これも決めてもらうようにしてください。
それらすべてが決まったら、発券を進めてください。
発券を進める際には必ず「{TRIGGER_MESSAGE}」と伝えてください。
それ以外ではこのメッセージは決して使わないでください。
リスト:
"""

# 顧客からリクエストを受け取るAPIアシスタントに、チャット履歴からトリガーとなる条件を特定の形式で作成させます
BACKYARD_SYSTEM_PRONPT = """
あなたは発券業務だけに機械的に特化したアシスタントです。
以下のデータから最終的に発券を希望していると思われる条件を以下のjson形式で教えてください。
{"date":"yyyy-mm-dd", "time":"hh:ii", "name":nameのコード番号, ["sheet":sheetのキー名(, "sheet":sheetのキー名)]}
丸カッコ内は可変で、sheetのリストは最大5件まで追加できます。
上記形式で組み立てることができない場合、またはその組み立てに必要な情報がデータから取得できない場合は{}とだけ答えてください。
データ:
"""

# ユーザープロンプトの初期値
USER_MESSAGE = "マドマギが最も早く観れる日を教えてください"

openai = OpenAI()
reception_res = ""
backyard_res = ""
bookable = False

def chat(message, history, request: gr.Request):
    global reception_res, backyard_res, bookable

    if reception_res == "":
        qs = request.query_params
        params = {
            #リストAPIの取得条件
        }

        list_api = f"https://{#リストAPIのエンドポイント}"
        headers = {'Content-Type': 'application/json'}
        response = requests.post(list_api, json=params)
        if (response.status_code == 200):
            list = response.json().get("list", [])
            reception_res = json.dumps(list)

    if reception_res == "":
        bookable = False
        return "ただいま混みあっております。恐れ入りますがしばらく経ってから再度ご確認ください"

    # 反復するチャットのやりとりを履歴として蓄積します
    history = [{"role":h["role"], "content":h["content"]} for h in history]
    messages = [{"role": "system", "content": RECEPTION_SYSTEM_PRONPT + res}] + history + [{"role": "user", "content": message}]

    # チャット履歴から希望された発券条件を取得します
    response = openai.chat.completions.create(model=MODEL, messages=messages)
    bookable = TRIGGER_MESSAGE in response.choices[0].message.content

    if bookable:
        contents = []
        matches = re.findall(r'("user", "content": ([^\}]+)|"assistant", "content": ([^\}]+))', json.dumps(messages))
        if matches:
            for match in matches:
                match = [item for item in match if item != '']
                match = match[-1].replace("[{'text': ", "")
                contents.append(match)

            # 発券条件から発券APIに渡すパラメータを取得します
            summary = ', '.join(contents)
            reqs = [{"role": "system", "content": BACKYARD_SYSTEM_PRONPT + summary}] + [{"role": "user", "content": "発券をお願いします。"}]
            response = openai.chat.completions.create(model=MODEL, messages=reqs)

    return response.choices[0].message.content

def update_button():
    global bookable
    return gr.update(visible=bookable)

def click_button():
    global ex_res
    return ex_res

# gradioを使ったチャットアプリ(サービスサイトにiframeで埋め込んで使います)
# 最終的に「発券する」ボタンを表示して、実際にそれを押すことでサービスサイトに発券させます
with gr.Blocks() as service:
    ui = gr.ChatInterface(
        fn=chat,
        title="券売機コンシェルジュ",
        textbox=custom_textbox,
    )
    condition = gr.Textbox(visible=False)
    condition.change(
        fn=None,
        inputs=condition,
        outputs=condition,
        js="""
        (params) => {
            // 直で発券APIをコールしてもよいですが、ここでは発券前の最終確認画面のURLにjavascriptで発券条件を渡して表示させます
            window.parent.postMessage(params, '{#発券前の最終確認画面のURL}');
        }
        """
    )

    book_button = gr.Button("発券する", visible=False)
    book_button.click(
        fn=click_button,
        inputs=None,
        outputs=schedule,
    )
    ui.chatbot.change(
        fn=update_button,
        inputs=None,
        outputs=[book_button],
    )

service.launch()

最後に

これを応用すれば、基本的に生成AIが持っていない独自なサービスの情報も自由に補完しながらどんな業態にも適用していけるのではないかと思いました。

RAG(MCP)が使えるようになるまでは、そのフローで必要なロジックやシーケンスは実際にプログラムとして開発する必要がありましたが、もはやAPI(DB)さえあればかなりショートカットできますね。
界隈のひとたちには当たり前かもしれませんが、LLMエンジニアリングの将来性の高さの一端を体感することができました。

ちなみに私のような実際に手を動かしながらでないとなかなか覚えられないようなエンジニアにとって、CursorとJupytor nootbookの組み合わせは本当に便利ですね。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?