본문으로 건너뛰기

© 2026 Molayo

LangChain헤드라인2026. 05. 21. 04:00

RAG를 넘어: 더 스마트한 지식 검색을 위한 LangGraph 기반의 에이전트 검색(Agent Search) 구현

요약

전통적인 RAG 방식이 해결하기 어려운 복잡하고 모호한 기업용 질문을 처리하기 위해 LangGraph 기반의 '에이전트 검색(Agent Search)' 구현 방식을 소개합니다. 이 방식은 질문을 하위 질문으로 분해하고, 검색된 정보를 바탕으로 답변을 구성한 뒤, 다시 정교화하는 다단계 논리적 과정을 통해 답변의 품질을 높입니다.

핵심 포인트

  • 전통적인 RAG는 여러 엔티티가 포함되거나 모호한 질문에 대응하는 데 한계가 있음
  • 에이전트 검색은 질문 분해, 하위 질문 생성, 답변 구성, 답변 정교화의 단계를 거침
  • LangGraph를 백본으로 활용하여 복잡한 LLM 호출과 논리적 오케스트레이션을 수행함
  • Onyx는 이를 통해 기업 내 다양한 문서 소스에서 고도화된 지식 검색을 제공함

*편집자 주: 이 글은 Onyx의 파트너가 작성한 게스트 포스트입니다. LangGraph가 성숙해짐에 따라, 점점 더 많은 기업(Klarna, Replit, AppFolio 등)이 이를 선호하는 에이전트 프레임워크(Agent Framework)로 사용하기 시작했습니다. 저희는 이 글이 그러한 평가가 어떻게 이루어지는지를 상세히 설명하는 훌륭한 블로그 포스트라고 생각했습니다. *이 포스트의 다른 버전은 그들의 블로그에서도 읽어보실 수 있습니다.

Evan Lohn, Joachim Rahmfeld 작성

Onyx에서 저희는 사용자가 기업 데이터로부터 얻을 수 있는 지식과 통찰력을 확장하여, 이를 통해 모든 직무 전반의 생산성을 높이는 데 전념하고 있습니다.

그렇다면, Onyx란 무엇일까요? Onyx는 기업이 노트북, 온프레미스(On-premises), 또는 클라우드 등 어떤 규모로든 배포할 수 있는 AI 어시스턴트(AI Assistant)로, Slack, Google Drive, Confluence를 포함한 다양한 소스의 문서화된 지식을 연결합니다. Onyx는 LLM(대규모 언어 모델)을 활용하여 팀을 위한 분야별 전문가(Subject Matter Expert)를 생성하며, 사용자가 관련 문서를 찾는 것뿐만 아니라 "기능 X가 이미 지원되나요?" 또는 "기능 Y에 대한 풀 리퀘스트(Pull Request)가 어디에 있나요?"와 같은 질문에 대한 답변을 얻을 수 있도록 지원합니다.

지난해, 저희는 다음과 같은 목표를 설정하여 기업 검색(Enterprise Search) 및 지식 검색(Knowledge Retrieval) 역량을 강화하는 여정을 시작했습니다:

복잡하고 모호한 질문에 대해 확장 가능한 답변 제공
여러 엔티티(Entities)가 포함된 경우의 답변 품질 개선
질문의 핵심 측면에 대해 더 풍부한 세부 정보와 맥락(Context) 제공

이러한 범주에 속하는 질문들은 대개 사용자에게 높은 가치를 지니지만, 전통적인 RAG(검색 증강 생성) 방식의 시스템은 이러한 상황에서 어려움을 겪는 경향이 있습니다.

예를 들어, 다음과 같은 질문을 생각해 보십시오: "우리가 Nike와 Puma에 대해 포지셔닝했던 제품 관련 차이점 중, 우리의 서로 다른 판매 결과에 영향을 미칠 수 있는 요소는 무엇인가요?" 이 질문은 여러 엔티티를 포함할 뿐만 아니라 모호성(제품 관련 판매 결과는 많은 의미를 가질 수 있음)도 포함하고 있습니다.

코퍼스(Corpus) 내에 이 질문과 거의 정확히 일치하는 내용을 다루는 문서가 있지 않는 한, RAG 시스템은 여기서 좋은 답변을 찾아내는 데 어려움을 겪습니다.

