LangChain기반 멀티스텝AI 구현검토 - 100-hours-a-week/3-team-ssammu-wiki GitHub Wiki
이력서 정보 추출
왜 LangChain이 필요한가?
- 기능 요약
💡
- 사용자가 업로드한 PDF 이력서를 텍스트로 변환
- 변환된 텍스트를 LangChain + LLM 으로 전달
- 자격증 수, 프로젝트 수, 전공 유무, 경력(회사 종류, 기간, 직무), 기타 사항을 자연어 기반으로 추론
- 추론 결과를 JSON으로 반환
LLM 기반 추론이 정규식 기반 파싱보다 효과적인 이유
-
추출 대상 정보가 자연어 속에 숨겨져 있음
- 프로젝트 수, 자격증 수, 경력 기간 등은 이력서에 직접적으로 숫자로 표현되어 있지 않음
- 예: “삼성전자에서 3년간 백엔드 개발 경험” → 사람은 ‘36개월’로 이해 가능하지만 정규표현식으로는 추론 불가
- ⇒ LLM이 문맥을 이해해서 유연하게 추출할 수 있음
-
PDF 이력서 텍스트는 구조가 불안정함
- 줄바꿈이 애매하거나
- 표가 깨지거나
- 섹션이 섞이는 경우가 많음
- 전통적인 파싱(re, split)으로는 처리 어려움
- ⇒ LLM은 텍스트 의미 기반으로 추론하므로 유연하며, 정보 누락 시
null
반환도 가능
-
LangChain을 쓰면 출력 포맷을 통제할 수 있음
- Prompt template 관리
- Chain 흐름 구조화
- Output schema 및 파싱 설정 (예:
StructuredOutputParser
) - ⇒ 유지보수성과 확장성이 뛰어남 (항목이 늘어나도 관리 용이)
-
사용자마다 이력서 형식이 달라도 적용 가능
- 어떤 사용자는 “프로젝트”를 bullet으로
- 어떤 사용자는 “경력 요약”에 자연스럽게 녹여서 서술함
- ⇒ LLM은 다양한 표현을 이해하고 추론할 수 있으므로 비정형 이력서에 적합
=> 요약: LangChain + LLM은 정규식 기반 파싱보다 훨씬 효율적이며, 확장성 있는 아키텍처를 제공함.
실제 구현한 코드
llm = ChatOpenAI(temperature=0, model="gpt-3.5-turbo")
template = """
다음은 이력서 텍스트야. 이 텍스트를 바탕으로 다음 정보를 JSON 형식으로 추출해줘:
- 이메일 주소 (없으면 null)
- 프로젝트 개수
- 자격증 개수
- 총 경력 기간 (개월 단위, 없으면 0)
아래 이력서를 분석해줘:
-------------------------
{text}
-------------------------
"""
prompt = PromptTemplate(input_variables=["text"], template=template)
chain = LLMChain(llm=llm, prompt=prompt)
doc = fitz.open("코딩몬스터-표준이력서.pdf")
text = "\n".join([page.get_text() for page in doc])
result = chain.run(text=text)
print(result)
# 추출 결과:
{
"이메일 주소": "[email protected]",
"프로젝트 개수": 6,
"자격증 개수": 1,
"총 경력 기간": 27
}
기업 추천 기능 - LangChain
1. LangChain 기반 추천 흐름도
단계 | 설명 |
---|---|
1 | 사용자가 입력한 자연어 추천 요청(예: “강남역 근처 MZ가 선호하는 회사 추천해줘”)을 수신 |
2 | LLM(Gemini API 등)을 이용해 추천 조건(위치, 연봉, 근무형태 등)을 structured parsing |
3 | 조건 기반으로 벡터 DB(ChromaDB)에서 유사 기업 검색 수행 |
4 | 검색 결과를 기업명 리스트 형태로 반환 |
5 | 선택 시 프론트에서 상세 기업 정보 페이지로 이동 |
2. 사용한 LangChain 구성 요소
포넌트 | 역할 |
---|---|
PromptTemplate + ChatOpenAI(Gemini API) | 사용자의 자연어 요구를 정제하여 structured JSON 형태로 파싱 |
ChromaDB + similarity_search_with_filter | SBERT 임베딩 기반으로 기업 벡터 검색, 필터링 지원 |
Retriever + LLMChain | 자연어 요청 → 필터 조건 추출 → 필터링 검색 → 결과 반환 체인 구성 |
3. 추천 흐름 주요 코드 예시
(1) 사용자 요청 → 조건 추출
from langchain.prompts import ChatPromptTemplate
from langchain.chat_models import ChatOpenAI
llm = ChatOpenAI(model="gemini-pro")
prompt = ChatPromptTemplate.from_template("""
너는 취준생 관점에서 기업 추천을 도와주는 역할이야.
사용자 요청에서 다음 조건을 추출해 JSON으로 정리해줘:
- location (위치 조건)
- salary (희망 연봉 조건, 없으면 null)
- company_size (희망 회사 규모, 없으면 null)
사용자 요청: {query}
""")
chain = LLMChain(llm=llm, prompt=prompt)
result = chain.run({"query": user_query})
(2) 조건 기반 벡터 검색
from langchain.vectorstores import Chroma
retriever = chroma.as_retriever(
search_kwargs={
"k": 5,
"filter": {
"location": parsed_conditions["location"],
"salary": parsed_conditions["salary"],
"company_size": parsed_conditions["company_size"]
}
}
)
docs = retriever.get_relevant_documents("유사 기업 추천")
4. LangChain 체인을 도입한 이유 및 장점
항목 | 설명 |
---|---|
정확한 기업 추천 | LLM이 의미 기반으로 조건 필터링, Vector Search는 유사 기업 추천 |
자연어 입력 지원 | 사용자가 키워드가 아닌 자유로운 자연어로 입력 가능 |
확장성 | 직무/위치/급여 외 추가 필터링도 쉽게 확장 가능 |
멀티스텝 체인 적용 가능 | Conversational Retrieval 가능 (예: 사용자 보완 질문 응답 후 재추천) |
LangGraph 기반 에이전트 구현
개요
LangGraph를 활용한 대화형 이력서 생성 에이전트를 구현했습니다. 이 시스템은 Redis 기반 상태 관리, 다단계 질문-답변 플로우, 그리고 최종 이력서 자동 생성까지의 전체 워크플로우를 제공합니다.
시스템 아키텍처
전체 플로우
사용자 → Spring → FastAPI → LangGraph Agent → Redis → LLM → S3
주요 구성 요소
- FastAPI: REST API 서버
- LangGraph: 대화형 에이전트 워크플로우 엔진
- Redis: 상태 관리 및 세션 저장소
- LLM: 질문 생성 및 이력서 작성 (VLLM/OpenAI)
- S3: 최종 이력서 파일 저장
왜 LangGraph인가?
1. 상태 기반 대화 관리
- 사용자와의 다단계 대화를 체계적으로 관리
- 각 단계별 상태 추적 및 복원 가능
- 중단된 대화의 재개 지원
2. 유연한 워크플로우 설계
# LangGraph 노드 구조
graph = StateGraph(ResumeAgentState)
graph.add_node("receive_answer", ReceiveAnswerNode)
graph.add_node("check_completion", CheckCompletionNode)
graph.add_node("generate_question", GenerateQuestionNode)
graph.add_node("create_resume", CreateResumeNode)
3. 조건부 분기 처리
- 질문이 더 필요한지 vs 이력서 생성 완료인지 동적 판단
- 사용자 답변에 따른 적응적 질문 생성
- 오류 발생 시 복구 로직
구현된 기능
/resume/agent/init
)
🚀 이력서 에이전트 초기화 (요청 흐름:
- Spring → FastAPI로 초기 사용자 정보 전달
- 초기
ResumeAgentState
생성 - LLM을 통한 첫 번째 질문 생성
- Redis에 상태 저장
- 생성된 질문을 Spring으로 반환
핵심 구현:
async def initialize_resume_agent(payload: ResumeAgentInitRequest):
# 1. 초기 상태 생성
initial_state = create_initial_state(member_id=member_id, inputs=payload.inputs)
# 2. 첫 번째 질문 생성
first_question = await _generate_first_question(initial_state)
# 3. Redis에 상태 저장
save_success = await redis_client.save_state(member_id, initial_state)
# 4. 응답 반환
return {"member_id": member_id, "question": first_question}
/resume/agent/update
)
🔄 이력서 에이전트 업데이트 (요청 흐름:
- 사용자 답변 수신
- Redis에서 기존 상태 로드
- LangGraph 워크플로우 실행
- 다음 질문 생성 OR 이력서 완성
- 결과에 따라 Redis 저장/삭제
핵심 구현:
async def update_resume_agent(payload: ResumeAgentUpdateRequest):
# 1. 동시성 제어 - 락 획득
await self._acquire_lock_or_fail(member_id)
try:
# 2. 기존 상태 로드 및 검증
current_state = await self._get_and_validate_state(member_id)
# 3. 답변 처리
updated_state = self._process_answer(current_state, answer)
# 4. LangGraph 실행
final_state = await self._execute_agent_workflow(updated_state)
# 5. 결과 처리
if final_state.info_ready:
# 완료: S3 업로드 & Redis 삭제
return await self._handle_completion(member_id, final_state)
else:
# 계속: 다음 질문 & Redis 저장
return await self._handle_next_question(member_id, final_state)
finally:
await self._safe_release_lock(member_id)
📊 Redis 기반 상태 관리
상태 저장/로드:
class RedisClient:
async def save_state(self, member_id: int, state: ResumeAgentState) -> bool:
state_dict = state.to_redis_dict()
# datetime 직렬화 문제 해결
state_json = json.dumps(state_dict, ensure_ascii=False, default=str)
async def load_state(self, member_id: int) -> Optional[ResumeAgentState]:
state_data = json.loads(state_json)
return ResumeAgentState.from_redis_dict(state_data)
동시성 제어:
async def acquire_lock(self, member_id: int) -> bool:
# Redis SET NX EX 명령으로 원자적 락 구현
result = await asyncio.to_thread(
self.redis_client.set, lock_key, lock_value, nx=True, ex=timeout
)
🤖 LangGraph 워크플로우
노드 구조:
- ReceiveAnswerNode: 사용자 답변 처리
- CheckCompletionNode: 완료 조건 확인
- GenerateQuestionNode: 다음 질문 생성
- CreateResumeNode: 최종 이력서 생성
실행 흐름:
async def _execute_agent_workflow(self, state: ResumeAgentState):
final_step = None
async for step in resume_agent.astream(state):
final_step = step
return self._extract_final_state(final_step)
해결한 기술적 과제
1. DateTime 직렬화 오류
문제: Redis 저장 시 datetime 객체 JSON 직렬화 실패
해결: json.dumps(..., default=str)
추가
2. 응답 스키마 불일치
문제: 클라이언트는 isComplete
를 기대하는데 서버는 is_complete
반환
해결: Pydantic alias 추가
class ResumeAgentUpdateResponse(BaseModel):
is_complete: bool = Field(..., alias="isComplete")
resume_object_key: Optional[str] = Field(None, alias="resumeObjectKey")
class Config:
by_alias = True
3. VLLM 연결 및 Fallback 처리
문제: 원격 VLLM 서버 연결 불안정
해결: OpenAI API fallback + 에러 처리 강화
4. 동시성 제어
문제: 같은 사용자의 동시 요청 처리
해결: Redis 락 메커니즘 구현
테스트 결과
✅ 성공한 테스트들
- Redis 연결 및 기본 CRUD 작업
- 상태 저장/로드/삭제 기능
- init → update 전체 플로우
- S3 파일 업로드
- 동시성 제어 (락 기능)
📊 실제 테스트 결과
🎯 === 이력서 에이전트 전체 플로우 테스트 ===
✅ 서버 상태: healthy
✅ 초기화 성공
✅ 3번의 질문-답변 완료
✅ 이력서 생성 완료
📄 S3 업로드: resume/resume_agent_20250708_102544.docx
시퀀스 다이어그램
초기화 플로우
sequenceDiagram
participant User
participant Spring
participant FastAPI
participant Redis
participant LLM
User->>Spring: 고급 이력서 생성 요청
Spring->>FastAPI: POST /resume/agent/init
FastAPI->>LLM: 첫 질문 생성
FastAPI->>Redis: 상태 저장
FastAPI-->>Spring: question 반환
업데이트 플로우
sequenceDiagram
participant User
participant Spring
participant FastAPI
participant Redis
participant LangGraph
participant S3
User->>Spring: 답변 제출
Spring->>FastAPI: POST /resume/agent/update
FastAPI->>Redis: 상태 로드
FastAPI->>LangGraph: 워크플로우 실행
alt 질문 필요
LangGraph->>FastAPI: 다음 질문
FastAPI->>Redis: 상태 저장
FastAPI-->>Spring: question 반환
else 완료
LangGraph->>FastAPI: 이력서 생성
FastAPI->>S3: 파일 업로드
FastAPI->>Redis: 상태 삭제
FastAPI-->>Spring: resumeObjectKey 반환
end
디렉토리 구조
fastapi_project/
├── app/
│ ├── agents/ # LangGraph 에이전트 구현
│ │ ├── nodes/ # 개별 노드 구현
│ │ │ ├── check_completion.py
│ │ │ ├── create_resume.py
│ │ │ ├── generate_question.py
│ │ │ └── receive_answer.py
│ │ └── resume_agent.py # 메인 에이전트 정의
│ ├── routes/ # API 엔드포인트
│ │ ├── resume_agent_init.py # 초기화 API
│ │ └── resume_agent_update.py # 업데이트 API
│ ├── schemas/ # 데이터 모델
│ │ ├── api_responses.py # 응답 스키마
│ │ └── resume_models.py # 상태 모델
│ ├── utils/ # 유틸리티
│ │ └── redis_client.py # Redis 연결 관리
│ └── main.py # FastAPI 앱 설정
├── tests/ # 테스트 코드
│ ├── test_redis_connection.py # Redis 테스트
│ └── test_init_update_flow.py # 통합 테스트
└── .env # 환경 설정
API 명세
1. 에이전트 초기화
Endpoint: POST /api/v1/resume/agent/init
Request:
{
"member_id": 1001,
"inputs": {
"email": "[email protected]",
"preferred_job": "AI 엔지니어",
"certification_count": 3,
"project_count": 5,
"major_type": "MAJOR",
"company_name": "스타트업",
"position": "백엔드 개발자",
"work_period": 24,
"additional_experiences": "Python, FastAPI, LangChain"
}
}
Response:
{
"member_id": 1001,
"question": "AI 엔지니어로서 성장하기 위해 현재 공부하고 있는 분야가 있나요?"
}
2. 에이전트 업데이트
Endpoint: POST /api/v1/resume/agent/update
Request:
{
"member_id": 1001,
"answer": "현재 LangChain과 LangGraph를 중점적으로 공부하고 있습니다."
}
Response (계속):
{
"member_id": 1001,
"isComplete": false,
"question": "현재 진행 중인 프로젝트에서 특별히 자신있는 부분이 있나요?"
}
Response (완료):
{
"member_id": 1001,
"isComplete": true,
"resumeObjectKey": "resume/resume_agent_20250708_102544.docx"
}
3. 상태 조회
Endpoint: GET /api/v1/resume/agent/status/{member_id}
Response:
{
"member_id": 1001,
"step": "questioning",
"asked_count": 2,
"max_questions": 5,
"pending_questions": 1,
"answers_count": 2,
"created_at": "2025-07-08T10:21:30",
"updated_at": "2025-07-08T10:25:44"
}
성과 및 개선사항
🎉 달성한 성과
- SpringBoot → FastAPI 성공적 이전
- LangGraph 기반 대화형 에이전트 구현
- Redis 상태 관리 시스템 구축
- 전체 플로우 테스트 통과
- S3 연동 및 파일 업로드 완료
🚀 향후 개선 방향
- Spring ↔ FastAPI 통신 테스트 완료
- VLLM 연결 최적화 (네트워크 설정)
- 에러 복구 로직 강화
- 성능 모니터링 추가
- 로그 분석 시스템 구축
기술 스택
- Backend: FastAPI, Python 3.11
- Agent Framework: LangGraph
- State Management: Redis
- LLM: VLLM (로컬) / OpenAI (fallback)
- Storage: AWS S3
- Testing: pytest, custom integration tests
설치 및 실행
1. 환경 설정
# Python 가상환경 생성
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
# 의존성 설치
pip install -r requirements.txt
2. 환경 변수 설정
# .env 파일 생성
cat > .env << EOF
# Redis 설정
REDIS_URL=redis://localhost:6379/0
REDIS_STATE_TTL=86400
REDIS_LOCK_TTL=30
# LLM 설정
LLM_TYPE=openai # 또는 vllm
OPENAI_API_KEY=your_openai_key
OPENAI_MODEL=gpt-3.5-turbo
# VLLM 설정 (선택)
VLLM_URL=http://localhost:8001
MODEL_NAME=your_model_name
# S3 설정
AWS_ACCESS_KEY_ID=your_access_key
AWS_SECRET_ACCESS_KEY=your_secret_key
S3_BUCKET_NAME=your_bucket_name
AWS_DEFAULT_REGION=ap-northeast-2
EOF
3. 서비스 실행
# Redis 서버 실행
redis-server
# FastAPI 서버 실행
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
4. 테스트 실행
# Redis 연결 테스트
python tests/test_redis_connection.py
# 전체 플로우 테스트
python tests/test_init_update_flow.py
이 구현을 통해 확장 가능하고 안정적인 AI 기반 이력서 생성 시스템을 구축했으며, 향후 추가 기능 확장의 견고한 기반을 마련했습니다.
LangChain 기반 최신 이슈 정리 파이프라인 설계 및 활용 정리
1. LangChain 기반 AI 추론 흐름도
-
전체 체인 흐름 설명
단계 설명 1 사용자가 지정한 기업 이름(corp_name)을 기반으로 벡터DB(ChromaDB)에서 의미 기반 검색 수행 2 검색 결과로 나온 뉴스/공시 텍스트를 하나의 context로 통합 3 이 context를 기반으로 LLM에 요약 요청 (프롬프트 기반) 4 요약 결과를 반환하여 ‘최신 이슈’ 콘텐츠 생성 -
체인 내 LangChain 컴포넌트 및 역할
-
SentenceTransformerEmbeddings
→ ChromaDB에 의미 기반 검색을 가능하게 하기 위해 문장 임베딩 수행
-
Chroma.as_retriever()
→ 검색(Query) 단계에서 의미 기반으로 문서를 찾아주는 Retriever 역할 수행
-
2. 사용된 도구 및 외부 리소스
도구/리소스 | 사용 목적 | 세부 설명 |
---|---|---|
SentenceTransformerEmbeddings | 의미 기반 검색 임베딩 모델 | "snunlp/KR-SBERT-V40K-klueNLI-augSTS" 모델 사용 |
ChromaDB | 벡터 저장 및 검색 | 뉴스/공시 임베딩 저장 후 의미 검색에 활용 |
vLLM 서버 (OpenAI API 호환) | 요약 생성용 LLM 서버 | http://localhost:8000/v1/chat/completions에 POST 요청 |
3. 실제 구현한 주요 코드
(1) 벡터DB + Retriever 설정
embedding_function = SentenceTransformerEmbeddings(
model_name="snunlp/KR-SBERT-V40K-klueNLI-augSTS"
)
chroma = Chroma(
persist_directory="db/chroma",
embedding_function=embedding_function
)
retriever = chroma.as_retriever(
search_kwargs={
"filter": {"corp": corp_name},
"k": 8
}
)
docs = retriever.get_relevant_documents(search_query)
(2) 요약 요청 프롬프트 구성 및 LLM 호출
prompt = (
f"너는 취업준비생들의 관점에서 기업 분석을 도와주는 역할이야.\n"
f"다음은 {corp_name}의 최근 뉴스 및 공시 정보야.\n"
f"'사업 전략', '채용 계획', '인사 정책', '미래 성장성', '경쟁력'에 관련된 내용을 중심으로, "
f"취업 준비생이 관심 가질 만한 주요 내용을 간결하고 이해하기 쉽게 요약해줘.\n\n"
f"{context}"
)
result = call_vllm(prompt)
4. LangChain 체인을 도입한 이유 및 장점
항목 | 설명 |
---|---|
답변 정확도 향상 | 의미 기반 검색(semantic retrieval)을 통해, 단순 키워드 매칭보다 문맥적으로 더 관련성 높은 뉴스/공시를 요약에 사용함 |
복잡한 작업 자동화 | 문서 검색 → context 구성 → 요약 요청 흐름을 하나의 체인처럼 구성해 자동화 |
서비스 기능과의 연관성 | ‘최신 이슈 탭’에서 사용자가 진짜 관심 가질 만한 채용/미래성장성 위주 요약을 제공하는 데 최적화됨 |
확장성 확보 | 추후 ‘경쟁사 비교 요약’, ‘3개월 트렌드 변화 요약’ 등 다양한 추가 체인 확장이 가능함 |
5. 향후 확장 가능성
- 현재는 “의미 기반 검색 + 단일 요약” 구조지만,
- 앞으로 멀티스텝 체인을 구성할 수 있음:
- 예: ① 의미 기반 검색 → ② 필터링(인사/성장성) → ③ 문단별 요약 → ④ 최종 요약 통합
- LangChain의 ConversationalRetrievalChain, MapReduceDocumentsChain 등을 사용하면 가능
⇒ 요약 : 이번 ‘최신 이슈 정리’ 기능은 LangChain을 활용하여 문서 검색과 요약 흐름을 체계화했고, 의미 기반 검색과 LLM 요약을 결합함으로써 답변 품질과 확장성을 모두 확보함.