Mulai membuat aplikasi obrolan berbasis visi multimodal menggunakan Azure OpenAI

Artikel ini menunjukkan cara menggunakan Azure model multimodal OpenAI untuk menghasilkan respons terhadap pesan pengguna dan gambar yang diunggah di aplikasi obrolan. Sampel aplikasi obrolan ini juga mencakup semua infrastruktur dan konfigurasi yang diperlukan untuk memprovisikan sumber daya OpenAI Azure dan menyebarkan aplikasi ke Azure Container Apps menggunakan Azure Developer CLI.

Dengan mengikuti instruksi dalam artikel ini, Anda akan:

  • Sebarkan aplikasi obrolan kontainer Azure yang menggunakan identitas terkelola untuk autentikasi.
  • Unggah gambar yang akan digunakan sebagai bagian dari aliran obrolan.
  • Berinteraksi dengan model bahasa besar (Large Language Model - LLM) multimodal Azure OpenAI menggunakan API Respons dari pustaka OpenAI.

Setelah menyelesaikan artikel ini, Anda dapat mulai memodifikasi proyek baru dengan kode kustom Anda.

Catatan

Artikel ini menggunakan satu atau beberapa templat aplikasi AI sebagai dasar untuk contoh dan panduan dalam artikel. Templat aplikasi AI memberi Anda implementasi referensi yang terawat dan mudah disebarkan dengan baik yang membantu memastikan titik awal berkualitas tinggi untuk aplikasi AI Anda.

Ikhtisar arsitektur

Arsitektur sederhana aplikasi obrolan ditampilkan dalam diagram berikut: Diagram memperlihatkan arsitektur dari klien ke aplikasi backend.

Aplikasi obrolan berjalan sebagai Aplikasi Kontainer Azure. Aplikasi ini menggunakan identitas terkelola melalui Microsoft Entra ID untuk mengautentikasi dengan Azure OpenAI dalam produksi, bukan kunci API. Selama pengembangan, aplikasi mendukung beberapa metode autentikasi termasuk kredensial CLI Pengembang Azure dan kunci API.

Arsitektur aplikasi bergantung pada layanan dan komponen berikut:

  • Azure OpenAI mewakili penyedia AI tempat kami mengirim kueri pengguna.
  • Azure Container Apps adalah lingkungan kontainer tempat aplikasi dihosting.
  • Identitas Terkelola membantu kami memastikan keamanan terbaik di kelasnya dan menghilangkan persyaratan bagi Anda sebagai pengembang untuk mengelola rahasia dengan aman.
  • file Bicep untuk menyediakan sumber daya Azure, termasuk peran Azure OpenAI, Azure Container Apps, Azure Container Registry, Azure Log Analytics, dan kontrol akses berbasis peran (RBAC).
  • Aplikasi Python Quart yang menggunakan paket openai untuk menghasilkan respons terhadap pesan pengguna dengan file gambar yang diunggah.
  • Frontend HTML/JavaScript dasar yang mengalirkan respons dari backend menggunakan JSON Lines melalui ReadableStream.

Biaya

Dalam upaya untuk menjaga harga serendah mungkin dalam sampel ini, sebagian besar sumber daya menggunakan tingkat harga dasar atau konsumsi. Ubah tingkat tingkat Anda sesuai kebutuhan berdasarkan penggunaan yang Anda inginkan. Untuk berhenti dikenakan biaya, hapus sumber daya saat Anda selesai dengan artikel.

Pelajari selengkapnya tentang biaya di repositori sampel.

Prasyarat

Lingkungan kontainer pengembangan tersedia dengan semua dependensi yang diperlukan untuk menyelesaikan artikel ini. Anda dapat menjalankan kontainer pengembangan di GitHub Codespaces (di browser) atau secara lokal menggunakan Visual Studio Code.

Untuk menggunakan artikel ini, Anda perlu memenuhi prasyarat berikut:

Membuka lingkungan pengembangan

Gunakan instruksi berikut untuk menyebarkan lingkungan pengembangan yang telah dikonfigurasi sebelumnya yang berisi semua dependensi yang diperlukan untuk menyelesaikan artikel ini.

