나침반 솔루션

생성형 AI를 사용해 에이전트 기반 여행 계획 앱 빌드하기

다음 다이어그램은 동급 최고의 Google 개발자 서비스를 결합하여 AI 기반 애플리케이션을 빌드하는 방법을 개략적으로 보여줍니다.

모바일 및 웹을 위한 아름다운 AI 기반 앱 빌드

Flutter와 Firebase Genkit를 사용하면 AI와 원활하게 통합되는 멀티 플랫폼 앱을 빌드할 수 있습니다.
Genkit를 사용하면 LLM의 출력을 검증할 수 있는 스키마를 정의하여 앱이 LLM의 데이터를 자신 있게 사용할 수 있도록 지원할 수 있습니다. Flutter에서는 Dart를 사용해 요청을 직렬화하고 표준 HTTP 요청을 사용해 Genkit 스키마와 일치하도록 응답을 역직렬화할 수 있습니다.
IDX를 사용하면 소프트웨어를 설치하지 않고도 Flutter 앱을 만들 수 있습니다. 이렇게 하면 브라우저에서 Android 앱과 웹 앱을 개발하고 테스트할 수 있습니다.
Firebase Data Connect는 Cloud SQL에 기반한 완전 관리형 PostgreSQL 데이터베이스를 사용하여 빌드하고 확장할 수 있는 모바일 및 웹 앱용 관계형 데이터베이스 서비스입니다. Firebase 인증과 원활하게 통합되는 GraphQL을 사용하여 안전한 스키마, 쿼리, 변형 관리를 제공합니다. Data Connect에는 Kotlin Android 및 웹용 SDK 지원이 포함되어 있습니다.

Firebase Genkit로 에이전트 빌드

에이전트는 AI 조정 프레임워크를 사용하여 사용자 입력을 수신하고 응답을 생성합니다.
---
model: googleai/gemini-1.5-flash-latest
config:
  temperature: 1.0
  safetySettings:
    - category: HARM_CATEGORY_HATE_SPEECH
      threshold: BLOCK_LOW_AND_ABOVE
    - category: HARM_CATEGORY_DANGEROUS_CONTENT
      threshold: BLOCK_ONLY_HIGH
    - category: HARM_CATEGORY_HARASSMENT
      threshold: BLOCK_LOW_AND_ABOVE
    - category: HARM_CATEGORY_SEXUALLY_EXPLICIT
      threshold: BLOCK_LOW_AND_ABOVE
input:
  schema:
    request: string, The users request for where they want to travel to.
    place: string, The place that closely represents the users request.
    placeDescription: string, A description of that place.
    activities(array, a stringify list of activities that can be found at the specified place): string
    restaurants?(array, a stringify list of all the restaurants found at that location): string
output:
  schema:
    place: string, The place the user is traveling to.
    itineraryName: string, a catchy itinerary name that encapsulates the spirit of the trip and includes the place name
    startDate: string, the start date of the trip
    endDate: string, the end date of the trip
    tags(array, relevant tags for the trip): string
    itinerary(array):
      day: number
      date: string
      planForDay(array):
        activityRef: string, the reference value for the activity - this comes from the available activities JSON. If no value is present use a ref value of restaurant.
        activityTitle: string, a catchy title for the activity
        activityDesc: string, a six word description of the activity
        photoUri?: string, set the photo uri value for restaurants only.
        googleMapsUri?: string, if this is a restaurant include the googleMapsUri
---

Generate an itinerary for a tourist planning on traveling to the location specified based in their request.
If there is something that does not exist within the list of activities, do not include it in your answer.
Feel free to relate the activitiy to the request in a meaningful way.
In the plan for day array, put activities as a travel brouchure might do.
Come up with a catchy name for the itinerary.

Pick three activities per day, minimum of three day trip unless otherwise specified in the request.

Output schema should not include the properties type or object.

Pick a date after 2024-05-14 but before 2024-12-31.

The output date must be in the format year-month-day.

Give each activity a unique title and description.

Limit activity descriptions to 6 words.

If no restaurants are supplied, do not recommend any restaurants to eat at.

