[ { "id": "docs-q8", "lesson_id": "docs-ollama-models", "question_type": "multiple_choice", "question": "Ollama에서 모델이 로컬에 없을 때 `ollama run qwen2.5:7b`를 실행하면?", "options": [ "자동으로 모델을 다운로드한 뒤 실행한다", "오류를 출력하고 종료한다", "최신 모델로 대체해 실행한다", "서버만 시작하고 대기한다" ], "answer": "자동으로 모델을 다운로드한 뒤 실행한다", "explanation": "`ollama run`은 모델이 없으면 자동으로 pull한 뒤 대화형 세션을 시작합니다. `ollama pull`을 먼저 실행할 필요가 없습니다.", "source_id": "official-docs-ollama" }, { "id": "docs-q9", "lesson_id": "docs-streamlit-secrets", "question_type": "multiple_choice", "question": "Streamlit 로컬 개발 시 API 키를 안전하게 관리하는 올바른 방법은?", "options": [ ".streamlit/secrets.toml에 저장하고 .gitignore에 추가", "st.session_state에 저장", "코드 상단에 상수로 선언", "requirements.txt에 포함" ], "answer": ".streamlit/secrets.toml에 저장하고 .gitignore에 추가", "explanation": "민감 정보는 secrets.toml에 넣고, 이 파일은 반드시 .gitignore에 추가해 Git에 커밋되지 않도록 해야 합니다.", "source_id": "official-docs-streamlit" }, { "id": "docs-q10", "lesson_id": "docs-hf-docker-space", "question_type": "multiple_choice", "question": "Hugging Face Docker Space에서 앱이 노출해야 하는 기본 포트는?", "options": [ "7860", "8501", "11434", "3000" ], "answer": "7860", "explanation": "Docker Space의 기본 앱 포트는 7860입니다. README 메타데이터의 `app_port`와 Dockerfile의 EXPOSE 포트를 7860으로 일치시켜야 합니다.", "source_id": "official-docs-huggingface" }, { "id": "docs-q11", "lesson_id": "docs-ollama-models", "question_type": "command", "question": "로컬에 있는 Ollama 모델 목록을 확인하는 CLI 명령어를 입력하세요.", "options": [], "answer": "ollama list", "explanation": "`ollama list`는 로컬에 다운로드된 모델 목록과 크기, 수정일을 출력합니다. `ollama pull <모델>`로 새 모델을 받을 수 있습니다.", "source_id": "official-docs-ollama" }, { "id": "docs-q12", "lesson_id": "docs-langchain-rag", "question_type": "multiple_choice", "question": "LangChain RAG 파이프라인에서 문서를 Vector Store에 넣기 전에 반드시 해야 하는 작업은?", "options": [ "문서를 작은 청크(chunk)로 분할 후 임베딩 생성", "문서를 PDF로 변환", "LLM에 문서를 직접 전달", "API 키를 환경변수에 저장" ], "answer": "문서를 작은 청크(chunk)로 분할 후 임베딩 생성", "explanation": "RAG는 문서 → 청크 분할(TextSplitter) → 임베딩 변환 → Vector Store 저장 순으로 인덱싱합니다. 청크가 너무 크면 검색 품질이 떨어집니다.", "source_id": "official-docs-langchain" }, { "id": "docs-q13", "lesson_id": "docs-streamlit-state", "question_type": "multiple_choice", "question": "Streamlit에서 버튼 클릭 횟수를 페이지 전환 후에도 유지하려면 어떻게 해야 하는가?", "options": [ "st.session_state에 저장", "전역 변수로 선언", "st.cache_data에 저장", "파일에 기록" ], "answer": "st.session_state에 저장", "explanation": "Streamlit은 매 상호작용마다 스크립트 전체를 재실행합니다. 전역 변수는 재실행 시 초기화되므로 세션 간 값을 유지하려면 `st.session_state`를 사용해야 합니다.", "source_id": "official-docs-streamlit" }, { "id": "docs-q14", "lesson_id": "docs-streamlit-cache", "question_type": "multiple_choice", "question": "`st.cache_data`와 `st.cache_resource`의 주된 차이는?", "options": [ "cache_data는 데이터(DataFrame 등) 직렬화 캐시, cache_resource는 DB 연결·모델 같은 공유 리소스 캐시", "두 데코레이터는 완전히 동일하다", "cache_resource는 파일을 캐시하고 cache_data는 연결을 캐시한다", "cache_data가 항상 더 빠르다" ], "answer": "cache_data는 데이터(DataFrame 등) 직렬화 캐시, cache_resource는 DB 연결·모델 같은 공유 리소스 캐시", "explanation": "`st.cache_data`는 반환값을 복사해 각 세션에 독립 제공합니다. `st.cache_resource`는 객체를 공유하므로 DB 연결, ML 모델처럼 한 번만 로드해야 하는 리소스에 씁니다.", "source_id": "official-docs-streamlit" }, { "id": "docs-q15", "lesson_id": "docs-hf-spaces", "question_type": "multiple_choice", "question": "Hugging Face Spaces에서 Streamlit 앱을 배포할 때 README.md에 반드시 포함해야 하는 메타데이터 필드는?", "options": [ "sdk: streamlit", "runtime: python", "framework: web", "deploy: auto" ], "answer": "sdk: streamlit", "explanation": "HF Spaces README 상단 YAML 메타데이터에서 `sdk: streamlit`을 지정해야 Streamlit 앱으로 인식합니다. `sdk_version`으로 버전도 고정할 수 있습니다.", "source_id": "official-docs-huggingface" }, { "id": "docs-q16", "lesson_id": "docs-vectorstore", "question_type": "multiple_choice", "question": "LangChain에서 Retriever의 역할은?", "options": [ "질문과 유사한 문서를 Vector Store에서 검색해 반환한다", "LLM을 호출해 답변을 생성한다", "프롬프트 템플릿을 관리한다", "API 키를 저장한다" ], "answer": "질문과 유사한 문서를 Vector Store에서 검색해 반환한다", "explanation": "Retriever는 질문 임베딩과 가장 유사한 문서 청크를 Vector Store에서 가져옵니다. RAG 체인에서 `retriever | prompt | llm` 파이프라인의 첫 단계입니다.", "source_id": "official-docs-langchain" }, { "id": "docs-q17", "lesson_id": "docs-streamlit-state", "question_type": "multiple_choice", "question": "`st.form`을 사용하는 주된 이유는?", "options": [ "여러 입력 위젯을 묶어 Submit 버튼 클릭 시에만 rerun이 발생하도록 한다", "폼 내 위젯이 더 빠르게 렌더링된다", "입력값을 자동으로 DB에 저장한다", "폼은 모바일에서만 사용 가능하다" ], "answer": "여러 입력 위젯을 묶어 Submit 버튼 클릭 시에만 rerun이 발생하도록 한다", "explanation": "일반 위젯은 값이 바뀔 때마다 rerun됩니다. `st.form`으로 묶으면 Submit 버튼을 누를 때만 rerun되어 불필요한 재실행을 막고 UX를 개선합니다.", "source_id": "official-docs-streamlit" }, { "id": "docs-q18", "lesson_id": "docs-hf-spaces", "question_type": "multiple_choice", "question": "Hugging Face `pipeline()` 함수로 감성 분석 태스크를 실행하려면?", "options": [ "pipeline('sentiment-analysis')", "pipeline('classification')", "pipeline('text-generation')", "pipeline('translation')" ], "answer": "pipeline('sentiment-analysis')", "explanation": "`from transformers import pipeline; classifier = pipeline('sentiment-analysis')` 로 기본 감성 분석 모델을 로드합니다. 결과는 `[{'label': 'POSITIVE', 'score': 0.99}]` 형식입니다.", "source_id": "official-docs-huggingface" }, { "id": "docs-q19", "lesson_id": "docs-lcel", "question_type": "multiple_choice", "question": "LangChain LCEL(LangChain Expression Language)에서 `|` 연산자의 역할은?", "options": [ "앞 단계의 출력을 다음 단계의 입력으로 연결하는 체인 파이프라인을 구성한다", "두 체인을 병렬로 실행한다", "체인을 취소한다", "출력을 JSON으로 변환한다" ], "answer": "앞 단계의 출력을 다음 단계의 입력으로 연결하는 체인 파이프라인을 구성한다", "explanation": "LCEL의 `|`는 유닉스 파이프와 같은 개념입니다. `chain = retriever | prompt | llm | parser`처럼 각 Runnable의 출력이 다음 입력이 됩니다.", "source_id": "official-docs-langchain" }, { "id": "docs-q21", "lesson_id": "docs-lcel", "question_type": "multiple_choice", "question": "LangChain LCEL에서 `RunnableParallel`을 사용하는 목적은?", "options": [ "여러 Runnable을 동시에 실행하고 결과를 딕셔너리로 합친다", "체인을 순차적으로 실행한다", "결과를 캐시한다", "에러를 무시한다" ], "answer": "여러 Runnable을 동시에 실행하고 결과를 딕셔너리로 합친다", "explanation": "`RunnableParallel({'context': retriever, 'question': RunnablePassthrough()})`처럼 여러 브랜치를 병렬 실행해 dict로 합칩니다. RAG에서 컨텍스트 검색과 질문 전달을 동시에 처리할 때 씁니다.", "source_id": "official-docs-langchain" }, { "id": "docs-q22", "lesson_id": "docs-vectorstore", "question_type": "multiple_choice", "question": "FAISS Vector Store에서 `similarity_search(query, k=4)`의 `k` 파라미터의 의미는?", "options": [ "반환할 유사 문서 청크의 최대 수", "검색 임베딩 차원 수", "최소 유사도 점수", "인덱스 파일 버전" ], "answer": "반환할 유사 문서 청크의 최대 수", "explanation": "`k`는 가장 유사한 상위 k개의 청크를 반환합니다. k가 크면 더 많은 컨텍스트를 LLM에 제공하지만 프롬프트 길이가 늘어납니다.", "source_id": "official-docs-langchain" }, { "id": "docs-q23", "lesson_id": "docs-streamlit-secrets", "question_type": "command", "question": "Streamlit 앱 코드에서 secrets.toml의 `OPENAI_API_KEY` 값을 읽는 방법을 입력하세요.", "options": [], "answer": "st.secrets[\"OPENAI_API_KEY\"]", "explanation": "`st.secrets[\"키이름\"]` 또는 `st.secrets.키이름`으로 접근합니다. 배포 시 Streamlit Cloud의 Secrets 관리 화면에서 동일 키로 등록하면 됩니다.", "source_id": "official-docs-streamlit" }, { "id": "docs-q24", "lesson_id": "docs-hf-docker-space", "question_type": "multiple_choice", "question": "Hugging Face Docker Space에서 환경변수(API 키 등)를 안전하게 주입하는 방법은?", "options": [ "Space Settings의 Repository Secrets에 등록하면 빌드/런타임 환경변수로 주입된다", "Dockerfile에 ENV로 하드코딩한다", "requirements.txt에 추가한다", "README 메타데이터에 base64로 인코딩해 넣는다" ], "answer": "Space Settings의 Repository Secrets에 등록하면 빌드/런타임 환경변수로 주입된다", "explanation": "HF Space의 Repository Secrets는 Docker 빌드와 컨테이너 런타임에 환경변수로 주입됩니다. Dockerfile이나 코드에 비밀을 하드코딩하면 공개 저장소에서 노출됩니다.", "source_id": "official-docs-huggingface" }, { "id": "docs-q20", "lesson_id": "docs-ollama-api", "question_type": "command", "question": "Ollama REST API로 `llama3.2` 모델에 'Hello' 프롬프트를 스트리밍 없이 요청하는 curl 명령어를 입력하세요.", "options": [], "answer": "curl http://localhost:11434/api/generate -d '{\"model\":\"llama3.2\",\"prompt\":\"Hello\",\"stream\":false}'", "explanation": "Ollama API는 기본적으로 스트리밍 응답을 반환합니다. `\"stream\": false`를 추가하면 완성된 응답을 한 번에 받을 수 있습니다.", "source_id": "official-docs-ollama" }, { "id": "docs-q25", "lesson_id": "docs-fastapi", "question_type": "multiple_choice", "question": "FastAPI에서 POST `/ask` 엔드포인트를 정의할 때 요청 바디를 검증하는 올바른 방법은?", "options": [ "Pydantic BaseModel을 상속한 클래스를 파라미터 타입으로 지정한다", "request.body()를 수동으로 파싱한다", "전역 딕셔너리에 JSON을 저장한다", "Flask의 request.json을 사용한다" ], "answer": "Pydantic BaseModel을 상속한 클래스를 파라미터 타입으로 지정한다", "explanation": "FastAPI는 함수 파라미터에 Pydantic 모델을 타입 힌트로 지정하면 자동으로 JSON 바디를 파싱·검증합니다. 잘못된 입력은 422 오류를 반환합니다.", "source_id": "official-docs-fastapi" }, { "id": "docs-q26", "lesson_id": "docs-fastapi", "question_type": "multiple_choice", "question": "FastAPI 개발 서버를 자동 재시작(reload) 모드로 실행하는 명령어는?", "options": [ "uvicorn main:app --reload", "python main.py --dev", "fastapi run --watch main.py", "gunicorn main:app --workers 1" ], "answer": "uvicorn main:app --reload", "explanation": "`uvicorn main:app --reload`는 파일 변경을 감지해 서버를 자동 재시작합니다. `main`은 파이썬 파일명, `app`은 FastAPI 인스턴스 변수명입니다.", "source_id": "official-docs-fastapi" }, { "id": "docs-q27", "lesson_id": "docs-langsmith", "question_type": "multiple_choice", "question": "LangSmith 추적을 활성화하기 위해 반드시 설정해야 하는 환경변수가 아닌 것은?", "options": [ "LANGCHAIN_MODEL", "LANGCHAIN_TRACING_V2", "LANGCHAIN_API_KEY", "LANGCHAIN_PROJECT" ], "answer": "LANGCHAIN_MODEL", "explanation": "LangSmith 추적에 필요한 환경변수는 `LANGCHAIN_TRACING_V2=true`, `LANGCHAIN_API_KEY`, `LANGCHAIN_PROJECT` 세 가지입니다. 모델명은 체인 코드에서 직접 지정합니다.", "source_id": "official-docs-langchain" }, { "id": "docs-q28", "lesson_id": "docs-langsmith", "question_type": "multiple_choice", "question": "LangSmith에서 프로덕션 오류 케이스를 수집해 자동화된 품질 테스트를 만드는 기능은?", "options": [ "Dataset + Evaluator로 평가 파이프라인을 구성한다", "A/B 테스트로 모델을 비교한다", "프롬프트를 자동 최적화한다", "모델을 파인튜닝한다" ], "answer": "Dataset + Evaluator로 평가 파이프라인을 구성한다", "explanation": "LangSmith의 Dataset은 테스트 케이스(입출력 쌍)를 저장하고, Evaluator(LLM-as-judge 등)로 자동 채점합니다. CI에 통합해 프롬프트 변경 시 회귀 테스트를 할 수 있습니다.", "source_id": "official-docs-langchain" }, { "id": "docs-q29", "lesson_id": "docs-guardrails", "question_type": "multiple_choice", "question": "Guardrails AI에서 LLM 출력이 검증 실패 시 자동으로 LLM에 재요청하는 `on_fail` 동작은?", "options": [ "reask", "exception", "noop", "fix" ], "answer": "reask", "explanation": "`on_fail='reask'`는 검증 실패 시 LLM에 수정 요청을 자동으로 다시 보냅니다. `exception`은 예외 발생, `noop`은 그냥 통과, `fix`는 자동 수정을 시도합니다.", "source_id": "official-docs-guardrails" }, { "id": "docs-q30", "lesson_id": "docs-guardrails", "question_type": "multiple_choice", "question": "Guardrails AI로 LLM이 항상 지정한 JSON 스키마를 반환하도록 강제하는 방법은?", "options": [ "Guard.from_pydantic(OutputModel)으로 Guard를 생성한다", "LLM 프롬프트에 JSON 출력을 요청한다", "json.loads()로 출력을 파싱한다", "OpenAI의 function_calling 파라미터를 쓴다" ], "answer": "Guard.from_pydantic(OutputModel)으로 Guard를 생성한다", "explanation": "`Guard.from_pydantic(OutputModel)`은 Pydantic 모델을 JSON 스키마로 변환해 LLM 출력이 이 스키마를 따르도록 강제합니다. 형식이 맞지 않으면 reask 또는 exception이 발생합니다.", "source_id": "official-docs-guardrails" }, { "id": "docs-q31", "lesson_id": "docs-gradio", "question_type": "multiple_choice", "question": "Gradio에서 LLM 챗봇 UI를 가장 간단하게 만드는 방법은?", "options": [ "gr.ChatInterface(fn=chat_function)으로 채팅 함수를 감싼다", "gr.Interface(fn=chat)에 텍스트 입출력을 지정한다", "gr.Blocks()에 gr.Textbox를 수동으로 배치한다", "Streamlit의 st.chat_message를 임포트해 쓴다" ], "answer": "gr.ChatInterface(fn=chat_function)으로 채팅 함수를 감싼다", "explanation": "`gr.ChatInterface`는 채팅 UI에 최적화된 컴포넌트입니다. 함수가 `(message, history) -> str` 시그니처를 따르면 자동으로 대화 히스토리 UI와 Submit 버튼이 생성됩니다.", "source_id": "official-docs-gradio" }, { "id": "docs-q32", "lesson_id": "docs-gradio", "question_type": "multiple_choice", "question": "Gradio에서 복잡한 레이아웃(사이드바, 탭 등)을 자유롭게 구성하려면?", "options": [ "gr.Blocks() 컨텍스트 매니저 안에서 컴포넌트를 배치한다", "gr.Interface에 layout 파라미터를 지정한다", "CSS를 직접 수정한다", "Streamlit 컴포넌트를 Gradio에 임베드한다" ], "answer": "gr.Blocks() 컨텍스트 매니저 안에서 컴포넌트를 배치한다", "explanation": "`gr.Blocks()`는 Gradio의 저수준 레이아웃 API입니다. `with gr.Row():`, `with gr.Tab():` 등을 중첩해 자유롭게 UI를 구성하고 컴포넌트 간 이벤트를 연결할 수 있습니다.", "source_id": "official-docs-gradio" }, { "id": "docs-q33", "lesson_id": "docs-airflow", "question_type": "multiple_choice", "question": "Airflow DAG에서 `task_a >> task_b >> task_c`의 의미는?", "options": [ "task_a → task_b → task_c 순서로 순차 실행된다", "세 태스크가 동시에 병렬 실행된다", "task_c가 실패하면 task_a로 롤백된다", "task_b는 task_a와 task_c의 출력을 합친다" ], "answer": "task_a → task_b → task_c 순서로 순차 실행된다", "explanation": "`>>` 연산자는 실행 의존성을 정의합니다. `task_a >> task_b`는 task_a가 성공한 뒤에만 task_b가 실행된다는 의미입니다. 병렬 실행은 `[task_b, task_c] >> task_d`처럼 리스트로 표현합니다.", "source_id": "official-docs-airflow" }, { "id": "docs-q34", "lesson_id": "docs-airflow", "question_type": "multiple_choice", "question": "Airflow에서 task 간 소량의 데이터를 전달하는 메커니즘은?", "options": [ "XCom (Cross-Communication)", "전역 변수", "파일 시스템의 임시 파일", "Airflow Variable" ], "answer": "XCom (Cross-Communication)", "explanation": "XCom은 `ti.xcom_push(key, value)`로 데이터를 저장하고 다른 task에서 `ti.xcom_pull(task_ids, key)`로 가져옵니다. 대용량 데이터는 S3/GCS 경로를 XCom으로 전달하는 방식을 씁니다.", "source_id": "official-docs-airflow" }, { "id": "docs-q35", "lesson_id": "docs-mlflow", "question_type": "multiple_choice", "question": "MLflow에서 실험 파라미터와 메트릭을 올바르게 기록하는 방법은?", "options": [ "with mlflow.start_run(): 블록 안에서 mlflow.log_param()과 mlflow.log_metric()을 호출한다", "mlflow.track(params={}, metrics={})를 한 번 호출한다", "print() 출력이 자동으로 MLflow에 기록된다", "mlflow.autolog()만 호출하면 모든 것이 기록된다" ], "answer": "with mlflow.start_run(): 블록 안에서 mlflow.log_param()과 mlflow.log_metric()을 호출한다", "explanation": "`with mlflow.start_run():` 컨텍스트 매니저가 run을 생성하고, 그 안에서 `log_param`(파라미터), `log_metric`(메트릭), `log_artifact`(파일)를 호출합니다.", "source_id": "official-docs-mlflow" }, { "id": "docs-q36", "lesson_id": "docs-mlflow", "question_type": "multiple_choice", "question": "MLflow UI를 로컬에서 실행하는 명령어는?", "options": [ "mlflow ui", "mlflow server --start", "python -m mlflow.dashboard", "mlflow run --ui" ], "answer": "mlflow ui", "explanation": "`mlflow ui` 명령으로 localhost:5000에서 실험 비교 대시보드를 실행합니다. 실험 목록, 파라미터·메트릭 비교 차트, 아티팩트 브라우저를 웹 UI로 볼 수 있습니다.", "source_id": "official-docs-mlflow" }, { "id": "docs-q37", "lesson_id": "docs-mcp", "question_type": "multiple_choice", "question": "MCP(Model Context Protocol)에서 `tool`의 역할은?", "options": [ "LLM이 직접 호출할 수 있는 함수로, 입출력 스키마가 자동 생성된다", "LLM이 읽는 정적 데이터 소스", "재사용 가능한 프롬프트 템플릿", "MCP 서버의 설정 파일" ], "answer": "LLM이 직접 호출할 수 있는 함수로, 입출력 스키마가 자동 생성된다", "explanation": "MCP `tool`은 LLM이 대화 중 직접 호출할 수 있는 함수입니다. FastMCP의 `@mcp.tool()` 데코레이터를 붙이면 함수 시그니처에서 JSON Schema가 자동 생성됩니다.", "source_id": "official-docs-mcp" }, { "id": "docs-q51", "lesson_id": "docs-chroma", "question_type": "multiple_choice", "question": "FAISS와 비교했을 때 Chroma의 가장 큰 장점은?", "options": [ "persist_directory만 지정하면 서버 재시작 후에도 데이터가 자동 유지된다", "검색 속도가 FAISS보다 10배 빠르다", "GPU 없이 임베딩을 생성할 수 있다", "OpenAI API 없이 동작한다" ], "answer": "persist_directory만 지정하면 서버 재시작 후에도 데이터가 자동 유지된다", "explanation": "FAISS는 인메모리 기반으로 `save_local()`을 수동 호출해야 합니다. Chroma는 `persist_directory`를 지정하면 SQLite 기반 로컬 DB에 자동 저장되어 재시작 후에도 유지됩니다.", "source_id": "official-docs-chromadb" }, { "id": "docs-q52", "lesson_id": "docs-chroma", "question_type": "multiple_choice", "question": "기존 Chroma DB에서 메타데이터 조건으로 검색 범위를 제한하는 방법은?", "options": [ "db.similarity_search(query, filter={\"source\": \"file.pdf\"})", "db.search(query, where={\"source\": \"file.pdf\"})", "db.similarity_search(query, metadata=\"file.pdf\")", "Chroma는 메타데이터 필터링을 지원하지 않는다" ], "answer": "db.similarity_search(query, filter={\"source\": \"file.pdf\"})", "explanation": "`filter` 파라미터로 메타데이터 조건을 지정하면 해당 조건을 만족하는 문서 중에서만 유사도 검색을 합니다. FAISS에는 없는 기능입니다.", "source_id": "official-docs-chromadb" }, { "id": "docs-q53", "lesson_id": "docs-sentence-transformers", "question_type": "multiple_choice", "question": "LangChain에서 sentence-transformers 로컬 임베딩 모델을 사용하는 클래스는?", "options": [ "HuggingFaceEmbeddings", "SentenceTransformerEmbeddings", "LocalEmbeddings", "OllamaEmbeddings" ], "answer": "HuggingFaceEmbeddings", "explanation": "`from langchain_huggingface import HuggingFaceEmbeddings`로 가져와 `model_name`에 HuggingFace 모델 경로를 지정합니다. `sentence-transformers/all-MiniLM-L6-v2`가 가장 많이 쓰이는 기본 모델입니다.", "source_id": "official-docs-huggingface" }, { "id": "docs-q54", "lesson_id": "docs-sentence-transformers", "question_type": "multiple_choice", "question": "sentence-transformers 임베딩 모델을 교체할 때 반드시 해야 하는 작업은?", "options": [ "기존 VectorStore 전체를 새 모델로 재인덱싱한다", "모델 파일을 덮어쓰기만 하면 된다", "VectorStore의 dimension 파라미터를 수정한다", "임베딩 모델은 언제든 자유롭게 교체 가능하다" ], "answer": "기존 VectorStore 전체를 새 모델로 재인덱싱한다", "explanation": "임베딩 모델이 다르면 벡터 공간이 달라집니다. 기존 인덱스의 벡터와 새 모델로 생성한 쿼리 벡터가 호환되지 않아 검색 결과가 엉터리가 됩니다. 전체 재인덱싱이 필수입니다.", "source_id": "official-docs-huggingface" }, { "id": "docs-q55", "lesson_id": "docs-ragas", "question_type": "multiple_choice", "question": "Ragas의 Faithfulness 메트릭이 측정하는 것은?", "options": [ "LLM 답변이 검색된 컨텍스트에 근거하는가 (환각 탐지)", "검색된 문서가 질문과 관련 있는가", "답변이 ground_truth와 일치하는가", "RAG 파이프라인의 실행 속도" ], "answer": "LLM 답변이 검색된 컨텍스트에 근거하는가 (환각 탐지)", "explanation": "Faithfulness는 LLM이 검색된 컨텍스트에 없는 내용을 지어내지 않는지 측정합니다. 1.0에 가까울수록 환각이 없습니다. Answer Relevancy는 답변이 질문과 관련 있는지, Context Precision은 관련 문서가 잘 검색됐는지입니다.", "source_id": "official-docs-ragas" }, { "id": "docs-q56", "lesson_id": "docs-ragas", "question_type": "multiple_choice", "question": "Ragas evaluate() 함수에 필수로 전달해야 하는 데이터 키가 아닌 것은?", "options": [ "ground_truth (faithfulness만 계산할 경우)", "question", "answer", "contexts" ], "answer": "ground_truth (faithfulness만 계산할 경우)", "explanation": "`question`, `answer`, `contexts`는 기본 메트릭에 필수입니다. `ground_truth`는 Answer Correctness 메트릭에만 필요하며, faithfulness와 answer_relevancy만 계산할 경우 없어도 됩니다.", "source_id": "official-docs-ragas" }, { "id": "docs-q57", "lesson_id": "docs-langgraph", "question_type": "multiple_choice", "question": "LangGraph에서 노드의 실행 결과에 따라 다음 노드를 동적으로 결정하는 메서드는?", "options": [ "graph.add_conditional_edges()", "graph.add_edge()", "graph.set_next_node()", "graph.branch()" ], "answer": "graph.add_conditional_edges()", "explanation": "`graph.add_conditional_edges('node', router_fn, {'yes': 'next_node', 'no': END})`로 노드 반환값에 따라 다음 노드를 동적으로 결정합니다. 고정된 다음 노드는 `add_edge()`를 씁니다.", "source_id": "official-docs-langgraph" }, { "id": "docs-q58", "lesson_id": "docs-langgraph", "question_type": "multiple_choice", "question": "LangGraph StateGraph에서 State TypedDict를 정의하는 이유는?", "options": [ "노드 간 공유되는 데이터 구조를 타입 안전하게 정의하기 위해", "그래프 실행 속도를 높이기 위해", "LangChain 체인과 호환성을 위해", "State는 선택 사항으로 정의하지 않아도 된다" ], "answer": "노드 간 공유되는 데이터 구조를 타입 안전하게 정의하기 위해", "explanation": "State TypedDict는 모든 노드가 읽고 쓰는 공유 상태를 정의합니다. 각 노드 함수는 `state: State`를 받아 일부 키를 업데이트한 딕셔너리를 반환합니다.", "source_id": "official-docs-langgraph" }, { "id": "docs-q59", "lesson_id": "docs-litellm", "question_type": "multiple_choice", "question": "LiteLLM에서 Ollama 로컬 모델을 호출하는 model 파라미터 형식은?", "options": [ "\"ollama/llama3.2\"", "\"local/llama3.2\"", "\"llama3.2\"", "\"ollama:llama3.2\"" ], "answer": "\"ollama/llama3.2\"", "explanation": "LiteLLM의 모델명 형식은 `\"프로바이더/모델명\"`입니다. `\"ollama/llama3.2\"`, `\"anthropic/claude-3-5-sonnet-20241022\"`, `\"gpt-4o-mini\"`(OpenAI는 예외로 접두사 없음) 형식을 씁니다.", "source_id": "official-docs-litellm" }, { "id": "docs-q60", "lesson_id": "docs-litellm", "question_type": "multiple_choice", "question": "LiteLLM을 LangChain 체인에서 사용하는 방법은?", "options": [ "from langchain_community.chat_models import ChatLiteLLM로 가져와 LLM으로 사용한다", "litellm.completion()을 LangChain 체인에 직접 넣는다", "LiteLLM은 LangChain과 통합되지 않는다", "LangChain의 ChatOpenAI에 litellm=True 파라미터를 추가한다" ], "answer": "from langchain_community.chat_models import ChatLiteLLM로 가져와 LLM으로 사용한다", "explanation": "`ChatLiteLLM`은 LangChain의 ChatModel 인터페이스를 구현하므로 LCEL 파이프라인에 바로 연결됩니다. `chain = prompt | ChatLiteLLM(model='ollama/llama3.2') | parser` 형식으로 씁니다.", "source_id": "official-docs-litellm" }, { "id": "docs-q61", "lesson_id": "docs-chainlit", "question_type": "multiple_choice", "question": "Chainlit에서 사용자 메시지를 받아 처리하는 데코레이터는?", "options": [ "@cl.on_message", "@cl.on_input", "@cl.message_handler", "@cl.chat" ], "answer": "@cl.on_message", "explanation": "`@cl.on_message`는 사용자가 메시지를 보낼 때마다 자동으로 호출됩니다. 함수 파라미터로 `message: cl.Message`를 받아 `message.content`로 텍스트에 접근합니다.", "source_id": "official-docs-chainlit" }, { "id": "docs-q62", "lesson_id": "docs-chainlit", "question_type": "multiple_choice", "question": "Chainlit에서 LangChain 체인을 호출할 때 동기 invoke() 대신 써야 하는 이유와 대안은?", "options": [ "invoke()는 이벤트 루프를 블로킹하므로 ainvoke() 또는 astream()을 써야 한다", "invoke()가 더 빠르므로 항상 invoke()를 쓴다", "Chainlit은 LangChain과 호환되지 않는다", "invoke()와 ainvoke()의 차이는 없다" ], "answer": "invoke()는 이벤트 루프를 블로킹하므로 ainvoke() 또는 astream()을 써야 한다", "explanation": "Chainlit은 asyncio 기반입니다. `invoke()`(동기)를 `async def` 함수 안에서 호출하면 이벤트 루프가 블로킹되어 다른 사용자 요청을 처리 못 합니다. `await chain.ainvoke()`를 써야 합니다.", "source_id": "official-docs-chainlit" }, { "id": "docs-q63", "lesson_id": "docs-redis", "question_type": "multiple_choice", "question": "서버 재시작 후에도 대화 히스토리가 유지되도록 ChatMessageHistory를 교체하는 클래스는?", "options": [ "RedisChatMessageHistory", "PersistentChatMessageHistory", "SQLChatMessageHistory", "FileChatMessageHistory" ], "answer": "RedisChatMessageHistory", "explanation": "`RedisChatMessageHistory(session_id, url='redis://localhost:6379')`는 대화 히스토리를 Redis에 저장합니다. 기존 `ChatMessageHistory`(인메모리)와 인터페이스가 동일해 `get_session_history` 함수만 바꾸면 됩니다.", "source_id": "official-docs-redis" }, { "id": "docs-q64", "lesson_id": "docs-redis", "question_type": "multiple_choice", "question": "Redis에서 대화 히스토리가 무한히 쌓이지 않도록 자동 만료를 설정하는 파라미터는?", "options": [ "ttl (초 단위)", "expire (분 단위)", "max_messages", "timeout" ], "answer": "ttl (초 단위)", "explanation": "`RedisChatMessageHistory(session_id, url, ttl=3600)`으로 1시간(3600초) 후 자동 만료됩니다. TTL이 없으면 히스토리가 무한히 쌓여 토큰 초과와 메모리 누수가 발생합니다.", "source_id": "official-docs-redis" }, { "id": "docs-q65", "lesson_id": "docs-unstructured", "question_type": "multiple_choice", "question": "Unstructured로 PDF 파일을 로드하려면 추가로 설치해야 하는 패키지는?", "options": [ "pip install unstructured[pdf]", "pip install unstructured-pdf", "pip install pdfminer", "pip install unstructured만으로 충분하다" ], "answer": "pip install unstructured[pdf]", "explanation": "`pip install unstructured`는 기본 패키지만 설치합니다. PDF 처리는 `unstructured[pdf]`, 이미지 OCR은 `unstructured[local-inference]`처럼 별도 extra를 설치해야 합니다.", "source_id": "official-docs-unstructured" }, { "id": "docs-q66", "lesson_id": "docs-unstructured", "question_type": "multiple_choice", "question": "Unstructured를 `mode='elements'`로 로드할 때 얻을 수 있는 추가 정보는?", "options": [ "Title, NarrativeText, Table 등 요소 타입이 metadata에 저장된다", "OCR 정확도가 높아진다", "이미지 파일도 텍스트로 변환된다", "청크 분할이 자동으로 된다" ], "answer": "Title, NarrativeText, Table 등 요소 타입이 metadata에 저장된다", "explanation": "`mode='elements'`로 로드하면 각 Document에 `metadata['category']`로 요소 타입이 저장됩니다. 헤더를 청크 메타데이터로 활용하거나 테이블만 별도 추출할 때 씁니다.", "source_id": "official-docs-unstructured" }, { "id": "docs-q45", "lesson_id": "docs-document-loaders", "question_type": "multiple_choice", "question": "LangChain `DirectoryLoader`로 하위 폴더까지 모든 `.md` 파일을 로드하는 glob 패턴은?", "options": [ "**/*.md", "*.md", "./**md", "**md" ], "answer": "**/*.md", "explanation": "`**/*.md`는 현재 디렉토리와 모든 하위 디렉토리에서 `.md` 파일을 재귀 검색합니다. `*.md`는 현재 폴더만 검색합니다.", "source_id": "official-docs-langchain" }, { "id": "docs-q46", "lesson_id": "docs-document-loaders", "question_type": "multiple_choice", "question": "LangChain Document 객체에서 파일 경로(출처)를 가져오는 코드는?", "options": [ "doc.metadata['source']", "doc.source", "doc.file_path", "doc.metadata.path" ], "answer": "doc.metadata['source']", "explanation": "Document Loader는 로드된 문서의 `metadata` 딕셔너리에 `source` 키로 파일 경로를 저장합니다. RAG 답변에 출처를 표시할 때 이 값을 씁니다.", "source_id": "official-docs-langchain" }, { "id": "docs-q47", "lesson_id": "docs-summarization", "question_type": "multiple_choice", "question": "LangChain에서 여러 뉴스 기사처럼 컨텍스트 창보다 긴 문서를 요약할 때 적합한 chain_type은?", "options": [ "map_reduce", "stuff", "direct", "compress" ], "answer": "map_reduce", "explanation": "`map_reduce`는 각 청크를 개별 요약(map)한 후 합쳐서 최종 요약(reduce)을 만듭니다. `stuff`는 전체를 한 프롬프트에 넣으므로 긴 문서에서 토큰 초과 오류가 납니다.", "source_id": "official-docs-langchain" }, { "id": "docs-q48", "lesson_id": "docs-summarization", "question_type": "multiple_choice", "question": "`load_summarize_chain` 실행 결과에서 최종 요약 텍스트를 가져오는 키는?", "options": [ "result['output_text']", "result['summary']", "result['text']", "result['answer']" ], "answer": "result['output_text']", "explanation": "`chain.invoke(docs)`의 반환값 딕셔너리에서 요약 결과는 `output_text` 키에 저장됩니다.", "source_id": "official-docs-langchain" }, { "id": "docs-q49", "lesson_id": "docs-sql-chain", "question_type": "multiple_choice", "question": "LangChain에서 SQLite DB를 `SQLDatabase`로 연결하는 코드는?", "options": [ "SQLDatabase.from_uri('sqlite:///./mydb.db')", "SQLDatabase.connect('./mydb.db')", "sqlite3.connect('./mydb.db')", "SQLDatabase('./mydb.db')" ], "answer": "SQLDatabase.from_uri('sqlite:///./mydb.db')", "explanation": "`SQLDatabase.from_uri()`는 SQLAlchemy 연결 문자열을 받습니다. SQLite는 `sqlite:///경로`, PostgreSQL은 `postgresql://user:pass@host/db` 형식입니다.", "source_id": "official-docs-langchain" }, { "id": "docs-q50", "lesson_id": "docs-sql-chain", "question_type": "multiple_choice", "question": "LangChain Text-to-SQL에서 `SQLDatabase`가 LLM에 자동으로 제공하는 정보는?", "options": [ "DB의 테이블명과 컬럼 스키마", "DB에 저장된 실제 데이터 샘플", "SQL 실행 결과 히스토리", "DB 접속 비밀번호" ], "answer": "DB의 테이블명과 컬럼 스키마", "explanation": "`SQLDatabase`는 연결된 DB의 테이블명과 각 컬럼의 이름·타입을 자동으로 프롬프트에 주입합니다. 덕분에 LLM이 실제 존재하는 테이블/컬럼만 참조하는 SQL을 생성할 수 있습니다.", "source_id": "official-docs-langchain" }, { "id": "docs-q39", "lesson_id": "docs-dotenv", "question_type": "multiple_choice", "question": "python-dotenv에서 `.env` 파일을 로드하는 올바른 방법은?", "options": [ "from dotenv import load_dotenv; load_dotenv()", "import dotenv; dotenv.read('.env')", "os.environ.load('.env')", ".env 파일은 자동으로 로드된다" ], "answer": "from dotenv import load_dotenv; load_dotenv()", "explanation": "`load_dotenv()`를 명시적으로 호출해야 `.env` 파일의 변수가 `os.environ`에 주입됩니다. 파일 상단에서 가장 먼저 호출하는 것이 중요합니다.", "source_id": "official-docs-python" }, { "id": "docs-q40", "lesson_id": "docs-dotenv", "question_type": "multiple_choice", "question": ".env 파일을 사용할 때 보안을 위해 반드시 해야 하는 것은?", "options": [ ".env를 .gitignore에 추가하고 .env.example을 커밋한다", ".env를 암호화해서 저장소에 커밋한다", ".env 파일명을 다르게 지어 숨긴다", "API 키를 짧게 줄여서 저장한다" ], "answer": ".env를 .gitignore에 추가하고 .env.example을 커밋한다", "explanation": "`.env`는 절대 Git에 커밋하지 않습니다. `.env.example`에는 키 이름만(값 없이) 적어 어떤 환경변수가 필요한지 문서화하는 용도로 커밋합니다.", "source_id": "official-docs-python" }, { "id": "docs-q41", "lesson_id": "docs-prompt-template", "question_type": "multiple_choice", "question": "LangChain에서 시스템 역할과 사용자 메시지를 구분해 프롬프트를 만드는 클래스는?", "options": [ "ChatPromptTemplate", "PromptTemplate", "SystemPrompt", "MessageTemplate" ], "answer": "ChatPromptTemplate", "explanation": "`ChatPromptTemplate.from_messages([(\"system\", \"...\"), (\"human\", \"{question}\")])` 형식으로 역할별 메시지를 구조화합니다. `PromptTemplate`은 역할 구분 없는 단순 문자열 포맷용입니다.", "source_id": "official-docs-langchain" }, { "id": "docs-q42", "lesson_id": "docs-prompt-template", "question_type": "multiple_choice", "question": "ChatPromptTemplate에서 대화 히스토리 메시지 배열을 주입하는 방법은?", "options": [ "(\"placeholder\", \"{chat_history}\") 또는 MessagesPlaceholder(variable_name=\"chat_history\")를 messages에 추가한다", "chat_history 문자열을 system 메시지에 직접 포함한다", "히스토리는 LLM이 자동으로 관리하므로 별도 처리가 불필요하다", "HistoryTemplate 클래스를 별도로 사용한다" ], "answer": "(\"placeholder\", \"{chat_history}\") 또는 MessagesPlaceholder(variable_name=\"chat_history\")를 messages에 추가한다", "explanation": "`MessagesPlaceholder`는 메시지 리스트 전체를 그 자리에 펼쳐줍니다. RunnableWithMessageHistory의 `history_messages_key`와 placeholder의 변수명이 일치해야 합니다.", "source_id": "official-docs-langchain" }, { "id": "docs-q43", "lesson_id": "docs-conversation-memory", "question_type": "multiple_choice", "question": "LangChain에서 멀티턴 대화 히스토리를 체인에 자동으로 주입하는 클래스는?", "options": [ "RunnableWithMessageHistory", "ConversationChain", "ChatMemoryBuffer", "HistoryRunnable" ], "answer": "RunnableWithMessageHistory", "explanation": "`RunnableWithMessageHistory`는 기존 체인을 감싸 `session_id`별로 히스토리를 관리합니다. `get_session_history` 함수로 저장소를 지정하면 자동으로 이전 대화가 주입됩니다.", "source_id": "official-docs-langchain" }, { "id": "docs-q44", "lesson_id": "docs-conversation-memory", "question_type": "multiple_choice", "question": "RunnableWithMessageHistory에서 모든 사용자가 동일한 대화 히스토리를 공유하는 원인은?", "options": [ "invoke 시 config에 동일한 session_id를 사용했다", "ChatMessageHistory를 클래스 레벨에서 하나만 생성했다", "get_session_history 함수를 정의하지 않았다", "RunnableWithMessageHistory 대신 체인을 직접 사용했다" ], "answer": "invoke 시 config에 동일한 session_id를 사용했다", "explanation": "`config={\"configurable\": {\"session_id\": \"fixed-id\"}}`처럼 고정 ID를 쓰면 모든 사용자가 같은 히스토리를 씁니다. 사용자별로 고유한 session_id(UUID 등)를 생성해야 합니다.", "source_id": "official-docs-langchain" }, { "id": "docs-q38", "lesson_id": "docs-mcp", "question_type": "multiple_choice", "question": "Claude Desktop에 로컬 MCP 서버를 연결하려면 어디에 등록해야 하는가?", "options": [ "~/Library/Application Support/Claude/claude_desktop_config.json", "~/.claude/mcp_servers.json", "CLAUDE.md 파일", "환경변수 MCP_SERVER_PATH" ], "answer": "~/Library/Application Support/Claude/claude_desktop_config.json", "explanation": "Claude Desktop의 MCP 서버 목록은 `claude_desktop_config.json`에 등록합니다. 서버 이름, 실행 명령어(`command`), 인수(`args`)를 JSON으로 정의하면 앱 재시작 후 자동 연결됩니다.", "source_id": "official-docs-mcp" }, { "id": "docs-q67", "lesson_id": "docs-vectorstore", "question_type": "multiple_choice", "question": "**[상황 판단]** 서버를 재시작할 때마다 벡터 인덱스가 사라지는 문제가 발생했습니다. 개발팀에서 `save_local()` 호출을 종종 빠뜨립니다. 어떤 VectorStore를 선택하고 그 이유는?", "options": [ "Chroma — persist_directory만 지정하면 save_local() 없이 자동 저장되므로", "FAISS — 성능이 더 빠르고 클라우드 배포에 유리하므로", "pgvector — PostgreSQL 기반이라 안정성이 높으므로", "Qdrant — 대규모 벡터 검색에 최적화되어 있으므로" ], "answer": "Chroma — persist_directory만 지정하면 save_local() 없이 자동 저장되므로", "explanation": "이 상황의 근본 문제는 수동 저장 호출을 빠뜨리는 것입니다. Chroma는 `persist_directory='./chroma_db'`만 지정하면 자동으로 저장되므로 실수 가능성이 없습니다. FAISS는 성능이 좋지만 `save_local()`을 반드시 호출해야 해 이 문제를 해결하지 못합니다.", "source_id": "official-docs-chromadb", "difficulty": "medium" }, { "id": "docs-q68", "lesson_id": "docs-sentence-transformers", "question_type": "multiple_choice", "question": "**[상황 판단]** RAG 시스템에 20만 개의 문서를 인덱싱해야 합니다. OpenAI 임베딩 API를 쓰면 비용이 예상보다 10배 나올 것 같습니다. 어떤 선택이 가장 적합한가?", "options": [ "sentence-transformers의 all-MiniLM-L6-v2로 로컬 임베딩 — 무료이고 인터페이스가 동일하므로", "OpenAI 임베딩 유지 — 품질이 중요하므로 비용을 감수한다", "임베딩을 사용하지 않고 키워드 검색만 쓴다", "Chroma로 교체하면 임베딩 비용이 절감된다" ], "answer": "sentence-transformers의 all-MiniLM-L6-v2로 로컬 임베딩 — 무료이고 인터페이스가 동일하므로", "explanation": "`HuggingFaceEmbeddings(model_name='sentence-transformers/all-MiniLM-L6-v2')`는 OpenAI Embeddings와 인터페이스가 동일해 코드를 한 줄만 바꾸면 됩니다. 로컬 실행이므로 API 비용이 없고, 대용량 인덱싱에 경제적입니다. 한국어 많으면 `jhgan/ko-sroberta-multitask`도 고려합니다.", "source_id": "official-docs-huggingface", "difficulty": "medium" }, { "id": "docs-q69", "lesson_id": "docs-summarization", "question_type": "multiple_choice", "question": "**[상황 판단]** 뉴스 기사 50개를 동시에 LLM에 넣어 요약하려다 토큰 초과 오류가 났습니다. 가장 적절한 해결 방법은?", "options": [ "load_summarize_chain(llm, chain_type='map_reduce') 사용 — 기사를 개별 요약 후 합치므로 토큰 초과가 없음", "모든 기사를 하나의 프롬프트에 넣는 stuff 방식을 유지하되 LLM 모델을 교체", "기사 개수를 5개로 줄여 stuff 방식 유지", "summarization 대신 RAG로 기사를 검색해 요약" ], "answer": "load_summarize_chain(llm, chain_type='map_reduce') 사용 — 기사를 개별 요약 후 합치므로 토큰 초과가 없음", "explanation": "`map_reduce`는 각 기사를 독립적으로 요약(map)한 뒤 개별 요약들을 합쳐 최종 요약(reduce)을 만듭니다. 기사가 100개든 1000개든 토큰 초과가 발생하지 않습니다. 모델 교체나 기사 수 감소는 근본 해결책이 아닙니다.", "source_id": "official-docs-langchain", "difficulty": "medium" }, { "id": "docs-q70", "lesson_id": "docs-langsmith", "question_type": "multiple_choice", "question": "**[상황 판단]** RAG 챗봇이 엉뚱한 답을 했습니다. 문제가 검색 단계인지, 프롬프트 렌더링 단계인지, LLM 생성 단계인지 파악이 안 됩니다. `print()`로 디버깅 중인데 어느 단계인지 구분이 어렵습니다. 가장 효과적인 도구는?", "options": [ "LangSmith — 체인 각 단계의 입출력을 트리 형태로 시각화하므로", "Python logging 모듈로 더 상세한 로그 출력", "LLM 모델을 더 강력한 것으로 교체", "chunk_size를 줄여 검색 정확도 향상" ], "answer": "LangSmith — 체인 각 단계의 입출력을 트리 형태로 시각화하므로", "explanation": "LangSmith는 환경변수 3개(`LANGCHAIN_TRACING_V2=true`, `LANGCHAIN_API_KEY`, `LANGCHAIN_PROJECT`) 설정만으로 체인 실행마다 retriever 입출력, 렌더링된 프롬프트, LLM 응답을 단계별로 기록합니다. 어느 단계가 문제인지 즉시 특정할 수 있습니다.", "source_id": "official-docs-langchain", "difficulty": "medium" }, { "id": "docs-q71", "lesson_id": "docs-guardrails", "question_type": "multiple_choice", "question": "**[상황 판단]** 서비스 사용자가 \"욕설을 섞어 요약해줘\"라고 입력했더니 LLM이 그대로 따랐습니다. 시스템 프롬프트에 '욕설 금지'를 추가했지만 우회되는 경우가 있습니다. 더 강력한 대응은?", "options": [ "Guardrails AI의 ToxicLanguage Guard를 LLM 출력에 적용 — 확률적으로 유해 콘텐츠를 실질적으로 차단", "시스템 프롬프트를 더 길고 자세하게 작성", "더 강력한 LLM 모델로 교체", "사용자 입력 길이를 100자로 제한" ], "answer": "Guardrails AI의 ToxicLanguage Guard를 LLM 출력에 적용 — 확률적으로 유해 콘텐츠를 실질적으로 차단", "explanation": "시스템 프롬프트는 프롬프트 인젝션으로 우회될 수 있습니다. Guardrails AI는 LLM 출력을 별도 분류 모델로 검증해 임계값 이상이면 예외를 발생시킵니다. `on_fail='reask'`로 설정하면 자동으로 재작성을 요청합니다. 시스템 프롬프트보다 실질적인 보호 수단입니다.", "source_id": "official-docs-guardrails", "difficulty": "hard" }, { "id": "docs-q72", "lesson_id": "docs-redis", "question_type": "multiple_choice", "question": "**[상황 판단]** 챗봇 서비스를 배포했더니 서버를 재시작할 때마다 모든 사용자의 대화 기록이 초기화됩니다. 인메모리 딕셔너리로 히스토리를 관리하고 있습니다. 최소한의 코드 변경으로 해결하는 방법은?", "options": [ "get_session_history 함수 내부를 RedisChatMessageHistory로 교체 — 나머지 코드는 그대로", "RunnableWithMessageHistory를 제거하고 매 요청마다 히스토리를 DB에 직접 저장", "Streamlit session_state에 히스토리를 저장", "대화 히스토리 없이 단일 턴으로 서비스를 재설계" ], "answer": "get_session_history 함수 내부를 RedisChatMessageHistory로 교체 — 나머지 코드는 그대로", "explanation": "`RunnableWithMessageHistory`는 `get_session_history` 함수만 보고 히스토리를 관리합니다. 이 함수 내부를 `RedisChatMessageHistory(session_id, url='redis://localhost:6379')`로 바꾸기만 하면 됩니다. 다른 코드는 전혀 수정할 필요가 없습니다.", "source_id": "official-docs-redis", "difficulty": "hard" }, { "id": "docs-q73", "lesson_id": "docs-litellm", "question_type": "multiple_choice", "question": "**[상황 판단]** 로컬 Ollama로 개발하고 프로덕션에는 Anthropic Claude를 쓸 계획입니다. 매번 LLM 교체 시 코드를 최소로 수정하려면 어떻게 구현해야 하는가?", "options": [ "LiteLLM completion()을 사용하고 model 파라미터만 변경 — 코드 나머지는 그대로", "OllamaLLM과 ChatAnthropic을 if문으로 분기", "개발/프로덕션마다 다른 코드베이스를 유지", "LangChain의 ChatOpenAI를 기본으로 Ollama는 별도 브랜치에서 관리" ], "answer": "LiteLLM completion()을 사용하고 model 파라미터만 변경 — 코드 나머지는 그대로", "explanation": "LiteLLM은 `completion(model='ollama/llama3.2', ...)`과 `completion(model='anthropic/claude-3-5-sonnet-20241022', ...)`의 코드가 동일합니다. model 파라미터 한 줄만 바꾸면 프로바이더를 전환합니다. if문 분기나 별도 브랜치 관리는 유지보수 비용이 높습니다.", "source_id": "official-docs-litellm", "difficulty": "medium" }, { "id": "docs-q74", "lesson_id": "docs-langgraph", "question_type": "multiple_choice", "question": "**[상황 판단]** RAG 챗봇에 다음 기능을 추가해야 합니다: \"검색 결과 품질이 낮으면 다른 검색어로 재시도하고, 3번 이상 실패하면 '찾지 못했습니다'를 반환.\" LCEL 체인만으로 이 기능을 구현할 수 있는가?", "options": [ "불가능 — LCEL은 선형이라 루프와 조건 분기가 없다. LangGraph를 써야 한다", "가능 — LCEL의 RunnableBranch로 조건 분기를 구현할 수 있다", "가능 — Python while문을 chain.invoke() 바깥에 쓰면 된다", "가능 — retry 파라미터를 chain에 전달하면 된다" ], "answer": "불가능 — LCEL은 선형이라 루프와 조건 분기가 없다. LangGraph를 써야 한다", "explanation": "LCEL은 A→B→C 선형 파이프라인입니다. '품질 판단 → 재시도 여부 결정 → 루프' 같은 반복/분기 로직은 표현할 수 없습니다. LangGraph의 `add_conditional_edges`와 루프 구조가 이런 에이전트 행동 패턴에 적합합니다.", "source_id": "official-docs-langgraph", "difficulty": "hard" }, { "id": "docs-q75", "lesson_id": "docs-airflow", "question_type": "multiple_choice", "question": "**[상황 판단]** 뉴스 요약 스크립트를 완성했습니다. 매일 오전 6시에 자동으로 실행되고 실패 시 알림을 받고 싶습니다. 담당자 없이도 주말에 동작해야 합니다. 가장 적합한 도구는?", "options": [ "Airflow — DAG로 스케줄 정의, 실패 알림, 이전 실행 이력 관리가 가능", "cron 스크립트 — 가장 단순하고 추가 설치가 없음", "Streamlit 버튼 — 수동 실행이지만 UI가 있음", "GitHub Actions — CI/CD 환경에서 실행 가능" ], "answer": "Airflow — DAG로 스케줄 정의, 실패 알림, 이전 실행 이력 관리가 가능", "explanation": "단순 cron도 스케줄링은 되지만 실패 시 알림, 이전 실행 이력 확인, 재실행 기능이 없습니다. Airflow는 `@daily` 스케줄로 자동 실행되고, 실패 시 이메일/Slack 알림, 과거 실행 이력 조회, 수동 재실행이 모두 지원됩니다. 담당자 없이 운영하는 프로덕션 파이프라인에 적합합니다.", "source_id": "official-docs-airflow", "difficulty": "medium" }, { "id": "docs-q76", "lesson_id": "docs-fastapi", "question_type": "multiple_choice", "question": "**[상황 판단]** RAG 챗봇을 완성했습니다. 프론트엔드 팀(React), 모바일 팀(Flutter), 데이터 팀(Python)이 모두 이 기능을 사용해야 합니다. Streamlit 앱 그대로 배포하는 것이 좋을까요?", "options": [ "아니오 — FastAPI로 REST API를 만들어 공유하는 것이 적합. 각 팀이 자기 방식으로 연결 가능", "예 — Streamlit UI를 iframe으로 각 팀 서비스에 삽입하면 됨", "예 — Streamlit은 API 기능도 자동으로 제공함", "아니오 — Gradio로 교체해야 다른 팀과 공유 가능" ], "answer": "아니오 — FastAPI로 REST API를 만들어 공유하는 것이 적합. 각 팀이 자기 방식으로 연결 가능", "explanation": "Streamlit은 UI와 로직이 결합된 단일 앱입니다. 다른 팀이 LLM 기능을 재사용하려면 API가 필요합니다. FastAPI로 `POST /ask` 엔드포인트를 만들면 React, Flutter, Python 어디서든 HTTP 요청으로 접근할 수 있습니다. Streamlit은 개인/팀 내부 도구, FastAPI는 서비스 통합에 적합합니다.", "source_id": "official-docs-fastapi", "difficulty": "medium" } ]