이러한 유형의 질문들이 바로 우리의 새로운 에이전트 검색(Agent Search)이 필요한 지점입니다. 여기서의 핵심 아이디어는 무엇일까요?

높은 수준(High level)에서 살펴보면, 이 접근 방식은 1) 먼저 질문을 더 좁은 문맥에 집중할 수 있는 하위 질문(sub-questions)들로 분해하고 잠재적으로 모호한 용어들을 명확하게 정의하며, 2) 답변된 하위 질문들과 검색된 문서들을 사용하여 초기 답변을 구성한 다음, 3) 초기 답변과 초기 과정 동안 학습된 다양한 사실들을 바탕으로 더욱 정교화된 답변을 생성하는 것입니다.

위의 예시를 더 구체화하기 위해, 유효한 초기 하위 질문으로는 ‘우리가 Puma와 어떤 제품들에 대해 논의했는가?’, ‘우리가 Nike와 어떤 제품들에 대해 논의했는가?’, ‘Puma에 의해 보고된 이슈는 무엇인가?’ 등이 될 수 있습니다.

이러한 유형의 논리적 과정을 캡슐화하기 위해서는 수많은 단계, 계산, 그리고 LLM 호출(LLM calls)을 조직하고 오케스트레이션(orchestration)해야 합니다.

이 블로그의 목적은 i) 우리가 기능적 수준에서 이 문제에 어떻게 접근했는지 설명하고, ii) 기술 선택 접근 방식을 어떻게 진행했는지 논의하며, iii) LangGraph를 백본(backbone)으로 어떻게 활용했는지, 특히 어떤 교훈을 얻었는지 상세히 공유하는 것입니다.

우리는 이 글이 이 분야에 관심이 있거나 LangGraph를 사용하여 에이전트를 구축하고자 하는 독자들에게 유용하기를 바라며, 우리의 요구 사항 중 일부를 공유하고자 합니다.

일반적인 흐름 및 기술적 요구 사항

대략적으로, 우리가 목표로 하는 논리적 흐름은 높은 수준에서 다음과 같습니다:

이 흐름의 주요 측면과 요구 사항은 다음과 같습니다:

  • 원래 질문에 대한 관련 문서를 직접 검색하는 것 외에도, 초기 질문을 더 좁고 명확하게 정의된 하위 질문(sub-questions)으로 분해합니다. 이는 모호성을 제거(disambiguation)하고 검색 초점을 좁히는 데 도움이 됩니다.

  • 이러한 분해(decomposition)는 초기 검색 결과에 기반하여 이루어지며, 분해 과정에 맥락(context)을 제공합니다.

  • 각 하위 질문에 대한 답변 과정은 쿼리 확장(query expansion), 검색(search), 문서 검증(document validations), 재순위화(reranking), 하위 답변 생성(subanswer generation), 하위 답변 검증(subanswer verification) 등 여러 단계로 구성됩니다.

  • 초기 답변은 검색 결과와 하위 질문들의 답변을 바탕으로 생성됩니다.

  • 만약 초기 답변이 불충분하다면, 부족한 점을 보완하거나 하위 질문에 대한 답변을 후속 추적하도록 설계된 정교한 답변(refined answer)을 생성하기 위해 추가적인 분해를 수행합니다. 이 정교화 분해(refinement decomposition)는 다음 사항들을 참고합니다:

    • 질문과 원래의 답변(그리고 그것이 불충분하다는 사실)
    • 하위 질문들과 그 답변들(그리고 답변할 수 없었던 하위 질문들)
    • 분해 과정을 문서 집합의 내용과 더 잘 일치시키기 위해, 초기 검색을 기반으로 수행된 별도의 개체/관계/용어 추출(entity/relationship/term extraction)
  • 전반적으로, 다음과 같은 여러 수준에서 병렬 처리(parallelism)가 필수적입니다:

    • 각 하위 질문 처리 시 검색된 문서에 대한 검증
    • 여러 하위 질문의 병렬 처리
    • 하위 질문 처리와 병렬로 진행되는 개체/관계/용어 추출
  • 마찬가지로, 의존성 관리(dependency management)도 필수적입니다. 예시는 다음과 같습니다:

    • 정교화 단계(refinement phase)의 분해는 초기 답변이 생성되고(정교화 필요성이 결정됨), 개체, 관계 및 용어 추출이 모두 완료될 때까지 기다려야 합니다.
    • 이후의 단계들은 이전 단계들의 결과에 따라 결정됩니다.

