제목에 표기된 기간동안 미니프로젝트를 수행했다.
크리스마스 연휴, 1월 1일 연휴까지 있어 시간은 조금 더 넉넉했지만
첫 프로젝트다 보니 시작부터 많은 에너지를 쓰게 된것같다.
이번 게시물로 기간동안 작업 내용을 총정리하고, 프로젝트의 복기까지 진행하려 한다.
미니 프로젝트 주제
미니 프로젝트 주제
- 개발자 생산성 AI (에러 로그 & 코드 분석 Agent)
- 서버 에러 로그와 코드 스니펫을 분석하여 필요 시 코드 컨텍스트를 함께 분석하여 디버깅 의사결정을 도우며 원인·해결책·재발 방지 가이드를 제공하는 AI 개발자 생산성 도구
- 단순한 에러 위치 탐지뿐 아니라 부가적으로 코드 로직 흐름, 잘못된 패턴 탐지의 부수적 기능도 수행
- 기능 예시
- 에러 로그 → 원인 분석
- 예상 발생 위치
- 초보자/시니어 설명 모드
- 해결 순서 제안
- 주제 선정 이유
- 결과물을 기술적으로 명확하게 설명하기에 적합하다.
- 결과가 정성적 데이터(좋다,나쁘다)아닌 명확하게 0과 1로 나온다.
- AI 활용도를 직관적으로 높이기 위하여 AI가 도움을 줄 수 있고, 실제로도 도움이 되는 주제
- 최종 포트폴리오에도 개발적인 개연성을 부여하여 포트폴리오의 질을 높이기 위하여 일반적인 도메인보다 개발에 관련된 주제를 선정하였음
- 실제로도 유용할 수 있는 주제를 선정하여 과제에 필요성 부여, 목표 달성 시 성취감 향상
- 결과물을 기술적으로 명확하게 설명하기에 적합하다.
프로젝트 실제 효과
개발자 관점
- 디버깅 시간 단축
- 초보자 진입장벽 감소
- 반복 에러 학습 비용 감소
회사 관점
- 개발 생산성 향상
- 장애 대응 속도 개선
- 신규 인력 온보딩 가속
기술적 어필
- LLM을 도구처럼 사용
- 프롬프트 → 구조화 출력
- API 중심 설계
- 확장 가능한 아키텍처
실무 어필 문장 (예시)
- “단순한 챗봇이 아니라, 에러 로그 분석이라는 실제 개발 문제를 해결하는 AI 에이전트를 설계했습니다.”
내가 준비해 간 미니프로젝트 주제였다.
주제 선정의 첫 째로는 내가 파이썬sql 공부하면서 너무 어렵고 검색하기도 힘들어서 기존에 관심없는 내용보다는
내가 느낀 점을 주제로 담는것이 가장 당위성 있다고 생각하였다.
두번째로는 이 프로젝트를 학생의 관점에서 ' 프로젝트를 위한 프로젝트' 가 아니라 정말 실무자의 관점으로 생각하고 실생활에 도움이 될 수 있는 프로젝트로 진행하여 추후 포트폴리오의 당위성 또한 부여하고 싶었다.
그렇게 다른 분이 선정한 주제와 내 주제, 두개를 가지고 두 팀으로 나눠서 프로젝트를 진행하게 되었다.
12.26 : 일정표 정리







