[AI] Flutter 에서 langchain 사용하기

도톨이·2024년 6월 11일

AI

목록 보기
8/14
post-thumbnail

다음의 블로그를 참고하였다.
https://blog.langchaindart.dev/introducing-langchain-dart-6b1d34fc41ef

다트에서 랭체인 프레임워크를 사용하려면 다음의 패키지를 사용해야한다.
아래 사이트에 들어가면 설치 방법이 나와있다.
나는 flutter pub add langchain 을 입력하 설치하였다.

https://pub.dev/packages/langchain

어떻게 사용해야할 지 아예 모르겠으니 도큐멘테이션을 보자.
https://langchaindart.dev/#/

openai 를 사용하려면 API Key 가 필요하기 때문에
ChatOpenAI 생성자에 Api key 를 파라미터로 넣어서 객체를 생성해야한다. llm 객체는 ChatOpenAI(apiKey: {OPEN API KEY})를 통해 생성하나보다.

import 'package:langchain/langchain.dart';
import 'package:langchain_openai/langchain_openai.dart';

final llm = ChatOpenAI(apiKey: openaiApiKey);

LangChain은 언어 모델 애플리케이션을 구축하기 위해 다양한 모듈을 제공하고있다!
이러한 모듈은 독립적으로 사용할 수도 조합해서 사용할 수도 있다.
이런 조합은 LangChain Expression Language (LCEL)에 의해 가능해지며, 많은 모듈이 통일된 Runnable 인터페이스를 구현하여 구성 요소를 연결할 수 있다고 한다.

가장 간단하고 일반적인 체인은 다음 세 가지 요소로 구분된다.

  • LLM/채팅:
    언어 모델은 핵심적인 추론 엔진이다. LangChain을 사용하려면 다양한 유형의 언어 모델과 이를 사용하는 방법을 이해해야 한다. 이는 텍스트 생성, 질문 답변, 번역 등 다양한 작업을 수행한다.

  • 프롬프트 템플릿:
    이는 언어 모델에 지시를 제공하는 것이다. 언어 모델이 무엇을 출력할지를 제어하므로, 프롬프트를 구성하는 방법과 다양한 프롬프트 전략을 이해해야한다. 프롬프트 템플릿을 잘 구성하면 언어 모델이 원하는 대로 작동하도록 유도할 수 있다.

  • 출력 파서:
    언어 모델의 원시 응답을 더 사용하기 쉬운 형식으로 번역하는 것으로. 예를 들어, JSON 형식으로 변환하거나 특정 키워드를 추출하도록 하는 것이다.

  • 구성 요소 결합하기:
    이 세 가지 구성 요소를 결합하여 LangChain 애플리케이션을 구축할 수 있.

LLM/채팅 모델

언어 모델에는 두 가지 유형이 있다.

  1. LLM: 기본 모델은 문자열을 입력으로 받아 문자열을 반환.
  2. ChatModel: 메시지 목록을 입력으로 받아 메시지를 반환합니다.

문자열은 다 알고있는 문자열이지만 메시지는 무엇일까?
메시지의 기본 인터페이스는 ChatMessage로 정의되며, 두 가지 필수 속성을 가지고 있다:

  1. content: 메시지의 내용. 일반적으로 문자열입니다.
  2. role: 메시지가 오는 주체를 나타냅니다.

랭체인에서는 이 role 의 구분을 쉽게 해준다!

LangChain이 제공하는 객체들

  • HumanChatMessage: 인간/사용자로부터 오는 메시지
  • AIChatMessage: AI/어시스턴트로부터 오는 메시지
  • SystemChatMessage: 시스템으로부터 오는 메시지
  • FunctionChatMessage / ToolChatMessage: 함수 또는 도구 호출의 결과를 포함하는 메시지
  • CustomChatMessage: 직접 역할을 지정할 수 있는 사용자 정의 메시지 클래스

LLM과 채팅 모델 모두 공통 인터페이스를 제공하지만, 효과적으로 프롬프트를 구성하려면 차이를 이해하는 것이 좋다.

LLM 또는 채팅 모델을 가장 간단하게 호출하려면 .invoke() 메서드를 사용한다.

  • LLM.invoke: 문자열을 입력으로 받아 문자열을 반환
  • ChatModel.invoke: 메시지 목록을 입력으로 받아 메시지를 반환

코드로 확인하면 위에게 llm 아래게 ChatModel 이다. 생성 방식에 차이가 있는 것을 확인할 수 있다. llm 은 OenAI 생성자로 객체를 만들고, chatmodel은 ChtOpenAI 생성자로 객체를 만든다. 이때 공통점은 둘 다 apikey를 집어넣는 것이다.

