<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>TRẠMDEV</title>
    <link>https://tramdev.io/</link>
    <description>Publication về kỹ thuật và tư duy lập trình — AIkisu, Devlog, Đạo, Toolbox.</description>
    <language>vi</language>
    <atom:link href="https://tramdev.io/feed.xml" rel="self" type="application/rss+xml" />
    <image>
      <url>https://tramdev.io/icon-144.png</url>
      <title>TRẠMDEV</title>
      <link>https://tramdev.io/</link>
      <width>144</width>
      <height>144</height>
    </image>
    <lastBuildDate>Thu, 15 Oct 2026 00:00:00 GMT</lastBuildDate>
    <item>
      <title>Hình mẫu mới trong thời đại AI: người làm sản phẩm</title>
      <link>https://tramdev.io/posts/hinh-mau-moi-trong-thoi-dai-ai-nguoi-lam-san-pham</link>
      <guid isPermaLink="true">https://tramdev.io/posts/hinh-mau-moi-trong-thoi-dai-ai-nguoi-lam-san-pham</guid>
      <pubDate>Thu, 15 Oct 2026 00:00:00 GMT</pubDate>
      <category>AIkisu</category>
      <description>AI không chỉ làm developer viết code nhanh hơn. Nó đang tạo ra một hình mẫu mới: người có thể giữ toàn bộ vision của sản phẩm và tự ship nó.</description>
      <content:encoded><![CDATA[<p>Trước đây, để build một sản phẩm từ đầu đến cuối, bạn cần ít nhất ba người: một người nghĩ về product, một người design, một người code. Một người thông minh vẫn có thể biết làm cả ba, chỉ là không đủ thời gian, vì mỗi phần tốn hàng tháng trời.</p>
<p>AI đang xóa cái giả định đó, và code nhanh hơn chỉ là một phần nhỏ của chuyện này. Bây giờ một người có thể giữ toàn bộ product trong đầu, từ lý do tại sao nó tồn tại, đến trải nghiệm người dùng, đến implementation chi tiết, rồi tự ship nó ở tốc độ trước đây chỉ team mới đạt được. Garry Tan <a href="https://tramdev.io/posts/gstack-nha-may-phan-mem-cua-mot-nguoi">nói rằng đã viết hơn 600.000 dòng code trong 60 ngày</a> trong khi vẫn là CEO của YC. Số dòng code chưa bao giờ là thước đo tốt cho giá trị của phần mềm, nên tôi không để ý nhiều đến con số đó. Cái tôi để ý là một người đang giữ được cả vòng lặp mà trước đây cần một team. Với tôi đó là một cách làm việc khác, và gọi nó là năng suất cao hơn thì chưa tả hết.</p>
<h2>Sự khác biệt quan trọng nhất</h2>
<p>Hình mẫu cũ có hai vai trò tách biệt: người biết phải làm gì (product, business) và người biết làm như thế nào (kỹ thuật). Hai vai trò này giao tiếp qua spec document, ticket, và meeting.</p>
<p>Hình mẫu mới xóa cái khoảng cách đó. Người làm sản phẩm trong thời đại AI không cần spec document vì họ đồng thời là người viết spec và người implement nó, nên ý tưởng đi vào code mà không bị rơi rớt qua bước chuyển giao nào.</p>
<p>Nghe thì giống &quot;full-stack developer&quot;, nhưng full-stack developer vẫn cần team: PM để quyết định ưu tiên, designer để làm UI, DevOps để deploy. Người làm sản phẩm trong thời đại AI có thể một mình giữ toàn bộ vòng lặp đó.</p>
<h2>Thứ AI không thể thay thế</h2>
<p>Hình mẫu này hoạt động được nhờ <em>taste</em>, tức là khả năng biết thứ gì đáng build, thứ gì nên bỏ, và thứ gì đủ tốt để ship. Khả năng code nhanh thì đứng sau.</p>
<p>AI viết code theo hướng dẫn. Nó không biết liệu feature này có cần thiết không, liệu UX này có confusing không, liệu đây có phải là vấn đề thực sự người dùng gặp không. Những câu hỏi đó vẫn cần người trả lời.</p>
<p>Vì vậy, người làm sản phẩm trong thời đại AI cần product sense tốt nhất, còn kỹ thuật thì chỉ cần đủ để không bị implementation chặn lại.</p>
<h2>Điều này thay đổi gì</h2>
<p>Tôi nghĩ đây là một trong những dịch chuyển lớn nhất trong nghề xây dựng phần mềm, nhưng nó không được nói đến đủ nhiều vì người ta đang tập trung vào câu chuyện &quot;AI thay thế developer&quot;.</p>
<p>Theo tôi, chuyện đang xảy ra giống mở rộng hơn là thay thế. Những người trước đây không thể build vì thiếu kỹ năng kỹ thuật thì nay build được, còn developer từng bị giới hạn bởi bandwidth thì nay không còn bị giới hạn như vậy. Đồng thời có một lớp người mới đang xuất hiện, không hẳn là developer cũng không hẳn là founder: người làm sản phẩm có thể ship một mình.</p>
<p>Nếu phải chọn người đáng gờm nhất trong tech hiện tại, tôi sẽ chọn người vừa có product taste vừa biết đủ để ship, hơn là kỹ sư giỏi nhất hay CEO có tầm nhìn nhất.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Khi AI viết code, tôi đang làm gì?</title>
      <link>https://tramdev.io/posts/khi-ai-viet-code-toi-dang-lam-gi</link>
      <guid isPermaLink="true">https://tramdev.io/posts/khi-ai-viet-code-toi-dang-lam-gi</guid>
      <pubDate>Thu, 15 Oct 2026 00:00:00 GMT</pubDate>
      <category>Đạo</category>
      <description>Tôi ngồi nhìn AI viết code và nhận ra tôi không còn là người viết nữa. Vậy thì tôi đang là gì?</description>
      <content:encoded><![CDATA[<p>Tôi gõ một câu mô tả vấn đề, nhấn Enter, rồi ngồi nhìn agent viết code. Có khi vài giây, có khi vài phút, và trong khoảng ấy tôi không gõ thêm gì. Mười ngón tay vẫn đặt trên bàn phím theo thói quen, dù chẳng còn gì để gõ. Lần đầu để ý tới khoảng lặng đó, tôi không gọi được tên cảm giác của mình.</p>
<p>Đầu tôi vẫn chạy, mắt vẫn theo từng dòng code hiện ra, nên gọi là rảnh hay chán thì đều không đúng. Gần nhất có lẽ là cảm giác đang đọc. Chỉ có điều tôi đang đọc một thứ mình chưa từng viết, và nó được viết ra nhanh hơn tốc độ tôi đọc kịp, nên tôi phải chọn chỗ nào đọc kỹ, chỗ nào lướt qua, như lúc phải review một PR vài nghìn dòng trước giờ họp.</p>
<p>Ai từng ngồi ghế phụ cạnh một người đang tập lái chắc nhận ra cảm giác này. Tay không cầm vô lăng, chân không đạp ga, vậy mà xuống xe thì mệt hơn cả tự lái. Người cầm lái là người đang tập, còn người chịu trách nhiệm cho chiếc xe lại là người ngồi bên. Mắt phải nhìn xa hơn người cầm lái một đoạn, còn chân thì lúc nào cũng hờ trên một cái phanh không có thật.</p>
<p>Trước đây tôi quen định nghĩa mình qua việc gõ phím. Code có mặt vì tôi viết ra nó, từng ký tự một. Giờ phần ấy đã có người làm thay, còn việc của tôi là nhìn kết quả và biết nó đúng chưa, còn thiếu gì, lúc nào thì phải hỏi lại. Vai trò vẫn còn đó, chỉ dời sang một chỗ khó nhìn thấy hơn.</p>
<p>Nhịp làm việc vì thế cũng đổi. Thời gian gõ phím ít đi, thời gian đọc nhiều lên: đọc diff, rồi đọc lại yêu cầu mình đã viết để xem agent có hiểu đúng ý không. Đọc code vốn khó hơn viết, vì người đọc phải dựng lại trong đầu những lựa chọn mà người viết đã làm mà không ghi ra. Khi người viết là một thực thể không biết mệt, toàn bộ sức nặng của việc đọc và ngẫm ấy đổ dồn về phía tôi.</p>
<p>Trên công trường, người thợ cả nhiều khi chẳng cầm bay. Họ đi dọc bức tường mới xây, nheo mắt một chút, rồi chỉ vào chỗ lệch mà người khác phải đặt thước mới thấy. Người mới học việc nhìn bức tường ấy chỉ thấy gạch và vữa, còn họ thấy cả những chỗ sẽ nứt sau mùa mưa đầu tiên. Công của họ nằm ở đôi mắt ấy, và đôi mắt ấy có được từ những năm họ tự tay đặt từng viên gạch.</p>
<p>Có một chữ tôi cứ nghĩ tới mãi là taste, khả năng phân biệt thứ tốt với thứ chỉ trông có vẻ tốt. Nó khác với kỹ năng viết code, cũng khác với tốc độ làm xong một tính năng. Một đoạn code chạy được và qua được test vẫn có thể là một lựa chọn sai cho cái hệ thống sẽ phải sống với nó vài năm. Tôi không nghĩ có thể khoán việc này cho AI, và càng ngày tôi càng tin đây là thứ đáng giữ, đáng mài nhất lúc này.</p>
<p>David Hume có một bài luận về chuẩn mực của thị hiếu, viết vào giữa thế kỷ mười tám. Theo ông, người thẩm định đáng tin cần một sự tinh tế được rèn qua luyện tập và hoàn thiện qua so sánh. Tôi thích chữ so sánh ở đó. Muốn biết một abstraction gọn thật hay chỉ gọn ngoài mặt, người ta phải từng thấy đủ nhiều cái gọn thật và cái gọn giả để đặt chúng cạnh nhau.</p>
<p>Vì thế tôi không thấy những năm viết code bằng tay trở nên vô ích. Chính vì từng viết đủ nhiều, tôi mới phân biệt được abstraction tốt với abstraction chỉ trông gọn gàng, và nhờ vậy mới đọc được những gì agent viết ra. Những lần ngồi lần một lỗi do chính mình gây ra, hay viết lại một module vì thiết kế ban đầu không chịu nổi yêu cầu mới, giờ hiện ra ở một chỗ tôi không ngờ, là lúc đọc code của người khác. Thứ kinh nghiệm ấy để lại hoá ra là phán đoán, nhiều hơn là ngón tay gõ nhanh.</p>
<p>Aristotle có nói tới một loại hiểu biết ông gọi là phronesis, thường được dịch là sự khôn ngoan thực tiễn, tức là biết nên làm gì trong một tình huống cụ thể. Ông nhận xét người trẻ có thể giỏi hình học và toán, nhưng khó có được thứ khôn ngoan này, vì nó cần kinh nghiệm với rất nhiều trường hợp riêng lẻ. Đọc đoạn ấy, tôi nghĩ tới việc review code. Thuộc các nguyên tắc thiết kế thì nhanh, còn biết nguyên tắc nào nên phá ở đúng chỗ này thì phải mất nhiều năm.</p>
<p>Michael Polanyi có một câu hay được nhắc: chúng ta biết nhiều hơn những gì ta nói ra được. Taste trong code có lẽ thuộc loại hiểu biết ấy. Nhiều khi tôi thấy một đoạn code có gì đó không ổn trước khi giải thích được vì sao. Lời giải thích chỉ tới sau vài lượt đọc lại, khi tôi chỉ ra được cái mà người trong nghề hay gọi là &quot;có mùi&quot;, như một hàm biết quá nhiều về những thứ xung quanh nó, hay một điều kiện bị lặp lại ở vài chỗ.</p>
<p>Cái khó nhất với tôi hoá ra lại là những lúc mọi thứ êm. Khi agent làm đúng nhiều lần liền, tôi bắt đầu đọc nhanh hơn, lướt hơn, và có khi gật đầu trước cả lúc đọc hết. Người ngồi ghế phụ cũng thế, người tập lái chạy êm được một quãng dài là lúc họ bắt đầu nhìn ra cửa sổ. Chiếc xe thì không biết người ngồi cạnh đã thôi để ý.</p>
<p>Để giữ con mắt ấy, thỉnh thoảng tôi vẫn tự viết một đoạn khó từ đầu, dù biết agent làm nhanh hơn. Thường đó là một chỗ xử lý đồng thời, hoặc một đoạn migrate dữ liệu mà sai một dòng là phải khôi phục từ backup. Viết xong, tôi đem so với bản agent viết, và thường thấy ít nhất một chỗ hai bên chọn khác nhau. Những chỗ khác nhau ấy chính là kiểu so sánh Hume nói tới, chỉ là giờ nó nằm gọn trong một cái diff.</p>
<p>Cũng có lúc tôi thấy hơi bất an. Người thợ cả có con mắt ấy nhờ những năm đặt gạch, còn một người mới vào nghề hôm nay, bắt đầu từ ghế phụ, sẽ học nhìn từ đâu? Tôi không lo cho mình nhiều, vì con mắt của tôi đã được mài từ trước khi có agent. Tôi chỉ hơi lo cho những người vào nghề sau này, những người sẽ quen nhìn code tốt hiện lên ngay từ đầu, trước khi kịp nếm trải thế nào là viết ra những dòng code dở.</p>
<p>Người vào nghề trước đây học nhìn qua những lần PR bị trả về với cả chục comment. Có những lỗi chỉ thấm khi chính mình viết ra rồi bị người khác chỉ vào, như một vòng lặp gọi database ở từng lượt, hay một biến dùng chung khiến hai request giẫm nhau. Những bài học ấy đến từ code dở do chính tay mình viết. Tôi nghĩ phần viết dở ấy là chỗ không bỏ được, và tôi chưa biết nghề mình sẽ giữ nó lại bằng cách nào.</p>
<p>Có một thứ thì không dời đi đâu cả, là cái tên trên commit. Agent viết, nhưng người bấm merge là tôi, và khi hệ thống lỗi lúc nửa đêm thì người bị gọi dậy cũng là tôi. Lịch sử git sẽ ghi tên tôi ở dòng approve, và một năm sau, ai đọc lại đoạn code ấy sẽ tìm tới tôi để hỏi vì sao nó được viết như thế. Cái tên ấy làm việc ngồi nhìn nặng hơn nhiều so với vẻ ngoài của nó.</p>
<p>Một người review code giỏi có lẽ gần nhất với việc tôi đang làm bây giờ. Họ không có tên trong phần lớn commit, nhưng một codebase tốt thường mang dấu tay của họ ở những chỗ người dùng không thấy, như một abstraction bị gỡ đi trước khi kịp lan ra, hay một trường hợp biên được hỏi lại. Họ đọc code của người khác kỹ hơn cả người viết đọc code của chính mình, vì trong đầu họ không có sẵn điều người viết định làm. Người dùng chỉ thấy sản phẩm chạy, còn những lần nó suýt hỏng thì chỉ người review biết.</p>
<p>Nên giờ, khi ngồi nhìn agent viết code, tôi biết mình đang làm phần việc khó nhất của nghề này, là nghĩ. Việc ấy khó thấy hơn gõ phím, và mệt theo một kiểu khác. Màn hình vẫn chạy chữ, tôi vẫn ngồi yên, và mắt tôi luôn nhìn trước người cầm lái một đoạn.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Một cách học mới, brainstorm với AI</title>
      <link>https://tramdev.io/posts/mot-cach-hoc-moi-brainstorm-voi-ai</link>
      <guid isPermaLink="true">https://tramdev.io/posts/mot-cach-hoc-moi-brainstorm-voi-ai</guid>
      <pubDate>Thu, 15 Oct 2026 00:00:00 GMT</pubDate>
      <category>AIkisu</category>
      <description>Mình đã hỏi AI rất nhiều, nhưng gần đây mới nhận ra sự khác biệt giữa hỏi để lấy câu trả lời và thực sự brainstorm cùng AI.</description>
      <content:encoded><![CDATA[<p>Gần đây mình bắt đầu nhận ra sự hữu ích thực sự của việc brainstorm với AI. Mình đã hỏi AI rất nhiều từ trước, nhưng hồi đó chỉ nghĩ đơn giản là hỏi để lấy câu trả lời, như một cái search engine biết nói chuyện.</p>
<p>Đợt này mình tìm hiểu về Protocol Buffers (proto), định dạng gRPC dùng để định nghĩa service. Mình mở Claude Code lên và prompt: &quot;Help me to learn proto quickly.&quot; Claude bắt đầu giới thiệu sơ lược, từ syntax cơ bản đến cách định nghĩa service. Mình đọc, và rồi bắt đầu thắc mắc. Tại sao phải define schema trước? Điều gì xảy ra nếu client và server dùng version proto khác nhau? Mình hỏi lại, Claude giải thích, mình hỏi tiếp.</p>
<p>Lúc đó mình mới để ý rằng phần có ích nhất là những câu hỏi mình tự đặt ra, nhiều hơn là câu trả lời của Claude.</p>
<p>Đặt câu hỏi là một kỹ năng. Mỗi kiến thức được đưa ra, nếu mình chỉ đọc và gật đầu thì nó sẽ trôi qua. Nhưng nếu mình nhìn nó từ nhiều góc độ, đặt nó vào bối cảnh khác nhau, tự hỏi &quot;cái này liên quan thế nào đến thứ mình đã biết&quot; hay &quot;trong trường hợp nào thì cách này không dùng được&quot;, thì kiến thức đó bắt đầu có cấu trúc trong đầu.</p>
<p>Mình cũng làm tương tự khi tìm hiểu một tài liệu thiết kế hệ thống. Thay vì đọc từ đầu đến cuối, mình đọc từng phần nhỏ rồi mở Claude lên nói chuyện. &quot;Tại sao họ chọn message queue ở đây thay vì gọi thẳng?&quot; &quot;Nếu service này down thì phần còn lại bị ảnh hưởng thế nào?&quot; Mục đích là buộc mình tự hỏi trước, rồi mới để Claude trả lời.</p>
<p>AI làm cho việc suy nghĩ dễ bắt đầu hơn, còn suy nghĩ thì mình vẫn phải tự làm. Khi học một mình, đôi lúc mình không biết mình nên hỏi cái gì. Có AI ở đó, mình có nơi để nói ra những thứ còn mơ hồ, và từ câu trả lời của nó, mình mới biết mình cần hỏi tiếp cái gì.</p>
<p>Cách học này không nhanh hơn đọc docs, nhưng những gì ở lại trong đầu sau đó thì nhiều hơn hẳn.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Một cách làm đúng</title>
      <link>https://tramdev.io/posts/mot-cach-lam-dung</link>
      <guid isPermaLink="true">https://tramdev.io/posts/mot-cach-lam-dung</guid>
      <pubDate>Thu, 15 Oct 2026 00:00:00 GMT</pubDate>
      <category>Đạo</category>
      <description>Thử nhiều cách cùng lúc trông có vẻ thận trọng. Nhưng thường thì nó chỉ là cách trì hoãn việc quyết định.</description>
      <content:encoded><![CDATA[<p>Có một ngụ ngôn cũ, thường được gắn với tên Buridan, kể về một con lừa đứng giữa hai đống cỏ khô giống hệt nhau. Nó đói, hai đống cỏ ở ngay trước mặt, nhưng vì chẳng có lý do gì để chọn đống này hơn đống kia nên nó cứ đứng đó cho tới khi chết đói. Lần đầu nghe, tôi thấy chuyện hơi vô lý. Nghĩ lại mới thấy, câu chuyện ấy giống hệt tôi lúc này, chỉ là tôi không muốn thừa nhận.</p>
<p>Khi chưa chắc nên làm một tính năng theo cách nào, tôi hay mở một branch cho cách này, một branch cho cách kia, rồi đọc thêm vài bài, hỏi thêm vài người. Nhìn từ ngoài vào thì rất kỹ lưỡng. Thực ra, phần lớn những lần như vậy tôi đã mang máng biết từ đầu cách nào khả năng cao sẽ đúng. Chỉ là tôi cố tình chưa chịu chọn, vì chọn rồi mà lỡ sai thì trách nhiệm hoàn toàn thuộc về mình.</p>
<p>Chữ &quot;decide&quot; trong tiếng Anh có gốc Latin nghĩa là cắt bỏ. Tôi thích cách hiểu ấy, vì nó giải thích được tại sao quyết định vẫn khó cả khi lựa chọn đã khá rõ. Chọn một cách đồng nghĩa với việc bỏ những cách kia lại, mà tôi thì không giỏi bỏ thứ gì lại.</p>
<p>Những chiếc branch bỏ dở có một đặc điểm buồn cười là chúng chẳng bao giờ biết đến thất bại. Những chiếc branch dừng lại ngay khi vừa qua phần dễ lúc nào trông cũng đầy hứa hẹn, bởi nó chưa hề phải chạm mặt dữ liệu thật hay chịu cảnh chạy chung với code của người khác. Người ta cũng hay giữ những phiên bản khác của đời mình theo kiểu ấy, chẳng hạn cái nghề lẽ ra đã theo hay thành phố lẽ ra đã sống. Mấy phiên bản đó đẹp một phần vì chưa bao giờ phải sống thử một ngày thứ Hai nào.</p>
<p>Triết gia Kierkegaard từng ví von rằng lo âu thực chất là cơn chóng mặt của sự tự do (&quot;Anxiety is the dizziness of freedom&quot;). Tôi nghĩ tới câu đó mỗi khi ngồi trước một màn hình trống và biết mình làm theo cách nào cũng được. Khi còn chưa chọn, tôi chưa phải chịu trách nhiệm cho cách nào cả. Cảm giác đó dễ chịu thật (tôi không giả vờ là không), và có lẽ vì thế mà tôi kéo dài nó.</p>
<p>Cái giá của nó là tôi cũng chẳng biết thêm được gì. Một cách làm chỉ lộ ra nó đúng hay sai khi đi gần tới cuối, lúc nó gặp những trường hợp không ai vẽ lên bảng. Ba branch dừng giữa chừng cho tôi ba lần cùng một thứ hiểu biết, loại hiểu biết có được ngay từ buổi đầu.</p>
<p>Thợ sửa đồ lâu năm làm việc theo một kiểu mà người ngoài thấy chậm. Gặp cái quạt hỏng, họ thử từng chỗ một, vì thay ba thứ cùng lúc mà quạt chạy lại thì cũng chẳng biết nó hỏng ở đâu. Ai từng thức khuya lần ra lỗi chỉ hiện ra trên máy khách hàng đều quen cách làm này, và biết nó nhanh hơn vẻ ngoài của nó.</p>
<p>Cân nhắc vẫn có chỗ của nó. Người đi thuê nhà phải xem vài căn, vì nắng buổi chiều với tiếng ồn ban đêm đâu có hiện lên trong ảnh. Chỉ là tới một lúc nào đó, so sánh thêm chẳng cho tôi biết gì mới, và tôi vẫn tiếp tục so sánh vì nó cho tôi một chỗ để trốn.</p>
<p>Để biết mình đã tới lúc đó chưa, tôi hay tự hỏi: nếu sáng mai phải trả lời thì mình nói gì? Nếu câu trả lời bật ra ngay, nghĩa là việc so sánh đã xong từ lâu rồi, tôi chỉ chưa chịu nhận.</p>
<p>Một điều giúp tôi can đảm chọn hơn, đó là chẳng có quyết định nào mang tính vĩnh viễn. Một module viết theo cách sai, nếu đã đi tới cùng, thường chỉ mất vài tuần để thay, và lúc thay thì tôi đã biết rõ nó sai ở đâu. Đổi việc hay chuyển nhà cũng vậy, mỗi lần như thế người ta đứng ở một chỗ khác và nhìn bước tiếp theo rõ hơn một chút.</p>
<p>Có một chuyện nữa tôi để ý. Người ta chỉ thật sự dồn tâm sức chăm chút khi đã chốt xong một ngả rẽ, giống như căn nhà thuê, phải dọn vào rồi mới thong thả treo rèm cửa. Với code cũng thế, branch tôi biết sẽ được giữ lại mới là branch tôi chịu viết test và đặt tên cho đàng hoàng. Mấy branch còn lại thì viết tạm cho xong, vì biết đâu ngày mai lại bỏ.</p>
<p>Nhà Thiền có câu &quot;khi đi thì đi, khi ngồi thì ngồi&quot;. Tôi từng nghĩ câu đó dành cho người tu, chẳng liên quan gì tới một ngày ngồi viết code. Giờ tôi thấy nó tả khá đúng những ngày làm việc mà tôi thấy ổn nhất, những ngày tôi chỉ làm một việc và làm cho tới khi xong.</p>
<p>Con lừa của Buridan chết đói giữa hai đống cỏ mà đống nào cũng đủ cho nó sống. Nó chờ một lý do để chọn, và lý do đó không bao giờ tới. Mỗi lần thấy mình đứng giữa hai branch quá lâu, tôi lại nhớ tới nó, rồi chọn cái mình đã nghiêng về từ đầu.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Cache-Control: Hiểu đúng để tối ưu hiệu năng hệ thống</title>
      <link>https://tramdev.io/posts/cache-control-hieu-dung-de-toi-uu</link>
      <guid isPermaLink="true">https://tramdev.io/posts/cache-control-hieu-dung-de-toi-uu</guid>
      <pubDate>Sat, 28 Mar 2026 00:00:00 GMT</pubDate>
      <category>Devlog</category>
      <description>Cache-Control là HTTP header quyết định response được lưu và dùng lại thế nào. Đặt đúng thì nhanh hơn đáng kể, đặt sai thì sinh ra hàng loạt bug khó chịu.</description>
      <content:encoded><![CDATA[<p>Cache là một trong những cách hiệu quả nhất để tăng hiệu năng cho web. Nhưng dùng sai thì nó gây ra rất nhiều bug khó chịu: dữ liệu cũ, UI không cập nhật, hoặc người dùng thấy nội dung khác nhau.</p>
<h2>1. Cache-Control là gì?</h2>
<p><code>Cache-Control</code> là HTTP header chỉ định cách một response được lưu cache và khi nào được phép dùng lại. Server gửi header này cho client (browser, CDN, proxy).</p>
<p>Ví dụ:</p>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">http</span></div><pre><code class="hljs language-http"><span class="hljs-attribute">Cache-Control</span><span class="hljs-punctuation">: </span>public, max-age=0, must-revalidate</code></pre></div><h2>2. Các directive quan trọng</h2>
<h3>2.1 <code>public</code> vs <code>private</code></h3>
<ul>
<li><code>public</code> cho phép mọi cache lưu lại, gồm browser, CDN và proxy.</li>
<li><code>private</code> chỉ cho cache ở browser của user, không cache ở CDN.</li>
</ul>
<p>Quy tắc chung: static assets dùng <code>public</code>, data cá nhân dùng <code>private</code>.</p>
<h3>2.2 <code>max-age</code></h3>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">http</span></div><pre><code class="hljs language-http"><span class="hljs-attribute">Cache-Control</span><span class="hljs-punctuation">: </span>max-age=3600</code></pre></div><p>Response được coi là fresh trong 3600 giây (1 giờ). Trong thời gian này không cần gọi server, cache được dùng trực tiếp. Đây là cách tối ưu performance mạnh nhất.</p>
<h3>2.3 <code>no-cache</code></h3>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">http</span></div><pre><code class="hljs language-http"><span class="hljs-attribute">Cache-Control</span><span class="hljs-punctuation">: </span>no-cache</code></pre></div><p>Cái tên dễ làm hiểu nhầm thành &quot;không cache&quot;. Thực ra response vẫn được cache, chỉ là phải kiểm tra với server trước khi dùng.</p>
<h3>2.4 <code>no-store</code></h3>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">http</span></div><pre><code class="hljs language-http"><span class="hljs-attribute">Cache-Control</span><span class="hljs-punctuation">: </span>no-store</code></pre></div><p>Không lưu cache ở bất kỳ đâu. Dùng cho thông tin nhạy cảm như banking, auth.</p>
<h3>2.5 <code>must-revalidate</code></h3>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">http</span></div><pre><code class="hljs language-http"><span class="hljs-attribute">Cache-Control</span><span class="hljs-punctuation">: </span>must-revalidate</code></pre></div><p>Khi cache hết hạn, bắt buộc phải hỏi lại server, không được dùng cache cũ.</p>
<h3>2.6 <code>s-maxage</code> và <code>stale-while-revalidate</code></h3>
<p>Khi có CDN đứng giữa, hai directive này rất hay dùng:</p>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">http</span></div><pre><code class="hljs language-http"><span class="hljs-attribute">Cache-Control</span><span class="hljs-punctuation">: </span>public, max-age=60, s-maxage=600, stale-while-revalidate=30</code></pre></div><ul>
<li><code>s-maxage</code> là thời gian fresh riêng cho cache dùng chung (CDN, proxy), ghi đè <code>max-age</code> ở đó.</li>
<li><code>stale-while-revalidate</code> cho phép trả bản cũ thêm 30 giây sau khi hết hạn, trong lúc cache lấy bản mới ở background.</li>
</ul>
<p>Với ví dụ trên, browser giữ 60 giây, CDN giữ 10 phút, và người dùng không phải chờ khi CDN làm mới.</p>
<h2>3. Hiểu sâu qua ví dụ thực tế</h2>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">http</span></div><pre><code class="hljs language-http"><span class="hljs-attribute">Cache-Control</span><span class="hljs-punctuation">: </span>public, max-age=0, must-revalidate</code></pre></div><p>Ý nghĩa:</p>
<ul>
<li><code>public</code>: cache được lưu ở mọi nơi</li>
<li><code>max-age=0</code>: hết hạn ngay lập tức</li>
<li><code>must-revalidate</code>: phải check server trước khi dùng</li>
</ul>
<p>Gộp lại: response có thể cache, nhưng mỗi lần dùng phải hỏi server xem có bản mới không.</p>
<h2>4. Cơ chế revalidation</h2>
<p>Khi cache cần kiểm tra lại, nó dùng ETag hoặc Last-Modified.</p>
<h3>ETag</h3>
<p>Server trả:</p>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">http</span></div><pre><code class="hljs language-http"><span class="hljs-attribute">ETag</span><span class="hljs-punctuation">: </span>&quot;abc123&quot;</code></pre></div><p>Client gửi lại:</p>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">http</span></div><pre><code class="hljs language-http"><span class="hljs-attribute">If-None-Match</span><span class="hljs-punctuation">: </span>&quot;abc123&quot;</code></pre></div><p>Nếu không thay đổi:</p>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">http</span></div><pre><code class="hljs language-http">HTTP 304 Not Modified</code></pre></div><p>Server không cần gửi lại body, nên tiết kiệm được bandwidth.</p>
<h3>Last-Modified</h3>
<p>Tương tự:</p>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">http</span></div><pre><code class="hljs language-http"><span class="hljs-attribute">Last-Modified</span><span class="hljs-punctuation">: </span>Thu, 26 Mar 2026 10:00:00 GMT</code></pre></div><p>Client:</p>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">http</span></div><pre><code class="hljs language-http"><span class="hljs-attribute">If-Modified-Since</span><span class="hljs-punctuation">: </span>...</code></pre></div><h2>5. Các chiến lược cache phổ biến</h2>
<h3>5.1 Static assets (JS, CSS, images)</h3>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">http</span></div><pre><code class="hljs language-http"><span class="hljs-attribute">Cache-Control</span><span class="hljs-punctuation">: </span>public, max-age=31536000, immutable</code></pre></div><p>Cache 1 năm và không bao giờ revalidate, dùng kèm filename hash (<code>app.abc123.js</code>). Đây là cấu hình cho performance tốt nhất.</p>
<h3>5.2 HTML (dynamic content)</h3>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">http</span></div><pre><code class="hljs language-http"><span class="hljs-attribute">Cache-Control</span><span class="hljs-punctuation">: </span>public, max-age=0, must-revalidate</code></pre></div><p>Luôn check server, nhưng vẫn tận dụng được 304. Phù hợp với SSR và content thay đổi thường xuyên.</p>
<p>Nếu HTML chứa dữ liệu của từng user (dashboard, trang tài khoản) thì <strong>không dùng <code>public</code></strong>: CDN có thể lưu trang của người này và trả cho người khác. Dùng:</p>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">http</span></div><pre><code class="hljs language-http"><span class="hljs-attribute">Cache-Control</span><span class="hljs-punctuation">: </span>private, no-cache</code></pre></div><h3>5.3 API responses</h3>
<h4>Trường hợp real-time</h4>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">http</span></div><pre><code class="hljs language-http"><span class="hljs-attribute">Cache-Control</span><span class="hljs-punctuation">: </span>no-store</code></pre></div><h4>Trường hợp có thể cache nhẹ</h4>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">http</span></div><pre><code class="hljs language-http"><span class="hljs-attribute">Cache-Control</span><span class="hljs-punctuation">: </span>public, max-age=60</code></pre></div><p>Ví dụ list sản phẩm, hoặc data không thay đổi liên tục.</p>
<h3>5.4 User-specific data</h3>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">http</span></div><pre><code class="hljs language-http"><span class="hljs-attribute">Cache-Control</span><span class="hljs-punctuation">: </span>private, max-age=300</code></pre></div><p>Cache ở browser và không leak qua CDN.</p>
<h2>6. Những sai lầm phổ biến</h2>
<h3>1. Dùng <code>no-cache</code> khi muốn &quot;không cache&quot;</h3>
<p><code>no-cache</code> vẫn cache. Muốn không cache thì phải dùng <code>no-store</code>.</p>
<h3>2. Không version static assets</h3>
<p>Người dùng bị kẹt với file cũ. Luôn đặt hash vào tên file:</p>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">text</span></div><pre><code class="hljs language-text">app.[hash].js</code></pre></div><h3>3. Cache API mà không nghĩ về consistency</h3>
<p>User thấy dữ liệu cũ, và bug kiểu này rất khó debug.</p>
<h3>4. Không hiểu CDN cache</h3>
<p>CDN có thể cache ngay cả khi bạn không để ý, nên cần đặt rõ <code>public</code> hay <code>private</code>.</p>
<h3>5. Dùng <code>public</code> cho trang có dữ liệu user</h3>
<p>CDN trả dashboard của user này cho user khác. Trang nào phụ thuộc vào người đang đăng nhập thì dùng <code>private</code>.</p>
<h2>7. Best Practices (tóm tắt)</h2>
<h3>Static assets</h3>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">http</span></div><pre><code class="hljs language-http">public, max-age=31536000, immutable</code></pre></div><h3>HTML</h3>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">http</span></div><pre><code class="hljs language-http">public, max-age=0, must-revalidate</code></pre></div><h3>API</h3>
<ul>
<li>Real-time: <code>no-store</code></li>
<li>Semi-static: <code>max-age=60</code></li>
</ul>
<h3>Sensitive data</h3>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">http</span></div><pre><code class="hljs language-http">no-store</code></pre></div><h3>Luôn dùng versioning</h3>
<p>Hash filename để tránh cache bug.</p>
<h2>8. Ví dụ: site tĩnh trên Cloudflare Pages</h2>
<p>Phần trên khá lý thuyết, nên mình thêm một ví dụ cụ thể: một site Next.js export tĩnh deploy lên Cloudflare Pages. Cloudflare Pages đọc header tuỳ chỉnh từ file <code>_headers</code> đặt trong thư mục output (với Next.js là <code>public/_headers</code>, khi build sẽ được copy ra output).</p>
<p>Next.js đặt mọi JS và CSS đã build trong <code>/_next/static/</code>, và tên file có hash. Nội dung đổi thì tên file đổi, nên các file này cache được rất lâu:</p>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">text</span></div><pre><code class="hljs language-text"># public/_headers
/_next/static/*
  Cache-Control: public, max-age=31536000, immutable</code></pre></div><p>HTML thì ngược lại: tên file không đổi (<code>/index.html</code>, <code>/posts/abc/index.html</code>), nên phải luôn kiểm tra lại với server. Mình không thêm rule <code>/*</code> cho HTML vào file này. Cloudflare Pages gộp header của mọi rule khớp với một URL, và nếu hai rule cùng đặt <code>Cache-Control</code> thì hai giá trị bị nối bằng dấu phẩy. Một file trong <code>/_next/static/</code> khớp cả <code>/*</code> lẫn <code>/_next/static/*</code> sẽ nhận header kiểu <code>public, max-age=0, must-revalidate, public, max-age=31536000, immutable</code>.</p>
<p>Nếu thật sự cần đặt header cho mọi đường dẫn, dùng cú pháp <code>!</code> để gỡ header của rule rộng hơn trước khi đặt lại:</p>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">text</span></div><pre><code class="hljs language-text">/*
  Cache-Control: public, max-age=0, must-revalidate

/_next/static/*
  ! Cache-Control
  Cache-Control: public, max-age=31536000, immutable</code></pre></div><p>Sau khi deploy, kiểm tra header thực tế bằng <code>curl</code>:</p>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">bash</span></div><pre><code class="hljs language-bash">curl -sI https://example.com/ | grep -i cache-control
curl -sI https://example.com/_next/static/chunks/main-abc123.js | grep -i cache-control</code></pre></div><p>Thay <code>example.com</code> bằng domain của bạn và đường dẫn file JS bằng một file thật lấy từ tab Network của trình duyệt.</p>
<h2>Kết</h2>
<p>Cache-Control nhìn thì đơn giản nhưng ảnh hưởng rất lớn đến hệ thống. Đặt đúng thì giảm load cho server và app chạy nhanh hơn, đồng thời tránh được hàng loạt bug khó chịu kể trên.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Tổng hợp và đóng gói kiến thức</title>
      <link>https://tramdev.io/posts/tong-hop-va-dong-goi-kien-thuc</link>
      <guid isPermaLink="true">https://tramdev.io/posts/tong-hop-va-dong-goi-kien-thuc</guid>
      <pubDate>Sat, 28 Mar 2026 00:00:00 GMT</pubDate>
      <category>Đạo</category>
      <description>Tôi làm nhiều, học nhiều, nhưng phần lớn những gì học được không ở lại. Thứ tôi thiếu là một hệ thống để tổng hợp và đóng gói kiến thức.</description>
      <content:encoded><![CDATA[<p>Có một điều tôi nhận ra mà thấy hơi đau. Tôi làm nhiều, học nhiều, giải quyết không ít vấn đề khó, vậy mà phần lớn những thứ ấy cứ trôi đi, không ở lại với tôi được bao lâu.</p>
<p>Một ngày của tôi có thể gồm debug một bug khó, thiết kế một flow mới, đọc tài liệu và thử một công nghệ. Việc nào cũng có giá trị, việc nào cũng dạy tôi một điều. Nhưng xong việc là tôi chuyển sang việc khác và gần như không bao giờ quay lại. Không có gì được tổng hợp hay đóng gói, nên cũng chẳng có gì thành tài sản.</p>
<p>Tôi đang tiêu thụ tri thức nhiều hơn là tích luỹ nó. Tôi vẫn học đều, chỉ chưa có cách nào giữ lại những gì đã học.</p>
<p>Lúc đầu tôi nghĩ đơn giản là mình lười ghi lại. Càng để ý, tôi càng thấy chuyện sâu hơn thế. Công việc có điểm kết thúc rõ ràng, task xong là xong. Tri thức thì chẳng bao giờ được đánh dấu là xong, nên nó cứ lơ lửng một thời gian rồi biến mất.</p>
<p>Chuyện thường được kể là Kurt Lewin và học trò của ông, Bluma Zeigarnik, để ý thấy người phục vụ ở một quán ăn tại Berlin nhớ rất kỹ những đơn chưa thanh toán, rồi quên gần như ngay khi khách trả tiền. Zeigarnik sau đó làm thí nghiệm và thấy người ta nhớ những việc bị bỏ dở tốt hơn những việc đã làm xong. Những năm ấy là cuối thập niên 1920.</p>
<p>Tôi nghĩ đầu óc mình cũng làm y như người phục vụ đó. Task đóng lại là một đơn đã thanh toán, và những gì học được trong lúc làm bị gạch đi cùng với nó. Cái cảm giác &quot;xong rồi&quot; dễ chịu ấy lại đúng là lúc tôi bắt đầu quên.</p>
<p>Mỗi lần định ghi lại, tôi lại phải nghĩ từ đầu: ghi gì, ghi kiểu gì, ghi cho ai. Quá nhiều quyết định nhỏ, và đầu óc chọn cách dễ hơn là bỏ qua. Việc ghi lại cũng không giúp task đang làm xong sớm hơn, nên lúc nào nó cũng bị xếp sau.</p>
<p>Người hay quên chìa khoá thường tự nhủ lần sau phải nhớ cho kỹ, rồi lần sau vẫn quên. Người hết quên thường là người đóng một cái móc ngay cạnh cửa. Trí nhớ của họ chẳng tốt hơn trước, chỉ là việc treo chìa khoá giờ không cần ai phải quyết định nữa. Chuyện ghi chép của tôi cũng vậy, vấn đề nằm ở hệ thống nhiều hơn là ở động lực hay kỷ luật.</p>
<p>Niklas Luhmann, một nhà xã hội học người Đức, giữ một hộp phiếu ghi chép suốt mấy chục năm, tới cuối đời có khoảng chín mươi nghìn tấm. Mỗi tấm ghi một ý, được đánh số và trỏ sang những tấm có liên quan. Ông viết khoảng bảy mươi cuốn sách, và có lần nói rằng ông không tự nghĩ hết mọi thứ, phần lớn việc nghĩ diễn ra trong cái hộp ấy.</p>
<p>Điều tôi để ý ở Luhmann là cái hộp không đòi ông phải có hứng. Mỗi tấm phiếu nhỏ tới mức viết xong trong vài phút, và chỗ của nó trong hộp đã có sẵn. Ông không phải quyết định lại từ đầu mỗi lần muốn ghi một điều gì.</p>
<p>Nhận ra những điều đó, tôi bắt đầu nhìn việc mình làm theo cách khác. Trước đây làm xong task là xong. Bây giờ làm xong task nghĩa là tôi vừa có thêm nguyên liệu, có thể là một insight, một cách giải quyết, hay một pattern dùng lại được. Nếu không giữ, vài tuần sau nó chỉ còn là một cảm giác mơ hồ rằng mình từng làm được việc này.</p>
<p>Nhiều nhà có một cuốn sổ chép tay công thức nấu ăn, mép giấy ố vàng, vài trang dính dầu mỡ. Người chép không định in nó thành sách. Họ chép để lần sau nấu món ấy không phải gọi điện hỏi lại ai, và nhờ thế cuốn sổ cứ dày dần qua nhiều năm.</p>
<p>Tôi cũng đổi lý do để viết. Trước đây cứ nghĩ tới chuyện viết là tôi thấy áp lực, viết có hay không, có ai đọc không. Bây giờ tôi viết để lần sau không phải nghĩ lại từ đầu. Mục tiêu thực dụng hơn thì việc viết nhẹ đi rất nhiều, và chia sẻ chỉ còn là chuyện đến sau.</p>
<p>Tôi không mong mọi thứ đổi ngay. Nếu giữ được thói quen này vài tháng, tôi hy vọng có một kho kiến thức của riêng mình, những vấn đề đã giải quyết không phải làm lại từ đầu, và cách nghĩ rõ hơn nhờ viết. Điều tôi muốn nhất là bắt đầu tích luỹ được từ những gì mình đã làm.</p>
<p>Tôi hình dung nó như một vòng quay: làm, ghi lại, lọc bớt, chia sẻ, rồi lặp lại. Làm mà không ghi thì những gì học được trôi đi. Ghi mà không lọc thì kho ghi chép nặng dần tới mức chẳng muốn mở ra, còn lọc xong mà chỉ giữ cho riêng mình thì nó khó lớn thêm.</p>
<p>Bài này cũng là một bước nhỏ trong vòng quay ấy. Viết xong, tôi đánh dấu nó là xong, một trong số ít lần tri thức của tôi được đánh dấu như vậy.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Cứ chia sẻ đi</title>
      <link>https://tramdev.io/posts/cu-chia-se-di</link>
      <guid isPermaLink="true">https://tramdev.io/posts/cu-chia-se-di</guid>
      <pubDate>Wed, 25 Mar 2026 00:00:00 GMT</pubDate>
      <category>Đạo</category>
      <description>Cái tôi nghĩ là quá đơn giản để chia sẻ, với người khác lại là đúng thứ họ đang tìm.</description>
      <content:encoded><![CDATA[<p>Có một dạo, tôi học được rất nhiều nhưng chẳng bao giờ chia sẻ với ai cả. Trong đầu rõ là có nhiều thứ để nói, chỉ là thấy chúng nhỏ nhặt quá, chưa đủ tầm vóc, hay chưa đáng để đem ra ngoài. Thế là mọi thứ cứ nằm lì ở đấy, rồi trôi đi đâu mất.</p>
<p>Lúc đầu tôi nghĩ giữ lại là để tích luỹ. Một thời gian sau tôi mới thấy, những điều cứ giữ khư khư cho mình thì không bao giờ thật sự sống. Nó giống nước, chảy thì trong, đứng lâu một chỗ thì đục. Kiến thức giữ quá lâu cũng vậy, càng để lâu càng nặng, mà chưa chắc đã sâu thêm chút nào.</p>
<p>Ai từng giảng bài cho một đứa em chắc đều nhớ cảm giác này. Bài toán mình làm được từ lâu, tưởng giảng năm phút là xong, vậy mà tới bước thứ ba thì đứng lại, vì đứa em hỏi &quot;sao lại thế&quot; và mình không trả lời được. Hoá ra mình biết làm, nhưng chưa biết vì sao.</p>
<p>Seneca từng viết trong một lá thư rằng người ta học được nhiều nhất là khi đi dạy. Đến khi bắt đầu chia sẻ, tôi mới thấm câu nói ấy. Tôi tưởng mình đang cho đi, rồi nhận ra mình đang hiểu lại. Lúc viết ra, những chỗ từng mơ hồ bắt đầu rõ nét, còn những chỗ tưởng đã hiểu thì hoá ra vẫn còn hổng, và phải đến tận khi viết xong, điều đó mới thực sự thành của mình.</p>
<p>Rào cản lớn nhất vẫn là câu &quot;cái này đơn giản quá&quot;. Mất một thời gian tôi mới hiểu, câu nói ấy thực ra chỉ vạch ra khoảng cách của người đã đi trước, chứ chẳng nói lên việc kiến thức đó nông hay sâu. &quot;Đơn giản&quot; là cách nhìn của người đã biết. Người đã biết hay quên rằng mình từng không biết, vì chính sự hiểu đã xoá mất những chỗ vướng ban đầu.</p>
<p>Năm 1990, Elizabeth Newton, khi đó là nghiên cứu sinh ở Stanford, làm một thí nghiệm nhỏ. Một người gõ nhịp một bài hát quen thuộc lên mặt bàn, người kia đoán tên bài. Người gõ đoán khoảng một nửa số bài sẽ được nhận ra, còn thực tế thì chỉ ba trên một trăm hai mươi bài. Người gõ nghe cả giai điệu vang trong đầu mình, trong khi người nghe chỉ có tiếng cộc cộc trên bàn.</p>
<p>Trong kinh tế học và tâm lý học, người ta gọi đây là &quot;lời nguyền của tri thức&quot;: khi đã biết rõ một điều, ta gần như quên mất cảm giác của chính mình lúc chưa hề biết đến nó. Cái &quot;đơn giản quá&quot; của tôi nhiều khi chỉ là giai điệu trong đầu người gõ.</p>
<p>Người địa phương chỉ đường cho khách lạ cũng hay vướng đúng chỗ ấy. &quot;Cứ đi thẳng, tới chỗ cây xăng cũ thì rẽ trái&quot;, trong khi cây xăng đã dẹp từ mấy năm trước và chỉ người trong xóm còn nhớ. Với người lạ, câu chỉ đường được nói chậm và rõ từng ngã rẽ mới là thứ giúp họ tới nơi.</p>
<p>Với người chưa biết, cùng một điều ấy có thể là cánh cửa đầu tiên. Thứ họ cần thường chỉ là một điều thật, nói ra từ chỗ người viết đang đứng. Có khi chỉ một dòng suy nghĩ cũng mở ra cho ai đó một hướng đi.</p>
<p>Người ta hay nói về chuyện xây dựng cộng đồng như một chiến lược. Tôi lại thấy nó giống một xóm cũ hơn, nơi nhà này có rổ ổi chín thì mang sang nhà kia, nhà kia nấu nồi chè thì để phần một bát. Chẳng ai lên kế hoạch cho những lần qua lại ấy. Cái xóm thành hình từ những điều giản dị thế thôi: người ta cứ đều đặn chia nhau những thứ nhỏ nhặt mình đang có.</p>
<p>Trên mạng cũng vậy. Một cộng đồng thường hình thành quanh một người nói điều mình đang nghĩ, làm việc mình đang làm, và không cố tỏ ra hơn chính mình. Những người hợp nhau sẽ tự tìm đến. Thứ giữ họ ở lại là sự chân thật, chứ không phải vì người viết nói đúng hay sai.</p>
<p>Lão Tử viết rằng điều thiện cao nhất giống như nước. Nước làm lợi cho muôn vật mà không tranh giành, và chịu ở những chỗ thấp mà người ta không ưa. Tôi hay nghĩ tới câu ấy mỗi khi ngại đăng một bài vì sợ nó tầm thường, và thường thì sau đó tôi vẫn đăng.</p>
<p>Tôi cũng từng đợi đến lúc mình &quot;đủ giỏi&quot; mới dám chia sẻ. Nhưng cái mốc ấy chẳng bao giờ đến, vì cứ mỗi lần mình giỏi hơn một chút, thì giới hạn của chữ &quot;đủ&quot; lại tự động lùi xa thêm một đoạn. Thứ duy nhất tôi có là chỗ mình đang đứng, nên hôm nay biết gì thì nói điều đó, hiểu tới đâu thì viết tới đó.</p>
<p>Tôi cũng thôi nghĩ nhiều về số người đọc hay số người theo dõi. Những con số ấy đến sau. Điều tôi quan tâm là liệu mình có giữ được nhịp chảy ấy không, tuần này mình có viết ra được điều gì mới không. Mỗi lần chia sẻ là một lần mở ra một lối đi, cho người khác và cho cả chính mình.</p>
<p>Có những thứ càng giữ càng nặng, và kiến thức là một trong số đó. Tôi không cần thành ai khác mới có quyền chia sẻ. Nên hôm nay biết được điều gì, dù nhỏ, tôi viết nó ra, rồi bấm đăng trước khi kịp hỏi lại mình xem nó có đơn giản quá không.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Làm – rồi mới hiểu</title>
      <link>https://tramdev.io/posts/lam-roi-moi-hieu</link>
      <guid isPermaLink="true">https://tramdev.io/posts/lam-roi-moi-hieu</guid>
      <pubDate>Wed, 25 Mar 2026 00:00:00 GMT</pubDate>
      <category>Đạo</category>
      <description>Có hai kiểu hiểu. Một kiểu đến từ việc đọc, một kiểu đến từ việc làm, và từ bên trong chúng cho cùng một cảm giác.</description>
      <content:encoded><![CDATA[<p>Có một dạo, tôi cắm cổ đọc rất nhiều: từ cách xây dựng hệ thống, viết code thế nào cho sạch, cho đến chuyện làm sản phẩm. Lần nào gấp sách lại tôi cũng thấy mình đã thông suốt. Cảm giác đó dễ chịu lắm, cứ như mọi thứ đã nằm gọn trong tay. Chỉ cho đến khi bắt tay vào làm, tôi mới biết cái sự &quot;hiểu&quot; ấy đáng tin đến mức nào.</p>
<p>Trên giấy, mọi thứ đều rõ ràng và hợp lý. Ngoài thực tế thì có chỗ không chạy, và có những chuyện chẳng cuốn sách nào nhắc tới. Lúc đó tôi mới thấy mình đã gọi chung hai thứ khác nhau bằng một chữ &quot;hiểu&quot;.</p>
<p>Triết gia Gilbert Ryle từng tách bạch hai loại hiểu biết này từ giữa thế kỷ trước: &quot;biết rằng&quot; và &quot;biết cách&quot;. Biết rằng muốn giữ thăng bằng trên xe đạp thì phải bẻ lái về phía mình sắp ngã là một chuyện. Đạp được xe là chuyện khác, và đứa trẻ tám tuổi đạp xe vòng quanh xóm vốn đang nắm giữ cái thứ hai, dù chẳng mảy may cần đến cái thứ nhất.</p>
<p>Tôi hay nghĩ về chuyện này bằng hình ảnh tấm bản đồ. Đọc giống như nhìn bản đồ, thấy được đường đi và toàn cảnh. Làm thì giống tự đi trên con đường ấy, với những đoạn vòng và những ngã rẽ bản đồ không vẽ. Người ở thành phố hay mưa đều biết, bản đồ chẳng bao giờ cho biết con hẻm nào ngập sau một trận mưa chiều.</p>
<p>Alfred Korzybski có một câu mà giới làm khoa học hay nhắc: bản đồ không phải là vùng đất. Nghe thì hiển nhiên tới mức hơi buồn cười. Có điều, khi đang cầm bản đồ trong tay, người ta rất dễ quên.</p>
<p>Cái khó là từ bên trong, hai kiểu hiểu ấy cho cùng một cảm giác. Đọc xong một bài về cách xử lý một lỗi, tôi thấy chắc chắn y như lúc đã tự tay xử lý nó. Chỉ khi đặt chân xuống đường tôi mới biết cảm giác nào có nền bên dưới.</p>
<p>Người xem nhiều video dạy bơi có thể giảng lại khá đúng cách thở, cách đạp chân. Xuống tới nước thì cơ thể không chịu nghe theo những gì cái đầu biết. Có những thứ chỉ học được sau vài ngụm nước, và nhiều lỗi trong code cũng thế, tôi phải gặp nó và sai với nó thì mới đi qua được.</p>
<p>Có lẽ vì vậy mà có người nói về nghề rất đúng nhưng làm thì lúng túng, còn có người ít nói mà làm rất chắc tay. Người thứ hai chưa chắc đã biết nhiều hơn. Họ đã đi qua nhiều đoạn đường hơn, và những đoạn đường ấy ở lại trong tay họ.</p>
<p>Làm thì chậm hơn đọc, và mệt hơn. Bù lại, những gì có được khi làm thường ở lại lâu. Mỗi lần debug một lỗi hay viết lại một đoạn code, ngoài việc xong việc, tôi còn có thêm một lớp hiểu mà lần sau gặp lại tôi nhận ra ngay.</p>
<p>Vương Dương Minh, một nhà Nho đời Minh, chủ trương &quot;tri hành hợp nhất&quot;, biết và làm là một. Ông cho rằng biết mà không làm thì chỉ là chưa biết. Hồi đầu đọc, tôi thấy ý đó có phần cực đoan. Giờ thì ngẫm lại, tôi hiểu ông đang gọi tên cái cảm giác &quot;tưởng mình đã biết&quot; rất dễ nảy sinh sau mỗi lần đọc sách.</p>
<p>Đứng trước một thứ mới, tôi để ý mình hay muốn đọc thêm một bài, xem thêm một video nữa. Tôi đã học được rằng đó thường là lúc việc đọc đã đủ. Việc còn lại là mở editor ra và làm.</p>
<p>Có thể tôi sẽ làm sai, sẽ mất thêm thời gian. Đổi lại tôi có thứ mà việc đọc không cho được, là biết mọi thứ thật sự chạy ra sao. Kinh nghiệm dạy chậm và hơi khắt khe, nhưng những gì nó dạy thì thường đúng.</p>
<p>Tôi vẫn đọc, vẫn xem bản đồ trước khi đi. Chỉ là bây giờ xem xong thì tôi gấp nó lại, rồi bước ra đường.</p>
]]></content:encoded>
    </item>
    <item>
      <title>gstack — nhà máy phần mềm của một người</title>
      <link>https://tramdev.io/posts/gstack-nha-may-phan-mem-cua-mot-nguoi</link>
      <guid isPermaLink="true">https://tramdev.io/posts/gstack-nha-may-phan-mem-cua-mot-nguoi</guid>
      <pubDate>Sun, 22 Mar 2026 00:00:00 GMT</pubDate>
      <category>AIkisu</category>
      <description>Garry Tan — CEO của Y Combinator — đã viết hơn 600,000 dòng code production trong 60 ngày, vừa làm vừa chạy YC. gstack là bộ công cụ ông dùng để làm điều đó.</description>
      <content:encoded><![CDATA[<p>Garry Tan là President &amp; CEO của Y Combinator, một trong những tổ chức startup có ảnh hưởng nhất thế giới. Trong 60 ngày gần đây, ông viết được hơn 600,000 dòng code production (35% trong số đó là test, phần còn lại được merge vào các dự án thật), tức 10,000 đến 20,000 dòng mỗi ngày, trong khi vẫn làm đầy đủ vai trò CEO. Con số trong tuần gần nhất: 140,751 dòng thêm mới, 362 commits.</p>
<blockquote>
<p><em>&quot;That is not a typo. We are at the dawn of something real — one person shipping at a scale that used to require a team of twenty.&quot;</em></p>
</blockquote>
<p><a href="https://github.com/garrytan/gstack">gstack</a> là bộ công cụ ông dùng để đạt được điều đó, và ông đã mã nguồn mở toàn bộ.</p>
<h2>Ý tưởng cốt lõi: một đội ngũ, không phải một trợ lý</h2>
<p>Cách dùng Claude Code thông thường coi AI như một trợ lý thụ động chờ lệnh. Khác biệt lớn nhất của gstack là nó mô hình hóa AI thành một đội ngũ chuyên biệt, mỗi vai trò rõ ràng:</p>
<ul>
<li><strong>CEO</strong> (<code>/plan-ceo-review</code>): thách thức scope và giả định trước khi build</li>
<li><strong>Eng Manager</strong> (<code>/plan-eng-review</code>): khóa kiến trúc, data flow, test matrix</li>
<li><strong>Designer</strong> (<code>/design-review</code>, <code>/design-consultation</code>): phát hiện &quot;AI slop&quot; về mặt visual</li>
<li><strong>Staff Engineer</strong> (<code>/review</code>): code review cấp senior với auto-fix</li>
<li><strong>QA Lead</strong> (<code>/qa</code>): mở trình duyệt thật, click qua từng flow, tìm bug</li>
<li><strong>Release Engineer</strong> (<code>/ship</code>, <code>/land-and-deploy</code>): tạo PR, verify test coverage, deploy</li>
</ul>
<p>Khi bạn có 10–15 phiên agent chạy song song, cấu trúc vai trò này là thứ ngăn mọi thứ khỏi trở thành hỗn loạn.</p>
<h2>Triết lý đằng sau</h2>
<p>Phần tôi thấy đáng đọc nhất trong repo là file <a href="https://github.com/garrytan/gstack/blob/main/ETHOS.md"><code>ETHOS.md</code></a>, bộ nguyên tắc được tự động inject vào mọi skill. Có hai nguyên tắc nổi bật.</p>
<h3>Boil the Lake</h3>
<p>Nguyên tắc đầu tiên: với AI, chi phí cận biên của sự hoàn chỉnh gần bằng không. Vì vậy, hãy luôn chọn giải pháp đầy đủ thay vì giải pháp &quot;đủ dùng&quot;.</p>
<p>Nguyên tắc này dùng hình ảnh &quot;lake vs ocean&quot;. Một cái hồ thì đun sôi được, như viết test 100%, implement đầy đủ edge case, xử lý hết error path. Một đại dương thì không, ví dụ viết lại toàn bộ hệ thống từ đầu. Nên cứ đun sôi hồ, đừng cố đun đại dương.</p>
<p>Có một bảng so sánh khá thú vị:</p>
<table>
<thead>
<tr>
<th>Loại task</th>
<th>Đội người</th>
<th>AI-assisted</th>
<th>Tỉ lệ nén</th>
</tr>
</thead>
<tbody><tr>
<td>Boilerplate / scaffolding</td>
<td>2 ngày</td>
<td>15 phút</td>
<td>~100x</td>
</tr>
<tr>
<td>Viết test</td>
<td>1 ngày</td>
<td>15 phút</td>
<td>~50x</td>
</tr>
<tr>
<td>Implement feature</td>
<td>1 tuần</td>
<td>30 phút</td>
<td>~30x</td>
</tr>
<tr>
<td>Fix bug + regression test</td>
<td>4 giờ</td>
<td>15 phút</td>
<td>~20x</td>
</tr>
<tr>
<td>Kiến trúc / thiết kế</td>
<td>2 ngày</td>
<td>4 giờ</td>
<td>~5x</td>
</tr>
<tr>
<td>Research / khám phá</td>
<td>1 ngày</td>
<td>3 giờ</td>
<td>~3x</td>
</tr>
</tbody></table>
<p>Nhìn bảng này thì quyết định &quot;bỏ qua test vì tốn thời gian&quot; không còn đứng được nữa. 10% cuối cùng mà các đội thường bỏ qua giờ chỉ tốn thêm vài phút.</p>
<h3>Search Before Building</h3>
<p>Nguyên tắc thứ hai: trước khi build bất cứ thứ gì, hãy tìm kiếm xem nó đã tồn tại chưa. Triết lý này chia nguồn kiến thức thành ba tầng:</p>
<ul>
<li><strong>Tầng 1 (Tried and true)</strong>: pattern chuẩn, cách tiếp cận đã được kiểm chứng. Rủi ro ở tầng này là bạn mặc định câu trả lời hiển nhiên luôn đúng.</li>
<li><strong>Tầng 2 (New and popular)</strong>: best practice hiện tại, blog posts, xu hướng. Tìm kiếm, nhưng đọc có phê phán, vì đám đông có thể sai về những thứ mới cũng dễ như với thứ cũ.</li>
<li><strong>Tầng 3 (First principles)</strong>: quan sát gốc rễ từ lý luận về bài toán cụ thể. Đây là loại kiến thức có giá trị nhất.</li>
</ul>
<p>Mục tiêu của việc tìm kiếm là hiểu tại sao mọi người làm theo cách thông thường, rồi lý luận xem cách thông thường đó có thực sự đúng không, chứ không phải tìm code để copy.</p>
<h2>Toàn bộ 25 skills</h2>
<table>
<thead>
<tr>
<th>Skill</th>
<th>Vai trò</th>
<th></th>
</tr>
</thead>
<tbody><tr>
<td><code>/office-hours</code></td>
<td>Reframe vấn đề trước khi code</td>
<td><a href="https://github.com/garrytan/gstack/tree/main/office-hours">↗</a></td>
</tr>
<tr>
<td><code>/plan-ceo-review</code></td>
<td>Thách thức scope và giả định</td>
<td><a href="https://github.com/garrytan/gstack/tree/main/plan-ceo-review">↗</a></td>
</tr>
<tr>
<td><code>/plan-eng-review</code></td>
<td>Khóa kiến trúc và data flow</td>
<td><a href="https://github.com/garrytan/gstack/tree/main/plan-eng-review">↗</a></td>
</tr>
<tr>
<td><code>/plan-design-review</code></td>
<td>Review design trước khi implement</td>
<td><a href="https://github.com/garrytan/gstack/tree/main/plan-design-review">↗</a></td>
</tr>
<tr>
<td><code>/design-consultation</code></td>
<td>Xử lý câu hỏi visual một cách có hệ thống</td>
<td><a href="https://github.com/garrytan/gstack/tree/main/design-consultation">↗</a></td>
</tr>
<tr>
<td><code>/review</code></td>
<td>Code review cấp staff engineer</td>
<td><a href="https://github.com/garrytan/gstack/tree/main/review">↗</a></td>
</tr>
<tr>
<td><code>/design-review</code></td>
<td>Phát hiện và fix AI slop về visual</td>
<td><a href="https://github.com/garrytan/gstack/tree/main/design-review">↗</a></td>
</tr>
<tr>
<td><code>/investigate</code></td>
<td>Debug có hệ thống</td>
<td><a href="https://github.com/garrytan/gstack/tree/main/investigate">↗</a></td>
</tr>
<tr>
<td><code>/qa</code></td>
<td>Browser testing thật + bug fix</td>
<td><a href="https://github.com/garrytan/gstack/tree/main/qa">↗</a></td>
</tr>
<tr>
<td><code>/qa-only</code></td>
<td>Báo cáo bug, không sửa code</td>
<td><a href="https://github.com/garrytan/gstack/tree/main/qa-only">↗</a></td>
</tr>
<tr>
<td><code>/benchmark</code></td>
<td>Đo hiệu năng</td>
<td><a href="https://github.com/garrytan/gstack/tree/main/benchmark">↗</a></td>
</tr>
<tr>
<td><code>/canary</code></td>
<td>Kiểm tra sau khi deploy</td>
<td><a href="https://github.com/garrytan/gstack/tree/main/canary">↗</a></td>
</tr>
<tr>
<td><code>/ship</code></td>
<td>Tạo PR với verify test coverage</td>
<td><a href="https://github.com/garrytan/gstack/tree/main/ship">↗</a></td>
</tr>
<tr>
<td><code>/land-and-deploy</code></td>
<td>Merge PR qua production + health check</td>
<td><a href="https://github.com/garrytan/gstack/tree/main/land-and-deploy">↗</a></td>
</tr>
<tr>
<td><code>/document-release</code></td>
<td>Cập nhật tài liệu sau release</td>
<td><a href="https://github.com/garrytan/gstack/tree/main/document-release">↗</a></td>
</tr>
<tr>
<td><code>/browse</code></td>
<td>Headless Chromium với ~100ms latency</td>
<td><a href="https://github.com/garrytan/gstack/tree/main/browse">↗</a></td>
</tr>
<tr>
<td><code>/codex</code></td>
<td>Cross-model review qua OpenAI</td>
<td><a href="https://github.com/garrytan/gstack/tree/main/codex">↗</a></td>
</tr>
<tr>
<td><code>/retro</code></td>
<td>Thống kê hoạt động dev</td>
<td><a href="https://github.com/garrytan/gstack/tree/main/retro">↗</a></td>
</tr>
<tr>
<td><code>/careful</code></td>
<td>Guardrail cho thao tác nguy hiểm</td>
<td><a href="https://github.com/garrytan/gstack/tree/main/careful">↗</a></td>
</tr>
<tr>
<td><code>/freeze</code></td>
<td>Khóa file khỏi bị chỉnh sửa</td>
<td><a href="https://github.com/garrytan/gstack/tree/main/freeze">↗</a></td>
</tr>
<tr>
<td><code>/guard</code></td>
<td>Bảo vệ branch khỏi force push</td>
<td><a href="https://github.com/garrytan/gstack/tree/main/guard">↗</a></td>
</tr>
<tr>
<td><code>/unfreeze</code></td>
<td>Gỡ lock từ <code>/freeze</code></td>
<td><a href="https://github.com/garrytan/gstack/tree/main/unfreeze">↗</a></td>
</tr>
<tr>
<td><code>/setup-browser-cookies</code></td>
<td>Thiết lập cookie cho browser testing</td>
<td><a href="https://github.com/garrytan/gstack/tree/main/setup-browser-cookies">↗</a></td>
</tr>
<tr>
<td><code>/setup-deploy</code></td>
<td>Cấu hình deploy pipeline</td>
<td><a href="https://github.com/garrytan/gstack/tree/main/setup-deploy">↗</a></td>
</tr>
<tr>
<td><code>/gstack-upgrade</code></td>
<td>Cập nhật gstack lên phiên bản mới</td>
<td><a href="https://github.com/garrytan/gstack/tree/main/gstack-upgrade">↗</a></td>
</tr>
</tbody></table>
<h2>Bắt đầu từ đâu?</h2>
<p>25 skills là nhiều, nhưng thực tế bạn chỉ cần 5 cái đầu tiên để thấy được giá trị ngay:</p>
<ol>
<li><code>/office-hours</code>: dùng trước khi bắt đầu bất kỳ feature nào. Nó sẽ reframe lại vấn đề theo cách bạn chưa nghĩ tới.</li>
<li><code>/plan-ceo-review</code> + <code>/plan-eng-review</code>: chạy lần lượt để có một bản kế hoạch được thách thức cả về product lẫn kỹ thuật.</li>
<li><code>/review</code>: chạy sau khi AI viết code xong, bước này không nên bỏ qua.</li>
<li><code>/ship</code>: tạo PR kèm verify test coverage, thay thế hoàn toàn việc tự tạo PR thủ công.</li>
</ol>
<p>Phần còn lại (<code>/qa</code>, <code>/investigate</code>, <code>/benchmark</code>...) thêm vào khi bạn đã quen với flow cơ bản.</p>
<h2>Suy nghĩ của tôi</h2>
<p>gstack và <a href="https://tramdev.io/posts/superpowers-vu-khi-cho-coding-agent">Superpowers</a> đang giải quyết cùng một bài toán theo hai góc nhìn khác nhau. Superpowers tập trung vào kỷ luật quy trình, buộc agent đi từng bước có thứ tự. gstack tập trung vào cấu trúc vai trò, mỗi skill là một chuyên gia với góc nhìn riêng. Bạn hoàn toàn có thể dùng cả hai trên cùng một project: Superpowers để giữ agent đi đúng quy trình, gstack để gọi đúng chuyên gia vào đúng thời điểm.</p>
<p>Cái tôi thích nhất ở gstack là triết lý này thay đổi cách ra quyết định trước khi bạn mở terminal. Khi bảng tỉ lệ nén cho thấy viết test chỉ tốn 15 phút thay vì một ngày, lý do để bỏ qua nó biến mất hoàn toàn.</p>
<p>Garry Tan gọi đây là &quot;live experiment&quot; và muốn chia sẻ với cả thế giới. Tôi nghĩ việc chia sẻ đó quan trọng hơn con số 600,000 dòng code. Ông công khai mọi thứ để chứng minh rằng một kỷ nguyên mới đang thực sự bắt đầu.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Superpowers — vũ khí cho coding agent</title>
      <link>https://tramdev.io/posts/superpowers-vu-khi-cho-coding-agent</link>
      <guid isPermaLink="true">https://tramdev.io/posts/superpowers-vu-khi-cho-coding-agent</guid>
      <pubDate>Sun, 22 Mar 2026 00:00:00 GMT</pubDate>
      <category>AIkisu</category>
      <description>Coding agent giỏi đến đâu cũng cần một quy trình. Superpowers là bộ kỹ năng buộc agent hỏi, lên kế hoạch, rồi mới viết code.</description>
      <content:encoded><![CDATA[<p>Một vấn đề tôi hay gặp khi làm việc với coding agent là nó có xu hướng bắt tay vào code ngay lập tức. Tôi đưa ra một yêu cầu mơ hồ, nó cũng hiểu theo cách của nó và bắt đầu viết. Kết quả là phải đi sửa lại nhiều hơn mức cần thiết, hoặc tệ hơn là nó đi sai hướng hoàn toàn mà tôi không hay biết cho đến khi quá muộn.</p>
<p><a href="https://github.com/obra/superpowers">Superpowers</a> là một dự án mã nguồn mở giải quyết đúng vấn đề này. Nó cung cấp một bộ 14 &quot;kỹ năng&quot; (skills) có cấu trúc, buộc agent phải đi qua các bước bài bản trước khi chạm vào code.</p>
<h2>Bộ quy trình 7 bước</h2>
<p>Các skill trong Superpowers nối với nhau thành một quy trình, mỗi skill là một mắt xích:</p>
<ol>
<li><strong>Brainstorming</strong>: Trước khi làm bất cứ điều gì, agent phải hỏi, thảo luận, và đề xuất 2–3 hướng giải quyết để bạn chọn.</li>
<li><strong>Using git worktrees</strong>: Tạo môi trường làm việc riêng biệt, tránh làm bẩn nhánh chính.</li>
<li><strong>Writing plans</strong>: Lên kế hoạch chi tiết với các task nhỏ 2–5 phút, bao gồm đường dẫn file chính xác và bước kiểm tra.</li>
<li><strong>Subagent-driven development</strong>: Triển khai bằng cách dispatch các subagent độc lập cho từng task, kèm review hai tầng.</li>
<li><strong>Test-driven development</strong>: Bắt buộc theo chu trình RED–GREEN–REFACTOR, tức viết test trước rồi mới viết code.</li>
<li><strong>Requesting code review</strong>: Review có hệ thống, phân loại mức độ nghiêm trọng của từng vấn đề.</li>
<li><strong>Finishing development branch</strong>: Kiểm tra toàn bộ, dọn dẹp branch, đề xuất merge hoặc tạo PR.</li>
</ol>
<p>Quy trình này là bắt buộc, agent không được bỏ qua bước nào.</p>
<h2>Những chi tiết thú vị bên trong</h2>
<p>Đọc kỹ từng skill thì tôi mới thấy từng quyết định thiết kế đã được nghĩ kỹ đến mức nào.</p>
<h3>HARD-GATE trong Brainstorming</h3>
<p>Skill brainstorming có một nguyên tắc cứng:</p>
<blockquote>
<p><em>&quot;Do NOT invoke any implementation skill, write any code, scaffold any project, or take any implementation action until you have presented a design and the user has approved it. This applies to EVERY project regardless of perceived simplicity.&quot;</em></p>
</blockquote>
<p>Và nó còn xử lý trực tiếp cái tâm lý &quot;task này quá đơn giản, không cần design&quot;:</p>
<blockquote>
<p><em>&quot;Simple projects are where unexamined assumptions cause the most wasted work.&quot;</em></p>
</blockquote>
<p>Quy trình brainstorming bao gồm 9 bước từ khám phá project context, hỏi từng câu một (không hỏi nhiều câu cùng lúc), đề xuất 2–3 hướng tiếp cận, trình bày design theo từng phần để lấy phê duyệt, viết spec document, cho subagent review spec, rồi mới chuyển sang lên kế hoạch. Agent chỉ được phép chuyển sang <code>writing-plans</code> sau khi bạn đã approve spec.</p>
<h3>Iron Law trong Debugging</h3>
<p>Skill <code>systematic-debugging</code> gọi nguyên tắc của nó là &quot;Iron Law&quot;:</p>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">text</span></div><pre><code class="hljs language-text">NO FIXES WITHOUT ROOT CAUSE INVESTIGATION FIRST</code></pre></div><p>Quy trình debug được chia thành 4 phase: điều tra nguyên nhân gốc rễ, phân tích pattern, đặt giả thuyết và kiểm tra, rồi mới implement fix. Điểm hay nhất là phần xử lý &quot;3+ lần fix thất bại&quot;:</p>
<blockquote>
<p><em>&quot;If 3+ fixes failed: Question the architecture — each fix reveals new shared state/coupling/problem in a different place. Stop and discuss with your human partner before attempting more fixes.&quot;</em></p>
</blockquote>
<p>Đến lúc đó, skill này yêu cầu agent dừng lại và đặt câu hỏi về kiến trúc thay vì thêm fix thứ 4. Đọc là biết quy tắc này đến từ người đã debug nhiều.</p>
<p>Skill còn liệt kê các &quot;red flag&quot;, tức những dấu hiệu cho thấy agent đang debug sai cách, ví dụ: &quot;Quick fix for now, investigate later&quot;, &quot;Just try changing X and see if it works&quot;, &quot;I don&#39;t fully understand but this might work&quot;. Khi agent bắt đầu nghĩ như vậy, nó phải dừng lại và quay về Phase 1.</p>
<h3>Kế hoạch viết cho người không biết gì</h3>
<p>Skill <code>writing-plans</code> có một tiêu chuẩn rất cụ thể cho chất lượng của một bản kế hoạch:</p>
<blockquote>
<p><em>&quot;Clear enough for an enthusiastic junior engineer with poor taste, no judgement, no project context, and an aversion to testing to follow.&quot;</em></p>
</blockquote>
<p>Mỗi task trong kế hoạch phải có đường dẫn file chính xác, code hoàn chỉnh (không phải &quot;add validation&quot; chung chung), lệnh chạy cụ thể kèm output mong đợi. Mỗi task chỉ làm một việc, theo đúng thứ tự: viết test, chạy test để xác nhận nó fail, viết code, chạy test để xác nhận nó pass, rồi commit.</p>
<h2>Toàn bộ 14 skills</h2>
<table>
<thead>
<tr>
<th>Skill</th>
<th>Mục đích</th>
<th></th>
</tr>
</thead>
<tbody><tr>
<td><code>brainstorming</code></td>
<td>Khám phá ý tưởng và thiết kế trước khi code</td>
<td><a href="https://github.com/obra/superpowers/tree/main/skills/brainstorming">↗</a></td>
</tr>
<tr>
<td><code>writing-plans</code></td>
<td>Lên kế hoạch chi tiết từng task nhỏ</td>
<td><a href="https://github.com/obra/superpowers/tree/main/skills/writing-plans">↗</a></td>
</tr>
<tr>
<td><code>executing-plans</code></td>
<td>Thực thi kế hoạch theo từng bước</td>
<td><a href="https://github.com/obra/superpowers/tree/main/skills/executing-plans">↗</a></td>
</tr>
<tr>
<td><code>subagent-driven-development</code></td>
<td>Dispatch subagent độc lập cho từng task</td>
<td><a href="https://github.com/obra/superpowers/tree/main/skills/subagent-driven-development">↗</a></td>
</tr>
<tr>
<td><code>dispatching-parallel-agents</code></td>
<td>Chạy nhiều agent song song</td>
<td><a href="https://github.com/obra/superpowers/tree/main/skills/dispatching-parallel-agents">↗</a></td>
</tr>
<tr>
<td><code>test-driven-development</code></td>
<td>Chu trình RED–GREEN–REFACTOR</td>
<td><a href="https://github.com/obra/superpowers/tree/main/skills/test-driven-development">↗</a></td>
</tr>
<tr>
<td><code>systematic-debugging</code></td>
<td>Debug 4 phase có hệ thống</td>
<td><a href="https://github.com/obra/superpowers/tree/main/skills/systematic-debugging">↗</a></td>
</tr>
<tr>
<td><code>verification-before-completion</code></td>
<td>Xác nhận kết quả trước khi khai báo xong</td>
<td><a href="https://github.com/obra/superpowers/tree/main/skills/verification-before-completion">↗</a></td>
</tr>
<tr>
<td><code>requesting-code-review</code></td>
<td>Khởi tạo và cấu trúc code review</td>
<td><a href="https://github.com/obra/superpowers/tree/main/skills/requesting-code-review">↗</a></td>
</tr>
<tr>
<td><code>receiving-code-review</code></td>
<td>Tiếp nhận và xử lý phản hồi review</td>
<td><a href="https://github.com/obra/superpowers/tree/main/skills/receiving-code-review">↗</a></td>
</tr>
<tr>
<td><code>using-git-worktrees</code></td>
<td>Quản lý môi trường làm việc riêng biệt</td>
<td><a href="https://github.com/obra/superpowers/tree/main/skills/using-git-worktrees">↗</a></td>
</tr>
<tr>
<td><code>finishing-a-development-branch</code></td>
<td>Hoàn tất và dọn dẹp sau khi xong</td>
<td><a href="https://github.com/obra/superpowers/tree/main/skills/finishing-a-development-branch">↗</a></td>
</tr>
<tr>
<td><code>writing-skills</code></td>
<td>Viết thêm skill mới</td>
<td><a href="https://github.com/obra/superpowers/tree/main/skills/writing-skills">↗</a></td>
</tr>
<tr>
<td><code>using-superpowers</code></td>
<td>Giới thiệu hệ thống cho agent</td>
<td><a href="https://github.com/obra/superpowers/tree/main/skills/using-superpowers">↗</a></td>
</tr>
</tbody></table>
<h2>Triết lý đằng sau</h2>
<p>Thứ Superpowers thêm vào cho agent là kỷ luật. Bốn nguyên tắc cốt lõi của dự án là: test trước code, có hệ thống thay vì tự phát, đơn giản là mục tiêu hàng đầu, và bằng chứng thay vì tuyên bố.</p>
<p>Tôi thấy triết lý này quen thuộc, vì một senior developer có kinh nghiệm vẫn làm đúng như vậy theo thói quen. Superpowers chỉ đang encode những thói quen đó thành các rule để agent không được phép bỏ qua.</p>
<p>Có một câu trong README mà tôi thích: <em>&quot;It&#39;s not uncommon for Claude to be able to work autonomously for a couple hours at a time without deviating from the plan.&quot;</em> Theo tôi, agent làm được vậy là nhờ có một bản đồ rõ ràng để đi theo.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Gửi email bằng AWS SES</title>
      <link>https://tramdev.io/posts/gui-email-bang-aws-ses</link>
      <guid isPermaLink="true">https://tramdev.io/posts/gui-email-bang-aws-ses</guid>
      <pubDate>Sat, 08 Nov 2025 00:00:00 GMT</pubDate>
      <category>Devlog</category>
      <description>Việc gửi email bằng AWS SES có thể phức tạp hơn một chút lúc ban đầu, nhưng bù lại là độ tin cậy cao, khả năng mở rộng tốt</description>
      <content:encoded><![CDATA[<h2>Bối cảnh</h2>
<p>Sau một thời gian sử dụng Gmail SMTP để gửi email cho các hệ thống, nay mình quyết định chuyển sang sử dụng AWS SES.</p>
<p>Mình cũng cân nhắc lựa chọn khác là SendGrid, tuy nhiên vì vấn đề giá nên mình đã quyết định sử dụng SES.</p>
<h2>Cài đặt</h2>
<h3>1. Kích hoạt SES</h3>
<p>Trước hết chúng ta sẽ vào  <a href="https://console.aws.amazon.com/ses/">AWS Console → SES</a> để thiết lập. Chú ý là chọn khu vực (region) muốn sử dụng, ở đây mình chọn <code>ap-southeast-1</code>.</p>
<p>Quá trình thiết lập khá dễ dàng, chỉ cần thực hiện theo các bước. Mình sẽ cần cung cấp 1 email và một domain.</p>
<p>Sau khi quá trình thiết lập hoàn tất thì AWS show cho chúng ta các bước phải làm tiếp theo:</p>
<ol>
<li>Xác minh email: chỉ cần check email và click và link để xác minh</li>
<li>Xác minh domain: AWS sẽ show cho chúng ta các bản ghi DNS, chúng ta copy sang domain provider là được, sẽ mất vài phút để việc xác minh được hoàn tất.</li>
</ol>
<p>Ban đầu, tài khoản SES thường ở <strong>sandbox mode</strong>, chỉ gửi được tới các địa chỉ đã xác minh.
Sau quá trình testing, chúng ta có thể gửi yêu cầu &quot;Production access&quot; trong phần <em>Account dashboard → Sending limits</em> để gỡ giới hạn này.</p>
<h3>2. Tạo IAM Policy và Access Key</h3>
<p>Bây giờ chúng ta cần tạo một user và tạo một cặp key để sử dụng trong app của chúng ta.</p>
<ol>
<li><p>Vào <strong>IAM → Users → Create user</strong>.</p>
</li>
<li><p>Gán policy sau cho user: đoạn này mình muốn tìm policy có sẵn chỉ cung cấp quyền gửi email và lấy dữ liệu thống kê email, tuy nhiên mình không tìm thấy policy như vậy. Nên mình đã tạo một policy mới:</p>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">json</span></div><pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;Version&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;2012-10-17&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;Statement&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
    <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;Effect&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Allow&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;Action&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
        <span class="hljs-string">&quot;ses:SendEmail&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-string">&quot;ses:SendRawEmail&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-string">&quot;ses:GetSendQuota&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-string">&quot;ses:GetSendStatistics&quot;</span>
      <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;Resource&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;*&quot;</span>
    <span class="hljs-punctuation">}</span>
  <span class="hljs-punctuation">]</span>
<span class="hljs-punctuation">}</span></code></pre></div><p>Nếu muốn chặt hơn, có thể thay <code>&quot;Resource&quot;: &quot;*&quot;</code> bằng ARN của identity (domain hoặc email) đã xác minh.</p>
</li>
<li><p>Sau khi tạo user, chọn <strong>Security credentials → Create access key</strong>.
Lưu lại <code>Access Key ID</code> và <code>Secret Access Key</code>, cặp thông tin chúng ta sẽ dùng trong ứng dụng.</p>
</li>
</ol>
<h2>Triển khai</h2>
<h3>Cài đặt AWS SDK</h3>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">bash</span></div><pre><code class="hljs language-bash">npm install @aws-sdk/client-ses</code></pre></div><h3>Tạo module gửi email</h3>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">js</span></div><pre><code class="hljs language-js"><span class="hljs-keyword">import</span> { <span class="hljs-title class_">SESClient</span>, <span class="hljs-title class_">SendEmailCommand</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;@aws-sdk/client-ses&quot;</span>;

<span class="hljs-comment">// SDK tự đọc AWS_ACCESS_KEY_ID và AWS_SECRET_ACCESS_KEY từ biến môi trường</span>
<span class="hljs-keyword">const</span> ses = <span class="hljs-keyword">new</span> <span class="hljs-title class_">SESClient</span>({ <span class="hljs-attr">region</span>: <span class="hljs-string">&quot;ap-southeast-1&quot;</span> });

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">sendEmail</span>(<span class="hljs-params">{ to, subject, html }</span>) {
  <span class="hljs-keyword">const</span> params = {
    <span class="hljs-title class_">Source</span>: <span class="hljs-string">&quot;noreply@myapp.com&quot;</span>,
    <span class="hljs-title class_">Destination</span>: {
      <span class="hljs-title class_">ToAddresses</span>: [to],
    },
    <span class="hljs-title class_">Message</span>: {
      <span class="hljs-title class_">Subject</span>: { <span class="hljs-title class_">Data</span>: subject },
      <span class="hljs-title class_">Body</span>: { <span class="hljs-title class_">Html</span>: { <span class="hljs-title class_">Data</span>: html } },
    },
  };

  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">await</span> ses.<span class="hljs-title function_">send</span>(<span class="hljs-keyword">new</span> <span class="hljs-title class_">SendEmailCommand</span>(params));
    <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-string">&quot;✅ Email sent to&quot;</span>, to);
  } <span class="hljs-keyword">catch</span> (err) {
    <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">error</span>(<span class="hljs-string">&quot;❌ Email send failed:&quot;</span>, err);
    <span class="hljs-keyword">throw</span> err;
  }
}</code></pre></div><p>Mình <code>throw</code> lại lỗi sau khi log để nơi gọi biết là gửi thất bại, thay vì lặng lẽ bỏ qua.</p>
<h3>Gọi trong ứng dụng</h3>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">js</span></div><pre><code class="hljs language-js"><span class="hljs-keyword">await</span> <span class="hljs-title function_">sendEmail</span>({
  <span class="hljs-attr">to</span>: <span class="hljs-string">&quot;user@example.com&quot;</span>,
  <span class="hljs-attr">subject</span>: <span class="hljs-string">&quot;Welcome to MyApp!&quot;</span>,
  <span class="hljs-attr">html</span>: <span class="hljs-string">&quot;&lt;h1&gt;Hello 👋&lt;/h1&gt;&lt;p&gt;Thanks for joining!&lt;/p&gt;&quot;</span>,
});</code></pre></div><h2>Kết luận</h2>
<p>AWS SES cài đặt ban đầu phức tạp hơn một chút, đặc biệt là ở bước xác minh domain và cấu hình DNS. Bù lại, SES đáng tin cậy, chi phí thấp và mở rộng tốt.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Format json trên console</title>
      <link>https://tramdev.io/posts/format-json-tren-console</link>
      <guid isPermaLink="true">https://tramdev.io/posts/format-json-tren-console</guid>
      <pubDate>Wed, 05 Nov 2025 00:00:00 GMT</pubDate>
      <category>Devlog</category>
      <description>Có khá nhiều tình huống mình cần format json trên console cho dễ đọc, chẳng hạn muốn xem api response được trả ra bởi curl</description>
      <content:encoded><![CDATA[<h2>Bối cảnh</h2>
<p>Có khá nhiều tình huống mình cần format json trên console cho dễ đọc. Một trong những ví dụ cụ thể là muốn xem api response được trả ra bởi curl.</p>
<h2>Phương án</h2>
<p>Nếu đã có nodejs, có thể sử dụng một dòng này:</p>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">bash</span></div><pre><code class="hljs language-bash"><span class="hljs-built_in">alias</span> jsp=<span class="hljs-string">&quot;node -e &#x27;let s=\&quot;\&quot;; process.stdin.on(\&quot;data\&quot;, (c) =&gt; s += c).on(\&quot;end\&quot;, () =&gt; console.log(JSON.stringify(JSON.parse(s), null, 2)));&#x27;&quot;</span></code></pre></div><p>Alias gom hết input rồi mới parse. Với response lớn, stdin đến thành nhiều khúc, nếu parse ngay từng khúc thì sẽ gặp lỗi <code>SyntaxError</code>.
Sau đó chúng ta có thể gọi</p>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">bash</span></div><pre><code class="hljs language-bash"><span class="hljs-built_in">echo</span> <span class="hljs-string">&#x27;{&quot;a&quot;: 1}&#x27;</span> | jsp

curl -s http://localhost:3000/api/v1/projects | jsp</code></pre></div><p>Nếu không có nodejs thì macOS có sẵn python:</p>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">bash</span></div><pre><code class="hljs language-bash">curl -s http://localhost:3000/api/v1/projects | python3 -m json.tool</code></pre></div><p>Ngoài ra mình cũng dùng fx, một công cụ chuyên cho việc này của tác giả Anton Medvedev <a href="https://fx.wtf/">https://fx.wtf/</a>. Có điều fx không cho copy json content, nên mình gặp vướng khi dùng nó.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Sử dụng nhiều tài khoản Git trên cùng một máy</title>
      <link>https://tramdev.io/posts/su-dung-nhieu-tai-khoan-git-tren-cung-mot-may</link>
      <guid isPermaLink="true">https://tramdev.io/posts/su-dung-nhieu-tai-khoan-git-tren-cung-mot-may</guid>
      <pubDate>Sat, 25 Oct 2025 00:00:00 GMT</pubDate>
      <category>Devlog</category>
      <description>Cách mình cấu hình SSH và Git config để làm việc với nhiều tài khoản GitHub mà không phải chuyển tay giữa các key và email.</description>
      <content:encoded><![CDATA[<h2>Bối cảnh</h2>
<p>Mình đang sử dụng khá nhiều tài khoản git, cả github và gitlab. Cá nhân mình cũng có nhiều profile, và cũng có profile riêng cho công việc ở công ty. Trong quá trình làm việc có một số rắc rối như sau:</p>
<ul>
<li>Mỗi lần chuyển repo là phải remove ssh key để add lại cho đúng</li>
<li>Với các project mới cần phải thiết lập lại user.name và user.email cho đúng với tài khoản</li>
</ul>
<p>Mình muốn mọi thứ tự động: repo nào thì dùng đúng key và đúng tên/email tương ứng, không cần nhớ hay đổi tay nữa.</p>
<h2>Giải pháp</h2>
<h3>1. Sử dụng SSH config: host match</h3>
<p>SSH hỗ trợ config ssh key theo từng host, nếu chỉ có 2 tài khoản ở hai host khác nhau thì chỉ cần thiết lập host là đáp ứng được yêu cầu:</p>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">ini</span></div><pre><code class="hljs language-ini">Match Host github.com
  IdentityFile ~/.ssh/id_rsa_github
  IdentitiesOnly yes

Match Host gitlab.com
  IdentityFile ~/.ssh/id_rsa_gitlab
  IdentitiesOnly yes</code></pre></div><h3>2. Sử dụng SSH config: host alias</h3>
<p>Trong trường hợp có nhiều tài khoản của cùng một host thì phương án trên không khả thi, mình cần sử dụng phương án mới: host alias.</p>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">ini</span></div><pre><code class="hljs language-ini"><span class="hljs-comment"># Tài khoản cá nhân</span>
Host github-personal
  HostName github.com
  User git
  IdentityFile ~/.ssh/id_rsa_personal
  IdentitiesOnly yes

<span class="hljs-comment"># Tài khoản công ty</span>
Host github-work
  HostName github.com
  User git
  IdentityFile ~/.ssh/id_rsa_work
  IdentitiesOnly yes</code></pre></div><p>Sau đó mình sẽ clone git về với alias host <code>github-personal</code>, <code>github-work</code> thay vì <code>github.com</code></p>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">bash</span></div><pre><code class="hljs language-bash"><span class="hljs-comment"># Repo công ty</span>
git <span class="hljs-built_in">clone</span> git@github-work:company/project.git

<span class="hljs-comment"># Repo cá nhân</span>
git <span class="hljs-built_in">clone</span> git@github-personal:me/my-repo.git</code></pre></div><p>SSH sau đó sẽ tự động chọn ssh key dựa trên host mình đã đặt.</p>
<h3>3. Sử dụng SSH config: custom match</h3>
<p>Với phương án trên vấn đề được giải quyết ổn thoả, tuy nhiên dần dần mình cảm thấy phiền với việc phải đổi host cho remote url. Mình chuyển qua sử dụng phương án mới:</p>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">ini</span></div><pre><code class="hljs language-ini">Match Exec &quot;<span class="hljs-section">[[ $(git config --get remote.origin.url) =~ .*git@github.com:my-username.* ]]</span>&quot;
  IdentityFile ~/.ssh/hailerity_rsa
  IdentitiesOnly yes</code></pre></div><p>Trên đây sử dụng <code>Match Exec</code> để thực hiện câu lệnh check xem git remote url khớp với một tài khoản không, nếu khớp sẽ dùng ssh key tương ứng, không khớp sẽ bỏ qua.</p>
<p>Cách này rất linh hoạt, và không cần phải đổi remote url host. Tuy nhiên có một hạn chế với phương án này là không thể áp dụng cho câu lệnh <code>git clone</code>: lúc clone, repo chưa tồn tại nên <code>git config --get remote.origin.url</code> chưa trả về gì. Mục 5 bên dưới giải quyết hạn chế này.</p>
<h3>4. Sử dụng Git config để tự chọn user/email</h3>
<p>Sau nhiều lần commit project dùng sai user.name và user.email, mình tìm cách thiết lập để tránh sai lầm đó.</p>
<p>Git có hỗ trợ set config file theo điều kiện trong <code>~/.gitconfig</code>:</p>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">ini</span></div><pre><code class="hljs language-ini"><span class="hljs-section">[user]</span>
  <span class="hljs-attr">name</span> = Tram Dev
  <span class="hljs-attr">email</span> = gactram@tramdev.io

<span class="hljs-section">[includeIf &quot;gitdir:~/Workspaces/personal/**&quot;]</span>
  <span class="hljs-attr">path</span> = ~/Workspaces/personal/.gitconfig

<span class="hljs-section">[includeIf &quot;gitdir:~/Workspaces/work/**&quot;]</span>
  <span class="hljs-attr">path</span> = ~/Workspaces/work/.gitconfig</code></pre></div><p>Với config trên, các project ở trong <code>~/Workspaces/personal</code> sẽ sử dụng file gitconfig ở trong thư mục đó, tương tự với <code>~/Workspaces/work</code>. Như vậy mình chỉ cần viết <code>.gitconfig</code> cho từng thư mục và đặt project vào đúng thư mục là được.</p>
<p>Ví dụ <code>~/Workspaces/work/.gitconfig</code>:</p>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">ini</span></div><pre><code class="hljs language-ini"><span class="hljs-section">[user]</span>
  <span class="hljs-attr">name</span> = Tram Dev
  <span class="hljs-attr">email</span> = tramdev@congty.com</code></pre></div><h3>5. Gộp SSH key vào Git config theo thư mục</h3>
<p>Mục 4 đã chọn đúng user/email theo thư mục. Git cũng có <code>core.sshCommand</code> để chỉ định lệnh SSH, nên mình đặt luôn SSH key vào cùng file <code>.gitconfig</code> của thư mục đó:</p>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">ini</span></div><pre><code class="hljs language-ini"><span class="hljs-comment"># ~/Workspaces/work/.gitconfig</span>
<span class="hljs-section">[user]</span>
  <span class="hljs-attr">name</span> = Tram Dev
  <span class="hljs-attr">email</span> = tramdev@congty.com

<span class="hljs-section">[core]</span>
  <span class="hljs-attr">sshCommand</span> = ssh -i ~/.ssh/id_ed25519_work -o IdentitiesOnly=<span class="hljs-literal">yes</span></code></pre></div><div class="code-wrap"><div class="code-top"><span class="code-lang mono">ini</span></div><pre><code class="hljs language-ini"><span class="hljs-comment"># ~/Workspaces/personal/.gitconfig</span>
<span class="hljs-section">[user]</span>
  <span class="hljs-attr">name</span> = Tram Dev
  <span class="hljs-attr">email</span> = gactram@tramdev.io

<span class="hljs-section">[core]</span>
  <span class="hljs-attr">sshCommand</span> = ssh -i ~/.ssh/id_ed25519_personal -o IdentitiesOnly=<span class="hljs-literal">yes</span></code></pre></div><p>Bây giờ key và email cùng đi theo thư mục. Repo nằm trong <code>~/Workspaces/work</code> dùng key công ty, repo trong <code>~/Workspaces/personal</code> dùng key cá nhân, remote url vẫn giữ nguyên <code>git@github.com:...</code>. Cách này thay được cả mục 2 và mục 3.</p>
<p>Điều mình quan tâm nhất là <code>git clone</code> có dùng đúng key không. Với git 2.50, <code>includeIf &quot;gitdir:...&quot;</code> được áp dụng ngay trong lúc clone nếu thư mục đích nằm trong <code>~/Workspaces/work</code>, nên lệnh clone dùng đúng <code>core.sshCommand</code> của thư mục đó:</p>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">bash</span></div><pre><code class="hljs language-bash"><span class="hljs-built_in">cd</span> ~/Workspaces/work
git <span class="hljs-built_in">clone</span> git@github.com:company/project.git</code></pre></div><p>Để chắc chắn trên máy của bạn, bật <code>GIT_TRACE</code> khi clone. Git in ra lệnh SSH thực sự được chạy, trong đó có đường dẫn key:</p>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">bash</span></div><pre><code class="hljs language-bash"><span class="hljs-built_in">cd</span> ~/Workspaces/work
GIT_TRACE=1 git <span class="hljs-built_in">clone</span> git@github.com:company/project.git 2&gt;&amp;1 | grep run_command
<span class="hljs-comment"># trace: run_command: ... ssh -i ~/.ssh/id_ed25519_work -o IdentitiesOnly=yes ... git@github.com</span></code></pre></div><p>Hoặc kiểm tra repo đã clone đang nhận cấu hình nào:</p>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">bash</span></div><pre><code class="hljs language-bash"><span class="hljs-built_in">cd</span> ~/Workspaces/work/project
git config --show-origin core.sshCommand</code></pre></div><p>Nếu dùng phiên bản git cũ hơn mà clone không nhận cấu hình, có thể chỉ định key ngay trong lệnh clone. Option <code>-c</code> cũng được ghi vào config của repo mới, nên các lần pull/push sau vẫn dùng đúng key:</p>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">bash</span></div><pre><code class="hljs language-bash">git <span class="hljs-built_in">clone</span> -c core.sshCommand=<span class="hljs-string">&quot;ssh -i ~/.ssh/id_ed25519_work -o IdentitiesOnly=yes&quot;</span> \
  git@github.com:company/project.git</code></pre></div><p>Nếu tạo key mới, mình dùng Ed25519 thay cho RSA vì key ngắn hơn và được GitHub, GitLab hỗ trợ đầy đủ:</p>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">bash</span></div><pre><code class="hljs language-bash">ssh-keygen -t ed25519 -C <span class="hljs-string">&quot;tramdev@congty.com&quot;</span> -f ~/.ssh/id_ed25519_work</code></pre></div>]]></content:encoded>
    </item>
    <item>
      <title>Sở hữu địa chỉ email với domain cá nhân</title>
      <link>https://tramdev.io/posts/so-huu-dia-chi-email-voi-domain-ca-nhan</link>
      <guid isPermaLink="true">https://tramdev.io/posts/so-huu-dia-chi-email-voi-domain-ca-nhan</guid>
      <pubDate>Sat, 20 Sep 2025 00:00:00 GMT</pubDate>
      <category>Devlog</category>
      <description>Với một chút thiết lập, bạn có thể sở hữu email với domain cá nhân đầy chuyên nghiệp như hello@mydomain.com</description>
      <content:encoded><![CDATA[<h2>Bối cảnh</h2>
<p>Có thể bạn đã để ý, email liên hệ của Trạm Dev là <a href="mailto:gocua@tramdev.io">gocua@tramdev.io</a>. Thông thường để có một email như vậy thì có thể trả vài đô để dùng Google Workspace. Tuy nhiên, phương án đó xem chừng hơi đao to búa lớn so với một blog còn đang xếp từng viên gạch. Sau khi tìm hiểu qua, mình xác định thực hiện phương án sử dụng dịch vụ miễn phí quen thuộc: Cloudflare và Gmail.</p>
<p>Sơ lược bài toán: bạn sở hữu một domain, ví dụ mydomain.com, và bạn muốn sử dụng email với domain của mình, chẳng hạn <a href="mailto:hello@mydomain.com">hello@mydomain.com</a>, để nhận và gửi email, và bạn cần một phương án đơn giản, không mất phí.</p>
<h2>Phương án</h2>
<p>Trước hết mình sẽ muốn sử dụng gmail để nhận và gửi email, lý do đơn giản, mình đã quá quen thuộc với gmail rồi.</p>
<p>Vấn đề đầu tiên, mình sẽ cần thiết lập để khi người khác gửi email vào <a href="mailto:hello@mydomain.com">hello@mydomain.com</a> thì mình sẽ nhận được trong gmail của mình, chẳng hạn mình có tài khoản gmail là <a href="mailto:myemail@gmail.com">myemail@gmail.com</a>. Vấn đề này mình sẽ sử dụng tính năng Email routing của Cloudflare.</p>
<p>Vấn đề tiếp theo là cho phép gửi email từ gmail với tư cách của <a href="mailto:hello@mydomain.com">hello@mydomain.com</a>. Cái này thì gmail cũng đã hỗ trợ, mình chỉ cần thiết lập là được.</p>
<h2>Triển khai</h2>
<h3>Thiết lập trong cloudflare</h3>
<p>Trước hết, domain của bạn cần setup để chuyển sang sử dụng DNS của Cloudflare. Sau khi setup xong, Cloudflare sẽ tạo sẵn cho mình các bản ghi MX, và TXT phục vụ cho việc nhận gửi email. Đoạn này nếu bạn nào chưa sử dụng Cloudflare thì sẽ cần tìm hiểu cách trỏ domain sang Cloudflare.</p>
<p>Tiếp theo mình sẽ vào mục <code>Email</code> &gt; <code>Email Routing</code>. Chuyển sang tab <code>Routing rules</code>. Ở đây mình sẽ thấy danh sách các email đã được thiết lập.</p>
<p>Click vào <code>Create address</code> để thêm email mới, sau đó bạn nhập email custom và gmail của bạn vào. Save là xong.</p>
<p><img src="https://tramdev.io/images/posts/so-huu-dia-chi-email-voi-domain-ca-nhan/cloudflare1.webp" alt="Cloudflare email routing rule" title="Cloudflare email routing rule"></p>
<p>Bây giờ bạn có thể gửi thử một email tới <a href="mailto:hello@mydomain.com">hello@mydomain.com</a> rồi vào gmail để kiểm tra xem nó có xuất hiện ở trong gmail của bạn hay không. Trong trường hợp không nhận được thì có gì đó sai sai rồi.</p>
<p>Sau khi chắc chắn là bạn đã nhận được email thì chuyển qua bước tiếp theo.</p>
<h3>Tạo App password</h3>
<p>Quá trình thiết lập tiếp theo sẽ cần sử dụng đến App password, vì Gmail không cho sử dụng trực tiếp password của tài khoản gmail.</p>
<p>Bạn click vào avatar tài khoản Gmail, rồi click <code>Manage your Google Account</code> để vào phần quản lý. Hoặc vào link <a href="https://myaccount.google.com/">https://myaccount.google.com/</a>. Bạn search <code>App passwords</code> trên thanh search để tìm đến tính năng <code>App passwords</code>.</p>
<p><img src="https://tramdev.io/images/posts/so-huu-dia-chi-email-voi-domain-ca-nhan/app-password.webp" alt="App password" title="App password"></p>
<p>Ở đây bạn nhập một cái tên cho password sẽ tạo rồi nhấn Create. Nó sẽ hiện ra cho bạn một password. Hãy copy nó để tạm đâu đó rồi chuyển sang bước tiếp theo. Sau khi đã xong bước tiếp theo, bạn có thể xóa bản copy tạm đó đi, không cần lưu lại. Lưu ý là đừng xóa app password trong Google Account, vì Gmail cần nó để gửi email.</p>
<h3>Thiết lập trong gmail</h3>
<p>Bạn vào tài khoản gmail. Vào <code>Settings</code>. Vào tab <code>Accounts and Import</code>. Kéo xuống mục <code>Send mail as:</code> và click vào <code>Add another email address</code>. Một popup sẽ hiện lên và bạn sẽ nhập thông tin domain của bạn vào:</p>
<p><img src="https://tramdev.io/images/posts/so-huu-dia-chi-email-voi-domain-ca-nhan/gmail-config-1.webp" alt="Gmail config" title="Gmail config"></p>
<p>Đoạn này bạn bỏ tick Treat as alias nhé. Lý do tại sao thì bạn có thể click vào Learn more để đọc.</p>
<p>Click Next step. Tiếp theo bạn nhập thông tin về tài khoản gmail của bạn như trên ảnh.</p>
<p><img src="https://tramdev.io/images/posts/so-huu-dia-chi-email-voi-domain-ca-nhan/gmail-config-2.webp" alt="Gmail config" title="Gmail config"></p>
<p>Thông tin:</p>
<ul>
<li>SMTP Server: smtp.gmail.com</li>
<li>Port: 587</li>
<li>Username: là username tài khoản gmail của bạn</li>
<li>Password: chính là app password đã tạo ở bước trên, bạn hãy paste vào đây</li>
</ul>
<blockquote>
<p>Chú ý: ô Username bạn chỉ nhập username thôi nhé, không nhập phần @gmail.com</p>
</blockquote>
<p>Click Add Account, gmail sẽ kiểm tra xác thực và thông báo thành công nếu không có gì bất thường. Sau đó gmail sẽ gửi một email xác thực tới <a href="mailto:hello@mydomain.com">hello@mydomain.com</a>. Lúc này bạn chỉ cần vào Inbox của gmail, mở email vừa nhận được và click vào link xác thực là xong.</p>
<h3>Cập nhật SPF trong cloudflare</h3>
<p>Đến đây đã gửi được email, nhưng email gửi đi rất dễ bị đánh dấu spam. Lý do là bản ghi SPF mà Cloudflare tạo cho Email routing chỉ cho phép máy chủ của Cloudflare, trong khi email bây giờ được gửi đi từ máy chủ của Google.</p>
<p>Vào <code>DNS</code> &gt; <code>Records</code>, tìm bản ghi TXT bắt đầu bằng <code>v=spf1</code> và thêm <code>include:_spf.google.com</code> vào trước <code>~all</code>:</p>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">text</span></div><pre><code class="hljs language-text">v=spf1 include:_spf.mx.cloudflare.net include:_spf.google.com ~all</code></pre></div><p>Chú ý là sửa bản ghi đang có, không tạo thêm bản ghi SPF thứ hai, vì mỗi domain chỉ được có một bản ghi SPF.</p>
<p>Để kiểm tra, gửi thử một email từ <a href="mailto:hello@mydomain.com">hello@mydomain.com</a> tới một tài khoản gmail khác, mở email đó, chọn <code>Show original</code> và xem dòng SPF có báo <code>PASS</code> hay không.</p>
<h2>Kết quả</h2>
<p>Bây giờ mình đã có email xịn xò để sử dụng <a href="mailto:hello@mydomain.com">hello@mydomain.com</a>. Việc đọc email và gửi email sẽ được thực hiện bằng tài khoản gmail quen thuộc.</p>
<p>Bạn có thể thực hiện tương tự để tạo nhiều email custom (<a href="mailto:xinchao@mydomain.com">xinchao@mydomain.com</a>, <a href="mailto:hola@mydomain.com">hola@mydomain.com</a>, ..) cùng forward về một tài khoản gmail, hoặc về các tài khoản gmail khác nhau.</p>
<p>Một chút lưu ý khi gửi email đi là bạn sẽ cần xác định đúng tài khoản email dùng để gửi đi trong trường hợp bạn đã thêm nhiều tài khoản email. Gmail cũng có phần thiết lập này bạn có thể cài theo mục đích sử dụng của mình:</p>
<p><img src="https://tramdev.io/images/posts/so-huu-dia-chi-email-voi-domain-ca-nhan/gmail-sender-setting.webp" alt="Gmail sender setting" title="Gmail sender setting"></p>
]]></content:encoded>
    </item>
    <item>
      <title>Các cấp độ sử dụng Github Copilot</title>
      <link>https://tramdev.io/posts/cac-cap-do-su-dung-github-copilot</link>
      <guid isPermaLink="true">https://tramdev.io/posts/cac-cap-do-su-dung-github-copilot</guid>
      <pubDate>Wed, 20 Aug 2025 00:00:00 GMT</pubDate>
      <category>AIkisu</category>
      <description>Từ hỏi đáp kỹ thuật đến giao cả một dự án: bốn cấp độ tôi đã đi qua với Github Copilot, và vì sao tôi vẫn dừng lại trước những dự án lớn.</description>
      <content:encoded><![CDATA[<p>Tôi bắt đầu dùng Github Copilot nhiều để thử nghiệm các ý tưởng, rồi dùng sâu hơn để xem khả năng của nó tới đâu và có thể đưa nó vào quá trình phát triển của mình như thế nào. Sau gần một tháng, tôi hết quota bản miễn phí đúng lúc các dự án đang dở dang, nên nâng cấp lên gói Pro, việc đằng nào tôi cũng sẽ làm. Gói Pro miễn phí tháng đầu, sau đó $10 mỗi tháng. Ngoài Copilot còn nhiều lựa chọn khác, nhưng tôi muốn dùng sâu một công cụ trước khi chuyển sang thử những công cụ khác.</p>
<p>Một chi tiết nhỏ: ngay sau khi nâng cấp, Copilot Chat trong VS Code ngừng hoạt động với lỗi <code>Copilot took too long to get ready. Please review the guidance in the Chat view.</code> Đăng xuất rồi đăng nhập lại tài khoản GitHub trong VS Code là Chat chạy lại.</p>
<p>Nhìn lại chặng đường từ đó, tôi thấy mình đã đi qua bốn cấp độ, theo dòng phát triển của AI và theo kinh nghiệm của chính tôi với AI. Mỗi cấp là một mức tin tưởng: tôi giao cho Copilot bao nhiêu, và tôi phải kiểm tra lại bao nhiêu.</p>
<h2>Level 1 - Hỏi đáp kỹ thuật</h2>
<p>Tôi dùng Copilot như việc sử dụng Google trong những năm về trước. Gặp khúc mắc về syntax, thư viện, hay cách triển khai một logic, đặt một câu hỏi và Copilot sẽ đưa ra câu trả lời nhanh chóng. Vẫn cần xác minh lại câu trả lời hoặc chất vấn thêm, nhưng so với việc lướt qua 10 kết quả của Google thì Copilot nhanh hơn rất nhiều.</p>
<h2>Level 2 - Viết function/script ngắn</h2>
<p>Ở cấp này tôi bắt đầu &quot;giao việc&quot; cho AI. Với những logic độc lập và ít dính tới nghiệp vụ, tôi viết prompt và để Copilot viết. Nhận kết quả, tôi đọc để hiểu cách triển khai, phát hiện những điểm bất thường, rồi tinh chỉnh lại theo phong cách code của mình.</p>
<h2>Level 3 - Tham gia vào dự án thật</h2>
<p>Lúc này Copilot trở thành &quot;đồng đội&quot; trong project. Tôi yêu cầu nó fix bug, refactor code, hoặc implement một tính năng nhỏ. Copilot đã ở ngay trong dự án nên tôi không còn phải copy/paste code qua lại nữa.</p>
<h2>Level 4 - Viết nguyên một dự án từ đầu</h2>
<p>Khi cảm nhận được sức mạnh của Copilot, tôi bắt đầu một số dự án mới từ đầu và giao cho Copilot thực hiện toàn bộ. Công việc của tôi gần như chỉ còn là viết yêu cầu và kiểm tra kết quả.</p>
<p>Tôi đã xây dựng một web app hoàn toàn bằng Copilot, và dựng một project quản lý hạ tầng dùng Docker Swarm cùng các dịch vụ chạy trên đó như Traefik, Prometheus, Grafana. Có những khoảnh khắc thực sự wow, nhưng cũng có những tình huống ức chế.</p>
<p>Cấu trúc dự án rất tốt, làm theo đúng quy chuẩn, không có gì phải lăn tăn. Giao diện thì quá xịn, có lẽ đây là việc coding agent đang làm tốt nhất. Logic khá tốt, nhưng có những vấn đề nó bế tắc và mãi không giải quyết được. Những lúc đó tôi buộc phải nhảy vào tìm hiểu và đề xuất giải pháp cho nó.</p>
<h2>Model quyết định cấp độ</h2>
<p>Hai dự án trên tôi dùng Claude Sonnet 4, và kết quả khá bất ngờ, tốt hơn hẳn thời điểm tôi dùng bản miễn phí để làm blog này. Khi hết premium requests, tôi chuyển sang GPT-4.1 và thấy rõ sự khác biệt. Nếu ví Sonnet 4 như một senior developer thì GPT-4.1 chỉ như một middle developer.</p>
<p>Cùng một công cụ, cùng một cách làm việc, nhưng cấp độ tôi dám giao việc phụ thuộc vào model đang chạy bên dưới. Lên được Level 4 với model này không có nghĩa là sẽ giữ được Level 4 với model khác.</p>
<h2>Làm việc với một cộng sự</h2>
<p>Từ Level 3 trở đi, tôi bắt đầu xem coding agent như một cộng sự. Tùy model và tùy bài toán, cộng sự đó có thể là một senior hoặc một junior. Khi đã xem nó như một người thật, tôi chấp nhận rằng:</p>
<ul>
<li>Nó cần được chỉ dẫn cụ thể phải làm gì.</li>
<li>Đôi khi nó code sai so với yêu cầu.</li>
<li>Đôi khi nó tư duy lòng vòng và không tìm được giải pháp tối ưu.</li>
<li>Đôi khi nó để lại bug.</li>
<li>Không thể kỳ vọng nó luôn được việc, vì đôi khi nó là senior, đôi khi là junior.</li>
<li>Luôn nghi ngờ kết quả của nó.</li>
</ul>
<p>Từ đó tôi đặt ra vài nguyên tắc làm việc với coding agent:</p>
<ul>
<li>Mô tả yêu cầu đầy đủ, chi tiết nhất có thể.</li>
<li>Thực hành pair programming.</li>
<li>Luôn review code và đánh giá những thay đổi mới tác động đến hệ thống hiện tại như thế nào.</li>
<li>Giải thích và đề xuất giải pháp cho những vấn đề mà coding agent gặp khó khăn.</li>
</ul>
<h2>Tôi đang dừng ở đâu</h2>
<p>Dù thấy coding agent làm chủ được một dự án hoàn toàn mới, đó vẫn là những dự án tương đối nhỏ. Với những dự án lớn hơn, hoặc khi nghiệp vụ nhiều hơn và phức tạp dần theo thời gian, tôi chưa thực sự yên tâm đặt niềm tin vào nó. Tôi đang ở Level 4 cho dự án nhỏ, và vẫn ở Level 3 cho mọi thứ còn lại.</p>
<p>Nhưng tôi tin nghề viết code sẽ thay đổi hoàn toàn. Coding agent sẽ trở thành công cụ không thể thiếu với developer, như framework hay IDE bây giờ. Đến lúc đó gọi nó là cộng sự thì đúng hơn, và một developer có thể có rất nhiều cộng sự là AI agent.</p>
<p>Thay vì cặm cụi viết từng dòng lệnh, công việc chính của developer sẽ giống một &quot;nhạc trưởng&quot;: điều phối các agent bằng ý tưởng tổng thể, bài toán và những giới hạn cần giải quyết. Những nguyên tắc ở trên (mô tả yêu cầu cho rõ, review mọi thay đổi, nhảy vào khi agent bế tắc) chính là phần việc đó. Ngoài viết code, kỹ sư phần mềm sẽ cần thêm tư duy hệ thống, am hiểu nghiệp vụ và khả năng đặc tả yêu cầu cho AI. Dùng thành thạo các công cụ AI sẽ trở thành một kỹ năng căn bản.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Trình độ tỉ lệ thuận với số lượng dòng code đã viết</title>
      <link>https://tramdev.io/posts/trinh-do-ti-le-thuan-voi-so-luong-dong-code-da-viet</link>
      <guid isPermaLink="true">https://tramdev.io/posts/trinh-do-ti-le-thuan-voi-so-luong-dong-code-da-viet</guid>
      <pubDate>Fri, 01 Aug 2025 00:00:00 GMT</pubDate>
      <category>Đạo</category>
      <description>&quot;Cần code được ít nhất 1 triệu dòng code để bạn trở thành một lập trình viên cừ khôi&quot;</description>
      <content:encoded><![CDATA[<p>Malcolm Gladwell có một con số mà nhiều người nhớ: cần khoảng 10.000 giờ luyện tập để giỏi một việc gì đó. Tôi hay nói một con số tương tự cho nghề mình, rằng phải viết chừng một triệu dòng code thì mới thành một lập trình viên giỏi. Con số ấy tôi nghĩ ra trong lúc nói, chẳng dựa vào đâu cả. Phần ý nằm sau nó thì tôi tin thật.</p>
<p>Ai từng học triết ở trường chắc còn nhớ câu &quot;lượng đổi thì chất đổi&quot;. Lúc học, câu ấy nghe khô và xa, giống một dòng phải thuộc để đi thi. Với người viết code, nó lại hiện ra khá cụ thể. Có một ngày người ta đọc một đoạn code lạ và hiểu nó định làm gì trước khi đọc hết, mà không chỉ ra được mình học điều đó từ lúc nào.</p>
<p>Từ hồi mới học lập trình, tôi có thói quen gặp đoạn code nào trong sách hay bài hướng dẫn là gõ lại ngay. Ban đầu thì nhìn mà chép. Sau đó chạy thử, sửa một chỗ, chạy lại xem kết quả đổi ra sao, rồi sửa chỗ khác. Đến khi thấy đã thấm, tôi xoá hết và viết lại từ đầu mà không nhìn mẫu.</p>
<p>Trẻ con tập viết cũng đi đúng con đường ấy. Lúc đầu tô theo nét chấm, rồi chép theo chữ mẫu, rồi tới một ngày viết được chữ của riêng mình, hơi nghiêng và hơi xấu. Không ai giảng được cho bàn tay một nét chữ. Bàn tay phải viết đủ nhiều cho tới khi nó tự biết.</p>
<p>Sách Luận Ngữ mở đầu bằng câu &quot;Học nhi thời tập chi, bất diệc duyệt hồ&quot;, học rồi thường xuyên luyện tập, chẳng vui lắm sao. Tôi để ý chữ &quot;tập&quot; đứng ngay sau chữ &quot;học&quot;. Tôi đọc nó như thể học mà chưa tập thì việc học vẫn chưa xong.</p>
<p>Khi gặp một bài toán, tôi chỉ dành vừa đủ thời gian để nghĩ ra một giải pháp tạm ổn, rồi bắt tay vào viết. Giải pháp đầu tiên ít khi là giải pháp tốt nhất. Có điều, phải viết thì tôi mới gặp những chỗ lúc nghĩ chưa thấy, và mới biết giả định nào của mình hoá ra sai. Dòng code đầu tiên dẫn tới mười dòng tiếp theo, và mười dòng ấy thường quay lại cho tôi biết dòng đầu tiên nên viết khác đi thế nào.</p>
<p>Người học đi xe đạp ngã nhiều lần trước khi giữ được thăng bằng, và chẳng ai coi những cú ngã đó là thời gian phí. Bug với tôi cũng vậy. Có viết thì mới có bug, và mỗi lần lần ra một bug là một lần tôi hiểu thêm về cái hệ thống mình tưởng đã hiểu. Tôi ngờ rằng thứ tích lại sau một triệu dòng code chủ yếu là những lần như thế.</p>
<p>Gladwell lấy con số 10.000 giờ từ nghiên cứu của Anders Ericsson về những người chơi violin. Về sau chính Ericsson nói con số ấy bị hiểu sai. Điều ông quan sát được là luyện tập có chủ đích, mỗi buổi nhắm vào một chỗ còn yếu, và số giờ chỉ là thứ đi kèm.</p>
<p>Tôi nghĩ dòng code cũng thế. Một triệu dòng gõ cho xong việc có lẽ chẳng dạy được bao nhiêu. Thói quen sửa một chỗ rồi chạy lại xem điều gì thay đổi, nhìn lại thì, gần với kiểu luyện tập Ericsson nói tới hơn tôi tưởng. Lượng vẫn cần, chỉ là phải là lượng mà mình có nhìn vào.</p>
<p>Có viết thì mới có code dở, và phải có code dở thì sau này mới có code tốt hơn. Người nấu ăn lâu năm hẳn cũng từng làm hỏng không ít nồi canh, mặn quá hoặc nhạt quá, trước khi nêm được bằng cảm giác. Những nồi canh hỏng ấy chẳng ai còn nhớ, nhưng cái lưỡi thì nhớ.</p>
<p>Làm nghề này đủ lâu, ai cũng có lúc mở lại code của mình từ vài năm trước và lẩm bẩm &quot;hồi đó code ngu thật&quot;. Để thấy đoạn code cũ dở, mình phải đứng ở một chỗ khác với lúc viết ra nó. Nên mỗi lần lẩm bẩm câu đó, tôi thấy ngượng thì ít mà vui thì nhiều, vì nó cho tôi biết mình đã đi được một đoạn kể từ hôm viết những dòng ấy.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Thiết lập PAC proxy trên MacOS</title>
      <link>https://tramdev.io/posts/thiet-lap-pac-proxy-tren-macos</link>
      <guid isPermaLink="true">https://tramdev.io/posts/thiet-lap-pac-proxy-tren-macos</guid>
      <pubDate>Thu, 29 May 2025 00:00:00 GMT</pubDate>
      <category>Devlog</category>
      <description>Thay vì thiết lập proxy cho toàn bộ máy, thiết lập proxy cho một số domain nhất định để tránh những rủi ro.</description>
      <content:encoded><![CDATA[<p>Gần đây Telegram bị chặn ở Việt Nam. Mình không dùng nhiều Telegram nhưng có một vài cuộc trò chuyện trên đó nên thỉnh thoảng cần truy cập vào.</p>
<p>Như đã kể lần trước, mình đã chạy một proxy server trên con EC2 để truy cập được vào Google Stitch. Tiện thể mình cũng dùng để truy cập Telegram luôn.</p>
<p>Nhưng mình gặp một vấn đề. Với cách thiết lập proxy cũ cho máy Mac, tất cả các request đều được proxied. Đó là điều không hay cho lắm. Một số ứng dụng bắt đầu gửi cảnh báo về hành vi bất thường, cụ thể là có người truy cập từ Singapore.</p>
<p>Mình hỏi Github Copilot để tìm cách thiết lập proxy cho một số domain nhất định mà thôi. Và phương án được đưa ra là sử dụng file PAC (Proxy Auto-Configuration).</p>
<h2>File PAC</h2>
<p>File PAC là một file JavaScript có một hàm <code>FindProxyForURL</code>. Trình duyệt gọi hàm này cho mỗi request, và hàm trả về đi qua proxy nào hoặc đi thẳng (<code>DIRECT</code>).</p>
<p>Dưới đây là một file mẫu: chỉ Telegram và Stitch đi qua proxy, còn lại đi thẳng. Thay <code>&lt;ip-ec2&gt;</code> bằng IP của server chạy proxy, và thêm bớt domain theo nhu cầu.</p>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">js</span></div><pre><code class="hljs language-js"><span class="hljs-comment">// proxy.pac</span>
<span class="hljs-keyword">function</span> <span class="hljs-title function_">FindProxyForURL</span>(<span class="hljs-params">url, host</span>) {
  <span class="hljs-keyword">var</span> proxied = [<span class="hljs-string">&#x27;telegram.org&#x27;</span>, <span class="hljs-string">&#x27;t.me&#x27;</span>, <span class="hljs-string">&#x27;stitch.withgoogle.com&#x27;</span>];
  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">var</span> i = <span class="hljs-number">0</span>; i &lt; proxied.<span class="hljs-property">length</span>; i++) {
    <span class="hljs-keyword">if</span> (host === proxied[i] || <span class="hljs-title function_">dnsDomainIs</span>(host, <span class="hljs-string">&#x27;.&#x27;</span> + proxied[i])) {
      <span class="hljs-keyword">return</span> <span class="hljs-string">&#x27;PROXY &lt;ip-ec2&gt;:3128&#x27;</span>;
    }
  }
  <span class="hljs-keyword">return</span> <span class="hljs-string">&#x27;DIRECT&#x27;</span>;
}</code></pre></div><p><code>dnsDomainIs(host, &#39;.telegram.org&#39;)</code> khớp với mọi subdomain như <code>web.telegram.org</code>, còn <code>host === &#39;telegram.org&#39;</code> khớp với chính domain đó.</p>
<p>File PAC không chứa được username và password. Nếu proxy có xác thực, trình duyệt sẽ tự hỏi tài khoản ở lần đầu đi qua proxy.</p>
<h2>Serve file PAC qua HTTP</h2>
<p>Đây là chỗ mình mất hơn một tiếng. Lúc đầu mình khai báo đường dẫn tới file trên máy, <code>file:///Users/me/proxy.pac</code>, và Chrome không hề nhận proxy config. Trên các bản macOS gần đây, cấu hình PAC bằng đường dẫn <code>file://</code> không được hỗ trợ, nên phải serve file qua HTTP.</p>
<p>Mình chạy một static server ngay trong thư mục chứa file PAC:</p>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">bash</span></div><pre><code class="hljs language-bash">npx http-server -p 4040</code></pre></div><p>Kiểm tra file đã được serve:</p>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">bash</span></div><pre><code class="hljs language-bash">curl http://127.0.0.1:4040/proxy.pac</code></pre></div><p>Server này cần chạy mỗi khi dùng proxy. Nếu tắt nó đi, macOS không lấy được file PAC.</p>
<h2>Khai báo trong macOS</h2>
<p>Vào <strong>System Settings → Network → (Wi-Fi hoặc Ethernet) → Details → Proxies</strong>, bật <strong>Automatic proxy configuration</strong> và điền URL:</p>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">text</span></div><pre><code class="hljs language-text">http://127.0.0.1:4040/proxy.pac</code></pre></div><p>Bấm <strong>OK</strong> để lưu.</p>
<h2>Khởi động lại Chrome</h2>
<p>Sau khi đổi sang URL HTTP, mình vẫn chưa thấy Chrome dùng proxy ngay. Mình đoán là do chưa khởi động lại Chrome sau khi đổi cấu hình. Vì vậy sau mỗi lần đổi URL file PAC, mình thoát hẳn Chrome (<code>Cmd + Q</code>) rồi mở lại. Khi đã nhận cấu hình, Chrome hỏi tài khoản proxy ở lần đầu vào Telegram, nhập xong là vào được.</p>
<p>Muốn xem Chrome đang dùng cấu hình proxy nào, mở <code>chrome://net-internals/#proxy</code>. Trang này cũng có nút <strong>Re-apply settings</strong> để Chrome đọc lại cấu hình mà không cần khởi động lại.</p>
<p>Lần đầu mình tưởng thành công là do may, vì Chrome bỗng dưng hỏi tài khoản trong lúc mình đang đọc tài liệu. Kiểm tra lại mới thấy nguyên nhân chính nằm ở đường dẫn file PAC: đổi từ <code>file://</code> sang URL HTTP thì Chrome mới nhận cấu hình.</p>
<h2>Lưu ý</h2>
<ul>
<li>Cấu hình PAC áp dụng cho các ứng dụng dùng proxy hệ thống như Chrome, Safari. Lệnh trong terminal như <code>curl</code> không đọc file PAC; muốn đi qua proxy thì đặt <code>https_proxy</code> cho lệnh đó.</li>
<li>Ứng dụng Telegram cho desktop có phần cài proxy riêng trong <strong>Settings → Advanced → Connection type</strong>, không dùng file PAC.</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Thiết lập proxy để trải nghiệm Stitch</title>
      <link>https://tramdev.io/posts/thiet-lap-proxy-de-trai-nghiem-stitch</link>
      <guid isPermaLink="true">https://tramdev.io/posts/thiet-lap-proxy-de-trai-nghiem-stitch</guid>
      <pubDate>Tue, 20 May 2025 00:00:00 GMT</pubDate>
      <category>Devlog</category>
      <description>Tự chạy proxy server trên EC2 để trải nghiệm Google Stitch cho việc thiết kế giao diện.</description>
      <content:encoded><![CDATA[<p>Khi được biết Google vừa mới ra mắt Stitch, một công cụ cho phép thiết kế giao diện, mình đã nhanh chóng vào đăng ký tài khoản để trải nghiệm thử. Tuy nhiên sau khi đăng nhập vào bằng tài khoản Google của mình thì mình nhận được thông báo dịch vụ không khả dụng. Có lẽ Google chưa ra mắt công khai cho Việt Nam.</p>
<p>Mình nghĩ đến proxy. Tuy nhiên hiện tại mình đang không dùng công cụ proxy nào. Cơ bản mình cũng không thích dùng proxy. Để kiểm chứng việc dùng proxy có thể vào được Stitch, mình nhờ một đứa em kiểm tra, và đúng là có vào được sau khi thiết lập proxy. Mình quyết định tự tạo proxy server để dùng thay vì cài công cụ.</p>
<p>Giải pháp của mình là chạy một proxy server (forward proxy) trên một con server nằm ở nước ngoài. Hiện tại mình đang có một con EC2 nằm ở Singapore. Còn phần mềm proxy, sau khi cân nhắc mình quyết định sử dụng squid.</p>
<p>Quá trình thiết lập cũng khá đơn giản. Đầu tiên là chạy và thiết lập squid. Mình chọn docker để chạy vì đã có sẵn docker trên server rồi. Và đây là câu lệnh mình dùng để chạy squid:</p>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">bash</span></div><pre><code class="hljs language-bash">docker run --name squid -d --restart=always \
  --publish 3128:3128 \
  --volume ./squid.conf:/etc/squid/squid.conf \
  --volume ./passwords:/etc/squid/passwords \
  --volume /srv/docker/squid/cache:/var/spool/squid \
  sameersbn/squid:3.5.27-2</code></pre></div><p>Lệnh trên mount hai file từ thư mục hiện tại: <code>squid.conf</code> là cấu hình của squid, <code>passwords</code> là danh sách tài khoản được phép dùng proxy. Proxy mở trên Internet mà không có xác thực sẽ bị người khác dùng ké rất nhanh, nên mình bật xác thực ngay từ đầu.</p>
<p>Dưới đây là một <code>squid.conf</code> tối thiểu có xác thực:</p>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">text</span></div><pre><code class="hljs language-text"># squid.conf
http_port 3128

auth_param basic program /usr/lib/squid/basic_ncsa_auth /etc/squid/passwords
auth_param basic realm proxy
acl authenticated proxy_auth REQUIRED

http_access allow authenticated
http_access deny all</code></pre></div><p>Đường dẫn tới <code>basic_ncsa_auth</code> có thể khác tuỳ image. Nếu squid không khởi động được, kiểm tra đường dẫn đúng trong container:</p>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">bash</span></div><pre><code class="hljs language-bash">docker <span class="hljs-built_in">exec</span> squid sh -c <span class="hljs-string">&#x27;ls /usr/lib/squid*/basic_ncsa_auth&#x27;</span></code></pre></div><p>File <code>passwords</code> tạo bằng <code>htpasswd</code>, có trong gói <code>apache2-utils</code> trên Ubuntu:</p>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">bash</span></div><pre><code class="hljs language-bash"><span class="hljs-built_in">sudo</span> apt install apache2-utils
htpasswd -c passwords &lt;ten-dang-nhap&gt;</code></pre></div><p>Lệnh sẽ hỏi mật khẩu rồi ghi vào file. Sau khi sửa <code>squid.conf</code> hoặc <code>passwords</code>, chạy <code>docker restart squid</code> để squid đọc lại.</p>
<p>Cuối cùng, mở cổng 3128 trong Security Group của EC2, và chỉ mở cho IP của mình thay vì <code>0.0.0.0/0</code>. Có xác thực rồi nhưng giới hạn IP vẫn bớt được rất nhiều request dò quét.</p>
<p>Kiểm tra proxy từ máy của mình:</p>
<div class="code-wrap"><div class="code-top"><span class="code-lang mono">bash</span></div><pre><code class="hljs language-bash">curl -x http://&lt;ten-dang-nhap&gt;:&lt;mat-khau&gt;@&lt;ip-ec2&gt;:3128 https://ifconfig.me</code></pre></div><p>Nếu lệnh trả về IP của EC2 thì proxy đã chạy.</p>
<p>Sau khi chạy được squid thì thiết lập proxy cho máy của mình. Mình dùng macOS: vào <strong>System Settings → Network → Wi-Fi → Details → Proxies</strong>, bật <strong>Web proxy (HTTP)</strong> và <strong>Secure web proxy (HTTPS)</strong>, điền IP của EC2 và cổng <code>3128</code> cho cả hai, bật <strong>Proxy server requires password</strong> rồi điền tài khoản vừa tạo.</p>
<p>Thiết lập xong proxy thì mình đã vào được stitch thành công. Mình bắt đầu trải nghiệm với một prompt để thiết kế một website. Khá ấn tượng với kết quả ban đầu mà nó đưa ra. Tuy nhiên mình chưa rõ khả năng của nó như thế nào khi cần phải tùy chỉnh thêm các chi tiết nhỏ. Stitch cho phép tải code về hoặc chuyển code sang Figma.</p>
<p>Bài này chỉ ghi lại lần thử đầu tiên. Proxy cho toàn bộ máy cũng có nhược điểm, mọi request đều đi qua Singapore; cách chỉ proxy cho một số domain mình viết ở bài <a href="https://tramdev.io/posts/thiet-lap-pac-proxy-tren-macos">Thiết lập PAC proxy trên MacOS</a>.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
