AI-Native Solutions Architect

AI & Legacy Code: Cách dùng AI để đọc và viết tài liệu cho các hệ thống \"di sản\" 10 năm tuổi

7/19/2026 · 15p đọc


title: "AI & Legacy Code: Cách dùng AI để đọc và viết tài liệu cho các hệ thống "di sản" 10 năm tuổi"
series: "AI-Native Solutions Architect: Từ Coder đến Kiến trúc sư AI"
season: "Season 1 — AI-Native Coding & Tooling"
order: 6
audience: "Software Engineer hướng tới Solutions Architect"
reading_time: "13 phút"
tags:

  • legacy-code
  • documentation
  • ai-native
  • code-archaeology
  • solutions-architect
  • context-engineering

AI & Legacy Code: Cách dùng AI để đọc và viết tài liệu cho các hệ thống "di sản" 10 năm tuổi

Bạn được giao maintain một module tính hoa hồng bán hàng viết cách đây 9 năm. File commission_calculator.php (hoặc .java, .cs — chọn ngôn ngữ nào cũng vậy) dài 1800 dòng, không có test, không có comment nào giải thích tại sao, và người viết ra nó đã nghỉ việc từ 2019. Bạn cần sửa một bug: hoa hồng tính sai cho một nhóm khách hàng cụ thể. Nhưng mỗi lần bạn định sửa điều kiện if (customer.type == 3 && order.total > 5000000 && !order.hasFlag('LEGACY_PRICING')), bạn không dám chắc điều kiện đó bảo vệ business rule nào — sửa sai có thể làm sai lệch hoa hồng của hàng trăm sales trong quá khứ.

Đây là tình huống mọi kỹ sư đều gặp ít nhất một lần trong sự nghiệp, và nó không phải vấn đề kỹ thuật thuần túy — nó là vấn đề mất ngữ cảnh nghiệp vụ. Code vẫn chạy đúng (hoặc gần đúng), nhưng lý do đằng sau từng điều kiện đặc biệt đã biến mất cùng với người viết ra nó. Không ai còn nhớ năm 2017 có một đợt khuyến mãi đặc biệt cho khách hàng miền Trung khiến logic phải rẽ nhánh thêm một lần nữa.

AI không giải quyết được vấn đề "không ai còn nhớ" — không có ma thuật nào tái tạo lại một cuộc họp năm 2017 mà không ai ghi biên bản. Nhưng AI làm được một việc rất cụ thể và rất có giá trị: đọc hàng nghìn dòng code, dữ liệu lịch sử, commit message, và tài liệu rải rác nhanh hơn con người hàng chục lần, rồi đưa ra giả thuyết có cấu trúc về nghiệp vụ ẩn — thứ mà bạn xác nhận lại thay vì phải tự suy ra từ đầu. Bài này nói về cách làm việc đó một cách có phương pháp, không phải cách ném cả file vào chatbot và hy vọng.

Vấn đề

Legacy code có ba đặc điểm khiến việc đọc hiểu tốn thời gian không tương xứng với độ phức tạp thuật toán thực sự:

1. Logic nghiệp vụ được mã hóa thành điều kiện, không phải tài liệu. Một rule kiểu "khách hàng VIP mua trên 5 triệu trong kỳ khuyến mãi thì không áp giá legacy" bị nén thành một dòng if với ba biến boolean không tên gợi nhớ. Đọc code không cho bạn biết rule đó là gì — nó chỉ cho bạn biết rule đó được implement như thế nào, và implementation thường có edge case che khuất intent gốc.

2. Context bị phân mảnh qua thời gian. Rule ban đầu được thêm vì lý do A, sau đó bị sửa lại vì lý do B (một bug fix), rồi bị sửa lần nữa vì lý do C (yêu cầu kinh doanh mới) — và mỗi lần sửa chỉ thêm một nhánh else if mới thay vì refactor lại toàn bộ. Kết quả là code hiện tại là một lớp trầm tích của nhiều quyết định nghiệp vụ khác nhau, không phải một thiết kế nhất quán.

