1. 배경: 대상이 640배로 늘어날 때
검증 단계에서 이 파이프라인이 다룬 회사는 20곳이었습니다. 상용 대상은 12,880곳입니다. 사기업 12,000곳과 공기업 880곳입니다.
이 정도 배율이면 "돌려보고 문제가 생기면 고친다"가 성립하지 않습니다. 한 번 돌리는 데 며칠이 걸리고, 그 사이 사내 LLM 게이트웨이의 토큰을 소진하며, 외부 API의 일일 한도를 씁니다. 다 쓰고 나서야 부족한 것을 알면 그날 하루가 날아갑니다.
사업부서에서 먼저 물어본 것도 정확히 이것이었습니다.
- 전체를 한 바퀴 도는 데 며칠 걸리는가
- 토큰을 얼마나 쓰는가
- 하루에 몇 곳씩 끊어야 하는가
세 질문 모두 아직 해보지 않은 일에 대한 답을 요구합니다. 그리고 사내망에 세팅하는 중이라, 답을 내는 도구 자체가 외부 API를 부르면 안 되는 상황이었습니다.
2. 문제 정의
2.1 추정의 근거를 어디서 가져올 것인가
12,880곳을 돌려본 적이 없으니 실측값이 없습니다. 그렇다고 코드에 적힌 설정값만으로 계산하면 빗나갑니다. 뒤에서 보겠지만 이 차이가 2배 넘게 벌어졌습니다.
2.2 병목이 어디인지 모른다
LLM 토큰, DART 호출, 나라장터 수집, 뉴스 검색이 각각 다른 한도를 갖습니다. 가장 먼저 바닥나는 것 하나가 전체 소요일수를 결정하는데, 넷 중 어느 것인지는 각각을 재보기 전에는 알 수 없습니다.
2.3 산정 도구가 사내망에서 돌아야 한다
고객사 세팅은 사내망에서 진행됩니다. API 키가 없고, 방화벽이 닫혀 있고, 서버가 꺼져 있을 수도 있는 환경입니다. 용량 산정이 외부 호출에 의존하면 정작 필요한 자리에서 못 돌립니다.
3. 기술적 난점
첫째, LLM 출력 길이는 상한과 실제가 다릅니다. 에이전트마다 token_steps 로 출력 상한을 두었지만, 실제 생성되는 초안은 그보다 짧습니다. 어느 쪽으로 계산하느냐에 따라 답이 2배 이상 벌어집니다.
둘째, 한국어 토큰은 자릿수로 세야 합니다. 영어 기준 환산비를 쓰면 어긋납니다. 사내 게이트웨이 기준으로 1토큰 ≈ 1.5자를 잡았습니다.
셋째, 근거 블록의 크기가 회사마다 다릅니다. 사업보고서가 있는 사기업과 조달 이력만 있는 공기업은 입력 크기가 다릅니다. 평균 하나로 뭉뚱그리면 어느 쪽도 맞지 않습니다.
4. 설계 결정 및 근거
4.1 상수를 코드가 아니라 적재된 데이터에서 뽑습니다
핵심 결정입니다. 산정 스크립트가 PAGE_AGENTS 의 지시문·스키마 길이를 코드에서 직접 읽고, 출력 길이는 이미 생성해 둔 초안에서 잽니다.
python# 근거 크기: 저장된 초안 payload 길이의 중앙값으로 근사한다.
lens = [len(str(r[0] or "")) for r in rows(
"company.db",
"SELECT payload_json FROM account_drafts "
"WHERE page_key IN ('sector','valueChain','customerStatus') "
"ORDER BY draft_date DESC LIMIT 60")]
lens = [n for n in lens if n > 200]
draft_chars = int(sorted(lens)[len(lens) // 2]) if lens else 0평균이 아니라 중앙값을 씁니다. 초안 중에는 생성이 덜 된 짧은 것과 유난히 긴 것이 섞이는데, 평균은 그 꼬리에 끌려갑니다. 200자 미만은 실패한 생성으로 보고 제외했습니다.
이 방식의 장점은 파이프라인을 고치면 추정치가 따라온다는 것입니다. 프롬프트를 손보면 다음 산정에 그대로 반영됩니다. 코드에 상수로 박아두면 고칠 때마다 사람이 기억해서 같이 고쳐야 합니다.
4.2 상한과 실측을 함께 냅니다
둘 중 하나만 내지 않고 둘 다 냅니다. 12,537곳 기준입니다.
| 기준 | 입력 | 출력 | 합계 |
|---|---|---|---|
| 실측 중앙값 | 105,862,428 | 98,540,820 | 204,403,248 |
| 설정 상한 | 105,862,428 | 225,666,000 | 331,528,428 |
회사당 출력이 상한 6,000토큰인데 실측 중앙값은 2,620토큰입니다. 상한만 쓰면 전체가 1.6배 과대추정됩니다.
두 값을 다 내는 이유는 쓰임이 다르기 때문입니다. 예산을 확보할 때는 상한이 맞습니다. 모자라면 배치가 중간에 멈춥니다. 실제 소요를 예상할 때는 실측이 맞습니다. 상한으로 계획하면 필요 없는 일수를 잡게 됩니다. 하나로 합치면 둘 중 한쪽 용도에서 틀립니다.
4.3 근거를 에이전트별로 나눠 넣습니다
토큰을 줄이는 작업은 산정보다 먼저 했습니다. 세 에이전트가 같은 근거를 통째로 받고 있었습니다. 사업보고서 9,094자가 세 번 들어갔습니다.
측정해 보니 근거 17,754자 × 3 = 입력 53,262자였고, 그중 사업보고서 하나가 51%를 차지했습니다.
에이전트마다 필요한 블록만 넘기도록 바꿨습니다.
| 블록 | 받는 에이전트 |
|---|---|
| 산업 정책·규제 · 사회·환경 · 경쟁 구도 | sector |
| 실적·재무 · 현안·리스크 · 투자·전략 · DART 공시 | customerStatus |
| 사업보고서 발췌 | valueChain, sector |
| 조달 실적 · 계약 내역 · 입찰공고 | valueChain, customerStatus |
| 회사 | 이전 | 이후 | 감소 |
|---|---|---|---|
| 삼성전자 | 53,262자 | 29,002자 | 46% |
| 한국마사회 | 24,144자 | 15,171자 | 37% |
한 가지 규칙을 함께 두었습니다. 배분표에 없는 블록은 전원에게 넣습니다. 새 블록을 추가했을 때 배분표에 적는 것을 잊으면 조용히 빠지는데, 그러면 초안 품질이 떨어지고 원인은 안 보입니다. 중복되는 편이 낫습니다.
공기업의 감소폭이 작은 것은 사업보고서가 없고 그 자리를 조달 이력이 채우는데, 그 블록은 두 에이전트가 나눠 쓰기 때문입니다.
4.4 뉴스는 받은 것의 21%만 씁니다
검색으로 받은 기사를 전부 근거에 넣으면 입력이 감당이 안 됩니다. 4단계로 걸렀습니다. 삼성전자 기준입니다.
| 단계 | 건수 |
|---|---|
| 받은 기사 | 210 |
| 중복 제거 | -47 |
| 무관 제외 | -52 |
| 최종 | 44 |
검색어는 LLM이 회사 자료를 읽고 만듭니다. 여기에 규칙 두 개를 넣었습니다. 검색어에 회사명을 넣지 않습니다 — 호출할 때 자동으로 붙으므로 넣으면 중복됩니다. 그리고 자료에 나온 고유명사를 우선합니다. 제품명·기술명처럼 그 회사에만 해당하는 단어여야 합니다. '성장'·'혁신' 같은 일반어는 금지했습니다. 일반어로 검색하면 그 회사와 무관한 기사가 대량으로 들어오고, 무관 제외 단계에서 걸러야 할 양이 늘어납니다.
LLM이 실패하면 고정 조합 6개로 폴백합니다. 회사명만으로도 최소한은 돕니다.
4.5 산정 도구는 외부를 부르지 않습니다
data/*.db 와 상태 파일만 읽습니다. API 키도 방화벽도 필요 없고, 서버가 떠 있는 중에 돌려도 되도록 읽기 전용(mode=ro)으로 엽니다.
출력은 마크다운·엑셀·JSON 셋으로 냅니다. 엑셀을 넣은 것은 취향이 아니라 사내망 사정입니다. 마크다운이 에디터에서 안 열리거나 표가 깨지는 경우가 있고, 사업부서에 전달할 때는 시트로 나뉜 편이 읽힙니다.
5. 실험 결과
5.1 병목은 LLM이 아니었습니다
넷을 각각 재보니 순서가 예상과 달랐습니다.
| 자원 | 소요 | 제약 여부 |
|---|---|---|
| DART 호출 | 회사당 6회 · 하루 예산 18,000회 → 3,000곳/일 | 병목 |
| LLM 초안 | 회사당 4~5회 (초안 3종 + 검색어 1회 + 대목 선별 0~1회) | 배치 크기로 조절 |
| 네이버 뉴스 | 회사당 12회 · 월 775,000건의 19% | 제약 아님 |
| 나라장터 | 적재분 DB 조회 (초안 시 호출 없음) | 해당 없음 |
뉴스가 12회로 가장 많이 부르는데 한도의 19%밖에 안 씁니다. 호출 횟수가 많은 것과 제약이 되는 것은 별개입니다. 한도 대비 비율로 봐야 합니다.
배치 크기를 3,000곳/일로 잡으면 전체 한 바퀴가 5일입니다.
5.2 회사당 LLM 호출을 3회로 세고 있었습니다
산정을 정정하면서 나온 오류입니다. 초안 3종만 세고 검색어 생성 1회를 빠뜨렸습니다. 실제는 4회입니다. 여기에 사업보고서 대목 선별이 회사당 0~1회 더 붙습니다. 사업보고서가 없거나 대목을 못 찾으면 고정 목록으로 떨어지므로 상한 산정에서는 5회로 잡는 편이 안전합니다.
25% 차이입니다. 12,880곳이면 38,640회와 51,520회의 차이가 됩니다. 파이프라인을 직접 만든 사람이 세도 이렇게 빠집니다. 호출 지점이 코드 여러 곳에 흩어져 있으면 머릿속으로 세는 방식은 신뢰할 수 없습니다.
5.3 배치가 고객사를 돌고 있지 않았습니다
산정 중에 드러난 가장 큰 문제입니다.
DART 백필과 공시 동기화로 363곳을 채웠는데, 그 363곳이 고객사가 아니었습니다. KOSPI200+KOSDAQ150 개발용 표본이었습니다. 실제 고객사는 29곳입니다.
원인은 대상을 고르는 조건에 있었습니다.
python# dart_backfill._iter_companies · company_db.list_corp_codes 둘 다
"WHERE corp_code IS NOT NULL AND corp_code != ''"corp_code 가 있는지만 봅니다. 고객인지는 보지 않습니다. 그래서 개발하며 넣어둔 표본 353곳이 대상에 들어오고, 실제 고객 29곳은 그 안에 묻혔습니다. DART 호출 약 2,200회를 고객이 아닌 회사에 썼습니다.
더 중요한 것은 고객군의 성격이 전제와 달랐다는 점입니다.
| 구분 | 수 | 예 |
|---|---|---|
| 공기업 | 16 | 대검찰청 · 외교부 · 대법원 · 식약처 · 환경부 |
| 사기업 | 13 | SBS · JTBC · 한겨레 · 전자신문 · GS홈쇼핑 |
정부기관과 언론사입니다. 29곳 기준으로 다시 재면 공시 0건이 16곳입니다. 정부기관은 DART에 공시를 내지 않기 때문입니다. 반면 나라장터 공고는 9곳에서 1,188건이 붙습니다. 식약처 252건, 외교부 154건, 대검찰청 118건입니다.
이 고객군은 DART가 아니라 나라장터가 주력입니다. 12,000곳을 상정해 DART 호출 예산제와 커서 백필을 넣었는데, 정작 이 고객군에서는 DART가 병목이 아닙니다.
5.4 왜 지금까지 안 드러났는가
화면을 열면 데이터가 있는 것처럼 보였습니다. 개발용 기본 대상 4개사 중 한국마사회와 GS리테일이 우연히 고객 목록에도 있었기 때문입니다.
근본 원인은 세는 단위였습니다. 적재량을 소스 단위로 세고 있었습니다. 공시 58,467건, 공고 81,664건. 이 숫자는 계속 늘어나므로 정상으로 보입니다.
고객사 단위로 "이 회사에 무엇이 있는가"를 세자 바로 드러났습니다. 등기임원 캐시 없음 22곳, 공시 0건 16곳. 그래서 리포트에 회사 — 개발용 표본(고객사 아님) 1,256 행을 따로 두었습니다. 표본이 섞여 있다는 사실 자체가 리포트에 보여야 합니다.
6. 결론 및 시사점
첫째, 추정치는 파이프라인에서 자동으로 나와야 합니다. 산정을 문서에 손으로 적으면 코드를 고치는 순간 낡습니다. 지시문 길이는 코드에서 읽고 출력 길이는 적재된 초안에서 재도록 만들면, 프롬프트를 손볼 때마다 추정이 따라옵니다. 사람이 기억해서 갱신하는 절차는 결국 빠집니다.
둘째, 상한과 실측은 쓰임이 다르므로 둘 다 내야 합니다. 예산 확보는 상한으로, 일정 계획은 실측으로 합니다. 하나로 합치면 둘 중 하나는 틀립니다. 이 경우 1.6배 차이였습니다.
셋째, 제약은 절대량이 아니라 한도 대비 비율로 판단해야 합니다. 뉴스는 회사당 12회로 가장 많이 부르지만 한도의 19%라 제약이 아니고, DART는 6회지만 병목입니다. 호출 횟수가 많은 쪽을 먼저 줄이려 했다면 병목은 그대로 남았을 것입니다.
넷째, 무엇을 세는가가 무엇이 보이는가를 결정합니다. 이번에 가장 크게 걸린 부분입니다. 소스 단위로 세는 동안 배치는 한 달 내내 엉뚱한 대상을 돌았고 숫자는 계속 정상으로 보였습니다. 집계 단위가 실제 목적(고객사에 초안을 준다)과 어긋나 있으면, 지표가 늘어나는 것과 목적을 달성하는 것이 무관해집니다.
다섯째, 규모를 키우는 작업 전에 대상부터 확인해야 합니다. DART 호출 예산제와 커서 백필은 12,000곳을 상정하고 만든 것인데, 실제 고객군에서는 DART 자체가 주력 소스가 아니었습니다. 기능이 틀린 것은 아니지만 지금 필요한 것이 아니었습니다. 확장을 설계하기 전에 확장 대상의 성격을 먼저 재는 편이 낫습니다.
