dimesum

/ 블로그 / 엔지니어링

엔지니어링

우리는 자연어 지출 파서를 삭제했습니다

· 7분 소요 ·

규칙 파서는 일주일을 버텼고, 평범한 네 문장이 돈을 엉뚱한 사람에게 붙였으며, 하나의 거절 규칙이 방어 코드 전체를 대체했습니다.

막 완성한 파서를 삭제했습니다. Dimesum이 자연어 지출 입력에 처음 시도한 방식은 규칙 엔진이었고, 평범한 네 문장이 그것을 은퇴시켰습니다. dinner 12-08 17500은 ₩12로 읽혔고, ravioli 26250은 Ravindra라는 이름의 멤버를 정산에 넣었으며, kirana 21875는 Kiran이라는 멤버에게 비용을 물렸고, refund -21150은 ₩21,150 청구로 바뀌었습니다.

문장을 이해하는 일은 오로지 모델의 몫입니다. 규칙 엔진을 대체한 것은 amount_only라는 하나의 단계입니다. 텍스트에 모호하지 않은 금액 후보가 정확히 하나 있을 때에만 숫자 하나를 돌려주며, 사람에 관해서는 어떤 주장도 하지 않습니다. 후보가 둘이면 금액은 없습니다. 규칙 하나가 계속 쌓여만 가던 방어 코드 더미를 대체했습니다.

네 문장이 규칙 파서를 끝냈습니다

삭제한 파서는 AI 설계 문서가 규정한 엔터티 집합 전체를 다뤘습니다. 멤버와 별명 매칭, 제외 어휘 목록, 결제자 추론, 카테고리 어휘 목록, 그리고 필드별로 조정된 신뢰도까지 말입니다. 아래 네 건의 실패에는 각각 뻔한 수정책이 있었습니다. 그리고 그 수정책 하나하나가 저마다 사각지대를 지닌 새 방어 코드였습니다.

Dimesum의 규칙 파서를 은퇴시킨 네 건의 실패로, 2026-08-20에 docs/tech/10-ai-design.md에 기록되었습니다.
사용자가 입력한 것파서가 한 일왜 그랬는가
dinner 12-08 17500금액을 ₩12로 읽음사람은 날짜와 총액을 봅니다. 정규식은 숫자 세 개를 보고 첫 번째를 집습니다.
ravioli 26250Ravindra라는 멤버를 정산에 넣음흐릿한 별명 매칭이 음식 이름을 사람으로 점수 매겼습니다.
kirana 21875Kiran이라는 멤버에게 청구함같은 매처가 이번에는 가게 이름을 물고 늘어졌습니다.
refund -21150₩21,150 청구를 기록함숫자는 살아남고 부호는 사라져서, 돈이 반대 방향을 가리켰습니다.

넷 중 둘은 옷만 갈아입은 하나의 버그입니다. 흐릿한 매칭은 음식을 사람과, 가게를 사람과 구분하지 못합니다. 글자 단위로 보면 ravioliRavindra는 정말로 닮았기 때문입니다. 더 긴 접두사 일치를 요구하면 ravi가 깨지는데, 바로 그 경우를 위해 매처가 존재합니다.

방어 코드를 하나 더할 때마다 표현 두 개가 새로 튀어나옵니다

규칙 파서는 한 가지 특정한 형태로 실패합니다. 자신 있게, 그리고 틀리게 답합니다. 빈 필드는 사용자에게 탭 한 번의 비용입니다. 잘못된 정산은 장부에 대한 신뢰를 잃게 하고, 비용 분담 추적 앱에서 장부는 곧 제품입니다. 위의 네 줄은 아슬아슬한 실수가 아니라 돈 버그입니다.

진짜 논거는 어느 한 결함이 아니라 이 끝없는 쳇바퀴입니다. 날짜 방어 코드를 더하면 주문 번호 경우가 찾아옵니다. 주문 번호 방어 코드를 더하면 단순 숫자가 오고, 이어서 수량이, 그다음엔 테이블 번호가 옵니다. 방어 코드 목록은 늘어나기만 하고 결코 수렴하지 않습니다. 자연어에는 열거할 수 있는 유한한 표현 집합이 없기 때문입니다.

