Xây dựng trò chơi đoán chữ bằng Prompt API

Ngày xuất bản: 10 tháng 10 năm 2025

Trẻ em độ tuổi đi học chơi trò chơi Guess Who vào năm 2014.

Trò chơi trên bàn kinh điển Guess Who? (Đoán xem ai?) là một trò chơi bậc thầy về suy luận diễn dịch. Mỗi người chơi bắt đầu với một bảng gồm các khuôn mặt và thông qua một loạt câu hỏi có hoặc không, bạn sẽ thu hẹp các khả năng cho đến khi có thể tự tin xác định nhân vật bí mật của đối thủ.

Sau khi xem bản minh hoạ về AI tích hợp tại Google I/O Connect, tôi đã tự hỏi: điều gì sẽ xảy ra nếu tôi có thể chơi trò chơi Đoán người với AI ngay trong trình duyệt? Với AI phía máy khách, ảnh sẽ được diễn giải cục bộ, vì vậy, trò chơi Guess Who? tuỳ chỉnh về bạn bè và gia đình sẽ vẫn ở chế độ riêng tư và an toàn trên thiết bị của tôi.

Tôi chủ yếu làm việc trong lĩnh vực phát triển giao diện người dùng và trải nghiệm người dùng, đồng thời quen với việc tạo ra những trải nghiệm hoàn hảo đến từng pixel. Tôi hy vọng có thể làm được điều đó bằng bản diễn giải của mình.

Ứng dụng của tôi, AI Guess Who?, được xây dựng bằng React và sử dụng Prompt API cùng một mô hình tích hợp sẵn trong trình duyệt để tạo ra một đối thủ có khả năng đáng kinh ngạc. Trong quá trình này, tôi nhận ra rằng việc tạo ra kết quả "hoàn hảo đến từng pixel" không hề đơn giản. Tuy nhiên, ứng dụng này minh hoạ cách AI có thể được dùng để xây dựng logic trò chơi chu đáo và tầm quan trọng của kỹ thuật tạo câu lệnh để tinh chỉnh logic này và nhận được kết quả mà bạn mong đợi.

Hãy đọc tiếp để tìm hiểu về tính năng tích hợp AI có sẵn, những thách thức tôi gặp phải và các giải pháp tôi đã tìm ra. Bạn có thể chơi trò chơi và tìm mã nguồn trên GitHub.

Nền tảng trò chơi: Ứng dụng React

Trước khi xem xét việc triển khai AI, chúng ta sẽ xem xét cấu trúc của ứng dụng. Tôi đã tạo một ứng dụng React tiêu chuẩn bằng TypeScript, với một tệp App.tsx trung tâm đóng vai trò là người điều phối của trò chơi. Tệp này chứa:

  • Trạng thái trò chơi: Một enum theo dõi giai đoạn hiện tại của trò chơi (chẳng hạn như PLAYER_TURN_ASKING, AI_TURN, GAME_OVER). Đây là phần quan trọng nhất của trạng thái, vì nó quyết định giao diện hiển thị những gì và người chơi có thể thực hiện những hành động nào.
  • Danh sách nhân vật: Có nhiều danh sách chỉ định các nhân vật đang hoạt động, nhân vật bí mật của mỗi người chơi và những nhân vật đã bị loại khỏi bảng.
  • Trò chuyện trong trò chơi: Nhật ký liên tục về các câu hỏi, câu trả lời và tin nhắn hệ thống.

Giao diện được chia thành các thành phần logic:

GameSetup là màn hình ban đầu.
GameBoard hiển thị lưới các ký tự và chế độ điều khiển trò chuyện để xử lý mọi thông tin đầu vào của người dùng.

Khi các tính năng của trò chơi tăng lên, độ phức tạp của trò chơi cũng tăng theo. Ban đầu, toàn bộ logic của trò chơi được quản lý trong một React hook tuỳ chỉnh duy nhất, có kích thước lớn useGameLogic, nhưng nó nhanh chóng trở nên quá lớn để điều hướng và gỡ lỗi. Để cải thiện khả năng duy trì, tôi đã tái cấu trúc hook này thành nhiều hook, mỗi hook có một trách nhiệm duy nhất. Ví dụ:

  • useGameState quản lý trạng thái cốt lõi
  • usePlayerActions là lượt của người chơi
  • useAIActions là dành cho logic của AI

Giờ đây, hook useGameLogic chính đóng vai trò là một thành phần kết hợp rõ ràng, đặt các hook nhỏ hơn này lại với nhau. Thay đổi về cấu trúc này không làm thay đổi chức năng của trò chơi, nhưng giúp cơ sở mã trở nên gọn gàng hơn rất nhiều.

