Hướng dẫn thực hành GLM-4v-9b: Sử dụng pipeline transformers và tạo mẫu prompt tùy chỉnh

Đã bao giờ bạn muốn trải nghiệm một mô hình AI có thể hiểu hình ảnh và trò chuyện với bạn chưa? Hôm nay chúng ta sẽ cùng khám phá mô hình GLM-4v-9b - một mô hình đa phương thức mạnh mẽ có thể xử lý cả văn bản và hình ảnh. Với chỉ 9 tỷ tham số, mô hình này có thể chạy trên một card RTX 4090 duy nhất và hỗ trợ đầu vào hình ảnh chất lượng cao 1120×1120 pixel, đặc biệt hiệu quả trong việc nhận diện biểu đồ và chữ nhỏ.

Mô hình này thậm chí còn vượt trội hơn GPT-4 hoặc Claude 3 trong một số bài kiểm tra đánh giá khả năng hiểu hình ảnh. Điều tuyệt vời hơn nữa là nó hoàn toàn mã nguồn mở và miễn phí thương mại cho các công ty có doanh thu hàng năm dưới 2 triệu USD.

Bài viết này không hướng dẫn sử dụng giao diện web mà sẽ đưa bạn vào lập trình trực tiếp bằng mã nguồn. Chúng ta sẽ sử dụng thư viện transformers của Hugging Face - cách phổ biến nhất hiện nay, đồng thời học cách xây dựng các mẫu hỏi tùy chỉnh để mô hình trả lời theo định dạng và phong cách mong muốn.

1. Thiết lập môi trường và tải mô hình

Trước khi bắt đầu lập trình, hãy chuẩn bị đầy đủ môi trường và tài nguyên cần thiết.

1.1 Tạo môi trường Python

Việc sử dụng môi trường ảo là rất quan trọng để tránh xung đột phiên bản thư viện giữa các dự án khác nhau.

# Tạo môi trường ảo mới tên glm4v_environment sử dụng Python 3.10
conda create -n glm4v_environment python=3.10 -y

# Kích hoạt môi trường
conda activate glm4v_environment

# Cài đặt framework học sâu và các thư viện hỗ trợ
# PyTorch: Truy cập trang chủ (https://pytorch.org/get-started/locally/) để lấy lệnh cài đặt phù hợp với phiên bản CUDA của bạn
# Ví dụ cho CUDA 11.8:
# pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118

# Cài đặt transformers và thư viện tăng tốc
pip install transformers accelerate

Nếu card đồ họa của bạn có bộ nhớ hạn chế (dưới 24GB), bạn nên cài đặt thêm thư viện bitsandbytes để lượng hóa mô hình và tiết kiệm RAM.

pip install bitsandbytes

1.2 Tải về trọng số mô hình

Trọng số của GLM-4v-9b được lưu trữ trên kho mô hình của Hugging Face. Bạn có thể tải bằng lệnh git, nhưng cách đơn giản hơn là để mã tự động tải khi chạy lần đầu tiên.

Do tốc độ mạng tại Việt Nam có thể chậm khi truy cập Hugging Face, bạn cần biết địa chỉ mô hình trên Hub: THUDM/glm-4v-9b. Có thể sử dụng các nguồn mirror nội địa hoặc tìm kiếm địa chỉ lưu trữ thay thế.

2. Sử dụng Pipeline Transforms để bắt đầu nhanh chóng

Tính năng pipeline của thư viện transformers đóng gói các bước phức tạp như tải mô hình, tiền xử lý, suy luận và hậu xử lý thành một dòng mã đơn giản. Đây là lựa chọn tối ưu cho việc thử nghiệm nhanh và ứng dụng đơn giản.

2.1 Dòng mã gọi đầu tiên của bạn

Hãy bắt đầu với ví dụ đơn giản nhất: yêu cầu mô hình mô tả một bức ảnh.

from transformers import pipeline
from PIL import Image
import requests

# Khởi tạo pipeline cho nhiệm vụ hỏi đáp thị giác (Visual Question Answering)
# Chỉ định tên mô hình, device_map="auto" giúp thư viện tự chọn GPU hoặc CPU
vision_qa_pipe = pipeline("visual-question-answering", model="THUDM/glm-4v-9b", device_map="auto")

