에이전트설계

에이전트로 만들지 않기로 한 것들

2026. 08. 30

선행 기획서는 CrewAI 기반 멀티에이전트 구성을 제시했습니다. 구현하면서 역할마다 'LLM이어야 하는가'를 다시 물었고, 사실 검증과 중복 판정은 LLM 대신 문자열 대조와 임계값으로 내렸습니다. 무엇을 에이전트로 둘지 고르는 기준을 정리했습니다.

프로젝트 배경

소속
KT · B2B 영업 지원 (Client: 사내 영업조직)
프로젝트
Account Plan 초안 자동 생성 파이프라인
기간
2026.07 ~ 진행 중
담당
데이터 파이프라인·에이전트 설계 단독

영업담당자가 고객사를 방문하기 전에 읽을 Account Plan 초안을 자동으로 만드는 시스템입니다. 나라장터 조달 공고, OpenDART 공시, 뉴스를 회사 단위로 통합해 세 종류의 초안을 생성합니다. 사내 AI 게이트웨이(codi)로만 LLM을 호출할 수 있고, 모델은 추론(reasoning) 계열입니다.

에이전트설계LLM아키텍처프롬프트설계비용

1. 배경: 기획서가 제안한 에이전트 구성

이 시스템에는 선행 기획서가 있었습니다. 「외부고객 정보 수집 Agent」라는 문서이고, CrewAI 기반의 멀티에이전트 구성을 제시하고 있었습니다. 에이전트가 여덟, 태스크가 열둘이고 Process.sequential 로 순차 실행되는 구조입니다.

기획서 에이전트역할
company_searcher기업 검색·매칭
quant_data_collector정량 수집 (재무 3개년)
qual_data_collector정성 수집 (사업보고서 원문)
hierarchy_builder리포트 구조 설계
internal_plan_analyst검색 키워드 전략
news_collector뉴스 수집·중복 제거
skeptical_critic팩트 교차검증 (공시 vs 뉴스)
executive_report_writerJSON 리포트 작성

읽으면 설득력이 있습니다. 각 역할이 하는 일이 분명하고, 서로 견제하는 구조도 갖췄습니다. 특히 다른 에이전트의 결과를 검증하는 skeptical_critic 은 요즘 멀티에이전트 설계에서 흔히 권장되는 패턴입니다.

그대로 구현할 수도 있었습니다. 하지만 각 역할을 실제로 만들어 보면서 반복해서 같은 질문에 부딪혔습니다. 이 역할은 LLM이어야 하는가, 아니면 코드로 되는가.

지금 구성은 다섯 모듈입니다. 역할이 사라진 것이 아니라, 그중 둘이 LLM 호출 없이 도는 코드가 됐습니다.

현재 구성LLM초안 1회당 호출
draft_agents (sector·valueChain·customerStatus)사용3회
news_keyword_agent (검색어 조합 생성)사용1회
biz_report_agent (사업보고서 대목 선별)사용0~1회
news_search_agent (뉴스 수집·중복 제거)미사용네이버 12회
draft_verify (사실 무결성 검증)미사용0회 (문자열 대조)

biz_report_agent 가 0회가 되는 경우가 있습니다. 사업보고서에서 대목을 못 찾거나 LLM 사용이 불가능하면 고정 목록으로 떨어집니다. 초안 하나를 만드는 데 드는 LLM 호출은 넷에서 다섯이고, 나머지는 규칙으로 돕니다.

이 글은 그 경계를 어디에 그었는지에 대한 기록입니다.

2. 문제 정의: 에이전트 하나의 실제 비용

LLM을 부르는 자리를 하나 늘리는 비용은 API 요금만이 아닙니다. 이 프로젝트에서 확인한 것은 넷입니다.

첫째, 결과가 흔들립니다. 같은 입력에 같은 출력이 나오지 않습니다. 검증처럼 판정이 일정해야 하는 자리에서는 이것이 치명적입니다.

둘째, 실패 경로가 늘어납니다. 호출이 실패하면 폴백을 준비해야 합니다. LLM 호출을 하나 늘리면 폴백도 하나 늘어납니다.

