Issue 01중국 AI
AC POST
중국 AI 목록
掘金2026년 9월 19일 18:54중국어 → 한국어

RAG에서 Agentic RAG로: 세 번째 단계, 웹 검색 연동

로컬 지식이 부족할 때 웹 검색을 수행하도록 RAG 흐름을 확장한 Agentic RAG 세 번째 단계가 소개됐다.

중국어 원문을 AI로 번역했습니다. 고유명사와 수치는 원문 표기를 우선하며, 중요한 판단에는 아래 출처 원문을 함께 확인하세요.

앞선 두 단계에서 우리는 RAG가 '검색을 해야 하는가'를 판단하게 했고, 복잡한 문제를 쪼개서 여러 번 검색하도록 만들었다. 하지만 여기에는 아주 현실적인 문제가 하나 남아 있다. 만약 답이 애초에 로컬 지식베이스에 없다면 어떻게 해야 할까? 이번 편에서는 LangGraph를 계속 개조한다. 검색 후 먼저 컨텍스트가 충분한지 평가하고, 부족하면 Web Query를 생성해 자동으로 네트워크 검색 노드에 진입해 정보를 보충한 뒤, 모델에게 최종 답변을 생성하도록 넘긴다.

앞의 두 편에서 우리는 이미 RAG가 두 가지 일을 하도록 만들었다.

첫 번째 단계:

이 질문은 지식베이스를 조회해야 하는가?

두 번째 단계:

이 복잡한 문제는 한 번 검색하면 충분한가?

그래서 흐름은 맨 처음의:

Question ↓ Retrieve ↓ Generate

에서 점차 판단하고 순환하는 Graph로 변해갔다.

하지만 여기에는 피할 수 없는 문제가 하나 있다.

만약 답이 애초에 로컬 지식베이스에 없다면?

이것은 '몇 번 더 조회'하는 것으로 해결되지 않는다.

一、여러 번 검색해도 없는 것은 찾을 수 없다

우리의 로컬 지식베이스는 여전히 《天龙八部》(천룡팔부)다.

이제 사용자가 묻는다.

《天龙八部》에서 虚竹는 마지막에 어떤 신분을 물려받았는가? 그리고 첫 번째 《天龙八部》 드라마는 언제 방영되었는가?

첫 번째 질문:

虚竹는 마지막에 어떤 신분을 물려받았는가?

답은 소설 안에 있다.

Milvus는 로컬 지식베이스에서 관련 구절을 검색할 수 있다.

하지만 두 번째 질문:

첫 번째 《天龙八部》 드라마는 언제 방영되었는가?

는 다르다.

우리 지식베이스에 저장된 것은 소설 내용이며, 드라마의 방영 정보는 없다.

이때 설령:

Top K = 5

를:

Top K = 20

으로 바꾸거나, 심지어 여러 번 다시 검색하더라도 해결되지 않는다.

왜냐하면 문제는:

검색을 정확히 못 했다.

가 아니라:

로컬에 애초에 없다.

이기 때문이다.

이 두 문제는 구분해야 한다.

二、전통적 RAG의 문제: 검색이 끝나면 곧바로 답한다

일반적인 RAG는 흔히:

즉:

무엇을 검색했든 ↓ 그대로 LLM에 밀어넣고 ↓ 답변 생성

여기에는 사실 한 단계가 빠져 있다.

이 자료들이 정말로 사용자의 질문에 답하기에 충분한가?

예를 들어 방금의 질문에서 로컬 지식베이스는 이미 虚竹 관련 소설 구절을 찾아냈을 수 있다.

하지만:

첫 번째 《天龙八部》 드라마가 언제 방영되었는가

에 대해서는 로컬에 정보가 전혀 없다.

이때 검색 결과를 그대로 모델에 넘겨 답하게 하면, 모델은 자신의 파라미터 기억에 의존해 뒷부분을 채워 넣을 수 있다.

그래서 쉽게 나타난다:

컨텍스트에 없음 + 모델이 스스로 채움 = 환각

따라서:

Retrieve → Generate

를 그대로 연결해서는 안 된다.

중간에 노드 하나를 추가해야 한다.

Evaluate

다음처럼 된다.

Retrieve ↓ Evaluate ↓ 정보가 충분한가?

이 노드가 바로 이번 편의 진짜 핵심이다.

三、먼저 모델이 판단하게 하라: 기존 정보가 충분한가

먼저 Graph에 몇 가지 상태를 추가한다.

const GraphState = Annotation. Root ({ question : Annotation, retrievedDocs : Annotation, localContext : Annotation, webContext : Annotation, evaluation : Annotation, generation : Annotation, });

