검색을 도구로 바꾸면 달라지는 것, Agentic RAG

도서관에서 조사 숙제를 할 때 사서에게 "무조건 책 세 권을 먼저 가져다 달라"고 부탁하는 방법이 있고 필요하다고 생각할 때 직접 서가에 가는 방법이 있습니다. 앞쪽은 검색 횟수가 정해져 있어 빠르고 예측하기 쉽습니다. 뒤쪽은 질문마다 가는 횟수가 달라집니다.

RAG(검색해서 찾은 글을 모델에게 근거로 함께 주는 방식)도 둘로 나뉩니다. 질문마다 검색을 한 번 하고 답하게 하는 방식은 2-Step RAG이고 검색을 도구로 줘서 모델이 할지 말지와 검색어를 정하는 방식은 Agentic RAG입니다. 같은 질문을 두 방식에 보내면 어떻게 달라지는지 아래에서 확인합니다.

질문

설명을 위한 예시 장면입니다. 실제 검색 결과와 호출 횟수는 모델과 자료에 따라 다릅니다.

매뉴얼은 청크로 나눠 벡터 저장소에 넣습니다

검색을 도구로 만들기 전에 검색할 자료를 준비해야 합니다. IT 매뉴얼 같은 긴 글은 한꺼번에 컨텍스트에 넣을 수 없어서 청크(chunk, 검색할 때 한 번에 꺼내는 작은 토막)로 나눕니다. 각 청크는 임베딩(embedding, 글의 뜻을 숫자 목록으로 바꾼 것)으로 바뀌어 벡터 저장소(vector store, 임베딩을 모아 두고 비슷한 것을 찾아 주는 곳)에 들어갑니다. 스크롤하며 청크가 뜻의 지도 위 점이 되는 과정을 보세요.

긴 매뉴얼 한 권

VPN 접속 오류와 프린터 용지 걸림 해결법이 한 파일에 이어져 있습니다. 통째로 모델에게 주기에는 깁니다.

청크로 자르기

일정한 길이로 자르면 세 조각이 나옵니다. 둘째 조각은 VPN 해결 방법의 둘째 단계인데 잘려 나오면서 VPN이라는 말을 잃었습니다.

뜻의 지도에 점으로 놓기

임베딩은 글의 뜻을 지도 위 좌표로 바꿉니다. 뜻이 비슷한 글은 가까이 놓이고 다른 글은 멀리 놓입니다. 청크마다 점 하나가 되어 벡터 저장소에 들어갑니다.

질문도 점이 되어 가까운 것을 찾음

질문도 같은 방식으로 점이 되고 가장 가까운 청크 두 개(k=2)가 뽑힙니다. 질문을 바꿔 가며 어떤 청크가 뽑히는지 보세요. "VPN 인증서"를 검색해도 청크 2가 안 나올 수 있습니다.

설명을 위한 예시 매뉴얼을 langchain-text-splitters 1.1의 RecursiveCharacterTextSplitter(chunk_size=60)로 직접 나눈 결과이고 지도 위 점 위치는 설명을 위한 값입니다. Anthropic은 청크마다 앞뒤 맥락을 덧붙이는 Contextual Retrieval로 상위 20개 청크의 검색 실패율을 5.7%에서 3.7%로 줄였다고 밝혔습니다.

검색을 함수로 감싸면 도구가 됩니다

벡터 저장소의 검색은 함수 하나입니다. 이 함수를 앞서 만든 방식대로 @tool로 감싸면 모델이 부를 수 있는 검색 도구가 됩니다. 모델이 설명서를 읽고 검색할지 말지와 어떤 검색어를 쓸지를 정합니다. 검색어를 쓰는 일도 모델이 하니 질문을 다듬어 쓸 수 있고 첫 검색이 빗나가면 다른 검색어로 다시 찾을 수 있습니다.

도구 결과는 모델이 읽는 글과 코드가 보관하는 부가 정보로 나눌 수 있습니다. response_format="content_and_artifact"로 두면 도구가 글(content)과 부가 정보(artifact) 두 개를 돌려주고 글만 모델에게 갑니다. 글과 출처 같은 정보(metadata)를 함께 담는 LangChain의 Document 객체를 artifact에 두면 모델 컨텍스트를 늘리지 않고도 출처를 보관할 수 있습니다.

content_and_artifact로 두면 도구가 (글, 부가 정보) 두 값을 돌려주고 그중 글만 모델의 컨텍스트에 들어갑니다.

rag_tool.pypython
# 1 청크로 나눠 벡터 저장소에 넣는다
splitter = RecursiveCharacterTextSplitter(chunk_size=60, chunk_overlap=10)
chunks = splitter.split_documents([manual])
vs = InMemoryVectorStore(embeddings); vs.add_documents(chunks)

