📚 Agentic AI - 기업용 자율 에이전트 개발 6장 · 에이전트 ④ RAG 문서검색 Agentic AI란

검색을 도구로 만들면 다른 도구와 함께 쓸 수 있다

한 줄 요약

지금까지의 answer()RAG 전용 함수입니다. 이걸 도구(tool) 로 만들면, 3~5장에서 만든 도구들과 똑같은 방식으로 에이전트에 붙일 수 있습니다. 8장으로 가는 다리입니다.


1. 지금 방식의 한계

아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다.

r = answer("불량 상품 반품은 며칠 이내인가요?")

이 함수는 문서 질문만 처리합니다. 다른 질문이 섞이면 어떻게 될까요?

"P0003 재고가 몇 개이고, 반품 기간은 며칠인가요?"CSV문서"승승 스마트워치 Fit 5의 제품 사양과 환불·교환 기간을 알려주세요."제품매뉴얼환불교환정책한 질문 안에 서로 다른 곳을 봐야 하는 대목이 섞여 있습니다. 그래서 검색도 '도구 하나'가 되어야 합니다.
한 질문 안에 서로 다른 곳을 봐야 하는 대목이 섞여 있다

answer()문서만 봅니다. 재고는 답할 수 없습니다.

반대로 5장의 CSV 에이전트는 문서를 못 봅니다.

둘을 한 에이전트에 붙이려면 검색도 "도구"여야 합니다.


2. 검색을 도구로 만들기

아래는 code/ch06_rag_agent.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.

from langchain_core.tools.retriever import create_retriever_tool

policy_tool = create_retriever_tool(
    vectorstore.as_retriever(search_kwargs={"k": 6}),
    "policy_search",
    "승승장구몰의 사내 규정과 제품 매뉴얼을 검색한다. "
    "환불·교환·반품 기간, 멤버십 등급과 적립률, 제품 사양 같은 질문에 사용한다.",
)

create_retriever_tool(retriever, name, description) — 인자 셋입니다.

인자 무엇 3장으로 치면
첫 번째 검색기 함수 본체
두 번째 도구 이름 함수 이름
세 번째 설명 독스트링

세 번째가 독스트링 자리입니다. 3장 3절에서 배운 원칙이 그대로 적용됩니다.


3. 설명을 뜯어보기

"승승장구몰의 사내 규정과 제품 매뉴얼을 검색한다.
 환불·교환·반품 기간, 멤버십 등급과 적립률, 제품 사양 같은 질문에 사용한다."
부분 역할
"사내 규정과 제품 매뉴얼을 검색한다" 무엇을 하는가
"환불·교환·반품 기간, 멤버십 등급..., 제품 사양" 언제 쓰는가 (구체적 예)

구체적인 예를 나열한 것이 핵심입니다. 4장 4절에서 배운 경계선 긋기입니다.

8장에서 이 도구는 run_pandas·search_web 과 함께 쓰이게 됩니다. 그때 모델이 헷갈리지 않으려면 "이건 규정과 사양 질문에 쓴다" 가 분명해야 합니다.


4. k=6 인 이유

앞에서 만든 retrieverk=4 인데, 도구용은 k=6 입니다.

아래는 code/ch06_rag_agent.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.

retriever = vectorstore.as_retriever(search_kwargs={"k": 4})        # answer() 용

policy_tool = create_retriever_tool(
    vectorstore.as_retriever(search_kwargs={"k": 6}),               # 도구용
    ...)

같은 인덱스에서 검색기를 두 개 만든 것입니다. 인덱스를 두 번 만드는 게 아닙니다.

인덱스 (벡터 15개)파일에서 불러온 인덱스 하나as_retriever(k=4)as_retriever(k=6)retrieveranswer()출처가 붙는다create_retriever_toolpolicy_tool출처는 안 붙는다→ 8장 규정담당에게같은 인덱스를 k만 달리해 두 번 씁니다. 사람에게 보여 줄 때는 출처가 필요하고, 담당자끼리 주고받을 때는 필요 없습니다.
인덱스 하나를 k 만 달리해 두 갈래로 쓴다

