Chào bạn! Bạn có tin không, sau một năm trời "địa ngục" phát triển, hàng tỉ phiên gỡ lỗi nát óc, và nỗi ám ảnh biến AI thành "thổ địa" Ấn Độ, cuối cùng tôi cũng đã khai sinh ra AI Associate! Đây không chỉ là một trợ lý AI đa ngôn ngữ thời gian thực bình thường đâu nhé, nó còn "hiểu" được cả tâm hồn và văn hóa Ấn Độ nữa cơ!<video controls src='https://truyentranh.letranglan.top/api/v1/proxy?url=https://www.youtube.com/embed/iwPx7lwibBI'></video>Bạn có thể 'sờ tận tay' ngay tại đây: <a href="https://ai-associate-2025.vercel.app">ai-associate-2025.vercel.app</a> và nghía qua mã nguồn mở tại: <a href="https://github.com/Aadya-Madankar/AI-Associate-2025">GitHub Repo</a>.Tưởng tượng mà xem: Bạn đang buôn chuyện với trợ lý AI bằng tiếng Hindi, rồi bỗng dưng hỏi chen vào bằng tiếng Anh kiểu 'अरे यaar, आज कैसा weather है?' (tức là 'Ê bạn, thời tiết hôm nay thế nào?'). Thế mà nó lại đáp lại bằng thứ tiếng Hindi chuẩn ngữ pháp, cứng nhắc như rô-bốt, nghe cứ như Google Dịch đang 'tức nước vỡ bờ' vậy. Quả thực, đây chính là thực trạng đau lòng của 1.4 tỷ người dân Ấn Độ đấy bạn ạ. Trong khi các ông lớn ở Silicon Valley mải mê xây AI cho dân nói tiếng Anh, chúng ta lại phải vật lộn với mấy công cụ dịch thuật chẳng hiểu gì về 'linh hồn' của những cuộc trò chuyện bản địa. Chính vì điều này mà tôi đã quyết tâm phải tạo ra một thứ gì đó hoàn toàn khác biệt!<img src='https://truyentranh.letranglan.top/api/v1/proxy?url=https://i.imgur.com/frustrated_ai_user.png' alt='Người dùng bực mình với AI cứng nhắc'> Vậy, điều gì làm cho AI Associate 'khác bọt' đến thế? Để tôi bật mí nhé!1. 🗣️ Hiểu Văn Hóa, Không Chỉ Dịch Ngôn Ngữ: Thay vì dịch 'How are you?' một cách máy móc thành 'आप कैसे हैं?', AI Associate sẽ 'ngấm' được ngữ cảnh và sắc thái mối quan hệ để biết lúc nào nên nói 'क्या हाल है भाई?' (tạm dịch: 'Ê, dạo này sao rồi ông bạn?'). Nghe thân tình hẳn đúng không?<img src='https://truyentranh.letranglan.top/api/v1/proxy?url=https://i.imgur.com/cultural_nuance_ai.png' alt='AI hiểu ngữ cảnh văn hóa'> 2. ⚡ Ngắt Lời Thời Gian Thực: Bạn có thể chen ngang cuộc trò chuyện với AI giữa chừng, y hệt như đang nói chuyện với một người bạn thật sự vậy! Chẳng cần phải chờ AI 'độc thoại' xong xuôi mới được lên tiếng đâu. Siêu tiện lợi!<img src='https://truyentranh.letranglan.top/api/v1/proxy?url=https://i.imgur.com/realtime_interruption.png' alt='Ngắt lời AI tự nhiên'> 3. 👁️ Hiểu Biết Đa Phương Thức: Chỉ cần đưa văn bản, vật thể hay cử chỉ qua camera, AI Associate sẽ xử lý mọi thứ ngay lập tức, mà vẫn giữ cho cuộc hội thoại trôi chảy mượt mà. AI giờ có mắt có tai luôn rồi!<img src='https://truyentranh.letranglan.top/api/v1/proxy?url=https://i.imgur.com/multimodal_ai.png' alt='AI hiểu đa phương thức'> 4. 🧠 Tích Hợp Kiến Thức Trực Tuyến: Hỏi về trận đấu cricket hôm nay à? Yên tâm, nó sẽ tự động 'phi' ngay lên Google để tìm kiếm thông tin theo thời gian thực và trả lời bạn bằng ngôn ngữ yêu thích. Đúng là trợ lý 'biết tuốt'!<img src='https://truyentranh.letranglan.top/api/v1/proxy?url=https://i.imgur.com/ai_live_knowledge.png' alt='AI tích hợp kiến thức trực tuyến'> 5. 🎭 Trí Tuệ Cảm Xúc: AI Associate còn 'bắt sóng' được năng lượng của bạn nữa cơ! Bạn mà 'làm mình làm mẩy' một chút, nó cũng sẽ 'đáp trả' lại một cách tinh nghịch. Còn nếu bạn cần sự động viên, nó sẽ phản hồi bằng sự quan tâm chân thành. Cứ như nói chuyện với người thật ấy!<img src='https://truyentranh.letranglan.top/api/v1/proxy?url=https://i.imgur.com/ai_emotional_intelligence.png' alt='AI có trí tuệ cảm xúc'> Giờ thì cùng lặn sâu vào hậu trường công nghệ một chút nhé! Đây là những quyết định then chốt đã giúp AI Associate 'ra đời'.<img src='https://truyentranh.letranglan.top/api/v1/proxy?url=https://i.imgur.com/technical_journey_map.png' alt='Bản đồ hành trình kỹ thuật'> Triết Lý Kiến Trúc: Lựa chọn: Giao tiếp WebSocket thời gian thực thay vì API REST. Lý do: Tốc độ phản hồi dưới 200ms là cực kỳ quan trọng để cuộc trò chuyện tự nhiên. Đánh đổi: Quản lý trạng thái phức tạp hơn, nhưng xứng đáng với trải nghiệm người dùng! Chiến Lược AI: Lựa chọn: Google Gemini là LLM chính, kết hợp với các lớp xử lý ngữ cảnh văn hóa riêng. Lý do: Hỗ trợ đa ngôn ngữ tốt hơn các mô hình khác, khả năng lập luận đỉnh cao. Thách thức: Phải 'tự tay' xây dựng các lớp tùy chỉnh để AI hiểu sâu về văn hóa Ấn Độ. Xử Lý Giọng Nói: Lựa chọn: API Web Speech gốc của trình duyệt, có cơ chế dự phòng tùy chỉnh. Lý do: Độ trễ thấp hơn nhiều so với các giải pháp dựa trên đám mây. Vấn đề đau đầu: Tương thích với Safari (vẫn đang trong quá trình xử lý!). Triển Khai (Deployment): Lựa chọn: Vercel cho frontend + Node.js cho backend. Lý do: Dễ dàng mở rộng, hỗ trợ WebSocket tốt. Bài học: Ứng dụng thời gian thực cần chiến lược tối ưu khác biệt đấy! À mà, hành trình nào mà chẳng có chông gai, đúng không? Đây là mấy 'cửa ải' khó nhằn nhất mà tôi đã phải vượt qua.<img src='https://truyentranh.letranglan.top/api/v1/proxy?url=https://i.imgur.com/challenges_ahead.png' alt='Những thách thức khó khăn'> 1. Độ Trễ Chính Là Kẻ Thù: Vấn đề: Thời gian phản hồi ban đầu lên tới 2-3 giây (ôi trời, lâu như chờ người yêu trang điểm!). Giải pháp: Xây dựng một đường ống xử lý song song – trong khi AI 'vắt óc' tạo câu trả lời, công cụ chuyển văn bản thành giọng nói (TTS) đã kịp 'khởi động' rồi. Kết quả: Phản hồi dưới 200ms cho hầu hết các câu hỏi (nhanh như chớp!).<img src='https://truyentranh.letranglan.top/api/v1/proxy?url=https://i.imgur.com/latency_solution.png' alt='Giải pháp giảm độ trễ'> 2. Mã Hóa Ngữ Cảnh Văn Hóa Khó Nhằn Cực Độ: Vấn đề: Làm sao dạy AI biết rằng từ "अच्छा" (acha) có thể có nghĩa là đồng ý, ngạc nhiên, hoặc thậm chí là mỉa mai, tùy ngữ cảnh? Đúng là 'hại não' mà! Giải pháp: Xây dựng một hệ thống phát hiện mẫu văn hóa kèm theo phân tích giọng điệu. Bài học: Tôi dành nhiều thời gian cho khoản này hơn cả việc xây dựng toàn bộ backend cộng lại đó!<img src='https://truyentranh.letranglan.top/api/v1/proxy?url=https://i.imgur.com/cultural_coding_dilemma.png' alt='Thách thức mã hóa ngữ cảnh văn hóa'> 3. Xử Lý Ngắt Lời: Vấn đề: Người dùng muốn ngắt lời AI giữa cuộc trò chuyện, y như con người làm vậy. Giải pháp: Sử dụng tính năng Phát hiện Hoạt động Giọng nói (Voice Activity Detection) với quản lý trạng thái tùy chỉnh. Thách thức: Duy trì ngữ cảnh cuộc trò chuyện xuyên suốt các lần ngắt lời. 4. Hạn Chế Của Trình Duyệt: Vấn đề: Các quyền truy cập âm thanh quá nghiêm ngặt của Safari. Tình trạng hiện tại: Hoạt động hoàn hảo trên Chrome/Edge, nhưng người dùng Safari thì phải dùng tạm bản dự phòng. Bài học: Cứ xây dựng cho 80% trường hợp sử dụng trước đã, rồi tính tiếp! Có một giai đoạn, tôi bị 'lạc lối' giữa cuộc hành trình vì quá mê mẩn việc tích hợp một nhân vật ảo 3D (Metahuman) để tạo ra những cuộc trò chuyện 'siêu thực'. Một cơn ác mộng đẹp đẽ: rendering 3D thời gian thực + tổng hợp giọng nói + khớp khẩu hình trên trình duyệt web mà không làm 'chết' hiệu năng máy. Thời gian đầu tư: 6 tháng trời. Tình trạng hiện tại: vẫn đang tiếp tục 'vật lộn' với nó. Bài học rút ra: Sự hoàn hảo là kẻ thù của việc 'xuất xưởng' sản phẩm!<img src='https://truyentranh.letranglan.top/api/v1/proxy?url=https://i.imgur.com/metahuman_obsession.png' alt='Ám ảnh Metahuman'> Và đây là điều tuyệt vời nhất: chỉ 48 giờ sau khi ra mắt! Hơn 10.000 lượt xem video! Hơn 500 sao trên GitHub! (Thật sự bất ngờ!) Hàng loạt bình luận từ 12 ngôn ngữ khác nhau. Đặc biệt, không một lời phàn nàn nào về tính xác thực văn hóa (đây mới là điều làm tôi tự hào nhất!). Những ngôn ngữ được yêu cầu demo nhiều nhất là: Tamil (38%) Telugu (22%) Bengali (18%) Punjabi (14%)<img src='https://truyentranh.letranglan.top/api/v1/proxy?url=https://i.imgur.com/community_response_charts.png' alt='Phản hồi cộng đồng'> Giờ thì cùng điểm qua 'đội hình' công nghệ đã làm nên AI Associate nhé: Frontend: React + Tailwind + SHAD CN (cho giao diện sạch sẽ, gọn gàng). Real-time: Kết nối WebSocket với xử lý ngắt lời tùy chỉnh. AI: Google Gemini kết hợp RAG (Retrieval-Augmented Generation) để tích hợp kiến thức trực tuyến. Speech: Web Speech API + hệ thống TTS (Text-to-Speech) tùy chỉnh. Vision: WebRTC + API Thị giác máy tính (Computer Vision API). Deployment: Vercel với khả năng tự động mở rộng (auto-scaling).<img src='https://truyentranh.letranglan.top/api/v1/proxy?url=https://i.imgur.com/tech_stack_overview.png' alt='Tổng quan công nghệ'> Mỗi cuộc hành trình đều mang lại những bài học quý giá, và đây là 5 điều tôi đã 'ngộ' ra được:<img src='https://truyentranh.letranglan.top/api/v1/proxy?url=https://i.imgur.com/lessons_learned_icon.png' alt='Biểu tượng bài học kinh nghiệm'> 1. Bắt Đầu Đơn Giản, Mở Rộng Thông Minh: Đừng cố gắng xây dựng mọi thứ cùng lúc! Tôi đã lãng phí cả tháng trời với mấy cái avatar 3D trong khi người dùng chỉ cần một cuộc trò chuyện đáng tin cậy thôi. Quá thấm! 2. Tính Xác Thực Văn Hóa Quan Trọng Hơn Sự Hoàn Hảo Kỹ Thuật: Người Ấn Độ có thể ngay lập tức phát hiện ra AI nào 'giả vờ' hiểu văn hóa đấy! Hãy đảm bảo bạn nắm rõ các sắc thái trước khi tối ưu hóa hiệu năng. 3. Thời Gian Thực Là Thử Thách Lớn: Hãy dành thêm thời gian cho việc tối ưu độ trễ. Người dùng đánh giá AI đàm thoại chỉ trong vài mili giây, chứ không phải vài giây đâu! 4. Phát Triển Dựa Trên Cộng Đồng: Hãy để người dùng dẫn lối cho việc phát triển tính năng. Hệ thống bình chọn ngôn ngữ đã dạy cho tôi nhiều điều về nhu cầu thực sự hơn bất kỳ nghiên cứu thị trường nào! 5. Tương Thích Trình Duyệt Là Thứ Cần Quan Tâm: Thị phần 15% của Safari vẫn có nghĩa là hàng trăm người dùng sẽ thất vọng. Luôn có kế hoạch dự phòng nhé! Vậy, tương lai của AI Associate sẽ đi về đâu? Tôi đã có vài kế hoạch đây!<img src='https://truyentranh.letranglan.top/api/v1/proxy?url=https://i.imgur.com/whats_next_road.png' alt='Kế hoạch tương lai'> Ngay lập tức (30 ngày tới): Phát triển ứng dụng di động. Sửa lỗi tương thích Safari. Tối ưu hiệu năng để 'đón' lượng truy cập 'khủng'. Trung hạn (Quý 4 năm 2025): Hoàn thiện tích hợp Metahuman. Nhân bản giọng nói của người dùng. Khả năng hoạt động ngoại tuyến để đảm bảo quyền riêng tư. Tầm nhìn dài hạn: Tích hợp IoT cho nhà thông minh. Trợ lý giáo dục cho chương trình học Ấn Độ. Giải pháp doanh nghiệp cho các công ty Ấn Độ. AI Associate được phát triển dưới dạng mã nguồn mở, bởi vì tôi tin rằng sự đổi mới không nên bị 'giữ kín'. Cộng đồng lập trình viên Ấn Độ có tài năng, chúng ta chỉ cần những công cụ phù hợp thôi! Những lĩnh vực mà bạn có thể đóng góp: Cải thiện ngôn ngữ khu vực. Các mẫu ngữ cảnh văn hóa. Tối ưu hiệu năng. Phát triển ứng dụng di động.<img src='https://truyentranh.letranglan.top/api/v1/proxy?url=https://i.imgur.com/open_source_community.png' alt='Cộng đồng mã nguồn mở'> Gửi tới các bạn đồng nghiệp lập trình viên thân mến!<img src='https://truyentranh.letranglan.top/api/v1/proxy?url=https://i.imgur.com/developers_united.png' alt='Cộng đồng lập trình viên'> Nếu bạn đang xây dựng AI đàm thoại: Hãy đầu tư mạnh vào tối ưu độ trễ. Ngữ cảnh văn hóa khó hơn dịch ngôn ngữ nhiều đấy! Xử lý ngắt lời thời gian thực là yếu tố sống còn để tạo cảm giác tự nhiên. Hãy thử nghiệm với người dùng thật, đừng chỉ 'tự sướng' một mình nhé! Nếu bạn đang xây dựng sản phẩm cho Ấn Độ: Tính xác thực quan trọng hơn sự hoàn hảo. 'Code-switching' (pha trộn ngôn ngữ) là chuyện bình thường, không phải ngoại lệ. Các biến thể vùng miền quan trọng hơn bạn nghĩ đấy! Phản hồi từ cộng đồng chính là 'vàng'! Đây không chỉ là việc xây dựng thêm một công cụ AI khác. Đây là việc đảm bảo rằng khi AI trở nên phổ biến, nó sẽ bao gồm tất cả chúng ta – không chỉ giới tinh hoa thành thị nói tiếng Anh. Khi bà của tôi có thể trò chuyện tự nhiên với AI bằng tiếng Konkani, khi nông dân nhận được lời khuyên bằng tiếng Punjabi chính gốc, khi học sinh học bài bằng tiếng Tamil với đầy đủ ngữ cảnh văn hóa – đó mới chính là thành công đích thực! Hãy tự mình trải nghiệm: <a href="https://ai-associate-2025.vercel.app">ai-associate-2025.vercel.app</a> và cho tôi biết bạn muốn AI Associate 'thành thạo' ngôn ngữ Ấn Độ nào tiếp theo nhé! GitHub: <a href="https://github.com/Aadya-Madankar/AI-Associate-2025">github.com/Aadya-Madankar/AI-Associate-2025</a>
Câu chuyện của tôi bắt đầu cách đây hơn một năm. Hồi đó, tôi cứ quanh quẩn với mấy đoạn code nhỏ xíu, mấy cái script vụn vặt, chẳng có dự án phức tạp nào ra hồn. Thú thật, đầu óc tôi lúc đó cần một 'cú sốc điện' để tỉnh táo lại, nếu không thì cứ ù lì mãi thôi. Với ý định nghiêm túc chuyển sang làm lập trình viên full-time, tôi quyết định tự tay xây dựng một dự án hoàn chỉnh từ A đến Z để mài giũa kỹ năng. Mục tiêu là phải vừa thử thách, vừa gần gũi với cuộc sống hằng ngày. Và thế là ý tưởng về một ứng dụng ghi chú ra đời! Ai mà chẳng ghi chú mỗi ngày, đúng không? Tôi tự nhủ: "Chắc dễ òm à!". Ôi thôi, cái câu "Chúng ta làm việc này không phải vì nó dễ, mà vì chúng ta nghĩ nó sẽ dễ" đúng là dành cho tôi! Hóa ra có cả tỉ thứ để học, nhưng chính điều đó lại là động lực để tôi lao vào.Ngay từ đầu, tôi đã đặt ra vài mục tiêu cốt lõi cho 'đứa con' này: ứng dụng phải chạy mượt mà trên mọi thiết bị, hoạt động được cả khi offline, và quan trọng nhất là phải có tính năng 'hoàn tác/làm lại' (undo/redo) đầy đủ lịch sử. Xây dựng một ứng dụng web là lựa chọn hợp lý nhất. Tuy nhiên, một 'tảng đá' khổng lồ cứ lởn vởn trong đầu tôi: làm sao để xử lý xung đột khi nhiều người cùng chỉnh sửa văn bản theo thời gian thực? Dùng mấy thư viện có sẵn như ShareDB, Etherpad, Yjs, hay Automerge thì quá dễ rồi, nhưng làm thế thì còn gì là thử thách học hỏi nữa! Không, tôi phải tự tìm ra lời giải mới chịu!Tôi biết ngay là cái kiểu 'thay thế văn bản đơn giản' sẽ chẳng ăn thua. Giải pháp phải là chỉ gửi đi những thay đổi mà người dùng thực sự đã làm. Tự mình nghĩ ra một thuật toán từ con số 0 ư? Chắc chắn sẽ tốn rất nhiều thời gian mà hiệu quả thì... hên xui. Sau một hồi 'đào bới' không ngừng nghỉ, cuối cùng tôi cũng tìm thấy 'viên ngọc quý': cuốn <a href='https://github.com/knemerzitski/notes/blob/article/packages/collab/docs/easysync-full-description.pdf'>Etherpad EasySync Technical Manual</a> trong repo GitHub của Etherpad. Cuốn sách này giải thích rất chi tiết về Operational Transformation (OT) – một khái niệm nghe là lạ mà quen đó. Đọc xong, tôi như được khai sáng, mọi khúc mắc đều được gỡ bỏ, đủ để tôi bắt tay vào việc. À mà để tôi nói rõ luôn nhé: tôi không hề nhìn vào mã nguồn của Etherpad đâu, vì tôi muốn tự mình hiểu và giải quyết vấn đề.Trong bài viết này, tôi sẽ tập trung kể về những thử thách cam go khi thiết kế và triển khai OT từ đầu, đồng thời giải thích một chút về cách OT hoạt động 'trong thực tế' nhé.Operational Transformation (OT) là gì? Chỉ là dịch chuyển các chỉ số thôi mà, đúng không?Nghe cái tên Operational Transformation (OT) thì có vẻ "nguy hiểm", nhưng ban đầu nó nghe thật đơn giản: hai người dùng cùng chỉnh sửa một tài liệu, bạn chỉ cần điều chỉnh các 'chỉ số' (vị trí) sao cho các thay đổi của họ không... giẫm đạp lên nhau. Dễ như ăn kẹo ấy nhỉ?Vâng, đó chính xác là cái suy nghĩ "ngây thơ" của tôi lúc ban đầu!Tôi lao vào cuộc chiến này chỉ với mỗi cuốn Etherpad Manual trong tay ảo. Không dùng pseudocode, không dùng thư viện nào hết. Cuối cùng, tôi phải mất cả tá lần thử và sai chỉ để viết cho đúng một hàm transform đơn giản. Nhưng cái 'cơn đau đầu' lớn nhất lại là triển khai tính năng hoàn tác/làm lại (undo/redo) cho từng người dùng mà không làm hỏng tính nhất quán của tài liệu hay ý định của người dùng. Thêm cả chỉnh sửa offline, đồng bộ qua WebSocket, và theo dõi vị trí con trỏ nữa... ôi thôi, bạn sẽ thấy mình đang 'chơi đùa' với một mớ các bộ phận chuyển động liên tục. Sự phức tạp ở đây không nằm nhiều ở bản thân thuật toán, mà là ở toàn bộ hệ thống xung quanh nó.Thế là cái thứ ban đầu chỉ là một "just a side project" đã biến thành dự án đòi hỏi kỹ thuật cao nhất mà tôi từng xây dựng. Nhưng tôi mừng vì mình đã không 'đi đường tắt', bởi vì chính nó đã cho tôi một sự hiểu biết sâu sắc về cách các hệ thống cộng tác thực sự hoạt động.Để hiểu rõ hơn về OT, hãy cùng 'nghía' qua vài ví dụ thực tế nhé!Ví dụ 1: Xử lý chỉnh sửa đồng thời (Không xung đột)Khi hai người dùng cùng 'nhảy' vào chỉnh sửa tài liệu, máy chủ phải làm sao để hòa giải các thay đổi của họ, đảm bảo rằng cả hai đều có cùng một kết quả cuối cùng.Tình huống ban đầu: Alice và Bob cùng có tài liệu là "Hello!".Alice làm gì? Alice chèn thêm " world" ngay trước dấu "!".Bob làm gì? Bob chèn thêm " Bob!" ngay sau dấu "!".Thứ tự xử lý trên máy chủ:Máy chủ áp dụng thay đổi của Alice trước. Tài liệu giờ là "Hello world!".Khi xử lý thay đổi của Bob, máy chủ sẽ 'biến đổi' nó, có tính đến thay đổi của Alice.Kết quả: Cả Alice và Bob đều thấy tài liệu là "Hello world! Bob!".Bạn có thể hình dung chuỗi sự kiện này qua sơ đồ dưới đây:<img src='https://truyentranh.letranglan.top/api/v1/proxy?url=https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7451u3fwej34mbwl6f8g.webp' alt='Sơ đồ chỉnh sửa không xung đột của Alice và Bob'>Mặc dù không có xung đột trực tiếp, nhưng thay đổi của Bob vẫn cần được điều chỉnh vì nó được thực hiện trên một tài liệu... 'cũ rích' (chưa có thay đổi của Alice). Cả hai thay đổi ban đầu đều tham chiếu đến cùng một tài liệu "Hello!". Vì Alice đã chèn " world" vào, nên cái " Bob!" của Bob cần phải 'dịch sang phải' 6 ký tự để giữ nguyên ý định ban đầu của anh ấy.Đây là cách máy chủ 'nhìn thấy' thay đổi của Bob:Thay đổi của Bob ban đầu được áp dụng cho "Hello!".Thay đổi của Alice đã được xử lý trước đó.Thay đổi của Bob được 'biến đổi' (transform) so với thay đổi của Alice, dịch chuyển vị trí sang phải 6 ký tự.Máy chủ thêm thay đổi đã được điều chỉnh của Bob vào danh sách.<img src='https://truyentranh.letranglan.top/api/v1/proxy?url=https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzf58jvxq8m6hmznh4fb9.webp' alt='Dịch chuyển thay đổi của Bob'>Bằng cách áp dụng phép biến đổi này, cả Alice và Bob đều sẽ thấy cùng một tài liệu cuối cùng: "Hello world! Bob!". Thật vi diệu!Ví dụ 2: Xử lý chỉnh sửa đồng thời (Có xung đột)Xung đột có thể xảy ra khi hai người dùng cùng chèn văn bản vào... chính xác cùng một vị trí. Hãy xem một biến thể nhỏ của ví dụ trước để thấy điều này nhé:Tình huống ban đầu: Alice và Bob cùng có tài liệu là "Hello!".Alice làm gì? Alice chèn thêm " world" ngay trước dấu "!".Bob làm gì? Bob chèn thêm " cat" ngay trước dấu "!".Thứ tự xử lý trên máy chủ:Máy chủ áp dụng thay đổi của Alice trước. Tài liệu giờ là "Hello world!".Khi xử lý thay đổi của Bob, máy chủ sẽ 'biến đổi' nó, có tính đến thay đổi của Alice.Kết quả: Cả Alice và Bob đều thấy tài liệu là "Hello world cat!".Chuỗi sự kiện này được minh họa trong sơ đồ dưới đây:<img src='https://truyentranh.letranglan.top/api/v1/proxy?url=https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxkfknrhdvwqp8ao8e6xd.webp' alt='Sơ đồ chỉnh sửa có xung đột của Alice và Bob'>Bạn có thể thắc mắc tại sao kết quả lại là "Hello world cat!" thay vì "Hello cat world!". Cả hai kết quả đều hợp lệ và không cái nào phản ánh 'ý định' của người dùng một cách chính xác hơn. Khi triển khai OT, bạn phải chọn một 'xu hướng' (bias) để xử lý các thao tác chèn đồng thời vào cùng một vị trí. Trong trường hợp này, thao tác chèn mà máy chủ xử lý trước sẽ được đặt ở bên trái. Chúng ta gọi đó là 'ưu tiên bên trái' (left bias).Tại sao lại tự triển khai OT từ đầu? Thử thách và Bài học xương máu!Tôi không tự xây dựng OT từ đầu vì... bị ép buộc. Một ứng dụng ghi chú đơn thuần thì có vẻ không đủ 'sâu' để tôi học hỏi, nên việc đào sâu vào sự phức tạp của cộng tác thời gian thực dường như là một thử thách rất 'vừa tầm'. Dùng một thư viện có sẵn ư? Tuyệt đối không! Nó giống như một 'hộp đen' bí ẩn vậy, tôi không muốn thế. Thay vào đó, tôi đã dũng cảm chấp nhận thử thách, dù lúc đó cũng không biết mình đang 'đâm đầu' vào cái gì! Để giữ cho phạm vi dự án không 'phình to' quá mức, tôi đã đặt ra một vài ràng buộc:Chỉ xử lý văn bản thuần túy (không có định dạng phức tạp như in đậm hay cỡ chữ khác nhau).Máy chủ tập trung (để tránh rắc rối của kiến trúc phi tập trung).'Ưu tiên theo thứ tự từ điển' – Sai lầm đầu đời!Một trong những 'bước đi hụt' đầu tiên của tôi là chọn kiểu ưu tiên theo thứ tự từ điển (lexicographical bias) cho các thao tác chèn đồng thời. Lúc đó, tôi thấy nó có vẻ hợp lý vì các ký tự có thứ tự rõ ràng mà. Nhưng hóa ra, nó lại tạo ra những lỗi không nhất quán rất 'tinh vi', cực kỳ khó gỡ lỗi. Ví dụ, việc triển khai tính năng lịch sử (history) sẽ bị 'toang' vì các thao tác không phải lúc nào cũng áp dụng theo một thứ tự có thể dự đoán được. Và để 'thêm dầu vào lửa', sau này tôi mới phát hiện ra rằng một số hệ thống còn xử lý thứ tự ký tự khác nhau khi gặp các trường hợp như chữ có dấu hoặc quy tắc tùy chỉnh theo địa phương. Đó thực sự là một bài học đắt giá về việc một quyết định thiết kế quan trọng, nếu sai, có thể khiến bạn 'ngã sấp mặt' như thế nào. Cuối cùng, tôi đành quay về với một lựa chọn 'dễ tính' hơn: ưu tiên bên trái (left bias).Mutable State – Cơn ác mộng!Quản lý trạng thái (state management) hóa ra lại là một 'ổ bug' không ngừng nghỉ. Phiên bản đầu tiên của tôi dùng 'trạng thái có thể thay đổi' (mutable state). Điều này biến việc gỡ lỗi thành một cơn ác mộng thực sự. Khi trạng thái bị hỏng, tôi phải 'cày' qua hàng tá file log chỉ để tìm ra nguyên nhân lỗi. Chuyển sang dùng 'trạng thái bất biến' (immutable state) với sự trợ giúp của <a href='https://www.npmjs.com/package/immer'>immer.js</a> đã tạo ra một sự thay đổi khổng lồ. Các lỗi trở nên dễ dàng cô lập hơn, và hành vi của hệ thống cũng trở nên dễ đoán. Dù bất biến có thể làm dấy lên lo ngại về hiệu suất, nhưng đó là một sự đánh đổi rất đáng giá. Nếu hiệu suất có bao giờ trở thành vấn đề, bạn luôn có thể tối ưu từng chút một khi đã có một logic vững chắc và được kiểm thử kỹ càng.API Tối giản – Đừng 'phô trương' quá nhiều!Giao diện (API) mà tôi dùng để 'phơi bày' các chức năng của OT ban đầu được thiết kế tệ hại. Có quá nhiều cách để tương tác với OT, dẫn đến việc sử dụng không nhất quán, các lỗi 'ẩn mình' và rất khó bảo trì. Thậm chí, tôi còn vô tình 'lộ' ra một số chức năng thông qua các đối tượng nội bộ. Sau nhiều lần 'cải cách' và với cái nhìn sâu sắc hơn về hệ thống, tôi đã 'gọt giũa' API chỉ còn những cái thiết yếu nhất. Điều này giúp hệ thống dễ bảo trì hơn và mã nguồn cũng dễ hiểu hơn rất nhiều.Hoàn tác/Làm lại (Undo/Redo) khi cộng tác – Không đơn giản đâu nhé!Tính năng hoàn tác/làm lại (undo/redo) khi làm việc một mình thì đơn giản thôi: cứ đẩy các thao tác vào một 'ngăn xếp' (stack) rồi lấy ra khi cần – mọi thứ đều tĩnh và tuyến tính. Nhưng trong môi trường cộng tác, nó lại không hề dễ dàng chút nào. Bạn phải 'biến đổi' (transform) các thao tác đó so với các thay đổi bên ngoài xảy ra đồng thời. Thậm chí còn có trường hợp phức tạp hơn là khôi phục các thao tác cũ từ máy chủ. Điều này đòi hỏi tôi phải thử nghiệm với rất nhiều thiết kế ngăn xếp lịch sử khác nhau, một quá trình đầy rẫy thử nghiệm, sai lầm và những suy nghĩ... 'xoắn não'.Viết lại toàn bộ – Thà đau một lần!Thực tế là, với tất cả những vấn đề kể trên gộp lại, cuối cùng tôi đã quyết định viết lại toàn bộ phần triển khai OT. Lần này, tôi dùng trạng thái bất biến (immutable state) và một giao diện (interface) đơn giản hơn. Chính lần viết lại này đã biến một bản prototype 'mong manh dễ vỡ' thành một hệ thống vững chắc và dễ bảo trì.Tóm lại, việc tự xây dựng OT từ đầu là một hành trình học hỏi cực kỳ 'khắc nghiệt' nhưng đã mang lại cho tôi những hiểu biết vô giá về các hệ thống chỉnh sửa cộng tác. Nếu bạn đang cân nhắc tự triển khai thư viện OT của riêng mình, hãy chuẩn bị tinh thần cho một đường cong học tập 'dốc đứng' và ít nhất thì... làm ơn hãy dùng trạng thái bất biến nhé! Chỉ nhắc nhẹ thôi, các thư viện hiện có có thể giúp bạn tránh được rất nhiều 'nỗi đau' đó. Còn với tôi, tự làm từ đầu đã trở thành một trong những nền tảng của dự án này và là minh chứng rõ nhất cho kỹ năng kỹ thuật của tôi.Đào sâu kỹ thuật: Phép Hợp (Composition), Phép Biến Đổi (Transform), và Hoàn tác (Undo)Phần này chúng ta sẽ cùng 'mổ xẻ' các khối xây dựng cốt lõi của OT: Hợp nhất các thay đổi (composing changes), Biến đổi các thay đổi đồng thời (transforming concurrent changes), và Xử lý hoàn tác (handling undo). Đây không phải là bản sao của Etherpad Manual, mà là cái nhìn thực tế về cách tôi đã hiểu và áp dụng các khái niệm này vào thực tiễn.1. Phép Hợp (Composition) – Khi các thay đổi 'hòa quyện' vào nhauPhép Hợp là quá trình áp dụng một thay đổi lên một thay đổi khác, ký hiệu là A ⋅ B, có nghĩa là B được áp dụng/hợp nhất lên A.Ví dụ: Phép Hợp đơn giảnHãy xem một ví dụ minh họa về cách hai thay đổi được hợp nhất với nhau:Thay đổi A: chèn "Hello"Thay đổi B: giữ lại (retain) "Hello", chèn thêm " world!"Kết quả A ⋅ B: chèn "Hello world!"Bạn có thể hình dung điều này qua sơ đồ dưới đây, trong đó văn bản được chia thành các ký tự, mỗi ký tự nằm trong một ô. Các mũi tên chỉ mang tính trang trí, hướng đến nguồn gốc của ký tự đó.<img src='https://truyentranh.letranglan.top/api/v1/proxy?url=https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F755oupvghbgkhr89q0ih.webp' alt='Sơ đồ hợp nhất các thay đổi'>Thay đổi A chèn "Hello", và thay đổi B giữ lại tất cả từ A rồi chèn thêm " world!". Sự hợp nhất của chúng cho ra kết quả là "Hello world!". Dễ hiểu đúng không nào?2. Thuật toán Biến đổi (Transform) – Trái tim của OTHàm `transform` chính là 'linh hồn' của OT. Khi có hai thay đổi đồng thời trên cùng một tài liệu, hàm `transform` sẽ điều chỉnh chúng sao cho dù áp dụng theo thứ tự nào, chúng ta vẫn nhận được cùng một kết quả cuối cùng. Trong thực tế, điều này có nghĩa là dịch chuyển các chỉ số và thay thế các thao tác chèn bằng các ký tự được giữ lại.Trong Etherpad Manual và trong mã nguồn của tôi, hàm để điều chỉnh các thao tác được gọi là `follow(A,B)` hoặc `f(A,B)`. Nhưng trong bài viết này (và để dễ hiểu hơn), tôi sẽ dùng thuật ngữ `transform` và ký hiệu là `t(A,B)`.Thuật toán này dựa trên ba quy tắc 'vàng' sau:R1. Các thao tác chèn trong A sẽ trở thành các ký tự được giữ lại trong `t(A, B)`.R2. Các thao tác chèn trong B sẽ vẫn là các thao tác chèn trong `t(A, B)`.R3. Giữ lại (retain) bất kỳ ký tự nào được giữ lại ở cả A và B.(Được điều chỉnh đôi chút từ Etherpad và EasySync Technical Manual, AppJet, Inc., có sửa đổi bởi Etherpad Foundation)Ví dụ: Biến đổi trong hành độngHãy cùng xem các quy tắc này được sử dụng trong một tình huống hoàn chỉnh nhé:Tài liệu gốc T: "is long cute tail".Thay đổi X của Bob: Thay thế "is long " bằng "big " → T ⋅ X = "big cute tail".Thay đổi Y của Alice: Thay thế "is " bằng "has ", và thay thế "cute " bằng "round " → T ⋅ Y = "has long round tail".Các thay đổi này được thể hiện trong sơ đồ dưới đây. Tài liệu T là điểm xuất phát. Thay đổi X của Bob đi lên, và thay đổi Y của Alice đi xuống.<img src='https://truyentranh.letranglan.top/api/v1/proxy?url=https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj2rk93q9sonph9srp4q5.webp' alt='Ví dụ thuật toán biến đổi'>`t(X, Y)` là thay đổi của Bob đã được điều chỉnh bởi Alice: chèn "has ", giữ lại "big ", chèn "round ", xóa "cute ", và giữ lại "tail".`t(Y, X)` là thay đổi của Alice đã được điều chỉnh bởi Bob: giữ lại "has ", chèn "big ", và giữ lại "round tail".Cuối cùng, cả Alice và Bob đều sẽ thấy "has big round tail" trên màn hình của họ. Nó bao gồm tất cả những gì được chèn bởi cả hai người dùng và loại bỏ tất cả những gì một trong số họ đã xóa.Một chi tiết tinh tế khác: máy chủ coi các thao tác chèn mới nhận được là 'nguyên tử' (atomic), nghĩa là chúng không thể bị chia cắt bởi những người dùng khác. Ví dụ, khi Alice thay thế "is" bằng "has". Bob không thể chèn "rnes" vào giữa "has" để thành "harness". Thay vào đó, kết quả sẽ là "hasrnes". Suy cho cùng, làm sao Bob có thể chèn thứ gì đó vào giữa đoạn văn bản mà anh ta còn chưa thấy cơ chứ?Pseudocode: Hàm Transform – Bạn muốn 'biến hình' thế nào?Đoạn pseudocode dưới đây minh họa hàm `transform` hoạt động bằng cách lặp qua từng vị trí một. Tuy nhiên, bạn hoàn toàn có thể tối ưu nó để duyệt theo từng khoảng (range) nhằm giảm độ phức tạp về thời gian nhé!```// Hàm này dùng để biến đổi các thay đổi A và B, với tham số isAfirst là ưu tiên thứ tự của A// Kết quả là một thay đổi có thể hợp nhất được trên A, phản ánh ý định của B.Function transform(A, B, isAfirst) Khởi tạo một danh sách rỗng tên là result // Duyệt qua cả hai thay đổi, từng vị trí một For mỗi bộ ba (opA, posA, opB) trong walk(A, B) Do If opA.type là 'retain' và opB.type là 'retain' Then // R3. Giữ lại, vì cả hai thay đổi đều muốn giữ lại Thêm posA vào result Else If opA.type là 'insert' và opB.type là 'insert' Then If isAfirst Then // R1. Các thao tác chèn trong A trở thành các ký tự được giữ lại Thêm posA vào result // R2. Các thao tác chèn trong B vẫn là các thao tác chèn Thêm opB vào result Else // R2. Các thao tác chèn trong B vẫn là các thao tác chèn Thêm opB vào result // R1. Các thao tác chèn trong A trở thành các ký tự được giữ lại Thêm posA vào result End If Else If opA.type là 'insert' Then // R1. Các thao tác chèn trong A trở thành các ký tự được giữ lại Thêm posA vào result Else If opB.type là 'insert' Then // R2. Các thao tác chèn trong B vẫn là các thao tác chèn Thêm opB vào result End If End For Return resultEnd Function```3. Hoàn tác (Undo) giữa muôn vàn thay đổi bên ngoài – 'Khó nhằn' hơn bạn tưởng!Hoàn tác (undo) nghe có vẻ tầm thường cho đến khi bạn phải tính đến những thay đổi bên ngoài do người dùng khác tạo ra. Bạn phải 'biến đổi' một thao tác hoàn tác để 'chống lại' các thay đổi bên ngoài đó, nếu không thì tài liệu của bạn sẽ 'loạn xạ' mất.Ví dụ: Hoàn tác sau khi có thay đổi bên ngoàiHãy cùng xem một ví dụ mà Bob hoàn tác thay đổi mới nhất của mình sau khi Alice đã gửi một thay đổi bên ngoài nhé.Bắt đầu với một tài liệu trống rỗng.Bob gõ "abc" → Bob's undo stack: [chèn "a", chèn "b", chèn "c"].Alice chèn "hi" vào đầu tài liệu → Tài liệu hiện tại: "hiabc".Bob nhấn hoàn tác (undo). Thao tác ngược của chèn "c" là xóa tại vị trí 2, nhưng nếu áp dụng trực tiếp nó sẽ xóa sai ký tự (sẽ thành "hibc" thay vì "hiab").Thay vào đó, chúng ta 'biến đổi' thao tác xóa tại vị trí 2 bằng cách dịch chuyển nó sang phải 2 vị trí → xóa tại vị trí 4 → kết quả là xóa đúng ký tự "c", cho ra "hiab".Đây là sơ đồ minh họa cách trạng thái của Bob được điều chỉnh khi anh ấy nhấn hoàn tác:<img src='https://truyentranh.letranglan.top/api/v1/proxy?url=https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhfpswxcynj6zu2a2eom2.webp' alt='Thao tác hoàn tác'>Mỗi thao tác hoàn tác có thể có danh sách các thay đổi bên ngoài riêng của nó. Nhưng trong thực tế, những thay đổi đó sẽ 'chuyển xuống' trong ngăn xếp khi các thao tác hoàn tác được 'bật' ra. Bằng cách đó, hoàn tác chỉ được điều chỉnh khi cần thiết.Hoàn tác/Làm lại (Undo/Redo) – Cặp đôi 'đối xứng' nhưng không hoàn hảoHoàn tác và làm lại gần như đối xứng hoàn toàn, nhưng có một điểm khác biệt quan trọng: các 'thao tác rỗng' (no-ops). Một thao tác trở thành 'no-op' nếu nó không tạo ra bất kỳ hiệu ứng nào nhìn thấy được (ví dụ: xóa một đoạn văn bản đã bị xóa rồi).Một thao tác hoàn tác 'no-op' vẫn có thể điền vào ngăn xếp làm lại (khi thao tác làm lại vẫn có ý nghĩa).Thao tác làm lại 'no-op' thì luôn bị loại bỏ.Điều này có thể rất hữu ích để khôi phục văn bản bị người dùng khác xóa. Ví dụ:Alice chèn "abc"Bob xóa "c" (thay đổi bên ngoài)Alice bây giờ thấy "ab"Alice nhấn hoàn tác (undo) và thấy "a"Alice nhấn làm lại (redo) và thấy "ab"Alice nhấn làm lại lần nữa và thấy "abc" (thao tác xóa "c" của Bob đã được khôi phục)Thật thông minh phải không?Pseudocode: Hàm Undo – Cách để 'trở về quá khứ' một cách an toàn!Đây là pseudocode cho hàm `undo`. Nó sẽ hoàn tác thao tác cuối cùng bằng cách áp dụng 'thao tác ngược' đã được biến đổi của nó, đồng thời cập nhật ngữ cảnh hoàn tác tiếp theo với các biến đổi bên ngoài.```Procedure undo() Gỡ bỏ thao tác cuối cùng khỏi ngăn xếp hoàn tác và gán nó cho op Xem qua (nhưng không gỡ bỏ) thao tác tiếp theo trên ngăn xếp hoàn tác và gán nó cho nextOp Gán applyOp bằng inverse(op) // Tạo thao tác ngược lại của op For mỗi external (thay đổi bên ngoài) trong op.externalChanges Do // Biến đổi external bằng cách dùng applyOp và lưu trữ nó cho lần hoàn tác tiếp theo Thêm transform(external, applyOp) vào nextOp.externalChanges // Cập nhật applyOp để phản ánh thay đổi bên ngoài Gán applyOp bằng transform(applyOp, external) End For Đẩy applyOp vào ngăn xếp làm lại (redo stack) Áp dụng applyOp vào tài liệuEnd Procedure```Lời kết – Chấm dứt hành trình 'khó nhằn'!Việc triển khai OT từ đầu hóa ra 'khó nhằn' hơn tôi tưởng rất nhiều. Có những lúc tôi đã tự hỏi liệu mình có 'chọn nhầm đường' không, nhưng cuối cùng, đó là một trải nghiệm vô cùng đáng giá và giúp tôi trưởng thành vượt bậc. Chắc chắn rồi, các thư viện có sẵn đã có thể 'cứu' tôi khỏi hàng tá 'nỗi đau', nhưng tự tay xây dựng nó đã mang lại cho tôi sự hiểu biết sâu sắc hơn rất nhiều về cách giải quyết xung đột và cộng tác thời gian thực.Dự án này vẫn chưa hoàn hảo đâu. Vẫn còn vài điểm chưa 'nhẵn nhụi' và vài tính năng tôi có thể thêm vào sau này. Nhưng tôi tự hào về những gì mình đã làm được và giờ thì vui vẻ 'nghỉ xả hơi' thôi. Nếu bạn tò mò, hãy thử 'nghía' qua <a href='https://notes.knemerzitski.com/'>bản demo trực tiếp</a> hoặc 'đào bới' <a href='https://github.com/knemerzitski/notes'>repo GitHub</a> nhé.Rất mong được nghe suy nghĩ hoặc câu hỏi của bạn, đừng ngại ngần liên hệ với tôi nha!