langgraph에서 벗어나기
llm api 를 사용한 제품을 만들면서 처음엔 langgraph 를 도입했습니다. 이후에 langgraph 를 벗어나 직접 채팅 내역 관리와 상태 관리를 구현했습니다. 이 과정에서 느꼈던 점들과 배웠던 점을 정리했어요.
막막할 때 구원의 손길 langgraph
처음 챗봇을 만들어야 했을 때 langgraph 가 큰 도움이었습니다. 유저가 질문하면 이미 알고 있는 정보를 바탕으로 답변을 하는 챗봇을 만들어야 했어요. 은행 앱에 있는 챗봇과 비슷합니다. 이 때 비슷한 작업을 해본 적이 없어서 막막했습니다. 사용자가 질문을 하면 그 질문으로부터 적절한 검색어를 추출해야했어요. 이 검색어로 디비에 있는 정보를 검색해야했습니다. 검색한 결과를 잘 다듬어서 유저의 질문에 맞는 답으로 제공해야했어요. 각각이 어려웠습니다.
langgraph 가 제공해주는 tutorial1 을 따라하면서 최소한의 원하는 기능을 쉽게 구현할 수 있었습니다. 모르던 많은 개념들도 익히게 되었어요. rag2 가 무엇인지, 어떻게 원본 정보를 쪼개고 vector화 해서 저장하는지, 어떻게 다시 쿼리하는지 등에 대해서 알게 되었습니다. 아무 것도 모르는 상태로도 동작하는 걸 만들 수 있었고, 이를 분석하면서 많이 배웠어요.
langgraph 를 벗어나게 된 이유 - graph 구조의 답답함
가장 큰 이유는 langgraph 의 graph 구조가 답답했기 때문이었습니다. langgraph에서는 여러 작업을 graph 형태로 표현합니다. 시작 node에서 끝 node로 이동하면서 llm이나 db 호출들을 진행해요. 끝 노드에 도착해서 나오는 결과물을 chatbot output 으로 사용합니다.
예를 들어서 자산을 이체하거나, 자산을 조회하는 기능을 가진 챗봇을 가정할게요. alice가 bob에게 500원을 이체해달라는 요청을 챗봇에게 했어요. 이 때 이렇게 그래프의 노드를 정의할 수 있습니다.
- alice 의 의도 파악
- 얼마의 자산을, 누구에게 이체할지 정보 추출
- 서버 api를 사용해서 자산을 실제 전송
- alice 에게 전달할 채팅 메시지 생성
이렇게 graph로 코드의 흐름을 표현하면 코드의 실행 흐름을 파악하기가 어려웠어요. 특히 각 노드들 사이에 의존 관계가 있는 경우 이 의존 관계가 잘 정의되어있는지 파악하기 어려웠습니다. 에러 처리를 하는 것도 까다로웠어요. 어떤 에러들이 발생할 수 있는지 알고 잘 처리하려면 사실상 langgraph 소스코드를 읽어야했습니다.
코드흐름을 쉽게 파악하면서 에러처리도 잘 해내려면 if/else/while 을 쓰는 게 더 편하다고 판단했어요. 우리 팀이 구현해야했던 요구사항 정도는 분기와 반복문으로도 충분히 간결하게 표현이 가능했어요. 앞선 단계에서 뒤로 전달해야하는 정보도 함수의 반환값과 인자로 표현할 수 있어 명확했구요. 에러처리도 익숙한 try catch 로 원하는 걸 쉽게 구현할 수 요었습니다.
langgraph 를 벗어나게 된 이유 - 기능 확장의 어려움
채팅 내역을 관리하는 것과 rag(지식 검색)의 확장이 까다로운 것도 langgraph에서 빠져나오게 된 추가적인 이유들이었어요. langgraph에서는 채팅 내역을 자동으로 postgresql 에 저장할 수 있어요. 처음은 쉽지만 조금 쓰다 보면 문제가 생겨요. 계속해서 채팅 내역이 쌓이기 때문에 context 제한을 넘쳐버리는 문제가 생겨요. 히스토리를 삭제하거나 요약해야하는데 이게 꽤 까다로웠습니다. remove message 를 만들어서 langgraph library에 전달해야해요. 그런데 단순하게 history를 지우는 코드를 작성하면 실행시점에 에러가 발생했어요. 항상 같이 지워져야하는 message들이 있었습니다. tool call과 tool call result message 는 항상 동시에 지워져야했어요. 이 중 하나만 지우면 langgraph 에서 나중에 에러가 나구요. 디비에 저장하는 구조도 꽤 복잡한 편이라서 디비 도구로 현재 history를 직접 쿼리하는 것도 어려웠어요. 우리 팀에 필요한 요구사항만 포함하도록 chatting history를 저장하도록 직접 구현하는 게 langgraph를 잘 쓰기 위한 노력보다 쉬웠습니다.
rag 를 확장하는 것도 까다로웠어요. rag 에 신뢰할 수 있는 소수의 정보를 저장할 때는 잘 동작했어요. llm 에 더 많은 정보를 제공하기 위해서 특정 시점에만 유효했던 정보나, 신뢰도가 낮은 정보들을 추가하게 되면서 문제가 어려워졌어요. langgraph가 제공해주는 rag에서는 검색할 때 정보의 출처에 따라 검색 순위를 바꾸거나 시간에 따라 점수를 부여하는 게 까다로웠습니다. 디비에 정보를 저장할 때 추가정보를 저장해야하는데 langgraph에서는 db schema에 json 형태로 meta data를 저장하게 하더라구요. 디비 스키마에 칼럼을 따로 두고 관리하고 싶은 입장에서 꽤 불편했어요. rag 역시 필요한 요구사항만을 지원하는 가벼운 시스템을 직접 구현하는 게 비용이 덜 들겠다는 판단이 들었습니다.
결론
langgraph 는 아무것도 모르는 상황에서 간단한 prototype 을 만들고, 이를 바탕으로 학습을 하는데 큰 도움을 준 도구였습니다. 다만 제가 작업하는 요구사항에 비해 기능이 과다했어요. 필요한 요구사항만을 만족시키는 코드를 작성하는 게 더 쉽고 미래에도 좋겠다는 판단을 했고 langgraph가 해주는 일들을 직접 구현했습니다.
아래 두 튜토리얼을 따라하고 분석하면서 많은 걸 배웠어요. langgraph의 rag tutorial, langgraph의 run agent tutorial↩︎
rag는 디비에 다양한 참고자료를 저장한 뒤 llm 이 필요할 때 백터 검색으로 찾아다 쓰는 기술을 의미합니다.↩︎