Cốt lõi của các tính năng AI trong ứng dụng là các yêu cầu về mô hình tạo sinh, nhưng hiếm khi bạn chỉ có thể lấy dữ liệu đầu vào của người dùng, chuyển dữ liệu đó đến mô hình và hiển thị đầu ra của mô hình cho người dùng. Thông thường, có các bước xử lý trước và sau phải đi kèm với lệnh gọi mô hình. Ví dụ:
- Truy xuất thông tin theo bối cảnh để gửi cùng với lệnh gọi mô hình
- Truy xuất nhật ký của phiên hiện tại của người dùng, ví dụ: trong ứng dụng trò chuyện
- Sử dụng một mô hình để định dạng lại dữ liệu đầu vào của người dùng theo cách phù hợp để truyền sang một mô hình khác
- Đánh giá "mức độ an toàn" của kết quả của một mô hình trước khi trình bày kết quả đó cho người dùng
- Kết hợp đầu ra của một số mô hình
Mọi bước trong quy trình công việc này phải hoạt động cùng nhau để mọi tác vụ liên quan đến AI đều thành công.
Trong Genkit, bạn thể hiện logic được liên kết chặt chẽ này bằng cách sử dụng một cấu trúc có tên là dòng chảy. Flow được viết giống như các hàm, sử dụng mã TypeScript thông thường, nhưng thêm các chức năng khác nhằm giúp dễ dàng phát triển các tính năng AI:
- An toàn về kiểu: Các giản đồ đầu vào và đầu ra được xác định bằng Zod, cung cấp cả tính năng kiểm tra kiểu tĩnh và thời gian chạy
- Tích hợp với giao diện người dùng dành cho nhà phát triển: Gỡ lỗi các luồng độc lập với mã ứng dụng bằng giao diện người dùng dành cho nhà phát triển. Trong giao diện người dùng dành cho nhà phát triển, bạn có thể chạy các luồng và xem dấu vết cho từng bước của luồng.
- Triển khai đơn giản: Triển khai trực tiếp các luồng dưới dạng điểm cuối API web, sử dụng Cloud Functions cho Firebase hoặc bất kỳ nền tảng nào có thể lưu trữ ứng dụng web.
Không giống như các tính năng tương tự trong các khung khác, luồng của Genkit có kích thước nhỏ và không gây khó chịu, đồng thời không buộc ứng dụng của bạn phải tuân theo bất kỳ khái niệm trừu tượng cụ thể nào. Tất cả logic của flow đều được viết bằng TypeScript chuẩn và mã bên trong flow không cần phải nhận biết flow.
Xác định và gọi flow
Ở dạng đơn giản nhất, flow chỉ gói một hàm. Ví dụ sau đây gói một hàm gọi generate()
:
export const menuSuggestionFlow = ai.defineFlow(
{
name: 'menuSuggestionFlow',
},
async (restaurantTheme) => {
const { text } = await ai.generate({
model: gemini15Flash,
prompt: `Invent a menu item for a ${restaurantTheme} themed restaurant.`,
});
return text;
}
);
Chỉ cần gói các lệnh gọi generate()
như thế này, bạn đã thêm một số chức năng: việc này cho phép bạn chạy luồng từ Genkit CLI và từ giao diện người dùng dành cho nhà phát triển, đồng thời là yêu cầu đối với một số tính năng của Genkit, bao gồm cả việc triển khai và khả năng quan sát (các phần sau sẽ thảo luận về các chủ đề này).
Giản đồ đầu vào và đầu ra
Một trong những lợi thế quan trọng nhất của luồng Genkit so với việc gọi trực tiếp API mô hình là độ an toàn về kiểu của cả dữ liệu đầu vào và đầu ra. Khi xác định luồng, bạn có thể xác định giản đồ cho các luồng đó bằng cách sử dụng Zod, tương tự như cách bạn xác định giản đồ đầu ra của lệnh gọi generate()
; tuy nhiên, không giống như generate()
, bạn cũng có thể chỉ định giản đồ đầu vào.
Dưới đây là ví dụ tinh chỉnh về ví dụ cuối cùng, trong đó xác định một flow lấy một chuỗi làm đầu vào và xuất ra một đối tượng:
const MenuItemSchema = z.object({
dishname: z.string(),
description: z.string(),
});
export const menuSuggestionFlowWithSchema = ai.defineFlow(
{
name: 'menuSuggestionFlow',
inputSchema: z.string(),
outputSchema: MenuItemSchema,
},
async (restaurantTheme) => {
const { output } = await ai.generate({
model: gemini15Flash,
prompt: `Invent a menu item for a ${restaurantTheme} themed restaurant.`,
output: { schema: MenuItemSchema },
});
if (output == null) {
throw new Error("Response doesn't satisfy schema.");
}
return output;
}
);
Xin lưu ý rằng giản đồ của một flow không nhất thiết phải khớp với giản đồ của các lệnh gọi generate()
trong flow (thực tế, một flow thậm chí có thể không chứa lệnh gọi generate()
). Dưới đây là một biến thể của ví dụ truyền giản đồ đến generate()
, nhưng sử dụng đầu ra có cấu trúc để định dạng một chuỗi đơn giản mà luồng trả về.
export const menuSuggestionFlowMarkdown = ai.defineFlow(
{
name: 'menuSuggestionFlow',
inputSchema: z.string(),
outputSchema: z.string(),
},
async (restaurantTheme) => {
const { output } = await ai.generate({
model: gemini15Flash,
prompt: `Invent a menu item for a ${restaurantTheme} themed restaurant.`,
output: { schema: MenuItemSchema },
});
if (output == null) {
throw new Error("Response doesn't satisfy schema.");
}
return `**${output.dishname}**: ${output.description}`;
}
);
Luồng gọi
Sau khi xác định một flow, bạn có thể gọi flow đó từ mã Node.js:
const { text } = await menuSuggestionFlow('bistro');
Đối số cho flow phải tuân thủ giản đồ đầu vào, nếu bạn đã xác định giản đồ.
Nếu bạn đã xác định giản đồ đầu ra, thì phản hồi của flow sẽ tuân theo giản đồ đó. Ví dụ: nếu bạn đặt giản đồ đầu ra thành MenuItemSchema
, thì đầu ra của flow sẽ chứa các thuộc tính của giản đồ đó:
const { dishname, description } =
await menuSuggestionFlowWithSchema('bistro');
Luồng phát trực tuyến
Flow hỗ trợ tính năng phát trực tuyến bằng cách sử dụng giao diện tương tự như giao diện phát trực tuyến của generate()
. Tính năng truyền trực tuyến rất hữu ích khi luồng của bạn tạo ra một lượng lớn đầu ra, vì bạn có thể hiển thị đầu ra cho người dùng trong khi đầu ra đang được tạo, giúp cải thiện khả năng phản hồi của ứng dụng. Ví dụ quen thuộc: giao diện LLM dựa trên cuộc trò chuyện thường truyền trực tuyến các phản hồi của chúng cho người dùng trong khi các phản hồi đó được tạo.
Dưới đây là ví dụ về một luồng hỗ trợ truyền trực tuyến:
export const menuSuggestionStreamingFlow = ai.defineStreamingFlow(
{
name: 'menuSuggestionFlow',
inputSchema: z.string(),
streamSchema: z.string(),
outputSchema: z.object({ theme: z.string(), menuItem: z.string() }),
},
async (restaurantTheme, streamingCallback) => {
const response = await ai.generateStream({
model: gemini15Flash,
prompt: `Invent a menu item for a ${restaurantTheme} themed restaurant.`,
});
if (streamingCallback) {
for await (const chunk of response.stream) {
// Here, you could process the chunk in some way before sending it to
// the output stream via streamingCallback(). In this example, we output
// the text of the chunk, unmodified.
streamingCallback(chunk.text);
}
}
return {
theme: restaurantTheme,
menuItem: (await response.response).text,
};
}
);
- Tuỳ chọn
streamSchema
chỉ định loại giá trị mà luồng của bạn truyền. Loại này không nhất thiết phải giống vớioutputSchema
, là loại đầu ra hoàn chỉnh của flow. streamingCallback
là một hàm callback nhận một tham số duy nhất thuộc loại dostreamSchema
chỉ định. Bất cứ khi nào có dữ liệu trong luồng, hãy gửi dữ liệu đó đến luồng đầu ra bằng cách gọi hàm này. Xin lưu ý rằngstreamingCallback
chỉ được xác định nếu phương thức gọi của luồng yêu cầu đầu ra truyền trực tuyến, vì vậy, bạn cần kiểm tra để đảm bảo rằng phương thức gọi này được xác định trước khi gọi.
Trong ví dụ trên, các giá trị do flow truyền trực tuyến được ghép nối trực tiếp với các giá trị do lệnh gọi generate()
truyền trực tuyến bên trong flow. Mặc dù đây thường là trường hợp, nhưng không nhất thiết phải như vậy: bạn có thể xuất giá trị vào luồng bằng cách sử dụng lệnh gọi lại thường xuyên khi hữu ích cho luồng của mình.
Gọi luồng truyền trực tuyến
Luồng truyền trực tuyến cũng có thể gọi được, nhưng các luồng này sẽ trả về ngay một đối tượng phản hồi thay vì một lời hứa:
const response = menuSuggestionStreamingFlow('Danube');
Đối tượng phản hồi có một thuộc tính luồng mà bạn có thể sử dụng để lặp lại đầu ra truyền trực tuyến của flow khi được tạo:
for await (const chunk of response.stream) {
console.log('chunk', chunk);
}
Bạn cũng có thể nhận được kết quả đầy đủ của luồng, như bạn có thể làm với luồng không truyền trực tuyến:
const output = await response.output;
Xin lưu ý rằng đầu ra truyền trực tuyến của một flow có thể không cùng loại với đầu ra hoàn chỉnh; đầu ra truyền trực tuyến tuân theo streamSchema
, trong khi đầu ra hoàn chỉnh tuân theo outputSchema
.
Chạy flow từ dòng lệnh
Bạn có thể chạy flow từ dòng lệnh bằng công cụ Genkit CLI:
genkit flow:run menuSuggestionFlow '"French"'
Đối với luồng truyền trực tuyến, bạn có thể in đầu ra truyền trực tuyến vào bảng điều khiển bằng cách thêm cờ -s
:
genkit flow:run menuSuggestionFlow '"French"' -s
Việc chạy một flow qua dòng lệnh rất hữu ích để kiểm thử flow hoặc để chạy các flow thực hiện các tác vụ cần thiết trên cơ sở đặc biệt, ví dụ: để chạy một flow nhập tài liệu vào cơ sở dữ liệu vectơ.
Gỡ lỗi luồng
Một trong những lợi ích của việc đóng gói logic AI trong một luồng là bạn có thể kiểm thử và gỡ lỗi luồng độc lập với ứng dụng bằng cách sử dụng giao diện người dùng dành cho nhà phát triển Genkit.
Để khởi động giao diện người dùng dành cho nhà phát triển, hãy chạy các lệnh sau từ thư mục dự án:
genkit start -- tsx --watch src/your-code.ts
Trong thẻ Run (Chạy) của giao diện người dùng dành cho nhà phát triển, bạn có thể chạy bất kỳ luồng nào được xác định trong dự án:
Sau khi chạy một flow, bạn có thể kiểm tra dấu vết của lệnh gọi flow bằng cách nhấp vào Xem dấu vết hoặc xem trên thẻ Kiểm tra.
Trong trình xem dấu vết, bạn có thể xem thông tin chi tiết về quá trình thực thi toàn bộ luồng, cũng như thông tin chi tiết về từng bước riêng lẻ trong luồng. Ví dụ: hãy xem xét quy trình sau đây, trong đó chứa một số yêu cầu tạo:
const PrixFixeMenuSchema = z.object({
starter: z.string(),
soup: z.string(),
main: z.string(),
dessert: z.string(),
});
export const complexMenuSuggestionFlow = ai.defineFlow(
{
name: 'complexMenuSuggestionFlow',
inputSchema: z.string(),
outputSchema: PrixFixeMenuSchema,
},
async (theme: string): Promise<z.infer<typeof PrixFixeMenuSchema>> => {
const chat = ai.chat({ model: gemini15Flash });
await chat.send('What makes a good prix fixe menu?');
await chat.send(
'What are some ingredients, seasonings, and cooking techniques that ' +
`would work for a ${theme} themed menu?`
);
const { output } = await chat.send({
prompt:
`Based on our discussion, invent a prix fixe menu for a ${theme} ` +
'themed restaurant.',
output: {
schema: PrixFixeMenuSchema,
},
});
if (!output) {
throw new Error('No data generated.');
}
return output;
}
);
Khi bạn chạy luồng này, trình xem dấu vết sẽ hiển thị cho bạn thông tin chi tiết về từng yêu cầu tạo, bao gồm cả đầu ra của yêu cầu đó:
Các bước trong quy trình
Trong ví dụ cuối cùng, bạn đã thấy mỗi lệnh gọi generate()
xuất hiện dưới dạng một bước riêng biệt trong trình xem dấu vết. Mỗi hành động cơ bản của Genkit sẽ xuất hiện dưới dạng các bước riêng biệt của một luồng:
generate()
Chat.send()
embed()
index()
retrieve()
Nếu muốn đưa mã khác ngoài mã trên vào dấu vết, bạn có thể thực hiện việc này bằng cách gói mã trong lệnh gọi run()
. Bạn có thể thực hiện việc này đối với các lệnh gọi đến thư viện của bên thứ ba không nhận biết được Genkit hoặc đối với bất kỳ phần mã quan trọng nào.
Ví dụ: sau đây là một luồng có hai bước: bước đầu tiên truy xuất trình đơn bằng một số phương thức không xác định và bước thứ hai bao gồm trình đơn làm ngữ cảnh cho lệnh gọi generate()
.
export const menuQuestionFlow = ai.defineFlow(
{
name: 'menuQuestionFlow',
inputSchema: z.string(),
outputSchema: z.string(),
},
async (input: string): Promise<string> => {
const menu = await run('retrieve-daily-menu', async (): Promise<string> => {
// Retrieve today's menu. (This could be a database access or simply
// fetching the menu from your website.)
// ...
return menu;
});
const { text } = await ai.generate({
model: gemini15Flash,
system: "Help the user answer questions about today's menu.",
prompt: input,
docs: [{ content: [{ text: menu }] }],
});
return text;
}
);
Vì bước truy xuất được gói trong lệnh gọi run()
, nên bước này được đưa vào dưới dạng một bước trong trình xem dấu vết:
Triển khai flow
Bạn có thể triển khai luồng trực tiếp dưới dạng điểm cuối API web, sẵn sàng để bạn gọi từ ứng dụng khách. Việc triển khai được thảo luận chi tiết trên một số trang khác, nhưng phần này cung cấp thông tin tổng quan ngắn gọn về các tuỳ chọn triển khai.
Cloud Functions cho Firebase
Để triển khai flow bằng Cloud Functions cho Firebase, hãy sử dụng trình bổ trợ firebase
. Trong định nghĩa luồng, hãy thay thế defineFlow
bằng onFlow
và thêm authPolicy
.
import { firebaseAuth } from '@genkit-ai/firebase/auth';
import { onFlow } from '@genkit-ai/firebase/functions';
export const menuSuggestion = onFlow(
ai,
{
name: 'menuSuggestionFlow',
authPolicy: firebaseAuth((user) => {
if (!user.email_verified) {
throw new Error('Verified email required to run flow');
}
}),
},
async (restaurantTheme) => {
// ...
}
);
Để biết thêm thông tin, hãy xem các trang sau:
Express.js
Để triển khai flow bằng bất kỳ nền tảng lưu trữ Node.js nào, chẳng hạn như Cloud Run, hãy xác định flow bằng defineFlow()
, sau đó gọi startFlowServer()
:
export const menuSuggestionFlow = ai.defineFlow(
{
name: 'menuSuggestionFlow',
},
async (restaurantTheme) => {
// ...
}
);
ai.startFlowServer({
flows: [menuSuggestionFlow],
});
Theo mặc định, startFlowServer
sẽ phân phát tất cả các luồng được xác định trong cơ sở mã của bạn dưới dạng điểm cuối HTTP (ví dụ: http://localhost:3400/menuSuggestionFlow
). Bạn có thể gọi một luồng bằng yêu cầu POST như sau:
curl -X POST "http://localhost:3400/menuSuggestionFlow" \
-H "Content-Type: application/json" -d '{"data": "banana"}'
Nếu cần, bạn có thể tuỳ chỉnh máy chủ flow để phân phát một danh sách flow cụ thể, như minh hoạ bên dưới. Bạn cũng có thể chỉ định một cổng tuỳ chỉnh (cổng này sẽ sử dụng biến môi trường PORT nếu được đặt) hoặc chỉ định chế độ cài đặt CORS.
export const flowA = ai.defineFlow({ name: 'flowA' }, async (subject) => {
// ...
});
export const flowB = ai.defineFlow({ name: 'flowB' }, async (subject) => {
// ...
});
ai.startFlowServer({
flows: [flowB],
port: 4567,
cors: {
origin: '*',
},
});
Để biết thông tin về cách triển khai cho các nền tảng cụ thể, hãy xem phần Triển khai bằng Cloud Run và Triển khai luồng cho mọi nền tảng Node.js.