AI-Native Solutions Architect

AI-Assisted Debugging: Kỹ thuật bắt bug bằng cách cho AI đọc stack trace và logs

7/19/2026 · 12p đọc


title: "AI-Assisted Debugging: Kỹ thuật bắt bug bằng cách cho AI đọc stack trace và logs"
series: "AI-Native Solutions Architect: Từ Coder đến Kiến trúc sư AI"
season: "Season 1 — AI-Native Coding & Tooling"
order: 4
audience: "Software Engineer hướng tới Solutions Architect"
reading_time: "12 phút"
tags:

  • ai-native-coding
  • debugging
  • stack-trace
  • prompt-engineering
  • solutions-architect

AI-Assisted Debugging: Kỹ thuật bắt bug bằng cách cho AI đọc stack trace và logs

2 giờ sáng, production ném ra một TypeError: Cannot read properties of undefined (reading 'unitPrice'). Bạn copy nguyên stack trace, dán vào chat AI, gõ thêm "fix giúp lỗi này", rồi Enter. Ba giây sau AI trả lời: "Lỗi này thường do biến bị undefined khi truy cập thuộc tính. Bạn nên kiểm tra và thêm optional chaining (?.) hoặc validate dữ liệu đầu vào trước khi sử dụng." Đúng về mặt lý thuyết, vô dụng về mặt thực hành — vì nó không nói được undefined ở đâu ra, tại sao nó undefined, và thêm ?. ở dòng nào thì che bug thay vì sửa bug.

Đây là trải nghiệm debug-với-AI phổ biến nhất, và cũng là lý do nhiều kỹ sư kết luận "AI debug không dùng được, chỉ hợp viết code mới". Kết luận đó sai một nửa. Vấn đề không nằm ở khả năng suy luận của model — GPT/Claude-class model hoàn toàn có thể truy vết logic phức tạp — mà nằm ở việc chúng ta cho nó quá ít để suy luận. Một stack trace đơn lẻ giống như đưa bác sĩ tờ kết quả X-quang mà giấu hết bệnh sử, triệu chứng, và kết quả xét nghiệm máu. Bác sĩ giỏi đến đâu cũng chỉ đoán được diện rộng.

Bài này đi thẳng vào kỹ thuật cụ thể: cấu trúc 4 lớp thông tin cần đưa cho AI cùng lúc để nó debug hiệu quả như một senior engineer ngồi cạnh bạn, và cách buộc nó liệt kê giả thuyết thay vì phán một câu kết luận vội vàng.

Vấn đề

Khi một kỹ sư gặp lỗi và mở AI chat, hành vi mặc định gần như luôn là: copy stack trace → dán → gõ một câu ngắn gọn kiểu "lỗi gì đây" hoặc "fix giúp". Đây là request tối thiểu thông tin, và AI buộc phải lấp đầy khoảng trống bằng suy đoán thống kê — nó trả lời dựa trên "trong 10,000 lần thấy TypeError: Cannot read properties of undefined trước đây, nguyên nhân phổ biến nhất là X" chứ không phải dựa trên logic thực tế của codebase bạn.

Ba lỗ hổng ngữ cảnh phổ biến nhất:

1. Stack trace bị cắt bớt. Nhiều người chỉ paste 3-4 dòng đầu (nơi lỗi nổ ra) và bỏ qua phần còn lại của call stack — chính là nơi chứa manh mối về luồng gọi dẫn tới lỗi. Dòng lỗi cho biết lỗi xảy ra ở đâu; phần stack còn lại cho biết dữ liệu đi qua những hàm nào trước khi tới đó.

2. Thiếu code liên quan. AI được hỏi về một dòng lỗi nhưng không thấy được hàm chứa dòng đó, không thấy các hàm gọi nó, không thấy type/interface của dữ liệu. Nó chỉ có tên biến và một dòng số — không đủ để xác định nguồn gốc.

3. Thiếu log ngay trước lỗi. Log trước thời điểm crash cho biết trạng thái hệ thống lúc đó: request nào đang chạy, dữ liệu nào vừa được load, service nào vừa trả về response bất thường. Thiếu phần này, AI không phân biệt được "lỗi luôn xảy ra" với "lỗi chỉ xảy ra trong một race condition hiếm gặp".

Hậu quả của cả ba lỗ hổng cộng lại: AI trả lời đúng ngữ pháp, sai ngữ cảnh — và tệ hơn, câu trả lời nghe rất tự tin nên kỹ sư có xu hướng tin theo, áp fix vào, rồi bug tái xuất hiện ở production tuần sau dưới dạng khác.

Kỹ thuật cốt lõi