셋째, 대상 규모에 곱해집니다. 회사당 1회짜리 호출도 12,880곳이면 12,880회입니다. 검증 단계에서 20곳으로 돌 때는 보이지 않던 비용입니다.

넷째, 지연이 누적됩니다. 초안 생성이 몇 분씩 걸리면 사용자는 기다리지 않습니다.

그래서 기준을 하나 세웠습니다. 판단에 해석이 필요한 자리에만 LLM을 둡니다. 규칙으로 쓸 수 있으면 규칙으로 씁니다.

3. 남긴 것: 해석이 필요한 자리

3.1 검색어 전략 — 회사마다 답이 다릅니다

기존에는 어느 회사든 고정 키워드 7개(규제·관세·보조금·수출통제·정책·법안·지원금)로 검색했습니다. 그래서 삼성전자의 'HBM', 한국마사회의 '장외발매소'처럼 그 회사에만 해당하는 기사를 못 찾았습니다.

이건 규칙으로 안 됩니다. 회사마다 중요한 고유명사가 다르고, 그 목록을 미리 만들어 둘 수 없습니다. 회사 자료(사업보고서 발췌·조달 이력)를 읽고 그 회사에 맞는 검색어를 뽑는 일은 해석입니다. LLM으로 남겼습니다.

대신 규칙 두 개를 프롬프트에 넣었습니다.

  • 검색어에 회사명을 넣지 않습니다. 호출할 때 자동으로 붙으므로 넣으면 중복됩니다.
  • 자료에 나온 고유명사를 우선합니다. '성장'·'혁신' 같은 일반어는 금지했습니다.

일반어를 금지한 것은 품질 문제이자 비용 문제입니다. 일반어로 검색하면 무관한 기사가 대량으로 들어오고, 뒤의 필터가 걸러야 할 양이 늘어납니다. 앞 단계의 정밀도가 뒤 단계의 부하를 결정합니다.

3.2 사업보고서 대목 선택 — 문서 구조가 회사마다 다릅니다

기존 구현은 뽑을 섹션 6개가 코드에 박혀 있었습니다. 사업의 개요, 주요 제품 및 서비스, 매출 및 수주상황 같은 제목입니다.

문제는 그 제목을 안 쓰는 회사는 통째로 빈다는 것입니다. 대상이 KOSPI200에서 공공기관까지 넓어지자 이 방식이 버티지 못했습니다.

그래서 순서를 뒤집었습니다.

① 보고서에서 대목을 전부 추출 (제목 + 본문)
② 제목과 첫 줄만 LLM에 보여주고 쓸 것을 고르게 함
③ 고른 대목의 본문만 이어 붙여 근거로 반환

본문을 통째로 넣지 않는 이유는 보고서가 3~4MB라 프롬프트에 안 들어가기 때문입니다. 목차만 보고 고르게 하고 본문은 코드가 붙입니다. LLM에게는 판단만 시키고 운반은 코드가 합니다.

3.3 초안 작성 3종 — 병렬로 두되 근거는 나눕니다

산업/섹터환경, Value Chain, 고객사 현황 세 종류를 각각 다른 역할·지침·JSON 스키마를 가진 에이전트로 두고 ThreadPoolExecutor로 동시에 호출합니다.

셋을 하나로 합치지 않은 이유는 품질입니다. 한 프롬프트에 여러 관점을 넣으면 모델이 앞부분에 집중하고 뒷부분은 대충 훑습니다. 관점을 나누면 각각의 결과가 충실해집니다.

다만 셋이 같은 근거를 통째로 받고 있었습니다. 사업보고서 9,094자가 세 번 들어갔습니다. 에이전트별로 필요한 블록만 넘기도록 바꿔 입력을 46% 줄였습니다.

여기에 규칙 하나를 함께 두었습니다. 배분표에 없는 블록은 전원에게 넣습니다. 새 블록을 추가하고 배분표에 적는 것을 잊으면 조용히 빠지는데, 그러면 초안 품질이 떨어지고 원인은 안 보입니다. 중복은 비용이지만 누락은 버그입니다.

4. 내린 것: 규칙으로 되는 자리

4.1 사실 검증을 에이전트로 만들지 않았습니다

가장 중요한 판단이었습니다.

