Bỏ qua để đến nội dung

Trí tuệ mã nguồn

Control Center cho agent bốn cách để hiểu mã nguồn ngoài việc đọc nó. Chúng trả lời những câu hỏi thực sự khác nhau, và lý do có bốn cách chứ không phải một là vì không có cơ chế đơn lẻ nào trả lời được tất cả.

Cơ chế Trả lời Chi phí
Đồ thị mã Có gì ở đây, và cái gì phụ thuộc vào nó Một lượt lập chỉ mục; truy vấn thì rẻ
Máy chủ ngôn ngữ Đoạn này có thực sự biên dịch được không, và kiểu này là gì Một tiến trình máy chủ cho mỗi dự án
Tìm kiếm cấu trúc HÌNH DẠNG này xuất hiện ở đâu Một lần phân tích cú pháp cho mỗi tệp
Trình gỡ lỗi Điều gì đúng vào thời điểm này, trong khung này Một tiến trình bị dừng

Khoảng trống mà máy chủ ngôn ngữ lấp đầy

Phần tiêu đề “Khoảng trống mà máy chủ ngôn ngữ lấp đầy”

Đồ thị mã được dựng từ tree-sitter: ký hiệu, cạnh gọi, bán kính ảnh hưởng. Nó nhanh, nó rẻ, và nó không biết gì về kiểu. Nó không thể cho bạn biết lời gọi bạn vừa viết có biên dịch được không, trường đó có tồn tại trên đối tượng không, hay lần đổi tên của bạn có bỏ sót một lần tái xuất nào không.

Không có máy chủ ngôn ngữ, agent chỉ biết được điều đó khi chạy kiểm thử — hoặc không bao giờ. Nó viết ra một thứ trông có vẻ hợp lý, rồi đi tiếp, và lỗi chỉ nổi lên ba lượt sau, gắn với một thay đổi khác.

Vậy nên lợi ích thật sự không nằm ở công cụ lsp. Mà là ở chỗ write, edit và apply_patch được bọc lại: sau mỗi lần ghi, tệp đi qua máy chủ ngôn ngữ và những chẩn đoán mới xuất hiện được gộp vào kết quả công cụ. Agent biết mình vừa làm hỏng bản dựng ngay tại lúc nó làm hỏng, bất kể nó có nghĩ đến việc hỏi hay không.

Nhắc lại điều agent đã được cho biết còn tệ hơn là không nói gì: nó lấp đầy kết quả bằng thứ nhiễu mà agent sẽ học cách lướt qua, và lỗi mới duy nhất cũng bị cuốn theo. Một sổ cái theo từng lần chạy ghi lại những gì đã được báo, khóa theo mức độ nghiêm trọng, mã và thông điệp — không theo vị trí, nên một chẩn đoán chỉ dịch xuống bốn dòng sẽ không bị báo lại như thể là mới.

Khởi động muộn, và trung thực khi không hiện diện

Phần tiêu đề “Khởi động muộn, và trung thực khi không hiện diện”

Không có máy chủ nào khởi động cùng lúc bật máy. Một máy chủ chỉ khởi động ở yêu cầu đầu tiên cần đến nó — gần như luôn là lần sửa đầu tiên vào một tệp mà nó nhận quản lý — và những máy chủ nhàn rỗi sẽ bị dọn đi. Một máy không bao giờ đụng tới Dart sẽ không bao giờ khởi động trình phân tích.

Việc phát hiện là một phép giao: dấu hiệu gốc phải tồn tại trong bản checkout và tệp nhị phân phải phân giải được. Chỉ riêng dấu hiệu thì nói rằng “dự án này là Python” và không nói gì về việc máy chủ đã được cài hay chưa; chỉ riêng tệp nhị phân phân giải được thì nói rằng máy này có bộ công cụ và không nói gì về bản checkout này. Chỉ khi cả hai cùng có thì một truy vấn mới có thể thành công, và một công cụ được đưa ra nhưng luôn thất bại thì tệ hơn một công cụ không hiện diện.

Vì sao cần tìm kiếm cấu trúc khi đã có grep

Phần tiêu đề “Vì sao cần tìm kiếm cấu trúc khi đã có grep”

grep so khớp ký tự. Đó là công cụ đúng cho một cái tên và sai cho một hình dạng.

Một mẫu cấu trúc được phân tích cú pháp bằng cùng ngữ pháp với tệp, nên dispose($X) tìm thấy lời gọi bất kể nó được đặt cách, xuống dòng hay ngắt dòng thế nào, và không bao giờ khớp chỉ vì trùng ký tự bên trong một hằng chuỗi hay một chú thích.

Tính chất khiến nó không chỉ là một regex thêm vài bước là: một biến meta lặp lại trong cùng một mẫu phải bắt được cùng một đoạn văn bản:

if ($X != null) $X.dispose()

sẽ khớp với một điều kiện bảo vệ giải phóng đúng thứ nó vừa kiểm tra, và không khớp với một điều kiện giải phóng một thứ khác. Đó là một loại lỗi mà bạn hoàn toàn không thể diễn đạt bằng văn bản.

Nó chạy trên tree-sitter vốn đã được liên kết

Phần tiêu đề “Nó chạy trên tree-sitter vốn đã được liên kết”

Không có bộ máy thứ hai. cc_natives vốn đã nạp tree-sitter cùng các ngữ pháp của nó cho đồ thị mã, kèm bộ nhớ đệm truy vấn đã biên dịch và một bảng ngôn ngữ. Bộ so khớp thuần Dart, chạy trên những cây cú pháp vốn dĩ đã được phân tích sẵn. Một bộ máy cấu trúc thứ hai sẽ đồng nghĩa với một trình phân tích cú pháp thứ hai, một bộ ngữ pháp thứ hai phải xây dựng và triển khai trên mọi nền tảng, và hai câu trả lời khác nhau cho câu hỏi “thế nào là một biểu thức gọi”.