여기서 가장 주목할 만한 것은:

localContext webContext evaluation

각각 다음을 나타낸다.

localContext → 로컬 지식베이스에서 찾은 내용 webContext → 이후 네트워크 검색으로 보충한 내용 evaluation → 현재 정보가 충분한지에 대한 모델의 판단

이어서 여전히 먼저 로컬 검색을 수행한다.

四、로컬 검색 노드는 자료를 찾는 일만 담당한다

이 계층에는 사실 새로운 것이 없다.

여전히 Milvus에서 관련 문서를 검색한다.

const retrieveLocalNode = async (state) => { const retrievedDocs = await retrieveRelevantContent( state.question, state.k ) ; const localContext = retrievedDocs .map((doc) => doc.content) .join("\n\n") ; return { retrievedDocs, localContext, } ; } ;

흐름은:

Question ↓ Embedding ↓ Milvus ↓ Top K Documents ↓ localContext

하지만 여기서 주목할 만한 역할 구분이 하나 있다.

Retrieve는 자료를 찾는 일만 담당하고, 그 자료가 충분한지 판단하는 일은 담당하지 않는다.

정보가 충분한지는 다음 노드에 넘긴다.

五、Evaluate가 이번 라운드의 진짜 의사결정 노드다

우리는 모델이 구조화된 결과를 반환하기를 원한다.

const EvaluateSchema = z .object ({ enough: z. boolean (), missing: z. array (z. string ()). max ( 6 ), reason: z. string (), web_query: z. string (). optional (), });

몇 개의 필드가 각각 서로 다른 문제를 해결한다.

enough

현재 컨텍스트가 답하기에 충분한가:

true → 답할 수 있다 false → 정보가 아직 충분하지 않다

missing

만약 부족하다면, 구체적으로 무엇이 부족한가.

예를 들어 방금의 질문에서는 이렇게 나올 수 있다.

첫 번째 《天龙八部》 드라마 방영 시간 정보가 부족하다

reason

왜 현재 컨텍스트가 충분하다고 또는 부족하다고 여기는가.

web_query

만약 로컬 정보가 부족하다면:

다음 단계에서 인터넷에 무엇을 검색해야 하는가?

그래서 Evaluate 노드가 하는 일은 아주 명확해진다.

사용자 질문 + 로컬 검색 결과 ↓ LLM ↓ 정보가 충분한가?

六、왜 사용자의 원래 질문을 그대로 검색에 쓰지 않는가?

여기에는 아주 중요한 디테일이 하나 있다.

사용자의 원래 질문은:

로컬 검색을 거친 뒤:

이 부분은 이미 답이 있다.

진짜 부족한 것은:

만약 원래 질문 전체를 그대로 검색 엔진에 던지면, 오히려 검색 목표가 충분히 집중되지 않는다.

더 합리적인 것은:

먼저 무엇이 부족한지 판단 ↓ 부족한 정보에만 겨냥해 Web Query를 생성 ↓ 그런 다음 검색

예를 들어 이렇게 생성한다.

첫 번째 《天龙八部》 드라마 방영 시간

그래서 web_query는 아무렇게나 끼워 넣은 필드가 아니다.

그것이 해결하는 것은:

이미 로컬에서 얻은 정보는 다시 찾을 필요 없이, 진짜로 부족한 부분만 검색한다.

이 사고방식은 지난 편의 하위 문제 분해와 사실 꽤 비슷하다.

본질적으로 둘 다 다음을 하는 것이다.

Query를 다음 검색에 더 적합하게 만든다.

七、이제 Graph가 처음으로 데이터 소스를 전환하기 시작한다

Evaluate에 결과가 생기면, 다음 단계로 어디로 갈지 결정할 수 있다.

로직은 아주 단순하다.

function afterEvaluateLocal ( state ) { const parsed = JSON . parse (state. evaluation || "{}" ); return parsed. enough === true ? "generate" : "web_search" ; }

만약 정보가 이미 충분하다면:

Evaluate ↓ Generate

만약 아직 부족하다면:

Evaluate ↓ Web Search

Graph는 이렇게 된다.

Local Retrieve ↓ Evaluate ↓ 충분한가? ↙ ↘ 충분 부족 ↓ ↓ Generate Web Search

여기서 사실 아주 중요한 변화가 일어난다.

예전에는:

데이터 소스가 고정되어 있었다.

이제는:

데이터 소스가 현재 정보에 따라 동적으로 전환되기 시작한다.

