Xây dựng các Agent không bao giờ quên

@akshay_pachaar
TIẾNG ANH3 tháng trước · 13 thg 4, 2026
625K
1.9K
305
46
4.1K

TL;DR

Một bài phân tích chuyên sâu về các hệ thống bộ nhớ cho AI agent, từ các danh sách đơn giản đến công cụ đồ thị vector mã nguồn mở của Cognee, giúp tối ưu hóa khả năng suy luận có tính liên kết và bền vững.

Một bài phân tích từ nguyên lý cơ bản về bộ nhớ agent: từ danh sách Python đến file markdown, tìm kiếm vector, kết hợp đồ thị-vector, và cuối cùng là một giải pháp mã nguồn mở, sạch sẽ cho tất cả những điều này.

Akshay 🚀 - inline image

LLM được thiết kế để không có trạng thái (stateless). Mỗi lần gọi API đều bắt đầu từ đầu. "Bộ nhớ" mà bạn cảm thấy khi trò chuyện với ChatGPT thực chất là một ảo ảnh được tạo ra bằng cách gửi lại toàn bộ lịch sử hội thoại trong mỗi yêu cầu.

Thủ thuật đó hiệu quả cho các cuộc trò chuyện thông thường. Nhưng nó sẽ sụp đổ ngay khi bạn cố gắng xây dựng một agent thực sự.

Dưới đây là 7 kiểu lỗi xuất hiện ngay khi bạn bỏ qua bộ nhớ:

  1. Quên ngữ cảnh: agent hỏi lại thông tin bạn đã cung cấp
  2. Không có cá nhân hóa: mọi tương tác đều chung chung
  3. Thất bại tác vụ đa bước: trạng thái trung gian âm thầm bị mất giữa chừng
  4. Lặp lại sai lầm: không có hồi ức tình huống dẫn đến mãi mãi mắc cùng một lỗi
  5. Không tích lũy kiến thức: mỗi phiên làm việc đều bắt đầu từ con số 0
  6. Ảo giác từ khoảng trống: khi ngữ cảnh tràn, mô hình sẽ tự bịa ra
  7. Sụp đổ nhận dạng: không có tính liên tục, không có sự tin tưởng

Phản ứng hiển nhiên là "ném thêm ngữ cảnh vào nó". Đó là lý do tại sao các cửa sổ token 128K và 200K dường như có thể giải quyết mọi thứ.

Nhưng không phải vậy.

Độ chính xác giảm hơn 30% khi thông tin liên quan nằm ở giữa một ngữ cảnh dài. Đây là hiệu ứng "bị lạc ở giữa" (lost in the middle) đã được ghi nhận rõ ràng.

Ngữ cảnh là một ngân sách dùng chung: prompt hệ thống, tài liệu được truy xuất, lịch sử hội thoại và đầu ra đều tranh giành cùng một số token.

Ngay cả với 100K token, việc thiếu tính bền vững, ưu tiên và nổi bật khiến cho độ dài ngữ cảnh thô trở nên không đủ.

Akshay 🚀 - inline image

Bộ nhớ không phải là nhồi nhét thêm văn bản vào prompt. Đó là về việc cấu trúc những gì agent ghi nhớ để nó có thể tìm thấy những gì quan trọng.

Khung khoa học nhận thức thực sự hữu ích

Công thức năm 2023 của Lilian Weng đã trở thành khuôn khổ mặc định:

Agent = LLM + Bộ nhớ + Lập kế hoạch + Sử dụng công cụ.

Bốn trụ cột ngang hàng nhau.

Phân loại của cô ấy vay mượn từ khoa học nhận thức, nơi bộ nhớ con người được chia thành ba hệ thống:

  • Bộ nhớ giác quan thu thập đầu vào nhận thức thô và giữ nó trong một phần nhỏ của giây. Chỉ những phần bạn chú ý mới được chuyển tiếp.
  • Bộ nhớ làm việc là nơi diễn ra quá trình suy nghĩ tích cực. Nó chứa khoảng 7±2 mục cùng một lúc (Phát hiện năm 1956 của Miller). Mất tập trung, và nội dung sẽ biến mất.
  • Bộ nhớ dài hạn là kho lưu trữ bền vững với không có giới hạn dung lượng thực tế. Truy xuất là nút thắt cổ chai: bạn có thể lưu trữ hàng triệu thứ và vẫn không thể nhớ lại thứ mình cần.