Một mảnh mã cần một chỗ để tồn tại

Phần tiêu đề “Một mảnh mã cần một chỗ để tồn tại”

Một ngữ pháp chỉ định nghĩa thế nào là một tệp hoàn chỉnh. Khi được phân tích trần trụi ở cấp cao nhất của Dart, dispose(x) là một chữ ký hàm hoàn toàn hợp lệ — một khai báo tên dispose nhận tham số x — và không có lỗi nào ở đâu để tiết lộ điều đó. Một bộ so khớp dựng trên cách phân tích đó sẽ đi tìm các khai báo, không thấy lời gọi nào, rồi báo “không khớp” cho một mẫu vốn chưa bao giờ được đọc theo đúng cách nó trông như vậy.

Vì vậy, một mẫu được thử lần lượt qua một thang các khung bao — câu lệnh, biểu thức, thành viên lớp, cấp cao nhất — và khung đầu tiên chấp nhận nó sẽ thắng. Một mảnh mã không có ngữ cảnh nào chấp nhận sẽ được báo là không thể phân tích cú pháp, thay vì bị đem đi tìm rồi âm thầm không thấy gì.

Vì sao thao tác viết lại phải tạm giữ thay vì ghi thẳng

Phần tiêu đề “Vì sao thao tác viết lại phải tạm giữ thay vì ghi thẳng”

Một thao tác viết lại theo cấu trúc là chỉnh sửa có độ phủ rộng nhất mà agent có thể thực hiện: một mẫu, bốn mươi tệp, và không ai đọc kết quả từng dòng một — cả mô hình lẫn người đã yêu cầu nó. Ghi thẳng rồi báo “xong” nghĩa là lần nhìn thẳng thắn đầu tiên vào thay đổi sẽ là một lệnh git diff sau đó.

Vì thế ast_edit báo cáo điều nó sẽ làm — số lượng, danh sách tệp, các vị trí khớp — và thay đổi chỉ được áp dụng khi resolve xác nhận. Đó cũng là điều cho phép một bản diff cùng lựa chọn Chấp nhận/Hủy nằm trước mặt con người trước khi bất cứ thứ gì chạm tới đĩa.

Việc xác nhận là tất cả hoặc không gì cả, và mỗi tệp đều được đối chiếu với nội dung đã ghi nhận tại thời điểm tạm giữ. Giữa lúc tạm giữ và lúc xác nhận, agent có thể đã sửa tay một trong các tệp đó, một lượt chẩn đoán có thể đã viết lại nó, hoặc một trình định dạng có thể đã đụng vào nó — cứ thế xác nhận sẽ hủy bỏ công sức đó và bản diff sẽ trông như thể là chủ ý. Mọi sai lệch đều khiến toàn bộ thay đổi bị từ chối, vì một thao tác viết lại theo cấu trúc được áp dụng dở dang sẽ để lại một cây mã không biên dịch được theo hình dạng nào cả.

Điều trình gỡ lỗi trả lời được mà câu lệnh in thì không

Phần tiêu đề “Điều trình gỡ lỗi trả lời được mà câu lệnh in thì không”

Vòng lặp câu lệnh in là thế này: thêm một câu lệnh in, chạy lại toàn bộ, đọc kết quả, xóa câu lệnh in. Mỗi câu hỏi tốn một lần chạy kiểm thử trọn vẹn, mà nó chỉ trả lời được câu hỏi bạn đã nghĩ ra trước khi chạy.

Một khung đang dừng trả lời mọi câu hỏi về khoảnh khắc đó cùng lúc — mọi biến cục bộ, mọi nơi gọi, và một biểu thức được tính trong chính phạm vi của khung. Với một tiến trình bị kẹt hay một lỗi chỉ tái hiện một lần trong mười lần chạy, đó là khác biệt giữa cả một buổi chiều và một phút.

Bị giới hạn như một vòng bao kín, và vì cùng lý do

Phần tiêu đề “Bị giới hạn như một vòng bao kín, và vì cùng lý do”

Một bộ điều hợp gỡ lỗi sở hữu một tiến trình bị dừng đang giữ bất cứ thứ gì tiến trình đó giữ — một cổng, một khóa, một kết nối cơ sở dữ liệu. Một phiên không ai điều khiển là một rò rỉ tồn tại lâu hơn cả lượt đã khởi tạo nó. Vì vậy luôn có một TTL cứng, đúng một phiên cho mỗi cuộc hội thoại, và lệnh launch thứ hai sẽ bị từ chối thay vì âm thầm thay thế lệnh đầu tiên: một agent khởi tạo cái thứ hai mà không kết thúc cái thứ nhất gần như chắc chắn đã đánh mất dấu vết của cái thứ nhất.

Debug Adapter Protocol dùng chung cách đóng khung Content-Length với Language Server Protocol và không chia sẻ gì ở tầng trên — khóa tương quan khác, dạng lỗi khác, và một loại thông điệp thứ ba (sự kiện) không có tương đương trong LSP. Phần đóng khung nằm ở một chỗ; các giao thức nằm riêng phía trên nó.

Viết phần đóng khung hai lần sẽ đồng nghĩa với hai câu trả lời cho “một thông điệp trải dài qua hai khối”, “một khối chứa ba thông điệp” và “máy chủ đã ghi một dòng biểu ngữ ra stdout trước khung đầu tiên”, và chỉ một trong hai sẽ được sửa.