GitHub Codespaces menjalankan kontainer pengembangan yang dikelola oleh GitHub dengan Visual Studio Code untuk Web sebagai antarmuka pengguna. Untuk lingkungan pengembangan yang paling mudah, gunakan GitHub Codespaces sehingga Anda memiliki alat dan dependensi pengembang yang benar yang telah diinstal sebelumnya untuk menyelesaikan artikel ini.

Penting

Semua akun GitHub dapat menggunakan Codespace hingga 60 jam gratis setiap bulan dengan dua instans inti. Untuk informasi selengkapnya, lihat penyimpanan dan jam inti bulanan yang disertakan di GitHub Codespaces.

Gunakan langkah-langkah berikut untuk membuat GitHub Codespace baru di cabang main dari repositori Azure-Samples/openai-chat-vision-quickstart GitHub.

  1. Klik kanan pada tombol berikut, dan pilih Buka tautan di jendela baru. Tindakan ini memungkinkan Anda memiliki lingkungan pengembangan dan dokumentasi yang tersedia untuk ditinjau.

    Buka di GitHub Codespaces

  2. Pada halaman Buat codespace , tinjau lalu pilih Buat codespace baru

  3. Tunggu sampai codespace mulai. Proses startup ini dapat memakan waktu beberapa menit.

  4. Masuk ke Azure dengan Azure Developer CLI di terminal di bagian bawah layar.

    azd auth login
    
  5. Salin kode dari terminal lalu tempelkan ke browser. Ikuti petunjuk untuk mengautentikasi dengan akun Azure Anda.

Tugas yang tersisa dalam artikel ini berlangsung dalam konteks kontainer pengembangan ini.

Sebarkan dan jalankan

Repositori sampel berisi semua kode dan file konfigurasi untuk penyebaran aplikasi obrolan Azure. Langkah-langkah berikut memandu Anda melalui proses penyebaran aplikasi obrolan contoh Azure.

Menyebarkan aplikasi obrolan ke Azure

Penting

Untuk menjaga biaya tetap rendah, sampel ini menggunakan tingkat harga dasar atau konsumsi untuk sebagian besar sumber daya. Sesuaikan tingkat sesuai kebutuhan, dan hapus sumber daya saat Anda selesai untuk menghindari biaya.

  1. Jalankan perintah Azure Developer CLI berikut untuk provisi sumber daya Azure dan penyebaran kode sumber:

    azd up
    
  2. Gunakan tabel berikut untuk menjawab perintah:

    Perintah Jawaban
    Nama lingkungan Tetap pendek dan huruf kecil. Tambahkan nama atau alias Anda. Contohnya,chat-vision. Ini digunakan sebagai bagian dari nama grup sumber daya.
    Langganan Pilih langganan untuk membuat sumber daya di dalamnya.
    Lokasi (untuk hosting) Pilih lokasi di dekat Anda dari daftar.
    Lokasi untuk model OpenAI Azure Pilih lokasi di dekat Anda dari daftar. Jika lokasi yang sama tersedia sebagai lokasi pertama Anda, pilih lokasi tersebut.
  3. Tunggu hingga aplikasi diterapkan. Penyebaran biasanya membutuhkan waktu antara 5 dan 10 menit untuk diselesaikan.

Menggunakan aplikasi obrolan untuk mengajukan pertanyaan ke Model Bahasa Besar

  1. Terminal menampilkan URL setelah penyebaran aplikasi berhasil.

  2. Pilih URL yang diberi Deploying service web label untuk membuka aplikasi obrolan di browser.

    Cuplikan layar memperlihatkan gambar yang diunggah, pertanyaan tentang gambar, respons AI, dan kotak teks.

  3. Di browser, unggah gambar dengan mengklik Pilih File dan pilih gambar.

  4. Ajukan pertanyaan tentang gambar yang diunggah seperti "Apa gambarnya?".

  5. Jawabannya berasal dari Azure OpenAI dan hasilnya ditampilkan.

Menjelajahi kode sampel

Sampel ini menggunakan model multimodal OpenAI Azure untuk menghasilkan respons terhadap pesan pengguna dan gambar yang diunggah.

Pengodean Base64 gambar yang diunggah di frontend

Gambar yang diunggah perlu dikodekan Base64 sehingga dapat digunakan langsung sebagai URI Data sebagai bagian dari pesan.

