Reflection Agent (CLI → API) + React 구현 가이드
프로젝트 개요
기존 CLI 기반의 Reflection Agent를 웹 애플리케이션으로 전환하는 프로젝트
LangGraph를 활용한 AI 트윗 생성 및 자체 개선 시스템을 FastAPI 백엔드와 React 프론트엔드로 구현하여,
사용자가 웹 브라우저에서 직관적으로 바이럴 트윗을 생성하고,
개선 과정을 실시간으로 확인
시스템 아키텍처
전체 흐름
1. 사용자 입력 - React 프론트엔드에서 트윗 주제 입력
2. 초기 트윗 생성 - AI가 첫 번째 트윗 생성
3. 자체 분석 - AI가 생성한 트윗을 스스로 분석하고 개선점 도출
4. 트윗 개선 - 분석 결과를 바탕으로 트윗 재작성
5. 반복 개선 - 최대 2회까지 분석-개선 사이클 실행
6. 결과 출력 - 각 단계별 결과를 실시간으로 화면에 표시
컴포넌트 구조
1. React Frontend (Port 3000): 사용자 인터페이스 및 결과 표시
2. FastAPI Backend (Port 8000): REST API 서버 및 LangGraph 실행
3. LangGraph Agent: 트윗 생성 및 반성 워크플로우 관리
FastAPI 백엔드 구현
1. 기본 서버 설정 및 CORS 구성
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# React 앱과의 통신을 위한 CORS 설정
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 개발용 전체 허용
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
CORS 미들웨어는 브라우저의 동일 출처 정책(Same-Origin Policy)을 우회하여
React 앱(localhost:3000)에서 FastAPI 서버(localhost:8000)로의 API 호출을 허용
2. LangGraph 노드 구현
Generation Node (트윗 생성)
def generation_node(state: Sequence[BaseMessage]):
"""트윗 생성 노드"""
result = generate_chain.invoke({"messages": state})
print(f"\n[DEBUG] generate_chain result: {type(result)}\n")
# 타입 안전성 보장 - 예상치 못한 응답 타입 처리
if not isinstance(result, BaseMessage):
result = AIMessage(content=str(result))
return list(state) + [result]
generate_chain을 호출하여 트윗을 생성하고,
결과가 BaseMessage 타입이 아닐 경우 AIMessage로 변환하여 타입 안전성을 보장
Reflection Node (자체 분석)
def reflection_node(messages: Sequence[BaseMessage]) -> List[BaseMessage]:
"""트윗 분석 및 개선 제안 노드"""
# 최신 AI 메시지를 역순으로 탐색
last_ai_message = None
for msg in reversed(messages):
if isinstance(msg, AIMessage):
last_ai_message = msg
break
if last_ai_message:
analysis_messages = [HumanMessage(content=f"""
다음 트윗을 분석하고 구체적인 개선점을 찾아주세요:
트윗: "{last_ai_message.content}"
다음 관점에서 평가하고 개선안을 제시해주세요:
1. 길이 (280자 이내 최적화)
2. 감정적 임팩트 (호기심, 공감, 놀라움 등)
3. 행동 유도 (리트윗, 댓글을 유도하는 요소)
4. 트렌드 반영 (최신 트렌드, 밈, 표현)
5. 가독성 (이모지, 줄바꿈 등)
""")]
res = reflect_chain.invoke({"messages": analysis_messages})
return list(messages) + [HumanMessage(content=res.content)]
메시지 목록을 역순으로 순회하여 가장 최근의 AI 생성 트윗을 찾고,
5가지 관점에서 구체적인 분석과 개선안을 제시
3. LangGraph 워크플로우 구성
builder = MessageGraph()
builder.add_node(GENERATE, generation_node)
builder.add_node(REFLECT, reflection_node)
builder.set_entry_point(GENERATE)
def should_continue(state: List[BaseMessage]):
"""종료 조건: 3회 반복 후 종료 (초기 요청 + 2번의 개선)"""
return END if len(state) > 5 else REFLECT
builder.add_conditional_edges(GENERATE, should_continue)
builder.add_edge(REFLECT, GENERATE)
graph = builder.compile()
GENERATE 노드에서 시작
should_continue 함수로 메시지가 5개를 초과하면 종료, 아니면 REFLECT로 이동
REFLECT에서는 항상 GENERATE로 돌아가는 순환 구조
4. API 엔드포인트
@app.post("/generate")
async def generate(request: Request):
data = await request.json()
input_text = data.get("text", "")
result = graph.invoke(HumanMessage(content=input_text))
return {
"results": [
{"type": msg.type, "content": msg.content}
for msg in result
]
}
사용자의 입력을 받아 LangGraph를 실행하고, 모든 메시지를 JSON 형태로 반환
React 프론트엔드 구현
1. 상태 관리 및 API 호출
const [inputText, setInputText] = useState('');
const [results, setResults] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setIsLoading(true);
try {
const response = await axios.post(`${API_BASE_URL}/generate`, {
text: inputText
});
setResults(response.data.results);
} catch (err) {
console.error('API Error:', err);
} finally {
setIsLoading(false);
}
};
React Hooks를 사용하여 상태를 관리하고, axios로 FastAPI 서버와 통신
2. 단계별 결과 표시
const getStepLabel = (index, type) => {
if (index === 0) return '📝 초기 입력';
if (type === 'ai') {
const aiIndex = Math.floor(index / 2);
return `🤖 AI 트윗 생성 ${aiIndex > 1 ? `(${aiIndex}차 개선)` : ''}`;
} else {
const analysisIndex = Math.floor((index + 1) / 2);
return `🔍 AI 분석 ${analysisIndex}`;
}
};
메시지의 인덱스와 타입을 기반으로 각 단계에 적절한 라벨을 생성
3. 글자수 체크 및 복사 기능
const getCharacterCountColor = (length) => {
if (length <= 250) return 'text-green-600';
if (length <= 280) return 'text-orange-600';
return 'text-red-600';
};
const copyToClipboard = (text) => {
navigator.clipboard.writeText(text);
// 토스트 알림 표시
showToast('클립보드에 복사되었습니다!');
};
트위터의 280자 제한에 맞춰 글자수를 색상으로 표시하고,
클립보드 API를 사용하여 원클릭 복사 기능을 구현
4. 반응형 UI 컴포넌트
return (
<div className="space-y-6">
{results.map((message, index) => (
<div key={index} className={`bg-white rounded-lg shadow-lg p-6 ${
message.type === 'ai' ? 'border-l-4 border-blue-500' : 'border-l-4 border-green-500'
}`}>
<div className="flex items-start gap-3">
{getMessageIcon(message.type)}
<div className="flex-1">
<h3 className="font-medium text-gray-800 mb-2">
{getStepLabel(index, message.type)}
</h3>
<div className="bg-gray-50 rounded-lg p-4">
<p className="text-gray-700 whitespace-pre-wrap leading-relaxed">
{message.content}
</p>
</div>
{message.type === 'ai' && (
<div className="mt-3 flex items-center justify-between">
<span className={`text-sm font-medium ${getCharacterCountColor(message.content.length)}`}>
{message.content.length}/280자
</span>
<button onClick={() => copyToClipboard(message.content)}
className="flex items-center gap-1 px-3 py-1 bg-blue-500 text-white rounded">
<Copy className="w-4 h-4" />
복사
</button>
</div>
)}
</div>
</div>
</div>
))}
</div>
);
Tailwind CSS를 사용하여 반응형 카드 레이아웃을 구현하고,
메시지 타입에 따라 다른 색상의 보더를 적용
워크플로우 제어 로직
종료 조건 최적화
def should_continue(state: List[BaseMessage]):
return END if len(state) > 5 else REFLECT
총 메시지 수가 5개를 초과하면 종료하여 적절한 개선 횟수(초기 요청 + 2라운드)를 보장
모든 노드에서 BaseMessage 타입을 체크하고, 예상치 못한 응답을 AIMessage로 변환하여 워크플로우의 안정성을 확보
주요 특징
1. 자체 반성 시스템: AI가 생성한 트윗을 5가지 관점(길이, 감정적 임팩트, 행동 유도, 트렌드 반영, 가독성)에서 스스로 분석하고 개선하는 메타인지 능력 구현
2. 실시간 워크플로우 시각화: 각 단계별 메시지를 순차적으로 표시하여 AI의 사고 과정을 투명하게 조회
3. 확장 가능한 아키텍처: FastAPI와 React의 분리된 구조로 독립적인 확장 및 배포가 가능하며, RESTful API를 통해 다른 클라이언트와도 쉽게 통합
동작 화면
1. 사용자가 트윗 내용 입력
2. 개선 결과
https://github.com/myjhye/reflection-agent
GitHub - myjhye/reflection-agent
Contribute to myjhye/reflection-agent development by creating an account on GitHub.
github.com