Ⅰ. 오전 수업
A. 1교시
1. 지난 시간 복습
2. 실습
B. 2교시
1. DB에서 값 가져오기
C. 3교시
1. DB에서 값 가져오기 (cont.)
Ⅱ. 오후 수업
A. 4교시
1. 지난 시간 복습
2. 유효성 검사
3. LangChain 구성 요소
B. 5교시
1. State
2. Node
3. Edge
C. 6교시
1. Conditional Edge
2. 시작점 설정 및 시각화
Ⅲ. CAREER UP
app.set() 세팅 (for EJS)
.render() 사용 →<% %> : JS의 문법 작성이 가능한 공간 → 조건문, 반복문<%= %> : 넘겨준 값을 사용하는 공간오늘은 라우터 하나로 전부 처리해봅시다!


* nick 님 환영합니다!
* 로그아웃/회원목록 리스트3) 로그아웃 → 실제 로그아웃: "세션(session)" 개념 필요
app.use(express.urlencoded({extended:true}));app.set("view engine","ejs");app.set("views",__dirname+"/views");react를 쓰면 view 폴더 대신 react 폴더 → build






action="http://localhost:3000/login"


conn.connect();module.exports = conn;const conn = require("../config/db"); 

조건문: 삼항연산자로 쓸 수도 있음!
router.post("/login",(req,res)=>{ let {id,pw}=req.body; let sql="SELECT nick FROM member WHERE id=? AND pw=?" conn.query(sql, [id, pw], (err, rows) => { console.log(rows); rows.length > 0 ? res.render("main", {nick: rows}) : res.render("login"); }); });→ 더 깔끔

여기서도 삼항연산자 쓸 수 있는데 주의해야 할 점이 있음!
<%= nick == null ? '<a href="/login">로그인</a>' : `<strong>${nick} 님 환영합니다.</strong><a href="#">로그아웃</a>` %>HTML 이스케이프:
위처럼<%= ... %>로 출력하면 문자열이 이스케이프(escape)되어<a>태그가 "그대로" HTML로 인식되지 않고, 브라우저에는 문자열로 보일 수 있습니다.
해결 방법:
HTML 태그가 그대로 동작하려면<%- ... %>(unescaped output)를 사용하세요.<%- nick == null ? '<a href="/login">로그인</a>' : `<strong>${nick} 님 환영합니다.</strong><a href="#">로그아웃</a>` %>




{rows[0]}) 오류가 나지만 객체 구조로 넘기는 경우에는 rows가 '값'이기 때문에 인덱싱이 가능함({user:rows[0]})


의미론적 차이 이해하기
: 변수 이름 수정 & 인덱스 제거 → 점점 볼륨을 줄여 나가는 과정
※ 최대한 간결하게 적어야 함! (가독성)
→ EJS는 원래도 가독성 문제가 있어서 최대한 간결하게 작성해 주는 게 좋음









예시:
"'크라임씬 제로'를 스트리망하는 회사의 2024년도 매출액을 알려줘"
↓
문서: '크라임씬 제로'는 넷플릭스에서 스트리밍합니다.
↓
1번 LLM
답변: '크라임씬 제로'를 스트리밍하는 회사는 넷플릭스입니다.
하지만 매출액 정보는 문서에 나와있지 않습니다.
↓
2번 LLM
검색 쿼리 작성
"'크라임씬 제로'를 스트리망하는 회사인 넷플릭스의 2024년도 매출액"
↓
검색 실행
'크라임씬 제로'를 만든 스튜디오슬램의 2024년 매출액은 116억 7천만원입니다.
↓
1번 LLM
답변: '크라임씬 제로'를 스트리밍하는 회사는 넷플릭스입니다.
하지만 매출액 정보는 문서에 나와있지 않습니다.
↓
2번 LLM
검색 쿼리 작성
"'크라임씬 제로'를 스트리망하는 회사인 넷플릭스의 2024년도 매출액"
↓
…





# 유효한 데이터 생성
try:
valid_member = Member(
name = "홍길동"
, age = 20
)
print("유효한 고객 데이터", valid_member)
except ValidationError as e:
print("유효성 검사 오류:")
for error in e.errors():
print(f"{error['loc'][0]}: {error['msg']}")
유효한 고객 데이터 name='홍길동' age=20
errors() 안에 있는 것errors(): 에러 dict의 리스트를 반환# 유효하지 않은 데이터 생성
try:
valid_member = Member(
name = "톰" # 너무 짧은 이름
, age = 15 # 범위를 벗어난 나이
)
print("유효한 고객 데이터", valid_member)
except ValidationError as e:
print("유효성 검사 오류:")
for error in e.errors():
print(f"{error['loc'][0]}: {error['msg']}")
유효성 검사 오류:
name: String should have at least 2 characters
age: Input should be greater than 18
여러 개의 노드를 연결하여 흐름을 구성해 나갑니다.



from typing import TypedDict
# 정보를 전달할 때 타입을 지정해서 전달
class GraphState(TypedDict):
time: int # 시간
name: str # 사용자 이름
llm: str # 모델 이름
# 키별로 느슨하게 정보를 전달하는 방법
from typing import NotRequired
class GraphState(TypedDict):
time: NotRequired[int] # '삼'. '3', '세 번째' 등의 결과를 받을 수 있게 됨
name: str
llm: str

