การกําหนดเวิร์กโฟลว์ AI

หัวใจสําคัญของฟีเจอร์ AI ของแอปคือคําขอโมเดล Generative แต่คุณไม่สามารถนําอินพุตของผู้ใช้ส่งไปยังโมเดลและแสดงเอาต์พุตของโมเดลกลับไปยังผู้ใช้ได้ง่ายๆ โดยปกติแล้ว จะมีการประมวลผลก่อนและหลังการเรียกใช้โมเดล เช่น

  • ดึงข้อมูลตามบริบทเพื่อส่งไปพร้อมกับการเรียกใช้โมเดล
  • เรียกดูประวัติของเซสชันปัจจุบันของผู้ใช้ เช่น ในแชทแอป
  • การใช้โมเดลหนึ่งเพื่อจัดรูปแบบอินพุตของผู้ใช้ใหม่ในลักษณะที่เหมาะที่จะส่งต่อไปยังโมเดลอื่น
  • การประเมิน "ความปลอดภัย" ของเอาต์พุตของโมเดลก่อนที่จะแสดงต่อผู้ใช้
  • การรวมเอาต์พุตของหลายรูปแบบ

ขั้นตอนเวิร์กโฟลว์นี้ทุกขั้นตอนต้องทำงานร่วมกันเพื่อให้งานที่เกี่ยวข้องกับ AI บรรลุผล

ใน Genkit คุณจะแสดงตรรกะที่เชื่อมโยงกันอย่างแน่นหนานี้โดยใช้โครงสร้างที่เรียกว่าฟิวเจอร์ ฟลายว์เขียนขึ้นเหมือนกับฟังก์ชันโดยใช้โค้ด TypeScript ธรรมดา แต่จะมีความสามารถเพิ่มเติมที่ช่วยให้การพัฒนาฟีเจอร์ AI ง่ายขึ้น ดังนี้

  • ความปลอดภัยของประเภท: สคีมาอินพุตและเอาต์พุตที่กําหนดโดยใช้ Zod ซึ่งให้บริการทั้งการตรวจสอบประเภทแบบคงที่และแบบรันไทม์
  • การผสานรวมกับ UI นักพัฒนาซอฟต์แวร์: แก้ไขข้อบกพร่องของขั้นตอนโดยไม่เกี่ยวข้องกับโค้ดแอปพลิเคชันโดยใช้ UI นักพัฒนาซอฟต์แวร์ ใน UI ของนักพัฒนาซอฟต์แวร์ คุณสามารถเรียกใช้โฟลว์และดูร่องรอยของโฟลว์แต่ละขั้นตอนได้
  • การปรับใช้งานที่ง่ายขึ้น: ปรับใช้งานโฟลว์เป็นปลายทางของ Web API โดยตรงโดยใช้ Cloud Functions สําหรับ Firebase หรือแพลตฟอร์มใดก็ได้ที่โฮสต์เว็บแอปได้

ขั้นตอนของ Genkit นั้นเบาและไม่รบกวน ต่างจากฟีเจอร์ที่คล้ายกันในเฟรมเวิร์กอื่นๆ และไม่บังคับให้แอปของคุณเป็นไปตามการแยกความคิดที่เฉพาะเจาะจง ตรรกะทั้งหมดของโฟลว์เขียนด้วย TypeScript มาตรฐาน และโค้ดภายในโฟลว์ไม่จำเป็นต้องรับรู้โฟลว์

การกําหนดและเรียกใช้โฟลว์

รูปแบบที่ง่ายที่สุดของโฟลว์คือเพียงแค่รวมฟังก์ชัน ตัวอย่างต่อไปนี้จะรวมฟังก์ชันที่เรียก 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;
  }
);

เพียงแค่รวมการเรียกใช้ generate() แบบนี้ คุณก็เพิ่มฟังก์ชันการทำงานบางอย่างได้ ซึ่งจะช่วยให้คุณเรียกใช้ขั้นตอนต่างๆ จาก Genkit CLI และจาก UI ของนักพัฒนาซอฟต์แวร์ได้ และเป็นข้อกําหนดสําหรับฟีเจอร์หลายอย่างของ Genkit ซึ่งรวมถึงการติดตั้งใช้งานและการสังเกตการณ์ (ส่วนต่อๆ ไปจะกล่าวถึงหัวข้อเหล่านี้)

สคีมาอินพุตและเอาต์พุต

