Xác định quy trình công việc AI

Phần cốt lõi trong các tính năng AI của ứng dụng là các yêu cầu dựa trên mô hình tạo sinh, nhưng hiếm khi bạn có thể chỉ cần lấy dữ liệu đầu vào của người dùng, truyền dữ liệu đó đến mô hình và hiện lại kết quả của mô hình cho người dùng. Thông thường, lệnh gọi mô hình cần có các bước trước và sau xử lý. 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ý phiên hiện tại của người dùng, chẳng hạn như trong ứng dụng trò chuyện.
  • Sử dụng một mô hình để định dạng lại dữ liệu hoạt động đầ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á "độ an toàn" của đầu ra của mô hình trước khi hiển thị kết quả đó cho người dùng.
  • Kết hợp kết quả của một số mô hình.

Mọi bước trong quy trình này phải phối hợp với nhau để có thể thành công trong mọi tác vụ liên quan đến AI.

Trong Genkit, bạn biểu thị 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 gọi là flow. Flow được viết tương tự như các hàm, sử dụng mã Go thông thường, nhưng flow bổ sung thêm các tính năng nhằm hỗ trợ quá trình phát triển các tính năng AI:

  • An toàn về kiểu: Giản đồ đầu vào và đầu ra, giúp kiểm tra cả kiểu tĩnh và kiểu thời gian chạy.
  • Tích hợp với giao diện người dùng của nhà phát triển: Gỡ lỗi quy trình một cách độc lập với mã xử lý ứng dụng bằng cách sử dụng giao diện người dùng của 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 flow và xem dấu vết cho từng bước của flow.
  • Triển khai đơn giản: Triển khai các luồng trực tiếp ở dạng điểm cuối API web, sử dụng bất kỳ nền tảng nào có thể lưu trữ ứng dụng web.

Flow của Genkit đơn giản và không phô trương, cũng như không buộc ứng dụng của bạn phải tuân thủ bất kỳ mô hình trừu tượng cụ thể nào. Toàn bộ logic của flow được viết bằng Go tiêu 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ỉ bao bọc một hàm. Ví dụ sau đây gói một hàm gọi 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
    })

Chỉ bằng cách gói các lệnh gọi genkit.Generate() như thế này, bạn sẽ thêm một số chức năng: Tính năng này cho phép bạn chạy flow từ Genkit CLI và từ giao diện người dùng của nhà phát triển. Đây là yêu cầu bắt buộc đối với một số tính năng của Genkit, bao gồm cả việc triển khai và quan sát (các phần sau thảo luận về các chủ đề này).

Giản đồ đầu vào và đầu ra

Một trong những ưu điểm quan trọng nhất mà luồng Genkit có so với việc gọi trực tiếp API mô hình là an toàn về kiểu cho cả đầu vào và đầu ra. Khi xác định luồng, bạn có thể xác định giản đồ đầu vào giống như cách xác định giản đồ đầu ra của lệnh gọi genkit.Generate(). Tuy nhiên, không giống như genkit.Generate(), bạn cũng có thể chỉ định giản đồ đầu vào.

Dưới đây là phần tinh chỉnh của ví dụ cuối cùng, trong đó xác định một luồng lấy một chuỗi làm dữ liệu đầu vào và xuất ra một đối tượng:

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

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 genkit.Generate() trong flow (trên thực tế, một flow thậm chí có thể không chứa lệnh gọi genkit.Generate()). Dưới đây là biến thể của ví dụ gọi genkit.GenerateData(), nhưng sử dụng đầu ra có cấu trúc để định dạng một chuỗi đơn giản mà flow sẽ trả về. Hãy lưu ý cách chúng ta truyền MenuItem dưới dạng tham số loại; điều này tương đương với việc truyền tuỳ chọn WithOutputType() và nhận giá trị của loại đó trong phản hồi.

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

Quy trình gọi

Sau khi xác định một luồng, bạn có thể gọi luồng đó từ mã Go:

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

Đối số cho luồng phải phù hợp với giản đồ đầu vào.

Nếu bạn đã xác định một giản đồ đầu ra, thì phản hồi luồng sẽ tuân theo giản đồ đó. Ví dụ: nếu bạn đặt giản đồ đầu ra thành MenuItem, đầu ra luồng sẽ chứa các thuộc tính:

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

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

Luồng phát trực tuyến