Debug hiệu quả — dù với người hay với AI — về bản chất là một bài toán thu hẹp không gian giả thuyết (hypothesis space) bằng bằng chứng (evidence). AI không thiếu khả năng suy luận; nó thiếu bằng chứng. Kỹ thuật cốt lõi của bài này là cung cấp đủ 4 lớp bằng chứng cùng một lúc, trong cùng một prompt, thay vì để AI hỏi lại nhiều vòng (tốn thời gian) hoặc đoán bừa (tốn công sửa sai).

flowchart TD
    A["Lớp 1: Stack trace đầy đủ<br/>(không cắt bớt)"] --> E["Prompt debug<br/>đầy đủ ngữ cảnh"]
    B["Lớp 2: Code liên quan<br/>(hàm lỗi + hàm gọi nó)"] --> E
    C["Lớp 3: Log/output<br/>ngay trước lỗi"] --> E
    D["Lớp 4: Expected vs Actual<br/>behavior"] --> E
    E --> F["Yêu cầu AI liệt kê<br/>NHIỀU giả thuyết"]
    F --> G["Tự verify từng giả thuyết<br/>bằng cách chạy thử / thêm log"]
    G --> H["Kết luận nguyên nhân gốc<br/>+ fix có review"]

Bốn lớp thông tin, và lý do mỗi lớp cần thiết:

  1. Stack trace đầy đủ — không chỉ dòng crash mà toàn bộ chuỗi gọi hàm. Đây là "grep có định hướng": nó cho AI biết chính xác thứ tự các hàm đã thực thi, giúp AI phân biệt lỗi ở tầng data access, tầng business logic, hay tầng transport/serialize.

  2. Code liên quan trực tiếp — không chỉ dòng bị lỗi mà cả hàm chứa nó VÀ các hàm gọi nó (caller). Lý do: hầu hết bug "undefined property" không nằm ở dòng crash, mà nằm ở nơi dữ liệu được tạo ra hoặc truyền vào sai từ vài bước trước đó. AI cần thấy contract giữa các hàm để suy luận ngược.

  3. Log/output ngay trước lỗi — cho biết trạng thái runtime thực tế tại thời điểm xảy ra sự cố: request payload, kết quả query, response từ service ngoài. Đây là bằng chứng thực nghiệm, khác với code (chỉ là logic tĩnh).

  4. Expected vs Actual behavior — một câu mô tả ngắn gọn giúp AI không lạc đề. Nhiều lỗi không crash ở exception mà biểu hiện là "kết quả sai" (silent bug); nếu không nói rõ bạn mong đợi gì, AI không biết đang tìm cái gì.

Kỹ thuật quan trọng thứ hai, đi kèm 4 lớp trên: buộc AI liệt kê nhiều giả thuyết (multiple hypotheses) trước khi kết luận. Nếu bạn hỏi "nguyên nhân là gì", model có xu hướng chọn giả thuyết có xác suất prior cao nhất và trình bày nó như sự thật — đây là hành vi "convergence sớm" khá giống thiên kiến xác nhận (confirmation bias) ở con người. Nếu bạn yêu cầu rõ ràng "liệt kê ít nhất 3 giả thuyết, xếp theo khả năng, kèm cách kiểm chứng từng giả thuyết", model buộc phải giữ không gian giả thuyết mở lâu hơn, và bạn — người có toàn quyền truy cập hệ thống thật — sẽ là người quyết định giả thuyết nào đúng bằng cách chạy thử.

Thực hành

Dưới đây là một ví dụ đầy đủ: một lỗi TypeError xảy ra trong hàm xử lý đơn hàng (order processing) của một service NestJS giả định, cùng prompt mẫu áp dụng đúng 4 lớp thông tin.

Tình huống lỗi

Giả sử service có hàm tính tổng tiền đơn hàng:

// order-pricing.service.ts
export class OrderPricingService {
  calculateOrderTotal(order: Order): number {
    let total = 0;
    for (const item of order.items) {
      // Dòng gây lỗi: item.product có thể undefined
      total += item.product.unitPrice * item.quantity;
    }
    return applyDiscount(total, order.discountCode);
  }
}

// order.service.ts
export class OrderService {
  constructor(
    private readonly pricingService: OrderPricingService,
    private readonly productRepo: ProductRepository,
  ) {}

  async createOrder(dto: CreateOrderDto): Promise<Order> {
    const products = await this.productRepo.findByIds(
      dto.items.map((i) => i.productId),
    );

    // Map sản phẩm vào từng item — nghi vấn: nếu productRepo trả về
    // thiếu 1 sản phẩm (VD đã bị xoá/deactivate), item.product sẽ undefined
    const order: Order = {
      items: dto.items.map((i, idx) => ({
        product: products[idx], // <-- lắp theo INDEX, không theo ID
        quantity: i.quantity,
      })),
      discountCode: dto.discountCode,
    };

    return this.pricingService.calculateOrderTotal(order) as any;
  }
}

