manual_react.py의 while 루프를 LangGraph 그래프로 옮긴 파일입니다. 노드는 agent와 tools 두 개뿐이고,
tools → agent 엣지가 사이클을 만듭니다. 코드 주석의 [수동 루프의 ...]가 어느 부분을 옮겼는지 가리킵니다.
agent가 도구를 요청하면 tools로 갔다가 다시 agent로 돌아옵니다. 요청이 없으면 END로 끝납니다.
| manual_react.py | react_agent.py |
|---|---|
messages = [...] 리스트와 append | AgentState.messages: Annotated[list, add_messages] |
llm_with_tools.invoke(messages) + append | agent 노드가 {"messages": [응답]} 반환 |
if not ai_message.tool_calls: break | should_continue + add_conditional_edges |
for tool_call ... 실행 + ToolMessage 포장 | ToolNode(tools) |
while 처음으로 돌아가기 | add_edge("tools", "agent") |
MAX_TURNS = 10 | 코드에는 없음. recursion_limit(기본 25 step)이 대신 막음 |
messages 하나입니다. add_messages 리듀서 덕분에 노드는 새 메시지만 돌려주면 기존 기록 뒤에 붙습니다.
SystemMessage를 맨 앞에 붙여 보냅니다. System은 반환하지 않으므로 State에는 저장되지 않습니다. 그래서 State에는 대화만 남고, 시스템 프롬프트는 코드에서 한 곳으로 관리됩니다.
tool_calls만 봅니다. "end"라는 라벨을 반환하고, path_map {"tools": "tools", "end": END}가 그 라벨을 실제 목적지로 바꿉니다.
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") # 사이클
eval로 계산합니다. 실패하면 "수식 형식을 확인하세요"를 붙여 돌려줘서 모델이 다시 시도할 여지를 줍니다.
시스템 프롬프트의 [도구 사용 전략]과 각 도구의 docstring이 모델이 도구를 고르는 기준입니다. 도구가 늘수록 이 두 곳의 설명이 겹치지 않게 쓰는 게 중요합니다.
이 파일에는 실행 코드가 없어서, graph를 import해 질문 세 개를 stream_mode="updates"로 돌려 봤습니다. 최종 답변은 LLM 출력이라 실행마다 달라질 수 있습니다.
search_employee(name="김하늘") calculate(expression="3480000 * 1.1")3828000.0000000005classify_request(text="노트북 화면이 안 켜져요") search_employee(name="최서연")tools 노드를 한 번만 거쳤습니다.calculate는 부동소수점 오차가 있는 3828000.0000000005를 돌려줬고, 모델이 답변에서 반올림했습니다. 금액 계산을 모델의 반올림에 맡기면 안 되는 이유입니다.classify_request 안에서 1번.| 지점 | 내용 |
|---|---|
| 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})로 조정하고 예외를 처리합니다. |
eval | manual_react.py와 같은 계산기라 같은 보안 문제가 있습니다. |
| 금액 계산 | float 오차가 그대로 나옵니다. 금액은 Decimal로 계산하거나 도구 안에서 반올림해서 돌려줍니다. |
| 도구 안 LLM | classify_request는 호출될 때마다 비용과 지연이 추가됩니다. 에이전트 그래프의 노드 실행 기록에는 이 내부 호출이 따로 보이지 않습니다. |
add_messages 덕분에 노드는 새 메시지만 반환하고, ToolNode가 실행과 ToolMessage 포장을 대신합니다.recursion_limit이 걸립니다. 실서비스에서는 그 예외를 잡아 처리합니다.