3. Không ai dám xóa gì vì không ai chắc điều gì còn cần dùng. Đây là lý do legacy code chỉ có xu hướng phình to theo thời gian — thêm điều kiện an toàn hơn sửa điều kiện cũ, vì sửa cũ đòi hỏi hiểu đúng bản chất cũ.

Cách làm cũ — đọc từng dòng, debug từng trường hợp, hỏi han đồng nghiệp — vẫn đúng về nguyên tắc nhưng chậm tuyến tính theo độ dài code. AI dùng hời hợt (kiểu "giải thích file này giúp tôi" rồi paste nguyên 1800 dòng) cũng không giải quyết được gì hơn — nó trả về một bản tóm tắt code làm gì (mô tả lại syntax bằng lời), chứ không trả lời được câu hỏi quan trọng hơn: tại sao code lại làm như vậy. Sự khác biệt giữa hai câu hỏi này là toàn bộ giá trị của kỹ thuật trong bài.

Kỹ thuật cốt lõi: Đọc code theo lớp, từ ngoài vào trong — và ép AI suy luận nghiệp vụ, không chỉ mô tả cú pháp

Ý tưởng cốt lõi: coi AI như một nhà khảo cổ học đang khai quật một di chỉ — không đào bừa xuống một điểm, mà bóc từng lớp đất theo thứ tự, ghi lại giả thuyết ở mỗi lớp trước khi đào sâu hơn.

flowchart TD
    A["Lớp 1: Entry point<br/>API endpoint / cron job / event handler<br/>→ Module này được GỌI khi nào, bởi ai?"] --> B["Lớp 2: Luồng gọi chính (happy path)<br/>Hàm nào gọi hàm nào, theo thứ tự nào<br/>→ Vẽ sơ đồ luồng tổng quan"]
    B --> C["Lớp 3: Rẽ nhánh & điều kiện đặc biệt<br/>if/else, switch, early return<br/>→ Từng nhánh bảo vệ business rule nào?"]
    C --> D["Lớp 4: Chi tiết implementation<br/>Công thức tính toán, transform dữ liệu<br/>→ Tại sao công thức này, không phải công thức khác?"]
    D --> E["Đối chiếu với dữ liệu thực tế / người liên quan<br/>→ Xác nhận giả thuyết trước khi sửa code"]

Nguyên tắc quan trọng nhất khi prompt AI đọc legacy code: luôn tách rời hai câu hỏi "code làm gì" và "tại sao code làm vậy", và luôn hỏi câu thứ hai một cách tường minh. Một AI được hỏi "giải thích đoạn code này" sẽ mặc định trả lời câu thứ nhất (diễn dịch syntax sang tiếng Anh/Việt) — vì đó là câu dễ trả lời và ít rủi ro sai hơn. Bạn phải chủ động yêu cầu nó nhảy sang bước suy luận nghiệp vụ, và quan trọng không kém: yêu cầu nó nêu rõ đây là giả thuyết, không phải sự thật đã xác nhận.

Một hệ quả thực dụng của cách đọc theo lớp: bạn không đưa cả 1800 dòng vào context cùng lúc. Bạn đưa entry point trước, để AI xây context về "cái này được gọi từ đâu", rồi mới đưa dần các hàm được gọi theo đúng thứ tự luồng chạy. Cách này vừa tiết kiệm context window, vừa buộc AI (và cả bạn) xây dựng hiểu biết tăng dần đúng như cách một kỹ sư mới đọc code thật sự nên làm, thay vì nhồi toàn bộ file và nhận về một bản tóm tắt phẳng, thiếu phân cấp quan trọng/không quan trọng.

Thực hành

Giai đoạn 1 — Prompt giải thích tổng quan module + sinh sơ đồ luồng

Dùng ở bước đầu tiên khi bạn chưa biết gì về module. Mục tiêu: có bức tranh tổng thể trước khi đọc chi tiết.