Dalam sampel, cuplikan kode frontend berikut di tag script dari file src/quartapp/templates/index.html menangani fungsionalitas tersebut. Fungsi toBase64 panah menggunakan metode readAsDataURL dariFileReader untuk membaca berkas gambar yang diunggah secara asinkron sebagai string yang dikodekan base64.

    const toBase64 = file => new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.readAsDataURL(file);
        reader.onload = () => resolve(reader.result);
        reader.onerror = reject;
    });

Fungsi toBase64 ini dipanggil oleh pendengar pada peristiwa formulir submit .

Pendengar acara submit menangani alur interaksi obrolan lengkap. Saat pengguna mengirimkan pesan, alur berikut terjadi:

  1. Mendapatkan file gambar yang diunggah (jika ada) dan mengodekan sebagai Base64
  2. Membuat dan menampilkan pesan pengguna dalam obrolan, termasuk gambar yang diunggah
  3. Menyiapkan wadah pesan asisten dengan indikator "Mengetik..."
  4. Menambahkan pesan pengguna ke array riwayat pesan dalam format Api Respons
  5. fetch Mengirim permintaan POST ke /chat/stream titik akhir dengan riwayat dan konteks pesan (termasuk gambar dan nama file yang dikodekan Base64)
  6. Memproses respons baris JSON yang dialirkan untuk menampilkan setiap delta teks secara bertahap
  7. Menangani kesalahan apa pun selama streaming
  8. Menambahkan tombol output ucapan setelah menerima respons lengkap sehingga pengguna dapat mendengar respons
  9. Menghapus bidang input dan mengembalikan fokus untuk pesan berikutnya
form.addEventListener("submit", async function(e) {
    e.preventDefault();

    // Hide the no-messages-heading when a message is added
    document.getElementById("no-messages-heading").style.display = "none";

    const file = document.getElementById("file").files[0];
    const fileData = file ? await toBase64(file) : null;

    const message = messageInput.value;

    const userTemplateClone = userTemplate.content.cloneNode(true);
    userTemplateClone.querySelector(".message-content").innerText = message;
    if (file) {
        const img = document.createElement("img");
        img.src = fileData;
        userTemplateClone.querySelector(".message-file").appendChild(img);
    }
    targetContainer.appendChild(userTemplateClone);

    const assistantTemplateClone = assistantTemplate.content.cloneNode(true);
    let messageDiv = assistantTemplateClone.querySelector(".message-content");
    targetContainer.appendChild(assistantTemplateClone);

    messages.push({
        "role": "user",
        "content": [{"type": "input_text", "text": message}]
    });

    try {
        messageDiv.scrollIntoView();
        const response = await fetch("/chat/stream", {
            method: "POST",
            headers: {"Content-Type": "application/json"},
            body: JSON.stringify({
                messages: messages,
                context: {
                    file: fileData,
                    file_name: file ? file.name : null
                }
            })
        });

        if (!response.ok || !response.body) {
            throw new Error(`Request failed (${response.status})`);
        }

        let answer = "";
        for await (const chunk of readNDJSONStream(response.body)) {
            if (chunk.type === "error" || chunk.type === "response.failed") {
                messageDiv.innerHTML = "Error: " + (chunk.error || "Unknown error");
                break;
            }
            if (chunk.type === "response.output_text.delta") {
                // Clear out the DIV if its the first answer chunk we've received
                if (answer == "") {
                    messageDiv.innerHTML = "";
                }
                answer += chunk.delta;
                messageDiv.innerHTML = converter.makeHtml(answer);
                messageDiv.scrollIntoView();
            }
        }

        messages.push({
            "role": "assistant",
            "content": [{"type": "output_text", "text": answer}]
        });

        messageInput.value = "";

        const speechOutput = document.createElement("speech-output-button");
        speechOutput.setAttribute("text", answer);
        messageDiv.appendChild(speechOutput);
        messageInput.focus();
    } catch (error) {
        messageDiv.innerHTML = "Error: " + error;
    }
});

Mengelola gambar dengan sistem backend

Dalam file src\quartapp\chat.py, kode backend yang menangani gambar dimulai setelah mengonfigurasi autentikasi tanpa kunci.

Catatan

