만들어 보기 · 글 11편
만들어 보기이 글에서 다루는 것CSV 파일을 올리면 로컬 모델이 행마다 분류와 한 줄 요약을 붙여 주고, 결과를 다시 CSV로 내려받는 HTML 도구를 만듭니다.
무엇을 만드나
CSV 파일을 끌어다 놓으면 표로 보여 주고, 분류 시작을 누르면 행을 하나씩 모델에 보내 담당 부서, 유형, 한 줄 요약 세 칸을 채워 주는 도구입니다. 끝나면 원래 열에 세 열을 붙인 CSV를 내려받습니다.
이럴 때 쓸모가 있습니다.
- 접수된 문의나 의견 수십~수백 건을 미리 나누어 담당을 정할 때
- 분류 기준(부서 목록, 유형 목록)이 자주 바뀔 때
- 내용에 개인 사정이 적혀 있어 외부 서비스에 올리기 어려울 때
준비물
| 항목 | 내용 |
|---|---|
| Ollama | 실행 중, 모델 하나 이상 |
| Python | 화면 파일을 띄우는 데만 씁니다. 추가 패키지 없음 |
| 브라우저 | 크롬, 엣지 등 |
구조
브라우저(HTML 한 파일)
├─ CSV 읽기·표 그리기·결과 내려받기 : 브라우저 안에서 처리
└─ 행마다 POST http://localhost:11434/api/generate
serve.py : 이 HTML을 http://localhost:8080 으로 띄워 줌
모델을 부르는 것은 HTML 쪽입니다. Python은 HTML 파일을 내 PC 주소로 열어 주는 일만 합니다. HTML을 더블클릭해 file://로 열면 브라우저 보안 정책 때문에 Ollama 호출이 막힐 수 있어서 이렇게 합니다.
| 부품 | 역할 |
|---|---|
| CSV 읽기 | 쉼표와 탭 구분을 자동으로 가리고, 따옴표로 묶인 칸을 처리합니다 |
| 설정 | 분류할 열, 부서 목록, 유형 목록을 화면에서 고칩니다 |
| 분류 반복 | 행마다 프롬프트를 만들어 모델에 보내고 답을 해석합니다 |
| 내려받기 | 원래 열 + 결과 세 열을 CSV로 저장합니다 |
에이전트에게 줄 요청
CSV 일괄 분류 도구를 HTML 한 파일로 만들어 줘. 외부 라이브러리와 CDN은 쓰지 않는다.
- CSV를 끌어다 놓거나 선택하면 표로 보여 준다. 쉼표/탭 구분 자동 판별, 따옴표 처리.
- 분류할 내용이 든 열을 고르게 한다.
- 부서 목록과 유형 목록은 화면에서 한 줄에 하나씩 고칠 수 있다.
- 시작하면 행을 하나씩 http://localhost:11434/api/generate (stream=false)에 보낸다.
프롬프트에 목록과 예시 3개를 넣고, 답에서 목록에 있는 낱말만 골라 채운다.
- 진행률 표시, 중단 버튼, 이미 채운 행은 건너뛰기.
- 결과는 AI_부서, AI_유형, AI_요약 열을 붙여 CSV로 내려받는다. 엑셀에서 한글이 깨지지 않게 BOM을 붙인다.
- serve.py: 표준 라이브러리만으로 이 HTML을 127.0.0.1:8080에 띄우고 브라우저를 연다.
포트가 쓰이고 있으면 다음 번호를 시도한다.
핵심 코드
프롬프트는 “목록 안에서만 고르라”는 지시, 예시, 그리고 답을 이어 쓰게 만드는 끝맺음으로 이루어집니다. 마지막 줄을 부서:로 끝내면 모델이 그 형식을 이어서 씁니다.
const prompt = `민원을 분류하세요. 반드시 아래 부서와 유형 중에서만 고르세요.
부서: ${deptsStr}
유형: ${typesStr}
예시1)
민원: 쓰레기 불법 투기가 심합니다.
부서: 환경과
유형: 신고
요약: 쓰레기 불법 투기 단속 요청
(예시2, 예시3 생략)
민원: ${content}
부서:`;
답은 그대로 믿지 않습니다. 목록의 낱말이 답 안에 들어 있는지 찾아서 그 낱말만 칸에 넣습니다. 목록에 없는 부서를 지어내도 표에는 들어가지 않습니다.
const res = await fetch(OLLAMA+"/api/generate",{
method:"POST",headers:{"Content-Type":"application/json"},
body:JSON.stringify({model:currentModel,prompt,stream:false})
});
const data = await res.json();
const answer = (data.response||'').trim();
let dept='-',type='-',summary='-';
for(const d of depts){if(answer.includes(d)){dept=d;break}}
for(const t of types){if(answer.includes(t)){type=t;break}}
모델 목록을 가져올 때는 대화용이 아닌 임베딩 모델을 이름으로 걸러 냅니다.
const skip = ["embed","embedding","bge-","snowflake"];
const models = (data.models||[]).filter(m => !skip.some(k => m.name.toLowerCase().includes(k)));
화면을 띄우는 serve.py는 표준 라이브러리만 씁니다.
port = PORT
server = None
for attempt in range(10):
try:
server = http.server.HTTPServer(("127.0.0.1", port), QuietHandler)
break
except OSError:
port += 1
실행하고 확인하기
python serve.py를 실행합니다. 브라우저가 열립니다.- 위쪽 표시가 Ollama 연결 상태와 모델 수를 보여 주는지 확인합니다.
- 직접 만든 시험용 CSV 10행쯤을 올립니다. 실제 자료를 쓰기 전에 지어낸 예문으로 먼저 봅니다.
- 분류할 열을 고르고, 부서와 유형 목록을 내 기준에 맞게 고칩니다.
- 분류 시작을 누르고 행이 하나씩 채워지는지 봅니다.
- 중간에 중단을 눌렀다가 다시 시작해, 채워진 행을 건너뛰는지 확인합니다.
- CSV를 내려받아 엑셀에서 열고 한글이 깨지지 않는지 봅니다.
정확도는 직접 재야 합니다. 20~30행을 사람이 먼저 분류해 두고 도구의 결과와 맞춰 봅니다. 자주 틀리는 유형이 보이면 그 사례를 예시에 넣습니다.
자주 생기는 문제
| 증상 | 원인과 해결 |
|---|---|
| “Ollama 연결실패” | HTML을 더블클릭해 열었거나 Ollama가 꺼져 있습니다. serve.py로 엽니다 |
칸이 -로 채워짐 |
답에 목록의 낱말이 없었습니다. 목록 표기와 예시 표기를 같게 맞춥니다 |
| 부서가 엉뚱함 | 목록에 이름이 서로 포함되는 항목이 있으면 앞의 것이 먼저 잡힙니다. 이름을 구분되게 고칩니다 |
| 한 행이 오래 걸림 | 내용이 아주 긴 행입니다. 앞부분 일정 길이만 보내도록 고칩니다 |
| 열이 밀려 보임 | 칸 안에 줄바꿈이나 쉼표가 있는데 따옴표로 묶이지 않은 CSV입니다. 엑셀에서 다시 CSV로 저장합니다 |
확장하기
- 확인 열 추가: 답에 목록 낱말이 없어
-가 된 행만 모아 보여 주는 거름 단추를 답니다. 사람이 볼 행이 줄어듭니다. - 프롬프트 화면 편집: 예시를 화면에서 고칠 수 있게 해, 코드를 건드리지 않고 분류 기준을 다듬습니다.
- 두 번 물어 비교: 같은 행을 두 번 분류해 결과가 다르면 표시합니다. 모델이 망설이는 행을 찾는 방법입니다.
- 엑셀 파일 직접 읽기:
.xlsx를 읽으려면 별도 라이브러리가 필요하므로, 외부 연결 없이 쓰려면 파일을 프로젝트 안에 넣어 두게 합니다.
엑셀 안에서 같은 분류를 수식으로 하려면 엑셀에서 로컬 모델을 함수로 부르기를 봅니다.