이전과 이후: 늘어나는 방어 코드 더미가 하나의 거절 규칙으로 대체됨 이전: 규칙 파서 (삭제됨) dinner 12-08 17500 금액 정규식 날짜 방어 코드 주문 번호 방어 코드 별명 매처 제외 어휘 목록 ₩12로 읽고, 그대로 기록함 수정할 때마다 표현 두 개가 더 튀어나왔습니다. 이후: amount_only dinner 12-08 17500 모호하지 않은 금액 후보가 정확히 하나인가? 아니오, 숫자 세 개 금액은 빈 채로 둠 예, 숫자 하나 최소 단위로 반환 이 단계는 사람에 관해 아무 말도 하지 않으므로, 돈을 엉뚱한 사람에게 붙일 수 없습니다.
삭제한 파서는 모든 문장에 답했습니다. 그것을 대체한 단계는 숫자 하나로 답하거나 아무 답도 하지 않습니다.
결정, 날짜와 함께

자연어 입력은 AI 기능이며, Dimesum 저장소의 어떤 것도 여기에 패턴이 들어맞지 않습니다. 이 결정은 2026-08-20에 내려졌고 네 건의 실패와 함께 기록되어, 누구도 실수로 방어 코드를 다시 쌓지 않도록 했습니다.

실제로 나온 것은 숫자 하나와 거절 하나입니다

amount_only은 우리 AI 설계 문서의 저하 단계를 문자 그대로 구현한 것입니다. 그 단계는 "클라이언트 측 금액 정규식으로 미리 채운 일반 양식"이라고 적혀 있어서, 이 계층은 금액 하나만 돌려주고 그 밖에는 아무것도 내지 않습니다. 설명도, 카테고리도, 결제자도, 참여자도, 제외 대상도 없습니다. 비용이 들지 않고 아무도 호출하지 않으며, 그래서 테스트와 CI가 이 계층 위에서 돕니다.

모호함은 거절이지 무승부 판정이 아닙니다

규칙 전체는 extract_amount_minor라는 함수 하나에 담겨 있습니다. 텍스트에 후보가 정확히 하나 있을 때에만 숫자가 돌아오므로, order 90210 dinner 17500flat 402 rent 656250은 승자를 고르는 대신 빈 값을 돌려줍니다. 통화 표시가 붙은 숫자는 맨숫자 곁에 있어도 모호하지 않은 것으로 치며, 그래서 split 3 ways ₩52,500은 여전히 ₩52,500으로 읽힙니다.

부호가 붙어 음수가 된 숫자는 절댓값이 아니라 아예 금액 없음으로 처리됩니다. -21875, minus 8750, 그리고 회계사식 표기인 (21875)은 모두 빈 값으로 돌아옵니다. 이 필드는 청구이고, 숫자는 남기면서 부호를 버리면 돈이 텍스트와 반대 방향을 가리키기 때문입니다. 괄호는 숫자 자체를 감쌀 때에만 인정되므로, (21875 each)은 그냥 괄호 표현으로 남습니다.

amount_only이 숫자 하나와 무응답 사이에서 결정하는 방법 돈이 될 수 있는 모든 숫자를 모음 하나도 없거나, 그중 하나라도 음수 부호가 붙었는가? 아니오 ₩52,500이나 500/- 같은 통화 표시 숫자가 정확히 하나인가? 아니오 통화 표시가 전혀 없고, 맨숫자가 정확히 하나인가? amount_minor: 정수 최소 단위로, ISO 4217 지수를 통해 아니오 금액 없음 필드가 빈 채로 렌더링됨
질문 셋 중 둘은 빈 값으로 끝납니다. 그럴듯한 금액 두 개 사이에서 고르려던 것이 ₩12짜리 저녁을 낳았습니다.

산술을 결정하는 것은 통화입니다

