왼쪽 아이콘은 증빙 종류입니다. 오른쪽 막대는 에이전트가 매긴 신뢰도이고, 기준을 넘지 못한 것은 주황이 아니라 노랑으로 칠해 사람이 볼 것임을 표시합니다.
글자를 읽지 못했습니다
뽑아낸 것
증빙 한 장이 장부에 들어가기까지 지나는 일곱 단계
평균 8.4초 · 건당 ₩41거래처가 보내는 경로가 세 갈래입니다 — 메일 첨부, 카톡 사진, 사무실 스캐너. 사람에게 한 곳으로 보내 달라고 하지 않았습니다. 그렇게 부탁하면 결국 안 지켜지고, 안 지킨 건이 조용히 빠집니다.
mail.fetchkakao.filesscanner.watch이미지에서 글자와 표를 뽑습니다. 여기서 실패하는 건이 전체의 3.1% 입니다 — 사진이 흐리거나, 반이 잘렸거나, 영수증이 접혀 있습니다. 실패하면 모델에게 억지로 추측시키지 않고 그대로 사람에게 올립니다.
ocr.readtable.detect공급자·사업자번호·일자·금액·품목을 정해진 모양으로 받습니다. 모델이 아무 모양이나 뱉지 못하게 스키마로 먼저 막습니다. 그리고 값마다 얼마나 확신하는지를 같이 받습니다 — 이게 없으면 어느 칸을 봐야 할지 모릅니다.
// extract/schema.ts — 모델이 아무 모양이나 뱉지 못하게 먼저 막는다export const Slip = z.object({ kind: z.enum(["세금계산서", "카드전표", "영수증"]), vendor: z.string().min(1), bizNo: z.string().regex(/^\d{3}-\d{2}-\d{5}$/), date: z.string().date(), item: z.string(), supply: z.number().int().nonnegative(), vat: z.number().int().nonnegative(), total: z.number().int().positive(), // 값마다 얼마나 확신하는지 같이 받는다. 이게 없으면 어디를 봐야 할지 모른다. fieldConfidence: z.record(z.number().min(0).max(1)),}); // 스키마를 못 맞추면 두 번까지 다시 시킨다. 세 번째는 사람에게 올린다.const slip = await callWithSchema(Slip, prompt, { retries: 2 });if (!slip.ok) return toHuman(doc, "구조를 못 맞춤: " + slip.error); // 합계가 안 맞으면 모델이 뭐라 하든 사람에게 올린다if (slip.data.supply + slip.data.vat !== slip.data.total) { return toHuman(doc, "공급가액 + 부가세 ≠ 합계");}
모델에게 묻지 않아도 되는 것은 묻지 않습니다. 사업자번호가 실재하는지, 휴폐업은 아닌지, 같은 승인번호가 이미 들어와 있는지는 조회하면 답이 나오는 일입니다. LLM 을 쓸 자리가 아닙니다.
biz.lookupdup.checkvendor.rules여기가 사람의 판단을 넘기는 자리입니다. 과거에 이 거래처가 같은 공급자를 어떻게 처리했는지 5건을 찾아 함께 넣습니다. "알아서 판단해" 가 아니라 "이 사무소가 해 온 대로 판단해" 입니다.
history.searchclaude.messages# 지시너는 세무사무소의 매입 증빙을 계정과목으로 분류한다.확신이 없으면 억지로 고르지 말고 confidence 를 낮게 매겨라.낮게 매긴 건은 사람이 본다. 틀리게 확신하는 것보다 낫다. # 입력증빙: {{slip}}거래처가 이 공급자와 예전에 한 거래 5건: {{history}}사무소가 이 거래처에 적용해 온 규칙: {{rules}} # 규칙- 접대비 / 기업업무추진비로 볼 여지가 있으면 confidence 를 0.8 아래로 둔다.- 간이영수증, 면세사업자, 비영업용 승용차 관련은 부가세 불공제로 표시한다.- 과거 5건 중 3건 이상이 같은 계정과목이면 그것을 우선한다. 다르면 이유를 적는다.- 품목이 여러 개면 금액이 가장 큰 품목으로 판단하고, 나머지를 note 에 남긴다. # 출력{ "account": "...", "confidence": 0.0~1.0, "deductible": true|false, "alternatives": [{ "account": "...", "p": 0.0~1.0 }], "reasons": ["..."], "note": "..." }
신뢰도가 기준을 넘고, 가드레일에 하나도 안 걸리면 자동으로 넘어갑니다. 가드레일은 신뢰도와 무관합니다 — 접대비나 100만원 초과는 모델이 99% 확신해도 사람이 봅니다. 틀렸을 때 드는 비용이 다르기 때문입니다.
// route.ts — 자동으로 넘길지 사람에게 올릴지 정하는 곳const GUARDS = [ { when: (d) => d.account === "접대비", why: "불공제 판단이 걸려 있음" }, { when: (d) => d.total > 1_000_000, why: "금액이 큼" }, { when: (d) => d.vendorIsNew, why: "처음 보는 거래처" }, { when: (d) => d.duplicateOf != null, why: "중복 의심" }, { when: (d) => !d.deductible, why: "부가세 불공제 판단" },]; export function route(d, gate) { // 신뢰도가 아무리 높아도 이 목록에 걸리면 사람이 본다 const hit = GUARDS.find((g) => g.when(d)); if (hit) return { to: "human", why: hit.why, guard: true }; if (d.confidence * 100 < gate) return { to: "human", why: "신뢰도 미달" }; return { to: "auto" }; // 넘긴 뒤에도 되돌릴 수 있고, 기록은 남는다}
회계 프로그램에 전기하고, 누가·무엇을 근거로 정했는지를 같이 남깁니다. 반년 뒤 세무조사에서 "이건 왜 여비교통비예요" 라는 질문을 받습니다. 그때 답할 수 있어야 합니다.
ledger.postaudit.write일을 조각내고, 조각마다 따로 정합니다
세 달 전과 지금
| 단계 | 예전 | 지금 |
|---|---|---|
| 모아서 정리 | 42초 | 0초 |
| 읽고 옮겨 적기 | 1분 38초 | 0초 |
| 계정과목 정하기 | 1분 04초 | 0초 |
| 중복·휴폐업 확인 | 28초 | 0초 |
| 사람이 보고 승인 | 20초 | 47초 |
| 합계 | 4분 12초 | 47초 |
사람이 보는 시간은 오히려 늘었습니다. 예전에는 자기가 적었으니 20초면 됐지만, 지금은 남이 정한 것을 확인하는 일이라 근거를 읽어야 합니다. 그래서 근거를 잘 보여주는 것이 이 프로젝트의 절반이었습니다.
할 수 있어도 안 하는 자리
실패는 지우지 않고 남깁니다. 남겨야 고칠 수 있습니다
재시도하고도 안 된 것만
무한정 다시 시키지 않습니다
2s · 8s · 30s 로 3회 다시.
그래도 안 되면 조회 없이 분류하되 반드시 사람에게 올립니다.고치고 끝내지 않습니다 — 고친 것이 어디로 가는지까지 만들어 둡니다
사람이 손으로 라벨을 붙인 200건으로 매주 잽니다
전체 94.5%| 계정과목 | 건수 | 정확도 | 신뢰도 평균 | 주로 틀리는 쪽 |
|---|---|---|---|---|
화면보다 먼저 정한 것들
"이 업무를 자동화할까요" 는 답할 수 없는 질문입니다. 업무는 한 덩어리가 아니라 열두 조각이고
조각마다 답이 다릅니다. 쪼개 놓고 조각마다 따로 정합니다.
덩어리로 맡기면 틀렸을 때 덩어리로 되돌리게 됩니다.
맞으면 아무 일도 없고 틀리면 가산세가 나오는 자리입니다. 이 비대칭이 있는 곳은 사람이 지킵니다. 대신 사람이 봐야 할 것만 보게 만드는 데 힘을 씁니다 — 16건을 대충 넘기는 것보다 4건을 제대로 보는 편이 낫습니다.
모델에게 답만 받지 않고 얼마나 확신하는지를 값마다 함께 받습니다. 그리고 프롬프트에 이렇게 적어 둡니다 — "확신이 없으면 억지로 고르지 말고 낮게 매겨라. 낮게 매긴 건은 사람이 본다." 틀리게 확신하는 것보다 모른다고 하는 편이 싸게 먹힙니다.
접대비, 100만원 초과, 처음 보는 거래처, 중복 의심, 부가세 불공제 판단 —
이 다섯은 모델이 99% 확신해도 사람이 봅니다.
확률이 아니라 틀렸을 때의 비용으로 정한 목록이기 때문입니다.
사업자번호가 실재하는지, 휴폐업인지, 같은 승인번호가 이미 있는지는 조회하면 답이 나옵니다. 여기에 LLM 을 쓰면 비싸고 느리고 가끔 틀립니다. 모델은 사람이 매번 똑같이 하던 판단에만 씁니다.
사람이 확인하는 시간은 오히려 늘었습니다. 예전에는 자기가 적었으니 20초였는데, 지금은 남이 정한 것을 확인하는 일이라 47초가 걸립니다. 그래서 규칙·도구 조회 결과·과거 유사 건을 한 화면에 놓았습니다. 근거가 없으면 사람은 그냥 다 승인하거나 그냥 다 다시 봅니다. 둘 다 자동화가 아닙니다.
고치고 끝내면 다음 달에 똑같이 고칩니다. 세 갈래로 보냅니다 — 같은 상황이 또 오면 규칙, 모델이 헷갈릴 만하면 평가셋, 물어본 방식이 나빴으면 프롬프트.
전체 정확도 94.5% 는 쓸모가 없습니다. 접대비·회의비만 81.2% 이고
나머지는 98%가 넘습니다. 평균을 보면 아무 데도 손대지 않게 됩니다.
잘 못하는 칸을 찾아 거기에만 가드레일을 겁니다.
사람이 하던 때는 못 읽으면 다시 물어봤습니다. 자동이 되면 그냥 넘어갑니다. 그래서 재시도 횟수를 정해 두고, 다 쓰고도 안 되면 반드시 사람에게 올립니다. 모델에게 억지로 추측시키지 않습니다.
모델도 서버도 붙어 있지 않습니다. 판단은 미리 적어 둔 값이고, 새로고침하면 처음으로 돌아갑니다. 실제 프로젝트에서는 이 자리에 OCR, Claude API, 국세청 조회, 회계 프로그램 연동, 그리고 매일 아침 이 화면에서 네 건을 승인하는 사람이 있습니다.