Logic trò chơi bằng Prompt API

Trọng tâm của dự án này là việc sử dụng Prompt API.

Tôi đã thêm logic trò chơi AI vào builtInAIService.ts. Sau đây là những trách nhiệm chính của nhóm này:

  1. Cho phép câu trả lời nhị phân, mang tính hạn chế.
  2. Dạy cho mô hình chiến lược chơi trò chơi.
  3. Hướng dẫn phân tích mô hình.
  4. Khiến mô hình bị mất trí nhớ.

Cho phép câu trả lời nhị phân, mang tính hạn chế

Người chơi tương tác với AI như thế nào? Khi người chơi hỏi: "Nhân vật của bạn có đội mũ không?", AI cần "nhìn" vào hình ảnh nhân vật bí mật của mình và đưa ra câu trả lời rõ ràng.

Những lần đầu tiên tôi làm đều thất bại. Câu trả lời mang tính trò chuyện: "Không, nhân vật mà tôi đang nghĩ đến, Isabella, dường như không đội mũ", thay vì đưa ra câu trả lời có hoặc không. Ban đầu, tôi đã giải quyết vấn đề này bằng một câu lệnh rất nghiêm ngặt, về cơ bản là yêu cầu mô hình chỉ phản hồi bằng "Có" hoặc "Không".

Mặc dù cách này có hiệu quả, nhưng tôi đã tìm hiểu được một cách hay hơn nữa bằng cách sử dụng đầu ra có cấu trúc. Bằng cách cung cấp JSON Schema cho mô hình, tôi có thể đảm bảo nhận được câu trả lời đúng hoặc sai.

const schema = { type: "boolean" };
const result = session.prompt(prompt, { responseConstraint: schema });

Điều này giúp tôi đơn giản hoá câu lệnh và cho phép mã của tôi xử lý phản hồi một cách đáng tin cậy:

JSON.parse(result) ? "Yes" : "No"

Dạy mô hình chiến lược trò chơi

Việc yêu cầu mô hình trả lời một câu hỏi đơn giản hơn nhiều so với việc yêu cầu mô hình bắt đầu và đặt câu hỏi. Một người chơi giỏi trò chơi Đoán xem ai? sẽ không hỏi những câu hỏi ngẫu nhiên. Họ đặt những câu hỏi loại bỏ được nhiều ký tự nhất cùng một lúc. Một câu hỏi lý tưởng sẽ giảm một nửa số ký tự còn lại có thể bằng cách sử dụng các câu hỏi nhị phân.

Làm cách nào để dạy cho mô hình chiến lược đó? Một lần nữa, đó là thiết kế câu lệnh. Câu lệnh cho generateAIQuestion() thực ra là một bài học ngắn gọn về lý thuyết trò chơi Guess Who?

Ban đầu, tôi yêu cầu mô hình "đặt một câu hỏi hay". Kết quả không thể đoán trước. Để cải thiện kết quả, tôi đã thêm các ràng buộc phủ định. Giờ đây, câu lệnh sẽ có hướng dẫn tương tự như sau:

  • "QUAN TRỌNG: CHỈ hỏi về các tính năng hiện có"
  • "NGHIÊM TRỌNG: Tạo nội dung nguyên gốc. KHÔNG lặp lại câu hỏi".

Những ràng buộc này thu hẹp phạm vi tập trung của mô hình, ngăn mô hình đặt những câu hỏi không liên quan, nhờ đó khiến mô hình trở thành một đối thủ thú vị hơn nhiều. Bạn có thể xem toàn bộ tệp câu lệnh trên GitHub.

Dạy mô hình phân tích

Đây là thách thức khó khăn và quan trọng nhất. Khi mô hình đặt câu hỏi, chẳng hạn như "Nhân vật của bạn có đội mũ không" và người chơi trả lời không, làm cách nào để mô hình biết những nhân vật nào trên bảng của họ đã bị loại bỏ?

Mô hình này sẽ loại bỏ tất cả những người đội mũ. Những lần thử đầu tiên của tôi đều gặp phải lỗi logic, đôi khi mô hình loại bỏ sai ký tự hoặc không loại bỏ ký tự nào. Ngoài ra, "mũ" là gì? "Mũ len" có được tính là "mũ" không? Thành thật mà nói, đây cũng là điều có thể xảy ra trong một cuộc tranh luận giữa người với người. Và tất nhiên, những lỗi chung cũng có thể xảy ra. Theo góc độ của AI, tóc có thể trông giống như một chiếc mũ.