Untuk informasi selengkapnya tentang cara menggunakan koneksi tanpa kunci untuk autentikasi dan otorisasi ke Azure OpenAI, lihat artikel Memulai dengan blok penyusun keamanan OpenAI Azure Microsoft Learn.

Konfigurasi autentikasi

Fungsi ini configure_openai() menyiapkan klien OpenAI sebelum aplikasi mulai melayani permintaan. Ini menggunakan dekorator Quart @bp.before_app_serving untuk mengonfigurasi autentikasi berdasarkan variabel lingkungan. Sistem fleksibel ini memungkinkan pengembang bekerja dalam konteks yang berbeda tanpa mengubah kode.

Mode autentikasi dijelaskan
  • Pengembangan lokal (OPENAI_HOST=local): Terhubung ke layanan API lokal yang kompatibel dengan OpenAI (seperti Ollama atau LocalAI) tanpa autentikasi. Gunakan mode ini untuk pengujian tanpa biaya internet atau API.
  • Azure OpenAI dengan kunci API (variabel lingkungan AZURE_OPENAI_KEY_FOR_CHATVISION): Menggunakan kunci API untuk autentikasi. Hindari mode ini dalam produksi karena kunci API memerlukan rotasi manual dan menimbulkan risiko keamanan jika terekspos. Gunakan untuk pengujian lokal di dalam kontainer Docker tanpa kredensial Azure CLI.
  • Production dengan Identitas Terkelola (RUNNING_IN_PRODUCTION=true): Menggunakan ManagedIdentityCredential untuk mengautentikasi dengan Azure OpenAI melalui identitas terkelola aplikasi kontainer. Metode ini direkomendasikan untuk produksi karena menghilangkan kebutuhan untuk mengelola rahasia. Azure Container Apps secara otomatis memberikan identitas terkelola dan memberikan izin selama penyebaran melalui Bicep.
  • Development dengan Azure CLI (mode default): Menggunakan AzureDeveloperCliCredential untuk mengautentikasi dengan Azure OpenAI menggunakan kredensial Azure CLI yang masuk secara lokal. Mode ini menyederhanakan pengembangan lokal tanpa mengelola kunci API.
Detail implementasi utama
  • Fungsi get_bearer_token_provider() menyegarkan kredensial Azure dan menggunakannya sebagai token pembawa.
  • Jalur titik akhir Azure OpenAI berakhir dengan /openai/v1/, titik akhir berkemampuan OpenAI yang sudah tersedia untuk umum untuk Model Foundry Microsoft.
  • Fungsi ini asinkron, karena Quart adalah kerangka kerja aplikasi web asinkron. Quart memungkinkan penangan permintaan menjadi asinkron, jadi saat aplikasi menunggu respons LLM API yang lambat, server dapat terus menangani permintaan lain.

Berikut adalah kode penyiapan autentikasi lengkap dari chat.py:

@bp.before_app_serving
async def configure_openai():
    bp.model_name = os.getenv("OPENAI_MODEL", "gpt-4o")
    openai_host = os.getenv("OPENAI_HOST", "azure")

    if openai_host == "local":
        bp.openai_client = AsyncOpenAI(api_key="no-key-required", base_url=os.getenv("LOCAL_OPENAI_ENDPOINT"))
        current_app.logger.info("Using local OpenAI-compatible API service with no key")
    elif os.getenv("AZURE_OPENAI_KEY_FOR_CHATVISION"):
        bp.openai_client = AsyncOpenAI(
            base_url=os.environ["AZURE_OPENAI_ENDPOINT"].rstrip("/") + "/openai/v1",
            api_key=os.getenv("AZURE_OPENAI_KEY_FOR_CHATVISION"),
        )
        current_app.logger.info("Using Azure OpenAI with key")
    elif os.getenv("RUNNING_IN_PRODUCTION"):
        client_id = os.environ["AZURE_CLIENT_ID"]
        azure_credential = ManagedIdentityCredential(client_id=client_id)
        token_provider = get_bearer_token_provider(azure_credential, "https://cognitiveservices.azure.com/.default")
        bp.openai_client = AsyncOpenAI(
            base_url=os.environ["AZURE_OPENAI_ENDPOINT"].rstrip("/") + "/openai/v1",
            api_key=token_provider,
        )
        current_app.logger.info("Using Azure OpenAI with managed identity credential for client ID %s", client_id)
    else:
        tenant_id = os.environ["AZURE_TENANT_ID"]
        azure_credential = AzureDeveloperCliCredential(tenant_id=tenant_id)
        token_provider = get_bearer_token_provider(azure_credential, "https://cognitiveservices.azure.com/.default")
        bp.openai_client = AsyncOpenAI(
            base_url=os.environ["AZURE_OPENAI_ENDPOINT"].rstrip("/") + "/openai/v1",
            api_key=token_provider,
        )
        current_app.logger.info("Using Azure OpenAI with az CLI credential for tenant ID: %s", tenant_id)

