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

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

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

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

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

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

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

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

รูปแบบที่ง่ายที่สุดคือโฟลว์จะรวมฟังก์ชัน ตัวอย่างต่อไปนี้จะรวมฟังก์ชันที่เรียก GenerateData()

menuSuggestionFlow := genkit.DefineFlow(g, "menuSuggestionFlow",
    func(ctx context.Context, theme string) (string, error) {
        resp, err := genkit.GenerateData(ctx, g,
            ai.WithPrompt("Invent a menu item for a %s themed restaurant.", theme),
        )
        if err != nil {
            return "", err
        }

        return resp.Text(), nil
    })

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

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

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

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

type MenuItem struct {
    Name        string `json:"name"`
    Description string `json:"description"`
}

menuSuggestionFlow := genkit.DefineFlow(g, "menuSuggestionFlow",
    func(ctx context.Context, theme string) (MenuItem, error) {
        return genkit.GenerateData[MenuItem](ctx, g,
            ai.WithPrompt("Invent a menu item for a %s themed restaurant.", theme),
        )
    })

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

type MenuItem struct {
    Name        string `json:"name"`
    Description string `json:"description"`
}

menuSuggestionMarkdownFlow := genkit.DefineFlow(g, "menuSuggestionMarkdownFlow",
    func(ctx context.Context, theme string) (string, error) {
        item, _, err := genkit.GenerateData[MenuItem](ctx, g,
            ai.WithPrompt("Invent a menu item for a %s themed restaurant.", theme),
        )
        if err != nil {
            return "", err
        }

        return fmt.Sprintf("**%s**: %s", item.Name, item.Description), nil
    })

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

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

item, err := menuSuggestionFlow.Run(ctx, "bistro")

อาร์กิวเมนต์ของโฟลว์ต้องเป็นไปตามสคีมาอินพุต

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

item, err := menuSuggestionFlow.Run(ctx, "bistro")
if err != nil {
    log.Fatal(err)
}

log.Println(item.DishName)
log.Println(item.Description)

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

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

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

type Menu struct {
    Theme  string     `json:"theme"`
    Items  []MenuItem `json:"items"`
}

type MenuItem struct {
    Name        string `json:"name"`
    Description string `json:"description"`
}

menuSuggestionFlow := genkit.DefineStreamingFlow(g, "menuSuggestionFlow",
    func(ctx context.Context, theme string, callback core.StreamCallback[string]) (Menu, error) {
        item, _, err := genkit.GenerateData[MenuItem](ctx, g,
            ai.WithPrompt("Invent a menu item for a %s themed restaurant.", theme),
            ai.WithStreaming(func(ctx context.Context, chunk *ai.ModelResponseChunk) error {
                // Here, you could process the chunk in some way before sending it to
                // the output stream using StreamCallback. In this example, we output
                // the text of the chunk, unmodified.
                return callback(ctx, chunk.Text())
            }),
        )
        if err != nil {
            return nil, err
        }

        return Menu{
            Theme: theme,
            Items: []MenuItem{item},
        }, nil
    })

ประเภท string ใน StreamCallback[string] จะระบุประเภทค่าที่สตรีมการไหล ซึ่งไม่จำเป็นต้องเป็นประเภทเดียวกับประเภทผลลัพธ์ ซึ่งเป็นประเภทของเอาต์พุตที่สมบูรณ์ของโฟลว์ (Menu ในตัวอย่างนี้)

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

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

โฟลว์สตรีมมิงสามารถทํางานได้เหมือนโฟลว์แบบไม่สตรีมด้วย menuSuggestionFlow.Run(ctx, "bistro") หรือจะสตรีมก็ได้ โดยทําดังนี้

streamCh, err := menuSuggestionFlow.Stream(ctx, "bistro")
if err != nil {
    log.Fatal(err)
}