# Chuẩn bị hình ảnh, ở đây tải trực tiếp từ internet
image_url = "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/transformers/tasks/cat.jpg"
loaded_image = Image.open(requests.get(image_url, stream=True).raw)

# Đặt câu hỏi cho mô hình
query_text = "Hãy mô tả con mèo trong hình."
response = vision_qa_pipe(loaded_image, query_text)

print(f"Câu hỏi: {query_text}")
print(f"Trả lời: {response}")

Khi chạy đoạn mã này, mô hình sẽ tự động tải nếu chưa có sẵn, sau đó phân tích hình ảnh mèo và tạo ra mô tả như: "Đây là một con mèo màu cam đang nằm trên ghế sofa, trông rất thư giãn."

Giải thích mã:

  • pipeline("visual-question-answering", ...): Tạo một pipeline cho nhiệm vụ hỏi đáp thị giác. Đối với mô hình đa phương thức như GLM-4v-9b, pipeline này cũng áp dụng cho mô tả hình ảnh thuần túy.
  • device_map="auto": Tham số này tự động phát hiện thiết bị của bạn, ưu tiên sử dụng GPU (CUDA), nếu không có GPU sẽ chuyển sang CPU.
  • vision_qa_pipe(loaded_image, query_text): Gọi pipeline, truyền hình ảnh và câu hỏi. Bên trong, pipeline sẽ chuyển đổi hình ảnh thành định dạng mà mô hình hiểu được (gọi là pixel values), chuyển văn bản thành token, sau đó đưa vào mô hình tính toán, cuối cùng giải mã output token thành văn bản.

2.2 Thực hiện hội thoại nhiều vòng

GLM-4v-9b hỗ trợ hội thoại nhiều vòng, cho phép bạn tiếp tục hỏi sâu hơn dựa trên các câu trả lời trước đó. Pipeline cũng hỗ trợ truyền lịch sử hội thoại.

# Tiếp nối mã trước và biến loaded_image

# Vòng hội thoại đầu tiên
initial_query = "Trong hình là con vật gì?"
initial_response = vision_qa_pipe(loaded_image, initial_query)
print(f"Vòng 1 - Câu hỏi: {initial_query}")
print(f"Vòng 1 - Trả lời: {initial_response}")

# Vòng hội thoại thứ hai: dựa trên câu trả lời trước để hỏi tiếp
# Xây dựng lịch sử hội thoại. Định dạng là danh sách, mỗi phần tử là từ điển chứa "role" và "content".
# "user" đại diện người dùng, "assistant" đại diện mô hình.
dialogue_history = [
    {"role": "user", "content": initial_query},
    {"role": "assistant", "content": initial_response},
]

# Đặt câu hỏi tiếp theo
subsequent_query = "Nó có màu gì?"
# Truyền lịch sử cùng với hình ảnh và câu hỏi mới.
# Lưu ý: Một số pipeline mô hình đa phương thức có thể cần tên tham số cụ thể để truyền lịch sử, GLM-4v-9b có thể cần xử lý qua `tokenizer.apply_chat_template`.
# Cách tổng quát hơn là phần tiếp theo: "Tạo mẫu prompt tùy chỉnh" và sử dụng trực tiếp đối tượng mô hình.
# Với pipeline đơn giản, chúng ta có thể ghép lịch sử và câu hỏi mới thành chuỗi câu hỏi mới (cách xử lý đơn giản).
updated_query_for_pipeline = f"Dựa trên cuộc hội thoại trước, chúng ta đang thảo luận về một bức hình. Trước đó hỏi: {initial_query} Bạn trả lời: {initial_response}. Bây giờ hãy tiếp tục xem cùng bức hình và trả lời: {subsequent_query}"
second_response = vision_qa_pipe(loaded_image, updated_query_for_pipeline)
print(f"\nVòng 2 - Câu hỏi: {subsequent_query}")
print(f"Vòng 2 - Trả lời: {second_response}")

Qua ví dụ này, bạn thấy cách mô phỏng hội thoại nhiều vòng. Nhưng để kiểm soát chính xác hơn định dạng hội thoại và lịch sử, chúng ta cần thoát khỏi pipeline và sử dụng trực tiếp mô hình cùng tokenizer.

3. Sử dụng trực tiếp mô hình và tokenizer