Stack trace production ném ra:

TypeError: Cannot read properties of undefined (reading 'unitPrice')
    at OrderPricingService.calculateOrderTotal (order-pricing.service.ts:7:29)
    at OrderService.createOrder (order.service.ts:22:45)
    at OrderController.create (order.controller.ts:18:41)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async /app/dist/main.js:1:22188

Log ngay trước khi crash (từ CloudWatch/pino):

{"level":"info","msg":"createOrder started","orderPayload":{"items":[{"productId":"p_101","quantity":2},{"productId":"p_deleted_205","quantity":1}],"discountCode":"SUMMER10"}}
{"level":"info","msg":"productRepo.findByIds resolved","foundCount":1,"requestedCount":2}

Chú ý: foundCount: 1 trong khi requestedCount: 2 — đây chính là log giúp AI (và bạn) phát hiện productRepo.findByIds trả về mảng ngắn hơn số lượng yêu cầu khi một sản phẩm không tồn tại, nhưng code lại map theo idx thay vì theo productId, gây lệch mảng.

Prompt mẫu (khung dùng lại mỗi lần debug)

Tôi cần debug một lỗi runtime trong hệ thống NestJS. Đọc kỹ 4 phần dưới đây,
KHÔNG bỏ qua phần nào, rồi liệt kê ÍT NHẤT 3 giả thuyết nguyên nhân khác nhau
trước khi đưa ra kết luận — không được chốt 1 nguyên nhân duy nhất ngay lập tức.

## 1. Stack trace đầy đủ
TypeError: Cannot read properties of undefined (reading 'unitPrice')
    at OrderPricingService.calculateOrderTotal (order-pricing.service.ts:7:29)
    at OrderService.createOrder (order.service.ts:22:45)
    at OrderController.create (order.controller.ts:18:41)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async /app/dist/main.js:1:22188

## 2. Code liên quan (hàm lỗi + hàm gọi nó)
```typescript
// order-pricing.service.ts
export class OrderPricingService {
  calculateOrderTotal(order: Order): number {
    let total = 0;
    for (const item of order.items) {
      total += item.product.unitPrice * item.quantity; // dòng 7 - nơi crash
    }
    return applyDiscount(total, order.discountCode);
  }
}

// order.service.ts
export class OrderService {
  async createOrder(dto: CreateOrderDto): Promise<Order> {
    const products = await this.productRepo.findByIds(
      dto.items.map((i) => i.productId),
    );
    const order: Order = {
      items: dto.items.map((i, idx) => ({
        product: products[idx],
        quantity: i.quantity,
      })),
      discountCode: dto.discountCode,
    };
    return this.pricingService.calculateOrderTotal(order) as any;
  }
}

3. Log ngay trước lỗi (từ CloudWatch)

{"level":"info","msg":"createOrder started","orderPayload":{"items":[{"productId":"p_101","quantity":2},{"productId":"p_deleted_205","quantity":1}],"discountCode":"SUMMER10"}}
{"level":"info","msg":"productRepo.findByIds resolved","foundCount":1,"requestedCount":2}

4. Expected vs Actual

  • Expected: đơn hàng được tạo bình thường với tổng tiền tính đúng cho các
    sản phẩm hợp lệ, hoặc trả về lỗi 400 rõ ràng nếu có productId không tồn tại.
  • Actual: server crash 500 với TypeError khi có ít nhất 1 productId
    không tìm thấy trong DB.

Môi trường

  • Node.js 20, NestJS 11, TypeORM 0.3.x, MySQL 8.
  • productRepo.findByIds dùng Repository.findBy({ id: In(ids) }) của TypeORM.

Yêu cầu:

  1. Liệt kê tối thiểu 3 giả thuyết nguyên nhân, xếp theo khả năng cao->thấp.
  2. Với mỗi giả thuyết, nêu cách tôi có thể tự kiểm chứng (thêm log ở đâu,
    test case nào để tái hiện).
  3. CHỈ đề xuất fix sau khi đã liệt kê xong toàn bộ giả thuyết ở bước 1.

### Kết quả kỳ vọng khi dùng prompt trên

Với đủ 4 lớp thông tin, AI có thể liệt kê giả thuyết đúng hướng, ví dụ:

- **Giả thuyết 1 (khả năng cao nhất):** `TypeORM.findBy({ id: In(ids) })` không đảm bảo thứ tự trả về giống thứ tự `ids` truyền vào, và bỏ qua id không tồn tại thay vì trả `undefined` ở đúng vị trí — code lại map theo `idx` nên khi 1 sản phẩm bị thiếu, toàn bộ mảng từ vị trí đó bị lệch. Bằng chứng: `foundCount: 1` khác `requestedCount: 2`.
- **Giả thuyết 2:** race condition — sản phẩm `p_deleted_205` bị xoá/deactivate giữa lúc validate và lúc query, cần kiểm tra thêm bằng cách log trạng thái sản phẩm tại thời điểm tạo đơn.
- **Giả thuyết 3 (khả năng thấp hơn):** lỗi ở tầng cache — nếu `productRepo` có lớp cache phía trước, dữ liệu cache có thể stale so với DB.

Từ đây, fix đúng không phải là thêm `item.product?.unitPrice ?? 0` (che triệu chứng), mà là sửa `OrderService.createOrder` để map sản phẩm theo `productId` thay vì `idx`, và validate đủ số lượng sản phẩm tìm thấy trước khi build `Order`:

```typescript
async createOrder(dto: CreateOrderDto): Promise<Order> {
  const products = await this.productRepo.findByIds(
    dto.items.map((i) => i.productId),
  );
  const productMap = new Map(products.map((p) => [p.id, p]));

  const missing = dto.items
    .map((i) => i.productId)
    .filter((id) => !productMap.has(id));
  if (missing.length > 0) {
    throw new BadRequestException(
      `Không tìm thấy sản phẩm: ${missing.join(', ')}`,
    );
  }

  const order: Order = {
    items: dto.items.map((i) => ({
      product: productMap.get(i.productId)!,
      quantity: i.quantity,
    })),
    discountCode: dto.discountCode,
  };
  return this.pricingService.calculateOrderTotal(order) as any;
}