Mỗi cái đều ánh xạ trực tiếp đến một thành phần trong kiến trúc agent hiện đại:

Akshay 🚀 - inline image

Bộ nhớ dài hạn tự nó được chia nhỏ hơn nữa:

  • Tình huống: các sự kiện cụ thể trong quá khứ ("Vào thứ Ba, cụm PostgreSQL đã bị sập")
  • Ngữ nghĩa: sự kiện và khái niệm ("PostgreSQL là một cơ sở dữ liệu quan hệ")
  • Thủ tục: kỹ năng và quy trình làm việc ("khi người dùng yêu cầu hoàn tiền, trước tiên hãy kiểm tra ngày mua hàng")

Cầu nối giữa tình huống và ngữ nghĩa là sự củng cố bộ nhớ: các sự kiện cụ thể lặp đi lặp lại chưng cất thành kiến thức tổng quát. Một agent nhận thấy "người dùng luôn thích bản tóm tắt điều hành" qua hàng chục tương tác nên biến điều đó thành một quy tắc có thể tái sử dụng. Nếu không có sự củng cố, agent của bạn sẽ phát lại các sự kiện riêng lẻ thay vì học hỏi từ chúng.

Akshay 🚀 - inline image

Agent tối thiểu, và thứ gì hỏng trước tiên

Bỏ qua các khuôn khổ, một agent là một vòng lặp: nhận thức, suy nghĩ, hành động.

python
1class Agent:
2 """Minimal AI agent: perceive, think, act"""
3 def __init__(self):
4 self.client = anthropic.Anthropic()
5 self.model = "claude-sonnet-4-20250514"
6
7 def run(self, user_input: str) -> str:
8 response = self.client.messages.create(
9 model=self.model,
10 max_tokens=1024,
11 messages=[{"role": "user", "content": user_input}],
12 )
13 return response.content[0].text

Bảo nó "Tôi có 4 quả táo", sau đó hỏi "Tôi đã ăn một quả, còn lại bao nhiêu?" và nó không biết bạn đang nói đến quả táo nào. Mỗi lần gọi tồn tại riêng lẻ.

Lớp 1: Danh sách Python

Sửa lỗi đầu tiên mà ai cũng nghĩ đến:

python
1class Agent:
2 def __init__(self):
3 self.client = anthropic.Anthropic()
4 self.messages = [] # The entire "memory" is a list
5
6 def chat(self, user_input: str) -> str:
7 self.messages.append({"role": "user", "content": user_input})
8 response = self.client.messages.create(
9 model="claude-sonnet-4-20250514",
10 max_tokens=1024,
11 messages=self.messages, # Full history sent every time
12 )
13 reply = response.content[0].text
14 self.messages.append({"role": "assistant", "content": reply})
15 return reply

Nhiều lượt tương tác giờ đã hoạt động. Câu hỏi về quả táo được trả lời chính xác vì toàn bộ cuộc trò chuyện được gửi lại mỗi lần gọi.

Tuy nhiên, hai vấn đề nhanh chóng xuất hiện:

  • Danh sách phát triển không giới hạn. Khoảng lượt thứ 200, bạn chạm trần ngữ cảnh và các tin nhắn cũ nhất âm thầm bị rơi. Tên người dùng từ lượt đầu tiên biến mất trước cả một câu chuyện cười bỏ đi của ngày hôm qua. Không có sự ưu tiên, chỉ có thứ tự thời gian nghiêm ngặt.
  • Mọi thứ đều nằm trong RAM. Khi tiến trình Python kết thúc, agent của bạn không còn biết bạn là ai.

Lớp 2: Tệp Markdown để duy trì

Bước tiếp theo là ghi bộ nhớ vào đĩa. Markdown là một lựa chọn tự nhiên: có thể đọc được bằng con người, thân thiện với Git, và agent có thể đọc lại nó dưới dạng văn bản thuần túy. Claude Code sử dụng chính xác mẫu này với các tệp CLAUDE.mdMEMORY.md.

