LangChain기반 멀티스텝AI 구현검토 - 100-hours-a-week/3-team-ssammu-wiki GitHub Wiki

이력서 정보 추출

왜 LangChain이 필요한가?

  • 기능 요약

💡

  1. 사용자가 업로드한 PDF 이력서를 텍스트로 변환
  2. 변환된 텍스트를 LangChain + LLM 으로 전달
  3. 자격증 수, 프로젝트 수, 전공 유무, 경력(회사 종류, 기간, 직무), 기타 사항을 자연어 기반으로 추론
  4. 추론 결과를 JSON으로 반환

LLM 기반 추론이 정규식 기반 파싱보다 효과적인 이유

  1. 추출 대상 정보가 자연어 속에 숨겨져 있음

    • 프로젝트 수, 자격증 수, 경력 기간 등은 이력서에 직접적으로 숫자로 표현되어 있지 않음
    • 예: “삼성전자에서 3년간 백엔드 개발 경험” → 사람은 ‘36개월’로 이해 가능하지만 정규표현식으로는 추론 불가
    • ⇒ LLM이 문맥을 이해해서 유연하게 추출할 수 있음
  2. PDF 이력서 텍스트는 구조가 불안정함

    • 줄바꿈이 애매하거나
    • 표가 깨지거나
    • 섹션이 섞이는 경우가 많음
    • 전통적인 파싱(re, split)으로는 처리 어려움
    • ⇒ LLM은 텍스트 의미 기반으로 추론하므로 유연하며, 정보 누락 시 null 반환도 가능
  3. LangChain을 쓰면 출력 포맷을 통제할 수 있음

    • Prompt template 관리
    • Chain 흐름 구조화
    • Output schema 및 파싱 설정 (예: StructuredOutputParser)
    • ⇒ 유지보수성과 확장성이 뛰어남 (항목이 늘어나도 관리 용이)
  4. 사용자마다 이력서 형식이 달라도 적용 가능

    • 어떤 사용자는 “프로젝트”를 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)

요청 흐름:

  1. Spring → FastAPI로 초기 사용자 정보 전달
  2. 초기 ResumeAgentState 생성
  3. LLM을 통한 첫 번째 질문 생성
  4. Redis에 상태 저장
  5. 생성된 질문을 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)

요청 흐름:

  1. 사용자 답변 수신
  2. Redis에서 기존 상태 로드
  3. LangGraph 워크플로우 실행
  4. 다음 질문 생성 OR 이력서 완성
  5. 결과에 따라 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 워크플로우

노드 구조:

  1. ReceiveAnswerNode: 사용자 답변 처리
  2. CheckCompletionNode: 완료 조건 확인
  3. GenerateQuestionNode: 다음 질문 생성
  4. 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 연동 및 파일 업로드 완료

🚀 향후 개선 방향

  1. Spring ↔ FastAPI 통신 테스트 완료
  2. VLLM 연결 최적화 (네트워크 설정)
  3. 에러 복구 로직 강화
  4. 성능 모니터링 추가
  5. 로그 분석 시스템 구축

기술 스택

  • 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 요약을 결합함으로써 답변 품질과 확장성을 모두 확보함.