Sử dụng AutoModelForCausalLMAutoProcessor mang lại sự linh hoạt tối đa, đặc biệt khi tạo mẫu prompt tùy chỉnh.

3.1 Tải mô hình và bộ xử lý

from transformers import AutoModelForCausalLM, AutoProcessor
import torch

model_identifier = "THUDM/glm-4v-9b"

# Tải mô hình
# torch_dtype=torch.float16 sử dụng số dấu phẩy động nửa độ chính xác, tiết kiệm RAM, chất lượng suy luận giảm nhẹ.
# device_map="auto" tự động phân bổ các lớp mô hình lên thiết bị khả dụng (hữu ích với nhiều card).
model_instance = AutoModelForCausalLM.from_pretrained(
    model_identifier,
    torch_dtype=torch.float16,
    device_map="auto",
    trust_remote_code=True  # Mô hình GLM thường cần tham số này
)

# Tải bộ xử lý (Processor)
# Với mô hình đa phương thức, Processor thường bao gồm bộ xử lý hình ảnh (biến đổi hình ảnh) và tokenizer văn bản.
processor_instance = AutoProcessor.from_pretrained(model_identifier, trust_remote_code=True)

Điểm quan trọng:

  • trust_remote_code=True: Vì GLM-4v-9b có thể sử dụng mã mô hình tùy chỉnh, tham số này cho phép tải và chạy mã từ Hub, là bắt buộc.
  • AutoProcessor: Là bộ xử lý "tất cả trong một", có thể xử lý cả đầu vào hình ảnh và văn bản của bạn.

3.2 Chuẩn bị đầu vào và tạo phản hồi

Bây giờ, chúng ta không dùng pipeline mà tự thực hiện toàn bộ quy trình.

# 1. Chuẩn bị đầu vào
loaded_image = Image.open(requests.get(image_url, stream=True).raw)  # Sử dụng hình mèo trước đó
input_text = "Hãy mô tả bức hình này."

# 2. Sử dụng processor để xử lý đầu vào
# Processor sẽ resize, chuẩn hóa hình ảnh, chuyển thành định dạng tensor mà mô hình cần.
# Đồng thời, nó chuyển văn bản thành ID token.
processed_inputs = processor_instance(images=loaded_image, text=input_text, return_tensors="pt").to(model_instance.device)

# 3. Tạo phản hồi
# Truyền tensor đầu vào đã xử lý cho mô hình.
with torch.no_grad():  # Giai đoạn suy luận, không cần tính gradient, tiết kiệm bộ nhớ
    model_outputs = model_instance.generate(**processed_inputs, max_new_tokens=512)  # max_new_tokens kiểm soát độ dài văn bản sinh tối đa

# 4. Giải mã output
# Bỏ qua phần đầu vào (processed_inputs['input_ids']), chỉ giải mã phần mô hình sinh mới.
newly_generated_ids = model_outputs[:, processed_inputs['input_ids'].shape[1]:]  # Lấy ID token mới sinh
response_text = processor_instance.batch_decode(newly_generated_ids, skip_special_tokens=True)[0]

print(f"Văn bản đầu vào: {input_text}")
print(f"Phản hồi mô hình: {response_text}")

Quá trình này minh họa rõ ràng những gì diễn ra phía sau: tiền xử lý -> lan truyền thuận mô hình -> giải mã. Nhưng vẫn sử dụng định dạng hội thoại mặc định của mô hình. Bây giờ, chúng ta đến phần cốt lõi - tạo prompt tùy chỉnh.

4. Xây dựng mẫu Prompt tùy chỉnh

Cách hỏi mặc định của mô hình có thể không phù hợp với nhu cầu của bạn. Ví dụ, bạn muốn mô hình xuất ra định dạng JSON, hoặc xây dựng hệ thống hội thoại với danh tính cụ thể (như "chuyên gia phân tích"). Đây là lúc cần mẫu prompt tùy chỉnh.

4.1 Hiểu định dạng hội thoại của GLM-4v-9b

Đầu tiên, chúng ta cần biết định dạng hội thoại mà mô hình sử dụng trong quá trình huấn luyện. Theo tài liệu chính thức của GLM-4v-9b, nó thường tuân theo cấu trúc tương tự như sau:

<|user|>
[Hình ảnh]
Đây là hình ảnh.
Câu hỏi: {câu hỏi người dùng}
<|assistant|>
{phản hồi mô hình}

Hình ảnh sẽ được đánh dấu bằng token hình ảnh đặc biệt (như <image>) và nhúng vào chuỗi văn bản. Bộ xử lý (processor) sẽ tự động xử lý quá trình nhúng này. Nhiệm vụ của chúng ta là xây dựng cấu trúc phần văn bản.

4.2 Triển khai hàm mẫu tùy chỉnh

Giả sử chúng ta muốn mô hình đóng vai "kiểm duyệt nội dung hình ảnh", xuất kết quả có cấu trúc.

def create_custom_template(image_input, user_question, system_identity="Bạn là chuyên gia kiểm duyệt nội dung hình ảnh. Hãy phân tích kỹ hình ảnh và trả lời theo định dạng JSON sau."):
    # Xây dựng lịch sử hội thoại. Bắt đầu với chỉ thị hệ thống.
    conversation_messages = [
        {
            "role": "system",
            "content": system_identity
        },
        {
            "role": "user",
            # Tin nhắn người dùng chứa hình ảnh và câu hỏi văn bản.
            # Lưu ý: Việc nhúng hình ảnh thực tế do processor thực hiện ở bước sau.
            # Ở đây dùng placeholder `<image>` để chỉ vị trí hình ảnh (một số processor cần token rõ ràng này).
            # Với GLM-4v-9b, cách phổ biến hơn là truyền trực tiếp đối tượng hình ảnh và văn bản cho processor, nó xử lý bên trong.
            # Vậy nên, chúng ta xây dựng danh sách hội thoại thuần văn bản, hình ảnh truyền riêng khi gọi processor.
            "content": user_question
        }
    ]

    # Sử dụng phương thức `apply_chat_template` của processor để chuyển danh sách hội thoại thành chuỗi token mô hình chấp nhận.
    # Đây là cách chuẩn của thư viện transformers cho mô hình hội thoại.
    # Lưu ý: Cần xác nhận processor của GLM-4v-9b hỗ trợ phương thức này. Nếu không, phải ghép thủ công chuỗi.
    try:
        # Phương pháp 1: Dùng apply_chat_template (nếu hỗ trợ)
        text_template = processor_instance.apply_chat_template(conversation_messages, tokenize=False, add_generation_prompt=True)
        # add_generation_prompt=True thêm gợi ý để mô hình bắt đầu phản hồi (như <|assistant|>)
    except AttributeError:
        # Phương pháp 2: Xây dựng thủ công (nếu mô hình không định nghĩa chat_template)
        # Cần ghép thủ công theo định dạng cụ thể của mô hình.
        # Giả sử định dạng: `[chỉ thị hệ thống]\n\n<|user|>\n[hình ảnh]\n{câu hỏi người dùng}<|assistant|>\n`
        # Lưu ý: Token hình ảnh thường do processor thêm khi xử lý, nên phần văn bản không viết placeholder hình ảnh.
        system_section = f"{system_identity}\n\n"
        user_section = f"<|user|>\n{user_question}\n"
        assistant_section = "<|assistant|>\n"
        text_template = system_section + user_section + assistant_section

    return text_template

# Sử dụng mẫu tùy chỉnh của chúng ta
system_instruction = "Bạn là chuyên gia kiểm duyệt nội dung hình ảnh. Hãy phân tích hình ảnh để kiểm tra sự tồn tại của các nội dung sau: 1. Bạo lực; 2. Nội dung không phù hợp; 3. Vật phẩm vi phạm. Nếu có, hãy chỉ ra và mô tả; nếu không có, vui lòng nói 'Nội dung hình ảnh an toàn'. Trả lời bằng định dạng JSON, bao gồm trường 'safe' (kiểu boolean) và 'details' (chuỗi ký tự)."
question_to_analyze = "Vui lòng kiểm duyệt hình ảnh này."
constructed_prompt = create_custom_template(None, question_to_analyze, system_instruction)  # Đối tượng hình ảnh truyền ở bước sau

print("Prompt văn bản đã xây dựng:")
print(constructed_prompt)

4.3 Suy luận với mẫu tùy chỉnh

Bây giờ, chúng ta sẽ đưa prompt văn bản đã xây dựng và hình ảnh cùng nhau vào processor và mô hình.