Tôi đã thiết kế lại cấu trúc để tách nhận thức khỏi suy luận mã:

  1. AI chịu trách nhiệm phân tích hình ảnh. Các mô hình vượt trội trong việc phân tích hình ảnh. Tôi đã hướng dẫn mô hình trả về câu hỏi và một bản phân tích chi tiết theo giản đồ JSON nghiêm ngặt. Mô hình này phân tích từng nhân vật trên bảng và trả lời câu hỏi "Nhân vật này có đặc điểm này không?" Mô hình này trả về một đối tượng JSON có cấu trúc:

    { "character_id": "...", "has_feature": true }
    

    Một lần nữa, dữ liệu có cấu trúc là yếu tố then chốt để đạt được kết quả thành công.

  2. Mã trò chơi sử dụng thông tin phân tích để đưa ra quyết định cuối cùng. Mã ứng dụng sẽ kiểm tra câu trả lời của người chơi ("Có" hoặc "Không") và lặp lại quy trình phân tích của AI. Nếu người chơi nói "Không", mã sẽ biết cách loại bỏ mọi ký tự mà has_featuretrue.

Tôi nhận thấy việc phân chia công việc này là yếu tố then chốt để xây dựng các ứng dụng AI đáng tin cậy. Sử dụng AI cho các chức năng phân tích và để mã ứng dụng của bạn đưa ra các quyết định nhị phân.

Để kiểm tra khả năng nhận thức của mô hình, tôi đã tạo một bản trực quan hoá cho bản phân tích này. Điều này giúp bạn dễ dàng xác nhận xem nhận thức của mô hình có chính xác hay không.

Thiết kế câu lệnh

Tuy nhiên, ngay cả khi có sự tách biệt này, tôi nhận thấy nhận thức của mô hình vẫn có thể bị sai lệch. Có thể tính năng này sẽ đánh giá sai việc một nhân vật có đeo kính hay không, dẫn đến việc loại bỏ nhầm và gây khó chịu. Để khắc phục vấn đề này, tôi đã thử nghiệm một quy trình gồm hai bước: AI sẽ đặt câu hỏi. Sau khi nhận được câu trả lời của người chơi, hệ thống sẽ thực hiện một phân tích mới lần thứ hai với câu trả lời làm bối cảnh. Lý thuyết là lần xem xét thứ hai có thể phát hiện ra những lỗi trong lần xem xét đầu tiên.

Quy trình đó sẽ diễn ra như sau:

  1. Lượt tương tác của AI (lệnh gọi API 1): AI hỏi: "Nhân vật của bạn có râu không?"
  2. Lượt của người chơi: Người chơi nhìn vào nhân vật bí mật của mình (một người đàn ông cạo râu) và trả lời: "Không".
  3. Lượt AI (lệnh gọi API 2): AI tự yêu cầu xem xét lại tất cả các ký tự còn lại và xác định những ký tự cần loại bỏ dựa trên câu trả lời của người chơi.

Ở bước 2, mô hình vẫn có thể nhận nhầm một nhân vật có râu quai nón nhạt là "không có râu" và không loại bỏ nhân vật đó, mặc dù người dùng mong đợi mô hình sẽ làm như vậy. Lỗi nhận thức cốt lõi không được khắc phục và bước bổ sung chỉ làm chậm kết quả. Khi chơi với đối thủ là người, chúng ta có thể chỉ định một thoả thuận hoặc làm rõ về vấn đề này; trong chế độ thiết lập hiện tại với đối thủ là AI, điều này không xảy ra.

Quy trình này làm tăng độ trễ từ lệnh gọi API thứ hai mà không tăng độ chính xác đáng kể. Nếu lần đầu tiên mô hình dự đoán sai, thì lần thứ hai mô hình cũng thường dự đoán sai. Tôi đã điều chỉnh lời nhắc để chỉ xem xét một lần.

Cải thiện thay vì thêm nhiều thông tin phân tích

Tôi dựa vào một nguyên tắc về trải nghiệm người dùng: Giải pháp không phải là phân tích nhiều hơn mà là phân tích hiệu quả hơn.