Dimesum에서 돈이 취하는 유일한 표현은 최소 단위이므로, 이 계층은 100을 곱하는 하드코딩 대신 ISO 4217 지수 표로 변환합니다. 일본 엔은 하위 단위가 아예 없어서, ×100은 ¥1,200 영수증을 백 배로 부풀립니다. 통화의 최소 단위보다 더 잘게 쪼갠 숫자는 반올림하지 않고 거절합니다. 누군가 입력한 금액을 반올림하는 것은 없는 금액을 지어내는 일이기 때문입니다.

인도식 숫자 표현은 숫자를 쓰는 방식의 일부이지 문장을 이해하는 일의 일부가 아닙니다. 1.2k, 2 lakh, 500/-1,200처럼 모두 해석됩니다. 장부의 최대 금액을 넘어서는 값은 빈 값으로 돌아오므로, 잘못 입력한 숫자는 하류에서 정수를 넘치게 하는 대신 필드 하나만 비웁니다.

각 파싱 단계가 주장해도 되는 것, 2026년 8월 29일 기준.
필드규칙 파서 (삭제됨)amount_only (가동 중)모델 계층 (연결됨, 키 없음)
금액여러 숫자에서 추측함모호하지 않은 숫자 하나, 아니면 빈 값문맥 속에서 읽음
설명, 카테고리어휘 목록 매칭항상 null문장에서 추출함
참여자, 제외 대상흐릿한 이름 매칭항상 비어 있음실제 멤버 id로 해석됨
결제자표현에서 추론함항상 비어 있음이름을 지정하며, 금액은 null 가능
전체 신뢰도필드별로 조정됨0.3으로 고정파싱마다
보고된 프롬프트존재하지 않음null, 프롬프트를 읽지 않음프롬프트 id와 버전

가동 중인 단계는 사용 가능 기준선 0.6을 결코 넘지 못합니다

우리 파싱 계약은 전체 신뢰도가 0.6 미만인 파싱을 포기하고 사용자를 일반 양식으로 내립니다. amount_only은 모든 답에 0.3을 보고하며, 이 상수는 조정된 값이 아니라 구조적인 값입니다. 금액 하나만으로는 파싱이 아니므로, 이 단계는 무엇을 찾았든 기준선의 절반에 앉아 있습니다. 모든 답에 같은 상수 하나를 쓰는 것이 그 자리를 지키는 방법입니다. 경우마다 매기는 점수는 언젠가 누군가 위로 슬쩍 올리는 점수이기 때문입니다.

이 계층은 프롬프트도 전혀 보고하지 않습니다. prompt_idprompt_version은 둘 다 null로 돌아오는데, 이 단계가 프롬프트를 하나도 읽지 않았기 때문입니다. 아무 프롬프트나 이름 붙이면 모든 평가 결과를 이 계층이 본 적 없는 프롬프트 버전에 귀속시키게 됩니다. 그리고 어떤 단계를 원탭 제안으로 승격할 수 있는 유일한 도구는 평가 하니스이며, 금액과 참여자를 합쳐 95% 정밀도에서만 그렇게 합니다.

추가로 두 계층이 선언되어 있지만 둘 다 가동 중이 아닙니다. cheap-fast 계층은 openai/gpt-oss-120b용 Groq 어댑터가 있고 키는 없습니다. 중간 계층은 어댑터가 없습니다. 둘 중 어느 쪽을 선택하든 사용자의 첫 요청 때가 아니라 시작 시점에 실패합니다. LLM은 호출마다 요금이 부과되고, 요금이 나가는 의존성은 닫힌 채로 실패해야 하기 때문입니다.

무엇도 자동으로 기록되지 않으므로, 빈 값은 탭 한 번의 비용입니다

빈 필드의 비용이 이토록 적은 것은 오직 Dimesum의 어떤 캡처도 돈을 기록할 수 없기 때문입니다. 캡처는 제안을 만들고, 사람이 그것을 확인하며, 지출을 만드는 것은 바로 그 확인입니다. Brief 결정 D5가 이 규칙을 명시하고, .go-arch-lint.yml이 이를 강제합니다. 수집 컨텍스트는 expense나 ledger에 대한 어떤 의존성도 거부당하므로, 캡처는 실수로라도 저널을 기록할 수 없습니다. CI가 그 import를 실패시키며, 우리는 하나를 추가해 이를 확인했습니다.