Tôi đang tiếp nhận một module legacy không có tài liệu, tác giả gốc đã nghỉ việc.
Tôi sẽ dán vào đây: (1) entry point của module — nơi nó được gọi vào
(REST controller / cron job / message consumer), và (2) danh sách tên các hàm
private mà entry point gọi tới (chỉ tên hàm và signature, chưa cần thân hàm).

Nhiệm vụ của bạn — CHỈ dựa trên thông tin tôi cung cấp, KHÔNG suy đoán những gì
không thấy:

1. Xác định: module này được kích hoạt trong hoàn cảnh nghiệp vụ nào? (ví dụ:
   "được gọi mỗi khi đơn hàng chuyển sang trạng thái COMPLETED")
2. Vẽ sơ đồ luồng gọi chính (happy path) dạng mermaid flowchart — chỉ luồng
   thành công, chưa cần các nhánh lỗi/edge case.
3. Liệt kê các hàm mà signature của nó gợi ý có khả năng chứa business rule
   đặc biệt (tên hàm có "special", "legacy", "override", "exception", hoặc
   có nhiều tham số boolean/flag — đây thường là dấu hiệu của rule ẩn).
4. Với mỗi hàm ở mục 3, nêu RÕ: "Tôi cần xem thân hàm này để xác nhận vì
   [lý do]" — không tự bịa nội dung hàm khi chưa thấy code.
5. Đánh giá mức độ tin cậy tổng thể của phần tóm tắt này (cao/trung bình/thấp)
   dựa trên lượng thông tin đã có.

--- ENTRY POINT ---
[dán code entry point, ví dụ CommissionController.calculateForOrder()]

--- DANH SÁCH HÀM ĐƯỢC GỌI (chỉ signature) ---
[dán danh sách, ví dụ:
 - private BigDecimal getBaseRate(Customer customer, Order order)
 - private boolean isEligibleForLegacyPricing(Customer customer)
 - private BigDecimal applyRegionalAdjustment(BigDecimal rate, String region, Date orderDate)
 - private BigDecimal applyVipOverride(BigDecimal rate, Customer customer, Order order)]

Kết quả kỳ vọng ở giai đoạn này là một sơ đồ mermaid dạng:

flowchart LR
    A[Order chuyển COMPLETED] --> B[CommissionController.calculateForOrder]
    B --> C[getBaseRate]
    C --> D{isEligibleForLegacyPricing?}
    D -->|true| E[applyRegionalAdjustment]
    D -->|false| F[applyVipOverride]
    E --> G[Lưu commission_record]
    F --> G

Có sơ đồ này trong tay, bạn đã tiết kiệm được buổi đầu tiên vốn phải dùng để "đọc lướt xem nó gọi gì" bằng tay.

Giai đoạn 2 — Prompt suy luận nghiệp vụ ẩn từ một đoạn code cụ thể

Dùng khi bạn đã khoanh vùng được đoạn code nghi ngờ chứa rule đặc biệt (thường là điều kiện if phức tạp, hoặc hàm có tên mơ hồ như applyRegionalAdjustment, checkSpecialCase).

Đây là một hàm trong hệ thống tính hoa hồng bán hàng, viết khoảng 8-9 năm
trước, không có comment, tác giả đã nghỉ việc. Tôi cần hiểu QUY TẮC NGHIỆP VỤ
đứng sau điều kiện, không chỉ cú pháp.

Yêu cầu:
1. Với MỖI điều kiện rẽ nhánh trong hàm, hãy nêu giả thuyết về quy tắc kinh
   doanh mà nó đang bảo vệ — trả lời câu hỏi "nếu điều kiện này SAI thì hệ quả
   nghiệp vụ gì sẽ xảy ra?" thay vì chỉ dịch code sang lời văn.
