🚩 주제 : AI 면접관 Agent 시스템 구축
- 중점 사항
1) LLM 기반 Text 인식, 요약 및 생성 활용
2) LangGraph를 활용한 AI Agent 구축
개요
1. 데이터 : 이력서/자기소개서 문서(.pdf, .docx)
2. LLM : OpenAI / GPT-4o mini (비용효율성 + 성능)
3. LangGraph : LangChain 기반 워크플로우 설계
✅ 진행 과정
[사전 준비 단계]
0. 파일을 입력받는 함수, State 선언
# pdf 또는 docx 파일 입력받기
def extract_text_from_file(file_path: str) -> str:
ext = os.path.splitext(file_path)[1].lower()
if ext == ".pdf":
doc = fitz.open(file_path)
text = "\n".join(page.get_text() for page in doc)
doc.close()
return text
elif ext == ".docx":
doc = Document(file_path)
return "\n".join(p.text for p in doc.paragraphs if p.text.strip())
else:
raise ValueError("지원하지 않는 파일 형식입니다. PDF 또는 DOCX만 허용됩니다.")
from typing import TypedDict, List, Dict
class InterviewState(TypedDict):
# 고정 정보
resume_text: str
resume_summary: str
resume_keywords: List[str]
question_strategy: Dict[str, Dict]
# 추가
keyword_weights: Dict[str, str]
triggers: List[str]
# 인터뷰 로그
current_question: str
current_answer: str
current_strategy: str
conversation: List[Dict[str, str]]
evaluation : List[Dict[str, str]]
next_step : str
# 추가
reflect_count: int
reflection_status: str
total_feedback: str
1. Resume 분석
- [고도화] 키워드별 중요도와 이력서 트리거 포인트 기능 추가
- 처음 도입 질문 생성 시, 추가한 속성을 반영하여 생성되도록
class ResumeInfo(BaseModel):
summary: str
keywords: List[str]
keyword_weights: Dict[str, str] # {"답러닝" : "높음", "영상처리": "보통"}
triggers: List[str] # {"단점", "2개월 단기 근무"}
def analyze_resume(state: InterviewState) -> InterviewState:
# 여기에 코드를 완성합니다.
parser = PydanticOutputParser(pydantic_object=ResumeInfo)
prompt = ChatPromptTemplate.from_messages([
("system", """너는 유능한 인재를 선발하는 기업의 면접관이야.
다음 주어지는 이력서 데이터로 지원자에게 질문을 해야해.
다음과 같은 분석을 수행해줘:
1. 핵심 키워드 추출 : 지원자의 이력서와 자기소개서에서 핵심 키워드 5가지를 쉼표로 구분해서 추출해줘.
2. 키워드 중요도 분류 : 추출한 키워드를 '높음', '보통', '낮음' 세 단계로 중요도를 분류해줘.
3. 이력서 트리거 포인트 탐지 : 면접 시에 추가 설명을 요구하거나 의문을 유발할 수 있는 특이사항(예: 경력 공백, 단점, 짧은 근무, 중단된 활동 등)을 'trigger point'로 뽑아줘.
4. 전체 이력서 요약 : 위 내용을 바탕으로 지원자의 이력서와 자기소개서를 요약해줘.
응답 예시는 아래와 같아.
{{
"summary": "지원자는 전기전자공학을 전공하고, AI 영상처리 프로젝트와 관련된 다양한 경험을 보유하고 있다...",
"keywords": ["딥러닝", "OpenCV", "연합동아리", "인턴십", "캡스톤디자인"],
"keyword_weights": {{
"딥러닝": "높음",
"OpenCV": "높음",
"연합동아리": "보통",
"인턴십": "보통",
"캡스톤디자인": "낮음"
}},
"triggers": ["2개월 단기 인턴십", "중단된 팀 프로젝트"]
}}
"""),
("human", state["resume_text"]),
("system", "{format_instructions}")
])
messages = prompt.format_messages(format_instructions=parser.get_format_instructions())
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.5) # temperature 조정
response = llm.invoke(messages)
resume_info = parser.parse(response.content)
resume_summary = resume_info.summary
resume_keywords = resume_info.keywords
keyword_weights = resume_info.keyword_weights # 추가
triggers = resume_info.triggers # 추가
# return 코드는 제공합니다.
return {
**state,
"resume_summary": resume_summary,
"resume_keywords": resume_keywords,
"keyword_weights": resume_info.keyword_weights, # 추가
"triggers": resume_info.triggers # 추가
}
2. 질문 전략 수립
- 질문 분야, 질문 방향, 예시질문 생성
- 질문 분야 : 경력 및 경험, 동기 및 커뮤니케이션, 논리적 사고
- 중괄호 오류 해결함수 ~ 괄호 문제 처리 코드를 넣은 이유는,
{state['keyword_weights']}가 {"답러닝" : "높음", "영상처리": "보통"} 이런형식으로 저장되어 있는데
"human"에서 이력서 중요도를 전달할 때, str.format() 과정에서 {~~~}를 변수로 인식하려고 시도하면서 keyerror가 난 것으로 파악된다. - ChatPromptTemplate.from_messages() 내부에서는 str.format()이 사용되는데, 그 과정에서 입력 문자열에 중괄호가 포함되어 있어 키 오류가 발생한 것. 예를 들어, 만약 이력서 요약의 값이 "AI 연구소 인턴 경험 {딥러닝 모델 개발}"이면 {딥러닝 모델 개발}을 변수로 인식하려고 해서 KeyError가 발생하는 것.
class Strategy(BaseModel):
questions: Dict[str, Dict]
def generate_question_strategy(state: InterviewState) -> InterviewState:
# 여기에 코드를 완성합니다.
parser = PydanticOutputParser(pydantic_object=Strategy)
# 중괄호 오류 해결 함수
def escape_braces(s: str) -> str:
return s.replace("{", "{{").replace("}", "}}")
# 키워드 중요도를 사람이 읽기 좋은 문자열로 변환
keyword_weights_str = ", ".join([f"{k}: {v}" for k, v in state['keyword_weights'].items()])
# 괄호 문제 처리
keyword_weights = escape_braces(keyword_weights_str)
prompt = ChatPromptTemplate.from_messages([
("system", """너는 유능한 인재를 선발하는 기업의 면접관이야.
다음 주어지는 이력서&자기소개서 요약과 주요 키워드, 키워드 중요도를 바탕으로 면접자에게 질문을 해야 해.
이러한 점을 주의하여 다음 3가지 분야에 대해 질문 방향과 예시 질문을 만들어줘.
3가지 분야는 다음과 같아.
1. 경력 및 경험
2. 동기 및 커뮤니케이션
3. 논리적 사고.
응답에 대한 예시는 다음과 같아.
{{
"경력 및 경험": {{
"질문 방향": "지원자의 전기정보 공학 배경과 관련된 실무 경험을 파악하고, 그에 따른 직무 적합성을 평가 할 수 있는 질문들.",
"예시 질문": [
"전기정보공학을 전공하며 습득한 지식이 머신러닝 및 데이터 마이닝 프로젝트에 어떻게 활용되었는지 구체적으로 설명해 주실 수 있나요?",
"빅데이터 학생연합에서 기술부장으로 활동하면서 겪은 도전과 이를 통해 배운 점은 무엇인가요?"
]
}}
}}
위 예시를 보고 포맷에 맞춰 응답해줘.
"""),
("human", f"""이력서 요약: {state['resume_summary']},
이력서 키워드: {state['resume_keywords']},
이력서 중요도 : {keyword_weights}"""), # 수정
("system", "{format_instructions}")
])
messages = prompt.format_messages(format_instructions=parser.get_format_instructions())
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.5)
response = llm.invoke(messages)
strategy = parser.parse(response.content)
strategy_dict = strategy.questions
# return 코드는 제공합니다.
return {
**state,
"question_strategy": strategy_dict
}
3. 위의 기능들 하나로 묶기
- 첫 질문 생성 시, 질문 분야와 해당 분야의 예시질문 모두 랜덤으로 선택되게
def select_random_question_with_category(question_strategy:Dict[str, Dict]):
all_example_questions=[]
for category, strategy in question_strategy.items():
questions = strategy.get("예시 질문", [])
for question in questions:
all_example_questions.append((category, question)) # (카테고리, 질문)
return random.choice(all_example_questions) if all_example_questions else (None, None)
def preProcessing_Interview(file_path: str) -> InterviewState:
resume_text = extract_text_from_file(file_path)
state: InterviewState = {
"resume_text": resume_text,
"resume_summary": '',
"resume_keywords": [],
"question_strategy": {},
# 추가
"keyword_weights": {},
"triggers": [],
"current_question": '',
"current_answer": '',
"current_strategy": '',
"conversation": [],
"evaluation": [],
"next_step" : '',
# 추가
"reflect_count": 0,
"reflection_status": '',
"total_feedback": '',
}
state = analyze_resume(state)
state = generate_question_strategy(state)
current_strategy, selected_question = select_random_question_with_category(state['question_strategy'])
# return 코드는 제공합니다.
return {
**state,
"current_question": selected_question,
"current_strategy": current_strategy,
}
✅ 면접 Agent 단계
0. 답변 입력 함수
def update_current_answer(state: InterviewState, user_answer: str) -> InterviewState:
return {
**state,
"current_answer": user_answer.strip()
}
1. 답변 평가 : 면접자의 답변이 적절한지 평가
- [고도화] '답변의 구체성' 평가 기준 추가
- 분기 판단을 위한 reflect_count 연산을 evaluate_answer(노드함수)에 적기 : state 변경은 일반 노드함수에서만 가능
- 분기함수에서 처리하면 state 변경이 적용되지 않아 무한루프에 빠질 수 있음
- 분기함수는 입력받은 상태를 변형하지 않고 반환값만 사용하는 것이 중요
class Evaluation(BaseModel):
content: Dict[str, str]
def evaluate_answer(state: InterviewState) -> InterviewState:
# 여기에 코드를 완성합니다.
parser = PydanticOutputParser(pydantic_object=Evaluation)
prompt = ChatPromptTemplate.from_messages([
("system",
"""너는 기업의 면접관이야.
다음 주어지는 질문과 질문에대한 관련 분야와 그에 대한 답변에 평가를 해줘.
평가의 기준은 다음과 같아.
1. 질문과의 관련성
상(우수): 질문의 핵심 의도에 정확히 부합하며, 전반적인 내용을 명확히 다룸.
중(보통): 질문과 관련은 있지만 핵심 포인트가 부분적으로 누락됨.
하(미흡): 질문과 관련이 약하거나 엉뚱한 내용중심.
2. 답변의 구체성
상(우수): 질문에 대한 답변이 대부분 구체적으로 다루어짐.
중(보통): 질문에 대한 답변이 일부 구체적이나 핵심적인 구체성이 떨어짐.
하(미흡): 질문에 대한 답변이 대부분 구체적이지 못함.
응답에 대한 예시는 다음과 같아.
{{
"분야": "경력 및 경험",
"질문": "전기정보공학을 전공하며 습득한 지식이 머신러닝 및 데이터 마이닝 프로젝트에 어떻게 활용되었는지 구체적으로 설명해 주실 수 있나요?",
"답변": "잘 모르겠습니다",
"질문과의 관련성": "하(미흡)",
"답변의 구체성": "하(미흡)",
"평가종합": "하(미흡)",
"평가에 대한 이유": "지원자의 답변은 질문의 핵심 의도인 전공 지식의 실무 적용 사례와 전혀 연결되지 않았으며, 내용 또한 단편적이고 구체적인 예시나 설명이 부족해 질문에 대한 명확한 이해와 준비가 부족한 것으로 판단됨."
}}
위 예시를 보고 포맷에 맞춰 응답해줘. 특히 평가에 대한 이유 부분은 질문에 대한 답변이 어느 부분에서 어떻게 부족했는지 300자 내외로 상세히 작성해줘.
"""),
("human", f"분야: {state['current_strategy']}, 질문: {state['current_question']}, 답변: {state['current_answer']}"),
("system", "{format_instructions}")
])
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.5)
messages = prompt.format_messages(format_instructions=parser.get_format_instructions())
response = llm.invoke(messages)
evaluation = parser.parse(response.content).content
if len(state["conversation"]) > 0:
prev_evaluation = state["conversation"][-1]
if evaluation["질문"] == prev_evaluation["질문"] and evaluation["답변"] == prev_evaluation["답변"]:
state["conversation"].pop()
state['reflect_count'] += 1
else:
state['reflect_count'] = 0
state["conversation"].append(evaluation)
# return 코드는 제공합니다.
return {
**state,
"evaluation": [evaluation]
}
def reflect_evaluation(state: InterviewState) -> Literal["decide_next_step", "evaluate_answer"]:
if state['reflect_count'] > 2:
state['reflect_count'] = 0
return "decide_next_step"
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
latest_eval = state["evaluation"][0]
reflection_prompt = ChatPromptTemplate.from_messages([
("system", """너는 기업 면접의 평가 보조 시스템이야.
다음은 면접 질문, 지원자의 답변, 그리고 그것에 대한 평가와 평가에 대한 이유야.
이 평가가 적절한지 판단해줘. 만약 평가가 적절하지 않다면 '재평가 필요'를 반환하고, 적절하면 '정상'을 반환해.
판단 기준:
- 평가가 질문과 답변의 내용을 제대로 반영했는가?
- 평가가 너무 과하거나 부족하지 않은가?
출력: "정상" 또는 "재평가 필요"
"""),
("human", f"질문: {state['current_question']}\n답변: {state['current_answer']}\n평가: {latest_eval['평가종합']}\n평가이유: {latest_eval['평가에 대한 이유']}"),
])
messages = reflection_prompt.format_messages()
response = llm.invoke(messages)
reflection_status = response.content.strip()
state['reflection_status'] = reflection_status
return "decide_next_step" if reflection_status == "정상" else "evaluate_answer"
2. 인터뷰 진행 검토 : 인터뷰가 길어지지 않도록 진행 제한
- 전략 3개(경력 및 경험, 동기 및 커뮤니케이션, 논리적 사고)를 모두 커버했는지 확인
- 5개 이상의 질문, 답변 진행 시 종료
def decide_next_step(state: InterviewState) -> InterviewState:
next_step = state["next_step"]
# (1) 전략 3개를 모두 커버했는지 확인
strategy_covered = set(turn.get("분야") for turn in state["conversation"])
all_strategies = {"경력 및 경험", "동기 및 커뮤니케이션", "논리적 사고"}
# 5개 이상 질문,답변 진행 시 종료
if len(state["conversation"]) >= 5:
next_step = "end"
else:
last_eval = state["conversation"][-1] if state["conversation"] else {}
# 평가 값 상, 중, 하
relevance = last_eval.get("질문과의 관련성", "")
specificity = last_eval.get("답변의 구체성", "")
# 평가가 모두 "중" 이상이면 다음 전략으로 전환
if relevance in ["중(보통)", "상(우수)"] and specificity in ["중(보통)", "상(우수)"]:
next_step = "next_strategy"
else:
next_step = "additional_question"
return {
**state,
"next_step": next_step
}
3. 질문 생성
- 기존 전략 예시 질문과 최근 답변을 기반으로 심화질문 생
- [고도화] Chroma Vector DB를 생성하여 LLM프롬프트에 질문 생성 참고 정보로 포함
- 예시 질문들이 저장되어 있는 Vector DB
- 질문 생성 시 참고만. 그대로 사용X
def generate_question(state: InterviewState) -> InterviewState:
next_step = state['next_step']
current_strategy = state['current_strategy']
# 여기에 코드를 완성합니다.
question_strategy = '\n'.join([
f"""
{x}:
- 질문 방향: {state['question_strategy'][x]['질문 방향']}
- 예시 질문: {", ".join(state['question_strategy'][x]['예시 질문'])}
"""
for x in ['경력 및 경험', '동기 및 커뮤니케이션', '논리적 사고']
])
conversation = '\n\n'.join([
f"""분야: {x['분야']},
질문: {x['질문']}
답변: {x['답변']}
질문과의 관련성: {x['질문과의 관련성']}
답변의 구체성: {x['답변의 구체성']},
평가종합: {x['평가종합']},
평가에 대한 이유: {x['평가에 대한 이유']}
"""
for x in state["conversation"]
])
# vector db 기반 유사 질문 검색
# strategy_focus = state["current_strategy"]
if next_step == "next_strategy":
all_strategies = {"경력 및 경험", "동기 및 커뮤니케이션", "논리적 사고"}
used_strategies = set(turn.get("분야") for turn in state["conversation"])
remaining_strategies = list(all_strategies - used_strategies)
if remaining_strategies:
strategy_focus = remaining_strategies[0]
else:
strategy_focus = "경력 및 경험"
else:
strategy_focus = state["current_strategy"]
keywords = state["resume_keywords"]
query = f"{strategy_focus}, {keywords}"
retrieved = vectordb.similarity_search(query, k=2)
reference_questions = '\n'.join([f"- {doc.page_content.strip()}" for doc in retrieved])
# prompt 설정
if next_step == "next_strategy":
prompt_text = f"""다음 주어지는 면접자의 이력서 요약, 키워드, 질문전략, 이전 질문과 답변, 평가를 기반으로
지원자의 '{strategy_focus}' 분야에 대한 새로운 질문을 생성해줘. 이때 '{state['triggers']}' 중 해당 분야와 관련이 있는 키워드가 있다면 해당 키워드를 활용해 질문이 만들어줘.
이전 질문과 중복되지 않으면서 지원자의 역량을 효과적으로 평가할 수 있는 질문이어야 해
유사 질문 예시는 그대로 쓰지 말고, 참고만 해서 새로운 질문을 만들어줘.
[출력 예시 포맷]
AI 연구소에서 OCR 기반 문서 처리 시스템을 고도화하는 과정에서, 특정 기술이나 알고리즘을 선택한 이유와 그 선택이 프로젝트에 미친 영향을 설명해 주실 수 있나요?
"""
else:
prompt_text = f"""다음 주어지는 면접자의 이력서 요약, 키워드, 질문전략, 이전 질문과 답변, 평가를 기반으로
지원자의 가장 최근 답변{state['current_answer']}에 대한 '{strategy_focus}' 분야의 새로운 질문을 생성해줘
지원자가 언급한 내용에 대해 더 깊이 있는 설명이나 구체적인 사례를 요구하는 질문이어야 해
지원자의 이전 질문에 대한 평가가 좋지 못하다면, 같은 주제를 다른 각도에서 접근하는 질문을 생성해줘
[출력 예시 포맷]
딥러닝 모델을 활용한 프로젝트에서 데이터 전처리 과정에서 발생한 문제를 어떻게 분석하고 해결하였는지 설명해 주실 수 있나요?
"""
prompt = ChatPromptTemplate.from_messages([
("system", prompt_text),
("human", f"""
이력서 요약: {state['resume_summary']},
키워드: {state['resume_keywords']},
질문전략: {question_strategy},
이전 질문과 평가: {conversation},
유사 질문 예시: {reference_questions}
"""),
])
messages = prompt.format_messages()
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
response = llm.invoke(messages)
# return 코드는 제공합니다.
if next_step == "next_strategy":
return {
**state,
"current_question": response.content.strip(),
"current_answer": "",
"current_strategy": strategy_focus
}
else:
return {
**state,
"current_question": response.content.strip(),
"current_answer": ""
}
4. 인터뷰 피드백 보고서
- 각 질문답변에 대한 피드백
- [고도화] 종합 피드백
class FeedbackInfo(BaseModel):
feedback: str = Field(..., description="면접 질문과 답변에 대한 3~5줄의 평가 피드백")
def generate_feedback_paragraph(question: str, answer: str, eval_result: str, eval_reason: str) -> str:
"""LLM을 이용해 면접 피드백 문단 생성 (LangChain 스타일)"""
# Pydantic 출력 파서
parser = PydanticOutputParser(pydantic_object=FeedbackInfo)
# 프롬프트 정의
prompt = ChatPromptTemplate.from_messages([
("system", """너는 AI 면접관이야.
질문과 답변, 그리고 다른 면접관이 판단한 종합 평가 결과를 보고, 평가자의 입장에서 3~5줄로 된 구체적인 평가 피드백을 작성해줘.
답변의 논리성, 구체성, 경험에 기반한 설명, 개선할 부분 등을 모두 고려해줘."""),
("human", "질문: {question}\n답변: {answer}\n평가: {eval_result}\n평가이유: {eval_reason}"),
("system", "{format_instructions}")
])
# 메시지 포맷팅
messages = prompt.format_messages(
question=question,
answer=answer,
eval_result=eval_result,
eval_reason=eval_reason,
format_instructions=parser.get_format_instructions()
)
# LLM 호출
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.5)
response = llm.invoke(messages)
# 결과 파싱
result = parser.parse(response.content)
return result.feedback
class TotalFeedbackInfo(BaseModel):
feedback: str = Field(..., description="면접 질문과 답변, 피드백에 대한 3~5줄의 종합 평가 피드백")
def summarize_interview(state: InterviewState) -> InterviewState:
# 여기에 코드를 완성합니다.
print('\n[summary]')
print("-" * 100)
for evaluate in state["conversation"]:
question = evaluate.get("질문", "")
answer = evaluate.get("답변", "")
eval_result = evaluate.get("평가종합", "")
eval_reason = evaluate.get("평가에 대한 이유", "")
# LLM 기반 평가 피드백 생성
feedback = generate_feedback_paragraph(question, answer, eval_result, eval_reason)
evaluate["상세피드백"] = feedback
# 출력
print(f"질문: {question}")
print(f"답변: {answer}")
print(f"피드백:\n{feedback}")
print("-" * 100)
parser = PydanticOutputParser(pydantic_object=TotalFeedbackInfo)
feedback_results = '\n'.join(
[f'질문: {evaluate["질문"]}\n답변: {evaluate["답변"]}\n피드백: {evaluate["상세피드백"]}'
for evaluate in state['conversation']]
)
prompt = ChatPromptTemplate.from_messages([
("system", """너는 AI 면접관이야. 질문, 답변에 대한 피드백을 보고, 평가자의 입장에서 3~5줄로 된 구체적인 종합 평가 피드백을 작성해줘.
답변의 논리성, 구체성, 경험에 기반한 설명, 개선할 부분 등을 모두 고려해줘."""),
("human", "{feedback_results}"),
("system", "{format_instructions}")
])
# 메시지 포맷팅
messages = prompt.format_messages(
feedback_results=feedback_results,
format_instructions=parser.get_format_instructions()
)
# LLM 호출
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.5)
response = llm.invoke(messages)
state['total_feedback'] = parser.parse(response.content).feedback
print(f"종합 피드백: {state['total_feedback']}")
# return 코드는 제공합니다.
return state
5. 면접 Agent
- LangGraph 연결
# 분기 판단 함수
def route_next(state: InterviewState) -> Literal["generate", "summarize"]:
if state['next_step'] == "end":
return "summarize"
else:
return "generate"
# 그래프 정의 시작
builder = StateGraph(InterviewState)
# 노드 추가
builder.add_node('evaluate_answer', evaluate_answer)
builder.add_node('decide_next_step', decide_next_step)
builder.add_node('generate', generate_question)
builder.add_node('summarize', summarize_interview)
# 노드 연결
builder.add_edge(START, 'evaluate_answer')
builder.add_conditional_edges('evaluate_answer', reflect_evaluation)
builder.add_conditional_edges('decide_next_step', route_next)
builder.add_edge('generate', END)
builder.add_edge('summarize', END)
# 컴파일
graph = builder.compile()
## 그래프 생성기
from IPython.display import Image, display
# 외부 서버를 호출하는 동안 대기시간 초과로 오류가 날 수 있습니다.
try:
display(Image(graph.get_graph().draw_mermaid_png()))
except Exception:
print(graph.get_graph().draw_mermaid())
print('https://mermaid.live 에 복붙')
pass
✅ 시스템 실행
1. 인터뷰 사전 준비
# 파일 입력
file_path = path + 'Resume_sample.pdf'
state = preProcessing_Interview(file_path)
state
2. Agent 실행
# 사용자 응답 루프
while True:
print("\n[질문]")
print(state["current_question"])
state["current_answer"] = input("\n[답변 입력]:\n")
# 그래프 실행: 평가 → 판단 → 다음 질문 생성 or 종료
state = graph.invoke(state)
if state["next_step"] == "end":
break
'KT aivleschool' 카테고리의 다른 글
3차 미니프로젝트 : 클라우드 기본 인프라 구현 (2) | 2025.05.23 |
---|---|
[5주차] RAG (2) | 2025.05.22 |
[5주차] LangChain (2) (0) | 2025.05.18 |
[5주차] LangChain (1) (0) | 2025.05.18 |
1차 미니프로젝트 : 스마트폰 센서 데이터 기반 모델 분류 (0) | 2025.05.18 |