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

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

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

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

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

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

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

ขั้นตอนการกำหนดและการเรียกใช้

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

menuSuggestionFlow := genkit.DefineFlow(g, "menuSuggestionFlow",
    func(ctx context.Context, theme string) (string, error) {
        resp, err := genkit.Generate(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() อย่างไรก็ตาม คุณจะระบุสคีมาอินพุตได้ด้วย ซึ่งต่างจาก 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.GenerateData() แต่ใช้เอาต์พุตที่มีโครงสร้างเพื่อจัดรูปแบบสตริงแบบง่ายที่โฟลว์แสดงผล บันทึกวิธีที่เราส่ง MenuItem เป็นพารามิเตอร์ประเภท ซึ่งเทียบเท่ากับการส่งตัวเลือก WithOutputType() และรับค่าประเภทดังกล่าวในการตอบกลับ

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() ภายในโฟลว์โดยตรง แม้ว่าจะเป็นกรณีนี้อยู่บ่อยๆ แต่คุณก็ไม่จำเป็นต้องเป็นเช่นนั้น คุณสามารถส่งออกค่าไปยังสตรีมโดยใช้ Callback ให้บ่อยครั้งเท่าที่จะเป็นประโยชน์สำหรับโฟลว์ของคุณ

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

ขั้นตอนสตรีมมิงทำงานเหมือนกับขั้นตอนที่ไม่ใช่สตรีมมิงได้ด้วย 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"'

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

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

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

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

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

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

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

genkit start -- go run .

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

ภาพหน้าจอของตัววิ่งโฟลว์

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

การติดตั้งใช้งานขั้นตอน

คุณสามารถทำให้โฟลว์ใช้งานได้โดยตรงเป็นปลายทาง 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 with Cloud Run