프로젝트 데이터 흐름
- 소스 코드 변경 사항 푸시 (GitHub)
- 기능 브랜치에서 작업한 코드를 GitHub 저장소에 푸시합니다.
- CI/CD 파이프라인 실행 (GitHub Actions)
- 코드 푸시를 트리거로 자동 빌드 및 테스트가 수행됩니다.
- Docker 이미지를 생성하고 배포 단계가 실행됩니다.
- EC2 환경에 Docker Compose 기반 서비스 배포
- EC2 인스턴스에서 docker-compose를 통해 FastAPI 백엔드와 Streamlit 프론트엔드 컨테이너가 실행됩니다.
- 사용자 요청 입력 (Streamlit UI)
- 사용자가 에러 로그 또는 코드 스니펫을 Streamlit 웹 인터페이스를 통해 입력합니다.
- 백엔드 API 요청 전달 (Streamlit → FastAPI)
- Streamlit이 분석 요청을 FastAPI 엔드포인트(/analyze/log, /analyze/code, /analyze/combined)로 전송합니다.
- 입력 데이터 처리 및 분석 로직 호출
- FastAPI가 입력 데이터를 검증한 뒤, Agent 분석 로직을 호출합니다.
- RAG 기반 유사 사례 검색
- 사전에 구축된 벡터 데이터베이스(에러 로그 및 코드 패턴)를 조회하여 유사한 사례를 검색합니다.
- LLM 호출을 통한 분석 결과 생성
- 검색된 컨텍스트와 입력 데이터를 기반으로 LLM(Claude 또는 Google 모델)을 호출하여 분석 결과를 생성합니다.
- 분석 결과 수신 및 응답 구성
- LLM 응답을 구조화된 JSON 형태로 파싱하고, FastAPI 응답으로 구성합니다.
- 분석 결과 반환 및 UI 출력
- FastAPI 응답을 Streamlit으로 전달하고, 프론트엔드에서 분석 결과를 시각적으로 출력합니다.
Day 1 ~ Day 4 / 일별 업무 목표
Day1: 설계 확정(스키마/흐름/환경/구조)로그-only MVP 완성(연동까지)
- 프로젝트 전체 정리, 흐름 설명, 최소 작동 모델 구축(로그 ONLY 모델)
- AI
- 로그 입력 기반 LLM 작동 함수 구현(로그 ONLY)
- 분석결과 json으로 맞게 오는지 확인
- FULLSTACK
- Streamlit + fastAPI 연동 구축
- fastapi 엔드포인트 구현 (/analyze/log) 등
- 로그 입력 시 최소 작동 모델 작동 확인 (streamlit 출력)
- DEVOPS
- 깃허브 저장소, 브랜치 만들기(백엔드,프론트엔드,ai 작업할 브랜치)
- .env / .yml 파일 세팅 / .gitignore / pip requiremets.txt 등 기본 세팅
- AI
Day2: 코드/결합 분석 + RAG + 인스턴스 생성
- 로그 + 코드 분석 추가, RAG 기반 검색 기능 추가
- 최소 작동 모델 DOCKER EC2 배포 확인
- AI
- 로그 + 코드 결합 분석 함수 구현
- RAG용 에러 사례 데이터 생성 (특정 회사의 컨셉도 적용할지 이 때 결정)
- FULLSTACK
- 코드 입력 UI, 로그 입력 UI 추가
- UI 업그레이드
- DEVOPS
- AWS EC2 인스턴스 생성
- DOCKER-COMPOSE 서비스 배포 완성 후 작동 확인
- AI
Day3: 안정화(품질·UX·CI/CD·문서)
- UI 업그레이드, 응답 시나리오, 프롬프트 정립
- AI
- 프롬프트 응답 안정화
- LLM 호출 전 RAG 결과 응답에 포함 시키기 (LLM을 사용할수 없는 환경 고려)
- FULLSTACK
- FASTAPI 예외처리 추가( LLM호출 실패, 타임아웃 등 오류 메세지 설정)
- STREAMLIT 예외처리 추가(사용자 빈값 입력)
- 주니어 / 시니어 모드 선택 가능한 UI 구축
- DEVOPS
- GIT ACTION CICD 까지 완성
- AI
Day4: 발표/시연/README 마감
- 로그, 에러 발생 시와 그의 해답을 사이트에 업로드 할 수 있는 ui와 기능도 있으면 좋겠다
일정은 노션과 ai를 사용해서 이렇게 정리하게 되었다.
일정을 정하는게 제일 어려웠던 것 같다.
1. 내가 진행하는 프로젝트이니 이 목표를 수행하기 위해서 모든 기능들의 흐름을 알고 순서대로 일정을 조율해야 한다.
2. 각 파트별로 붙는 부분을 정하고 그에 맞게 일정을 정리해야 한다.
3. 그냥 내가 다 알아야 한다 그래야 프로젝트를 하지
4. 이쁜 템플릿 필요한 내용만 있는 템플릿 찾기가 힘들다, 그래서 결국 내가 커스텀 했다.
먼저 프로젝트 데이터 흐름을 정리하고, 그를 위해 필요한 각 스택별로 수행해야 하는 업무를 리스트화 했다.
그 이후 리스트를 3일간의 일정으로 나눈 이후, 각 일별로 최소한으로 구현해야하는 목표치를 설정했다, 그래야 일정이 안 밀리고 끝낼수 있을거라고 생각했고 실제로 이는 효과적이었다.
오늘 일이 늘어져서 내일 오전쯤에 마무리 하고 끝낼수 있을거같으면? 그럼 내일 오전에 해야될 일 오후에 하면 내일 오후에 할 일은 그 다음날 오전? 이렇게하면 주말에도 해야된다.
물론 프로젝트 열심히 해야하니 주말에도 쉬지않고 하는게 바람직하지만, 일정이 늘어져서 주말에 작업하는것은 얘기가 다르지 않은가, 나는 일정 조율까지도 나의 능력으로 처리하고 싶었다.
실제로 우리는 팀장의 발표 준비를 제외하고는 연휴, 쉬는 날에는 작업을 하지 않았다.
이럻게 일정 조율을 마무리하고, 본격적 프로젝트에 들어갔다.
Day1: 설계 확정(스키마/흐름/환경/구조)로그-only MVP 완성(연동까지)
- AI
- 로그 입력 기반 LLM 작동 함수 구현(로그 ONLY)
- 분석결과 json으로 맞게 오는지 확인
- FULLSTACK
- Streamlit + fastAPI 연동 구축
- fastapi 엔드포인트 구현 (/analyze/log) 등
- 로그 입력 시 최소 작동 모델 작동 확인 (streamlit 출력)
- DEVOPS
- 깃허브 저장소, 브랜치 만들기(백엔드,프론트엔드,ai 작업할 브랜치)
- .env / .yml 파일 세팅 / .gitignore / pip requiremets.txt 등 기본 세팅
Day1의 목표는 최소 작동 모델까지 구축하는것이었다. 이것만 완성되면 일단 프로젝트는 끝나는거다 ㅋㅋ 배포 빼고
먼저 우리는 실무 환경에 익숙해지기 위해서 슬랙을 통해서 소통과 정보 교환 또한 진행하였다.

