티스토리 뷰

LLM 기반 Agent를 설계하고 구현하다 보면, 모델의 성능이나 Tool 연동보다 먼저 마주치는 문제가 있습니다. Agent가 “해야 할 일”보다 “하지 말아야 할 일”을 구분하지 못하는 상황입니다. 특히 여러 개의 Tool을 연결한 Agent일수록, 이 문제는 더 빠르게 드러납니다.

 

이 글은 LangChain4j 기반 Agent를 운영하면서 겪은 실제 문제를 출발점으로, System Prompt에 constraints 패턴을 도입해 Agent의 행동 범위를 제어한 경험을 정리한 글입니다. 코드 수정 없이 프롬프트 설계만으로 동작을 개선한 과정과, 이를 운영 환경에서 어떻게 활용했는지를 중심으로 설명합니다.

 


 

문제의 발단: Agent는 왜 엉뚱한 일을 했을까

 

운영 중이던 Agent는 빅테크 AI 기업 5곳(OpenAI, Anthropic, Google, Meta, xAI)의 업데이트를 추적하도록 설계되어 있었습니다. 이 범위는 서비스 요구사항으로 명확히 정의되어 있었고, Tool 역시 해당 Provider들의 공식 채널만을 대상으로 구성되어 있었습니다.

 

그런데 사용자로부터 다음과 같은 요청이 들어왔습니다.

 

“LangChain, LlamaIndex, Hugging Face의 최신 릴리스 정보를 수집해주세요.”

 

이 요청은 명백히 지원 범위를 벗어나는 요청이었기 때문에, 미지원 안내 메시지가 반환될 것으로 기대했습니다. 그러나 실제 결과는 전혀 달랐습니다. Anthropic 블로그, Meta 블로그, GitHub 릴리스, RSS 피드 등에서 수십 건의 데이터가 수집되어 반환되었습니다. 요청 대상과 무관한 정보가 대량으로 포함된 응답이었습니다.

 

이 시점에서 확인할 수 있었던 사실은 단순합니다. Agent는 자신이 무엇을 지원하지 않는지 알지 못했다는 점입니다.

 


 

원인 분석: System Prompt에 빠져 있던 한 가지

 

LangChain4j 공식 문서에서는 System Message가 LLM의 행동을 정의하는 가장 중요한 입력임을 설명합니다. LLM은 System Message에 다른 메시지 유형보다 높은 우선순위를 두고 주의를 기울이도록 학습되어 있습니다.

 

문제는 기존 System Prompt에 다음과 같은 정보만 포함되어 있었다는 점입니다.

 

  • Agent의 역할
  • 사용 가능한 Tool
  • 응답 형식 및 일반 규칙

 

반면, 지원 대상과 미지원 대상에 대한 명확한 경계는 존재하지 않았습니다. LLM의 입장에서 보면, 요청이 들어왔을 때 “이건 내가 처리하면 안 되는 요청이다”라고 판단할 근거가 없었습니다. 따라서 가진 도구를 최대한 활용해 무언가를 수행하려는 방향으로 추론이 진행된 것은 자연스러운 결과였습니다.

 

이 문제를 코드로 막으려면, 요청 파싱 후 별도의 검증 로직을 추가하거나 Tool 호출 전 필터링을 해야 합니다. 하지만 이 글에서 다루는 접근은 다릅니다. Agent의 판단 기준 자체를 System Prompt에 명시적으로 정의하는 방식입니다.

 


 

Constraints 패턴: Agent의 책임 범위를 선언하다

 

해결 접근은 비교적 단순합니다. System Prompt에 constraints(제약 조건) 섹션을 추가하고, 다음 두 가지를 명확히 선언합니다.

 

첫째, 이 Agent가 지원하는 범위입니다.

이 사례에서는 OpenAI, Anthropic, Google, Meta, xAI 다섯 Provider만을 명시적으로 지원 대상으로 선언했습니다.

 

둘째, 미지원 요청을 어떻게 처리해야 하는지에 대한 규칙입니다.

지원되지 않는 대상이 요청에 포함된 경우 Tool을 호출하지 않고, 사전에 정의된 안내 메시지를 반환하도록 규칙을 명시했습니다. 일부만 지원되는 경우에는 지원 대상만 처리하고, 미지원 대상은 별도로 안내하도록 했습니다.

 

중요한 점은, 이 모든 변경이 Tool 코드나 LangChain4j 설정을 수정하지 않고 이루어졌다는 것입니다. Agent의 행동은 여전히 자율적이지만, 그 자율성이 작동할 수 있는 경계가 명확해졌습니다.

 


 

