Bạn thức dậy vào sáng thứ Hai với năm báo cáo lỗi nghiêm trọng. Công cụ giám sát đánh giá của bạn đã hoàn thành tốt nhiệm vụ. Nó đã bắt được mọi báo cáo crash, mọi đánh giá một sao giận dữ, mọi thông báo "ứng dụng bị treo khi tôi nhấn lưu". Bạn biết chính xác cái gì đang bị hỏng. Điều bạn không biết là phải tìm ở đâu.

Đó là bức tường mà tôi đã vấp phải sau khi xây dựng pipeline đầu tiên của mình. Nó giám sát các đánh giá ứng dụng và các bản ghi crash (crash logs) gửi đến mà không gặp khó khăn gì, phân loại mỗi phản hồi vào các nhóm gọn gàng: lỗi (bugs), crash, hoặc yêu cầu tính năng. Dashboard trông rất ổn. Nhưng quy trình gỡ lỗi thực tế thì không.

Biết một lỗi tồn tại mới chỉ là bước khởi đầu của một hành trình dài. Tôi vẫn phải mở IDE, dùng grep xuyên qua các module, đối chiếu stack trace với mã nguồn hiện tại, và tái dựng lại lộ trình gây lỗi trong đầu. Khi các ticket đang chất đống và cà phê vẫn còn nóng, việc "khảo cổ học" thủ công đó sẽ ngốn hết khoảng thời gian mà bạn vốn không có. Tôi cần pipeline làm được nhiều hơn là chỉ gắn cờ các vấn đề. Tôi cần nó điều tra chúng.

Vì vậy, tôi đã xây dựng lại hệ thống xoay quanh một mục tiêu duy nhất: tiếp nhận một báo cáo lỗi thô và trả về một chẩn đoán đã được xác thực. Không phải là một đoạn văn dài dòng suy diễn của LLM. Mà là một kết quả có cấu trúc, chỉ rõ tên tệp, trỏ đến dòng code, ước tính rủi ro và đề xuất cách sửa. Dưới đây là cách nó được thực hiện.

Tại sao Cấu trúc lại ưu việt hơn Nhật ký Chat

Tôi đã xây dựng agent điều tra bằng PydanticAI. Lý do rất đơn giản. Khi bạn yêu cầu một mô hình ngôn ngữ suy luận về mã nguồn, đầu ra mặc định của nó là một dòng văn bản thân thiện. Điều đó có thể giúp ích cho người đọc, nhưng lại vô dụng đối với một script xử lý phía sau. Tôi cần một "hợp đồng" mà máy tính có thể đọc được.

Agent trả về một mô hình dữ liệu đã được xác thực với bốn trường cụ thể: nguyên nhân gốc rễ, các tệp bị ảnh hưởng, các thay đổi đề xuất, và đánh giá về độ phức tạp cũng như rủi ro. Nếu mô hình thiếu một trường hoặc "ảo giác" ra một đường dẫn tệp không tồn tại, quá trình xác thực sẽ thất bại và tôi có thể phát hiện ngay lập tức. Sự nghiêm ngặt đó giữ cho pipeline luôn trung thực.

Để thực hiện công việc thám tử thực sự, agent được cấp bốn công cụ chỉ đọc (read-only) và không có gì khác. Nó có thể tìm kiếm mã nguồn qua grep, đọc các phạm vi dòng cụ thể từ một tệp, liệt kê nội dung thư mục, và định vị các ký hiệu như class hoặc function. "Chỉ đọc" là phần quan trọng nhất. Tôi không muốn một agent có quyền ghi dữ liệu đi lang thang trong repository của mình vào lúc 2 giờ sáng. Hiểu trước, sửa sau.

Bản đồ Repo: Ngữ cảnh trước khi dùng Công cụ

Phiên bản đầu tiên của agent tuy chính xác nhưng cực kỳ tốn kém. Nó tiêu tốn token như một khách du lịch đi vòng quanh một chỗ. Mô hình sẽ gọi list-dir, sau đó grep, rồi đọc một tệp, rồi lại list-dir, chậm chạp lắp ghép mô hình tư duy về cấu trúc dự án từng token đắt đỏ một.

