day3_practice/react_agent.py

ReAct 에이전트를 그래프로

manual_react.py의 while 루프를 LangGraph 그래프로 옮긴 파일입니다. 노드는 agent와 tools 두 개뿐이고, tools → agent 엣지가 사이클을 만듭니다. 코드 주석의 [수동 루프의 ...]가 어느 부분을 옮겼는지 가리킵니다.

그래프 구조

agent가 도구를 요청하면 tools로 갔다가 다시 agent로 돌아옵니다. 요청이 없으면 END로 끝납니다.

START HumanMessage agent System + messages → llm_with_tools should_continue → "end" → END 마지막 AIMessage에 tool_calls 없음 END tools ToolNode(tools) "tools" tool_calls 있음 ToolMessage들 사이클 (add_edge)

수동 루프 → 그래프 대응

manual_react.pyreact_agent.py
messages = [...] 리스트와 appendAgentState.messages: Annotated[list, add_messages]
llm_with_tools.invoke(messages) + appendagent 노드가 {"messages": [응답]} 반환
if not ai_message.tool_calls: breakshould_continue + add_conditional_edges
for tool_call ... 실행 + ToolMessage 포장ToolNode(tools)
while 처음으로 돌아가기add_edge("tools", "agent")
MAX_TURNS = 10코드에는 없음. recursion_limit(기본 25 step)이 대신 막음

구성 요소

State AgentState 필드는 messages 하나입니다. add_messages 리듀서 덕분에 노드는 새 메시지만 돌려주면 기존 기록 뒤에 붙습니다.
모델 노드 agent 호출할 때마다 SystemMessage를 맨 앞에 붙여 보냅니다. System은 반환하지 않으므로 State에는 저장되지 않습니다. 그래서 State에는 대화만 남고, 시스템 프롬프트는 코드에서 한 곳으로 관리됩니다.
라우팅 should_continue 마지막 메시지의 tool_calls만 봅니다. "end"라는 라벨을 반환하고, path_map {"tools": "tools", "end": END}가 그 라벨을 실제 목적지로 바꿉니다.
도구 실행 노드 ToolNode(tools) 마지막 AIMessage의 tool_calls를 모두 실행하고, 각 결과를 같은 tool_call_id를 단 ToolMessage로 돌려줍니다. 수동 루프의 tools_by_name 조회와 포장 코드가 여기 들어 있습니다.
def agent(state): messages = [SystemMessage(content=SYSTEM_PROMPT)] + state["messages"] # 매번 앞에 붙여서 전송 return {"messages": [llm_with_tools.invoke(messages)]} # 새 메시지만 반환 def should_continue(state) -> Literal["tools", "end"]: return "tools" if state["messages"][-1].tool_calls else "end" builder.add_conditional_edges("agent", should_continue, {"tools": "tools", "end": END}) builder.add_edge("tools", "agent") # 사이클

도구 세 개

search_employee 순수 함수 더미 사전에서 조회합니다. 없는 이름이면 예외 대신 조회 가능한 이름 목록을 문자열로 돌려줘서 모델이 사용자에게 설명할 수 있게 합니다.
calculate 순수 함수 eval로 계산합니다. 실패하면 "수식 형식을 확인하세요"를 붙여 돌려줘서 모델이 다시 시도할 여지를 줍니다.
classify_request 도구 안에서 LLM 호출 문의를 IT지원 / 인사 / 총무로 분류합니다. 에이전트 입장에서는 그냥 도구지만, 호출될 때마다 LLM 호출이 한 번 더 일어납니다.

시스템 프롬프트의 [도구 사용 전략]과 각 도구의 docstring이 모델이 도구를 고르는 기준입니다. 도구가 늘수록 이 두 곳의 설명이 겹치지 않게 쓰는 게 중요합니다.

실행 결과

이 파일에는 실행 코드가 없어서, graph를 import해 질문 세 개를 stream_mode="updates"로 돌려 봤습니다. 최종 답변은 LLM 출력이라 실행마다 달라질 수 있습니다.

"안녕! 오늘 기분 어때?"
agent→END
agent · 최종 답변 (LLM 호출 1번)저는 AI 어시스턴트라 감정은 없지만, 삼성SDS 임직원분들을 도와드릴 준비가 되어 있어서 늘 기쁩니다! ...
"김하늘 님 소속 팀 알려주고, 연봉 3480000에 10% 인상하면 얼마인지 계산해줘."
agent→tools→agent→END
agent · tool_calls 2개search_employee(name="김하늘") calculate(expression="3480000 * 1.1")
tools · ToolMessage 2개김하늘 / 클라우드운영팀 / haneul.kim@... · 3828000.0000000005
agent · 최종 답변김하늘 님 소속 팀: 클라우드운영팀 / 10% 인상 후: 3,828,000원
"노트북 화면이 안 켜져요. 어느 부서에 문의해야 하나요? 그리고 최서연 님 이메일도 알려줘."
agent→tools→agent→END
agent · tool_calls 2개classify_request(text="노트북 화면이 안 켜져요") search_employee(name="최서연")
tools · ToolMessage 2개IT지원 · '최서연' 님을 찾을 수 없습니다. 조회 가능한 임직원: ['김하늘', '박도윤']
agent · 최종 답변노트북 화면 문제는 IT지원 부서에 문의하시면 됩니다. 최서연 님은 사내 시스템에서 찾을 수 없습니다. ...

주의할 점

지점내용
import 시 PNG 저장시각화 코드가 if __name__ == "__main__": 밖에 있어서, 다른 파일에서 import만 해도 react_agent.png를 쓰고 출력이 찍힙니다. 위 실행 결과를 만들 때도 그랬습니다.
턴 한도수동 루프의 MAX_TURNS에 해당하는 코드가 없습니다. agent + tools 한 바퀴가 2 step이라, 기본 recursion_limit 25에서는 대략 12번 도구를 부르면 GraphRecursionError가 납니다. 필요하면 invoke(..., config={"recursion_limit": N})로 조정하고 예외를 처리합니다.
evalmanual_react.py와 같은 계산기라 같은 보안 문제가 있습니다.
금액 계산float 오차가 그대로 나옵니다. 금액은 Decimal로 계산하거나 도구 안에서 반올림해서 돌려줍니다.
도구 안 LLMclassify_request는 호출될 때마다 비용과 지연이 추가됩니다. 에이전트 그래프의 노드 실행 기록에는 이 내부 호출이 따로 보이지 않습니다.

정리