final llm = OpenAI(apiKey: openaiApiKey);
final chatModel = ChatOpenAI(apiKey: openaiApiKey);

둘 다 invoke 메소드가 있으므로 코드에서 사용하려면 이렇게 사용할 수 있다.
아래 예시를 보자. 차이를 알겠는가?
우선 string 문자열로 텍스트를 입력한다. 그 후, LLM 과 ChatModel 둘 다 invoke 할 때는 PromptValue를 입력으로 받는다. PromptValue는 문자열 또는 메시지로 입력을 반환하는 자체 로직을 정의하는 객체이다.
llm 은 invoke() 시 PromptValue 로 문자열을 전달하고, (PromptValue.string(text)) / ChatModel 은 invoke() 시 PromptValue 로 메시지를 전달한다 (PromptValue.chat(messages))
이때 llm 은 텍스트 문자열을 그대로 넣으면 되고, ChatModel은 ChatMessage.humanText(text) 로 객체를 생성한 뒤 리스트에 넣어서 전달해야한다.
출력 또한 llm 은 그냥 문자열이 나오는데, chatmodel 은 AIChatMessage 객체가 나온다.

const text = "컬러풀한 양말을 하는 회사 이름을 추천해줄래?";
final messages = [ChatMessage.humanText(text)];

final res1 = await llm.invoke(PromptValue.string(text));
print(res1.output);
// 'Feetful of Fun'

final res2 = await chatModel.invoke(PromptValue.chat(messages));
print(res2.output);
// AIChatMessage(content='RainbowSock Co.')

프롬프트 템플릿 (Prompt Templates)

다음 요소는 프롬프트 템플릿이다.

대부분의 LLM 애플리케이션은 사용자 입력을 직접 언어 모델에 전달하지 않고,사용자 입력을 더 큰 텍스트 조각인 프롬프트 템플릿에 추가하여 특정 작업에 대한 추가 컨텍스트를 제공한다.

이전 예제에서 모델에 전달한 텍스트에 회사 이름을 생성하기 위한 문장('컬러풀한 양말을 하는 회사 이름을 추천해줄래?')이 있었다. 사실 저렇게 문장을 전부 넣지 않고, 회사나 제품 설명만 넣으면 더 간편할 것이다.

PromptTemplates는 바로 이 점을 도와준다.
템플릿을 사용하면, 사용자 입력을 완전히 포맷된 프롬프트로 변환하는 로직을 묶어줄 수 있다. 아래 예시로 살펴보자.

{ } 로 변수를 넣어서 프롬프트 템플릿을 만든 후 format 을 사용하여 해당 변수에 값을 넣어주면 res 값처럼 프롬프트가 나온다.

final prompt = PromptTemplate.fromTemplate(
  '{product} 를 만드는 회사의 이름으로 무엇이 좋을까요?'
);
final res = prompt.format({'product': '컬러풀한 양말'});
print(res);
// '컬러풀한 양말을 만드는 회사 이름은 무엇이 좋을까요?'

프롬프트 템플릿을 사용하면 다음의 장점을 가진다.

  • 변수의 일부만 포맷할 수 있다. 즉, 일부 변수만 채우고 나머지는 나중에 채울 수 있다.
  • 조합 가능: 다양한 템플릿을 쉽게 조합하여 하나의 프롬프트로 만들 수 있다.

Chat Prompt Templates
프롬프트 템플릿은 메시지 리스트를 생성하는 데도 사용할 수 있다. 이 경우 프롬프트는 콘텐츠에 대한 정보뿐만 아니라 각 메시지(역할, 리스트 내 위치 등)에 대한 정보도 포함한다.

가장 일반적으로 사용되는 것은 ChatPromptTemplate로, 이는 ChatMessagePromptTemplates의 리스트 입니다. 각 ChatMessagePromptTemplate은 해당 ChatMessage를 포맷하는 방법에 대한 지침을 포함한다. 여기에는 메시지의 role과 content가 포함된다.

예제로 살펴보자.

ChatPromptTemplate 은 ChatMessagePromptTemplate의 리스트인데, ChatMessagePromptTemplate 의 템플릿은 fromTemplate 으로 지정할 수 있다. 그 내부를 보면 role, content 가 있다.
예시에는 ChatPromptTemplate 에 대해 두 개의 ChatMessagePromptTemplate 가 있는데 하나는 "system"에게 You are an assistant that helps create company names. 의 역할을 정해주는 거고 다음은 'user'가 What is a good name for a company that makes {product}? 게 메시지를 보낸다는 뜻이다.