python
1class MarkdownMemoryAgent:
2 def __init__(self):
3 self.client = anthropic.Anthropic()
4 self.history_file = Path("memory/conversation_history.md")
5 self.facts_file = Path("memory/known_facts.md")
6
7 def save_to_disk(self, role: str, content: str) -> None:
8 with open(self.history_file, "a") as f:
9 f.write(f"### {role} at {datetime.now().isoformat()}\n{content}\n\n")
10
11 def load_history(self) -> str:
12 if self.history_file.exists():
13 return self.history_file.read_text()
14 return ""
15
16 def chat(self, user_input: str) -> str:
17 self.save_to_disk("user", user_input)
18 history = self.load_history()
19 response = self.client.messages.create(
20 model="claude-sonnet-4-20250514",
21 max_tokens=1024,
22 system=f"Previous conversation:\n{history}",
23 messages=[{"role": "user", "content": user_input}],
24 )
25 reply = response.content[0].text
26 self.save_to_disk("assistant", reply)
27 return reply

Vấn đề duy trì đã được giải quyết. Khởi động lại script và cuộc trò chuyện vẫn còn trên đĩa. Bạn cũng có thể duy trì một tệp sự kiện riêng biệt mà agent trích xuất theo thời gian:

text
1- User's name is Sarah
2- Sarah manages the backend team at Acme Corp
3- Acme Corp is a B2B SaaS company
4- Currently migrating production database to a new AWS region

Bạn có thể mở tệp trong bất kỳ trình soạn thảo nào, thấy chính xác những gì agent biết và sửa nó bằng tay. Thực sự hữu ích cho việc tạo mẫu thử.

Với 4 sự kiện, điều này hoạt động hoàn hảo. Tải toàn bộ tệp vào ngữ cảnh và LLM xử lý mọi câu hỏi về Sarah, công ty của cô ấy hoặc ngành của cô ấy.

Bây giờ hãy tua nhanh ba tháng. Agent của bạn có 2.000 sự kiện được trích xuất và 200 nhật ký hội thoại. Đó là hơn 500K token markdown trên đĩa và cửa sổ ngữ cảnh của bạn là 128K.

Bạn không còn có thể tải mọi thứ. Bạn cần truy xuất có chọn lọc chỉ những sự kiện liên quan đến truy vấn hiện tại. Với các tệp phẳng, lựa chọn duy nhất của bạn là tìm kiếm từ khóa:

python
1# User asks: "What's the status of our cloud migration?"
2grep("cloud migration", facts_file)
3# Returns: []
4# The fact on disk says "migrating production database to a new AWS region."
5# The words "cloud migration" appear nowhere.
6
7# User asks: "Which team is handling the database work?"
8grep("database team", facts_file)
9# Returns: []
10# One fact says Sarah "manages the backend team." Another says the team
11# is "migrating production database." But no single line contains
12# both "database" and "team" together.

Ở quy mô nhỏ, tệp markdown hoạt động. Ở quy mô thực, chúng buộc phải truy xuất từ khóa, và từ khóa không thể xử lý các từ đồng nghĩa, diễn giải hoặc kết nối giữa các sự kiện.

Thông tin có trên đĩa. Nhưng bạn không thể tải tất cả và tìm kiếm từ khóa quá mong manh để tìm đúng phần.

Nếu bạn đã sử dụng OpenClaw, bạn sẽ thấy điều này diễn ra. Nó lưu trữ bộ nhớ dưới dạng các tệp checkpoint markdown và sau nhiều tuần sử dụng hàng ngày, các sự kiện trước đó âm thầm biến mất khi ngữ cảnh tích lũy và bị nén lại. Bộ nhớ thì có. Nhưng việc truy xuất thì không.

Lưu trữ mà không có truy xuất thông minh là một thư viện không có danh mục.

Lớp 3: Tìm kiếm vector và bức tường nó va phải

Thêm embeddings vào. Chia nhỏ markdown của bạn, nhúng các đoạn, tìm kiếm bằng độ tương đồng cosine. Bây giờ "cơ sở dữ liệu" khớp với "PostgreSQL" vì các vector của chúng nằm gần nhau trong không gian embedding. Vấn đề từ đồng nghĩa đã được giải quyết.

Sau đó, bạn va phải một bức tường mới. Hãy xem xét ba sự kiện này trong cơ sở dữ liệu vector của bạn:

text
1- "Alice is the tech lead on Project Atlas"
2- "Project Atlas uses PostgreSQL for its primary datastore"
3- "The PostgreSQL cluster experienced an outage on Tuesday"