따라서 실질적으로 훨씬 더 광범위하고 모호한 질문을 처리하겠다는 목표를 달성하기 위해서는 실제로 매우 많은 과정이 이루어져야 합니다.

이러한 흐름은 확실히 워크플로 중심적(workflow-centric)이지만, 더 광범위한 에이전트 검색(Agent Search) 흐름을 향한 초기 단계를 제시합니다. 저희는 이 흐름에 다양한 도구(tools)를 연결하고, 정제 과정(refinement process)을 업데이트하는 등의 작업을 진행할 계획입니다. 추후에는 정제 전 답변을 승인하거나 수동 변경 사항을 반영하여 흐름의 일부를 재실행하는 것과 같이, 사용자와의 인간 참여형(Human-in-the-Loop) 방식의 상호작용을 도입할 수도 있습니다.

가까운 미래까지 고려한 저희의 요구사항을 해결하기 위해서는 다음과 같은 프레임워크가 필요합니다.

  • 잘 제어될 수 있고 (well-controlled),
  • 확장하거나 (재)설정하기 쉬우며 (easy to extend or (re)configure),
  • 비용 효율적이고 (cost-effective),
  • 높은 수준의 병렬화(parallelization)를 허용하며,
  • 논리적 의존성(logical dependencies)을 관리할 수 있어야 합니다 (예: C가 시작되기 전에 A와 B가 완료되어야 하며, E는 이 모든 과정과 병렬로 실행될 수 있음 등),
  • 토큰(tokens) 및 기타 객체의 스트리밍(streaming)을 지원하고,
  • 향후 더 복잡한 상호작용을 가능하게 해야 합니다.

그리고 — 아, 맞습니다! — 답변 또한 사용자의 기대에 부응하도록 적시에 생성되어야 하며, 대규모(at scale)로 처리될 수 있어야 합니다.

따라서 저희가 해결해야 했던 핵심 질문은 "이를 어떻게 가장 잘 구현할 것인가?"였습니다.

프레임워크 옵션 및 평가 접근 방식

저희에게 주어진 선택지는 본질적으로 기존 흐름을 확장하여 이 흐름을 처음부터 직접 구현할 것인지, 아니면 기존의 에이전트 프레임워크(agentic framework)를 활용할 것인지 — 활용한다면 어떤 것을 사용할 것인지 — 사이의 문제였습니다.

위에서 설명한 우선순위를 고려했을 때, 저희는 구현 프레임워크의 주요 후보로 LangGraph를 선택했으며, 처음부터 직접 구현하는 방식이 아마도 그 뒤를 바짝 쫓는 두 번째 대안이었을 것입니다.

LangGraph를 지지하게 된 초기 동기들은 다음과 같습니다:

  • 복잡한 그래프 전반에 걸쳐 노드(node)의 수가 상당히 많아질 수 있습니다. 저희는 (서로 다른 노드를 위한 함수 재사용을 제외하고) 파일당 하나의 노드를 사용하는 방식을 채택하기로 결정했습니다.

  • 노드가 많을 경우, 명확한 디렉토리 구조와 파일 명명 전략을 사용하는 것이 권장됩니다. 저희는 각 서브그래프(subgraph)마다 디렉토리를 생성하였고, 일반적으로 <action>_<object>.py 명명 규칙을 채택했습니다. 단계 번호를 위한 숫자를 추가하는 것도 도움이 될 수 있지만, 노드가 추가되거나 삭제될 때 추가적인 작업이 필요할 수 있습니다.

  • 저희는 병렬화(parallelization, 아래 참조) 및 재사용을 목적으로 서브그래프를 광범위하게 사용합니다. 각 서브그래프 디렉토리는 자체적인 엣지(edges), 상태(states), 모델(models) 파일뿐만 아니라 그래프 빌더(graph builder)를 가집니다.

  • 그래프를 시각화하기 위해, 전체 그래프의 Mermaid PNG 파일을 사용하는 것이 매우 유용하다는 것이 증명되었습니다.

타이핑 및 상태 관리 (Typing & State Management)