기획서의 skeptical_critic 은 다른 에이전트의 결과를 LLM으로 검증하는 역할입니다. 이 프로젝트에서 검증해야 할 것은 하나였습니다. 초안에 나온 숫자가 근거에 실제로 있는가. 초안이 "2025년 영업이익 1조 3,049억원"이라고 썼는데 근거 어디에도 그 숫자가 없으면 지어낸 것입니다.

이 판정에는 해석이 필요하지 않습니다. 숫자가 근거 텍스트에 있는지 없는지는 문자열 대조로 판정됩니다. 그래서 LLM을 한 번 더 태우지 않고 정규식으로 처리했습니다.

python# "1조 3,049억원", "4.3억원", "1,532억원" 같은 표기에서 숫자만 뽑는다
_NUMBER_PATTERN = re.compile(r"\d[\d,]*\.?\d*")

def _normalize_number(text: str) -> str:
    """'1,532' → '1532'. 비교는 쉼표를 뺀 형태로 한다."""
    return text.replace(",", "").rstrip(".")

LLM으로 했을 때보다 나은 점이 셋입니다. 빠르고, 회사당 호출이 늘지 않고, 결과가 흔들리지 않습니다. 검증기가 같은 초안을 두 번 보고 다르게 답하면 그건 검증이 아닙니다.

대신 한계를 분명히 하고 그에 맞게 동작을 정했습니다. 근거에서 계산해 낸 값(증감률·합계)이나 표기가 다른 값(1,532억 vs 153,200백만)은 못 걸러냅니다. 그래서 틀렸다고 단정하지 않고 '확인 필요'로만 표시합니다. 초안을 지우거나 고치지 않습니다.

오탐을 줄이는 규칙도 넣었습니다. 연도(1990~2100)와 100 미만의 작은 수는 대조 대상에서 뺐습니다. 근거에 안 적혀 있어도 상식으로 쓸 수 있는 값이라 오탐만 늘립니다.

정확도가 낮은 검증기를 조언자로 두는 것이 이 설계의 요점입니다. 판정을 강제하지 않으니 틀려도 피해가 없고, 맞으면 사람이 확인할 지점을 줄여 줍니다. 반대로 LLM 검증기를 두고 그 판정으로 초안을 고치게 했다면, 검증기가 틀릴 때 멀쩡한 초안이 망가집니다.

4.2 중복 판정은 임계값으로 내렸습니다

기획서에는 뉴스 중복 제거 기준이 '유사도 95% 이상' 이라고 적혀 있었습니다. 그대로 넣어 보니 하나도 안 걸렸습니다. 매체마다 제목과 도입부를 다시 쓰기 때문입니다.

실제로 재봤습니다. 한국마사회 창원지사 성금 기사 3건과 무관한 광복절 기사를 비교했습니다.

비교유사도
같은 사건끼리0.204 ~ 0.298
다른 사건과0.037 ~ 0.040

두 무리 사이가 크게 벌어져 있습니다. 0.15로 자르면 같은 사건만 걸립니다.

여기서 얻은 것은 임계값 자체가 아니라 분포를 보고 정했다는 사실입니다. 0.95는 직관적으로 그럴듯한 숫자지만 실제 데이터에서는 작동하지 않습니다. 두 무리가 벌어진 구간의 가운데를 고르면, 왜 그 값인지 설명할 수 있고 나중에 데이터가 바뀌면 다시 재서 옮길 수 있습니다.

이런 판정을 LLM에 맡겼다면 회사당 수십 번의 추가 호출이 들고, 같은 기사 쌍을 매번 다르게 판정했을 것입니다.

4.3 조회 경로에서는 LLM을 아예 뺐습니다

에이전트 설계에서 놓치기 쉬운 지점입니다. 화면을 여는 것만으로 과금이 일어나면 안 됩니다.

대시보드는 60초마다 자동 갱신됩니다. 오버뷰 요약을 그때마다 LLM으로 만들면 새로고침 1회가 과금 1회입니다. 그래서 경로를 둘로 갈랐습니다.

경로LLM 호출동작
브라우저 조회 · 60초 자동 갱신없음저장된 요약을 읽는다
AI 초안 생성 버튼사용요약을 새로 만들어 저장한다