Fungsi handler obrolan

Fungsi chat_handler() memproses permintaan obrolan yang dikirim ke /chat/stream endpoint. Ini menerima permintaan POST dengan payload JSON.

Payload JSON terdiri dari:

  • pesan: Daftar riwayat percakapan. Setiap pesan memiliki role ("pengguna" atau "asisten") dan content (array bagian konten menggunakan format input API Responses).
  • konteks: Data tambahan untuk diproses, termasuk:
    • file: Data gambar yang dikodekan Base64 (misalnya, data:image/png;base64,...).
    • file_name: Nama file asli gambar yang diunggah (berguna untuk mencatat atau mengidentifikasi jenis gambar).

Handler mengekstrak riwayat pesan dan data gambar. Jika tidak ada gambar yang diunggah, nilai gambar adalah null, dan kode menangani kasus ini.

@bp.post("/chat/stream")
async def chat_handler():
    request_json = await request.get_json()
    request_messages = request_json["messages"]
    # Get the base64 encoded image from the request context
    # This will be None if no image was uploaded
    image = request_json["context"]["file"]

Membangun array input untuk permintaan visi

Fungsi response_stream() menyiapkan array input yang dikirim ke AZURE OpenAI Responses API. Dekorator @stream_with_context menyimpan konteks permintaan saat melakukan streaming respons.

Logika persiapan input

  1. Mulai dengan riwayat percakapan: Fungsi dimulai dengan all_input, yang mencakup semua pesan sebelumnya kecuali yang terbaru (request_messages[0:-1]). Pesan sudah dalam format API Respons dari frontend.
  2. Tangani pesan pengguna saat ini berdasarkan kehadiran gambar:
    • Dengan gambar: Tambahkan input_image bagian konten ke array konten pengguna yang sudah ada.
    • Tanpa gambar: Tambahkan pesan pengguna as-is.
    @stream_with_context
    async def response_stream():
        # This sends all messages, so API request may exceed token limits
        all_input = list(request_messages[0:-1])

        # Add the current user message, appending image if provided
        if image:
            user_content = request_messages[-1]["content"] + [{"type": "input_image", "image_url": image}]
            all_input.append({"role": "user", "content": user_content})
        else:
            all_input.append(request_messages[-1])

Selanjutnya, bp.openai_client.responses.create memanggil AZURE OpenAI Responses API dan mengalirkan respons. Parameter store=False menginstruksikan API untuk tidak menyimpan respons di server, membuat panggilan stateless.

        openai_stream = await bp.openai_client.responses.create(
            model=bp.model_name,
            input=all_input,
            stream=True,
            temperature=0.3,
            store=False,
        )

Akhirnya, respons dialirkan kembali ke klien. API Respons memancarkan banyak jenis peristiwa, tetapi hanya peristiwa yang response.output_text.delta diperlukan untuk streaming teks yang dihasilkan. Peristiwa kesalahan dicatat dan diteruskan ke frontend.

        try:
            async for event in openai_stream:
                if event.type == "response.output_text.delta":
                    yield json.dumps({"type": event.type, "delta": event.delta}, ensure_ascii=False) + "\n"
                elif event.type in ("response.failed", "error"):
                    current_app.logger.error("Responses API error: %s", event)
                    yield json.dumps({"type": event.type}, ensure_ascii=False) + "\n"
        except Exception as e:
            current_app.logger.exception("Error in response stream")
            yield json.dumps({"error": str(e)}, ensure_ascii=False) + "\n"

    return Response(response_stream())

Pustaka dan fitur antarmuka