슬랙은 같이 수업하는분이 디스코드같다고 하던데 옆의 채널 부분에서 대화 내용, 주제에 맞춰서 채널을 나눠서 대화할 수 있다.
그래서 나는
일별 업무 보고 : 그 날 해야 할 일을 오전에 정리, 오후에 한번 더 확인, 집 가기전에 다음날 일정 정리
파일 : md 파일, 이미지, 등 파일 주고받는 채널
환경설정 : env 키 등 유출되면 안되는 정보
미니프로젝트 : 잡다한 llm에게서 온 답변, 결과물 이미지 캡쳐 등 잡다한 내용 보내는 메인 공간
으로 나누어서 대화하였고 대화내용이 분산되지만 깔끔히 정리되어 조금 편했다.
그리고 각자의 메시지에 댓글을 달면서 정보를 첨부할수 있어서 그 또한 유용했다.
소통 툴의 중요성 또한 깨달았다.
ai파트에서는 모델을 호출하는 함수를 구현하여 (우리는 claude를 사용했다) 답변을 받는것,
풀스택에서는 프론트와 백엔드의 구현, 그리고 이후 ai파트와 연동하여 모델이 프론트까지 답변을 출력하는것
cicd 파트는 그 전에 깃허브, 브랜치, 트리구조, env, yml 파일 세팅등이 우선되었어야 하는데 이게 우리의 문제였다 (추후 문제가 된다)
cicd 파트가 가장 선행되고, 그 이후에 작업을 진행했어야 했다, 우리는 이 사실을 마지막날 직전에 깨닫는다 ㅋㅋ


우리는 기존 실습때 진행했던 streamlit에서 더욱 나아가서 ui또한 신경쓰고, 사용자 경험을 위해 개선했다는 포인트 또한 가져가고 싶었다.
그래서 생각하는 이미지를 llm에 넣어서 예시로 받고, 실제 구현까지 해내는데 성공하였다. 위 이미지는 최소작동모델의 이미지다.
기존 ai에 더해서 에러 로그의 원인, 해결법, 추후 재발 방지를 위한 가이드라인까지 제공받는데 성공하였다.
이제 일단 미니프로젝트는 끝난거다, 이 다음부터는 디벨롭의 시간이다.
Day2: 코드/결합 분석 + RAG + 인스턴스 생성
- 로그 + 코드 분석 추가, RAG 기반 검색 기능 추가
- 최소 작동 모델 DOCKER EC2 배포 확인
- AI
- 로그 + 코드 결합 분석 함수 구현
- RAG용 에러 사례 데이터 생성 (특정 회사의 컨셉도 적용할지 이 때 결정)
- FULLSTACK
- 코드 입력 UI, 로그 입력 UI 추가
- UI 업그레이드
- DEVOPS
- AWS EC2 인스턴스 생성
- DOCKER-COMPOSE 서비스 배포 완성 후 작동 확인
- AI
둘쨰날의 목표는 프롬프트 엔지니어링을 통해서 주니어 모드, 시니어 모드, 로그 입력, 코드 입력, 로그 + 코드의 입력을 각각 다르게 받는 프롬프트 엔지니어링을 진행하고, rag 툴을 사용하기 위한 벡터디비 구축과 연결, rag 도구 사용 랭그래프 구축까지다.
그리고 배포까지 진행하길 원했지만 이날도 배포는 되지 않았다.
prompt.py
# prompts.py
# 공통 규칙: 모든 프롬프트에 포함될 JSON 출력 가이드
JSON_FORMAT_INSTRUCTION = """
반드시 아래 JSON 형식으로만 응답하며, 키값은 반드시 영어여야 함:
{
"cause": "원인 설명",
"solution": "해결 방법",
"prevention": "재발 방지책"
}
다른 텍스트나 Markdown 기호(##, ```json 등)를 포함하지 마시오.
"""
PROMPTS = {
# --- 주니어 모드: 친절함, 개념 설명, 단계별 가이드 ---
("junior", "log"): f"""
당신은 초보 개발자를 위한 친절한 멘토입니다.
에러 로그를 분석하여 왜 이런 에러가 발생했는지 쉬운 용어로 설명하세요.
{JSON_FORMAT_INSTRUCTION}
""",
("junior", "code"): f"""
당신은 코드 리뷰를 해주는 친절한 선배입니다.
코드의 로직 결함을 찾아주고, 초보자가 이해하기 쉽게 수정 방향을 제시하세요.
{JSON_FORMAT_INSTRUCTION}
""",
("junior", "log_code"): f"""
당신은 초보자를 위한 종합 에러 컨설턴트입니다.
로그와 코드를 대조하여 에러의 근본 원인을 교육적인 관점에서 상세히 설명하세요.
{JSON_FORMAT_INSTRUCTION}
""",
# --- 시니어 모드: 핵심 위주, 기술적 깊이, Best Practice ---
("senior", "log"): f"""
당신은 숙련된 시니어 엔지니어입니다.
불필요한 설명은 생략하고, Stack Trace를 바탕으로 에러의 기술적 핵심 원인을 즉시 파악하여 보고하세요.
{JSON_FORMAT_INSTRUCTION}
""",
("senior", "code"): f"""
당신은 아키텍트 급 개발자입니다.
코드의 안티 패턴, 성능 이슈, 보안 취약점을 중심으로 비판적으로 분석하세요.
{JSON_FORMAT_INSTRUCTION}
""",
("senior", "log_code"): f"""
당신은 테크 리드입니다.
로그상의 에러 지점과 코드의 상관관계를 분석하여, 시스템 전체 관점에서 최적의 해결책과 코드 최적화 방안을 제시하세요.
{JSON_FORMAT_INSTRUCTION}
"""
}
1. llm의 답변이 json으로 가야 프론트와 백, 랭그래프 노드를 오고갈때 문제가 없는데 답변이 정의되지 않아서 오류가 나왔다.
그래서 json으로 답변을 강제시키는 기능을 사용했고
2. 주니어 / 시니어, 로그 / 코드 / 로그 + 코드 로 6개의 프롬프트를 구성하였다.