이것은 이미 Agentic RAG의 핵심에 아주 가까워졌다.

八、Web Search 노드는 도대체 무엇을 하는가?

여기에는 임의의 Web Search API를 붙일 수 있다.

구체적으로 어느 검색 서비스를 사용하는지는 이 절의 핵심이 아니다.

전체 Graph 입장에서 Web Search 노드는 단지 세 가지 일만 완수하면 된다.

1. State에서 web _query를 가져온다 2. 네트워크 검색 서비스를 호출한다 3. 검색 결과를 webContext에 다시 기록한다

핵심 로직은 이렇게 압축할 수 있다.

const webSearchNode = async (state) => { const parsed = JSON.parse(state.evaluation || "{}" ) ; const query = parsed.web_query?.trim() || state.question ; const webContext = await webSearch(query) ; return { webContext, } ; } ;

주의:

Web Search

자체는 사용자에게 답하는 역할을 담당하지 않는다.

그것은 단지:

자료를 한 묶음 더 찾아오는 것뿐이다.

이 점은 Milvus의 Retrieve 노드와 사실 아주 비슷하다.

다만 데이터 소스가 다를 뿐이다.

Local Retrieve → 로컬 벡터 데이터베이스 Web Search → 인터넷

九、네트워크 검색 결과도 본질적으로는 그저 Context다

네트워크 검색으로 가져온 뒤, 이렇게 정리할 수 있다.

인용: 1 제목: ... URL: ... 요약: ... 인용: 2 제목: ... URL: ... 요약: ...

그런 다음 이렇게 기록한다.

webContext

이때 State에는 두 부분의 컨텍스트가 생긴다.

localContext + webContext

마지막으로 답을 생성할 때, 이것들을 합친다.

const context = [ state.localContext, state.webContext, ] . filter ( Boolean ) . join ("\n\n");

그래서 LLM이 최종적으로 보는 것은:

소설 지식베이스의 관련 내용 + 네트워크 검색으로 보충한 정보 + 사용자 질문

방금의 질문에 대해:

최종 답변의 근거는 두 곳에서 온다.

虚竹의 신분 → 로컬 소설 지식베이스 드라마 방영 시간 → 네트워크 검색

이것이 다중 데이터 소스 RAG의 가장 직관적인 형태다.

十、왜 Web Search 이후에 다시 Evaluate를 해야 하는가?

여기는 전체 흐름에서 아주 주목할 만한 점이다.

우리는 곧바로 이렇게 쓰지 않았다.

Web Search ↓ Generate

Web Search ↓ Evaluate

이유는 아주 간단하다.

네트워크를 검색했다고 해서 검색해 온 정보가 반드시 충분한 것은 아니다.

검색 결과도 다음과 같을 수 있다.

적중하지 않음 결과가 빗나감 정보가 불완전함 핵심 사실을 확인할 수 없음

따라서 더 합리적인 역할 구분은 이래야 한다.

Search → 찾는 일을 담당 Evaluate → 판단하는 일을 담당

전체 과정은 이렇게 된다.

로컬 검색 ↓ 평가 ↓ 부족 ↓ 네트워크 검색 ↓ 재평가

이때에야 전체 검색 흐름이 진정한 폐루프를 형성한다.

十一、Agentic은 무한 루프를 뜻하지 않는다

하지만 여기에는 아주 현실적인 문제가 하나 더 있다.

만약 이렇게 작성하면:

Evaluate ↓ Web Search ↓ Evaluate ↓ Web Search ↓ ……

이론상 계속 순환할 수 있다.

이것은 분명 우리가 원하는 것이 아니다.

그래서 실제 시스템은 반드시 경계를 추가해야 한다.

현재의 이 간단한 Demo는 이렇게 규정할 수 있다.

로컬 검색 ↓ 첫 번째 Evaluate ↓ 부족 ↓ Web Search ↓ 두 번째 Evaluate ↓ Generate

만약 네트워크로 보충한 뒤에도 여전히 답을 확인할 수 없다면, 사용자에게 분명히 알린다.

현재 컨텍스트만으로는 여전히 확인하기에 부족하다

그리고 계속 지어내지 않는다.

이것 역시 Agent를 구현할 때 아주 중요한 점이다.

자율적 의사결정이 무한한 자율을 뜻하지는 않는다.

시스템에는 여전히 명확한 종료 조건과 실행 경계가 필요하다.

十二、이번 편의 Graph를 하나로 이어 붙이기

이제 전체 흐름은 대략 이렇다.