ข้อดีที่สำคัญที่สุดอย่างหนึ่งของเวิร์กโฟลว์ Genkit เหนือการเรียกใช้ API โมเดลโดยตรงคือความปลอดภัยของประเภททั้งอินพุตและเอาต์พุต เมื่อกําหนดโฟลว์ คุณสามารถกําหนดสคีมาสําหรับโฟลว์ได้โดยใช้ Zod ในลักษณะเดียวกับที่กําหนดสคีมาเอาต์พุตของคําเรียก generate() อย่างไรก็ตาม คุณสามารถระบุสคีมาอินพุตได้ด้วย ซึ่งแตกต่างจาก generate()

ต่อไปนี้เป็นการปรับแต่งตัวอย่างสุดท้าย ซึ่งจะกำหนดขั้นตอนที่รับสตริงเป็นอินพุตและแสดงผลออบเจ็กต์

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;
  }
);

โปรดทราบว่าสคีมาของโฟลว์ไม่จำเป็นต้องสอดคล้องกับสคีมาของคําเรียก generate() ภายในโฟลว์ (อันที่จริงแล้ว โฟลว์อาจไม่มีคําเรียก generate() เลยก็ได้) ต่อไปนี้คือตัวอย่างรูปแบบหนึ่งที่ส่งสคีมาไปยัง generate() แต่ใช้เอาต์พุตที่มีโครงสร้างเพื่อจัดรูปแบบสตริงแบบง่าย ซึ่งโฟลว์จะแสดงผล

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}`;
  }
);

ขั้นตอนการโทร

เมื่อกําหนดขั้นตอนแล้ว คุณจะเรียกใช้ขั้นตอนจากโค้ด Node.js ได้โดยทำดังนี้

const { text } = await menuSuggestionFlow('bistro');

อาร์กิวเมนต์ของโฟลว์ต้องสอดคล้องกับสคีมาอินพุต หากคุณได้กําหนดไว้

หากคุณกําหนดสคีมาเอาต์พุตไว้ การตอบกลับของเวิร์กโฟลว์จะเป็นไปตามสคีมานั้น ตัวอย่างเช่น หากคุณตั้งค่าสคีมาเอาต์พุตเป็น MenuItemSchema เอาต์พุตของโฟลว์จะมีพร็อพเพอร์ตี้ต่อไปนี้

const { dishname, description } =
  await menuSuggestionFlowWithSchema('bistro');

ขั้นตอนการสตรีม

ฟีดรองรับการสตรีมโดยใช้อินเทอร์เฟซที่คล้ายกับอินเทอร์เฟซสตรีมมิงของ generate() สตรีมมิงมีประโยชน์เมื่อโฟลว์สร้างเอาต์พุตจำนวนมาก เนื่องจากคุณสามารถแสดงเอาต์พุตต่อผู้ใช้ขณะที่สร้างขึ้น ซึ่งจะช่วยปรับปรุงการตอบสนองที่ผู้ใช้รับรู้ของแอป ตัวอย่างที่คุ้นเคยคือ อินเทอร์เฟซ LLM ที่อิงตามแชทมักจะสตรีมคำตอบไปยังผู้ใช้ขณะที่สร้างขึ้น

ต่อไปนี้คือตัวอย่างของขั้นตอนที่รองรับสตรีมมิง

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,
    };
  }
);
  • ตัวเลือก streamSchema จะระบุประเภทค่าที่สตรีมข้อมูลพร็อพเพอร์ตี้ ซึ่งไม่จำเป็นต้องเป็นประเภทเดียวกับ outputSchema ซึ่งเป็นประเภทของเอาต์พุตที่สมบูรณ์ของโฟลว์
  • streamingCallback คือฟังก์ชัน Callback ที่ใช้พารามิเตอร์เดียวซึ่งมีประเภทตามที่ระบุโดย streamSchema เมื่อใดก็ตามที่มีข้อมูลภายในโฟลว์ ให้ส่งข้อมูลไปยังสตรีมเอาต์พุตโดยการเรียกใช้ฟังก์ชันนี้ โปรดทราบว่า streamingCallback จะได้รับการกําหนดก็ต่อเมื่อผู้เรียกใช้โฟลว์ขอเอาต์พุตสตรีมมิงเท่านั้น คุณจึงต้องตรวจสอบว่าได้กําหนด streamingCallback ไว้แล้วก่อนที่จะเรียกใช้

ในตัวอย่างข้างต้น ค่าที่สตรีมโดยโฟลว์จะเชื่อมโยงโดยตรงกับค่าที่สตรีมโดยการเรียกใช้ generate() ภายในโฟลว์ แม้ว่ากรณีนี้จะเกิดขึ้นบ่อยครั้ง แต่ก็ไม่จำเป็นต้องเป็นเช่นนั้น คุณส่งออกค่าไปยังสตรีมได้โดยใช้การเรียกกลับบ่อยเท่าที่ต้องการสำหรับขั้นตอนของคุณ

การเรียกใช้สตรีมโฟลว์

คุณสามารถเรียกใช้โฟลว์สตรีมมิงได้เช่นกัน แต่โฟลว์จะแสดงผลออบเจ็กต์คำตอบแทนพรอมต์ในทันที

const response = menuSuggestionStreamingFlow('Danube');

ออบเจ็กต์การตอบกลับมีพร็อพเพอร์ตี้สตรีม ซึ่งคุณใช้เพื่อวนซ้ำเอาต์พุตสตรีมมิงของโฟลว์ขณะที่สร้างขึ้นได้โดยทำดังนี้

for await (const chunk of response.stream) {
  console.log('chunk', chunk);
}

นอกจากนี้ คุณยังดูเอาต์พุตทั้งหมดของขั้นตอนการทำงานได้เช่นเดียวกับขั้นตอนการทำงานแบบไม่สตรีม ดังนี้

const output = await response.output;

โปรดทราบว่าเอาต์พุตสตรีมมิงของโฟลว์อาจไม่ใช่ประเภทเดียวกับเอาต์พุตที่สมบูรณ์ เอาต์พุตสตรีมมิงเป็นไปตาม streamSchema ส่วนเอาต์พุตที่สมบูรณ์เป็นไปตาม outputSchema

การเรียกใช้โฟลว์จากบรรทัดคำสั่ง

คุณเรียกใช้โฟลว์จากบรรทัดคำสั่งได้โดยใช้เครื่องมือ Genkit CLI โดยทำดังนี้

genkit flow:run menuSuggestionFlow '"French"'

สำหรับสตรีมโฟลว์ คุณสามารถพิมพ์เอาต์พุตสตรีมมิงไปยังคอนโซลได้โดยเพิ่ม Flag -s ดังนี้

genkit flow:run menuSuggestionFlow '"French"' -s

การเรียกใช้โฟลว์จากบรรทัดคำสั่งมีประโยชน์สำหรับการทดสอบโฟลว์ หรือเพื่อเรียกใช้โฟลว์ที่ทำงานตามความจำเป็นเฉพาะกิจ เช่น เพื่อเรียกใช้โฟลว์ที่ส่งผ่านข้อมูลเอกสารไปยังฐานข้อมูลเวกเตอร์

ขั้นตอนการแก้ไขข้อบกพร่อง

ข้อดีอย่างหนึ่งของการรวมตรรกะ AI ภายในขั้นตอนคือคุณสามารถทดสอบและแก้ไขข้อบกพร่องของขั้นตอนแยกจากแอปได้โดยใช้ UI สำหรับนักพัฒนาซอฟต์แวร์ Genkit

หากต้องการเริ่ม UI ของนักพัฒนาซอฟต์แวร์ ให้เรียกใช้คําสั่งต่อไปนี้จากไดเรกทอรีโครงการ

genkit start -- tsx --watch src/your-code.ts

จากแท็บเรียกใช้ของ UI สําหรับนักพัฒนาซอฟต์แวร์ คุณสามารถเรียกใช้ขั้นตอนใดก็ได้ที่กําหนดไว้ในโปรเจ็กต์

ภาพหน้าจอของโปรแกรมเรียกใช้โฟลว์

หลังจากเรียกใช้โฟลว์แล้ว คุณสามารถตรวจสอบร่องรอยของการเรียกใช้โฟลว์ได้โดยคลิกดูร่องรอยหรือดูในแท็บตรวจสอบ

ในเครื่องมือดูการติดตาม คุณจะเห็นรายละเอียดเกี่ยวกับการดําเนินการของทั้งเวิร์กโฟลว์ รวมถึงรายละเอียดของแต่ละขั้นตอนในเวิร์กโฟลว์ ตัวอย่างเช่น ให้พิจารณาขั้นตอนต่อไปนี้ซึ่งมีคำขอการสร้างหลายรายการ

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;
  }
);

เมื่อเรียกใช้ขั้นตอนนี้ เครื่องมือตรวจสอบการติดตามจะแสดงรายละเอียดเกี่ยวกับคำขอการสร้างแต่ละรายการ รวมถึงเอาต์พุตของคำขอ

ภาพหน้าจอของเครื่องมือตรวจสอบการติดตาม

ขั้นตอนของโฟลว์

ในตัวอย่างล่าสุด คุณเห็นว่าการเรียกใช้ generate() แต่ละรายการปรากฏเป็นขั้นตอนแยกกันในเครื่องมือดูการติดตาม การดำเนินการพื้นฐานแต่ละรายการของ Genkit จะปรากฏเป็นขั้นตอนแยกต่างหากของขั้นตอนการทำงาน ดังนี้

  • generate()
  • Chat.send()
  • embed()
  • index()
  • retrieve()

หากต้องการรวมโค้ดอื่นนอกเหนือจากข้างต้นไว้ในร่องรอย ให้ทำโดยรวมโค้ดไว้ในการเรียก run() คุณอาจใช้การแยกส่วนนี้กับการเรียกใช้ไลบรารีของบุคคลที่สามที่ไม่รู้จัก Genkit หรือส่วนโค้ดที่สำคัญ

ตัวอย่างเช่น นี่เป็นเวิร์กโฟลว์แบบ 2 ขั้นตอน ขั้นตอนแรกจะดึงข้อมูลเมนูโดยใช้วิธีการที่ระบุไม่ชัดเจน และขั้นตอนที่ 2 จะรวมเมนูเป็นบริบทสําหรับการเรียกใช้ 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;
  }
);

เนื่องจากขั้นตอนการดึงข้อมูลอยู่ในการเรียกใช้ run() จึงรวมอยู่ในขั้นตอนในเครื่องมือดูการติดตาม

ภาพหน้าจอของขั้นตอนที่กําหนดไว้อย่างชัดเจนในเครื่องมือตรวจสอบการติดตาม

การปรับใช้โฟลว์

คุณสามารถทำให้โฟลว์ใช้งานได้โดยตรงเป็นปลายทางของ Web API ซึ่งพร้อมให้คุณเรียกใช้จากไคลเอ็นต์แอป เราได้อธิบายรายละเอียดเกี่ยวกับการทำให้ใช้งานได้ในหน้าอื่นๆ อีกหลายหน้า แต่ส่วนนี้จะกล่าวถึงภาพรวมคร่าวๆ ของตัวเลือกต่างๆ ในการทำให้ใช้งานได้

Cloud Functions for Firebase

หากต้องการทําให้เวิร์กโฟลว์ใช้งานได้ด้วย Cloud Functions for Firebase ให้ใช้ปลั๊กอิน firebase ในคำจำกัดความของโฟลว์ ให้แทนที่ defineFlow ด้วย onFlow และใส่ 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) => {
    // ...
  }
);

ดูข้อมูลเพิ่มเติมได้ที่หน้าต่อไปนี้

Express.js

หากต้องการทำให้เวิร์กโฟลว์ใช้งานได้โดยใช้แพลตฟอร์มโฮสติ้ง Node.js เช่น Cloud Run ให้กำหนดเวิร์กโฟลว์โดยใช้ defineFlow() แล้วเรียกใช้ startFlowServer() ดังนี้

export const menuSuggestionFlow = ai.defineFlow(
  {
    name: 'menuSuggestionFlow',
  },
  async (restaurantTheme) => {
    // ...
  }
);

ai.startFlowServer({
  flows: [menuSuggestionFlow],
});

โดยค่าเริ่มต้น startFlowServer จะแสดงขั้นตอนทั้งหมดที่กําหนดไว้ในโค้ดเบสของคุณเป็นปลายทาง HTTP (เช่น http://localhost:3400/menuSuggestionFlow) คุณสามารถเรียกใช้ขั้นตอนด้วยคําขอ POST ดังนี้

curl -X POST "http://localhost:3400/menuSuggestionFlow" \
  -H "Content-Type: application/json"  -d '{"data": "banana"}'

หากจําเป็น คุณสามารถปรับแต่งเซิร์ฟเวอร์โฟลว์ให้แสดงรายการโฟลว์ที่เฉพาะเจาะจงได้ ดังที่แสดงด้านล่าง นอกจากนี้ คุณยังระบุพอร์ตที่กำหนดเองได้ด้วย (ระบบจะใช้ตัวแปรสภาพแวดล้อม PORT หากตั้งค่าไว้) หรือระบุการตั้งค่า 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: '*',
  },
});

ดูข้อมูลเกี่ยวกับการทำให้ใช้งานได้ในแพลตฟอร์มที่เฉพาะเจาะจงได้ที่หัวข้อทำให้ใช้งานได้ด้วย Cloud Run และทำให้โฟลว์ใช้งานได้ในแพลตฟอร์ม Node.js ใดก็ได้