창고는 하나, 창구가 둘입니다. 창구마다 "몇 개씩 꺼내 줄지"(k)를 다르게 정한 것이죠.

왜 도구용은 더 넉넉한가 — 8장에서 이런 요청이 들어오기 때문입니다.

"P0003 재고가 몇 개이고, 반품 기간은 며칠인가요?"CSV문서"승승 스마트워치 Fit 5의 제품 사양과 환불·교환 기간을 알려주세요."제품매뉴얼환불교환정책한 질문 안에 서로 다른 곳을 봐야 하는 대목이 섞여 있습니다. 그래서 검색도 '도구 하나'가 되어야 합니다.
이 질문도 마찬가지다 — 사양은 제품매뉴얼에, 기간은 환불교환정책에 있다

주제가 둘입니다. k=4 면 한쪽 문서의 조각들이 4자리를 다 차지해 다른 쪽을 놓칠 수 있습니다.

실제로 8장을 만들 때 이 문제가 났습니다.

[k=4 일 때]
  규정담당 보고: 제품 사양은 ... (사양만 나옴)
                환불·교환 기간에 대한 사내 규정은 문서에 없습니다.   ← 틀림!

[k=6 일 때]
  규정담당 보고: 제품 사양은 ...
                단순 변심: 7일 이내 / 상품 불량·하자: 30일 이내      ← 맞음

이런 게 실제로 RAG를 손보는 일입니다. 모델을 바꾼 게 아니라 숫자 하나를 4에서 6으로 올렸습니다. 4절에서 "RAG 품질은 대부분 세 숫자에서 결정된다"고 한 것이 이런 뜻입니다.


5. 도구로 쓰면 무엇이 달라지나

answer() — 우리가 흐름을 정한다

아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다.

docs = retriever.invoke(question)      # 우리가 검색하고
context = ...                          # 우리가 합치고
body = llm.invoke(PROMPT...)            # 우리가 프롬프트를 만든다

항상 검색합니다. 문서와 무관한 질문이 와도 검색합니다.

policy_tool — 모델이 판단한다

아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다.

agent = create_agent(llm, tools=[policy_tool, run_pandas], system_prompt=...)

모델이 "이 질문에는 문서 검색이 필요하다"고 판단할 때만 부릅니다. 그리고 다른 도구와 섞어 쓸 수 있습니다.

질문: "P0003 재고가 몇 개이고, 반품 기간은 며칠인가요?"

  [1단계] run_pandas(...)      → 재고 3개
  [2단계] policy_search(...)   → 수령일로부터 30일 이내
  [3단계] 두 결과를 합쳐 답변

4장의 ReAct 루프 그대로입니다. 도구 종류만 늘었습니다.


6. 잃는 것 — 출처 표시

도구로 만들면 출처를 붙이는 코드가 사라집니다.

아래는 code/ch06_rag_agent.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.

# answer() 에는 있었던 것
sources.append(f"{key[0]} {key[1] + 1}쪽")

create_retriever_tool 은 조각의 본문만 모델에게 돌려줍니다. metadata 는 안 넘어갑니다.

7절의 두 갈래 그림에서 아래 갈래가 잘린 것입니다.

질문retriever (k=6)Document ×6page_content (본문)metadata (출처)✂ 여기서 끊긴다모델에 전달규정담당의 보고출처 없음create_retriever_tool은 본문 문자열만 돌려줍니다. 출처가 필요하면 6장 7절처럼 직접 꺼내야 합니다.
도구로 만들면 본문만 넘어가고 출처는 여기서 끊긴다

그래서 8장의 답변에는 "환불교환정책.pdf 1쪽" 같은 출처가 안 붙습니다. 대신 "(규정담당)" 처럼 누가 조사했는지가 출처 자리를 대신합니다 — 8장 7절에서 봅니다.

되찾으려면

create_retriever_tool 을 안 쓰고 직접 도구를 만들면 됩니다.

아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다.