Người dùng hỏi: "Dự án của Alice có bị ảnh hưởng bởi sự cố hôm thứ Ba không?"

Truy vấn đề cập đến Alice và sự cố hôm thứ Ba, vì vậy tìm kiếm vector xếp hạng cao sự kiện thứ nhất và thứ ba. Nhưng cầu nối quan trọng, "Project Atlas sử dụng PostgreSQL," không đề cập đến Alice hay thứ Ba. Nó là mảnh ghép kết nối và là mảnh sẽ không xuất hiện.

Mỗi sự kiện là một điểm biệt lập trong không gian embedding. Các mô liên kết chúng vô hình đối với vector.

Akshay 🚀 - inline image

Đây không phải là trường hợp ngoại lệ. Đó là hình dạng bình thường của các câu hỏi trong thế giới thực. Kiến thức kinh doanh vốn có tính quan hệ: mọi người thuộc về nhóm, nhóm sở hữu dự án, dự án phụ thuộc vào hệ thống, hệ thống có sự cố. Bất kỳ câu hỏi nào vượt qua hai bước nhảy trở lên đều vượt quá khả năng truy xuất vector phẳng.

Ma trận năng lực

Mỗi lớp sửa chữa nỗi đau trước đó nhưng lại bộc lộ một nỗi đau sâu sắc hơn:

Akshay 🚀 - inline image

Bạn cần tính bền vững, hiểu ngữ nghĩa và lý luận quan hệ trong một lớp bộ nhớ duy nhất.

Tự xây dựng điều này có nghĩa là ghép nối một cơ sở dữ liệu vector, một cơ sở dữ liệu đồ thị, một kho lưu trữ quan hệ, một trình trích xuất thực thể, một đường ống khử trùng lặp và một hệ thống tính trọng số cạnh. Đó là hàng tuần làm việc về cơ sở hạ tầng trước khi bạn viết một dòng logic agent nào.

Tôi đã sử dụng một giải pháp lấp đầy khoảng trống này một cách sạch sẽ. Nó hoàn toàn là mã nguồn mở, xử lý cả ba mô hình lưu trữ dưới một mái nhà và bạn có thể chạy nó trong vài phút. Hãy nói về Cognee.

Cognee: ba kho lưu trữ, một công cụ, bốn lời gọi

Cognee là một công cụ tri thức mã nguồn mở được xây dựng cho bộ nhớ agent. Nó kết hợp tìm kiếm vector với đồ thị tri thức và một lớp nguồn gốc quan hệ thành một hệ thống duy nhất.

Toàn bộ bề mặt API là bốn lời gọi bất đồng bộ:

python
1import cognee
2
3await cognee.add("Your document here") # Ingest anything
4await cognee.cognify() # Build knowledge graph + embeddings
5await cognee.memify() # Self-improve the memory
6await cognee.search("Your query") # Retrieve with reasoning

Đằng sau bốn lời gọi đó là một kiến trúc ba kho lưu trữ.

Akshay 🚀 - inline image

Tại sao lại là ba kho lưu trữ mà không phải một?

Mỗi kho lưu trữ nắm bắt một khía cạnh của tri thức mà những kho khác không thể:

  • Kho lưu trữ quan hệ → nguồn gốc: dữ liệu đến từ đâu, khi nào được đưa vào, ai có quyền truy cập
  • Kho lưu trữ vector → ngữ nghĩa: nội dung có nghĩa là gì, nó tương tự với cái gì
  • Kho lưu trữ đồ thị → mối quan hệ: các thực thể kết nối như thế nào, cái gì gây ra cái gì, ai báo cáo cho ai

Làm phẳng bất kỳ cái nào trong số này và bạn sẽ mất thông tin quan trọng cho độ chính xác của việc truy xuất.

Ngăn xếp mặc định là SQLite + LanceDB + Kuzu, hoàn toàn được nhúng và dựa trên tệp. pip install cognee cộng với một khóa API LLM và bạn đã có thể chạy.

Không cần Docker, không cần dịch vụ bên ngoài.

Đối với môi trường sản xuất, hãy đổi SQLite lấy Postgres, LanceDB lấy Qdrant/Pinecone/pgvector và Kuzu lấy Neo4j/FalkorDB/Neptune.

Cùng một API bốn lời gọi trong cả hai trường hợp.

