실무 Multi-Agent 오케스트레이션 21장 · 전문 에이전트 구현 5 / 9 ← 이전목차다음 → TechLead Cro

21장. 전문 에이전트 구현

검색을 도구로 만든다 — 언제, 몇 번 찾을지를 에이전트가 정한다

한 줄 요약

14장에서는 코드가 먼저 검색하고 모델은 받은 발췌로 답했습니다. 이번 장의 정책안내 에이전트는 검색을 도구로 받습니다. 그러면 검색어를 무엇으로 할지, 몇 번 찾을지를 에이전트가 정합니다. 이 구조를 흔히 에이전틱 RAG(Agentic RAG) 라고 부릅니다.


1. 두 가지 방식

고정된 순서 (14장, 19장 정책 노드) 검색이 도구 (이번 장)
검색을 실행하는 것 우리 코드 에이전트가 요청하면 실행
검색어 고객 문의 그대로 에이전트가 만든다
검색 횟수 문의마다 정확히 1번 에이전트가 필요한 만큼
호출 수 적다. 미리 알 수 있다 많아질 수 있다. 미리 알 수 없다

19장의 정책 노드를 다시 봅니다.

docs = vectorstore.similarity_search(state["question"], k=3)   # 코드가 먼저 검색

고객이 무엇을 묻든 문의 문장 그대로 한 번 검색합니다. 단순하고 빠릅니다. 그런데 이런 문의에서는 아쉽습니다.

"VIP 등급 고객이 단순 변심으로 반품하면 배송비를 내야 하나요?"

이 문의의 답은 문서 두 곳에 걸쳐 있습니다. 반품 배송비 규정은 반품·교환·환불 정책에, VIP 혜택은 멤버십 정책에 있습니다. 문장 하나로 한 번 검색해서 두 곳의 조각이 모두 걸려 나올지는 운에 가깝습니다.


2. 검색 함수에 @tool을 붙인다

검색을 도구로 만드는 방법은 11장에서 본 그대로입니다. 함수를 만들고 @tool을 붙입니다.

@tool
def search_policy(query: str) -> str:
    """하루마켓 정책 문서에서 관련 조항을 검색한다.
    대상 문서: 반품·교환·환불 정책, 하루클럽 멤버십 정책(등급·적립금·혜택).

    Args:
        query: 검색할 정책 관련 질문. 예: '단순 변심 반품 배송비', 'VIP 등급 혜택'
    """
    docs = vectorstore.similarity_search(query, k=5)
    if not docs:
        return "검색 결과 없음"
    return "\n\n".join(
        f"[{d.metadata.get('doc_title', '정책')} · "
        f"{d.metadata.get('article') or '조항 미상'}]\n{d.page_content}"
        for d in docs)

볼 곳이 세 군데입니다.

부분 하는 일
독스트링의 "대상 문서: …" 이 도구로 무엇을 찾을 수 있는지 모델에게 알려 줍니다. 5장에서 본 대로 도구 설명은 모델이 읽는 글입니다
Args:의 예시 검색어 검색어를 짧은 핵심어로 만들도록 이끕니다. 고객 문장을 통째로 넣지 않게 됩니다
돌려주는 문자열의 [문서명 · 조항] 12장에서 조각마다 붙여 둔 메타데이터를 꺼내 머리표로 답니다. 에이전트가 근거를 표기할 재료입니다

조각은 질문과 가까운 순서로 다섯 개(k=5)를 가져옵니다. 정책 문서 두 개가 열두 조각이므로, 다섯 개면 한 조항과 그 조항을 다시 설명한 자주 묻는 질문이 함께 들어옵니다.

검색 결과가 0건이면 빈 문자열이 아니라 "검색 결과 없음" 을 돌려줍니다. 8장의 원칙 그대로, 도구는 어떤 경우에도 모델이 상황을 알 수 있는 값을 돌려줍니다.


3. 에이전트에게 "여러 번 찾아도 된다"고 알려 준다

도구를 주는 것만으로는 부족합니다. 정책안내 에이전트의 프롬프트에 이 줄이 있습니다.

- 질문에 두 정책이 얽혀 있으면(예: VIP의 반품 배송비) 필요한 만큼
  search_policy 를 여러 번 검색해 종합합니다.

나머지 규칙의 뼈대는 14장에서 쓴 것과 같습니다.