_build_overview_narrative(allow_llm=False) 가 기본값이고 allow_llm=True 는 버튼만 씁니다. 이 플래그는 라우트 시그니처에 없습니다. URL 쿼리로 ?_allow_llm=true 같은 방식을 주입해 과금을 유발할 수 없게 하기 위해서입니다.

돈이 나가는 경로는 기본값이 꺼짐이어야 하고, 밖에서 켤 수 없어야 합니다.

번역 캐시도 같은 원칙입니다. 원문이 같으면 번역 결과도 같으므로 DB에 영구 저장하고 재사용합니다. 서버를 재시작해도 남습니다.

5. 실험 결과: 추론 모델이 만든 예외

설계와 별개로, 모델 특성 때문에 구조를 고친 사례가 하나 있습니다.

사내 게이트웨이의 모델은 추론(reasoning) 계열입니다. 이 모델은 max_tokens 안에서 생각까지 합니다. 프롬프트가 길면 추론이 한도를 다 먹고 content 가 빈 채로 돌아옵니다.

문제는 이것이 예외가 아니라 빈 문자열이라는 점입니다. 호출부는 조용히 폴백으로 떨어집니다. 오버뷰 요약이 며칠 동안 폴백 문구로 나가고 있었는데, 에러가 없으니 아무도 몰랐습니다.

실측입니다.

max_tokens추론에 쓴 양실제 본문
900900 (전부)0자
2,000958348자

_call_chat_completions() 가 이 경우를 감지하면 한도를 3배(최대 8,000)로 올려 한 번 더 부르도록 고쳤습니다.

여기서 배운 것은 빈 응답을 성공으로 처리하면 안 된다는 것입니다. 에이전트를 여럿 두면 각각의 실패가 폴백에 흡수되어 전체적으로는 '동작하는 것처럼' 보입니다. 폴백은 실패를 감추는 장치이기도 합니다.

6. 결론 및 시사점

첫째, 에이전트로 만들 것과 코드로 만들 것을 가르는 기준은 '해석이 필요한가'입니다. 회사마다 답이 다른 일(검색어 선정, 문서 대목 고르기)은 LLM이 맞습니다. 숫자가 있는지 없는지, 유사도가 임계값을 넘는지는 코드가 맞습니다. 기획서가 Agent라고 이름 붙였다는 것은 그것이 LLM이어야 한다는 근거가 되지 않습니다.

둘째, 검증기는 판정을 강제하지 않을 때 더 안전합니다. 문자열 대조 검증기는 정확하지 않지만 '확인 필요'만 표시하고 초안을 고치지 않으므로, 틀려도 피해가 없습니다. 반대로 정확도가 높은 LLM 검증기에 수정 권한을 주면 틀리는 날 멀쩡한 결과가 망가집니다. 판정의 정확도보다 틀렸을 때의 영향 범위를 먼저 정하는 편이 낫습니다.

셋째, 기획서의 숫자는 실측으로 다시 정해야 합니다. 유사도 95%는 그럴듯하지만 실제 데이터에서 0건을 잡았습니다. 같은 사건끼리 0.2~0.3, 다른 사건과 0.04로 분포가 갈리는 것을 보고서야 0.15라는 값이 나왔습니다. 문서에 적힌 임계값은 가설이지 설정값이 아닙니다.

넷째, 비용이 발생하는 경로는 기본값이 꺼짐이어야 합니다. 조회 경로에서 LLM을 빼고, 그 플래그를 라우트 시그니처에서 제외해 외부에서 켤 수 없게 했습니다. 에이전트 구조를 설계할 때 "어디서 호출되는가"만큼 "실수로 호출될 수 있는가"를 봐야 합니다.

다섯째, 폴백은 실패를 감춥니다. 추론 모델의 빈 응답이 예외가 아니라 빈 문자열이라 폴백으로 조용히 넘어갔고, 며칠간 아무도 몰랐습니다. 에이전트를 여럿 두면 개별 실패가 전체 동작에 묻힙니다. 폴백을 넣을 때는 폴백이 발동했다는 사실 자체를 남기는지 함께 확인해야 합니다.