
건축사 1인의 '여름방학 자유연구'가 45.8만 회 AI를 구동하는 공공 공사 SaaS가 되다
요약
건축사가 개인 프로젝트로 시작하여 45.8만 회의 AI 구동을 달성한 공공 공사 SaaS 사례를 소개합니다. 로컬 LLM과 OCR을 활용해 파편화된 공공 데이터를 건설사가 즉시 활용 가능한 데이터로 자동 변환합니다.
핵심 포인트
- 로컬 LLM과 OCR을 활용한 공공 데이터 자동 추출 및 분석
- 외부 API 비용 없이 NVIDIA DGX Spark 기반의 대규모 AI 처리
- 파편화된 PDF, ZIP, HTML 데이터를 건설 맞춤형 정보로 변환
- Web, iOS, Android를 아우르는 실서비스 전개 및 운영
처음에는 여름방학 자유연구(自由研究)를 할 생각이었습니다.
본업은 건축사입니다.
AI 기업도, 소프트웨어 회사도 아닙니다.
그것이 현재, 2대의 NVIDIA DGX Spark가 국가나 지자체의 공공 공사를 매일 순회하는 실서비스가 되었습니다.
공고 PDF, 설계서 ZIP, 낙찰 결과, 유자격자 명부를 수집하여, OCR과 로컬 LLM (Local LLM)으로 읽고, "이 회사가 참여할 수 있는가", "누가 얼마에 낙찰했는가", "어느 전문 공사 회사에 영업해야 하는가"까지 정리합니다.
2026년 7월 31일 시점에서, 로컬 AI 실행 대장은 458,900회가 되었습니다.
최근에는 하루 7,000~12,000회. 외부 생성 AI API에 지불하는 종량제 요금은 0원입니다.
그리고 이것은 실험용 데모가 아닙니다.
- Web 서비스를 공개 완료
- iPhone 앱을 App Store에 공개 완료
- Android 앱을 Google Play에 공개 완료
- 10개 도도부현(都道府県)에서 본방 운용 중
| 지표 | 실측치 | 시점 |
|---|---|---|
| 로컬 AI 누적 실행 | 458,900회 | 2026-07-31 |
| 최근 일일 AI 처리 | 7,000~12,000회 | 최근 7일 |
| 입찰 안건 | 256,050건 | 2026-07-31 |
| 응찰 기록 | 1,269,348건 | 2026-07-31 |
| 건설 회사 | 429,206사 | 2026-07-31 |
| PostgreSQL | 34GB・336 테이블・704 인덱스 | 2026-07-31 |
| Web 요청 | 413,759건/일 | 24시간 실측 |
| AI 에이전트 병렬 실행 | 39체・약 6시간 | 후쿠오카 시정촌 전개 |
| 생성한 시정촌 파서 (Parser) | 21건 | 위와 동일 |
| 본방 공개 | 10개 현 + Web + iOS + Android | 2026-08-01 |
AI 실행 대장의 가장 오래된 기록은 2026년 5월 25일입니다. 7월 31일까지의 68일간으로 나누면, 단순 평균으로도 다음과 같은 양이 됩니다.
458,900회 ÷ 68일 ≒ 6,749회/일
이 45.8만 회는 채팅 횟수가 아닙니다.
- 공고문에서 참여 조건을 추출한다
- 공종 (工種)을 분류한다
- 안건과 회사 설명을 만든다
- 수량 내역서를 읽는다
- 이미지 PDF를 OCR 한다
- CAPTCHA 이미지를 인식한다
- 유사 안건용 임베딩 (Embedding)을 만든다
와 같은, 본방 데이터 처리의 기록입니다.
2026년 8월 1일 15:19 시점의 공개 화면에서는, 276,885안건, 236,391낙찰 결과, 429,446사까지 늘어나 있습니다.
공공 공사 정보 자체는 이미 공개되어 있습니다.
문제는 공개된 장소도 형식도 모두 제각각이라는 점입니다.
- 현(県)의 전자 입찰 포털
- 시정촌(市町村)의 신착 정보
- 연도별로 나뉜 HTML 표
- 1안건씩 공개되는 PDF
- ZIP에 들어있는 설계서와 도면
- 검색해야 표시되는 낙찰 결과
- PDF로만 공개되는 유자격자 명부
데이터는 공개되어 있어도, 그대로는 "자사가 참여할 수 있는가"라는 판단에는 사용할 수 없습니다.
그래서 1건의 공공 공사를 다음과 같은 흐름으로 처리하고 있습니다.
이용자에게 보여주고 싶은 것은 원본 PDF 목록이 아닙니다.
- 오늘 자사가 참여할 수 있을 것 같은 안건
- 필요한 공종, 등급, 경심점 (経審点), 영업소 요건
- 설계서에서 추출한 수량과 개략 견적
- 과거에 참여한 회사와 낙찰률
- 주변에서 움직이고 있는 원청·전문 공사 회사
즉, 공개 정보를 건설 회사가 다음 행동을 결정할 수 있는 데이터로 바꾸는 것이 목적입니다.
| 공개되어 있는 원본 | Newsatsu가 만드는 정보 |
|---|---|
| 입찰 공고 PDF | 참여 조건, 공종, 등급, 경심점, 지역 요건 |
| ... |
여기까지를 사람이 안건마다 읽는 것이 아니라, 로컬 AI가 매일 처리합니다.
가장 크게 개발 속도가 변한 것은 시정촌마다 다른 파서 (Parser)를 만들었을 때였습니다. 생성 20체・검증 19체의 AI 에이전트를 동시에 구동하여, 약 6시간 만에 21개 시정촌 분량을 만들었습니다.
다만, 그대로 사용할 수 있었던 것은 아닙니다. 속도와 품질을 어떻게 양립했는지는 후반부에서 실측 결과까지 작성하겠습니다.
계산 기반은 NVIDIA DGX Spark 2대입니다.
각 기기는 20코어의 Arm CPU, 128GB 통합 메모리, NVIDIA GB10을 탑재하고 있습니다. 2대는 200GbE 전용선으로 연결되어 있습니다.
| 노드 | 주요 역할 |
|---|---|
| DGX Spark 1 | 계산 노드, 무거운 집계·배치, 백업 |
| ... | |
| 운영 서버(Production)에는 PostgreSQL, 크롤러, API, Web, 로컬 LLM이 공존하고 있습니다. |
겉보기에는 AI 기반 시스템 같지만, 실제로는 데이터 수집, 검색, 모바일 API, 엣지 배포까지 포함하는 작은 데이터 센터입니다.
다만, 2대가 있다고 해서 단순히 여유가 있는 것은 아닙니다. 10개 현(県)을 대상으로 했을 때 운영 서버의 CPU 사용률은 88~95%에 달했습니다. 이 이야기는 후반부에서 다루겠습니다.
모델 실행에는 Ollama를 사용하고 있습니다.
외부 생성 AI API를 사용하지 않는 이유는 세 가지입니다.
- 관공서 자료와 자사의 판정 정보를 외부로 보내지 않기 위해
- 모든 안건에 AI를 적용하더라도, 호출 횟수에 따라 비용이 증가하지 않도록 하기 위해
- 모델과 프롬프트(Prompt)를 고정하여, 동일한 조건으로 재실행할 수 있도록 하기 위해
최근 7일간의 이용 실적입니다.
| 용도 | 모델 | 실행 횟수 |
|---|---|---|
| 조건·공종·수량 추출 | gpt-oss:20b | 40,985회 |
| ... | ||
| 가장 큰 120B 모델은 7일 동안 8번밖에 작동하지 않았습니다. |
가장 많이 사용된 것은 20B 모델이었습니다.
큰 모델을 매번 호출하는 것보다, 용도를 좁히고 작은 모델을 검증기(Verifier)와 조합하는 것이 실무에서 사용하기 더 편했기 때문입니다.
모든 AI 작업(Job)에는 다음 정보를 남기고 있습니다.
model_name
model_version
model_digest
...
동일한 입력이라도 모델이나 프롬프트, 검증기가 바뀌면 별개의 처리입니다.
"AI가 그렇게 말했다"가 아니라, 어떤 AI가, 어떤 지시로, 어떤 원문을 근거로 내놓았는지까지 추적할 수 있도록 하고 있습니다.
공고 PDF에서 참가 조건을 읽는 작업은 LLM과 매우 궁합이 좋습니다.
- 지역 요건
- 공종
- 등급
- 경심점 (경영상태 평가 지수)
- 건설업 허가
- 영업 연수
- 공사 실적
다만, "이 회사가 참가할 수 있는가"라는 최종 판단은 Python의 결정론적 규칙(Deterministic Rule)으로 수행합니다.
facts = llm_extract(document)
facts = require_verbatim_evidence(facts, document)
if not has_concrete_requirements(facts):
...
여기서 가장 중요한 것은 다음 두 가지를 구분하는 것입니다.
조건이 없음 -> 참가 조건 없음
조건을 가져올 수 없음 -> 판정 불능
값을 가져오지 못했을 때, AI가 추측으로 채우면 위험합니다. 따라서 LLM은 사실의 후보를 제시하는 단계까지만 수행합니다. 원문과의 대조 및 최종 판단은 외부에서 수행합니다.
수량 내역서의 품질 게이트(Quality Gate)에서는 120개 문서, 2,343행을 검사했습니다.
| 검사 항목 | 오류율 |
|---|---|
| 자릿수 오류 | 0.000% |
| ... | |
| 가장 많았던 실패는 화려한 AI 환각(Hallucination)이 아니라, 단가 열과 금액 열을 혼동하는 것이었습니다. |
AI를 사용할수록, 마지막에는 AI를 믿지 않아도 되는 설계가 필요했습니다.
이 시스템에서 가장 "AI를 사용했다"고 실감한 것은 후쿠오카현 내 시정촌(市町村) 전개입니다.
미배선 상태였던 23개 시정촌을 조사하니, 공통된 전자 입찰 시스템은 단 하나도 없었습니다.
같은 CMS를 사용하고 있더라도, 어떤 시는 낙찰자를 HTML로 공개하고, 어떤 마을은 PDF만, 다른 지자체는 월별 페이지로 구성되어 있습니다. 시정촌마다 파서(Parser)를 만들 필요가 있었습니다.
그래서 역할을 나누어 AI 에이전트(Agent)를 병렬 실행했습니다.
| 역할 | 에이전트 수 | 업무 |
|---|---|---|
| 생성 담당 | 20체 | 실제 페이지를 조사하여 파서 후보를 생성 |
| 검증 담당 | 19체 | 별도 경로로 원본과 출력을 대조 |
| 합계 | 39체 | 동일 사이트에 대한 동시 접속을 피하며 병렬 실행 |
결과는, 약 6시간 만에 21개 시정촌 분량의 파서를 만들어냈습니다.
사람 한 명으로는 동시에 21개 지자체를 조사할 수 없습니다. AI 에이전트였기에 가능한 속도입니다.
하지만 생성된 코드를 그대로 운영 환경에 넣을 수 있었던 것은 아닙니다.
| 독립 검증 | 건수 |
|---|---|
CONFIRMED | 6 |
PARTIALLY_WRONG | 13 |
생성 담당의 제출물 상태도 다음과 같았습니다.
| 출력 상태 | 건수 |
|---|---|
| 그대로 구문(Syntax)이 통과됨 | 15 |
| 앞부분에 이물질이 혼입됨 | 4 |
/* placeholder */만 있음 | 1 |
조사 내용은 맞는데, 최종 출력만 중간에 끊기는 케이스도 있었습니다.
따라서, AI 에이전트의 성과물은 완성품이 아니라, **고속으로 만들어진 PR (Pull Request)**로 취급하고 있습니다.
AI가 생성
↓
구문·import 검사
...
속도를 만들어낸 것은 39체의 에이전트입니다.
실제 서비스(본방)에 적용할 수 있었던 이유는, 그 뒤에 940건의 테스트와 원본 대조를 배치했기 때문이었습니다.
2대의 AI 머신을 사용하고 있기 때문에, 처음에는 GPU가 가장 먼저 한계에 도달할 것이라고 생각했습니다.
실제 CPU 사용량은 달랐습니다.
| 처리 | CPU 사용량 실측 |
|---|---|
| PostgreSQL | 8.9~9.7 코어 상당 |
| ... |
병목 지점(율속)은 AI가 아니라, PostgreSQL과 크롤러(Crawler)였습니다.
특히 무거웠던 것은 약 24만 건의 유사 검색입니다.
pgvector의 HNSW 검색을 요청마다 실행하고 있었으며, 평균 1,667.76ms였습니다. 누적 1,402,283회, 누적 실행 시간은 2,338,677초로, 약 27일 분량이었습니다.
그래서 검색을 빠르게 만드는 대신, 온라인으로 검색하는 것 자체를 그만두었습니다.
주간 배치(Weekly Batch)로 모든 안건의 유사 상위 6건을 일괄 계산하여, 약 147만 행의 사전 계산 테이블(Pre-computation table)에 저장합니다.
| 방식 | 실측·추정 시간 |
|---|---|
| HNSW를 1건씩 모든 안건에 실행 | 추정 약 82시간 |
| ... |
전체 처리: 82시간 × 60 ÷ 17분 ≒ 289배
DB 부분: 1,667.76ms ÷ (0.66ms + 0.74ms) ≒ 1,191배
가장 빠른 검색 쿼리를 만드는 것보다, 같은 답을 반복해서 검색하지 않는 것이 더 빠릅니다.
AI 기반을 만들면서, 최대의 고속화가 "AI도 GPU도 아닌, 사전 계산 테이블이었다"는 점은 의외였습니다.
Web으로의 24시간 요청은 413,759건이었습니다. 그 중 92.8%가 봇(Bot)이었습니다.
과거 안건의 롱테일 URL(Long-tail URL)로 전 세계에서 한 번씩 접속됩니다. 일반적인 CDN 캐시로는 동일한 장소에서 동일한 URL이 재방문되지 않기에, 안건 상세 페이지의 히트율은 2.7%였습니다.
그래서 Cloudflare D1에 최신 버전의 포인터를, R2에 생성된 HTML을 두었습니다.
Worker는 최신 버전의 존재를 확인할 수 있는 경우에만 HTML을 반환합니다. 조금이라도 의심스러우면 Next.js로 그대로 통과(Pass-through)시킵니다.
24시간의 결과입니다.
| 지표 | 실측값 |
|---|---|
| Worker가 받은 요청 | 30,149건 |
| ... |
TTFB는 거의 변하지 않았습니다.
그럼에도 성공입니다. 목적은 수 ms의 고속화가 아니라, CPU 사용률 88~95%인 운영 서버(본방기)가 동일한 페이지를 그리게 하지 않는 것이기 때문입니다.
지금까지의 숫자만 보면 처음부터 계획대로 만들어진 것처럼 보입니다.
실제로는 상당히 망가뜨렸습니다.
| 사고 | 영향 | 원인 |
|---|---|---|
| 날짜 검색이 한 번도 작동하지 않음 | 약 17,000행이 소리 없이 누락 | 존재하지 않는 폼 항목에 날짜를 설정 |
| 전국에서 동명이인이 1개사라면 동일 법인으로 판단 | 355건을 다른 회사로 오귀속 | 이름의 유일성을 법인 식별에 사용 |
| 일괄 UPDATE의 결합 키가 비유일함 | 1,707행을 오염 | 쓰기 전의 유일성 확인 부족 |
| 페이지를 대량으로 미리 생성 | 절약량의 13~17배 부하 | 생성 대상과 실제 요청이 불일치 |
| 안전장치가 통상 조작을 사고로 판정 | CI를 11일 정지·788회 스킵 | 감시역 자신의 정지를 감시하지 않음 |
특히 무서웠던 것은 에러가 아니라 정상 종료였습니다.
나가노현에서는 1년 치 검색을 12일씩 나누어 상한을 회피하려 했습니다. 하지만 검색 폼에 코드가 상정한 날짜 필드가 존재하지 않아, 기간은 한 번도 적용되지 않았습니다.
예외는 0건. 로그는 성공. 데이터만 누락되어 있었습니다.
현재는 다음 상태를 별도로 저장하고 있습니다.
| 상태 | 의미 |
|---|---|
empty_confirmed | 원본을 확인했으며, 정말로 0건 |
unmeasurable | 조건이 적용되었는지 확인할 수 없어 측정 불능 |
failed | 폐장, 인증, 통신, 상한 초과 등으로 실패 |
"설정했다"는 입력이고, "적용되었다"는 결과입니다.
이 구분을 도입한 이후로는 0건을 성공의 증거로 사용하지 않게 되었습니다.
실패를 반복한 결과, 품질 보증은 다음 5원칙으로 수렴했습니다.
| 원칙 | 구현상의 의미 |
|---|---|
| 원문 접지 (Grounding) | 저장할 값이 원문에 존재하는지 확인한다 |
| ... |
2026년 8월 1일 시점에서, 크롤러 계열 테스트는 940건 성공. 현 전개용 17개 트랩은 FAIL 0, 측정 불능 0까지 개선되었습니다.
다만, 녹색 표시만으로는 신뢰하지 않습니다.
- 몇 건을 검사했는가
- 마지막으로 성공한 것은 언제인가
- 이전 버그를 되돌렸을 때 빨간색으로 변하는가
- 운영 환경과 동일한 의존성 (Dependency)으로 동작했는가
- 원본과의 독립적인 대조 (Independent Verification)가 있는가
까지 확인합니다.
0건을 검사하고 PASS한 테스트는 FAIL보다 더 위험합니다.
Newsatsu는 현재 10개 현(県)에서 운영 중입니다.
전국 47개 도도부현의 진출 상태는 관리하고 있지만, "전국 대응 완료"라고는 쓸 수 없습니다.
운영 서버는 10개 현 시점에서 CPU 사용률이 88~95%입니다. PostgreSQL만으로 최대 9.7코어를 사용하고 있습니다. 현재 구성을 단순히 4.7배로 늘릴 여력은 없습니다.
다음에 필요한 것은 세 번째 GPU가 아니라 다음의 분리입니다.
- PostgreSQL을 Web·크롤러와 별도 장비로 이전
- 읽기 처리와 쓰기 처리 분리
- 현(県) 의존 데이터 분할
- 이력 취득 및 집계 배치 (Batch)를 계산 노드로 분리
- 새로운 현(県)마다 원본 모수를 등록하여 결락을 측정할 수 있도록 구성
47개 현 분량의 코드를 작성하는 것보다, 47개 현에서 매일 망가지지 않고 동작한다는 것을 증명하는 것이 더 어렵다는 것을 깨달았습니다.
건축사 1인이 시작한 시스템이 현재는 다음과 같은 규모가 되었습니다.
- DGX Spark 2대
- 로컬 AI 누적 458,900회
- 약 27만 건의 안건, 약 43만 개 사
- PostgreSQL 34GB, 336개 테이블, 704개 인덱스 (Index)
- AI 에이전트 39체를 병렬 실행
- 약 6시간 만에 21개 시정촌의 파서 (Parser) 후보 작성
- 10개 현, Web, iOS, Android로 운영 공개
단순히 AI에게 코드 작성을 시키기만 해서는 여기까지 동작하지 않았습니다.
원본을 저장하고, AI의 출력을 검증하며, 불분명한 것을 불분명한 상태로 취급하고, 망가지면 되돌릴 수 있는 메커니즘을 만드는 것. 그 지루한 부분이 있었기에 비로소 39체의 AI 에이전트와 45.8만 회의 로컬 AI를 운영 환경에서 사용할 수 있었습니다.
이 시스템의 대단함은 AI를 45.8만 번 호출했다는 사실뿐만이 아닙니다.
45.8만 번을 호출해도, 어떤 원본으로부터, 어떤 모델이, 왜 그 값을 내놓았는지 추적할 수 있다는 점이라고 생각합니다.
실제 서비스는 다음과 같습니다.
- Web: Newsatsu|일본의 공사를, 가까이서, 낭비 없이.
- iPhone: 공공 공사 입찰·영업 레이더
- Android: 공공 공사 입찰·영업 레이더
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기