결과 : 주니어 시니어 답변이 다르게 나오는 모습을 확인했다, 나름 만족스럽지만 아직 개선점이 있다.
토큰의 문제가 일단 있어서 세 부분의 토큰 할당 수를 어느정도 맞추는 작업이 필요했다.
그래서 그 부분을 풀스택에서 예외처리와 함께 맞춰주고, 나는 rag와 백터디비 작업에 들어갔다.
풀스택이 아무래도 오고가고를 담당하다보니 예외처리나 답변 정의 등의 작업은 풀스택 파트에서 진행하게 되더라.
우리는 백터디비를 파인콘을 사용했다.
1. rag에 최적화된 db : 할루시네이션 줄이기로 안정적인 top-k 선택
2. 자동 스케일링으로 서버 관리에 최족
3, langchain, langgraph 궁합이 좋음
목적에 맞는 기술을 사용하는게 맞다고 생각했다.
그래서 파인콘에 가입해서 인덱스를 만들고, 코드상에서 로컬에 있는 데이터를 파인콘으로 임베딩 후, 벡터화하여 디비에 업로드하는 코드를 작성하였다.
rag_store.py
from langchain_community.vectorstores import FAISS
from langchain_aws import BedrockEmbeddings
import boto3
import os, glob, hashlib
from dotenv import load_dotenv
from langchain_text_splitters import RecursiveCharacterTextSplitter
from pinecone import Pinecone
load_dotenv()
# 1) Embedder
embedder = BedrockEmbeddings(model_id=os.getenv('BEDROCK_EMBEDDING_MODEL_ID'),
region_name=os.getenv('AWS_REGION'),
aws_access_key_id=os.getenv("AWS_ACCESS_KEY_ID"),
aws_secret_access_key=os.getenv("AWS_SECRET_ACCESS_KEY")
)
# 2) Splitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=1500, # 대략 400~800 tokens 수준(경험치)
chunk_overlap=200
)
# 3) Pinecone init
pc = Pinecone(api_key=os.getenv("PINECONE_API_KEY"))
index = pc.Index(os.getenv("PINECONE_INDEX"))
namespace = os.getenv("PINECONE_NAMESPACE", "dev")
def load_md_docs(folder="data/kb_docs"):
paths = sorted(glob.glob(os.path.join(folder, "*.md")))
docs = []
for p in paths:
with open(p, "r", encoding="utf-8") as f:
docs.append((p, f.read()))
return docs
def make_id(source: str, chunk_index: int, text: str) -> str:
# 중복방지: 같은 내용이면 같은 id로 업서트(갱신)
h = hashlib.sha1(f"{source}|{chunk_index}|{text}".encode("utf-8")).hexdigest()
return h
def main():
docs = load_md_docs("data/kb_docs")
print(f"[load] docs={len(docs)}")
upserts = []
total_chunks = 0
for source, doc_text in docs:
chunks = splitter.split_text(doc_text)
total_chunks += len(chunks)
vectors = embedder.embed_documents(chunks) # List[List[float]]
for i, (chunk, vec) in enumerate(zip(chunks, vectors)):
_id = make_id(source, i, chunk)
metadata = {
"source": source,
"chunk_index": i,
# 너무 길면 잘라서 넣어(메타데이터 과다 방지)
"text": chunk[:1500],
"doc_type": "kb_md",
}
upserts.append((_id, vec, metadata))
# 너무 많이 쌓이면 배치 업서트
if len(upserts) >= 100:
index.upsert(vectors=upserts, namespace=namespace)
print(f"[upsert] +{len(upserts)}")
upserts.clear()
# 남은 것 업서트
if upserts:
index.upsert(vectors=upserts, namespace=namespace)
print(f"[upsert] +{len(upserts)}")
print(f"[done] total_chunks={total_chunks}")
if __name__ == "__main__":
main()
이 코드를 실행하면 경로상에 있는 데이터가 파인콘으로 올라간다.
그리고 rag 검색하는 함수또한 정의하고, 그를 랭그래프 코드에 라이브러리 형식으로 붙여넣었다.
tools.py
"""
tools.py
LangGraph Agent에서 사용하는 외부 Tool 정의 모듈.
이 파일은 LLM이 직접 호출할 수 있는 Tool과,
그 Tool이 내부적으로 사용하는 실제 구현 함수를 포함한다.
현재 포함된 Tool:
- rag_search:
Pinecone 벡터 DB를 사용해
에러 로그 / 코드 / 질문과 관련된 지식(KB)을 검색한다.
역할 분리 원칙:
- Agent(LLM)는 '언제 검색할지'만 판단한다.
- tools.py는 '어떻게 검색할지'만 책임진다.
"""
import os
from dotenv import load_dotenv
load_dotenv()
import sys
from langchain_aws import BedrockEmbeddings
from pinecone import Pinecone
from langchain_core.tools import tool
from typing import Optional
_embedder: Optional[BedrockEmbeddings] = None
_pinecone_index = None
_namespace: Optional[str] = None
def _require_env(name: str) -> str:
v = os.getenv(name)
if not v:
raise RuntimeError(
f"Missing required environment variable: {name}. "
f"Set it in your .env or CI secrets before using rag_search."
)
return v
def get_embedder() -> BedrockEmbeddings:
global _embedder
if _embedder is not None:
return _embedder
model_id = _require_env("BEDROCK_EMBEDDING_MODEL_ID")
region = _require_env("AWS_REGION")
_embedder = BedrockEmbeddings(
model_id=model_id,
region_name=region,
aws_access_key_id=os.getenv("AWS_ACCESS_KEY_ID"),
aws_secret_access_key=os.getenv("AWS_SECRET_ACCESS_KEY"),
)
return _embedder
def get_pinecone_index():
global _pinecone_index, _namespace
if _pinecone_index is not None:
return _pinecone_index
api_key = _require_env("PINECONE_API_KEY")
index_name = _require_env("PINECONE_INDEX")
_namespace = os.getenv("PINECONE_NAMESPACE", "dev")
pc = Pinecone(api_key=api_key)
_pinecone_index = pc.Index(index_name)
return _pinecone_index
def rag_search(query: str, top_k: int = 3) -> str:
"""
RAG 검색 도구
- query: 사용자 질문 또는 에러 시그니처
- return: LLM 프롬프트에 바로 넣을 수 있는 문자열
"""
embedder = get_embedder()
index = get_pinecone_index()
qvec = embedder.embed_query(query)
namespace = _namespace or "dev"
res = index.query(
vector=qvec,
top_k=top_k,
namespace=namespace,
include_metadata=True,
)
chunks = []
for m in res["matches"]:
md = m.get("metadata", {}) or {}
chunks.append(
f"- ({m['score']:.3f}) {md.get('source','?')}#{md.get('chunk_index','?')}\n"
f"{md.get('text','')}"
)
return "\n\n".join(chunks)
# =====================================================
# 2) LangGraph / Agent용 Tool 래퍼
# =====================================================
@tool("rag_search")
def rag_search_tool(query: str) -> str:
"""
에러 로그, 코드, 질문을 기반으로
Pinecone 벡터 DB에서 관련 트러블슈팅 지식을 검색한다.
"""
print("[TOOL CALLED] rag_search:", query[:80])
print("🛠️ TOOL ENTERED:", query, file=sys.stderr, flush=True)
return rag_search(query, top_k=5)
rag_Search 함수를 진행하는 tool이다. 추후 툴노드로서의 기능을 수행한다,
그리고 검색할때 rag 사용 지침으로 prompt.py에 추가해주었다
prompt.py
RAG 사용 규칙:
- 아래 중 하나라도 해당하면 rag_search를 사용하라.
1) 에러 원인/해결책에 확신이 없을 때
2) 특정 라이브러리/클라우드 서비스의 최신/정확한 설정 값이 필요할 때
3) 사용자 로그/코드만으로 근거가 부족할 때
- 반대로, 원인이 명확하고 일반적인 경우에는 rag_search 없이 답하라.
- rag_search를 사용했다면, 결과를 근거로 요약하고 최종 답변을 작성하라.
rag를 사용하기 위해서는 규칙을 명시해주어야 잘 사용한다.
이렇게 2일차의 목표인 rag + db 적용된 모델 만들기까지는 성공하였다.
하 참 아쉬은게 프로젝트할때 매일매일 이런 내용들을 캡처해서 이야기하고 공유하는게 참 중요하다는 생각을 이제야 하게 되었다.
만약에 이걸 보시는 여러분들도 프로젝트를 하신다면 매일매일 기록을 잘 나누길 바란다.
그리고 이를 만들어둔 llm 코드에 연결하면
agent.with.graph.py
from typing import Optional, Literal
from dotenv import load_dotenv
import os
from langgraph.graph import StateGraph, START, END, MessagesState
from langgraph.prebuilt import ToolNode, tools_condition
from langchain_core.messages import SystemMessage, HumanMessage, ToolMessage, AIMessage
from langchain_anthropic import ChatAnthropic
from dev.app.llm.prompts import PROMPTS
from dev.app.llm.tools import rag_search_tool
load_dotenv()
# LLM 설정
llm = ChatAnthropic(
model=os.getenv("ANTHROPIC_MODEL_ID"),
api_key=os.getenv("ANTHROPIC_API_KEY"),
temperature=0.4,
max_tokens=1500,
)
class AgentState(MessagesState):
persona: str
input_mode: str
log_text: str | None
code_text: str | None
# Tool 설정
tools = [rag_search_tool]
llm_with_tools = llm.bind_tools(tools)
tool_node = ToolNode(tools)
def build_user_prompt(mode: str, log_text: str, code_text: str) -> str:
# 텍스트가 있을 경우 양끝 공백을 먼저 제거합니다.
log_text = (log_text or "").strip()
code_text = (code_text or "").strip()
if mode == "log":
content = f"[로그]\n{log_text}"
elif mode == "code":
content = f"[코드]\n{code_text}"
else:
content = f"[로그]\n{log_text}\n\n[코드]\n{code_text}"
return content.strip()
def agent_draft(state: AgentState):
persona = state.get("persona", "junior")
mode = state.get("input_mode", "log")
# 시스템 프롬프트 조립 시 줄바꿈 뒤에 공백이 생기지 않도록 strip()
base_prompt = PROMPTS.get((persona, mode), "분석가 페르소나로 동작하세요.")
system_prompt = (base_prompt + "\n\n[중요] 1차 답변에서는 rag_search를 절대 호출하지 말고, 입력만으로 가능한 분석을 먼저 작성하라.").strip()
msgs = state.get("messages", [])
if not msgs:
user_content = build_user_prompt(mode, state.get("log_text") or "", state.get("code_text") or "")
msgs = [HumanMessage(content=user_content)]
# [핵심] Anthropic 400 에러 방지: 모든 메시지의 끝 공백 강제 제거
formatted_msgs = [SystemMessage(content=system_prompt)] + msgs
for m in formatted_msgs:
if hasattr(m, "content") and isinstance(m.content, str):
m.content = m.content.strip()
resp = llm.invoke(formatted_msgs)
return {"messages": [resp]}
def need_rag(state: AgentState) -> str:
# 1차 답변을 보고 RAG 호출 여부 판단
last_content = state["messages"][-1].content
if not last_content:
return END
last = last_content.lower()
triggers = ["모르겠", "불확실", "추정", "추가 정보", "확인이 필요", "가능성이", "근거 부족"]
return "tools" if any(t in last for t in triggers) else END
def agent_final(state: AgentState):
persona = state.get("persona", "junior")
mode = state.get("input_mode", "log")
base_prompt = PROMPTS.get((persona, mode), "분석가 페르소나로 동작하세요.")
system_prompt = (base_prompt + "\n\n검색된 지식을 바탕으로 최종 답변을 작성하세요. 추가 도구 호출은 중단하세요.").strip()
msgs = state.get("messages", [])
# 모든 메시지의 content에서 trailing whitespace 제거
formatted_msgs = [SystemMessage(content=system_prompt)] + msgs
for m in formatted_msgs:
if hasattr(m, "content") and isinstance(m.content, str):
m.content = m.content.strip()
resp = llm_with_tools.invoke(formatted_msgs)
return {"messages": [resp]}
# 그래프 정의
graph = StateGraph(AgentState)
graph.add_node("draft", agent_draft)
graph.add_node("tools", tool_node)
graph.add_node("final", agent_final)
graph.add_edge(START, "draft")
graph.add_conditional_edges("draft", need_rag, {"tools": "tools", END: END})
graph.add_edge("tools", "final")
graph.add_edge("final", END)
app = graph.compile()
if __name__ == "__main__":
# 테스트 시에도 불필요한 공백이 포함되지 않도록 strip() 적용
test_log = """
Uncaught ReferenceError: count is not defined
at increment (main.js:10:14)
at HTMLButtonElement.onclick (index.html:25:32)
function showUserName(user) {
console.log(user.name);
}
showUserName();
""".strip()
test_state = {
"messages": [],
"persona": "junior",
"input_mode": "log",
"log_text": test_log,
"code_text": ""
}
out = app.invoke(test_state)
print("\n=== OUTPUT ===")
if out["messages"]:
print(out["messages"][-1].content)
이것은 최종코드다
Day3: 안정화(품질·UX·CI/CD·문서)
def agent_draft에서 문제가 조금 있었다.
원래는 def agent_node였는데 3일차에서 생긴 문제점은 에러 로그를 넣으면 답변이 계속 계속 계속 계속 계속
에러 로그를 제공받지 않아서 내용을 모르겠지만
(문제점 그리고 해결방안)
계속 위에 저 짜증나는 말이 붙는거다 저거 이슈때문에 내가 9시 넘어서 집 갔다 31일인데
그래서 해결 방안으로 에이전트 구성을 바꿨다.
에이전트 구성 부분
def agent_draft(state: AgentState):
persona = state.get("persona", "junior")
mode = state.get("input_mode", "log")
# 시스템 프롬프트 조립 시 줄바꿈 뒤에 공백이 생기지 않도록 strip()
base_prompt = PROMPTS.get((persona, mode), "분석가 페르소나로 동작하세요.")
system_prompt = (base_prompt + "\n\n[중요] 1차 답변에서는 rag_search를 절대 호출하지 말고, 입력만으로 가능한 분석을 먼저 작성하라.").strip()
msgs = state.get("messages", [])
if not msgs:
user_content = build_user_prompt(mode, state.get("log_text") or "", state.get("code_text") or "")
msgs = [HumanMessage(content=user_content)]
# [핵심] Anthropic 400 에러 방지: 모든 메시지의 끝 공백 강제 제거
formatted_msgs = [SystemMessage(content=system_prompt)] + msgs
for m in formatted_msgs:
if hasattr(m, "content") and isinstance(m.content, str):
m.content = m.content.strip()
resp = llm.invoke(formatted_msgs)
return {"messages": [resp]}
def need_rag(state: AgentState) -> str:
# 1차 답변을 보고 RAG 호출 여부 판단
last_content = state["messages"][-1].content
if not last_content:
return END
last = last_content.lower()
triggers = ["모르겠", "불확실", "추정", "추가 정보", "확인이 필요", "가능성이", "근거 부족"]
return "tools" if any(t in last for t in triggers) else END
def agent_final(state: AgentState):
persona = state.get("persona", "junior")
mode = state.get("input_mode", "log")
base_prompt = PROMPTS.get((persona, mode), "분석가 페르소나로 동작하세요.")
system_prompt = (base_prompt + "\n\n검색된 지식을 바탕으로 최종 답변을 작성하세요. 추가 도구 호출은 중단하세요.").strip()
msgs = state.get("messages", [])
# 모든 메시지의 content에서 trailing whitespace 제거
formatted_msgs = [SystemMessage(content=system_prompt)] + msgs
for m in formatted_msgs:
if hasattr(m, "content") and isinstance(m.content, str):
m.content = m.content.strip()
resp = llm_with_tools.invoke(formatted_msgs)
return {"messages": [resp]}
기존의 설계의 문제는 llm 도구가 바로 rag에 들어가게 되어서 rag 기반 분석에 로그가 부족하다고 거의 무조건 판단하고 에러 로그가 제공되지 않았다는 답변이 출력되었다.
그래서 llm이 1차 답변을 무조건 혼자서 생각하게 하고, 그 이후 rag 툴에서 정보를 얻어와서 (need_rag) 그 정보를 기반으로 최종 답변을 도출하도록 설계를 변경하였다.
이렇게 함으로서 동시에 1차 답변에서 rag를 사용하지 않아도 될 만큼의 답변이 나오면 자원을 아깔 수 있는 영향 또한 있어서 아주 긍정적으로 답변이 변경되었다.
그 이후 답변을 고정시키기 위해서 프롬프트를 추가하였다.
[응답 구조]
- cause: 번호 매긴 리스트(1.~4.) + 줄바꿈으로 논리 단계가 보이게 작성
- solution: 단계별 해결책을 1.~N. 형태로 작성
- prevention: 최소 3개 이상의 예방 수칙을 1.~N. 형태로 작성
이 프롬프트를 추가함으로써 답변에 일관성을 부여하고 가독성이 더욱 좋도록 설계할 수 있었다,
3일차는 이렇게 끝났었다.
이제 4일차에는 최종적으로 확장 가능한 기능을 더 생각해보고, 추가하고, 안정화하는 날이었고, 팀장인 나는 발표 초안을 잡는 작업만 진행하였다,
원하는 작업은 우리가 4일차때까지 못 끝낸 배포를 마무리하고, 발표때 필요한 예시 사례 로그와 코드 등을 미리 받아두는거였는데, 이 때 한 팀원의 약간의 소통부족으로 내가 요청한 일을 잘 못해주는 경험이 있었다.
이런게 팀 과제지,,,, 생각하며 그냥 넘기고 내가 발표준비할때 하기로 했다, 그게 나도 마음이 편할 것 같아서,
팀 과제는 온갖 빌런, 사건 사고와 충돌 마찰이 있다는 얘기는 많이 들었지만 우리는 그런 일은 없었다. 오히려 생각보다 너무 일이 순탄하게 풀려서 걱정될 정도였으니까.
배포파트에서도 4일동안 배포를 못하는 문제가 있었지만 손이 남는 다른 팀원의 도움으로 배포도 3시반 경에 마무리할 수 있었다.
우리 팀의 문제는 내가 일을 너무 잘 나눠두고 다른사람 일은 진행척도만 듣고 신경을 크게 안써서 내가 다른 파트의 애로사항등을 자세히 파악하지 못한 것이었다. 그래도 잘 끝났으니 다행인거 아닌가?
그렇게 미니프로젝트 서비스는 4일차에 모두 마무리 할 수 있었고, 발표준비만 일요일에 하루종일 하였다.
최종 발표
전체 내용은 아니고 보여줄 만한 슬라이드만 정리해서 넣겠다.