→ 첫 번째 행동이 끝났을 때((4) Evaluate: score BAD까지 완료된 후) 선택할 수 있는 행동들이 다양함
1. 질문 재작성: Question node로 edge 연결



# LangGraph에서 사용할 상태(State) 설계도
class GraphState2(TypedDict):
context:str
question:str
answer:str
score:str
relevance:str
relevance?
해당 문맥(context)과 질문(question) 간의 "관련성"을 평가한 결과 값을 의미합니다. 즉, context와 question이 얼마나 잘 맞는지, context가 실제로 question에 대한 답을 포함하는지 판단한 점수(문자열)로 저장relevance의 의미
- relevance는 주로 retrieval, RAG, 또는 QA 워크플로우에서 "검색된 context가 입력 질문에 충분히 연관되어 있는가?"를 평가하는 기능입니다.- 보통 OpenAI 등의 LLM이나 별도 평가 함수(GroundednessChecker 등)를 사용하여 context와 question의 연관성을 평가해서 "yes"/"no" 또는 점수(예: 0~1)로 기록합니다.
- relevance 값은 이후 플로우 제어(예: 관련성이 낮으면 context 재검색, 높으면 답변 생성 등)에 활용됩니다.
예시
- 예를 들어, context가 "AI 관련 논문 요약"이고 question이 "AI가 사회에 미치는 영향은?"이라면 relevance가 높을 수 있습니다.
- relevance가 너무 낮으면, 답변 생성 대신 context를 재검색하거나 워크플로우를 다른 방향으로 유도할 수 있습니다.
참고 코드 패턴
class GraphState(TypedDict): context: str question: str answer: str relevance: str
- relevance는 각 노드에서 평가된 관련성 정보를 담으며, 다음 플로우 제어에 활용됩니다.
즉, relevance는 context와 question의 "연관성 평가 결과"이며, LangChain/Graph 워크플로우에서 답변 신뢰도 제어에 중요한 역할을 담당합니다.
# retrieve node 정의
def retrieve_Node(state: GraphState2) -> GraphState2:
# Question에 대한 검색 문서를 retriever로 수행
# retrieve_docs = 검색기이름.invoke(state["question"])
retrieve_docs = "문서1"
# 원래는 주석 처리한 부분처럼 써야 하지만 흐름 파악을 위해 간단하게 작성
# 검색한 문서를 context 키에 저장하여 반환
return GraphState2(context=retrieve_docs)
# llm_answer node 정의
def llm_answer_Node(state: GraphState2) -> GraphState2:
state['answer'] = '답변1'
# '답변1'이라는 예시 데이터를 받았다고 가정
return state
# relevance_check node 정의
def relevance_check_Node(state: GraphState2) -> GraphState2:
state['relevance'] = 'grounded'
# '관련이 있음'이라는 예시 데이터를 받았다고 가정
return state
add_node("노드이름",노드함수명)from langgraph.graph import StateGraph
# 그래프 생성
graph_builder = StateGraph(GraphState2)
# 노드 추가
graph_builder.add_node("retrieve", retrieve_Node)
graph_builder.add_node("answer", llm_answer_Node)
graph_builder.add_node("relevance", relevance_check_Node)
<langgraph.graph.state.StateGraph at 0x794560487740>
add_edge("시작노드명","다음노드명")graph_builder.add_edge("retrieve", "answer")
graph_builder.add_edge("answer", "relevance")
<langgraph.graph.state.StateGraph at 0x794560487740>
add_conditional_edges(source, path, path_map)lambda state: state["relevance"]from langgraph.graph import END
# 관련성이 있는지 확인하는 함수
def is_relevance(state: GraphState2):
return state["relevance"]
# retrieve 노드에 조건부 엣지를 추가
graph_builder.add_conditional_edges(
source="relevance" # 조건을 시작할 노드 이름
, path=is_relevance # 람다 함수로 처리해도 됨: lambda state: state["relevance"]
, path_map={
"grounded": END # 관련성이 있으면 END → 끝내세요~
, "NotGounded": "retrieve" # 관련성이 없으면 → 재검색하세요~
, "NotSure": "answer" # 결과가 모호하면 → 다시 답변 생성하세요~
}
)
<langgraph.graph.state.StateGraph at 0x794560487740>

set_entry_point("노드명"): 해당 노드를 graph의 시작점으로 설정# 시작점 설정
# 질문 후 검색 결과를 받았다고 가정 하에 진행
graph_builder.set_entry_point("retrieve")
from langchain_teddynote.graphs import visualize_graph
# 그래프 컴파일
graph = graph_builder.compile()
# 시각화
visualize_graph(graph)

다음주에는 실제로 전체 다 연결해볼 것
복습 꼭 하고 공부해서 오기!
추가: 아래 위키독스 꼭 읽어보기
LangGraph 가이드북 - 에이전트 RAG with 랭그래프
CH17 LangGraph ★★★