소개
저는 농약 분야의 전문가가 아닙니다. 이번 프로젝트는 오히려 그 점에서 시작했습니다.
제가 잘 모르는 도메인에도 AI와 데이터 엔지니어링 방법을 적용해 쓸 수 있는 시스템을 어디까지 만들 수 있는지 시험해 보고 싶었습니다. AI가 농약 전문가처럼 답하게 만드는 것이 아니라, 공식 데이터를 수집하고 구조화하며, 불확실한 판단을 멈추게 하는 과정까지 구축할 수 있는지를 확인하는 실험이었습니다.
그래서 농촌진흥청 농약안전정보시스템(PSIS) OpenAPI 데이터를 이용해 nongyak-kb를 만들었습니다.
농약 사용기준: 144,073건
제품: 3,961개
병해충·잡초: 990개
작물: 428개
등록취소 이력: 400건
분석법: 385건
결과물은 SQLite/FTS5 지식베이스, RDF/TTL 온톨로지, SHACL 제약, 그래프 JSON, CLI와 정적 웹앱으로 구성했습니다.
주의: 이 저장소는 참고용입니다. 실제 농약 사용과 방제 판단은 반드시 제품 라벨과 최신 공식 정보를 기준으로 해야 합니다.
진행 방법
1. 먼저 권위와 중단 조건을 정했습니다
낯선 도메인에서는 AI가 자연스럽게 설명하는 것보다 어떤 자료를 최종 근거로 인정할지가 더 중요했습니다.
농촌진흥청 PSIS와 제품 라벨을 최종 기준으로 사용
GPT나 Claude가 서로 같은 답을 내더라도 공식 근거가 없으면 확정하지 않음
작물, 병해충, 제품, 용도, 사용 시기, 희석배수, 사용 횟수와 등록 상태가 불명확하면
확인 필요로 처리원천 데이터와 변환 결과를 분리해 오류를 추적할 수 있게 함
검색 결과와 추천 결과에 등록취소 상태를 함께 표시
2. PSIS OpenAPI를 전량 수집했습니다
Bun과 TypeScript로 수집기를 만들고 다음 API를 연결했습니다.
SVC01 농약등록현황
SVC02 농약상세정보
SVC07 분석법
SVC08 등록취소농약
수집기는 약 1시간이 걸리는 전량 수집을 중단 후 재개할 수 있게 구성했습니다.
이 과정에서 문서만 읽어서는 알기 어려운 API 특성도 발견했습니다. 예를 들어 startPoint는 페이지 번호가 아니라 1부터 시작하는 아이템 오프셋이었습니다. 페이지 번호로 해석하면 데이터 대부분을 빠뜨릴 수 있습니다.
3. SQLite로 정규화하고 조회 계약을 분리했습니다
수집한 데이터를 제품, 작물, 병해충, 성분, 사용기준, 등록취소 이력 등의 테이블로 정규화했습니다.
CLI, 웹앱, exporter가 원시 테이블을 직접 읽지 않고 공통 v_usage 뷰와 contracts/의 계약을 사용하게 했습니다. 같은 질문에 CLI와 웹앱이 다른 결과를 내지 않도록 검색 컬럼과 매치 규칙도 통일했습니다.
웹앱이 SQLite 단일 파일을 직접 읽도록 설계했기 때문에 WAL 모드는 사용하지 않았습니다. 91.5MB DB는 13.3MB gzip 파일로 압축해 브라우저에서 해제하도록 했습니다.
4. RDF 온톨로지와 SHACL 제약을 만들었습니다
제품, 성분, 작물, 병해충, 회사 등을 노드와 관계로 변환했습니다.
dist/ontology.ttl: 약 120만 RDF 트리플dist/shapes.ttl: SHACL 제약dist/graph.json: 제품·성분·작물·병해충·회사 그래프
SHACL 검증과 표준 Turtle 파서를 함께 사용했습니다. SHACL 검사는 통과했지만 표준 파서에서는 읽히지 않는 IRI 문제가 발견되어, 두 검증을 분리해서 확인하게 했습니다.
5. CLI와 정적 웹앱을 붙였습니다
CLI에는 다음 기능을 넣었습니다.
query: 작물, 병해충, 제품, 상표, 회사, 영문명, 성분 통합 검색product: 제품 성분, 작용기작, 독성, 등록 상태와 사용기준 확인rotate: 이미 사용한 작용기작 계열과 겹치지 않는 후보 검색stats: 데이터 건수와 생성 시각 확인
기본 실행 방법입니다.
bun install
cp .env.example .env
bun run fetch
bun run normalize
bun run export
bun run compress검색 예시입니다.
bun run cli -- query "고추 탄저병"
bun run cli -- product "가스가민"
bun run cli -- rotate "고추 탄저병" --used 만코제브키 없이 구조만 확인할 때는 bun run fetch --mock으로 샘플 데이터를 사용할 수 있습니다.
6. 독립적인 QA를 두 번 돌렸습니다
기능이 실행된다는 것과 결과가 맞다는 것은 다른 문제였습니다. 두 차례의 독립적인 QA에서 10개의 결함을 찾았습니다.
상표명과 회사명만으로 등록취소를 연결해 현재 등록 제품까지 취소로 오인
rotate가 등록취소 제품을 후보에 포함최초 등록일이 실제 최솟값이 아니라 첫 번째 수집 행에 따라 결정
복합 제형 일부가 필터에서 누락
CLI와 웹앱의 검색 방식이 달라 결과 불일치
서로 다른 병해충 이름이 같은 RDF IRI로 합쳐짐
SHACL은 통과하지만 표준 Turtle 파서가 읽지 못하는 IRI 생성
등록취소 판정은 품목명, 상표명, 회사명 3중 일치로 강화했고, 검색과 필터 규칙을 통일했습니다. 현재 28개 테스트, TypeScript 타입 검사, SHACL 검증, 약 120만 트리플 파싱과 실제 브라우저 검색을 확인했습니다.
결과와 배운 점
이번 실험에서 가장 크게 배운 점은, 잘 모르는 도메인일수록 더 강한 프롬프트가 아니라 더 명확한 계약과 검증이 필요하다는 것입니다.
AI는 수집기, 정규화 코드, 온톨로지 exporter, CLI와 웹앱을 빠르게 만드는 데 유용했습니다. 하지만 어떤 필드가 실제 등록취소를 의미하는지, 같은 이름을 어떻게 연결해야 하는지, 어떤 결과를 사용자에게 보여주면 위험한지는 모델의 합의만으로 결정할 수 없었습니다.
특히 공식 데이터라고 해서 그대로 안전한 지식이 되는 것도 아니었습니다. API 문서와 실제 응답의 차이, 식별자 불일치, 회사명 변경, 중복 등록번호 같은 문제는 별도의 계약과 실패 처리가 필요했습니다.
현재 한계
등록취소 상태는 일부 필드의 일치에 따른 추정이며 미탐과 과잉 전파 가능성이 있음
복합제 성분명 파싱이 약 39%에서 불완전함
웹앱 자동 테스트가 없고 실제 브라우저 검증에 의존함
데이터는 2026년 7월 24일 시점의 스냅샷임
따라서 이 프로젝트의 의미는 농약 추천기를 완성했다는 데 있지 않습니다. 제가 잘 모르는 도메인에서 AI를 사용할 때, 공식 출처, 데이터 계약, 테스트, 거부 조건과 알려진 한계를 어디까지 시스템으로 만들 수 있는지를 확인했다는 데 있습니다.