Cognify thực sự làm gì?

cognee.cognify() chạy một đường ống đa giai đoạn chuyển đổi văn bản thô thành tri thức có cấu trúc, kết nối với nhau:

  1. Phân loại tài liệu theo loại và lĩnh vực
  2. Kiểm tra quyền để kiểm soát truy cập đa người thuê
  3. Trích xuất đoạn tôn trọng cấu trúc đoạn văn (không phải cắt kích thước cố định)
  4. Trích xuất thực thể và mối quan hệ thông qua LLM, với tính năng khử trùng lặp tự động thông qua băm nội dung
  5. Tạo tóm tắt để truy xuất hiệu quả
  6. Lập chỉ mục kép vào kho lưu trữ vector (embeddings) và kho lưu trữ đồ thị (cạnh)

Bước khử trùng lặp quan trọng hơn vẻ bề ngoài. Nếu cùng một thực thể xuất hiện trong 50 tài liệu, Cognee hợp nhất nó thành một nút đồ thị duy nhất với 50 cạnh đến. Agent của bạn không còn thấy "Alice" như 50 người lạ khác nhau nữa. Và đường ống này theo mặc định là tăng dần: chỉ các tệp mới hoặc đã sửa đổi mới được xử lý lại.

Akshay 🚀 - inline image

Mọi nút đồ thị đều có một embedding tương ứng. Biểu diễn kép này là thủ thuật cốt lõi: đi vào qua vector (tìm nội dung tương tự về mặt ngữ nghĩa) và thoát ra qua đồ thị (theo dõi các mối quan hệ đến các thực thể được kết nối), hoặc ngược lại. Đó là điều làm cho các truy vấn đa bước nhảy hoạt động mà không hy sinh tìm kiếm ngữ nghĩa.

Memify: bộ nhớ biết học hỏi

memify() là điều phân biệt Cognee với mọi công cụ "đưa vào và tìm kiếm". Nó chạy một bước tối ưu hóa lấy cảm hứng từ RL trên đồ thị:

  • Tăng cường các đường dẫn hữu ích dẫn đến truy xuất tốt
  • Tỉa bỏ các nút cũ không còn được chạm đến
  • Tự động điều chỉnh trọng số cạnh dựa trên việc sử dụng thực tế
  • Thêm các sự kiện suy diễn bằng cách xác định các mối quan hệ tiềm ẩn

Đồ thị của một agent hỗ trợ khách hàng sẽ tự nhiên tăng cường các đường dẫn qua tài liệu sản phẩm và chính sách hoàn tiền trong khi để các cạnh nhân sự hiếm khi được truy vấn bị suy yếu. Đồ thị phát triển ý thức riêng về mức độ liên quan theo thời gian.

Akshay 🚀 - inline image

Mười bốn chế độ truy xuất

Cognee cung cấp 14 chế độ tìm kiếm. Những chế độ bạn thực sự sẽ sử dụng:

Akshay 🚀 - inline image

Xây dựng một agent thực sự với bộ nhớ Cognee

Đây là mẫu hoàn chỉnh kết nối Cognee vào vòng lặp nhận thức-suy nghĩ-hành động:

python
1import cognee
2from cognee import SearchType
3
4class CogneeMemoryAgent:
5 """Agent with graph-vector hybrid persistent memory."""
6
7 def __init__(self, session_id: str = "default"):
8 self.llm_client = OpenAI()
9 self.session_id = session_id
10
11 async def ingest(self, text: str, dataset: str = "main"):
12 await cognee.add(text, dataset)
13 await cognee.cognify([dataset])
14
15 async def recall(self, query: str) -> str:
16 results = await cognee.search(
17 query_text=query,
18 query_type=SearchType.GRAPH_COMPLETION,
19 session_id=self.session_id,
20 )
21 return results[0] if results else ""
22
23 async def chat(self, user_input: str) -> str:
24 context = await self.recall(user_input)
25 messages = [
26 {"role": "system", "content": "You are helpful. Use memory context."},
27 {"role": "system", "content": f"Memory context:\n{context}"},
28 {"role": "user", "content": user_input},
29 ]
30 response = self.llm_client.chat.completions.create(
31 model="gpt-4o-mini", messages=messages
32 )
33 reply = response.choices[0].message.content
34 await cognee.add(
35 f"User: {user_input}\nAssistant: {reply}",
36 "conversations"
37 )
38 await cognee.cognify(["conversations"])
39 return reply

