LangChain
랭체인(LangChain)은 AI 모델과 다양한 데이터 소스, 도구들을 연결해 복잡한 애플리케이션을 쉽게 구축할 수 있도록 돕는 프레임워크이다. 원래 파이썬 기반으로 많이 알려져 있지만, 최근에는 타입스크립트 지원도 활발해져 프론트엔드 개발자나 Node.js 환경에서도 효율적으로 활용할 수 있다. 타입스크립트 랭체인은 강력한 타입 시스템과 친숙한 문법 덕분에 안정적이고 생산성 높은 AI 통합 개발을 가능하게 한다.
모델 생성
ChatOpenAI 인스턴스를 생성하는 것은 LangChain에서 OpenAI 기반 챗 모델을 사용하기 위한 첫 번째 단계이다. 이때 modelName, temperature, openAIApiKey 등의 옵션을 설정하여 사용할 모델의 종류, 응답의 창의성 정도, 인증에 사용할 API 키 등을 지정할 수 있다. 이렇게 생성한 인스턴스는 .invoke() 메서드를 통해 실제 사용자 메시지를 전달하고 응답을 받을 수 있으며, 프롬프트 템플릿, 출력 파서, 메모리와 같은 LangChain의 구성 요소들과 결합하여 복잡하고 유연한 대화 흐름을 구현하는 데 활용된다.
const model = new ChatOpenAI({
modelName: "gpt-4o-mini",
temperature: 1,
openAIApiKey: process.env.OPENAI_API_KEY
});
ChatOpenAI를 사용할 때는 modelName 옵션을 통해 사용할 OpenAI 모델을 지정할 수 있다. 예를 들어 "gpt-4o-mini"라는 값을 설정하면 해당 이름의 모델로 동작하게 되지만, 이는 실제 존재하지 않는 모델일 수 있으므로 주의해야 한다. OpenAI에서 공식적으로 지원하는 모델은 gpt-4o, gpt-4-turbo, gpt-3.5-turbo 등이 있으며, 정확한 모델명을 사용하지 않으면 에러가 발생하거나 예상과 다른 결과가 나올 수 있다. 따라서 항상 OpenAI의 공식 문서를 참고하여 지원되는 모델 이름을 확인한 후 사용하는 것이 바람직하다.
템플릿 생성
LangChain에서는 OpenAI와의 대화를 구성할 때 템플릿을 활용하여 시스템 메시지, 사용자 메시지, AI 응답 등을 유연하게 조합할 수 있다. SystemMessagePromptTemplate, HumanMessagePromptTemplate, AIMessagePromptTemplate 등의 템플릿을 사용할 수도 있지만, 보다 일반화된 ChatMessagePromptTemplate을 사용하여 메세지의 역할(role)을 직접 지정할 수도 있다. 이는 예를 들어 "function"이나 "tool" 같은 사용자 정의 역할을 갖는 메시지를 만들고 싶을 때 유용하게 사용된다. 즉, 역할이 고정된 템플릿들보다 더 유연하고 확장 가능한 방식으로 메시지를 구성할 수 있다. 이러한 템플릿은 ChatPromptTemplate 내부에서 메시지 배열로 조합되어 하나의 대화 흐름을 만든다.
const systemMessageForReviewer = ChatMessagePromptTemplate.fromTemplate(
`당신은 숙련된 소프트웨어 엔지니어이며 {role} 역할을 맡고 있습니다.
코드에 대한 피드백을 줄 때는 항상 명확하고 구체적으로 설명하세요.
문제점뿐 아니라 개선 방향도 함께 제시하고, {focusAreas} 등 다양한 측면에서 조언을 주세요.
{tone} 톤을 유지하세요.`,
'system'
);
프롬프트 생성
ChatPromptTemplate.fromMessages는 LangChain에서 여러 메시지 프롬프트 템플릿을 하나의 대화 흐름으로 결합하기 위해 설계된 메서드이다. 즉, 시스템 메시지, 사용자 메시지, AI 메시지 등 다양한 역할(role)을 가진 메시지 템플릿들을 배열로 받아 순서대로 조합하여, 실제 대화에 사용할 프롬프트를 생성한다. 이 방식은 복잡한 대화 시나리오를 유연하게 구성할 수 있게 하며, 각 메시지의 역할과 내용을 명확히 분리하여 관리할 수 있도록 돕는다.
ChatPromptTemplate.fromMessages는 각 메시지 템플릿에 변수 값을 주입하여 동적으로 메시지를 생성할 수 있으며, 대화의 맥락을 반영한 맞춤형 프롬프트를 만들 때 매우 유용하다. 이를 통해 AI가 특정 역할을 수행하거나, 이전 대화 기록을 포함한 상태로 응답을 생성할 수 있게 하며, LangChain 내에서 자연스럽고 일관된 대화형 워크플로우를 구현하는 데 핵심적인 역할을 한다.
const codeReviewPrompt = ChatPromptTemplate.fromMessages([
systemMessageForReviewer,
new MessagesPlaceholder("history"), // 대화 기록이 들어갈 자리 (키 이름은 중요)
HumanMessagePromptTemplate.fromTemplate("{input}"), //
]);
CustomChatMessageHistory
CustomChatMessageHistory 클래스는 LangChain에서 대화 이력을 관리하기 위한 맞춤형 메시지 히스토리 저장소 역할을 수행한다. 이 클래스는 LangChain의 BaseChatMessageHistory를 상속받아, 기본 메시지 히스토리 관리 기능을 확장하고, 실제 데이터 저장소 대신 간단한 인메모리 구조를 활용해 대화 세션별 메시지를 저장 및 조회할 수 있도록 구현되었다.
BaseChatMessageHistory는 기본적으로 LangChain에서 대화 메시지 히스토리를 추상화한 베이스 클래스다. 이를 상속받으면, 다양한 저장소(메모리, 데이터베이스, Redis 등)에 메시지를 저장하거나 불러오는 커스텀 로직을 구현할 수 있다. 이 베이스 클래스는 getMessages(), addMessage() 등 핵심 메서드를 정의하며, 이를 구체화하는 서브클래스가 실제 저장 방식과 데이터 구조를 결정한다.
// --- 간단한 인메모리 스토어 (실제로는 Redis/DB 클라이언트로 대체) ---
interface StoredMessageData {
type: string;
content: string | any[];
name?: string; // For ToolMessage, FunctionMessage
tool_calls?: any[]; // For AIMessage
tool_call_id?: string; // For ToolMessage, AIMessage (response to tool call)
id?: string; // BaseMessage의 id 필드
}
interface SimpleStore {
[sessionId: string]: StoredMessageData[]; // 이제 StoredMessageData 객체를 저장
}
const globalInMemoryStore: SimpleStore = {};
// --- CustomChatMessageHistory 클래스 정의 ---
class CustomChatMessageHistory extends BaseChatMessageHistory {
public lc_namespace = ["langchain", "stores", "message", "custom"];
private sessionId: string; // sessionId를 클래스 프로퍼티로 선언
constructor(sessionId: string) { // private 키워드 제거
super(); // BaseChatMessageHistory 생성자 호출
this.sessionId = sessionId; // 생성자 내부에서 할당
if (!globalInMemoryStore[this.sessionId]) {
globalInMemoryStore[this.sessionId] = [];
}
}
async getMessages(): Promise<BaseMessage[]> {
const storedMessagesData = globalInMemoryStore[this.sessionId] || [];
const messages: BaseMessage[] = storedMessagesData.map(data => {
switch (data.type) {
case "human":
return new HumanMessage(data);
case "ai":
return new AIMessage(data);
case "system":
return new SystemMessage(data);
default:
console.warn(`Unknown message type during deserialization: ${data.type}`);
// 기본적으로 HumanMessage로 처리하거나 오류를 발생시킬 수 있습니다.
return new HumanMessage({ content: `[Unknown Type: ${data.type}] ${data.content}`, id: data.id });
}
});
return Promise.resolve(messages);
}
async addMessage(message: BaseMessage): Promise<void> {
globalInMemoryStore[this.sessionId]!.push({...message, type: message.getType()});
return Promise.resolve();
}
async addSystemMessage(message: string): Promise<void> {
await this.addMessage(new SystemMessage(message));
}
async addUserMessage(message: string): Promise<void> {
await this.addMessage(new HumanMessage(message));
}
async addAIChatMessage(message: string): Promise<void> {
await this.addMessage(new AIMessage(message));
}
async clear(): Promise<void> {
delete globalInMemoryStore[this.sessionId];
globalInMemoryStore[this.sessionId] = []; // 다시 초기화
return Promise.resolve();
}
}
CustomChatMessageHistory는 sessionId를 인스턴스 프로퍼티로 가지고, 각 세션별 대화 기록을 globalInMemoryStore라는 전역 객체에 저장한다. 이 객체는 키-값 형태로, 키는 sessionId, 값은 메시지 배열이다. 이 구조는 단일 서버나 테스트 환경에서 빠른 저장과 조회가 가능하지만, 실제 운영 환경에서는 Redis나 데이터베이스 클라이언트로 대체하는 것이 일반적이다.
주요 메서드들은 다음과 같다. getMessages()는 해당 세션의 메시지 배열을 반환하며, addMessage()는 새 메시지를 세션에 추가한다. addSystemMessage(), addUserMessage(), addAIChatMessage()는 각각 시스템, 사용자, AI 메시지를 타입에 맞게 래핑하여 추가하는 편의 메서드다. 마지막으로 clear()는 세션 메시지를 초기화해 새 대화를 시작할 수 있도록 한다.
RunnableWithMessageHistory
Runnable 프로토콜은 작업 실행 단위를 추상화한 핵심 인터페이스이다. 이 프로토콜은 특정 작업을 실행하는 invoke() 메서드를 반드시 구현하도록 요구하며, 이를 통해 다양한 종류의 컴포넌트가 일관된 방식으로 실행될 수 있도록 설계되었다. 즉, Runnable 프로토콜을 구현한 객체는 입력값을 받아 처리하고, 결과를 반환하는 실행 가능한 단위로 동작한다. 이는 LangChain의 워크플로우, 체인(chain), 도구(tool) 등이 공통적으로 갖는 인터페이스로, 상호 교환 가능성과 확장성을 높인다.
또한, Runnable 프로토콜은 비동기 처리, 에러 핸들링, 입력 및 출력 타입 관리 등 실행 환경과 관련된 공통 기능을 표준화하는 기반을 제공한다. LangChain의 여러 고수준 추상화 계층은 Runnable 프로토콜을 확장하거나 구현하여, 복잡한 AI 워크플로우를 체계적이고 재사용 가능하게 만든다. 따라서 Runnable 프로토콜은 LangChain의 모듈화와 확장성을 뒷받침하는 중추적인 설계 요소라 할 수 있다.
RunnableWithMessageHistory는 LangChain에서 Runnable 프로토콜을 확장한 인터페이스로, 실행 단위가 이전 대화 내역과 연동되어 작동하도록 설계되었다. 즉, 단순히 입력값을 처리하는 것에 그치지 않고, 대화의 맥락이나 히스토리를 함께 관리하며 이를 기반으로 응답을 생성하거나 작업을 수행한다. 이 기능은 챗봇이나 대화형 AI 시스템에서 매우 중요하며, 대화 흐름을 자연스럽고 일관되게 유지하는 데 필수적이다. RunnableWithMessageHistory는 메시지 히스토리를 인자로 받아 처리함으로써, 과거 대화 내용에 기반한 적절한 의사결정과 응답 생성이 가능하게 한다. 따라서 LangChain에서 대화형 워크플로우를 구현할 때 대화 상태를 체계적으로 관리하고, 이전 대화 맥락을 반영한 동적인 처리를 지원하는 중요한 역할을 한다.
// RunnableWithMessageHistory 설정
const chainWithHistory = new RunnableWithMessageHistory({
runnable: codeReviewPrompt.pipe(model).pipe(new StringOutputParser()), // 실제 실행될 체인
getMessageHistory: (sessionId: string) => new CustomChatMessageHistory(sessionId), // 세션 ID별로 CustomChatHistory 인스턴스 반환
inputMessagesKey: "input", // 사용자의 현재 입력을 나타내는 키
historyMessagesKey: "history", // 프롬프트 내의 MessagesPlaceholder 키와 일치
});
invoke
invoke 메서드는 LangChain에서 모델이나 Runnable 객체를 실행하는 핵심 함수이다. 이 메서드에 입력값을 전달하면 내부적으로 설정된 프롬프트 템플릿과 함께 AI 모델에 요청을 보내고, 그 결과로 생성된 응답을 반환한다. 즉, invoke는 단순히 입력을 처리하는 것을 넘어, 대화 흐름이나 작업의 맥락을 반영해 적절한 출력을 만들어내는 역할을 한다. 이를 통해 개발자는 복잡한 AI 연산 과정을 추상화하고, 손쉽게 모델과 상호작용할 수 있다.
import * as readline from 'node:readline/promises';
import { stdin as input, stdout as output } from 'node:process';
async function startInteractiveCodeReview() {
const rl = readline.createInterface({ input, output }); // readline 인터페이스 생성
const sessionId = "interactive-interview-" + Date.now();
console.log(`세션 ID: ${sessionId}`);
try {
// 대화 루프
while (true) {
const userInput = await rl.question("You: "); // 터미널에서 사용자 입력 받기
if (userInput.toLowerCase() === "q") {
console.log("대화 종료");
break; // 대화 종료
}
const aiResponse = await chainWithHistory.invoke(
{
role: "코드 리뷰어",
focusAreas: "코딩 스타일, 성능, 보안",
tone: "친절하고 건설적이며 짧게",
input: userInput
},
{ configurable: { sessionId: sessionId } }
);
console.log("AI: ", aiResponse);
}
} catch (error) {
console.error("대화 중 오류 발생:", error);
} finally {
rl.close(); // readline 인터페이스 종료
}
}
// 대화형 코드 리뷰 시작
startInteractiveCodeReview();
RAG
RAG는 "Retrieval-Augmented Generation"의 약자로, AI 언어 모델의 생성 능력에 외부 지식 검색(Retrieval) 기능을 결합한 기술이다. 기존의 생성형 모델은 학습된 데이터에 기반해 답변을 생성하지만, 최신 정보나 특정 도메인 지식을 바로 반영하기 어려운 한계가 있다. RAG는 이 문제를 해결하기 위해, 먼저 관련 문서나 데이터베이스에서 필요한 정보를 검색한 후, 그 결과를 바탕으로 보다 정확하고 풍부한 답변을 생성한다.
즉, RAG는 검색과 생성 단계를 결합하여 AI의 응답 품질과 신뢰성을 크게 향상시킨다. 예를 들어, 대용량 문서나 웹사이트에서 실시간으로 필요한 내용을 찾아내어, 사용자의 질문에 대해 최신 정보와 맥락을 반영한 답변을 제공할 수 있다. 이러한 특성 덕분에 챗봇, 고객 지원, 지식 관리 시스템 등 다양한 분야에서 RAG가 널리 활용되고 있다.
이 부분은 나중에 더 알아보고자 한다.
블로그의 정보
Ayden's journal
Beard Weard Ayden