Giới thiệu
Gần đây, tôi đã làm việc trên một AI wrapper với NextJS và Google GenAI SDK. Trong quá trình phát triển, tôi nhận thấy rằng toàn bộ văn bản ngữ cảnh và prompt đều nằm bên trong các hàm, điều này làm cho các hàm trở nên cồng kềnh. Một file mà lẽ ra chỉ có khoảng 50 dòng đã phát triển lên tới khoảng 500 dòng. 🤯 Điều này đã thúc đẩy tôi di chuyển toàn bộ văn bản prompt vào các file riêng biệt và tổ chức chúng dưới một thư mục prompts. Việc này không chỉ vì độ dài file, mà còn vì khó khăn trong việc xem xét các hàm cùng nhau và xác định các khu vực cần thay đổi.
Tóm tắt
- Tách biệt prompt khỏi logic ứng dụng — coi chúng như tài sản tĩnh (hình ảnh, cấu hình) thay vì nhúng chúng vào bên trong các hàm.
- Separation of concerns — giữ cho các giá trị (prompt) độc lập với logic để tránh file cồng kềnh và cải thiện khả năng bảo trì.
- Tạo mẫu nhẹ với
pm— một hàm mẫu dựa trên regex đơn giản có thể thay thế các biến trong văn bản prompt, giảm thiểu sự lặp lại. - Lưu trữ ngôn ngữ độc lập — lưu các prompt dưới dạng file
.txthoặc.prompt(với các loader hoặc engine mẫu) loại bỏ sự phụ thuộc vào JavaScript trong thời gian chạy. - Tránh giải pháp black-box — không theo đuổi các framework tích hợp tất cả; thay vào đó, sử dụng các khối xây dựng mô-đun, có thể kết hợp giữ được tính linh hoạt và sự rõ ràng.
Những Giáo Huấn của Dan Abramov
Tôi là người tin tưởng vào những giáo huấn của Dan và muốn làm rõ mô hình tư duy về các giá trị, tức là,
Khi chúng ta bắt đầu xây dựng mô hình tư duy của mình, một trong những hiểu lầm phổ biến đầu tiên
mà chúng ta cần làm rõ là các giá trị chính là mã của chúng ta. Thay vào đó, chúng ta cần suy nghĩ về chúng
một cách riêng biệt—mã của chúng ta tương tác với các giá trị, nhưng các giá trị tồn tại trong một không gian hoàn toàn
riêng biệt.
Trích từ Just JavaScript của Dan.
Tôi tin tưởng mạnh mẽ rằng các giá trị—ở đây là các prompt đơn giản—nên tồn tại bên ngoài không gian logic. Giống như bạn đặt hình ảnh và các tài sản tĩnh khác trong thư mục công cộng, tách biệt khỏi các file mã của bạn, các prompt cũng nên được đặt ngoài các file mã.
Dựa trên khái niệm này, tôi muốn trình bày một vài cách để đặt các file prompt và context tách biệt khỏi mã của bạn.
Sử dụng clsx cho việc tạo mẫu
Đối với dự án này, tôi không muốn giới thiệu bất kỳ sự phức tạp nào ở cấp trình biên dịch. Thay vào đó, tôi đã nghĩ ra một cách sử dụng thông minh các chuỗi mẫu và regex, có thể được nhập khẩu như các hàm và gọi với một đối tượng dữ liệu để render mẫu.
Hãy để tôi ghi lại các yêu cầu để bạn dễ hiểu hơn. Tôi muốn:
- Không có sự lồng ghép đối tượng
- Thay thế đơn giản
- Có khả năng nhập khẩu các mẫu như các hàm có thể gọi
- Các mẫu chấp nhận một đối tượng dữ liệu
Ở phần này, tôi muốn cảm ơn goober css vì đã giới thiệu cho tôi về các hàm nhãn trong chuỗi mẫu. Cũng như, ghi nhận người sáng tạo hàm clsx, được sử dụng bởi các thành phần shadcn UI thời đó, khi nó chỉ là một dòng: classNames.filter(Boolean).join(' '). Hai điều này đã tạo ra sự kết nối và truyền cảm hứng cho tôi viết một hàm nhãn nhỏ có thể được sử dụng với các mẫu chuỗi và trả về một hàm.
Hàm trả về chạy một regex để xác định các thực thể đã được thoát, sau đó được thay thế bằng các giá trị. Do đó, tôi đã tạo ra hàm pm (viết tắt của prompt-maker). Phần tốt nhất của pm là nó trả về một hàm có thể được sử dụng lại nhiều lần để render mẫu với các tập hợp giá trị khác nhau.
javascript
function pm([string]) {
const pattern = /\{\{\s*?\.?([\w\d]+)\s*?\t*?\}\}/g;
return (context) => {
for (let [match, variable] of string.matchAll(pattern)) {
string = string.replaceAll(match, context[variable]);
}
return string;
};
}
const introductoryPrompt = pm`
Hello {{world}}, my name is {{name}}.
I'm working on {{project}}.
`;
introductoryPrompt({
world: "Thế Giới!",
name: "Ashish",
project: "tilde",
});
/*
Kết quả ->
"
Hello Thế Giới!, my name is Ashish.
I'm working on tilde.
"
*/
Giải pháp nhỏ này đã giúp tôi giải quyết vấn đề file cồng kềnh.
File .prompt và loader
Hàm pm vẫn phụ thuộc vào môi trường JavaScript để tạo ra các file prompt, điều này khiến nó phụ thuộc vào ngôn ngữ. Nhưng nếu tôi quyết định tạo một server Go chuyên dụng để xử lý các yêu cầu API và ghi log? Tất cả các file prompt vẫn sẽ yêu cầu một môi trường JavaScript.
Một giải pháp sẽ là lưu các prompt dưới dạng các file .txt đơn giản. Điều này loại bỏ sự phụ thuộc của các file prompt vào bất kỳ ngôn ngữ cụ thể nào. Ví dụ, hãy xem xét file prompt sau:
Hello {{world}}, my name is {{name}}.
I'm working on {{project}}.
Các từ được bao quanh có thể được xác định và thay thế—giống như trong bất kỳ ngôn ngữ mẫu nào. Nếu tôi sử dụng Go, tôi có thể tận dụng gói text/template từ thư viện chuẩn. Trong trường hợp các meta-framework như Next.js, mà sử dụng Webpack để phân tích các ngôn ngữ tùy ý (đúng vậy, JSX thực sự không phải là một ngôn ngữ!), chúng ta có thể viết một loader tùy chỉnh để chuyển đổi các file prompt (hoặc văn bản) thô thành các hàm có thể gọi. Điều này sẽ hoạt động tương tự như những gì hàm pm của chúng ta trả về, hoặc thậm chí triển khai một phiên bản rất cơ bản của HandlebarsJS.
Với ý tưởng này trong đầu, tôi đã tạo một loader tối thiểu để kiểm tra giả thuyết của mình—và nó đã hoạt động! Mã sau đây bao gồm file loader, cấu hình Webpack, và lệnh gọi hàm. Mã này sử dụng CommonJS vì tài liệu của loader là trong CJS (và ChatGPT đã theo mã ban đầu của tôi).
javascript
// loader.js
module.exports = function (source) {
const escaped = source.replace(/`/g, "\\`");
return `
module.exports = function (data) {
let template = ${JSON.stringify(escaped)};
return template.replace(
/\\{\\{(.*?)\\}\\}/g,
(_, key) => data[key.trim()] ?? "",
);
}
`;
};
// webpack.config.js
module: {
rules: [
{
test: /\.prompt$/,
use: {
loader: path.resolve(
__dirname,
"loader.js",
),
},
},
],
},
// context.prompt
Hello {{world}}, my name is {{name}}.
I'm working on {{project}}.
// index.js
const Context = require("./context.prompt");
console.log(
Context({
world: "Thế Giới!",
name: "Ashish",
project: "tilde",
}),
);
Bạn cũng có thể sử dụng một engine mẫu hoàn chỉnh để đảm bảo an toàn hơn—hoặc bất kỳ thứ gì ở giữa, tùy thuộc vào sự tò mò và yêu cầu của bạn.
Các lựa chọn khác
Lựa chọn gần nhất có sẵn là các mẫu prompt trên bảng điều khiển Vertex AI. Tuy nhiên, trang web liệt kê một số hạn chế, và chúng không thể được sử dụng với các prompt hệ thống. Quy trình triển khai nghiêm ngặt hơn, và tôi chỉ đã thử nghiệm chúng trên bảng điều khiển.
Tôi cũng tìm thấy dotprompt của Google, cung cấp các file có thể thực thi cùng với các prompt và siêu dữ liệu cho các mô hình, chẳng hạn như cấu trúc đầu ra. Tuy nhiên, điều này làm mất đi mục đích thực sự của việc lưu trữ các giá trị như những thực thể riêng biệt.
Còn có Generative AI Scripting của Microsoft, lại xử lý quy trình gọi LLM cho bạn. Mặc dù, việc sử dụng ký hiệu $ khiến tôi nhớ đến jQuery và phép màu mà nó đã mang lại.
Hầu hết các giải pháp đã kết hợp logic hành vi với chính dữ liệu. Hãy nghĩ xem—các bảng SQL có biết ngôn ngữ truy vấn nào mà chúng sử dụng không? Hay mã HTML có biết cửa sổ đại diện cho trình duyệt nào không?
Kết luận
Nếu có một bài học mà bạn nên rút ra từ bài viết này, thì đó là: đừng theo đuổi những thứ hào nhoáng chỉ vì chúng hứa hẹn một giải pháp tất cả trong một. Thay vào đó, hãy chọn những mảnh Lego đơn giản và xây dựng một giải pháp hoạt động cho riêng bạn. Nếu không, bạn sẽ mất đi sự tỉnh táo khi gỡ lỗi những black box này!
Ngoài ra, với tư cách là một kỹ sư phần mềm có học thức và có trách nhiệm, bạn nên áp dụng nguyên tắc separation of concerns—giữ cho dữ liệu, logic và cấu hình độc lập với nhau. Điều này không chỉ làm cho mã nguồn của bạn dễ bảo trì hơn mà còn cho phép bạn thay thế các phần riêng lẻ mà không làm hỏng toàn bộ hệ thống. Bằng cách đó, bạn sẽ có được tính linh hoạt, cải thiện khả năng kiểm thử và bảo vệ kiến trúc của bạn trước những thay đổi không thể tránh khỏi trong các công cụ và framework.
Hơn nữa, tôi vẫn đang khám phá không gian prompt và có xu hướng nhảy vào mã nhanh hơn là nghiên cứu. Nếu bạn tìm thấy giải pháp hoàn hảo cho vấn đề này, hãy cho tôi biết nhé. 🙇