Tôi đã đầu tư rất nhiều vào việc tinh chỉnh câu lệnh, thêm hướng dẫn rõ ràng để mô hình kiểm tra kỹ công việc của mình và tập trung vào các tính năng riêng biệt. Đây là một chiến lược hiệu quả hơn để cải thiện độ chính xác. Sau đây là cách hoạt động của quy trình hiện tại, đáng tin cậy hơn:

  1. Lượt phản hồi của AI (lệnh gọi API): Mô hình được nhắc tạo cả câu hỏi và phân tích nội bộ cùng một lúc, trả về một đối tượng JSON duy nhất.

    1. Câu hỏi: "Nhân vật của bạn có đeo kính không?"
    2. Phân tích (dữ liệu):
    [
      {character_id: 'brad', has_feature: true},
      {character_id: 'alex', has_feature: false},
      {character_id: 'gina', has_feature: true},
      ...
    ]
    
  2. Lượt của người chơi: Nhân vật bí mật của người chơi là Alex (không đeo kính), nên họ trả lời "Không".

  3. Kết thúc vòng: Mã JavaScript của ứng dụng sẽ tiếp quản. Bạn không cần hỏi AI bất cứ điều gì khác. Thao tác này lặp lại dữ liệu phân tích từ bước 1.

    1. Người chơi nói "Không".
    2. Đoạn mã này tìm kiếm mọi ký tự mà has_feature là đúng.
    3. Nó lật Brad và Gina xuống. Logic này mang tính xác định và tức thời.

Thử nghiệm này là bước quan trọng nhưng đòi hỏi nhiều lần thử nghiệm và sai sót. Tôi không biết liệu tình hình có cải thiện hay không. Đôi khi, tình trạng này còn tệ hơn. Xác định cách nhận được kết quả nhất quán nhất không phải là một khoa học chính xác (chưa, nếu có...).

Nhưng sau vài ván đấu với đối thủ AI mới, một vấn đề mới thú vị đã xuất hiện: thế cờ hoà.

Tránh tình trạng tắc nghẽn

Khi chỉ còn lại 2 hoặc 3 ký tự rất giống nhau, mô hình sẽ bị mắc kẹt trong một vòng lặp. Nó sẽ đặt câu hỏi về một đặc điểm mà tất cả các nhân vật đều có, chẳng hạn như "Nhân vật của bạn có đội mũ không?"

Mã của tôi sẽ xác định chính xác đây là một lượt chơi vô ích và AI sẽ thử một đặc điểm khác, cũng rộng như vậy mà các nhân vật cũng có chung, chẳng hạn như "Nhân vật của bạn có đeo kính không?"

Tôi đã cải thiện câu lệnh bằng một quy tắc mới: nếu một lần tạo câu hỏi không thành công còn lại 3 ký tự trở xuống, thì chiến lược sẽ thay đổi.

Hướng dẫn mới này rất rõ ràng: "Thay vì một đặc điểm chung chung, bạn phải hỏi về một đặc điểm trực quan cụ thể, độc đáo hoặc kết hợp hơn để tìm ra điểm khác biệt". Ví dụ: thay vì hỏi xem nhân vật có đội mũ hay không, hệ thống sẽ nhắc hỏi xem nhân vật có đội mũ bóng chày hay không.

Điều này buộc mô hình phải xem xét kỹ lưỡng hơn các hình ảnh để tìm ra một chi tiết nhỏ có thể dẫn đến bước đột phá, giúp chiến lược cuối trận của mô hình hoạt động hiệu quả hơn một chút trong hầu hết thời gian.

Khiến mô hình quên đi

Điểm mạnh nhất của mô hình ngôn ngữ là bộ nhớ. Nhưng trong trận đấu này, điểm mạnh nhất của anh lại trở thành điểm yếu. Khi tôi bắt đầu một trò chơi thứ hai, trò chơi sẽ hỏi những câu hỏi khó hiểu hoặc không liên quan. Tất nhiên, đối thủ AI thông minh của tôi đã lưu giữ toàn bộ nhật ký trò chuyện từ ván cờ trước. Nó cố gắng hiểu được hai (hoặc thậm chí nhiều) trận đấu cùng một lúc.

Thay vì sử dụng lại cùng một phiên AI, giờ đây tôi sẽ huỷ phiên đó một cách rõ ràng vào cuối mỗi trò chơi, về cơ bản là khiến AI bị mất trí nhớ.

Khi bạn nhấp vào Chơi lại, hàm startNewGameSession() sẽ đặt lại bảng và tạo một phiên AI hoàn toàn mới. Đây là một bài học thú vị về cách quản lý trạng thái phiên không chỉ trong ứng dụng mà còn trong chính mô hình AI.

Các tính năng bổ sung: Trò chơi tuỳ chỉnh và tính năng nhập bằng giọng nói