for result := range streamCh {
    if result.Err != nil {
        log.Fatal("Stream error: %v", result.Err)
    }
    if result.Done {
        log.Printf("Menu with %s theme:\n", result.Output.Theme)
        for item := range result.Output.Items {
            log.Println(" - %s: %s", item.Name, item.Description)
        }
    } else {
        log.Println("Stream chunk:", result.Stream)
    }
}

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

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

genkit flow:run menuSuggestionFlow '"French"'

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

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

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

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

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

UI ของนักพัฒนาแอปอาศัยที่แอป Go ทำงานต่อไป แม้ว่าตรรกะจะเสร็จสมบูรณ์แล้วก็ตาม หากคุณเพิ่งเริ่มต้นใช้งานและ Genkit ไม่ได้เป็นส่วนหนึ่งของแอปที่กว้างขึ้น ให้เพิ่ม select {} เป็นบรรทัดสุดท้ายของ main() เพื่อป้องกันไม่ให้แอปปิดลงเพื่อให้คุณตรวจสอบได้ใน UI

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

genkit start -- go run .

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

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

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

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

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

net/http เซิร์ฟเวอร์

หากต้องการทำให้โฟลว์ใช้งานได้โดยใช้แพลตฟอร์มโฮสติ้ง Go เช่น Cloud Run ให้กำหนดโฟลว์โดยใช้ DefineFlow() และเริ่มเซิร์ฟเวอร์ net/http ด้วยตัวแฮนเดิลโฟลว์ที่ระบุไว้

import (
    "context"
    "log"
    "net/http"

    "github.com/firebase/genkit/go/genkit"
    "github.com/firebase/genkit/go/plugins/googlegenai"
    "github.com/firebase/genkit/go/plugins/server"
)

func main() {
    ctx := context.Background()

    g, err := genkit.Init(ctx, genkit.WithPlugins(&googlegenai.GoogleAI{}))
    if err != nil {
      log.Fatal(err)
    }

    menuSuggestionFlow := genkit.DefineFlow(g, "menuSuggestionFlow",
        func(ctx context.Context, theme string) (MenuItem, error) {
            // Flow implementation...
        })

    mux := http.NewServeMux()
    mux.HandleFunc("POST /menuSuggestionFlow", genkit.Handler(menuSuggestionFlow))
    log.Fatal(server.Start(ctx, "127.0.0.1:3400", mux))
}

server.Start() เป็นฟังก์ชันตัวช่วยที่ไม่บังคับซึ่งจะเริ่มต้นเซิร์ฟเวอร์และจัดการวงจรชีวิตของเซิร์ฟเวอร์ รวมถึงจับสัญญาณการขัดจังหวะเพื่อพัฒนาซอฟต์แวร์ในเครื่องได้ง่ายขึ้น แต่คุณก็ใช้เมธอดของคุณเองได้

หากต้องการแสดงขั้นตอนทั้งหมดที่กําหนดไว้ในโค้ดเบส คุณสามารถใช้ ListFlows() ดังนี้

mux := http.NewServeMux()
for _, flow := range genkit.ListFlows(g) {
    mux.HandleFunc("POST /"+flow.Name(), genkit.Handler(flow))
}
log.Fatal(server.Start(ctx, "127.0.0.1:3400", mux))

คุณเรียกใช้ปลายทางของโฟลว์ด้วยคําขอ POST ได้โดยทําดังนี้

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

เฟรมเวิร์กเซิร์ฟเวอร์อื่นๆ

นอกจากนี้ คุณยังใช้เฟรมเวิร์กเซิร์ฟเวอร์อื่นๆ เพื่อทำให้โฟลว์ใช้งานได้ เช่น คุณสามารถใช้ Gin เพียงไม่กี่บรรทัด ดังนี้

router := gin.Default()
for _, flow := range genkit.ListFlows(g) {
    router.POST("/"+flow.Name(), func(c *gin.Context) {
        genkit.Handler(flow)(c.Writer, c.Request)
    })
}
log.Fatal(router.Run(":3400"))

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