2. Đánh dấu rõ mức độ tự tin cho từng giả thuyết: [Cao] nếu có bằng chứng
   trực tiếp trong code (tên biến, magic number khớp với logic rõ ràng),
   [Trung bình] nếu suy luận hợp lý nhưng cần xác nhận, [Thấp] nếu chỉ là
   phỏng đoán vì thiếu ngữ cảnh.
3. Chỉ ra magic number nào đáng ngờ (ví dụ ngưỡng 5000000, mã region "MT",
   flag "LEGACY_PRICING") và các khả năng nó đại diện cho — kèm câu hỏi
   cụ thể tôi nên hỏi người liên quan (nếu còn) hoặc dữ liệu tôi nên tra
   để xác nhận.
4. Đề xuất tên biến/hàm mới rõ nghĩa hơn CHO TỪNG điều kiện, dựa trên giả
   thuyết ở mục 1 — nhưng đừng đề xuất refactor code, chỉ đề xuất tên gọi.
5. Nếu có điều kiện nhìn giống nhau nhưng khả năng cao đại diện cho hai rule
   nghiệp vụ khác nhau (do lịch sử sửa nhiều lần), hãy chỉ rõ ra thay vì
   gộp chung một cách diễn giải.

--- CODE ---
private BigDecimal applyRegionalAdjustment(BigDecimal rate, Customer customer, Order order) {
    if (customer.getType() == 3 && order.getTotal().compareTo(new BigDecimal("5000000")) > 0
            && !order.hasFlag("LEGACY_PRICING")) {
        if (customer.getRegion().equals("MT") && order.getCreatedAt().before(CUTOFF_DATE_2018)) {
            rate = rate.multiply(new BigDecimal("0.85"));
        } else if (customer.getRegion().equals("MT")) {
            rate = rate.multiply(new BigDecimal("0.92"));
        }
    }
    return rate;
}

Một phản hồi AI hữu ích cho ví dụ trên thường trông giống thế này (đây là ví dụ minh họa, không phải câu trả lời "đúng" duy nhất):

Giả thuyết cho điều kiện ngoài cùng (customer.type == 3 && total > 5,000,000
&& !hasFlag("LEGACY_PRICING")):
[Trung bình] Đây có thể là điều kiện "khách hàng doanh nghiệp (type 3) với
đơn hàng lớn, KHÔNG thuộc diện giá cũ" — tức rule áp dụng ưu đãi region chỉ
dành cho khách hàng mới/lớn, khách đã có giá legacy thì không được cộng dồn
ưu đãi. Cần xác nhận: type == 3 có đúng là "doanh nghiệp" không, hay là một
enum khác (VIP, đại lý...)? Nên tra bảng enum CustomerType trong code hoặc DB.

Giả thuyết cho nhánh region MT + trước CUTOFF_DATE_2018 (hệ số 0.85):
[Thấp] Có khả năng đây là ưu đãi đặc biệt cho một chiến dịch khuyến mãi
miền Trung kết thúc vào 2018 (có thể liên quan thiên tai/hỗ trợ khu vực —
suy luận này KHÔNG có bằng chứng trực tiếp trong code, chỉ là một khả năng
hợp lý dựa trên hệ số ưu đãi mạnh hơn 0.92 và mốc cắt cứng). Nên hỏi phòng
Kinh doanh/Tài chính có lưu chương trình khuyến mãi nào theo mốc 2018 không,
hoặc tra bảng promotion_campaigns nếu còn lưu dữ liệu lịch sử.

Giả thuyết cho nhánh region MT sau CUTOFF_DATE_2018 (hệ số 0.92):
[Trung bình] Có thể là chính sách ưu đãi khu vực dài hạn hơn, mức thấp hơn
chương trình khuyến mãi tạm thời ở trên.

Đề xuất đặt tên lại:
- isEligibleForLegacyPricing → có thể đổi hướng đọc: điều kiện ngoài cùng nên
  tách thành `isNewLargeEnterpriseCustomer(customer, order)`
- Hệ số 0.85 → hằng số nên đặt tên `MIENTRUNG_PROMO_2018_DISCOUNT_RATE` thay
  vì magic number 0.85