Giải pháp là tạo ra một bản đồ repo (repo map) tinh gọn trước khi agent bắt đầu. Bản đồ này là một bản tóm tắt cô đọng của repository: các tệp chính, các hàm hoặc class chính của chúng, và cách các module lớn kết nối với nhau. Hãy coi nó như việc đưa cho agent một thiết bị GPS thay vì yêu cầu nó tự khám phá các con đường bằng cách thử và sai.

Với bản đồ đó trong context window, agent không lãng phí các lượt gọi để tìm xem src/utils/parser.ts có tồn tại hay không. Nó đã biết rõ địa hình. Nó tiến thẳng đến nơi có khói đang bốc lên. Thay đổi duy nhất đó đã loại bỏ hoàn toàn giai đoạn đi lòng vòng.

Phễu Công cụ: Ép buộc đưa ra Kết luận

Ngay cả khi có bản đồ, agent vẫn có thể do dự. Nó sẽ tìm thấy một tệp khả nghi, sau đó tự nghi ngờ chính mình, rồi lại tìm kiếm lần nữa, rồi đọc một tệp khác, bị kẹt trong một vòng lặp vô tận của việc "chỉ kiểm tra thêm một chút nữa thôi". Tôi cần một cách để tạo ra đà tiến tới.

Tôi đã triển khai một phễu công cụ ba giai đoạn nhằm giới hạn những gì agent có thể làm khi nó tiến triển.

Giai đoạn một là khám phá. Agent có toàn quyền truy cập vào cả bốn công cụ. Nó có thể tìm kiếm, duyệt và đọc bất cứ thứ gì nó cần để tái hiện lỗi trong quá trình suy luận.

Giai đoạn hai là đi sâu vào chi tiết (deep-dive). Một khi agent đã xác định được các điểm lỗi tiềm năng, nó sẽ mất đi các công cụ khám phá. Nó chỉ có thể đọc các tệp. Không còn grep, không còn liệt kê thư mục. Ở giai đoạn này, nó phải nghiên cứu mã nguồn đã tìm thấy và xây dựng chuỗi bằng chứng của mình.

Giai đoạn ba là xuất kết quả. Tất cả các công cụ đều bị khóa lại. Agent không thể truy vấn mã nguồn được nữa. Nó phải ngồi xuống và viết báo cáo. Điều này ngăn chặn vòng xoáy "để tôi kiểm tra thêm một thứ nữa" không hồi kết.

Cái phễu đó đã giảm số lượng lượt gọi công cụ trung bình từ hơn bốn mươi lượt mỗi lần phân tích xuống còn khoảng mười lượt. Agent trở nên nhanh hơn, rẻ hơn, và nghịch lý thay, tự tin hơn vì nó buộc phải đưa ra một kết luận cuối cùng.

Giữ cho Backend có thể thay thế linh hoạt

I did not want to hardcode the system against a single model provider. I use different engines depending on the task. Sometimes Claude Code, sometimes Grok Build, sometimes whatever is cheapest at the moment. To keep the core logic provider-agnostic, I split the work into two stages.

Stage one is exploration. The coding agent, which can be any capable model, reads the repo map, uses the tools, and produces a raw markdown report. This is the expensive thinking part.

Stage two is structuring. A cheap, fast LLM takes that markdown and reformats it into the strict Pydantic model. This stage requires almost no reasoning. It is just extraction and formatting, so it runs on lightweight hardware.

Because the boundary is clean, I can swap the backend without touching the validation logic. The markdown report acts as a universal adapter between the exploratory brain and the structured output I actually use.

What Actually Worked

This setup changed how I handle incoming issues. The classification layer still sorts bugs from feature requests, but now the analysis layer picks up immediately after. By the time I open my editor, I have a file path, a line range, and a proposed change waiting for me. I still review everything manually. This is assistance, not autopilot. But the context gathering that used to