이 글은 Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle에 제출하는 글입니다.
제가 만든 것
저는 이 주간 챌린지의 다른 참가자들의 게시글을 읽는 데 시간을 보낸 후에 저만의 프로젝트를 시작했습니다. 영수증 분류기, 송장 추출기, Etsy 상품 목록 작성기, 소규모 사업자를 위한 GST 도구, 견적 생성기 등 약 백 개의 프로젝트가 있었습니다. 많은 프로젝트들이 잘 만들어졌고 실질적인 문제를 해결했습니다.
하지만 대부분의 프로젝트는 동일한 가정에서 시작합니다. 즉, 이미 사이드 허슬(side hustle)을 시작했다는 것입니다.
저는 그 이전 단계에 대해 계속 생각했습니다. 만약 직장인이면서 부업으로 무언가를 만들고 싶다면, 가장 먼저 던져야 할 질문 중 하나는 고용 계약서가 자신이 무엇을 할 수 있는지 제한하는 조항이 있는지 여부입니다. 관련 조항은 외부 업무(outside work), 지적 재산권(intellectual property), 배타성(exclusivity), 또는 공개 의무(disclosure requirements) 등을 다룰 수 있습니다. 그리고 우리 중 많은 사람이 그 계약서에 서명하고, 내용을 대충 훑어본 후로는 다시는 보지 않았습니다.
그래서 저는 Moonlight를 만들었습니다. 이 도구는 고용 계약서를 스캔하여 사이드 프로젝트와 관련될 수 있는 조항을 찾고, 원래의 문구를 인용하며, 검토할 가치가 있는 특정 세부 사항을 강조 표시합니다.
자신이 만든 것을 판매할 수 있나요?
예를 들어, Moonlight는 다음과 같은 지적 재산권(intellectual-property) 조항을 찾아낼 수 있습니다:
"all Works conceived, created or developed by the Employee during the term of employment, whether or not during working hours, whether or not using the resources of the Company, and
...
그리고는 중요한 문구를 강조 표시합니다:
Moonlight는 사용자가 사이드 비즈니스를 운영하는 것이 법적으로 허용되는지 여부를 결정하지 않습니다. 대신, 계약서에 무엇이 명시되어 있는지 보여주고, 정확한 문구를 지적하며, 어떤 부분에 대해 질문해야 하는지 파악하는 데 도움을 줍니다. 이 경계 설정은 의도적인 것이며, 이는 아래에서 설명할 몇 가지 기술적 결정에 영향을 미쳤습니다.
이 도구의 예상 사용자는 사이드 프로젝트를 시작하고 싶지만 자신의 고용 계약서가 무엇을 말하는지 확신하지 못하는 사람입니다. 청구서나 판매에 대해 걱정하기 전에, 그들은 자신이 서명한 내용이 무엇인지 이해할 필요가 있습니다.
데모 (Demo)
계약서를 붙여넣고 **읽기(Read it)**를 클릭합니다. 제 노트북에서 28개 조항으로 이루어진 계약서는 약 90초 만에 처리되며, 이는 조항당 대략 5초에 해당합니다.
Moonlight reading clause 9.2 of a contract, showing the original sentence and four flags linked to the exact wording
Moonlight는 또한 검토 과정에서 무슨 일이 일어났는지 볼 수 있도록 상태 표시줄을 보여줍니다:
28 clauses in document · 12 matched on keywords · 12 quotes verified ·
0 readings discarded · 0 verdicts given · 88.3s · nothing left this laptop
호스팅된 데모는 없으며, 이는 의도적인 선택입니다. 고용 계약서에는 급여, 고용주 및 의무에 대한 민감한 세부 정보가 포함될 수 있습니다. 첫 번째 검토를 받기 위해 서버로 전송하는 것은 제가 이 도구를 만든 주요 이유 중 하나를 무효화할 것입니다.
이 도구는 npm install && npm start를 사용하여 로컬에서 실행할 수 있습니다. 자체 문서를 제공하지 않아도 시도해 볼 수 있도록 샘플 계약서가 포함되어 있습니다.
코드 (Code)
GitHub: imarpanpatra/moonlight MIT 라이선스입니다.
이 프로젝트는 Node.js, Express, Ollama, 그리고 gemma3:4b를 사용하며, 하나의 애플리케이션 의존성을 가집니다.
ollama pull gemma3:4b
ollama serve
npm install && npm start # http://127.0.0.1:4455
...
주요 파일들과 각 파일의 역할은 다음과 같습니다:
| 파일 | 책임 내용 |
|---|
src/clauses.js | 계약서를 조항(clause)으로 분할하고 키워드를 사용하여 후보를 검색합니다. 모델은 관여하지 않습니다. |
| ... | |
구축 과정
모델의 작업을 가능한 작게 유지하기
가장 중요했던 첫 번째 결정은 모델에게 전체 계약서를 읽고 어떤 조항이 중요한지 판단하도록 요청하는 것을 피하는 것이었습니다.
Moonlight는 조항 번호 매김을 사용하여 문서를 분할하고, 각 조항과 함께 섹션 제목을 유지합니다. 그런 다음 7가지 범주에 대한 키워드 세트를 기준으로 조항의 점수를 매깁니다:
- 업무 외 활동 (Outside work)
- 지적 재산권 양도 (Intellectual-property assignment)
- 독점성 (Exclusivity)
- 경업 금지 (Non-compete)
- 고객 유인 금지 (Non-solicitation)
- 기밀 유지 (Confidentiality)
- 공개 의무 (Duty to disclose)
조항이 후보가 되려면 그 앞에 앵커 구문(anchor phrase)을 포함해야 합니다. 이 요구 사항이 없으면
첫 번째 버전에서는 gemma3:4b에게 각 조항을 '직장 외적으로 적용되는지(yes)', '불분명한지(unclear)', 또는 '아닌지(no)'라는 간단한 답변으로 분류하도록 요청했습니다.
테스트한 8개 조항 모두에 대해 unclear라고 답했습니다.
프롬프트를 수정하고, 더 명확한 정의를 추가했으며, 두 가지 예시를 포함했습니다. 같은 모델을 사용했지만, 이번에는 8개 조항 모두에 대해 yes라고 답변했습니다. 모델과 계약은 변하지 않았고, 오직 프롬프트의 문구만 변경했을 뿐입니다.
이는 고용 계약서를 다루는 도구로서 신뢰할 수 있는 기반이 아니었기 때문에, 직관에 의존하여 프롬프트를 계속 수정하는 대신 작은 평가 세트(evaluation set)를 만들었습니다. src/eval.js에는 14개의 라벨링된 조항이 포함되어 있습니다: 직장 외적으로 적용되는 것 6개, 그렇지 않은 것 5개, 그리고 진정으로 모호한 것 3개입니다.
gemma3:4b의 결과는 다음과 같습니다:
Model: gemma3:4b cases: 14
Classification: 7/14 (50%)
...
결과는 두 가지 상반된 방향을 가리켰습니다.
모델은 분류에 있어 신뢰할 수 없었습니다. 50%의 점수를 받았고, 14개 조항 중 13개를 yes로 라벨링했습니다. 이는 연차 휴가와 같은 평범한 조항마저도 사이드 수입과 관련 있다고 플래그를 지정할 수 있음을 의미합니다.
하지만 이 테스트에서는 원문 텍스트를 복사하는 데는 신뢰할 수 있었습니다. 14개 인용구 모두 원래의 문구와 일치했습니다. 이러한 예시들 전반에 걸쳐, 모델은 의역하거나 새로운 문장을 만들어내지 않았습니다.
그래서 저는 제품에서 분류(classification) 기능을 제거하고 추출(extraction) 기능만 유지했습니다. 대체된 부분은 src/flags.js이며, 이는 패턴 매칭을 사용하여 특정 속성을 식별하고 각 속성을 유발한 구절을 인용합니다.
예시에는 다음이 포함됩니다:
- 명백한 금지를 선언하기보다 허가를 요구하는 경우:
플래그가 여전히 틀릴 수는 있지만, 그 증거는 눈에 보입니다. 정확한 구문을 검사하고 플래그가 말이 되는지 결정할 수 있습니다.
특히 권한의 구분은 중요합니다. 서면 동의를 요구하는 조항과 전반적인 금지는 반드시 같지 않습니다. 이 두 상황을 동일하게 취급하면, 필요한 승인을 받은 후에도 진행할 수 있었던 프로젝트를 포기하도록 만들 수 있습니다.
평가 결과는 또한 '예 -> 아니오' 오류가 0건임을 보여주었습니다. 모델은 이 소규모 테스트 세트에서 구속력 있는 조항을 무해하다고 분류하지 않았습니다. 이 사용 사례에서는 제한적인 조항을 놓치는 것이 누군가에게 추가 조항 검토를 요청하는 것보다 더 손상을 줄 수 있습니다.
여기서 오픈소스 AI가 중요한 이유
계약서는 사용자 기기에 남아 있어야 합니다
이것이 제가 로컬 모델을 선택한 주된 이유입니다.
클라이언트의 계약서를 호스팅되는 API에 업로드하는 것은 기밀성 위험을 초래할 수 있습니다. 고용 계약서에는 개인의 급여, 통지 기간, 고용주, 그리고 그들이 걱정하는 정확한 의무가 담겨 있을 수 있습니다. 단순히 한 조항을 이해하기 위해서라도 그 정보를 제3자에게 보내고 싶지 않을 수 있습니다.
Moonlight의 경우, 오픈 가중치(open-weight) 모델을 로컬에서 실행하는 것은 단순한 비용 최적화가 아닙니다. 그것은 제품의 개인 정보 보호 설계 일부입니다.
저는 애플리케이션이 단순히 사적이라고 주장하는 것 이상으로 외부 네트워크 연결을 수행하고 있지 않은지 확인하고 싶었습니다. src/airgap.js는 앱이 가져와지기 전에 net.Socket.prototype.connect, tls.connect, 그리고 dns.lookup를 감시(instrument)합니다. 이는 완전한 검토를 실행하며, 루프백 호스트가 아닌 곳으로 연결을 감지하면 실패합니다. DNS 조회도 계산에 포함되는데, 왜냐하면 조회만 해도 리졸버에게 정보를 노출할 수 있기 때문입니다.
다음은 한 번의 실행 결과입니다:
$ npm run airgap
관찰된 아웃바운드 연결: 7
...
이 테스트는 해당 실행 중에 관찰된 네트워크 활동을 확인합니다. 이는 실질적인 안전장치이며, 어떤 소프트웨어도 단 하나의 테스트만으로 안전하다고 증명될 수 있다는 주장은 아닙니다.
오픈 가중치가 실패를 눈에 보이게 했습니다
모델이 로컬에서 실행되기 때문에, 설정을 변경하여 더 작은 모델을 시도해 볼 수 있었습니다. gemma3:1b는 815 MB 정도이며, gemma3:4b의 3.3 GB와 비교했을 때 테스트 결과 15% 더 빨랐습니다. 저는 이 모델로 동일한 평가 세트를 실행했습니다.
Model: gemma3:1b cases: 14
Classification: 6/14 (43%)
...
이 작은 모델은 두 가지 다른 방식으로 실패했습니다.
긴 절(clause)의 경우, 때때로 예상되는 출력 형식을 따르지 않고 중단되었습니다. 다섯 개의 키를 요청했을 때, 하나만 반환했습니다:
{"governs_side_income":"yes"}
11개의 토큰을 생성하고 done_reason: stop으로 멈췄습니다. 인용할 내용이 없었기 때문에 Moonlight는 해당 절을 **위치 파악(located)했지만, 읽지 못함(not read)**으로 표시하고 원본 텍스트를 보여주었습니다. 결과는 불완전했지만, 앱은 이를 성공적인 읽기로 제시하지 않았습니다.
짧은 절의 경우, 때때로 중요한 한정 조건을 제거하여 인용문을 지어냈습니다. 외부 업무에 관한 조항을 받았을 때, 다음과 같이 생성했습니다:
"직원은 보수 여부와 관계없이 다른 고용, 컨설팅, 무역 또는 사업에 종사하지 않는다"
하지만 계약서에는 실제로 다음과 같이 쓰여 있었습니다:
"직원은 회사의 사전 서면 동의 없이, 다른 고용, 컨설팅, 무역 또는 사업에 종사하지 않는다"
모델은 동의 요건을 생략했습니다. 이 단어들이 조항의 의미를 바꿉니다: 첫 번째 버전은 명백한 금지처럼 들리는 반면, 원본은 사전 서면 동의가 조건의 일부임을 만듭니다.
출력 자체는 유창했지만, 출처에 충실하지 않았습니다. 이것이 바로 계약 요약문을 읽는 사람을 오도할 수 있는 종류의 실수입니다.
인용문 검증(quote-verification) 기능이 이를 잡아냈는데, 생성된 문장이 원본 텍스트에 존재하지 않았기 때문입니다. 저는 이 기능을 정당화하기 위해 가상의 실패를 만들 필요가 없었습니다. 그것은 제가 작은 모델로 테스트한 첫 번째 절에서 나타났습니다.
이것이 모델을 직접 실행하고 평가할 수 있다는 장점 중 하나입니다. 저는 모델을 전환하고, 동일한 테스트를 실행하며, 단순히 모델 카드나 벤치마크에 의존하기보다 실패 지점을 검사할 수 있었습니다. 오픈 웨이트(Open weights) 덕분에 동작을 검사할 수 있게 되었고, 이것이 Moonlight의 설계에 직접적인 영향을 주었습니다.
나의 에이전트 세션
설계 변경을 가져온 실험들을 포함한 전체 빌드는 여기에서 확인할 수 있습니다:
Moonlight 구축하기: 모델의 판단(judgement) claude-opus-5를 삭제하게 만든 평가
당신
저는 Hacktoberfest DEV Challenge에 참가했습니다. 2주차 프로젝트를 제출하고 싶습니다.
에이전트