- Hệ số 0.92 → `MIENTRUNG_STANDARD_REGIONAL_DISCOUNT_RATE`

Cảnh báo: hai nhánh MT trông giống nhau (cùng region, khác mốc thời gian)
nhưng CÓ THỂ đại diện cho hai rule nghiệp vụ hoàn toàn khác nhau (một là
khuyến mãi tạm thời, một là chính sách dài hạn) — đừng gộp chúng thành
"ưu đãi miền Trung" chung chung khi viết tài liệu, vì như vậy sẽ mất thông
tin quan trọng nếu sau này cần bỏ khuyến mãi 2018 mà giữ chính sách dài hạn.

Điểm mấu chốt: AI trả lời với mức độ tự tin phân tầng rõ ràng ([Cao]/[Trung bình]/[Thấp]) — đây không phải chi tiết trang trí, nó là phần bắt buộc bạn phải yêu cầu, vì nếu không, AI có xu hướng trả lời với giọng điệu chắc chắn như nhau cho mọi giả thuyết, kể cả giả thuyết chỉ là phỏng đoán suông.

Cấu trúc file tài liệu tự động sinh ra

Sau khi chạy hai giai đoạn trên cho toàn bộ module, tổng hợp kết quả vào một file markdown theo cấu trúc cố định — dùng cấu trúc này làm prompt template luôn để AI tự điền, tránh mỗi module một định dạng khác nhau:

# [Tên module] — Tài liệu khảo cổ (auto-generated, cần review)

> Tài liệu này được sinh bằng AI dựa trên đọc code, CHƯA được xác nhận với
> người phụ trách nghiệp vụ. Mức độ tin cậy được đánh dấu ở từng mục.
> Ngày sinh: [ngày] | Người review lần cuối: [chưa có / tên người]

## Tổng quan module
- Module này làm gì (1 đoạn, ngôn ngữ nghiệp vụ, không phải mô tả code):
- Được gọi khi nào / bởi ai (entry point, trigger):
- Input chính / Output chính:
- Các module/service phụ thuộc:

## Luồng chính
```mermaid
[sơ đồ luồng happy path từ Giai đoạn 1]
  • Mô tả từng bước bằng lời (map với sơ đồ trên):

Các trường hợp đặc biệt / edge case

Điều kiện trong code Giả thuyết nghiệp vụ Độ tin cậy Cần xác nhận với
customer.type==3 && total>5tr && !LEGACY_PRICING Khách DN lớn, không thuộc giá cũ Trung bình Phòng Kinh doanh
region=="MT" && date<2018-cutoff Khuyến mãi miền Trung tạm thời (2018) Thấp Phòng Tài chính / dữ liệu promotion_campaigns
... ... ... ...

Rủi ro khi sửa

  • Nếu sửa điều kiện X mà không xác nhận giả thuyết Y: hệ quả gì (dữ liệu sai lệch, hoa hồng tính sai hàng loạt, v.v.)
  • Chỗ nào KHÔNG có test coverage — cần viết test approval/characterization trước khi đổi bất cứ gì (xem Bài 7 về Automated Unit Testing)
  • Chỗ nào code này được các module khác phụ thuộc ngầm (side effect, shared state)

File này lưu cùng thư mục với module (`docs/legacy/commission-calculator.md` chẳng hạn), commit vào git, và cập nhật trạng thái "đã xác nhận" theo từng dòng sau khi bạn thực sự hỏi được người liên quan hoặc tra được dữ liệu lịch sử.

## Cạm bẫy thường gặp

**1. Coi giả thuyết nghiệp vụ của AI như sự thật đã kiểm chứng.** Đây là cạm bẫy nguy hiểm nhất và cũng dễ mắc nhất, vì output của AI thường được viết bằng giọng văn tự tin, mạch lạc — rất dễ đọc xong tin luôn. AI suy luận từ pattern trong code (tên biến, magic number, cấu trúc điều kiện), không phải từ việc "biết" ý định gốc của tác giả. Một điều kiện `region == "MT" && date < CUTOFF` trông giống hệt một khuyến mãi khu vực, nhưng cũng có thể là workaround tạm thời cho một bug về múi giờ trong hệ thống logistics — trông giống nhau về mặt cú pháp nhưng bản chất hoàn toàn khác. Luôn coi output giai đoạn 2 là "điểm khởi đầu để hỏi", không phải "câu trả lời để copy vào tài liệu chính thức".

**2. Bỏ qua bước xác nhận qua dữ liệu thực tế vì "không còn ai để hỏi".** Khi tác giả gốc đã nghỉ việc và không ai trong team nhớ rule, nhiều kỹ sư bỏ luôn bước xác nhận và coi giả thuyết AI là kết luận cuối. Nhưng vẫn còn một nguồn xác nhận độc lập không phụ thuộc trí nhớ con người: dữ liệu lịch sử. Truy vấn database xem có bao nhiêu bản ghi thực tế rơi vào từng nhánh điều kiện, đối chiếu với ngày tháng, số liệu tài chính, hoặc changelog/ticket cũ (nếu công ty còn lưu Jira/email cũ) — đây là cách kiểm chứng giả thuyết rẻ và đáng tin hơn nhiều so với tin tưởng suông.

**3. Dùng AI tóm tắt cả file 1800 dòng một lần rồi coi đó là tài liệu hoàn chỉnh.** Tóm tắt một lần, không theo lớp, thường tạo ra một bản mô tả phẳng — liệt kê tất cả các hàm ở cùng một mức độ quan trọng, không phân biệt được đâu là logic cốt lõi, đâu là edge case hiếm khi chạy tới. Kết quả là tài liệu đọc xong vẫn không giúp bạn tự tin sửa code, vì bạn không biết phần nào rủi ro cao. Luôn đọc theo lớp (entry point → luồng chính → chi tiết) và luôn tách riêng phần "Rủi ro khi sửa" thành mục có trọng số cao nhất trong tài liệu.

> 🧭 **Góc nhìn Solutions Architect**
> Khi review một tài liệu do AI sinh ra cho một module legacy, câu hỏi đầu tiên nên là: "phần nào trong tài liệu này là suy luận chưa xác nhận, và ai có thể xác nhận nó?" — nếu không trả lời được, tài liệu chưa sẵn sàng để dùng làm căn cứ sửa code. Câu hỏi thứ hai: liệu module này có đáng để đầu tư viết characterization test (chụp lại hành vi hiện tại) trước khi động vào, thay vì chỉ dựa vào tài liệu mô tả? Và câu hỏi thứ ba, mang tính chiến lược hơn: nếu team liên tục phải "khảo cổ" module này, đó có phải tín hiệu cho thấy nó cần được thiết kế lại thành một service tách biệt với business rule tường minh, thay vì tiếp tục vá thêm điều kiện?

## 🔗 Bài viết liên quan

- [Bài 5 — Refactoring with AI: Clean Architecture](ans-05-refactoring-with-ai-clean-architecture.md) — sau khi hiểu rõ nghiệp vụ ẩn, đây là bước tiếp theo để tái cấu trúc module một cách an toàn.
- [Bài 7 — Automated Unit Testing: AI viết test](ans-07-automated-unit-testing-ai-viet-test.md) — cách dùng AI viết characterization test để "chốt" lại hành vi hiện tại trước khi sửa bất cứ điều kiện nào đã suy luận ở bài này.

---

Bài trước: [Refactoring with AI: Clean Architecture](ans-05-refactoring-with-ai-clean-architecture.md) · Bài tiếp theo: [Automated Unit Testing: AI viết test](ans-07-automated-unit-testing-ai-viet-test.md)
AI & Legacy Code: Cách dùng AI để đọc và viết tài liệu cho các hệ thống \"di sản\" 10 năm tuổi