# 1. Sử dụng processor, truyền đồng thời hình ảnh và prompt văn bản tùy chỉnh
# processor sẽ xử lý chuyển đổi hình ảnh thành đặc trưng thị giác, sau đó hợp nhất với token văn bản.
final_inputs = processor_instance(images=loaded_image, text=constructed_prompt, return_tensors="pt").to(model_instance.device)

# 2. Tạo phản hồi
with torch.no_grad():
    # Có thể điều chỉnh tham số sinh để đạt hiệu quả tốt hơn
    final_outputs = model_instance.generate(
        **final_inputs,
        max_new_tokens=256,  # Độ dài sinh tối đa
        do_sample=True,      # Có sử dụng lấy mẫu hay không (True làm output đa dạng hơn, False chắc chắn hơn)
        temperature=0.7,     # Nhiệt độ lấy mẫu, thấp hơn thì output chắc chắn hơn, cao hơn thì ngẫu nhiên hơn
        top_p=0.9,           # Tham số lấy mẫu nucleus, giữ lại từ vựng có xác suất tích lũy đến top_p
    )

# 3. Giải mã output
# Tương tự, cần giải mã phần mô hình sinh mới.
# Vì đầu vào chứa prompt hoàn chỉnh, cần tìm vị trí bắt đầu sinh.
input_token_length = final_inputs['input_ids'].shape[1]
newly_generated_ids = final_outputs[:, input_token_length:]
final_response = processor_instance.batch_decode(newly_generated_ids, skip_special_tokens=True)[0]

print("\n--- Kết quả gọi với mẫu tùy chỉnh ---")
print(f"Chỉ thị hệ thống: {system_instruction}")
print(f"Câu hỏi người dùng: {question_to_analyze}")
print(f"Phản hồi mô hình:\n{final_response}")

Chạy đoạn mã này, bạn có thể nhận được output như sau:

{
  "safe": true,
  "details": "Nội dung hình ảnh an toàn. Hình ảnh hiển thị một con mèo, không phát hiện bạo lực, nội dung không phù hợp hoặc vật phẩm vi phạm."
}

Thông qua cách này, bạn hoàn toàn kiểm soát "ngữ cảnh" và "định dạng" tương tác với mô hình, điều này rất hữu ích trong dự án thực tế, ví dụ như xây dựng dịch vụ API cần output có cấu trúc.

5. Thủ thuật thực hành và vấn đề thường gặp

Sau khi nắm được phương pháp gọi cơ bản, đây là một số thủ thuật giúp bạn sử dụng hiệu quả hơn.

5.1 Điều chỉnh tham số sinh

Hàm model.generate() có nhiều tham số có thể điều chỉnh hiệu quả output:

  • max_new_tokens: Kiểm soát độ dài sinh. Điều chỉnh theo độ phức tạp câu hỏi và mức độ chi tiết bạn cần.
  • do_sample: Khi False dùng giải mã tham lam (luôn chọn từ có xác suất cao nhất), kết quả ổn định nhưng có thể nhàm chán; True dùng lấy mẫu, kết quả sáng tạo hơn.
  • temperature: Dùng khi lấy mẫu. Giá trị cao hơn (như 1.0), output ngẫu nhiên và sáng tạo hơn; giá trị thấp hơn (như 0.1), output chắc chắn và bảo thủ hơn.
  • top_p (lấy mẫu nucleus): Phối hợp với temperature. Chỉ lấy mẫu từ tập từ nhỏ nhất có xác suất tích lũy vượt top_p, tránh chọn từ kỳ lạ có xác suất quá thấp.
  • repetition_penalty: Giá trị lớn hơn 1.0 phạt các từ lặp lại, tránh mô hình nói đi nói lại.

5.2 Xử lý hình ảnh độ phân giải cao

GLM-4v-9b hỗ trợ đầu vào độ phân giải cao 1120×1120, đây là điểm mạnh của nó. Trong mã, bạn không cần xử lý đặc biệt, processor sẽ tự động điều chỉnh hình ảnh về kích thước mô hình chấp nhận. Nhưng lưu ý, tỷ lệ khung hình có thể bị thay đổi. Nếu cần cắt hoặc giữ tỷ lệ hình ảnh, có thể xử lý trước bằng thư viện PIL trước khi truyền vào processor.