이를 사용하려면 format 으로 변수 product 에 원하는 값을 넣어주면 되는데 이것은 회사 이름 지어주는 걸 돕는 챗봇에게 What is a good name for a company that makes colorful socks? 라는 메시지를 보내는 것과 같다.

final chatPrompt = ChatPromptTemplate([
  ChatMessagePromptTemplate.fromTemplate(
    role: 'system',
    content: 'You are an assistant that helps create company names.',
  ),
  ChatMessagePromptTemplate.fromTemplate(
    role: 'user',
    content: 'What is a good name for a company that makes {product}?',
  ),
]);

final chatMessages = chatPrompt.format({'product': 'colorful socks'});
print(chatMessages);
// [
//   SystemChatMessage(content='You are an assistant that helps create company names.'),
//   HumanChatMessage(content='What is a good name for a company that makes colorful socks?'),
// ]

이렇게 사용할 수도 있다.

const template = 'You are a helpful assistant that translates {input_language} to {output_language}.';
const humanTemplate = '{text}';

final chatPrompt = ChatPromptTemplate.fromTemplates([
  (ChatMessageType.system, template),
  (ChatMessageType.human, humanTemplate),
]);

final res = chatPrompt.formatMessages({
  'input_language': 'English',
  'output_language': 'French',
  'text': 'I love programming.',
});
print(res);
// [
//   SystemChatMessage(content='You are a helpful assistant that translates English to French.'),
//   HumanChatMessage(content='I love programming.')
// ]

Output parsers

출력 파서는 LLM(언어 모델)의 원시 출력을 후속 처리에 사용할 수 있는 형식으로 변환한다.

  • LLM의 텍스트를 구조화된 정보로 변환 (예: JSON)
  • ChatMessage를 단순한 문자열로 변환
  • 메시지 외에 호출에서 반환된 추가 정보를 문자열로 변환 (예: OpenAI 함수 호출 결과)

다음은 쉼표로 구분된 문자열을 리스트로 변환하는 출력 파서를 작성하는 예제이다.

class CommaSeparatedListOutputParser {
  // 문자열을 받아 쉼표로 구분된 리스트로 변환하는 메서드
  List<String> parse(String text) {
    // 문자열을 쉼표 기준으로 나누고, 각 항목의 앞뒤 공백을 제거
    return text.split(',').map((item) => item.trim()).toList();
  }
}

void main() {
  final parser = CommaSeparatedListOutputParser();
  final input = '사과, 바나나, 체리';
  final output = parser.parse(input);
  print(output);
  // ['사과', '바나나', '체리']
}

CommaSeparatedListOutputParser 클래스를 통해 커스텀 출력 파서를 정의할 수 있다.

parse 메서드는 입력 문자열을 받아 쉼표로 구분된 리스트로 변환하는 메서드이다다.
text.split(',')는 문자열을 쉼표 기준으로 나누고,.
.map((item) => item.trim())는 각 항목의 앞뒤 공백을 제거한다.
.toList()는 최종적으로 리스트 형태로 반환한다.

LCEL을 사용한 조합

이제 배운 3개의 구성 요소를 하나의 체인으로 결합해보자.
이 체인은 입력 변수를 받아 프롬프트 템플릿에 전달하여 프롬프트를 생성하고, 이를 언어 모델에 전달한 후, 출력 파서를 통해 결과를 처리한다. 이렇게 하면 모듈화된 로직을 하나로 묶을 수 있다. 예제를 통해 살펴보자.

pipe 또는 | 구문을 사용하여 프롬프트 템플릿, 언어 모델, 출력 파서를 연결할 수 있다.

const systemTemplate = '''
당신은 쉼표로 구분된 목록을 생성하는 도움이 되는 어시스턴트입니다.
사용자가 카테고리를 입력하면, 해당 카테고리의 항목 5개를 쉼표로 구분된 목록으로 생성해주세요.
[오직] 쉼표로 구분된 목록만 반환해주세요.
''';

const humanTemplate = '{text}';

final chatPrompt = ChatPromptTemplate.fromTemplates([
  (ChatMessageType.system, systemTemplate),
  (ChatMessageType.human, humanTemplate),
]);

final chatModel = ChatOpenAI(apiKey: openAiApiKey);

final chain = chatPrompt.pipe(chatModel).pipe(CommaSeparatedListOutputParser());
// 대안 문법:
// final chain = chatPrompt | chatModel | CommaSeparatedListOutputParser();

final res = await chain.invoke({'text': '색깔'});
print(res); // ['빨강', '파랑', '초록', '노랑', '주황']
profile
Kotlin, Flutter, AI | Computer Science

0개의 댓글