{{#if restaurants}}
Find a restaurant to eat at each day.

Include a restaurant to visit in the itinerary for each day from the available restaurants.
The restaurant should be the only activity with a photoUri.
The photoUri for the restaurant should be from the photoUri property from the restaurant array.
If there are no restaurants to pick from, do not include it in the list.

The photoUri from the restaurantFinder should be in the format of places/${placeId}/photos/${photoId}

Each restaurant should be unique to the overall itinerary.
Each restaurant must contain a photoUri in their output JSON schema.
Each restaurant must also include  an activitiyRef, activityTitle, and activityDesc in their output
{{/if}}
Output must be in JSON format.

REQUEST : {{request}}
PLACE : {{place}}
PLACE DESCRIPTION : {{placeDescription}}
AVAILABLE ACTIVITIES : {{activities}}
RESTAURANTS : {{restaurants}}
생성형 AI를 사용할 때는 모델이 고품질 응답을 반환하도록 효과적인 프롬프트를 만드는 것이 중요합니다. Firebase Genkit는 생성형 AI 프롬프트를 작성하고 구성하는 데 도움이 되는 Dotprompt 플러그인과 텍스트 형식을 제공합니다. 이 형식은 프롬프트, 입력 및 출력 스키마, 모델, 구성을 모두 단일 파일에 캡슐화합니다.

다음 코드 예는 여행 앱에 사용된 Dotprompt 파일을 보여줍니다. 스키마는 사용자가 꿈의 여행을 설명할 때 제공하는 정보를 기반으로 합니다.
Dotprompt는 프롬프트가 코드라는 전제를 중심으로 설계되었습니다. dotprompt 파일이라는 특수 형식의 파일에서 프롬프트를 작성 및 유지하고 코드에 사용하는 것과 동일한 버전 제어 시스템을 사용하여 변경사항을 추적하며 생성형 AI 모델을 호출하는 코드와 함께 배포합니다.
흐름은 강력한 유형으로 지정되고 스트리밍 가능하며 로컬 및 원격으로 호출할 수 있고 완전히 관찰 가능한 함수입니다. Firebase Genkit는 흐름 실행 또는 디버깅과 같은 흐름 작업에 필요한 명령줄 인터페이스와 개발자 UI를 제공합니다.
import {defineTool} from '@genkit-ai/ai/tool';
...
{
  name: 'restaurantFinder',
  description: `Used when needing to find a restaurant based on a users location.
  The location should be used to find nearby restaurants to a place. You can also
  selectively find restaurants based on the users preferences, but you should default
  to 'Local' if there are no indications of restaurant types in the users request.
  `,
  inputSchema: z.object({
    place: z.string(),
    typeOfRestaurant: z.string().optional() }),
    outputSchema: z.unknown(),
},
...
async (input) => {
  if (input.typeOfRestaurant == undefined) {
    input.typeOfRestaurant = "Local";
  }
  const geocodeEndpoint = "https://places.googleapis.com/v1/places:searchText";
  const textQuery = {textQuery: `${input.typeOfRestaurant} restaurants in ${input.place}`};

  const  response = await axios.post(
    geocodeEndpoint,
    JSON.stringify(textQuery),
    {
      headers: {
        "Content-Type": "application/json",
        "X-Goog-Api-Key": MAPS_API_KEY,
        "X-Goog-FieldMask": "places.displayName,places.formattedAddress,places.priceLevel,places.photos.name,places.editorialSummary,places.googleMapsUri"
      }
    }
  );
  console.log(response.data);
  let data = (response.data as PlaceResponse);
  for(let i = 0; i < data.places.length; i++) {
    if (data.places[i].photos) {
      data.places[i].photos = [data.places[i].photos[0]];
    }
  }
  return data as PlaceResponse;
}
Genkit의 함수 호출을 사용하여 에이전트의 기능을 확장하면 에이전트가 응답을 미세 조정하고 추가 작업을 완료할 수 있습니다. 여행 앱은 사용자가 원하는 경로에 따라 Places API에서 레스토랑 정보를 반환할 수 있는 도구를 정의합니다. 이 코드는 쿼리 결과를 검증할 수 있도록 Zod를 사용하여 입력 및 출력 스키마를 정의합니다.
...
export const textRefinement = defineFlow(
{
  name: 'textRefinement',
  inputSchema: z.string(),
  outputSchema: z.unknown(),
},
async (userRequest) => {
  const refinementPrompt = await prompt('textRefinement')
  const result = await refinementPrompt.generate({
      input: {
          request: userRequest
      },
  });
  return result.output();
});
사용자에게 보다 맞춤화된 검색 환경을 제공하기 위해 사용자가 꿈의 여행을 설명하면 Gemini는 여행 앱에서 제공하는 프롬프트를 기반으로 추가 정보가 필요한지 판단하고 추가 정보가 필요하다고 생각하는 경우 앱에 신호를 보냅니다. 그런 다음 앱은 사용자에게 해당 정보를 입력하라는 메시지를 표시하고 백엔드의 요청에 이 정보를 추가합니다.
import 'package:http:http.dart' as http;
...
Future<List<Trip>> generateTrips(String description, List<Image> images) async {
  final uri = Uri.parse('.../generateTrips');
  final request = http.MultipartRequest('POST', uri);
  request.fields['description'] = description;
  request.files.add(http.MultipartFile.fromData(
      images.name, images.bytes,
      contentType: MediaType('image', 'png'),
  ));
  var response = await request.send();
  if (response.statusCode == 200) {
      final body = await response.body.text();
      final items = jsonDecode(body) as List<dynamic>;
      return items.map(Trip.fromJson).toList();
  }
  ...
  import { imagen2, geminiProVision } from '@genkit-ai/vertexai';
  import { generate } from '@genkit-ai/ai';

  const imageResult = await generate({
    model: imagen2,
    prompt: 'Generate an image of a very specific historical time and place.',
  });
  const generatedImage = imageResult.media();

  const descriptionResult = await generate({
    model: geminiProVision,
    prompt: [
      {
        text: 'What is the historical time and place represented in this picture?',
      },
      { media: generatedImage },
    ],
  });
  console.log(descriptionResult.text());
  }
여행 앱이 사용자에게 텍스트 입력을 사용하거나 마이크 버튼을 탭하여 음성 텍스트 변환을 활성화하는 방식으로 꿈의 여행을 정의하도록 요청합니다. 사용자가 선택적으로 이미지를 업로드할 수도 있습니다.

이 앱은 pub.dev의 Dart 패키지를 활용하여 각 플랫폼의 기본 음성 텍스트 변환 기능과 통합하고, Firebase Genkit의 Gemini API를 사용하여 이미지나 동영상과 같은 멀티모달 입력을 처리합니다. Gemini API는 검색 증강 생성 (RAG)을 사용하여 Firebase Data Connect와 임베딩을 통해 최근접 이웃 검색을 수행하는 임베딩을 사용하여 추천 이동을 반환합니다.

프로덕션을 위해 앱 확장

Firebase 호스팅은 Flutter를 비롯하여 널리 사용되는 최신 웹 프레임워크와 통합됩니다. 이러한 프레임워크와 함께 Firebase 호스팅 및 Firebase용 Cloud Functions를 사용하면 원하는 프레임워크 환경에서 앱과 마이크로서비스를 개발한 다음 안전한 관리형 서버 환경에 배포할 수 있습니다. 프로덕션으로 이동하기 전에 앱에 포함된 모든 서비스의 보안과 성능을 이해해야 합니다. 자세한 내용은 Firebase 출시 체크리스트를 참조하세요.
여행 앱은 Google AI를 사용하여 테스트 데이터를 빠르게 반복하며, 확장할 필요 없이 AI 사용 사례가 최소화된 앱에 적합합니다. Vertex AI에는 프로덕션 애플리케이션 확장을 위해 더 많은 할당량이 제공되며, 사용자 데이터 보호를 위해 더 강력한 개인정보처리방침이 제공됩니다. Genkit에는 쉽게 모델을 전환하는 기능이 내장되어 있으므로 프롬프트나 API 호출을 다시 작성할 필요가 없습니다.