┌→ direct_answer → END │ Question → Router │ └→ local_retrieve ↓ evaluate ↓ 정보가 충분한가? ↙ ↘ 충분 부족 ↓ ↓ generate web_search ↑ ↓ └──── evaluate

LangGraph에 대응시키면:

const graph = new StateGraph (GraphState) . addNode ( "route_question" , routeQuestionNode) . addNode ( "direct_answer" , directAnswerNode) . addNode ( "local_retrieve" , retrieveLocalNode) . addNode ( "evaluate_local" , evaluateNode) . addNode ( "web_search" , webSearchNode) . addNode ( "generate" , generateNode) . addEdge (START, "route_question" ) . addConditionalEdges ( "route_question" , afterRoute, { direct_answer : "direct_answer" , local_retrieve : "local_retrieve" , }) . addEdge ( "local_retrieve" , "evaluate_local" ) . addConditionalEdges ( "evaluate_local" , afterEvaluateLocal, { generate : "generate" , web_search : "web_search" , } ) . addEdge ( "web_search" , "evaluate_local" ) . addEdge ( "direct_answer" , END) . addEdge ( "generate" , END) . compile ();

코드만 보면 그저 다음이 추가되었을 뿐이다.

두 개의 노드.

하지만 시스템 능력은 사실 이미 아주 뚜렷하게 달라졌다.

十三、'검색 흐름'에서 '검색 결정'으로

맨 처음의 RAG를 다시 돌아보자.

모든 단계가 고정되어 있다.

이후 Router를 추가했다.

검색을 할 것인가?

그다음 멀티홉 검색을 추가했다.

한 번의 검색으로 충분한가?

이제 여기에 Evaluate + Web Search를 또 추가했다.

로컬 정보가 충분한가? 만약 부족하다면, 다른 데이터 소스로 바꿔 계속 찾아야 하는가?

그래서 Agentic RAG에서 진짜 중요한 것은 다음이 아니다.

Agent를 몇 개나 썼는가

이것도 아니다.

Graph를 얼마나 복잡하게 그렸는가

진짜로 달라진 것은 이것이다.

LLM이 검색 흐름 자체의 의사결정에 참여하기 시작했다.

十四、여기까지 오면 Agentic RAG의 큰 줄기는 이미 기본적으로 완성되었다

이제 이 흐름은 전통적인 RAG에서의 몇 가지 아주 전형적인 문제를 이미 처리할 수 있다.

간단한 질문 → 검색하지 않고 바로 답변 지식베이스 질문 → Milvus 조회 복잡한 질문 → 여러 하위 질문으로 분해, 여러 차례 검색 정보 부족 → 현재 무엇이 빠졌는지 판단 로컬에 없음 → 자동으로 Web Search로 전환

이어 붙이면 이렇게 된다.

Question ↓ 조회할지 판단 ↓ 어떻게 조회할지 판단 ↓ 검색 ↓ 찾은 것이 충분한지 판단 ↓ 부족하면 계속 조회 / 데이터 소스 교체 ↓ Generate

이때 Agentic RAG를 다시 보면, 다음과 같이 간단히 이해해서는 안 된다.

RAG에 Agent 몇 개를 더한 것.

더 정확히 말하면 이렇다.

LLM을 검색 흐름의 의사결정 중추로 삼아, 현재 질문과 이미 가진 근거에 따라 다음에 무엇을 해야 할지를 동적으로 결정하게 하는 것.

十五、그런데 아직 보완되지 않은 검색 능력이 하나 더 있다

지금 우리에게는 이미 이것이 있다.

Milvus → 시맨틱 검색

그리고 이것도 있다.

Web Search → 네트워크 정보 보완

그런데 아직 이런 유형의 데이터가 있다.

전문 용어 정확한 엔터티 고유명사

이런 내용은 때로 필요로 하는 것이 다음이 아니다.

의미가 비슷하면 됨

오히려 필요한 것은 이것이다.

키워드가 정확히 적중함

이때 순수 벡터 검색이 반드시 적합한 것은 아니다.

그래서 다음으로 이런 능력 하나를 더 보완해야 한다.

키워드 검색

이제 Elasticsearch로 들어갈 차례다.

하이브리드 검색을 계속 다루기 전에, 먼저 Elasticsearch의 가장 핵심적인 세 가지를 분명히 해두자.

IK 분석기 → 단어를 어떻게 자를지 역색인 → 문서를 어떻게 빠르게 찾을지 BM25 → 찾은 뒤 어떻게 정렬할지

이것이 다음 편이 될 것이다.

东风破_

112

23k

읽음

67

팔로워