RAG부터 MCP 도구 실행까지: 운영용 AI 챗봇 아키텍처

데모 챗봇은 모델 API 한 번으로 만들 수 있지만, 운영 환경에서는 지식 검색과 도구 실행, 권한, 비용, 대화 상태를 함께 관리해야 합니다. 처음에는 모델 호출 앞에 검색 결과를 붙이고 필요한 기능을 tool로 노출하면 충분해 보였습니다. 기능이 늘어나자 검색 실패와 도구 실패, 권한 거부를 구분하기 어려워졌고 한 요청의 비용과 상태도 여러 곳에 흩어졌습니다.

이를 해결하기 위해 질문 해석부터 검색과 실행, 응답 생성까지를 독립된 계층으로 나눴습니다.

RAG와 Tool을 한 흐름에 넣자 실패 원인이 섞였다

사용자 질문을 그대로 모델에 보내면 사내 지식이 부족하고 최신 운영 데이터에도 접근할 수 없었습니다. 반대로 검색 문서와 모든 도구 결과를 한 번에 넣으면 컨텍스트 비용이 커지고 잘못된 도구 실행 위험이 생겼습니다. 모델 공급자별 메시지 형식과 tool calling 차이가 서비스 코드에 퍼지면서 교체와 장애 대응도 어려웠습니다.

검색과 실행 사이에 권한 경계를 뒀다

검색 결과는 답변의 근거일 뿐 실행 권한이 아니라고 명시했습니다. 모델이 도구 호출을 제안해도 정책 계층이 사용자 권한과 대상 자원을 다시 확인한 뒤 실행합니다. 이 분리 덕분에 검색 문서의 내용이 실행 가능 범위를 넓히지 못했습니다.

  • 모델 공급자, 검색, 도구 실행, 대화 저장을 명확한 인터페이스로 분리합니다.
  • 도구 실행은 사용자 권한을 상속하고 서버가 파라미터를 다시 검증합니다.
  • 각 단계의 입력·출력 토큰과 지연 시간을 관찰합니다.

질문을 답변으로 만드는 처리 계층

요청은 먼저 대화 컨텍스트와 사용자 권한을 불러오고, 의도 분류 결과에 따라 일반 답변, RAG, 도구 계획 경로로 나뉩니다. 검색 경로에서는 질문을 재작성하고 의도를 분류한 뒤 관련 문서 조각만 선택합니다.

도구 계획 결과는 허용 목록과 JSON Schema로 검증하고 MCP 또는 내부 API를 실행합니다. 실행 결과는 크기를 제한해 모델이 최종 답변과 근거를 구성하도록 전달합니다.

Provider adapter는 공통 메시지와 usage 모델을 각 LLM SDK 형식으로 변환합니다. 대화와 도구 실행 기록은 사용자가 취소하거나 재접속해도 이어질 수 있게 저장합니다.

각 계층은 작은 계약만 가졌다

question -> rewrite -> intent
  -> knowledge: retrieve -> answer + citations
  -> read tool: authorize -> execute -> summarize
  -> write tool: propose -> confirm -> execute -> audit
type TurnState =
  | { kind: 'retrieval'; query: string }
  | { kind: 'tool'; name: string; args: unknown; approved: boolean }
  | { kind: 'answer'; citations: Citation[] };
for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
  const decision = await model.next(messages, allowedTools);
  if (decision.type === 'answer') return decision;
  messages.push(await executeAuthorized(decision));
}
throw new ToolLoopExceeded();

검색 실패와 실행 실패를 따로 재현했다

같은 “답변 실패”라도 검색 결과가 비었는지, 모델이 근거를 놓쳤는지, 도구가 거부됐는지에 따라 고칠 위치가 달랐습니다. 각 계층의 trace id를 한 요청으로 연결하고 실패 사유를 공통 상태로 뭉개지 않았습니다. 운영 화면에서도 검색 품질과 도구 성공률을 별도 지표로 유지했습니다.

RAG는 문서를 근거로 답하는 데 강하지만 실시간 상태를 다루기 어렵습니다. Tool은 현재 상태를 조회하거나 작업을 실행할 수 있지만 설명의 근거가 부족할 수 있습니다. 운영용 챗봇에서는 router가 의도에 따라 두 경로를 선택하거나 결합하도록 했습니다.

  • 단계별 latency와 token usage를 분리해 병목과 비용을 확인합니다.
  • 도구 오류를 모델 환각으로 덮지 않고 사용자에게 명확한 실패로 전달합니다.
  • 검색 근거와 도구 실행 이력을 감사 가능한 형태로 보관합니다.
  • 각 turn에 intent와 검색 문서 ID, tool call, 승인, 결과를 연결해 감사할 수 있게 합니다.
  • tool round·token·시간 예산을 두고 초과 상태를 사용자에게 명시합니다.
  • 답변 citation이 실제 사용한 chunk를 가리키는지 자동 검증합니다.

계층을 나눈 뒤 생긴 추적 비용

새 모델 공급자와 새 도구를 핵심 대화 흐름을 바꾸지 않고 추가할 수 있었습니다. 검색이 필요한 질문과 실행이 필요한 질문을 나눠 불필요한 컨텍스트와 도구 호출을 줄이고 권한 검증 지점도 선명해졌습니다.

계층이 늘어난 만큼 요청 trace와 공통 오류 모델을 유지하는 비용도 생겼습니다. 작은 FAQ라면 단일 RAG 흐름이 더 알맞고, 검색·실행·승인을 독립적으로 바꿔야 할 때 이 분리가 효과를 냅니다.

모델 호출 하나에 모으지 않은 이유

챗봇을 확장하면서 가장 경계해야 했던 것은 모든 판단을 하나의 모델 호출에 모으는 일이었습니다. 검색 실패와 권한 거부, 도구 오류가 같은 답변 실패로 보이면 원인도 복구 방법도 찾기 어렵습니다.

질문 해석, retrieval, tool 실행과 응답 생성을 나눈 뒤 각 단계는 작은 계약을 갖게 됐습니다. 새 모델이나 도구를 추가할 때 전체 프롬프트를 다시 설계하지 않고 해당 경계의 입력·출력과 권한만 검증할 수 있었습니다.

댓글

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다