Bất kỳ ai từng phát hành một sản phẩm tác tử đều hiểu cảm giác này: làm cho bản trình diễn chạy được thì nhanh, nhưng biến nó thành thứ người dùng tin cậy hằng ngày — thứ chạy cả ngày trên chính máy của họ mà không gặp sự cố — thì khó. Và phần khó không phải là kết nối mô hình. Đó là toàn bộ lớp bao quanh mô hình.
Lớp đó có vài tên gọi; ở đây tôi sẽ gọi là Bộ khung vận hành tác tử. Nó nằm giữa “mô hình lớn” và “các tính năng sản phẩm”, và chính là môi trường thực thi: nó biến một yêu cầu của người dùng thành nhiều vòng hội thoại liên tiếp với mô hình, xen các lệnh gọi công cụ vào giữa, đưa kết quả trở lại, nén ngữ cảnh trước khi tràn, thử lại khi mạng chập chờn và khôi phục hội thoại ngay cả sau khi tiến trình gặp sự cố. Mô hình thực hiện suy nghĩ; bộ khung biến suy nghĩ đó thành một chuỗi hành động đáng tin cậy.
Orkas là ứng dụng tác tử trên máy tính chạy trên chính máy của người dùng, và bộ khung vận hành của nó nằm hoàn toàn ở phía máy khách. Bài viết này trình bày cách xây dựng lớp đó: cách phân lớp, hình thức của vòng lặp thực thi, cách trừu tượng hóa công cụ và mô hình, cùng cách xử lý bộ nhớ và phiên. Các chi tiết mã đã được lược bỏ thông tin riêng và khái quát hóa, nhưng cấu trúc kỹ thuật là có thật.
Các lớp
Nếu trải một sản phẩm tác tử ra, bạn sẽ thấy đại khái các lớp sau, xếp từ dưới lên:
┌─────────────────────────────────────────┐
│ Tính năng sản phẩm (trò chuyện / kỹ năng / trình kết nối / đồng bộ) │
├─────────────────────────────────────────┤
│ Bộ khung vận hành tác tử (vòng lặp thực thi / công cụ / phiên) │
├─────────────────────────────────────────┤
│ Lớp trừu tượng hóa nhà cung cấp (hợp nhất nhiều nhà cung cấp LLM) │
├─────────────────────────────────────────┤
│ Hạ tầng (kiểu dữ liệu / lỗi / ghi nhật ký / cấu hình) │
└─────────────────────────────────────────┘Có một lựa chọn mang tính quyết định được đặt nền móng ở đây: toàn bộ hoạt động suy luận của mô hình diễn ra ở phía máy khách. Ứng dụng máy tính không phải máy khách mỏng — nó chứa chính bộ khung vận hành và gọi trực tiếp mô hình. Máy chủ chỉ xử lý tài khoản, đồng bộ nhiều thiết bị và thanh toán; hoàn toàn không chạy tác tử. Quyết định này định hình gần như mọi thứ phía sau: phiên được lưu trên đĩa cục bộ, công cụ thao tác trực tiếp trên thư mục làm việc của người dùng và dữ liệu nhạy cảm không bao giờ rời khỏi máy.
Bản thân bộ khung vận hành được chia thành vài phần: vòng lặp thực thi, phiên, công cụ, lớp Nhà cung cấp và bộ nhớ. Hãy xem lần lượt từng phần.
Vòng lặp thực thi: bộ sinh truyền luồng
Trái tim của bộ khung là bộ chạy. Gói gọn trong một câu, nhiệm vụ của nó là: trò chuyện liên tục với mô hình cho đến khi mô hình nói “Tôi đã xong.”
Nó được triển khai dưới dạng bộ sinh bất đồng bộ, và lựa chọn đó rất quan trọng. Một lần chạy tác tử phức tạp hơn nhiều so với “gửi yêu cầu, chờ kết quả”. Rất nhiều việc diễn ra ở giữa — mô hình đang phát token, nó muốn gọi công cụ, công cụ đã hoàn tất, ngữ cảnh đủ dài để kích hoạt nén, mạng gặp lỗi và chúng ta đang thử lại. Với hàm gọi lại hoặc Promise thông thường, khó cung cấp gọn gàng các trạng thái trung gian đó cho bên gọi. Dưới dạng bộ sinh, tất cả trở thành một luồng sự kiện được phát bằng yield:
type AgentRunEvent =
| { type: "text_delta"; text: string } // model emitting tokens
| { type: "tool_start"; name: string; input: unknown } // a tool starts executing
| { type: "tool_end"; name: string; result: string } // a tool finished
| { type: "compaction"; tokensBefore: number; tokensAfter: number } // context compacted
| { type: "retry"; attempt: number; reason: string } // error, retrying
| { type: "done"; result: AgentRunResult } // terminalGiao diện đăng ký nhận luồng sự kiện này và hiển thị đầu ra của mô hình cùng quá trình thực thi công cụ theo thời gian thực. Bên trong, điểm vào không truyền luồng đơn giản chỉ là “đọc hết luồng, lấy sự kiện cuối cùng done” — cả hai điểm vào dùng chung một cách triển khai, nên không có nhánh mã thứ hai có thể bị lệch đồng bộ.
Điều gì diễn ra trong một lượt
Nếu trải ra, một lượt trông đại khái như sau:
- Đưa tin nhắn người dùng (có thể kèm ảnh) vào lịch sử phiên.
- Tạo chỉ dẫn hệ thống, chèn các công cụ hiện có, chỉ mục kỹ năng, v.v.
- Phân tích chuỗi mô hình và xác định Nhà cung cấp cùng ID mô hình cụ thể.
- Chuyển mọi công cụ thành định nghĩa mà mô hình hiểu được, rồi gửi cùng lịch sử.
- Đọc luồng phản hồi của mô hình,
yieldphát văn bản từng token một, đồng thời thu thập mọi lệnh gọi công cụ mà mô hình tạo ra. - Khi luồng kết thúc, kiểm tra lý do dừng của mô hình:
- Nếu là
tool_use, mô hình muốn gọi công cụ — chạy các công cụ, rồi quay lại bước 5 và hỏi mô hình lần nữa. - Nếu không, lượt đã kết thúc — tổng hợp kết quả,
yield done, rồi trả về.
Có một điều kiện bất biến phải giữ ở đây: mọi lệnh gọi công cụ do mô hình tạo ra phải được theo sau ngay trong lịch sử bởi một kết quả công cụ tương ứng. API mô hình thực thi việc ghép cặp này rất nghiêm ngặt — phá vỡ nó thì yêu cầu tiếp theo sẽ báo lỗi hoặc treo. Chúng ta sẽ quay lại vấn đề này khi nói về khả năng tự phục hồi của phiên.
Cách một lệnh gọi công cụ được định tuyến trở lại
Mô hình không tự thực thi công cụ; nó chỉ nói “Tôi muốn gọi read_file với các đối số này.” Khi bộ chạy nhận được ý định đó:
for (const call of toolUseBlocks) {
yield { type: "tool_start", name: call.name, input: call.input };
const tool = this.tools.get(call.name);
const ctx = { workingDir, signal, state: { sandboxEnv } };
const result = await tool.execute(call.input, ctx);
// append the result to the session as a tool-result message
session.addToolResult(call.id, result);
yield { type: "tool_end", name: call.name, result: result.content };
}Các công cụ chạy tuần tự, kết quả được ghi lại vào lịch sử theo thứ tự mô hình khai báo, rồi mô hình được hỏi lại với những kết quả đó. Khi xem kết quả, mô hình có thể gọi công cụ khác hoặc đưa ra câu trả lời cuối cùng. Vòng lặp “hỏi → gọi → trả lời → hỏi lại” này chính là điều cho phép tác tử hoàn thành tác vụ nhiều bước.
Một chi tiết đáng nói riêng: một số công cụ trả về hình ảnh — ảnh chụp màn hình, ảnh được tạo. Nhưng nhiều mô hình không chấp nhận ảnh trong kênh kết quả công cụ. Orkas xử lý bằng cách tách ảnh thành một tin nhắn người dùng riêng, đặt sau kết quả công cụ — mô hình trước tiên đọc “công cụ trả về văn bản này”, rồi ngay ở lượt tiếp theo thấy ảnh tương ứng. Một thỏa hiệp nhỏ giúp thích ứng với khác biệt về khả năng giữa các Nhà cung cấp.
Làm gì khi ngữ cảnh sắp tràn
Rào cản các tác vụ dài thường gặp nhất là cửa sổ ngữ cảnh. Orkas không đợi đến khi đầy — nó đặt một ngưỡng 60%: sau mỗi vòng công cụ, nó ước tính các token hiện tại chiếm bao nhiêu cửa sổ, và khi vượt 60% thì chủ động kích hoạt nén.
Bản thân việc nén yêu cầu mô hình tóm tắt hội thoại trước đó, rồi thay các tin nhắn cũ bằng bản tóm tắt, chỉ giữ phần cuối gần nhất. Nghe đơn giản, nhưng có một cái bẫy: sau khi thay, phần cuối được giữ lại không được bắt đầu bằng “kết quả công cụ mồ côi” — không thể có “kết quả không có lệnh gọi tương ứng”, nếu không bạn lại phá vỡ điều kiện ghép cặp. Vì vậy, logic nén bảo đảm điểm cắt nằm ở một ranh giới hợp lệ.
Có một lựa chọn thú vị hơn cần phân tích ở đây: tại sao dùng cách thô “tóm tắt cả khối ở mức 60%”, thay vì cách tinh hơn — chấm điểm từng tin nhắn và cắt theo mức độ quan trọng, trích xuất có cấu trúc từ đầu ra công cụ, duy trì cây bộ nhớ phân lớp? Những cách đó trông rất hay trong các bài nghiên cứu, nhưng chúng tôi chủ động không đi theo hướng ấy vì ba lý do.
Thứ nhất, bộ nhớ đệm. Bộ nhớ đệm chỉ dẫn của mô hình đối chiếu theo tiền tố: miễn là tiền tố lịch sử không đổi, phần đó sẽ dùng được bộ nhớ đệm, tiết kiệm cả tiền lẫn độ trễ. Nén chi tiết liên tục viết lại phần giữa lịch sử, đồng nghĩa với việc liên tục phá vỡ tiền tố đã lưu đệm — mỗi lần sửa đều buộc phải xử lý trước lại một lượng lớn đầu vào. Chiến lược “giữ nguyên, rồi nén một lần khi đạt ngưỡng” giữ tiền tố ổn định trong phần lớn các lượt, chỉ lần nén đó mới làm nó mất hiệu lực. Cách này thân thiện với bộ nhớ đệm hơn nhiều.
Thứ hai, độ phức tạp. Điều kiện bất biến “mọi lệnh gọi công cụ phải được ghép cặp” mà chúng ta liên tục nhấn mạnh — càng cắt lịch sử chi tiết, càng dễ phá vỡ nó ở một góc nào đó. Một bản tóm tắt thô chỉ phải bảo vệ một điểm cắt hợp lệ; số chỗ có thể sai giảm cả một bậc độ lớn. Bớt một nhóm trường hợp biên là bớt một nhóm sự cố trong vận hành thực tế.
Thứ ba, hưởng lợi từ mô hình ngày càng tốt hơn. Cửa sổ ngữ cảnh đã liên tục mở rộng trong vài năm qua, và mô hình xử lý ngữ cảnh dài ngày càng tốt. Dồn công sức hôm nay vào một thuật toán nén phức tạp về cơ bản là chống lại một vấn đề đang thu nhỏ — rất có thể bạn vừa tinh chỉnh xong thì thế hệ tiếp theo đã tăng gấp đôi cửa sổ, và độ phức tạp của bạn trở thành gánh nặng thuần túy. Ngược lại, giao việc tóm tắt cho chính mô hình sẽ tự động tốt lên khi mô hình tiến bộ: nó càng giỏi nhận ra điều quan trọng thì chất lượng tóm tắt càng cao, còn chúng tôi không phải sửa một dòng mã nào. Độ phức tạp mà mô hình có thể gánh giúp thì bạn không nên tự gánh.
Ước tính token ẩn chứa một vấn đề dễ bỏ sót: tiếng Trung. Ước tính tiếng Trung theo trực giác về tiếng Anh (khoảng một token cho vài ký tự) sẽ khiến bạn đếm thiếu nghiêm trọng. Orkas gán trọng số riêng cho ký tự CJK khi ước tính; nếu không, ngưỡng của một hội thoại hoàn toàn bằng tiếng Trung sẽ bị tính sai, và việc nén sẽ không kích hoạt đúng lúc.
Lỗi và thử lại
Chạy trên máy người dùng và phụ thuộc vào API mô hình bên ngoài, lỗi là chuyện thường ngày chứ không phải ngoại lệ. Bộ chạy phân chúng thành vài nhóm và xử lý khác nhau:
- Có thể thử lại: giới hạn tốc độ, hết thời gian chờ, mất kết nối, 5xx. Thời gian chờ tăng theo cấp số nhân kèm độ ngẫu nhiên, tối đa 30 giây; nếu là giới hạn tốc độ và máy chủ gửi
retry-after, hãy tuân theo. - Không thể thử lại: những lỗi như xác thực thất bại — thử lại bao nhiêu cũng không giúp được, nên báo lỗi ngay.
- Đặc biệt: tràn ngữ cảnh. Thử nén trước, sau đó thử lại một lần, và chỉ báo lỗi nếu vẫn thất bại.
Còn một nhóm nữa: “bản thân công cụ bị lỗi”. Điều này không làm hỏng cả lượt — lỗi công cụ tự nó là thông tin cho mô hình; khi thấy “lệnh đó gặp lỗi”, mô hình hoàn toàn có thể thử cách khác. Bộ khung phân biệt các lỗi công cụ nhất thời này với lỗi thực sự: nó không làm gián đoạn luồng xử lý cũng không bỏ mất chúng — chúng xuất hiện trong thống kê sau đó. (Dữ liệu này về sau được đưa vào cơ chế tự tiến hóa, chủ đề của bài viết tiếp theo.)
Tín hiệu hủy từ bên ngoài (AbortSignal) được kiểm tra ở mọi điểm quan trọng. Người dùng nhấn “dừng” thì lượt hiện tại dừng ngay — không khởi động thêm lần thử lại nào.
Trừu tượng hóa công cụ: đủ đơn giản để mở rộng
Giao diện công cụ được chủ ý giữ tối giản:
interface AgentTool {
readonly name: string;
readonly description: string; // shown to the model
readonly inputSchema: Record<string, unknown>; // JSON Schema to constrain inputs
execute(input: Record<string, unknown>, ctx: ToolContext): Promise<ToolResult>;
}Một công cụ chỉ là “tên + mô tả cho mô hình + lược đồ đầu vào + hàm thực thi”. Các công cụ tích hợp — đọc tệp, ghi tệp, chạy lệnh shell, tìm kiếm và tải nội dung web — đều triển khai giao diện này. Lớp ứng dụng máy tính bổ sung một nhóm công cụ thiên về cục bộ (tìm kiếm cơ sở tri thức, tạo ảnh, gọi trình kết nối bên ngoài), nhưng vẫn dùng cùng giao diện.
Lợi ích của giao diện tối giản là nguồn gốc công cụ không quan trọng với bộ chạy: tích hợp sẵn, do người dùng định nghĩa hay nạp từ kỹ năng — tất cả đều cùng một loại, được đăng ký vào một Map<string, AgentTool> và chuyển thành định nghĩa mà mô hình đọc được ở mỗi lượt.
Các công cụ có tác động phụ như lệnh shell đi qua một bộ thực thi cách ly: thời gian chờ, giới hạn độ dài đầu ra, danh sách chặn lệnh và biến môi trường được truyền riêng thay vì sửa môi trường toàn cục của tiến trình — cách sau sẽ lan sang hàng loạt tiến trình con và, trong kiến trúc đa tiến trình như Electron, dễ làm hỏng quá trình khởi động.
Lớp Nhà cung cấp: hợp nhất nhiều mô hình thành một giao diện
Sở thích mô hình của người dùng rất đa dạng, và sản phẩm không thể gắn chặt với một nhà cung cấp. Bên dưới bộ khung vận hành, Orkas đặt một lớp trừu tượng hóa Nhà cung cấp để thống nhất mô hình của các nhà cung cấp khác nhau qua một giao diện duy nhất:
interface LLMProvider {
readonly id: string;
complete(params: CompletionParams): Promise<CompletionResult>;
stream(params: CompletionParams): AsyncIterable<StreamEvent>;
validateAuth(): Promise<boolean>;
}Bộ chạy phía trên chỉ giao tiếp với giao diện này; nó không biết nhà cung cấp nào ở phía sau. Một sổ đăng ký xử lý định tuyến theo chuỗi mô hình: dạng tường minh provider/model được tách trực tiếp; tên mô hình đơn lẻ được xác định nhà cung cấp theo tiền tố. Xác thực (khóa API hoặc token OAuth) cũng được quản lý ở đây, và token OAuth hết hạn sẽ được làm mới tự động.
Khi hợp nhất nhiều mô hình, vấn đề đau đầu thực sự không phải là hoàn thành văn bản — mà là những chỗ ngữ nghĩa giữa các nhà cung cấp khác nhau. Sau đây là hai ví dụ từng khiến chúng tôi gặp rắc rối.
Một là bảo toàn các khối suy nghĩ giữa các nhà cung cấp. Mô hình suy luận phát ra một đoạn nội dung “suy nghĩ”; một số nhà cung cấp mã hóa nó và yêu cầu bạn gửi lại nguyên văn, số khác biểu diễn bằng một tập trường khác. Nếu người dùng chuyển từ nhà cung cấp A sang B giữa hội thoại, chữ ký trên đoạn suy nghĩ đó trong lịch sử không còn khớp. Cách khắc phục là gắn dấu “mô hình nào đã tạo ra tin nhắn này” cho mọi tin nhắn trong lịch sử, để lớp chuyển đổi có thể quyết định có giữ nguyên hay không: cùng mô hình thì giữ; khác mô hình thì hạ cấp theo quy tắc.
Hai là bộ nhớ đệm chỉ dẫn. Qua các lượt trong một phiên, tiền tố lặp lại rất nhiều, và lưu đệm nó giúp tiết kiệm đáng kể chi phí lẫn độ trễ. Cách triển khai truyền ID phiên làm khóa bộ nhớ đệm cho những nhà cung cấp hỗ trợ, đồng thời xử lý giới hạn độ dài khóa của từng nhà cung cấp (chẳng hạn cắt ngắn hoặc băm nếu quá dài).
Đây đều là công việc tỉ mỉ, lặp lại — nhưng chính lớp công việc này cho phép bộ chạy phía trên coi như “chỉ có một loại mô hình”.
Bộ nhớ: hai cơ chế, mỗi cơ chế một nhiệm vụ
“Bộ nhớ” trong Orkas thực ra là hai cơ chế song song giải quyết hai vấn đề hoàn toàn khác nhau. Một là cơ sở tri thức dựa trên truy xuất, dành cho lượng tài liệu lớn mà bạn “tra cứu khi cần”. Hai là bộ nhớ xuyên phiên, dành cho một tập nhỏ các thông tin quan trọng mà bạn “luôn phải nhớ”. Nhiều sản phẩm trộn hai thứ này với nhau; tách riêng giúp mọi thứ rõ ràng hơn nhiều.
Cơ sở tri thức: truy xuất kết hợp
Cơ chế đầu tiên nhắm đến nội dung lớn nhưng chỉ đôi khi liên quan — tài liệu của người dùng, ghi chú trước đây, kiến thức chuyên ngành. Đây là cơ sở tri thức cục bộ có truy xuất vectơ, với hai dạng lưu trữ: bản nhẹ hoàn toàn trong bộ nhớ (dành cho kiểm thử và sử dụng tạm thời), cùng bản lưu bền trong cơ sở dữ liệu cục bộ (dành cho vận hành thực tế, với chỉ mục toàn văn và vectơ).
Dữ liệu đi vào theo đường sau:
tài liệu → chia đoạn theo ranh giới dòng (có chồng lấn) → lập hai chỉ mục
├─ chỉ mục toàn văn (từ khóa, không tốn chi phí nhúng)
└─ chỉ mục vectơ (nếu có cấu hình mô hình nhúng)Các đoạn được cắt theo ranh giới dòng, có một chút chồng lấn giữa chúng để tránh cắt đôi một ý hoàn chỉnh. Việc truy xuất là kết hợp: một lượt vectơ (gần về ngữ nghĩa) và một lượt từ khóa (khớp nguyên văn), với hai tập kết quả được hợp nhất bằng RRF (hợp nhất thứ hạng nghịch đảo):
score = Σ 1 / (k + rank_i)Kết quả xếp càng cao trong một lượt thì đóng góp càng nhiều; cộng qua cả hai lượt, bạn vừa giữ được mức độ liên quan về ngữ nghĩa vừa không bỏ lỡ các kết quả khớp chính xác nguyên văn. Trọng số vectơ và từ khóa có thể điều chỉnh, mặc định ưu tiên ngữ nghĩa. Sau khi hợp nhất, kết quả được khử trùng lặp theo “(tài liệu, dòng bắt đầu)”, chỉ giữ kết quả tốt nhất cho mỗi vị trí, rồi loại mọi kết quả dưới ngưỡng và trả về top-K.
Tại sao không chỉ dựa vào vectơ? Vì truy xuất vectơ thường thất bại với tên riêng, ký hiệu mã và chuỗi nguyên văn chính xác — những truy vấn không có gì đặc biệt về ngữ nghĩa nhưng chữ nghĩa chính xác lại rất quan trọng — trong khi chỉ dùng từ khóa thì không bắt được “cùng ý, khác cách diễn đạt”. Chạy cả hai là một sự đánh đổi rất thực tế giữa chất lượng truy xuất và chi phí.
Bộ nhớ xuyên phiên: luôn ghi nhớ người dùng
Cơ sở tri thức giải quyết vấn đề “quá nhiều tài liệu để giữ hết”. Nhưng còn một loại thông tin khác — rất ít về lượng nhưng phải luôn được ghi nhớ: người dùng này là ai, họ thích gì, lần trước đã thống nhất điều gì. Những điều này không nên phụ thuộc vào việc truy xuất “may mắn nhớ ra” — chúng phải hiện diện trong từng lượt.
Vì vậy, Orkas xây dựng một lớp bộ nhớ xuyên phiên riêng, chia theo nội dung thành hai phần:
- Hồ sơ người dùng: thông tin ổn định về con người — vai trò, sở thích, phong cách giao tiếp, bộ công nghệ sử dụng.
- Ghi chú thông tin: thông tin lâu dài về công việc — quyết định, cột mốc, quy ước dự án.
Cả hai đều nhỏ, mỗi phần có giới hạn cứng vài nghìn ký tự, buộc chúng chỉ giữ những gì thực sự hữu ích lâu dài. Chúng không đi qua truy xuất; thay vào đó, chúng được đưa cố định trực tiếp vào chỉ dẫn hệ thống ở đầu mỗi lượt — nghĩa là tác tử đơn giản “biết” những điều này mà không cần nhớ phải đi tra cứu. Điều đó hoàn toàn ngược với cơ sở tri thức: cơ sở tri thức là “chỉ lấy khi cần, xong thì bỏ”, bộ nhớ xuyên phiên là “luôn hiện diện, luôn nhìn thấy”.
Việc ghi đi qua một công cụ bộ nhớ chuyên dụng mà mô hình gọi khi đánh giá trong lúc hội thoại rằng “điều này đáng nhớ lâu dài”, hỗ trợ thêm, thay thế chuỗi con và xóa. Những gì nên và không nên lưu được nêu rõ trong mô tả công cụ: các sửa sai và sở thích của người dùng được ưu tiên cao nhất; quyết định và quy ước lâu dài được lưu; còn trạng thái tạm thời của tác vụ hiện tại, thông tin gỡ lỗi dùng một lần và mọi thứ dễ tìm lại thì không — bộ nhớ dành cho “thông tin lâu dài về người dùng và dự án”, không phải “lần này tôi đã làm đến đâu”.
Có một chi tiết dễ bỏ qua nhưng khá quan trọng: quét bảo mật được thực hiện trước mỗi lần ghi. Nội dung này đi nguyên văn vào chỉ dẫn hệ thống và tồn tại qua các phiên trong thời gian dài — về thực chất là một bề mặt chèn chỉ dẫn bền vững. Vì vậy, mọi mục bộ nhớ sắp ghi xuống đĩa trước tiên đều được quét để tìm mẫu đáng ngờ — cách diễn đạt chèn chỉ dẫn điển hình (“bỏ qua mọi chỉ dẫn trước đó” và tương tự), lệnh cố đánh cắp khóa, ký tự unicode vô hình ẩn trong văn bản — và nếu khớp thì bị từ chối ngay. Kết hợp thêm khử trùng lặp và cắt nội dung vượt giới hạn, lớp bộ nhớ này vẫn hữu ích mà không trở thành gánh nặng.
Cùng nhau, hai cơ chế bao phủ cả hai đầu — “khổng lồ nhưng thỉnh thoảng” và “nhỏ nhưng thường trực”: cơ sở tri thức xử lý vế trước, bộ nhớ xuyên phiên xử lý vế sau. Thêm vào đó là hiểu biết của tác tử về chính mình (chủ đề của bài viết tiếp theo), và một tác tử Orkas bước vào với ba loại bộ nhớ cùng lúc — về tài liệu, về người dùng và về chính nó.
Phiên: được thiết kế để chịu sự cố và tự phục hồi
Một phiên quản lý lịch sử tin nhắn. Bản cơ bản chỉ là một mảng tin nhắn trong bộ nhớ với khả năng cắt gọn và nén lịch sử. Nhưng bất cứ thứ gì chạy trên máy người dùng đều phải giả định có thể bị kết thúc bất cứ lúc nào — người dùng thoát ứng dụng, hệ thống khởi động lại, bộ giám sát hết thời gian chờ và dừng tiến trình. Vì vậy, bản vận hành thực tế dùng phiên lưu bền, ghi vào tệp JSONL cục bộ, mỗi dòng một tin nhắn.
Có hai chiến lược ghi: thêm tin nhắn mới dùng thao tác ghi nối nguyên tử; mọi thao tác viết lại cả tệp (nén, xóa sạch) dùng “ghi tệp tạm + đổi tên nguyên tử”. Nhờ vậy, ngay cả khi mất điện giữa lúc ghi, bạn cũng không để lại một bản ghi hỏng dở dang.
Phần thú vị nhất là sửa chữa các lệnh gọi công cụ mồ côi. Quay lại điều kiện ghép cặp: mô hình tạo lệnh gọi công cụ, bộ khung thực thi, kết quả được ghi lại — ngắt bất kỳ bước nào trong ba bước đó đều để lại một mục mồ côi trên đĩa, “lệnh gọi không có kết quả”. Lần sau nạp phiên đó và gửi nguyên trạng cho mô hình, API sẽ từ chối hoặc treo.
Logic phục hồi chạy mỗi khi phiên được nạp từ đĩa, và chạy lặp lại vẫn cho cùng kết quả:
- Quét mọi tin nhắn trợ lý và thu thập ID lệnh gọi công cụ mà chúng tạo ra.
- Tìm phía sau các kết quả công cụ tương ứng.
- Với mọi lệnh gọi thiếu kết quả tương ứng, tạo một kết quả đánh dấu “bị gián đoạn”.
- Đồng thời, căn thứ tự kết quả theo thứ tự khai báo lệnh gọi, và loại bỏ mọi kết quả mồ côi không có lệnh gọi tương ứng.
Sau lượt xử lý này, phiên được bảo đảm đáp ứng yêu cầu ghép cặp của API và có thể gửi an toàn. Cơ chế trông không có gì đặc biệt, nhưng đó là lưới an toàn bảo đảm “hội thoại của người dùng không bị khóa vĩnh viễn chỉ vì một lần gặp sự cố”.
Một vài quyết định nhìn lại mới thấy quan trọng
Kết nối tất cả lại, một vài quyết định tỏ ra đặc biệt giá trị khi nhìn lại.
Dùng bộ sinh làm giao diện chính. Truyền luồng và không truyền luồng dùng chung một cách triển khai, trạng thái trung gian được bộc lộ tự nhiên, và giao diện có thể hiển thị chi tiết tùy ý. Điều này tránh được cả một nhóm lỗi thiếu nhất quán mà cách “làm không truyền luồng trước, gắn thêm truyền luồng sau” sẽ tạo ra.
Nén ở mức 60%, không đợi đầy. Điều này chừa khoảng trống cho chính việc nén (vốn cũng cần một lần gọi mô hình) và tránh cuống cuồng vào phút cuối.
Điều kiện ghép cặp xuyên suốt mọi thứ. Từ điểm cắt khi nén, đến ghi xuống đĩa, đến phục hồi lúc nạp, mọi nơi chạm vào phiên đều giữ cùng một quy tắc. Với một quy tắc chung, không nơi nào phải tự nghĩ ra logic vá lỗi riêng.
Tập trung công việc tỉ mỉ vào lớp Nhà cung cấp. Mọi khác biệt khó xử giữa các nhà cung cấp — khối suy nghĩ, khóa bộ nhớ đệm, khác biệt khả năng — đều được xử lý trong lớp này, đổi lại là bộ chạy gọn gàng phía trên. Sau này thêm nhà cung cấp mô hình mới, thay đổi hầu như không lan ra ngoài.
Lời kết
Bộ khung vận hành của Orkas không có thuật toán đáng kinh ngạc nào. Giá trị của nó nằm ở việc lấy bài toán “làm cho tác tử chạy đáng tin cậy trong môi trường thực” và chia thành các mô-đun có ranh giới rõ ràng, mỗi mô-đun phụ trách một phần: bộ chạy phụ trách vòng lặp và thử lại, công cụ phụ trách khả năng, lớp Nhà cung cấp phụ trách hợp nhất nhiều mô hình, bộ nhớ phụ trách truy xuất, phiên phụ trách lưu bền và phục hồi. Từng phần riêng lẻ không phức tạp; chỉ khi kết hợp, chúng mới nâng đỡ một thứ mọi người dùng hằng ngày.
Nếu có điều gì cần ghi nhớ: biến vòng lặp thực thi thành bộ sinh truyền luồng sẽ khiến trạng thái trung gian dễ xử lý hơn nhiều; một khi đã đặt ra điều kiện bất biến cốt lõi (như “lệnh gọi công cụ phải được ghép cặp”), hãy giữ nhất quán xuyên suốt việc nén, ghi đĩa và nạp — không để góc nào là ngoại lệ; tập trung công việc tỉ mỉ giữa các nhà cung cấp vào một lớp và giữ nó ngoài logic nghiệp vụ; và — rõ ràng nhất — hãy giả định tiến trình sẽ bị kết thúc đúng lúc tệ nhất, rồi viết sẵn cơ chế phục hồi cho thời điểm đó.
Bài viết tiếp theo đi vào phần thú vị hơn của Orkas: cách tác tử này học từ chính quá trình sử dụng, chắt lọc kinh nghiệm thành kỹ năng tái sử dụng và dần khiến bản thân hữu ích hơn.