Đây chính là điểm khác biệt giữa "AI đoán bừa một hướng" và "AI hỗ trợ tìm root cause": fix cuối cùng không đụng tới dòng crash (order-pricing.service.ts:7) chút nào — nó sửa ở lớp gọi phía trên, đúng như manh mối từ stack trace tầng 2 đã chỉ ra.

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

1. Tin kết luận đầu tiên mà không tự verify. Ngay cả khi đã cho đủ 4 lớp ngữ cảnh, AI vẫn có thể chọn sai giả thuyết #1 nếu log không đủ rõ ràng. Luôn tự chạy lại (thêm log tạm, viết unit test tái hiện lỗi, hoặc chạy debugger) trước khi merge fix — đừng coi câu trả lời của AI là bằng chứng, coi nó là một giả thuyết cần kiểm chứng như bất kỳ giả thuyết nào khác trong quy trình debug.

2. Thiếu ngữ cảnh môi trường khiến fix đề xuất không áp dụng được. Một fix dùng optional chaining ?. giả định Node.js/TypeScript version hỗ trợ, hoặc dùng API của một ORM version khác với version thực tế đang chạy (ví dụ hành vi findBy với In() có thể khác giữa TypeORM 0.2.x và 0.3.x). Luôn nêu rõ version Node, framework, ORM, và các dependency liên quan trực tiếp tới đoạn code lỗi — thiếu phần này, AI sẽ mặc định giả sử phiên bản mới nhất hoặc phổ biến nhất, dẫn tới đề xuất fix dùng API không tồn tại trong codebase của bạn.

3. Bám theo một giả thuyết sai vì AI trình bày nó quá tự tin. Model ngôn ngữ có xu hướng diễn đạt mọi kết luận bằng giọng điệu chắc chắn dù độ tin cậy thực tế thấp. Nếu không chủ động yêu cầu liệt kê nhiều giả thuyết (kỹ thuật đã nêu ở phần trên), rất dễ rơi vào vòng lặp: sửa theo giả thuyết #1 → bug vẫn còn → hỏi lại AI → AI vẫn tự tin bám theo đúng hướng cũ chỉ vì đó là điều nó vừa nói trước đó (anchoring theo lịch sử hội thoại). Khi thấy dấu hiệu này, hãy mở conversation mới và diễn đạt lại toàn bộ 4 lớp thông tin từ đầu thay vì tiếp tục hỏi trong cùng thread.

🧭 Góc nhìn Solutions Architect
Khi thiết kế hệ thống logging/observability cho một service mới, bạn có đang chủ động tối ưu để AI (và con người) đọc log dễ dàng hơn — structured log kèm correlation ID, đủ ngữ cảnh request tại điểm lỗi — hay log chỉ đang được viết theo thói quen "in ra cho có"? Nếu một bug xảy ra ở production lúc 2 giờ sáng, đội trực có đủ 4 lớp thông tin (stack trace, code, log, expected/actual) để đưa ngay vào AI trong vòng 5 phút, hay phải mất nửa tiếng đi tìm từng mảnh?

🔗 Bài viết liên quan


Bài trước: Advanced Prompting · Bài tiếp theo: Refactoring with AI

AI-Assisted Debugging: Kỹ thuật bắt bug bằng cách cho AI đọc stack trace và logs