Tender Radar: 소규모 계약자를 위한 정부 입찰 정보 탐지기 (개인 노트북에서 완전 작동)
요약
Tender Radar는 소규모 계약자를 위해 설계된 로컬 우선(local-first) 정부 입찰 정보 탐지기입니다. 이 도구는 공고에서 자격 요건을 추출하고, 회사의 자체 문서와 대조하여 적합성을 검증합니다. 또한 수주 확률을 추정하고 초안 문서를 작성하는 등 계약 업무의 효율성을 극대화합니다.
핵심 포인트
- 공고의 자격 요건(매출액, 경험 등) 자동 추출 및 분석
- 자체 문서와 공고 조항 대조를 통한 근거 기반 검증 제공
- 수주 확률 추정 기능과 초안 작성으로 업무 효율성 증대
- 로컬 환경에서 작동하여 보안성과 접근성을 높임
This is a submission for the Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle
제가 만든 것 (What I Built)
Tender Radar는 소규모 계약자를 위한 로컬 우선(local-first) 정부 입찰 정보 탐지기입니다.
소규모 전기 계약업체가 공공 입찰에 참여합니다. 관련 공고가 GeM, 주정부 포털 및 부서 게시판 등에 올라오면, 사무실은 세 가지 질문에 빠르게 답해야 합니다: 어떤 공고가 중요한지, 회사가 실제로 자격이 되는지(매출액, 유사 작업 경험, 라이선스), 그리고 빠짐없이 규정 준수 서류를 어떻게 준비할지입니다. 인증서 하나만 놓쳐도 기술적인 이유로 입찰이 거부되고, 준비 비용은 사라집니다. 많은 MSME 계약업체는 이러한 필터링을 위해 커미션 에이전트에게 돈을 지불하거나, 단순히 자신이 수주했을 공고를 놓치기도 합니다.
Tender Radar는 이 세 가지 질문에 대해 계약자의 자체 장비에서 답을 제공합니다:
- 각 공고에서 자격 요건 조항(매출액, EMD, 경험, 라이선스, 인증서)을 추출합니다.
- 모든 조항을 회사의 자체 문서(과거 입찰 기록, 완료 증명서, 라이선스 및 ISO 스캔본, 감사된 재무제표)와 대조하고, 모든 주장에 근거를 제시합니다.
- 평이한 언어로 위험한 용어(가치 대비 9%의 EMD 요구, 3일 남은 마감 기한, 회사가 수행했던 작업보다 10배 큰 프로젝트)를 표시합니다.
- 회사의 과거 입찰 기록을 바탕으로 TabPFN을 이용해 수주 확률을 추정합니다.
- 자체 문서로 뒷받침할 수 없는 문장은 삭제하는 인용문 보호 장치(citation guard) 뒤에 커버레터, 규정 준수 진술서, 역량 요약서를 초안 작성합니다.
누가 비용을 지불할까요: 입찰 건당 에이전트에게 돈을 지불하거나, 자신이 알지 못했던 공고를 놓치는 MSME 계약업체들입니다. 수주한 입찰 하나가 1년 동안의 도구 사용료를 충당합니다.
데모 (Demo)
적합도와 수주 확률 순으로 정렬된 보드:
모든 요구사항 뒤에는 증거 인용문과 문서 출처가 붙은 하나의 입찰 건:
생성되는 인쇄 준비가 된 입찰 패키지:
위의 모든 과정은 의도적으로 로컬에서 계산되었습니다. 핵심은 민감한 문서가 보관된 기기에서 실행된다는 것입니다. 다섯 가지 명령만으로 클론(clone)부터 보드(board)까지 이동할 수 있습니다 (전체 목록은 README 참조): 설치, 데모 코퍼스 생성, 불러오기, 8개 입찰 건 스캔, 대시보드 열기. 이 코퍼스는 명확하게 라벨링된 합성적이지만 현실적인 전기 MSME(Jaipur, Class-A 라이선스, 평균 매출 약 INR 3.7 crore)입니다. 실제 포털 수집은 키 뒤에 스캐폴딩되었습니다.
코드
github.com/praneshnikhar/tender-radar, 챌린지 기간 동안 구축되었습니다. scripts/eval_summary.py는 아래 모든 숫자를 재현합니다.
구축 방법
추출(Extraction), 판결(verdicts) 및 초안 작성(drafting)은 모두 Ollama를 통해 Gemma 4 (gemma4:latest, 4.6B Q4)에서 완전히 로컬로 실행됩니다. 금액은 모델의 기분에 의존해서는 안 되므로 코드 내에서 결정론적으로 파싱됩니다. 매칭은 하이브리드 검색(내장 TF-IDF 임베더와 BM25)을 거친 후, 입찰 건당 Gemma 판결 과정을 한 번 거칩니다. 낙찰 확률은 TabPFN v3.5 with thinking mode를 사용합니다.
인용문 보호 장치(citation guard)는 제가 가장 자랑스러워하는 부분이며, 이는 4B 모델이 프론티어 모델이 아니기 때문에 존재합니다. 저는 이 장치가 한 조항을 '충족됨(met)'으로 표시했지만, 자체적인 추론 과정에서는 해당 회사가 부족하다고 주장하는 것을 포착했습니다. 따라서 모든 '충족됨' 표시는 인용문과 함께 제공되며, 이는 표시되기 전에 검증됩니다: 정확히 일치하거나, 숫자를 수치적으로 비교하는 퍼지 매치(fuzzy match)의 경우입니다('INR 84.5 lakh'은 'INR 84.50 lakh'과 일치하며; 'two crore'는 '2 crore'와 불일치합니다). 결정론적인 금액 교차 확인(deterministic amount cross-check)은 모델과는 독립적으로 실행됩니다.
M3 노트북에 8GB RAM으로 측정했을 때, 모든 것이 로컬에서 작동했습니다: 8개 입찰 건에서 51개의 조항이 추출되었으며, 각각 파싱된 금액을 가졌습니다; 판결은 충족됨 24건, 부분적 5건, 누락됨 19건, 알 수 없음 3건이었습니다; 인용문 보호 장치는 모델의 인용문 3개(등급 하향 조정, 사유 기록)를 거부했고 교차 확인은 숫자 충돌 4개를 포착했습니다; 초안 작성 기능은 16개 섹션에 걸쳐 지원되지 않는 문장 8개를 제거했습니다; LLM 호출 21회, 약 83k 토큰, US$0.00이었습니다; 8개 입찰 건 처리 시간은 22분이 소요되었습니다.
동일한 시간 순서 분할(time-ordered split)을 사용하여 Win 모델의 제거 실험(ablation)을 진행했습니다 (제출된 입찰가 147건): sklearn 기준선 AUC 0.731 / Brier 0.260 대 TabPFN v3.5 + 사고 과정: AUC 0.752 / Brier 0.209. 'A should-I-bid' 숫자는 보정(calibration)에 따라 생사여탈권을 가집니다.
이 보호 장치를 만든 배경을 읽어보세요: 당신의 AI가 실제 파일을 인용했습니다. 하지만 여전히 당신에게 거짓말을 했습니다. 및 RAG가 추측할 때 인정하도록 만들기.
Open Innovation은 왜 중요할까요?
- 데이터는 경쟁적으로 민감합니다. 계약업체의 감사된 재무제표, 과거 입찰 및 가격 정보는 해당 회사가 소유한 가장 민감한 문서입니다. 모든 문서 읽기 및 쓰기 단계는 로컬 오픈 가중치 모델로 작동합니다. 시스템에서 유일하게 네트워크 호출이 발생하는 부분은 TabPFN 승리 모델이며, 이 모델은 문서를 전송하는 것이 아니라 특징(값, 거리, 경쟁사 수)만을 전송합니다.
- 한계 비용 변화가 행동을 바꿉니다. 기존 대안들은 입찰 건당(에이전트) 또는 좌석당(SaaS) 비용이 발생합니다. 로컬 추론은 하나의 공고를 스캔하는 비용을 무료로 만들어, 회사가 감당할 수 있는 공고만 확인하는 대신 모든 공고를 확인하도록 만듭니다.
- 소규모 데이터 문제는 소규모 데이터 모델과 폴백(fallback)이 필요합니다. 승리 확률은 약 220개의 과거 입찰 건에 걸쳐 모델링됩니다. TabPFN은 이 체제에 맞춰 설계되었으며, 수천 개의 행을 요구하는 폐쇄형 AutoML과는 다릅니다. 또한 파이프라인은 sklearn으로 우아하게 저하(degrades gracefully)되므로, 필수적인 외부 종속성이 전혀 없습니다.
솔직한 트레이드오프: 4B 로컬 모델은 입찰 건별로 볼 때 최첨단 API보다 속도가 느리고 산문 처리 능력이 떨어집니다. 하지만 이 가드가 그 부분을 안전하게 만들고, 프라이버시와 비용 측면의 장점들이 이를 충분히 가치 있게 만듭니다. 폐쇄형 API였다면 구축하기는 더 쉬웠겠지만, 이 고객에게 제공하는 것은 불가능했을 것입니다.
나의 에이전트 세션
Tender Radar: Gemma 4, TabPFN 및 인용 가드를 사용하여 로컬 우선 정부 입찰 레이더 구축
당신
부업(Side Hustle) 챌린지: 핵심에 오픈소스 AI를 활용하여 구축하기. 소규모 계약자를 위한 정부 입찰 정보 탐지기. 기능: GeM과 같은 입찰 포털을 크롤링하고, 해당 입찰 정보를 소규모 회사의 과거 작업 및 인증서와 매칭하며, 적격성 체크리스트를 작성하고, 규정 준수 섹션을 초안 작성합니다. 오픈 스택: 회사의 문서를 담은 벡터 인덱스를 가진 다중 에이전트 파이프라인으로, 로컬에서 실행됩니다. 비용 부담자: 현재 에이전시에게 수수료를 지불하거나 입찰 기회를 완전히 놓치는 중소기업(MSME) 계약업체. 오픈 방식의 장점: 이들의 재무 정보와 과거 입찰 기록은 경쟁적으로 민감합니다. 제한 사항: 현재는 3주차에만 한정, 예산 0, 전기 계약자 페르소나. 새 폴더를 만들고 모든 것을 만들기 시작하며, 먼저 계획을 세웁니다.
더 보기
에이전트
먼저 계획하고, 그다음에 구축합니다. 환경 정찰을 통해 얻은 주요 결정 사항:
▸ bash sysctl CPU + RAM; docker --version; ollama list
입력
$ sysctl CPU + RAM; docker --version; ollama list
출력
Apple M3, 8 GB RAM. Docker daemon hung (no response). Ollama installed with gemma4:latest (4.6B Q4, tools+thinking, 3.3GB) already pulled. No embedding model present. Local Postgres 17 on :5432 but no pgvector extension.
결정 사항: 기본 스토어는 SQLite를 사용합니다(완전 로컬, ACID, 데모 규모에서는 무차별 코사인 검색이 즉각적임)하며, 선택적으로 Postgres+pgvector 경로가 문서화됩니다. 오픈 가중치 LLM은 Ollama를 통해 사용하고; 다운로드할 필요가 없도록 내장된 해시 TF-IDF 임베더를 폴백(fallback)으로 사용합니다. 승리 확률 모델에는 TabPFN을 사용하며 sklearn이 폴백입니다. 아키텍처: 공지사항 -> 추출 -> 매칭 -> 점수화 -> 초안 작성 -> 입찰 패키지. 목표 대상 상품: Gemma (핵심 LLM), TabPFN (승리 모델), 그 외 모든 것에 대해 정직하게 접근합니다.
에이전트
프로젝트를 구축했습니다: pyproject, db/schema.sql, 그리고 핵심 모듈들.
▸ write src/tender_radar/ingest/fixtures.py
입력
src/tender_radar/ingest/fixtures.py
출력
FixtureGenerator: 가상의 전기 MSME(Sharma Electricals & Co., Jaipur, Class-A 라이선스, 평균 매출 약 3.7 크로어), 18개 기업 문서(감사 재무제표, 라이선스, ISO, 10개 완료 증명서, 역량 기술서), 실제 라자스탄 조달 스타일의 입찰 공고 40건, 과거 입찰 결과 220건.
▸ write src/tender_radar/match/init.py
Input
src/tender_radar/match/__init__.py
Output
Hybrid 검색: 문서 청크에 대한 벡터 유사도 + BM25, 이후 입찰 건별로 LLM 판정 1회. quote_verified(): 정확히 일치하는 경우 통과; 단위 인식 수치 비교를 통한 퍼지 매치. numeric_cross_check(): 조항의 INR 금액과 인용된 문서 간의 금액을 비교하는 결정론적 백스톱.
당신은 모델을 가져오지 않고, 내가 이미 가지고 있는 모델 중 아무거나 선택할 것이지만 API 키는 사용할 것입니다.
에이전트
이해했습니다 - LLM 작업에는 로컬의 gemma4:latest만 사용하고, TabPFN과 내장 임베더를 위해 API 키를 사용할 것입니다. 조항 추출을 위해 Gemma 4로 스모크 테스트를 진행했는데, 다섯 가지 요구 사항 유형 모두가 올바르게 분할된 깨끗한 구조화된 JSON을 반환했습니다. 전체 생성된 입찰 공고는 실제 주립 포털 문서처럼 보였습니다.
에이전트
빌드 과정에서 기록할 만한 두 가지 실제 버그를 발견하고 수정했습니다:
▸ bash python -m pytest test/ (sqlite 오류 디버깅 후)
Input
$ python -m pytest test/
Output
(1) 'table evidence has 11 columns but 10 values were supplied' 오류는 SQL 문자열 내의 리터럴 '?' 플레이스홀더 개수를 세어 추적되었습니다. 한 수정 작업에서 Python 튜플을 11개 값으로 업데이트했지만, 여전히 10개의 플레이스홀더만 남아 있었습니다. 이 경우 튜플이 아닌 플레이스홀더 개수를 수정했습니다. (2) Quote guard는 문서에 'INR 84.50 lakh'이라고 명시되어 있음에도 불구하고 'INR 84.5 lakh'을 거부했는데, 이는 토큰화 과정에서 '84.50'이 [84, 50]으로 분리되었기 때문입니다. 검사 로직을 재작성하여 통화 금액은 단위(crore/lakh -> 동일 규모)와 함께 파싱하고 수치적으로 비교하도록 했으며, 순수 숫자는 여전히 문자 그대로의 토큰 일치를 요구합니다. 검증 결과: 'two crore'는 '2 crore'와 불일치하며, 'INR 6.2 crore'는 'INR 62 lakh'과 불일치하는 등 동등한 재포맷팅은 통과했습니다. 총 8개의 테스트가 통과했습니다.
Agent
M3/8GB에서 로컬로 실행된 8개 입찰 건에 대한 전체 측정 결과: 51개 조항이 추출되었으며, 모든 조항에 파싱된 금액이 포함되었습니다. 판정 결과는 다음과 같습니다: 충족(met) 24건 / 부분 충족(partial) 5건 / 누락(missing) 19건 / 알 수 없음(unknown) 3건. guard는 모델 인용 3건을 거부하여 (사유와 함께 unknown으로 하향 조정) 처리했으며, 결정론적 금액 교차 확인(deterministic amount cross-check)은 모델이 해당 조항을 '충족'으로 표시했음에도 불구하고 자체적인 근거에서 회사가 부족하다고 주장한 경우를 포함하여 4개의 수치적 충돌을 포착했습니다. draft guard는 16개 섹션에 걸쳐 8개의 지원되지 않는 문장을 제거했습니다. 총 21회의 LLM 호출, 약 83k 토큰 사용, 비용 $0.00이 발생했습니다. 보드, 상세 페이지, 인쇄용 입찰 패키지의 스크린샷을 찍었습니다.
You
tabpfn_sk_REDACTED - 이것은 tabpfn 키
Agent
tabpfn-client를 설치하고 인증의 미묘한 차이점을 발견했습니다: 이 라이브러리는 TABPFN_TOKEN을 읽으며, TABPFN_API_KEY가 아닙니다. 따라서 설정(config)은 이제 둘 다 허용하며 set_access_token을 명시적으로 호출합니다. 147개의 제출된 입찰 건에 대해 동일한 시간 순서의 75/25 분할로 A/B 테스트를 수행했습니다:
▸ bash compare default vs v3.5_default vs v3.5_default+thinking (동일한 분할 기준)
Input
$ compare default vs v3.5_default vs v3.5_default+thinking on the same split
Output
default checkpoint: AUC 0.6713, Brier 0.2137. v3.5_default: AUC 0.6713, Brier 0.2137. v3.5_default + thinking(medium): AUC 0.7378, Brier 0.2084.
Thinking mode가 기본값(default)이 되었습니다. 최종 평가: sklearn 기준선 AUC 0.731 / Brier 0.260 대 TabPFN v3.5+thinking AUC 0.752 / Brier 0.209 - 둘 다 더 좋고, '입찰해야 할까?'라는 숫자에 필요한 것은 보정(calibration)입니다.
당신
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기