@tool
def policy_search(query: str) -> str:
    """승승장구몰의 사내 규정과 제품 매뉴얼을 검색한다. ..."""
    docs = vectorstore.as_retriever(search_kwargs={"k": 6}).invoke(query)
    return "\n\n".join(
        f"[{d.metadata['source']} {d.metadata['page']+1}쪽]\n{d.page_content}"
        for d in docs)

출처를 본문 안에 넣어 돌려주는 것입니다. 그러면 모델이 답할 때 인용할 수 있습니다.

3장 3절의 원칙이 여기서 다시 쓰입니다도구의 반환값은 "다음 사람이 읽을 보고서" 입니다. 출처를 보여 주고 싶으면 반환 문자열에 넣어야 합니다.

이 과정의 실습 코드는 단순함을 위해 create_retriever_tool 을 썼습니다. 출처가 중요한 서비스라면 위 방식으로 바꾸세요.

여기가 이 과정에서 출처 정보가 빠지는 지점입니다. answer() 는 출처를 붙이고, 도구로 만들면 사라집니다. 왜 사라지는지를 눈으로 본 것이 이 절에서 배운 점입니다 — 되찾는 법도 위에 있고요.


7. 네 종류가 모두 도구가 됐습니다

이 절로 준비가 끝났습니다.

에이전트 도구
① 사내 데이터 조회 get_order_status 3장
② 웹검색 search_web 4장
③ CSV 데이터분석 run_pandas 5장
④ RAG 문서검색 policy_tool 6장

전부 같은 모양입니다 — 문자열을 받아 문자열을 돌려주는 것.

이제 이것들을 한 에이전트에 다 붙일 수도 있고, 여러 에이전트에 나눠 줄 수도 있습니다. 어느 쪽이 나은지가 8장의 주제입니다.

미리 답을 말하면 — 4장 4절에서 봤듯 한 명에게 다 몰면 판단이 나빠집니다. 그래서 나눕니다.


스스로 해 보기

  1. 도구를 직접 불러 보세요.
    python print(policy_tool.invoke({"query": "반품 기간"})[:300])
    answer() 의 결과와 무엇이 다른가요?
  2. 위의 출처를 넣는 버전을 직접 만들어 실행해 보세요.
  3. policy_tool 과 5장의 run_pandas한 에이전트에 붙여 보세요.
    ```python
    from ch05_csv_agent import run_pandas
    from langchain.agents import create_agent
    from common import get_chat

llm = get_chat(temperature=0)
agent = create_agent(llm, tools=[policy_tool, run_pandas],
system_prompt="너는 승승장구몰 담당자다. 사실만 근거로 간결히 답한다.")
out = agent.invoke({"messages": [{"role": "user",
"content": "P0003 재고가 몇 개이고, 불량 반품 기간은 며칠인가요?"}]})
print(out["messages"][-1].text)
```
도구 두 개를 순서대로 쓰는지 확인하세요.

3번이 8장의 예고편입니다. 직접 해 보면 다음 질문이 자연스럽게 떠오릅니다 — "도구를 더 붙이면 어디까지 될까?"


핵심 정리

  • create_retriever_tool(retriever, name, description) 으로 검색을 도구로 만듭니다.
  • 세 번째 인자가 독스트링 자리입니다. 언제 쓰는지 구체적인 예를 나열합니다.
  • 도구용 검색기는 k=6 으로 넉넉히 잡았습니다. 8장에서 두 주제를 한 번에 묻는 일이 있기 때문입니다.
  • 같은 인덱스에서 검색기를 여러 개 만들 수 있습니다.
  • answer()항상 검색하고, policy_tool모델이 필요하다고 판단할 때만 불립니다.
  • 도구로 만들면 출처 표시가 사라집니다. 되찾으려면 반환 문자열에 출처를 넣어 직접 도구를 만듭니다.
  • 네 종류가 전부 도구가 됐습니다. 이제 한 명에게 다 줄지, 나눠 줄지가 8장의 주제입니다.
← 이전 절07. 따라하기 출처와 함께 답하는 검색 에이전트다음 절 →09. CSV 분석과 RAG 무엇을 언제 쓰나