Vòng lặp bộ nhớ: đưa vào, trích xuất, lưu trữ, truy xuất, phản hồi, lưu trữ lại. Mỗi lượt tương tác làm giàu đồ thị tri thức và xử lý tăng dần có nghĩa là bạn chỉ phải trả chi phí để lập chỉ mục nội dung mới.

Bộ nhớ phiên xử lý việc giải quyết đại từ một cách tự động:

python
1await cognee.search(query_text="Where does Alice live?", session_id="conv_1")
2await cognee.search(query_text="What does she do for work?", session_id="conv_1")
3# "she" resolves to Alice from session context

Tính đa người thuê được tích hợp sẵn ở cấp độ đồ thị với các quyền theo tập dữ liệu (đọc, ghi, xóa, chia sẻ). Không phải là sự tách biệt không gian tên, mà là sự cô lập ở cấp độ đồ thị thực tế.

Con đường thực tế phía trước

Nếu bạn đang xây dựng một agent ngay hôm nay, câu hỏi khởi đầu thực sự là: "Agent của tôi cần nhớ những gì và nó sẽ trả lời những loại câu hỏi nào?"

Nếu các truy vấn của bạn chỉ cần tìm kiếm tương tự ("tìm các cuộc hội thoại như thế này"), thì bộ nhớ chỉ dùng vector là đủ. Khi các truy vấn vượt qua ranh giới thực thể ("Dự án của Alice có bị ảnh hưởng bởi sự cố hôm thứ Ba không?"), bạn cần duyệt đồ thị.

Bạn có thể tự kết nối các kho lưu trữ vector, đồ thị và quan hệ riêng biệt. Các nhóm đi theo con đường này thường mất hàng tuần cho cơ sở hạ tầng cho một lớp bộ nhớ mà vẫn không học hỏi từ việc sử dụng của chính nó.

Cognee thu gọn điều đó thành bốn lời gọi API. Các giá trị mặc định được nhúng giúp bạn chạy trong vài phút. Các backend có thể thay thế (Postgres, Qdrant, Neo4j) đưa bạn đến môi trường sản xuất mà không cần thay đổi mã agent.

Trí thông minh đòi hỏi cấu trúc, không chỉ là lưu trữ. Ba mô hình lưu trữ (quan hệ, vector, đồ thị) không phải là các tùy chọn cạnh tranh. Chúng là các lớp bổ sung của cùng một hệ thống bộ nhớ. Đối xử với chúng như vậy là điều biến một trình bao bọc LLM không trạng thái thành một thứ thực sự học hỏi.

Điều tiếp theo bạn muốn agent của mình nhớ vào ngày mai mà nó đã quên hôm nay là gì? Hãy bắt đầu từ đó.

👉 **[Khám phá Cognee trên GitHub →**](https://github.com/topoteretes/cognee), hãy cho nó một ngôi sao và thử kết nối nó vào agent tiếp theo của bạn.

Bốn lời gọi bất đồng bộ, một lệnh pip install và bạn đã chạy.

Bài viết đến đây là kết thúc!

Nếu bạn thích đọc bài này:

Tìm tôi → @akshay_pachaar ✔️

Mỗi ngày, tôi chia sẻ các hướng dẫn và hiểu biết sâu sắc về AI, Học máy và các phương pháp hay nhất về lập trình vibe.

Lưu một chạm

Đọc sâu bài viết viral bằng AI trong YouMind

Lưu nguồn, đặt câu hỏi tập trung, tóm tắt lập luận và biến một bài viết viral thành các ghi chú có thể tái sử dụng trong một không gian làm việc AI duy nhất.

Khám phá YouMind
Dành cho nhà sáng tạo

Biến Markdown của bạn thành bài viết 𝕏 gọn gàng

Khi bạn đăng bài viết dài của riêng mình, việc định dạng hình ảnh, bảng và khối mã cho 𝕏 rất mệt mỏi. YouMind biến cả bản nháp Markdown thành một bài viết 𝕏 gọn gàng, sẵn sàng để đăng.

Thử Markdown sang 𝕏

Thêm pattern để giải mã

Bài viết viral gần đây

Khám phá thêm bài viết viral