- 2025.08.22 ~ 2025.08.25 (총 2일)
SKN15-3rd-1Team/
├── 📁 lecture_rag/ # 강의록 기반 RAG 패키지
│ ├── __init__.py # 패키지 초기화/공개 API
│ ├── app.py # Streamlit 앱 엔트리(UI, 인덱싱/QA)
│ ├── config.py # dataclass 설정(LLM/청킹/검색/프롬프트)
│ ├── document_processor.py # 날짜(YYYY.MM.DD) 기반 청킹 및 메타 생성
│ ├── google_drive.py # (신규) 구글 드라이브 연계 모듈(파일 로드/저장)
│ ├── llm_handler.py # ChatOpenAI 래퍼, 답변 생성/재시도/토큰검증
│ ├── utils.py # 텍스트 I/O, 코드블록 감지, 허용 토큰 유틸
│ └── vector_store.py # FAISS 임베딩/인덱싱/검색, allowed_tokens.json 관리
├── .gitignore # Git 제외 규칙
├── langgraph_flow.py # LangGraph 파이프라인 정의/다이어그램 생성 스크립트
├── main.py # (선택) 앱 실행 진입점
├── rag_flow.png # 파이프라인 이미지(README 삽입용)
├── rag_flow_mermaid.md # Mermaid 다이어그램 정의(문서화용)
├── README.md # 프로젝트 문서
└── requirements.txt # 의존성 목록
🚫 로컬에만 있는 파일들 (.gitignore로 제외)
#.env - OpenAI API 키 등
# .lecture_index/ - FAISS 인덱스 (토큰 포함)
# pycache/ - Python 캐시
# .claude/ - Claude Code 설정
📦 패키지/폴더, 🧩 모듈(파일), 🧪 테스트/예시, ⚙️ 설정/환경, 💾 저장/인덱스, 🔍 검색, 🧠 LLM/프롬프트, 🧰 유틸, 🖥️ UI
| 파일/모듈 | 주요 함수/클래스 | 설명 |
|---|---|---|
| ⚙️ config.py | Config |
전역 설정을 보관하는 데이터클래스(모델명, 온도, 경로 등). |
from_env() |
환경변수에서 설정 로드. | |
to_env() |
현재 설정을 환경변수에 반영. | |
| 🧰 utils.py | read_text() |
파일을 UTF-8로 읽어 텍스트 반환. |
detect_code_blocks() |
텍스트에서 코드/일반 텍스트 블록 분리. | |
extract_allowed_tokens() |
import/함수/클래스/상수에서 허용 토큰 추출. | |
find_unknown_tokens() |
코드에서 비허용 모듈/심볼 탐지. | |
| 🧩 document_processor.py | DocumentProcessor.process() |
강의록을 읽고 청크 Document 리스트로 변환. |
_chunk_by_date() |
YYYY.MM.DD 날짜 기준 분할 및 메타 생성. | |
_chunk_by_content() |
코드/텍스트 특성에 맞는 추가 분할. | |
| 💾 vector_store.py | VectorStore.index_document() |
임베딩 후 FAISS 인덱스 생성·저장(허용 토큰 JSON 포함). |
search() |
질의 임베딩 기반 Top‑K 유사 문서 검색. | |
_load_index() |
디스크의 FAISS 인덱스 로드. | |
_save_index() |
FAISS 인덱스 저장. | |
| 🧠 llm_handler.py | generate_answer() |
질의+컨텍스트로 LLM 답변 생성, 필요 시 제약 재시도. |
_make_context_block() |
프롬프트용 컨텍스트 문자열 구성. | |
_check_unknown_tokens() |
답변 코드 블록의 비허용 토큰 검사. | |
_create_style_hint() |
허용 모듈/심볼 힌트 문자열 생성. | |
| 🖥️ app.py | LectureRAGApp.run() |
Streamlit 앱 실행. |
_render_sidebar() |
설정 사이드바 렌더링(모델/온도/경로). | |
_render_qa_section() |
질의·Top‑K·답변 생성 UI 렌더링. | |
_handle_indexing() |
업로드/경로 입력 → 인덱싱 처리. | |
_handle_qa() |
검색→LLM 생성→토큰 검증→표시 파이프라인. | |
_render_evidence_snippets() |
근거 스니펫 및 메타 표시. |
- preprocess_query(불용어 제거/정규화) → vector_search(Top‑K 스니펫 수집) → load_allowed_tokens(화이트리스트 로드) → build_context(근거 블록 생성) → generate_answer(LLM 호출) → check_tokens(코드블록 검증) → retry_with_constraints(제약 강화 재시도)
- check_tokens 단계에서 허용되지 않은 모듈/심볼이 검출되면 retry_with_constraints로 분기해 화이트리스트를 강제한 프롬프트로 한 번 더 답변을 생성하고, 문제가 없으면 end로 종료한다.
| 단계 | 설명 요약 |
|---|---|
| 🧹 전처리 | 입력 질의를 정규화해 공백 정리·불용어 최소화 등으로 검색 친화적 형태로 변환한다. |
| 🔎 벡터 검색 | 인덱싱된 강의록에서 질의 임베딩과 유사한 문서 조각 Top‑K를 수집한다. |
| ✅ 토큰 로드 | 인덱싱 시 저장된 화이트리스트(modules, symbols)를 읽어 제약 조건의 기준으로 사용한다. |
| 🧩 컨텍스트 구성 | 스니펫 본문과 메타데이터(강의일, 시작/끝 라인, chunk_id)를 묶어 LLM에 전달할 컨텍스트 블록을 만든다. |
| 🧠 1차 생성 | 시스템 프롬프트·출력 가이드·컨텍스트·스타일 힌트를 포함해 LLM을 호출해 답변을 생성한다. |
| 🔒 토큰 검증 | 답변의 코드 블록만 추출해 화이트리스트에 없는 모듈/심볼 사용 여부를 검사한다. |
| ♻️ 재시도 | 위반 항목이 있으면 “금지 목록을 절대 사용하지 말라” 제약을 추가해 한 번 더 재생성한다. |
| ✅ 종료/표시 | 최종 답변을 출력하고, 거절 응답이 아니면 근거 스니펫(강의일/라인)을 함께 노출한다. |
| 단계 | 설명 요약 |
|---|---|
| 🧹 전처리 입력/출력 | 입력: 원문 질의 → 출력: 정규화 질의. |
| 🔎 벡터 검색 입력/출력 | 입력: 정규화 질의 → 출력: 문서 조각 리스트(docs). |
| ✅ 토큰 로드 입력/출력 | 입력: 인덱스 경로 → 출력: allowed={modules, symbols}. |
| 🧩 컨텍스트 구성 입력/출력 | 입력: docs → 출력: 컨텍스트 문자열(context). |
| 🧠 답변 생성 입력/출력 | 입력: query, context, allowed, 프롬프트 → 출력: answer(text/markdown). |
| 🔒 토큰 검증 입력/출력 | 입력: answer, allowed → 출력: unknown_tokens 리스트. |
| ♻️ 재시도 입력/출력 | 입력: query+금지목록, context, 프롬프트 → 출력: 교정된 answer. |
system_prompt: str = (
"당신은 수업용 코치입니다.\n"
"제공된 컨텍스트(강의록 조각들)에서 근거를 찾아 답변하십시오.\n"
"컨텍스트에 관련 내용이 있으면 그것을 기반으로 설명하고 코드를 제공하세요.\n"
"컨텍스트에 없는 라이브러리나 함수는 사용하지 마세요.\n"
"질문이 프로그래밍과 전혀 무관하거나(예: 요리, 여행, 운동 등) 컨텍스트와 완전히 무관한 경우에만 '죄송합니다. 해당 내용은 강의록에서 다루지 않은 주제입니다.'라고 거절하세요.\n"
"프로그래밍 관련 질문이라면 컨텍스트에서 유사한 내용을 찾아 최대한 도움을 주려고 노력하세요.\n"
"가능하면 강의록의 변수명/함수명/스타일을 모방하세요.\n"
)
answer_guide: str = (
"출력 형식:\n"
"1) 간단한 설명\n"
"2) 코드(필요 시)\n"
"3) 사용한 근거 스니펫들(강의일 기준으로 표시)\n"
"주의: 코드 블록은 반드시 ```python 으로 시작하세요."
)
-
역할: 본 프롬프트는 “수업용 코치 + 코딩 어시스턴트”로서, 강의록에서 추출한 스니펫을 근거로만 답하도록 설계되었다.
-
근거 원칙: 외부 지식이나 강의록에 없는 라이브러리/함수 사용을 금지해 일관성과 재현성을 보장한다.
-
실패 처리: 컨텍스트에 근거가 없을 때는 정중히 거절하고, 인접 주제/검색어 제안을 통해 탐색을 유도한다.
-
스타일: 한국어로 간결·정확하게 답하며, 가능하면 강의록의 변수명·함수명·코딩 스타일을 모방한다.
-
출력 포맷: “간단한 설명 → 코드(있을 때) → 근거 스니펫(강의일/라인)” 순서를 엄수하고, 코드블록은 반드시 ```python 으로 시작한다.
-
화이트리스트: 허용되지 않은 모듈/심볼은 사용하지 않으며, 필요한 경우 대체 표준 라이브러리를 설명 수준에서만 제안한다.
-
품질 관리: 유사·중복 스니펫은 통합하고 Top-K 내에서 핵심만 사용하여 장황한 인용을 피한다.
1️⃣ 입력: 파일 읽기
def process_file(self, file_path: Path) -> List[Document]:
"""
파일을 읽어서 Document 청크로 변환
"""
text = read_text(file_path) # <- 여기서 전체 텍스트 읽음
return self.chunk_documents(text, source=str(file_path))-
read_text(file_path) → utils.py의 함수
-
전체 텍스트를 불러와서 chunk_documents로 전달
2️⃣ 날짜 기반 청크 분할
def chunk_documents(self, text: str, source: str) -> List[Document]:
"""
텍스트를 날짜 기반으로 청킹 (YYYY.MM.DD 패턴)
"""
date_pattern = r'^(\d{4}\.\d{2}\.\d{2})\s*' # <- 날짜 패턴 정의
lines = text.splitlines()
docs: List[Document] = []
current_chunk_lines = []
current_date = None
current_start_line = 1
chunk_idx = 0
for line_idx, line in enumerate(lines, 1):
date_match = re.match(date_pattern, line.strip())
if date_match:
if current_chunk_lines and current_date:
# 이전 청크 저장
chunk_content = '\n'.join(current_chunk_lines)
if chunk_content.strip():
docs.append(self._create_date_chunk_document(
chunk_content, source, current_date, current_start_line,
line_idx - 1, chunk_idx
))
chunk_idx += 1
# 새 청크 시작
current_date = date_match.group(1)
current_start_line = line_idx
current_chunk_lines = [line]
else:
current_chunk_lines.append(line)
# 마지막 청크 처리
if current_chunk_lines and current_date:
chunk_content = '\n'.join(current_chunk_lines)
if chunk_content.strip():
docs.append(self._create_date_chunk_document(
chunk_content, source, current_date, current_start_line,
len(lines), chunk_idx
))
return docs-
핵심: 날짜가 나오면 새 청크 시작, 이전 청크는 _create_date_chunk_document로 Document 생성
-
줄 단위 순회하면서 문맥 유지
3️⃣ Document 객체 생성
def _create_date_chunk_document(
self, content: str, source: str, date: str,
start_line: int, end_line: int, chunk_idx: int
) -> Document:
"""날짜 기반 청크용 Document 생성"""
lines = content.splitlines()
first_line = lines[0] if lines else ""
last_line = lines[-1] if lines else ""
# 첫 줄에서 날짜 제거 후 미리보기
first_line_preview = re.sub(r'^\d{4}\.\d{2}\.\d{2}\s*', '', first_line)
first_line_preview = first_line_preview[:50] + "..." if len(first_line_preview) > 50 else first_line_preview
return Document(
page_content=content,
metadata={
"source": source,
"kind": "lecture_date",
"date": date,
"chunk_id": f"{source}:date_{chunk_idx}",
"start_line": start_line,
"end_line": end_line,
"line_count": len(lines),
"first_line_preview": first_line_preview,
"last_line_preview": last_line[:50] + "..." if len(last_line) > 50 else last_line
}
)-
날짜 기반 청크를 Document 객체로 변환
-
메타데이터 포함 → RAG에서 근거 스니펫 표시 가능
pip install -r requirements.txt
# Google Drive API (선택)
GOOGLE_CLIENT_ID=your_key
GOOGLE_CLIENT_SECRET=your_key
# OpenAI (필수: OpenAI 모델 사용 시)
OPENAI_API_KEY=your_key
# 선택: 기본 모델/온도 덮어쓰기
LECTURE_RAG_MODEL=gpt-4o-mini
LECTURE_RAG_TEMPERATURE=0.2
python -m streamlit run lecture_rag/app.py
# 파일 다운로드 후, streamlit 웹에 동적으로 업로드 후 이용가능
| Field | Tool |
|---|---|
| Frontend | |
| LLM | |
| Vector DB | |
| Embedding | |
| Framework | |
| Language |
설정 패널(좌측)
- FAISS 저장 디렉터리: 벡터 인덱스와 허용 토큰 파일이 저장될 로컬 경로를 지정한다(예:
.lecture_index). - LLM 모델 선택: 기본값은 환경변수(예:
LECTURE_RAG_MODEL)를 따르며, 드롭다운에서 실행 중에 변경 가능하다. - Temperature 슬라이더: 생성 다양성을 제어한다(낮을수록 보수적, 높을수록 창의적).
질문/옵션 영역(중앙)
- 질의 입력창: 분석·설명·코드 요청 등 자연어 질문을 입력한다.
- Top‑K 문서: 검색에서 가져올 스니펫 개수를 조절하여 컨텍스트의 폭을 설정한다.
- 답변 생성 버튼: 현재 설정과 인덱스를 기반으로 컨텍스트를 구성해 LLM 답변을 생성한다.
인덱싱(좌측 하단)
- 구글 드라이브에서 강의록 가져오기: 드라이브에 업로드된 텍스트/마크다운/코드 파일을 불러와 인덱싱할 수 있다.
- 로컬 파일 업로드(대안): 드라이브 사용이 어려울 때 로컬 파일을 직접 선택해 인덱싱을 수행한다.
사용 흐름 요약
- 좌측에서 모델·인덱스 경로·온도를 설정한다.
- 인덱싱 섹션에서 강의록을 업로드(또는 드라이브에서 가져오기)하여 벡터 인덱스를 생성한다.
- 중앙 입력창에 질문을 입력하고 Top‑K를 조절한 뒤 “답변 생성”을 누르면, 근거 스니펫을 바탕으로 답변과 코드가 출력된다.
안내
- OpenAI 계열 모델을 사용할 때는 사전에 API 키(예:
OPENAI_API_KEY)가 환경에 설정되어 있어야 정상 동작한다. - 프로젝트별 문서로 인덱싱을 분리하려면 저장 디렉터리를 서로 다른 경로로 지정해 독립적인 컨텍스트를 유지할 수 있다.
- 상단에 “답변” 제목과 함께 RAG의 개념 요약이 먼저 노출되고, 그 아래에는 기본 RAG 체인을 구성하는 코드 예시가 포함된다.
- 이 화면은 사용자가 모델이 어떤 방식으로 답을 생성하는지 이해하도록 돕는 “학습/가이드 영역” 역할을 한다.
- 검색된 문서 조각들이 Chunk 단위로 나열되며, 각 항목을 펼치면 해당 스니펫의 본문과 라인 범위, 타입(text/code) 같은 메타데이터를 확인할 수 있다.
- “원본에서 찾기” 힌트를 제공해 실제 파일의 위치(라인 번호 기준)를 빠르게 탐색하도록 돕는 검증용 UI다.
- 중앙 상단에 질문 입력창과 Top‑K 슬라이더가 배치되어 검색 범위를 조정할 수 있고, 우측 드롭존에는 추가 컨텍스트 파일을 일시 첨부할 수 있다.
- 좌측 사이드바에는 인덱스 경로, LLM 모델, Temperature 등의 런타임 설정이 모여 있어 “실행 전 준비/세팅”을 담당한다.
- 생성된 답변이 문단과 코드 블록으로 제시되며, 하단에는 “사용한 근거 스니펫들” 목록이 요약되어 근거 기반 응답임을 명확히 한다.
- 이 화면은 사용자가 즉시 실행 가능한 코드와 함께, 어떤 스니펫이 답변을 뒷받침했는지를 한 눈에 확인하는 “결과/검증” 단계다.
- 목적: 강의 컨텍스트에 존재하는 개념(RAG)을 질의했을 때의 정상 동작 예시를 보여준다.
- 특징:
- 상단 입력창과 Top‑K 조절 슬라이더로 검색 폭을 설정하고, 우측 드롭존에 추가 컨텍스트를 첨부할 수 있다.
- 하단 “답변” 섹션에서 RAG 정의와 동작 원리를 서술하고, 이어서 RAG 체인을 구성하는 코드 예시를 제공한다.
- 근거 스니펫 패널로 어떤 강의 일자/라인에서 정보를 인용했는지 검증 가능하다.
- 목적: 강의록 범위를 벗어난 일반 지식 질문에 대해 시스템이 어떻게 안전하게 응답하는지 보여준다.
- 특징:
- 출력 포맷은 동일하게 유지하되, 코드 섹션은 “필요 시” 조건에 따라 생략된다.
- “사용한 근거 스니펫들”에 컨텍스트 부재 안내가 포함되어, 강의 자료에 근거가 없음을 명확히 알린다.
- 환각 방지를 위해 외부 지식으로 채우지 않고, 정중한 제한 응답 정책을 따른다.
| 조태민 | 박진우 | 서혜선 |
|---|---|---|
| LLM을 활용해서, 데이터의 저장, 청크, 임베딩, 라우터, 래그 등 랭채인을 이용한 파이프라인을 처음부터 끝까지 다뤄볼 수 있었습니다. 의도대로 기능을 구현하고 논리를 설계하면서 흐름을 더 잘 이해할 수 있는 기회를 얻어 좋았습니다. | 문서 기반 질의응답을 목표로 RAG 파이프라인을 설계·구현하면서, 모델을 바꾸는 것보다 청킹 전략과 리트리버/프롬프트 튜닝, 간단한 평가 루프의 반복 개선이 성능에 더 효과적임을 배웠습니다. 팀원들과 코드를 중심으로 논리를 맞추며 전체 흐름을 선명하게 이해할 수 있어 큰 도움이 되었습니다. | 업로드된 문서 기반으로 LLM이 질의응답하는 구조를 구현하며, 코드 중심 접근의 장점을 체감할 수 있었습니다. 이해안됐던 부분들도 이해할 수 있었고 팀원들도 다 열심히 해줘서 좋은 결과로 마무리한 것 같아 좋습니다. 다들 고생하셨습니다. |
| 임가은 | 임경원 | 홍민식 |
|---|---|---|
| LLM에게 내 문서를 전달하여 그를 바탕으로 정보를 얻을 수 있다는 점이 흥미로웠다. gpt처럼 마냥 새로운 것을 주는 게 아니라, 내 문서를 기반으로 대답한다는 점이 좋았고, 다양한 문서 형식을 처리하기 위해 전처리 과정과 청킹 전략이 얼마나 중요한지 깨달았다. LLM 관련으로 좋은 경험이 된 것 같다. | 이번 프로젝트는 단순히 LLM을 활용하는 기술을 익히는 것을 넘어, 데이터의 흐름을 처음부터 끝까지 직접 설계하고 제어하는 경험이었습니다. 각 단계가 유기적으로 연결되어야만 의도한 대로 기능이 구현되는 것을 보며 시스템 전반의 논리적인 구조를 설계하는 능력을 기를 수 있었습니다. 앞으로 더 복잡하고 정교한 LLM 애플리케이션을 개발하는 데 도움이 되었습니다. | LLM을 통해 데이터 처리와 RAG 파이프라인에 대해서 복잡한 흐름을 배울수 있게 되었습니다 |





