앱의 AI 기능의 핵심은 생성형 모델 요청이지만, 단순히 사용자 입력을 받아 모델에 전달하고 모델 출력을 사용자에게 다시 표시할 수 있는 경우는 드뭅니다. 일반적으로 모델 호출과 함께 전처리 및 후처리 단계가 있습니다. 예를 들면 다음과 같습니다.
- 모델 호출과 함께 전송할 컨텍스트 정보를 검색합니다.
- 사용자의 현재 세션 기록을 가져옵니다(예: 채팅 앱).
- 한 모델을 사용하여 다른 모델에 전달하는 데 적합한 방식으로 사용자 입력의 형식을 변경합니다.
- 사용자에게 모델의 출력을 표시하기 전에 모델 출력의 '안전성'을 평가합니다.
- 여러 모델의 출력을 결합합니다.
AI 관련 작업이 성공하려면 이 워크플로의 모든 단계가 함께 작동해야 합니다.
Genkit에서는 플로우라는 구성을 사용하여 이러한 긴밀하게 연결된 논리를 나타냅니다. 플로우는 일반 Go 코드를 사용하여 함수와 마찬가지로 작성되지만 AI 기능의 개발을 용이하게 하기 위한 추가 기능이 추가됩니다.
- 유형 안전성: 정적 및 런타임 유형 검사를 모두 제공하는 입력 및 출력 스키마입니다.
- 개발자 UI와 통합: 개발자 UI를 사용하여 애플리케이션 코드와는 별개로 플로우를 디버그합니다. 개발자 UI에서 플로우를 실행하고 플로우의 각 단계에 대한 trace를 볼 수 있습니다.
- 간소화된 배포: 웹 앱을 호스팅할 수 있는 플랫폼을 사용하여 플로우를 웹 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의 여러 기능에 대한 요구사항입니다(이후 섹션에서 이러한 주제를 다룹니다).
입력 및 출력 스키마
모델 API를 직접 호출하는 것에 비교하여 Genkit 플로우 갖는 가장 중요한 이점 중 하나는 입력 및 출력 모두의 유형 안전성입니다. 플로우를 정의할 때는 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.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
})
StreamCallback[string]
의 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"'
스트리밍 플로우의 경우 -s
플래그를 추가하여 스트리밍 출력을 콘솔에 표시할 수 있습니다.
genkit flow:run menuSuggestionFlow '"French"' -s
명령줄에서 플로우를 실행하면 플로우를 테스트하거나 임시로 필요한 작업을 실행하는 플로우를 실행하는 데 유용합니다(예: 문서를 벡터 데이터베이스로 수집하는 플로우 실행).
디버깅 흐름
플로우 내에 AI 로직을 캡슐화하는 한 가지 이점은 Genkit 개발자 UI를 사용하여 앱과 별개로 플로우를 테스트하고 디버그할 수 있다는 것입니다.
개발자 UI를 시작하려면 프로젝트 디렉터리에서 다음 명령어를 실행합니다.
genkit start -- go run .
개발자 UI의 실행 탭에서 프로젝트에 정의된 플로우를 실행할 수 있습니다.
플로우를 실행한 후 트레이스 보기를 클릭하거나 검사 탭을 확인하여 플로우 호출의 트레이스를 검사할 수 있습니다.
흐름 배포
플로우를 웹 API 엔드포인트로 직접 배포하여 앱 클라이언트에서 호출할 수 있습니다. 배포는 다른 여러 페이지에서 자세히 설명되어 있지만 이 섹션에서는 배포 옵션에 대한 간략한 개요를 제공합니다.
net/http
서버
Cloud Run과 같은 Go 호스팅 플랫폼을 사용하여 플로우를 배포하려면 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"))
특정 플랫폼에 배포하는 방법에 관한 자세한 내용은 Cloud Run을 사용한 Genkit를 참고하세요.