from typing import Literal
from pydantic import BaseModel, Field
from langchain_openai import ChatOpenAI
class Feedback(BaseModel):
grade : Literal["funny","not funny"] = Field(description="농담이 재미있는지 평가")
feedback : str = Field(description="재미없다면 개선 방안 제시")
llm = ChatOpenAI(model="gpt-4o-mini",temperature=0)
evaluator = llm.with_structured_output(Feedback)
| 라이브러리 | 역할 |
|---|---|
| Literal | 아무 문자열이나 받는게 아니라 여기서 지정한 값들 중 하나만 허용하도록함 |
| BaseModel | BaseModel 상속을 통해서 어떤 필드를 가지고, 각 필드는 무슨 타입인지를 선언할 수 있음 |
| Field | 기본값, 설명, 검증 관련 옵션등을 지정 |
| 항목 | 프롬프트만 사용하는 방식 | with_structured_output() 방식 |
|---|---|---|
| 출력 형식 제어 방식 | 자연어 지시로 형식 요청 | JSON schema 기반 구조 강제 + 파싱 |
| 형식 안정성 | 낮음 (모델이 종종 어김) | 높음 (스키마 기반 검증 포함) |
| 타입 검증 | 없음 | 있음 (Pydantic validation) |
enum 값 제한 (Literal) | 불가능 | 가능 |
| 필드 누락 감지 | 직접 구현 필요 | 자동 감지 |
| JSON 파싱 안정성 | 직접 처리 필요 | 자동 처리 |
| 실패 처리 | 직접 예외 처리 필요 | validation error 발생 가능 |
| 코드 복잡도 | 단순 (초기 작성 쉬움) | 약간 증가 |
| 유지보수성 | 낮음 (프롬프트 의존) | 높음 (schema 중심 구조) |
| 확장성 (필드 증가 시) | 급격히 불안정해짐 | 안정적으로 유지 |
| 체인 연결 (pipeline) | 불편 | 매우 적합 |
| agent workflow 연결 | 제한적 | 적합 |
| structured DB 저장 | 추가 처리 필요 | 바로 가능 |
| tool 호출 결과 처리 | 불편 | 자연스럽게 연결 가능 |
| multi-step reasoning pipeline | 불안정 | 안정적 |
| production 환경 적합성 | 낮음 | 높음 |
| prototype / 실험용 | 매우 적합 | 약간 과함 |
| nested 구조 처리 | 어려움 | 자연스럽게 가능 |
| 리스트/객체 출력 | 불안정 | 안정적 |
| 자동 문서화(schema 역할) | 없음 | 있음 |
| IDE 타입 지원 | 없음 | 있음 |
from langchain.agents import initialize_agent
같은 전통적인 LangChain Agent는 내부적으로 상태를 자동 관리함.
즉, 메시지 히스토리, intermediate steps, tool 호출 결과들이 내부적으로 처리됨
LangGraph에서는 Agent가 "그래프 위에서 상태를 전달하며 실행되는 프로그램"이므로 반드시 State 구조가 필요함
여기서 State는 다음과 같은 역할을 함
즉 LangGraph에서는 Agent = state transition system이다.
LangGraph에서 조건 분기를 정의하는 부분. 특정 노드 실행 후 다음에 어떤 노드로 이동할지 조건에 따라 결정함
add_conditional_edges(
source_node,
routing_function,
routing_map
)
| 종류 | 설명 |
|---|---|
| HumanMessage | 사용자 입력을 나타내는 메시지 LLM 입장에서는 role = ' |
| user'와 동일하다 사용자 질문 전달, agent 입력, chain 시작 입력 | |
| SystmeMessage | 모델의 행동 규칙 또는 컨텍스트 설정 LLM 입장에서는 role = 'system'에 해당 역할 설정, 출력 형식 제한, 스타일 지정, 정책 설정, persona 정의 |
| AIMessage | 모델의 응답을 나타내는 메시지 LLM 입장에서는 role = "assistant"에 해당 이전 응답 저장, 대화 history 구성, agent intermediate reasoing 기록 |
| ToolMessage | tool 실행 결과를 나타내는 메시지 tool execution 결과 전달, agent-tool loop 연결, function calling 결과 전달 |
| ChatMessage | 사용자 정의 role 메시지 생성 multi-agent system, evaluator 역할 분리, palnner/critic/reviewer 구조 |
| RemoveMessage | state에서 특정 메시지 제거, LangGraph에서 message pruning 할 때 사용 memory trimming, context window 관리, 오래된 메시지 제거 |
HumanMessage()와 {"role":"user","content":...}의 차이점
| 구분 | ChatModel | Agent |
|---|---|---|
| 역할 | 한 번 답변 생성 | 여러 단계 문제 해결 |
| 입력 | messages | state(dict) |
| 내부 구조 | 단일 LLM 호출 | LLM + tool + routing + memory |
| 실행 흐름 | 1-step | multi-step |
| 예시 | llm.invoke() | agent.invoke() |
| 입력 형식 | HumanMessage(...) 또는 "텍스트" | {"messages": [...]} (state dict) |
ChatModel은 모델에게 대화 기록을 전달
Agent 호출은 워크플로우 전체 상태를 전달 ( {"messages" : [...]}구조로 state의 messages필드에 메시지를 넣어서 agent 실행을 시작함)
| 방식 | 언제 쓰는가 | 입력 예시 | 의미 |
|---|---|---|---|
| 문자열 | 단일 LLM 호출 | "요약해줘" | prompt 전달 |
| Message 객체 | 대화/역할 포함 호출 | [HumanMessage(...)] | 구조화된 대화 전달 |
| state(dict) | Agent / LangGraph 실행 | {"messages": [...]} | 실행 상태 전달 |
result = llm.invoke("다음 주제로 짧은 이야기 써줘: 용과 마법사")요약, 번역, 제목 생성, 간단한 질문, 단일 응답 생성
from langchain_core.messages import HumanMessage, SystemMessage
result = llm.invoke([
SystemMessage(content="너는 동화 작가다."),
HumanMessage(content="용과 마법사 이야기 써줘")
])
SystemMesasge 필요, multi-turn 대화, role 구분 필요, structured output, chain 구성
research_agent.invoke(
{"messages": [{"role": "user", "content": "Transformer 발전 과정 조사"}]}
)
prompt 전달이 아닌 state 전달
state 전달
↓
LLM 판단
↓
tool 호출 가능
↓
다시 LLM
↓
state 업데이트
↓
결과 반환
LangGraph에서 여러 노드가 같은 state 필드를 업데이트할 수 있음
이 경우 기존 값과 새 값을 어떻게 합칠지를 결정해야 하는데 이때 사용하는 것이 reducer이다
즉, 기존 값 + 새 값 -> 최종값을 정의하는 함수
override_reducer(current, new)
current는 기존 state 값, new는 새로 추가되는 값이다
def override_reducer(current, new):
"""override 타입 지원 reducer"""
if isinstance(new, dict) and new.get("type") == "override":
return new.get("value", new)
return operator.add(current, new)
type이 override인 경우 기존 값을 무시하고 새값을 덮어씌움
override가 아닌 경우 기존 값 + 새값으로
class AgentState(MessagesState):
"""메인 에이전트 상태"""
research_brief: Optional[str] = None
supervisor_messages: Annotated[List, override_reducer] = []
notes: Annotated[List[str], override_reducer] = []
final_report: str = ""
# Human-in-the-Loop: 최대 1회 질문 추적
clarification_asked: bool = False
MessageState : LangGraph에서 기본적으로 제공하는 state 클래스로 messages 필드를 기본으로 포함하는 state 구조를 제공함
| 라이브러리 | 설명 |
|---|---|
| Optional[str] = None | 문자열일 수도 있고 없을 수도 있음이라는 뜻 |
| Annotated[List, override_reducer] | 이 필드를 state 업데이트할 때 어떤 reducer를 사용할지 지정 이 필드는 List 타입이고 state 병합 시 override_reducer 사용 |
| 종류 | 언제 사용 | 특징 |
|---|---|---|
TypedDict 기반 State | 가장 일반적 | 가볍고 빠름 |
MessageState | Agent / 대화형 workflow | messages 필드 자동 포함 |
Pydantic BaseModel 기반 State | validation 필요 | 타입 안정성 높음 |
dataclass 기반 State | 간단한 구조 | Pythonic 스타일 |
| Custom reducer 포함 State | 병합 전략 제어 필요 | 필드별 update 방식 지정 |
-> Command[Literal["research_tools"]]
이 노드는 실행 후 "research_tools" 노드로 이동한다는 의미
실제 Command 객체 구조는 다음과 같다
Command(
update={...},
goto="next_node"
)
| 필드 | 의미 |
|---|---|
update | state 변경 |
goto | 다음 실행 노드 지정 |
일반 노드는 node 실행 -> state 업데이트 -> 그래프 edge 따라 이동
Command 노드는 node 실행 -> 다음 실행 노드 직접 지정
conditional edges는 그래프 밖에서 정의 한다
builder.add_conditional_edges(...)
하지만 Command는 노드 내부에서 정의함
return Command(goto="...")