- 모든 답변은 search_policy 검색 결과에 근거해야 합니다.
- 답변 끝에 (근거: 문서명 제N조) 를 표기합니다. 꼬리표에 조항이 여럿이면
  본문을 읽어, 답한 내용이 실제로 적힌 문단 바로 위의 조항만 적습니다.
  같은 내용이 조항과 자주 묻는 질문(FAQ) 양쪽에 있으면 조항을 적습니다.
- 배송비는 반품(편도)과 교환(왕복)을 구분해 보고합니다. 기간·금액이
  결제수단이나 사유에 따라 다르면 경우별로 모두 보고합니다.
- 검색 결과에 없는 내용은 '정책 문서에서 확인되지 않음'이라고 보고합니다.
- 금액·기간 숫자는 검색 결과 그대로만 사용합니다.

근거로만 답하고, 없으면 없다고 하고, 숫자는 그대로 옮긴다. 검색하는 방식이 바뀌어도 이 세 가지는 바뀌지 않습니다.

가운데 두 줄은 이 에이전트의 보고를 다른 쪽이 받아 쓴다는 점을 생각해 넣은 것입니다.

줄 왜 넣었나
근거 조항을 고르는 법 조각 하나에 조항이 여러 개 걸쳐 있을 수 있습니다(12장). 머리표에는 그 조항이 모두 적혀 있으므로, 답이 실제로 적힌 조항을 본문에서 골라 적게 합니다
반품과 교환을 구분, 경우별로 모두 배송비는 반품이 편도 3,000원, 교환이 왕복 6,000원으로 다릅니다. 환불 기간은 결제수단마다 다릅니다. 하나만 보고하면 받는 쪽이 나머지를 알 수 없습니다

4. 에이전트를 만드는 코드

11장에서 쓴 create_agent를 세 번 부릅니다. 정책안내 에이전트입니다.

policy_agent = create_agent(
    model=_llm(),
    tools=[search_policy],
    system_prompt=(
        "당신은 하루마켓 정책안내 전문 에이전트입니다. "
        "반품·교환·환불 정책과 하루클럽 멤버십 정책을 담당합니다.\n"
        ...),
)
인자 넣은 것
model Gemini를 LangChain에서 쓸 수 있게 감싼 모델 객체
tools 이 에이전트가 쓸 수 있는 도구 목록. 하나뿐입니다
system_prompt 이 에이전트의 역할과 규칙

세 에이전트는 이 세 인자의 값만 다릅니다. 에이전트를 나눈다는 것은 코드에서 tools와 system_prompt를 다르게 준다는 뜻입니다.

모델 객체를 만드는 _llm()에는 temperature를 넣지 않았습니다. Gemini 3 계열은 temperature를 기본값(1.0) 그대로 두라는 것이 공식 문서의 권장이고(2026년 10월 기준, https://ai.google.dev/gemini-api/docs/gemini-3), 모든 장에서는 config.MODEL에 지정한 gemini-3.8-flash를 사용합니다.


5. 얻는 것과 치르는 것

검색을 도구로 만들면 유연해집니다. 문서 두 곳에 걸친 질문에 두 번 검색할 수 있고, 첫 검색이 빈약하면 검색어를 바꿔 다시 찾을 수 있습니다.

대신 미리 알 수 없게 됩니다. 몇 번 검색할지를 모델이 정하므로 호출 수와 시간이 문의마다 달라집니다. 검색이 필요한데 하지 않을 가능성도 생깁니다. 그래서 프롬프트에 "모든 답변은 검색 결과에 근거해야 합니다"를 적었고, 「따라하기」에서 실제로 검색했는지를 도구 호출 기록으로 확인합니다.


핵심 정리

  • 14장과 19장은 코드가 먼저 한 번 검색했습니다. 이번 장은 검색을 도구로 줍니다.
  • 검색이 도구가 되면 검색어와 횟수를 에이전트가 정합니다. 이것이 에이전틱 RAG입니다.
  • 도구의 독스트링에 무엇을 찾을 수 있는지와 검색어 예시를 적습니다.
  • 검색 결과에 문서명과 조항을 머리표로 달아 근거 표기의 재료를 줍니다.
  • "근거로만, 없으면 없다고, 숫자는 그대로"는 검색 방식이 바뀌어도 그대로입니다.
  • 유연해지는 대신 호출 수를 미리 알 수 없게 됩니다.
← 이전 절처리를 위험도로 나눈다 — 직접 처리, 승인 요청, 상담원에게다음 절 →따라하기 1 — 처리 도구 만들기
오명운 · macro@prag-ai.com