구현 관점: Prompt를 설정으로 다루기

 

이 패턴을 적용하면서 함께 정리한 부분이 있습니다. System Prompt를 코드 상수가 아니라 설정 값으로 관리하는 방식입니다.

 

Spring Boot 환경에서는 @ConfigurationProperties를 활용해 Prompt 구성을 외부 설정으로 분리할 수 있습니다. constraints 문구 역시 설정 파일에서 관리하도록 분리했습니다. 이를 통해 다음과 같은 운영상의 장점을 얻을 수 있었습니다.

 

  • 새로운 미지원 대상이 발견되었을 때 코드 배포 없이 대응 가능
  • 안내 메시지 문구 조정 시 빠른 튜닝 가능
  • 환경별로 다른 constraints 적용 가능

 

구현 자체는 단순합니다. Prompt를 구성하는 클래스에서 role, constraints, rules 등을 조합해 최종 System Prompt를 생성하고, 이를 AI Services를 통해 LLM에 전달합니다. 코드 자체보다 중요한 것은 constraints 섹션이 Prompt 상단부, 역할 정의 바로 다음에 위치하도록 배치했다는 점입니다. LLM이 사용자 요청을 처리하기 전에 지원 범위를 먼저 인식하도록 하기 위함입니다.

 


 

LangChain4j에서의 System Message 활용 방식

 

LangChain4j에서는 System Message를 정의하는 방식이 여러 가지입니다. 정적인 경우에는 @SystemMessage 어노테이션을 사용할 수 있고, 보다 유연한 구성이 필요한 경우에는 systemMessageProvider를 통해 런타임에 Prompt를 생성할 수 있습니다.

 

이 사례에서는 후자의 방식을 선택했습니다. 사용자 요청과 System Prompt를 하나의 흐름으로 구성해 전달함으로써, 외부 설정과의 연동과 Prompt 구조 제어를 동시에 만족시킬 수 있었기 때문입니다. 이 방식은 constraints 패턴처럼 Prompt 구성이 중요한 경우 특히 유용했습니다.

 


 

검증 결과: Tool 호출은 사라졌고, 의도는 유지됐다

 

constraints 패턴을 적용한 후 동일한 요청을 다시 테스트했습니다.

 

“LangChain, LlamaIndex, Hugging Face의 최신 릴리스 정보를 수집해주세요.”

 

이번에는 Tool 호출 없이 미지원 안내 메시지만 반환되었습니다. 실제 실행 로그에서도 toolCallCount가 0으로 확인되었습니다. 불필요한 API 호출과 처리 비용이 제거된 것입니다.

 

부분 지원 요청에 대해서도 기대한 대로 동작했습니다. 예를 들어 OpenAI와 Hugging Face를 함께 요청한 경우, OpenAI에 대해서만 데이터를 수집하고 Hugging Face는 미지원 대상으로 안내했습니다. 반대로, 기존에 정상 동작하던 지원 대상 요청이나 모호한 요청(“최근 AI 업데이트 정보를 수집해주세요”)은 이전과 동일하게 처리되었습니다.

 


 

정리하며: Prompt는 계약에 가깝다

 

이번 작업을 통해 다시 한 번 느낀 점은, System Prompt는 단순한 설명 문구가 아니라 Agent와 시스템 간의 계약에 가깝다는 사실입니다. 무엇을 할 수 있는지뿐 아니라, 무엇을 하지 말아야 하는지까지 명시했을 때 Agent의 행동은 훨씬 안정적으로 변했습니다.

 

물론 constraints 패턴이 모든 상황에 적합한 해법은 아닙니다. constraints 내용이 지나치게 길어지면 토큰 비용이 증가하고, LLM이 핵심 규칙을 놓칠 가능성도 있습니다. 또한 최종 판단은 여전히 LLM에 의존하기 때문에, 중요한 비즈니스 로직까지 이 방식에 맡기는 것은 위험할 수 있습니다.

 

그럼에도 불구하고, 빠른 프로토타이핑이나 운영 중 동작 튜닝이 필요한 상황에서는 매우 실용적인 선택지라고 생각합니다. constraints 패턴은 Agent에게 “무엇을 해야 하는지”뿐 아니라 “무엇을 하지 말아야 하는지”를 명확히 알려주는 가장 단순한 방법 중 하나였습니다.

 

비슷한 문제를 겪고 있다면, 코드보다 먼저 System Prompt를 다시 한 번 점검해 보는 것도 충분히 의미 있는 시도일 것입니다.