Để tăng tính hấp dẫn cho trải nghiệm này, tôi đã thêm 2 tính năng khác:

  1. Nhân vật tuỳ chỉnh: Với getUserMedia(), người chơi có thể dùng camera để tạo bộ 5 nhân vật của riêng mình. Tôi đã sử dụng IndexedDB để lưu các ký tự, đây là một cơ sở dữ liệu trình duyệt phù hợp để lưu trữ dữ liệu nhị phân như các blob hình ảnh. Khi bạn tạo một bộ tuỳ chỉnh, bộ đó sẽ được lưu vào trình duyệt của bạn và một lựa chọn phát lại sẽ xuất hiện trong trình đơn chính.

  2. Nhập bằng giọng nói: Mô hình phía máy khách là mô hình đa phương thức. Mô hình này có thể xử lý văn bản, hình ảnh và cả âm thanh. Bằng cách sử dụng MediaRecorder API để ghi lại dữ liệu đầu vào từ micrô, tôi có thể cung cấp blob âm thanh thu được cho mô hình bằng một câu lệnh: "Chuyển lời nói sau đây thành văn bản...". Đây là một cách thú vị để chơi (và cũng là một cách thú vị để xem cách ứng dụng diễn giải giọng tiếng Hà Lan của tôi). Tôi tạo ứng dụng này chủ yếu để minh hoạ tính linh hoạt của chức năng mới này trên web. Nhưng nói thật, tôi đã quá mệt mỏi khi phải nhập câu hỏi hết lần này đến lần khác.

Những chỉnh sửa cuối

Việc xây dựng "AI Guess Who?" chắc chắn là một thử thách. Nhưng với một chút trợ giúp từ việc đọc tài liệu và một số AI để gỡ lỗi AI (vâng... Tôi đã làm điều đó), hoá ra đó là một thử nghiệm thú vị. Điều này cho thấy tiềm năng to lớn của việc chạy một mô hình trong trình duyệt để tạo ra trải nghiệm riêng tư, nhanh chóng và không cần có Internet. Đây vẫn là một thử nghiệm và đôi khi đối thủ không chơi hoàn hảo. Nó không hoàn hảo về mặt pixel hoặc logic. Với AI tạo sinh, kết quả sẽ phụ thuộc vào mô hình.

Thay vì cố gắng đạt được sự hoàn hảo, tôi sẽ hướng đến việc cải thiện kết quả.

Dự án này cũng nhấn mạnh những thách thức không ngừng của kỹ thuật tạo câu lệnh. Việc nhắc nhở đó thực sự trở thành một phần quan trọng của quá trình này, và không phải lúc nào cũng là phần thú vị nhất. Nhưng bài học quan trọng nhất mà tôi học được là thiết kế ứng dụng để tách biệt nhận thức với suy luận, phân chia các khả năng của AI và mã. Ngay cả khi có sự phân tách đó, tôi nhận thấy AI vẫn có thể mắc những lỗi rõ ràng (đối với con người), chẳng hạn như nhầm lẫn hình xăm với trang điểm hoặc không theo dõi được nhân vật bí mật của ai đang được thảo luận.

Mỗi lần như vậy, giải pháp là đưa ra những câu lệnh rõ ràng hơn nữa, thêm các chỉ dẫn mà con người cảm thấy hiển nhiên nhưng lại rất cần thiết cho mô hình.

Đôi khi, trò chơi này có vẻ không công bằng. Đôi khi, tôi cảm thấy AI "biết" nhân vật bí mật trước thời hạn, mặc dù mã không bao giờ chia sẻ thông tin đó một cách rõ ràng. Điều này cho thấy một phần quan trọng trong sự khác biệt giữa con người và máy móc:

Hành vi của AI không chỉ cần chính xác mà còn cần phải cảm thấy công bằng.

Đó là lý do tôi cập nhật các câu lệnh bằng những chỉ dẫn thẳng thắn, chẳng hạn như "Bạn KHÔNG biết tôi đã chọn nhân vật nào" và "Không gian lận". Tôi nhận thấy rằng khi xây dựng các tác nhân AI, bạn nên dành thời gian xác định các giới hạn, thậm chí có thể nhiều hơn cả hướng dẫn.

Tương tác với mô hình có thể tiếp tục được cải thiện. Khi làm việc với một mô hình tích hợp, bạn sẽ mất đi một số sức mạnh và độ tin cậy của một mô hình phía máy chủ quy mô lớn, nhưng bạn sẽ có được quyền riêng tư, tốc độ và khả năng hoạt động ngoại tuyến. Đối với một trò chơi như thế này, sự đánh đổi đó thực sự đáng để thử nghiệm. Tương lai của AI phía máy khách đang ngày càng phát triển, các mô hình cũng đang ngày càng nhỏ hơn và tôi rất nóng lòng được xem những gì chúng ta có thể xây dựng tiếp theo.