Frontend menggunakan API dan pustaka browser modern untuk menciptakan pengalaman obrolan interaktif. Pengembang dapat menyesuaikan antarmuka atau menambahkan fitur dengan memahami komponen-komponen ini:

  1. Pengolahan Ucapan: Komponen web kustom menggunakan API Suara browser:

    • <speech-input-button>: Mengonversi pidato ke teks menggunakan Web Speech API SpeechRecognition. Ini menyediakan tombol mikrofon yang mendengarkan input suara dan memancarkan speech-input-result peristiwa dengan teks yang ditranskripsikan.

    • <speech-output-button>: Membaca teks secara lantang menggunakan SpeechSynthesis API. Ini muncul setelah setiap respons asisten dengan ikon speaker, memungkinkan pengguna mendengar respons.

    Mengapa menggunakan API browser alih-alih Azure Speech Services?

    • Tanpa biaya - berjalan sepenuhnya di browser
    • Respons instan - tidak ada latensi jaringan
    • Privasi - data suara tetap berada di perangkat pengguna
    • Tidak perlu sumber daya Azure tambahan

    Komponen-komponen ini berada di src/quartapp/static/speech-input.js dan speech-output.js.

  2. Pratinjau Gambar: Menampilkan gambar yang diunggah dalam obrolan sebelum pengiriman analisis untuk konfirmasi. Pratinjau diperbarui secara otomatis saat file dipilih.

    fileInput.addEventListener("change", async function() {
        const file = fileInput.files[0];
        if (file) {
            const fileData = await toBase64(file);
            imagePreview.src = fileData;
            imagePreview.style.display = "block";
        }
    });
    
  3. Bootstrap 5 dan Ikon Bootstrap: Menyediakan komponen dan ikon UI responsif. Aplikasi ini menggunakan tema Cosmo dari Bootswatch untuk tampilan modern.

  4. Penyajian Pesan berbasis templat: Menggunakan elemen HTML <template> untuk tata letak pesan yang dapat digunakan kembali, memastikan gaya dan struktur yang konsisten.

Sampel sumber daya lainnya untuk dijelajahi

Selain sampel aplikasi obrolan, ada sumber daya lain dalam repositori untuk dijelajahi untuk pembelajaran lebih lanjut. Lihat buku catatan berikut ini di notebooks direktori:

Laptop Deskripsi
chat_pdf_images.ipynb Notebook ini menunjukkan cara mengonversi halaman PDF menjadi gambar dan mengirimkannya ke model visi untuk inferensi.
chat_vision.ipynb Notebook ini disediakan untuk eksperimen manual dengan model visi yang digunakan dalam aplikasi.

Konten yang Dilokalkan: Versi buku catatan spanyol ada di notebooks/Spanish/ direktori, menawarkan pembelajaran langsung yang sama untuk pengembang berbahasa Spanyol. Buku catatan bahasa Inggris dan Spanyol menunjukkan:

  • Cara memanggil model visi secara langsung untuk eksperimen
  • Cara mengonversi halaman PDF ke gambar untuk analisis
  • Cara menyesuaikan parameter dan perintah pengujian

Membersihkan sumber daya

Membersihkan sumber daya Azure

Sumber daya Azure yang dibuat dalam artikel ini ditagihkan ke langganan Azure Anda. Jika Anda tidak mengharapkan untuk membutuhkan sumber daya ini di masa mendatang, hapus sumber daya tersebut untuk menghindari dikenakan lebih banyak biaya.

Untuk menghapus sumber daya Azure dan menghapus kode sumber, jalankan perintah Azure Developer CLI berikut:

azd down --purge

Membersihkan GitHub Codespace

Menghapus lingkungan GitHub Codespaces memastikan bahwa Anda dapat memaksimalkan jumlah pemberian izin per jam inti gratis yang Anda dapatkan untuk akun Anda.

Penting

Untuk informasi selengkapnya tentang hak istimewa akun GitHub Anda, lihat penyimpanan dan jam inti GitHub Codespaces yang disertakan setiap bulan.

  1. Masuk ke dasbor GitHub Codespaces.

  2. Temukan Codespace yang saat ini berjalan yang bersumber dari repositori Azure-Samples//openai-chat-vision-quickstart GitHub.

  3. Buka menu konteks untuk codespace dan pilih Hapus.

Dapatkan bantuan

Catat masalah Anda ke repositori Issues.

Langkah berikutnya