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

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, 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 đầ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 đầu ra của mô hình trước khi trình bày 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ã Go 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 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 bất kỳ nền tảng nào có thể lưu trữ ứng dụng web.

Cá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 thủ 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 Go 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 luồng

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

Chỉ cần gói các lệnh gọi genkit.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 đồ, tương tự 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à ví dụ tinh chỉnh về ví dụ cuối cùng, trong đó xác định một luồng lấy một chuỗi làm đầu vào và xuất 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),
        )
    })

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 genkit.Generate() trong flow (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à một biến thể của ví dụ truyền giản đồ đến genkit.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ề.

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

Luồng gọi

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

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

Đối số cho flow phải tuân theo giản đồ đầu vào.

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 MenuItem, thì đầu ra của flow sẽ chứa các thuộc tính của giản đồ đó:

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ợ phát trực tuyến bằng giao diện tương tự như giao diện phát trực tuyến của genkit.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 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 họ đến người dùng khi các phản hồi đó được tạo.

Dưới đây là ví dụ về một luồng hỗ trợ phát 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ị mà luồng của bạn truyền. Loại này không nhất thiết phải giống với loại dữ liệu trả về, đây là loại dữ liệu đầu ra hoàn chỉnh của flow (Menu trong ví dụ này).

Trong ví dụ này, các giá trị do luồng 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 genkit.Generate() truyền trực tuyến bên trong luồng. Mặc dù thường thì đây 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

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

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 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 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.

Giao diện người dùng dành cho nhà phát triển dựa vào việc ứng dụng Go tiếp tục chạy, ngay cả khi logic đã hoàn tất. Nếu bạn mới bắt đầu và Genkit không phải là một phần của ứ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 tắt, nhờ đó bạn có thể kiểm tra ứng dụng đó trong giao diện người dùng.

Để khởi động 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:

genkit start -- go run .

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:

Ảnh chụp màn hình của trình chạy Flow

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 thẻ Kiểm tra.

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.

Máy chủ net/http

Để triển khai một flow bằng bất kỳ nền tảng lưu trữ Go nào, chẳng hạn như Cloud Run, hãy xác định flow 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, giúp khởi động máy chủ và quản lý vòng đời của máy chủ, bao gồm cả việc ghi lại các tín hiệu ngắt để dễ dàng phát triển cục bộ, nhưng 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 luồng đượ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 một điểm cuối của flow 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. 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 phần Genkit với Cloud Run.