Flow hỗ trợ truyền trực tuyến bằng cách sử dụng giao diện tương tự như giao diện truyền trực tuyến của genkit.Generate(). Tính năng truyền trực tuyến rất hữu ích khi flow của bạn tạo ra một lượng lớn dữ liệu đầu ra, vì bạn có thể trình bày kết quả đó cho người dùng ngay khi dữ liệu được tạo. Việc này giúp cải thiện khả năng phản hồi cảm nhận của ứng dụng. Một ví dụ quen thuộc là các giao diện LLM dựa trên hoạt động trò chuyện thường truyền trực tuyến phản hồi của chúng cho người dùng khi chúng được tạo.

Dưới đây là ví dụ về một quy trình có hỗ trợ tính năng truyền trực tuyến:

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

Loại string trong StreamCallback[string] chỉ định loại giá trị luồng của bạn. Kiểu dữ liệu này không nhất thiết phải cùng loại với kiểu dữ liệu trả về, là kiểu dữ liệu đầu ra đầy đủ của luồng (trong ví dụ này là Menu).

Trong ví dụ này, các giá trị do flow truyền trực tuyến được kết hợp trực tiếp với các giá trị do lệnh gọi genkit.Generate() truyền bên trong flow. Mặc dù điều này thường xảy ra, nhưng không nhất thiết phải như vậy: bạn có thể xuất các giá trị sang luồng bằng cách sử dụng lệnh gọi lại thường xuyên hữu ích cho flow của bạn.

Quy trình phát trực tuyến đang gọi

Các luồng truyền trực tuyến có thể được chạy như các luồng không phát trực tuyến bằng menuSuggestionFlow.Run(ctx, "bistro") hoặc có thể được truyền trực tuyến:

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

Chạy flow từ dòng lệnh

Bạn có thể chạy các luồng 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 ra bảng điều khiển bằng cách thêm cờ -s:

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

Việc chạy luồng từ dòng lệnh rất hữu ích khi kiểm thử luồng hoặc chạy các luồng thực hiện các tác vụ cần thiết một cách đặc biệt, ví dụ: chạy luồng nhập tài liệu vào cơ sở dữ liệu vectơ của bạn.

Quy trình gỡ lỗi

Một trong những ưu điểm 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 của mình bằng cách sử dụng giao diện người dùng dành cho nhà phát triển Genkit.

Giao diện người dùng của nhà phát triển sẽ dựa vào ứng dụng Go để tiếp tục chạy, ngay cả khi logic đã hoàn tất. Nếu bạn chỉ mới bắt đầu và Genkit không thuộc một ứng dụng rộng hơn, hãy thêm select {} làm dòng cuối cùng của main() để ngăn ứng dụng này dừng hoạt động để bạn có thể kiểm tra trong giao diện người dùng.

Để bắt đầu giao diện người dùng dành cho nhà phát triển, hãy chạy lệnh sau từ thư mục dự án của bạn:

genkit start -- go run .

Từ thẻ Run (Chạy) trong 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:

Ảnh chụp màn hình Trình chạy luồng

Sau khi chạy một luồng, bạn có thể kiểm tra dấu vết của lệnh gọi luồng bằng cách nhấp vào View trace (Xem dấu vết) hoặc xem thẻ Inspect (Kiểm tra).

Đang triển khai luồng

Bạn có thể triển khai flow trực tiếp dưới dạng điểm cuối API web, sẵn sàng để gọi từ ứng dụng khách. Triển khai sẽ đượ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 của bạn.

Máy chủ net/http

Để triển khai flow bằng bất kỳ nền tảng lưu trữ Go, chẳng hạn như Cloud Run, hãy xác định flow của bạn bằng DefineFlow() và khởi động máy chủ net/http bằng trình xử lý flow được cung cấp:

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() là một hàm trợ giúp không bắt buộc để khởi động máy chủ và quản lý vòng đời của máy chủ, bao gồm cả việc thu thập các tín hiệu gián đoạn để dễ dàng phát triển cục bộ. Tuy nhiên, bạn có thể sử dụng phương thức của riêng mình.

Để phân phát tất cả các flow được xác định trong cơ sở mã, bạn có thể sử dụng 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))

Bạn có thể gọi điểm cuối 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"}'

Các khung máy chủ khác

Bạn cũng có thể sử dụng các khung máy chủ khác để triển khai luồng của mình. Ví dụ: bạn có thể sử dụng Gin chỉ với một vài dòng:

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"))

Để biết thông tin về cách triển khai cho các nền tảng cụ thể, hãy xem nội dung Genkit với Cloud Run.