시연 영상



우리는 메인 컨셉으로 단순한 프로젝트가 아니라 정말 실무에도 적용될 가능성을 기반으로 하고싶었기 때문에 추후 확장 방향에 있어서도 특정 도메인을 정하고(로봇, 반도체) 등등의 기업에서 주로 나오는 로그를 가정하고(더미 데이터) 그 내용을 rag에 학습 시켜서 더욱 빠르고 정확한 결과를 도출하는것이 우리의 확장방향이었다.
그리고 더해서 조직별 / 프로젝트별로 db를 구분하여 각 역할마다 특화된 llm 모델을 구축하는 방향성도 제시하였는데 이는 나름 나쁘지 않게 칭찬도 받았다.
그리고 만약 프로젝트를 준비중인 사람이라면 피드백을 잘 볼것
피드백
먼저 pt다.
1. pt에서 기존 llm 챗봇과 우리 모델의 차별점을 ' 그저 말뿐이 아니라 이미지로 한장이라도 보여주는게 필요했다'
직관적으로 우리 서비스의 장점을 보여줄 수 있으니까. 그리고 claude를 사용한 이유에 대해서도
openai를 썼을때, google gemini를 썼을때 예시로 이래서 claude를 썼다, 같은 내용들도 말이 아니라 이미지로 보여줘야했었다.
2. 우리가 넣은 기능들의 예시 페이지 부족
우리는 데이터 마스킹 등의 내용도 추가하였는데, 이가 어떻게 작동하는지 예시로 보여주는 페이지가 필요했다. 왜 만들고 안보여주냐
3. ai engineer에 편중된 pt 내용
풀스택, cicd 파트에서 작업한 내용들을 나는 기본적인 내용이고, 프로젝트의 메인이 아니라고 생각하여 내용을 한페이지씩만 넣었다. 이는 곧 내가 작업하지 않은 파트를 경시한거나 마찬가지였다.
그 내용들이 있어서 이 프로젝트를 완성한건데 그 사실을 까맣게 잊고있었다. yml 파일의 구조, frond,backend 데이터가 연동되는 부분 이런 페이지를 더 할당했어야 했다. 너무 선택과 집중만 한 것 같다.
4. 글자 확대 필요, 강조 표현 필요
나는 지금까지 이미지가 메인이고 글자가 거의 없는 pt만 준비했었는데 이번에 마찬가지로 로그와 예시 사진을 넣었던것을 나는 이미지라고 생각했다. 그런데 pt에 들어가는 이미지이지만 이는 곧 텍스트였어서 그 부분에 대한 고심이 부족했다.
그래서 강조될 곳이 강조되지 못하고 텍스트가 작아서 강조가 부족했다.
다음으로는 프로젝트 피드백
1. 주니어 / 시니어 모드로 했을때 주니어모드에서 뭐 전구같은 아이콘 누르면 시니어모드는 이렇게 답변해요! 등의 추가기능 좋을듯
2. 맨 처음에 사이트 들어갈때 주니어모드, 시니어 모드가 어떤 특징이 있는지 설명해주고 사용자가 고르게 하는 ui가 잇어도 훨씬 당위성 있을것같다, 사용자도 편리하고.
3. 추후 확장 가능성으로는 이 서비스를 vs코드에 익스텐션으로 설계하여 추가하는 방법? 그렇게 해서 진짜 vs코드상 에이전트보다 가독성이 좋은 ai 에이전트가 된다면 실제로도 상용성이 있다고 생각한다.
정도가 있었다.
정리
일단 나는 이번 프로젝트에 아주 만족했다.
1. ui까지 생각해서 나름 만족스러운 ui를 만들었다.
2. 발표에서 내용적으로 당위성 등에서 까인 부분은 없다. -> 나름 설득력 있었다
3. 유일하게 까인건 발표 pt 자료 등이었다. 이건 내가 한거니까 상관없다.
4. 프로젝트 기한에 허덕이지 않고 주말 쉴거 다 쉬고 야근 거의 안하고 일별 최소 목표를 깔끔하게 달성했다. -> 일정 관리 성공
비전공자 3명이서 작업한 프로젝트인데 물론 우리 반에 비교대상이 사람이 너무 적어서 없긴 하지만
나름 나쁘지 않았다고 생각한다, 치어스
미니프로젝트 하느라 블로그도 아예 못쓰고 끝나고 나니까 긴장이 확 풀리는것같다.
다른 사람들도 조금 풀린듯? 기강 한번 잡아야 쓰겄다
프로젝트는 프로젝트고, 이제 다시 시작해야지
그래도 프로젝트 한번 하고 나니까 확실히 동기부여도 되고 자신감도 생기고 좋았던것같다.
그리고 나는 리더가 너무 좋다
오늘 하루도 화이팅!
여담
요즘 생각이 좀 많아진다. 내가 개발한것도 나름 ai에이전튼데 지금 외주에서 건당 6~70만원 받더라.
요즘 드는 생각은 일단 지금까지 외우고 이해하고 공부했던것들이 물론, 필요한 내용이고 당연한것이지만
실무를 진행하고, 작업과 창작을 하는데에 있어서 ai는 정말 엄청나다는것을 깨닫고
이제부터 공부하고 성장해야 할것이 과목 내용인가, 아니면 ai사용법인가,,,물론 다 해야겠지만
내가 지금 다른 사람들은 6~7년씩 공부하고, 회사에서도 배운 내용들도 있을텐데 그런걸 따라잡을수 있을까?
내가 잘 할 수 있는건 뭘까? 이전에도 말했지만 나는 앉아서 컴퓨터만 두들기는 체질은 아니다.
프로젝트를 하면서 확실히 느낀것같다, 다른 사람들과 일정을 조율하고 그 일정에 맞게 업무가 수행되도록 하며 일 외적인 것을 하는게 더 적성에 맞는 것 같다.
그래서 지금은 ai 에이전트 개발과 개발직군 PM분야에 조금 관심이 생기기 시작한다.
다른 많은 생각들이 있지만 일단은 뭐 여기까지
'ASAC-SK플래닛 T아카데미 데이터 엔지니어' 카테고리의 다른 글
| 26.01.08 64일차 [Airflow : 기본 개념, dag, operator 로컬 실습] (0) | 2026.01.08 |
|---|---|
| 26.01.06~26.01.07 62~63일차 [ 클라우드 , aws 특강] (1) | 2026.01.08 |
| 25.12.24 55일차 [LLM 기반 서비스 + LANGGRAPH,RAG 적용 / MCP 예제 ] (0) | 2025.12.28 |
| 25.12.23 54일차 [RAG_test, langgraph_test, mcp_basic, RAG 도구, LangChain 도구 구성 ] (0) | 2025.12.23 |
| 25.12.22 53일차 [llm_langchain_bedrock 기반 서비스 만들기 실습, 프론트, 백엔드 연동, RAG 관련 기술 업그레이드 ] (0) | 2025.12.22 |