# 2 검색을 도구로 감싼다: 글은 모델에게, Document는 코드에
@tool(response_format="content_and_artifact")
def search_manual(query: str):
    """IT 매뉴얼에서 해결 방법을 찾는다. 증상이나 오류 이름으로 검색한다."""
    docs = vs.similarity_search(query, k=2)
    text = "\n\n".join(f"[출처: {d.metadata['source']}]\n{d.page_content}" for d in docs)
    return text, docs
  1. 자료를 저장합니다. chunk_overlap=10은 이웃한 청크가 10자씩 겹치게 해서 문장이 잘리는 손실을 줄입니다. embeddings는 회사의 임베딩 모델을 가리키는 값이며 그 모델이 글을 숫자 목록으로 바꿉니다.
  2. 도구로 감쌉니다. 설명 글에는 "언제 쓰는 도구인지"를 적습니다. similarity_search는 질문과 가까운 청크를 k개(여기서는 2개) 찾습니다. 반환값은 (글, Document 목록) 두 개입니다. 글에는 출처를 붙여 모델이 답에 인용할 수 있게 합니다.
rag_agent.pypython
agent = create_agent(
    model, tools=[search_manual],
    # 3 근거 없이 답하지 않게 시킨다
    system_prompt="매뉴얼 근거 없이 답하지 않는다. 먼저 search_manual로 찾고 출처를 밝힌다.")
r = agent.invoke({"messages": [{"role": "user", "content": "VPN이 안 붙어요"}]})
tool_msg = [m for m in r["messages"] if m.type == "tool"][0]
print(tool_msg.content.splitlines()[0])
# 4 artifact로 보관한 출처 정보
print(len(tool_msg.artifact), tool_msg.artifact[0].metadata)
print(r["messages"][-1].content)
터미널
$ python rag_agent.py [출처: it-manual.md] 2 {'source': 'it-manual.md'} 매뉴얼(it-manual.md)에 따르면 앱을 완전히 종료한 뒤 다시 실행하세요.

출력은 예시 답입니다. 코드는 langchain 1.4 기준이고 검색 결과는 설명용 매뉴얼에서 찾은 것입니다.

  1. 검색을 강제하는 지시입니다. 시스템 프롬프트에 "근거 없이 답하지 않는다"를 적어 두면 모델이 기억에 의존하지 않고 먼저 검색합니다. LangChain의 RAG 문서도 비슷한 문장으로 먼저 검색하라고 시킵니다.
  2. 부가 정보를 꺼냅니다. 도구 결과 메시지의 artifact에 Document 목록이 들어 있어서 어느 파일에서 왔는지 코드가 읽을 수 있습니다. 모델에게는 글만 갔습니다.

설명을 위한 예시 답변입니다. 번호를 고르면 어느 청크에서 나온 문장인지 표시됩니다.

답 옆에 출처를 붙이면 확인할 수 있습니다

Agentic RAG의 답이 믿을 만한지는 어느 문서에서 가져왔는지로 확인합니다. 도구 결과에 [출처: 파일 이름]을 붙이고 시스템 프롬프트에서 답에 출처를 밝히라고 시키면 모델이 [1], [2] 같은 번호로 근거를 달 수 있습니다. 코드는 artifact의 메타데이터로 번호와 청크를 짝지어 화면에 보여 줍니다.

Anthropic API에는 이 일을 맡아 주는 citations(인용) 기능도 있습니다. 문서 블록에 "citations": {"enabled": true}를 주면 응답에 어느 문서의 어느 부분을 인용했는지가 붙고 인용된 글은 출력 토큰으로 세지 않는다고 문서에 적혀 있습니다.

검색이 빗나가거나 비면 Agent는 다시 찾아야 합니다

검색을 모델이 정하는 만큼 실패도 다양합니다. 결과가 질문과 무관하거나 아예 비어 있기도 합니다. 모델이 검색을 건너뛰고 기억으로 답하거나 같은 검색어를 되풀이하기도 합니다. 이 글에서 직접 확인한 것은 도구가 값을 돌려주는 구조와 검색을 두 번 부르는 흐름이고 나머지 처방은 앞 글들의 장치를 검색에 적용한 것입니다.

유형을 고르면 장면과 처방이 나옵니다. 처방은 모델에게 시키는 방법(프롬프트), 코드로 막는 방법, 한도 세 가지로 나뉩니다.

처방은 앞 글에서 본 시스템 프롬프트, 도구 오류 처리, 단계 한도(recursion_limit), 호출 제한 Middleware를 검색 도구에 적용한 것입니다. 검색 결과 글에 든 지시를 따르지 않게 하는 보안 문제는 별도로 다룹니다. LangChain 문서에도 검색된 글 속에 숨은 지시로 모델을 조종하는 공격(간접 프롬프트 주입)은 어떤 프롬프트로도 완전히 막지 못한다고 적혀 있습니다.

다음 글에서는 정해진 순서와 분기를 직접 설계하는 LangGraph의 State, Node, Edge를 알아보겠습니다.

참고 자료

출처 5개 펼치기