5.3 Thủ thuật tiết kiệm RAM

Nếu RAM card đồ họa căng thẳng, có thể thử các phương pháp sau:

  • Lượng hóa: Sử dụng thư viện bitsandbytes để lượng hóa 4-bit hoặc 8-bit. Khi tải mô hình, dùng tham số load_in_4bit=True hoặc load_in_8bit=True.
        model_instance = AutoModelForCausalLM.from_pretrained(
            model_identifier,
            load_in_4bit=True,  # Dùng lượng hóa 4-bit
            device_map="auto",
            trust_remote_code=True
        )
        
    
  • Dỡ xuống CPU: Với mô hình rất lớn, có thể dỡ một số lớp xuống RAM CPU. Tham số device_map có thể đặt thành từ điển chi tiết hơn để kiểm soát.
  • Gradient checkpointing: Nếu fine-tuning, có thể bật gradient_checkpointing=True để đổi thời gian lấy không gian.

5.4 Những lỗi có thể gặp và cách giải quyết

  • CUDA out of memory: Hết RAM. Thử giảm max_new_tokens, dùng lượng hóa (load_in_4bit), hoặc dùng hình ảnh đầu vào nhỏ hơn.
  • KeyError: 'pixel_values': Định dạng đầu vào có thể sai. Đảm bảo dùng processor để xử lý cả hình ảnh và văn bản cùng lúc, không dùng riêng tokenizer.
  • Kết quả sinh không như mong đợi: Kiểm tra mẫu prompt của bạn có khớp định dạng mà mô hình được huấn luyện không. Đọc kỹ phần "How to use" trong thẻ mô hình chính thức. Thử điều chỉnh các tham số temperaturetop_p.

6. Tổng kết

Hãy ôn lại những gì đã học hôm nay. GLM-4v-9b là một mô hình đa phương thức mã nguồn mở mạnh mẽ và hiệu quả, có thể chạy bằng một card tiêu dùng. Qua thư viện Hugging Face Transformers, chúng ta có thể gọi nó bằng hai cách chính:

  1. Sử dụng pipeline: Đơn giản và nhanh chóng nhất, phù hợp cho kiểm tra nhanh và ứng dụng đơn giản. Một dòng mã hoàn thành mô tả hình ảnh hoặc hỏi đáp.
  2. Sử dụng trực tiếp mô hình và processor: Cách này cung cấp sự linh hoạt tối đa. Chúng ta đã đi qua toàn bộ quy trình tải mô hình, xử lý đầu vào, thực thi suy luận, giải mã output.
  3. Tạo mẫu Prompt tùy chỉnh: Đây là chìa khóa để áp dụng mô hình vào dự án thực tế. Chúng ta đã học cách xây dựng chỉ thị hệ thống và gợi ý người dùng cụ thể, thậm chí khiến mô hình xuất định dạng JSON có cấu trúc, để dễ dàng tích hợp vào hệ thống riêng.

Nắm vững các phương pháp này, bạn sẽ không còn giới hạn ở giao diện demo sẵn có. Có thể tích hợp GLM-4v-9b vào đường ống phân tích dữ liệu, hệ thống kiểm duyệt nội dung, robot hỗ trợ khách hàng thông minh hoặc bất kỳ ứng dụng nào cần hiểu nội dung hình ảnh.

Bước tiếp theo, bạn có thể thử:

  • Dùng hình ảnh và câu hỏi riêng để kiểm tra khả năng của mô hình trong các tình huống khác nhau.
  • Thiết kế logic hội thoại nhiều vòng phức tạp hơn, ví dụ khiến mô hình kể chuyện dựa trên nhiều hình ảnh.
  • Khám phá giới hạn mô hình, thử xem nó xử lý hình ảnh phi chuẩn hoặc câu hỏi suy luận phức tạp thế nào.

Hy vọng hướng dẫn thực hành này giúp bạn khởi đầu thuận lợi. Cứ thực hành nhiều, khi gặp vấn đề hãy tra cứu tài liệu và cộng đồng, bạn sẽ thấy việc sử dụng mô hình này thật ra không khó.

Thẻ: GLM-4v-9b Transformers HuggingFace computer-vision Multimodal-Model

Đăng vào ngày 14 tháng 7 lúc 21:08