저희는 코드베이스 전반에 걸쳐 Pydantic을 사용하고 있으므로, LangGraph가 TypeDict 외에도 Pydantic 모델을 지원한다는 점은 매우 좋았습니다. 결과적으로, 저희는 LangGraph 구현 전체에서도 Pydantic 모델을 사용합니다. (불행히도, 그래프 출력(graph outputs)에 대해서는 아직 Pydantic이 지원되지 않습니다).

서브그래프 내에 각자의 액션(action)과 '출력'(상태 업데이트)을 가진 노드가 많기 때문에, 저희는 일반적으로 (서브)그래프 상태가 노드 업데이트에 의해 구동된다고 간주합니다. 따라서 서브그래프 상태 내에 키(key)를 직접 정의하기보다는, 다양한 노드 업데이트를 위한 Pydantic 상태 모델을 정의한 다음, 다양한 노드 업데이트(및 기타) 모델을 상속받아 그래프 상태를 구성합니다.

장점 (Benefits):

  • 키(key)들이 자연스럽게 그룹화됩니다.
  • 키를 추가할 때 노드 상태 모델(및 노드)만 업데이트하면 됩니다.
  • 키의 중복(overlap)이 허용됩니다.
  • 기본값(default values) 설정이 가능합니다.

과제 (Challenges):

  • 이 접근 방식을 따르면 전체 그래프 상태의 전체 키 세트를 파악하는 것이 확실히 조금 더 어려워집니다.
  • 상속 문제를 피하기 위해 좋은 구조를 선택해야 합니다.

당연하게도, 상태 키(state keys)에 대한 기본값(default values)을 설정할지 여부(또는 설정하지 않을지!)를 신중하게 결정하는 것은 매우 중요합니다. 주의 깊게 처리하지 않으면, 부적절한 상황에서 기본값을 설정하는 것이 감지하기 더 어려운 의도치 않은 동작을 초래할 수 있습니다. 예를 들어, 다음과 같은 문제가 있는 구성을 생각해 보십시오:

여기서 메인 그래프 노드(Main Graph Node) A는 ‘my_key’를 ‘my_data’로 설정합니다. 이 값은 나중에 내부 서브그래프(Inner Subgraph) 노드에서 사용될 의도입니다. 하지만 이 예시에서는 (의도적으로) 외부 서브그래프(Outer Subgraph)에 해당 키를 추가하는 것을 누락했습니다. 당연하게도, 내부 서브그래프에서 이 키의 값은 빈 문자열이 될 것입니다. 출력 측에서도 유사한 상황이 발생합니다. 만약 우리가 내부 서브그래프 노드 C에서 ‘my_key’를 업데이트한다면, 이는 메인 그래프의 ‘my_key’ 상태를 업데이트하지 않을 것입니다.

대신에 다음과 같이 내부 서브그래프에서 ‘my_key’에 대한 기본값을 설정하지 않고 주의를 기울였다면:

그러면 ‘my_key’가 내부 서브그래프를 위한 입력값을 가지고 있지 않기 때문에 에러가 발생할 것입니다. 그러면 외부 서브그래프(Outer SubGraph)에서 누락된 상태가 추가되어 적절한 구성에 도달하게 됩니다:

이것은 물론 전통적인 중첩 함수(nested functions)와 크게 다르지 않지만, 우리의 경험상 LangGraph 문맥에서는 이러한 문제를 식별하기가 조금 더 어렵습니다.

우리의 권장 사항은 — 놀랍지도 않게 — 다음과 같습니다:

  • 문서화된 예외(보통 중첩된 서브그래프의 문맥)를 제외하고는, 그래프 입력 상태(graph input states)에 기본값 없이 모든 키를 정의하십시오.
  • 그래프에서 업데이트되는 모든 키를, 키가 리스트(list)이고 여러 노드로부터 리스트에 요소를 추가할 것으로 예상되는 경우를 제외하고는, <type> | None = None 과 같이 정의하십시오.

그래프 구성 요소 및 고려 사항

병렬성 (Parallelism)

우리는 프로세스가 병렬로 실행되어야 한다는 많은 요구 사항을 가지고 있으며, 병렬성에는 여러 유형이 있습니다.

동일한 흐름의 병렬성 (Parallelism of Identical Flows):

AI 자동 생성 콘텐츠

본 콘텐츠는 LangChain Blog의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0