캡처는 파서가 무엇을 하든 원문을 그대로 보존하고, 어떤 필드가 아직 해석되지 않았는지 밝힙니다. 클라이언트는 지어낸 초안을 보여주는 대신 그 빈 곳을 강조하는데, 이것이 아무 말도 하지 않는 파서와 추측하는 파서의 차이입니다. 확인은 자신의 expense id를 suggestion id에서 파생하므로, 두 번 탭해도 두 번 청구되는 대신 같은 동작이 되풀이됩니다.

훔쳐 갈 만한 규칙

버그가 아니라 방어 코드를 세십시오. 매주 늘어나는 방어 코드 목록은 그 일이 이해의 문제임을, 그리고 이해는 모델의 몫임을 말해 줍니다. 다음 단계는 contracts/parse_expense/eval/의 골든 세트를 실제 모델 계층에 돌려 보는 것입니다. 그 판정 없이는 여기 어떤 것도 원탭 제안이 되지 못하기 때문입니다.

자주 묻는 질문

Dimesum은 왜 규칙 기반 지출 파서를 삭제했나요?

네 개의 평범한 문장이 돈 버그를 냈고, 방어 코드를 더할 때마다 표현이 두 개씩 더 튀어나왔기 때문에 Dimesum은 이를 삭제했습니다. dinner 12-08 17500은 ₩12로 읽혔고, ravioli 26250은 Ravindra라는 멤버를 추가했으며, kirana 21875는 Kiran이라는 멤버에게 청구했고, refund -21150은 ₩21,150 청구가 되었습니다. 문장을 이해하는 일은 모델의 몫입니다.

amount_only 단계는 실제로 무엇을 돌려주나요?

amount_only 단계는 숫자 하나만 돌려주고 그 밖에는 아무것도 내지 않습니다. 텍스트에 모호하지 않은 금액 후보가 정확히 하나 있을 때에만 금액으로 답하며, 참여자도 결제자도 설명도 카테고리도 결코 지정하지 않습니다. 후보가 둘이면 금액은 아예 없습니다. 파싱 계약의 나머지는 모두 모델 계층을 기다립니다.

amount_only은 왜 실제 점수 대신 0.3 신뢰도를 보고하나요?

0.3은 조정된 값이 아니라 구조적인 값입니다. 우리 파싱 계약은 신뢰도 0.6 미만인 파싱을 포기하고 사용자를 일반 양식으로 내리는데, 금액 하나만으로는 파싱이 아니므로 이 단계는 무엇을 찾았든 그 기준선의 절반에 앉아 있습니다. 모든 답에 같은 고정 상수 하나를 쓰면 경우마다 매긴 점수를 나중에 위로 슬쩍 올리는 일을 막을 수 있습니다.

자연어 캡처가 사람 없이 장부에 기록할 수 있나요?

아니요. 캡처는 제안을 만들고 사람이 그것을 확인합니다. Brief 결정 D5에 따른 것입니다. 이 규칙은 .go-arch-lint.yml에서 강제되며, 수집 컨텍스트가 expense나 ledger에 의존하는 것을 거부하므로 캡처가 저널에 손을 뻗으면 CI가 그 import를 실패시킵니다. 지출을 만드는 것은 확인입니다.

모델 계층을 쓸 수 없을 때 자연어 지출 파싱은 어떻게 되나요?

Dimesum은 amount_only으로 저하되어 빈 값을 보여줍니다. cheap-fast 계층은 Groq의 openai/gpt-oss-120b에 연결되어 있지만 키 없이 배포되고, 중간 계층은 어댑터가 없어서, 둘 중 하나를 선택하면 사용자의 첫 요청 때가 아니라 시작 시점에 실패합니다. 어느 쪽이든 캡처는 원문을 그대로 보존합니다.