AI 맞춤법 실측 — 문항 60개와 실행 스크립트

AI 맞춤법, 60문항 돌려본 결과에서 쓴 문항 전체와 실행 스크립트입니다. 본문에는 지면상 일부만 실었기 때문에 여기에 전부 공개합니다. 그대로 복사해 같은 실험을 다시 돌리실 수 있습니다.

채점 방식은 단순합니다. 문항마다 오류 표현과 정답 표현을 미리 붙여 두고, 모델 출력에서 오류가 사라지고 정답이 들어갔는지만 봅니다. 사람이 눈으로 판정하지 않습니다.

1차 — 흔한 오류 30문항 + 정상 문장 10문항

31~40번은 고칠 것이 없는 문장입니다. 모델이 멀쩡한 문장을 건드리는지(오검출) 보려고 섞었습니다.

#유형입력(오류 포함)정답
1되/돼이렇게 하면 안 되요.이렇게 하면 안 돼요.
2안/않숙제를 하지 안았다.숙제를 하지 않았다.
3왠/웬오늘은 웬지 기분이 좋다.오늘은 왠지 기분이 좋다.
4왠/웬네가 여기 왠일이야?네가 여기 웬일이야?
5어휘오늘이 몇 일이지?오늘이 며칠이지?
6어휘정말 오랫만에 만났다.정말 오랜만에 만났다.
7어휘소문이 금새 퍼졌다.소문이 금세 퍼졌다.
8사이시옷물건의 갯수를 세었다.물건의 개수를 세었다.
9사이시옷등교길이 막힌다.등굣길이 막힌다.
10사이시옷최대값을 구하시오.최댓값을 구하시오.
11띄어쓰기나는 할수 있다.나는 할 수 있다.
12띄어쓰기서울에 온지 삼 년이 됐다.서울에 온 지 삼 년이 됐다.
13띄어쓰기노력한만큼 결과가 나온다.노력한 만큼 결과가 나온다.
14띄어쓰기그저 웃을뿐이었다.그저 웃을 뿐이었다.
15띄어쓰기이번 일은 그가 한것이 아니다.이번 일은 그가 한 것이 아니다.
16로서/로써학생으로써 본분을 다하자.학생으로서 본분을 다하자.
17로서/로써대화로서 문제를 풀자.대화로써 문제를 풀자.
18어떻게/어떡해이 문제를 어떡해 풀어요?이 문제를 어떻게 풀어요?
19어휘경쟁에서 뒤쳐지지 않으려고 애썼다.경쟁에서 뒤처지지 않으려고 애썼다.
20어휘있다가 다시 연락할게.이따가 다시 연락할게.
21사물존칭주문하신 커피 나오셨습니다.주문하신 커피 나왔습니다.
22사물존칭이 상품은 품절이십니다.이 상품은 품절입니다.
23외래어초콜렛을 선물했다.초콜릿을 선물했다.
24외래어케잌에 초를 꽂았다.케이크에 초를 꽂았다.
25외래어슈퍼마켙에 다녀왔다.슈퍼마켓에 다녀왔다.
26던/든어렸을 때 얼마나 울었든지 모른다.어렸을 때 얼마나 울었던지 모른다.
27던/든가던지 말던지 마음대로 해.가든지 말든지 마음대로 해.
28어휘윗어른께 인사드렸다.웃어른께 인사드렸다.
29활용알맞는 답을 고르시오.알맞은 답을 고르시오.
30활용마음이 설레인다.마음이 설렌다.
31정상회의는 오후 세 시에 시작합니다.고칠 것 없음(오검출 대조군)
32정상그는 밤새 자료를 정리했다.고칠 것 없음(오검출 대조군)
33정상이번 주 안에 결과를 정리하겠습니다.고칠 것 없음(오검출 대조군)
34정상비가 와서 우산을 챙겼다.고칠 것 없음(오검출 대조군)
35정상이 계약서는 검토가 끝나는 대로 보내겠습니다.고칠 것 없음(오검출 대조군)
36정상설명이 부족했다면 다시 알려 주세요.고칠 것 없음(오검출 대조군)
37정상예산을 초과하지 않도록 주의하고 있습니다.고칠 것 없음(오검출 대조군)
38정상담당자가 자리를 비워 연락이 늦었습니다.고칠 것 없음(오검출 대조군)
39정상자료를 첨부했으니 확인 부탁드립니다.고칠 것 없음(오검출 대조군)
40정상다음 회의는 금요일로 미루기로 했습니다.고칠 것 없음(오검출 대조군)

2차 — 어려운 15문항 + 오류 3개씩 심은 문단 5개

1차가 천장에 닿아 문맥으로만 갈리는 것들로 다시 만들었습니다. 201~205번은 한 문단에 오류를 세 개씩 넣었습니다.

#유형입력(오류 포함)정답
101동음이의대금을 카드로 결재했다.대금을 카드로 결제했다.
102동음이의부장님께 서류를 결제받았다.부장님께 서류를 결재받았다.
103동음이의폭력적인 표현은 지향해야 한다.폭력적인 표현은 지양해야 한다.
104동음이의품질을 재고하기 위해 노력했다.품질을 제고하기 위해 노력했다.
105의미혼동내 생각은 너와 틀리다.내 생각은 너와 다르다.
106의미혼동우체국에서 편지를 붙였다.우체국에서 편지를 부쳤다.
107의미혼동바짓단을 늘렸다.바짓단을 늘였다.
108불규칙활용어머니가 김치를 담궜다.어머니가 김치를 담갔다.
109불규칙활용나가면서 문을 잠궜다.나가면서 문을 잠갔다.
110불규칙활용어제 시험을 치뤘다.어제 시험을 치렀다.
111어법실내에서는 흡연을 삼가해 주세요.실내에서는 흡연을 삼가 주세요.
112사이시옷예외회의 회수를 줄이기로 했다.회의 횟수를 줄이기로 했다.
113사이시옷예외사진의 촛점이 흐리다.사진의 초점이 흐리다.
114어법저희 나라 경제가 성장하고 있다.우리나라 경제가 성장하고 있다.
115이중피동잘 쓰여진 문장이다.잘 쓰인 문장이다.
201복합지난 번 회의에서 논의된 내용을 정리해서 보내 드립니다. 검토하신 후 회신 주십시요. 일정이 촉박해서 죄송할 따름 입니다.지난 번 → 지난번
주십시요 → 주십시오
따름 입니다 → 따름입니다
202복합이번 프로젝트는 예산이 초과되지 안도록 관리하겠습니다. 담당자가 바뀌였으니 참고 바랍니다. 자세한 내용은 첨부 파일을 참고 하시기 바랍니다.초과되지 안도록 → 초과되지 않도록
바뀌였으니 → 바뀌었으니
참고 하시기 → 참고하시기
203복합고객님께서 주문하신 상품이 품절되셨습니다. 대체 상품을 안내해 드릴수 있으니 연락 주시면 감사드리겠읍니다.품절되셨습니다 → 품절되었습니다
드릴수 → 드릴 수
감사드리겠읍니다 → 감사드리겠습니다
204복합회사에 입사한지 벌써 삼 년이 되었습니다. 그동안 많은 것을 배웠고 앞으로도 최선을 다 하겠습니다. 좋은 결과로 보답할 수 있도록 노력하겠읍니다.입사한지 → 입사한 지
다 하겠습니다 → 다하겠습니다
노력하겠읍니다 → 노력하겠습니다
205복합예산안은 다음 주 까지 제출해야 되요. 각 부서에서 취합한 자료를 빠짐 없이 보내 주시기 바랍니다.다음 주 까지 → 다음 주까지
제출해야 되요 → 제출해야 돼요
빠짐 없이 → 빠짐없이

실행 스크립트

위 문항을 testset.json으로 저장한 뒤 python run.py <OPENAI_API_KEY>로 돌립니다. 결과와 채점이 results.json에 저장됩니다.

"""한국어 맞춤법 교정 정확도 실측.

40문항(오류 30 + 정상 10) × 모델 3종 × 프롬프트 2종 = 240회 호출.
정상 문장 10개를 섞은 이유: 재현율보다 **오검출**이 실무에서 더 아프기 때문이다.
멀쩡한 문장을 건드리면 결국 전부 다시 읽어야 한다.
"""
import json, re, sys, time, urllib.request, urllib.error
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path

BASE = Path(__file__).parent
KEY = sys.argv[1]
MODELS = ["gpt-5.6-luna", "gpt-5.6-terra", "gpt-5.6-sol"]
PROMPTS = {
    "느슨": "다음 문장의 맞춤법을 고쳐 주세요. 고친 문장만 출력하세요.\n\n{s}",
    "엄격": ("다음 문장에서 한국어 맞춤법과 띄어쓰기 오류만 고치세요.\n"
             "오류가 없으면 원문을 그대로 출력하세요.\n"
             "문체·어투·낱말 선택은 절대 바꾸지 마세요.\n"
             "설명 없이 결과 문장 한 줄만 출력하세요.\n\n{s}"),
}

def norm(s):
    s = re.sub(r"\s+", " ", (s or "").strip())
    return s.strip('"\'「」“”‘’ ')

def call(model, prompt):
    body = json.dumps({"model": model,
                       "messages": [{"role": "user", "content": prompt}],
                       "max_completion_tokens": 2000}).encode()
    req = urllib.request.Request("https://api.openai.com/v1/chat/completions", data=body,
        headers={"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"})
    for attempt in range(3):
        try:
            r = json.loads(urllib.request.urlopen(req, timeout=180).read())
            return r["choices"][0]["message"]["content"], r["usage"]
        except urllib.error.HTTPError as e:
            if attempt == 2:
                return f"__ERR__ {e.code} {e.read().decode()[:120]}", None
            time.sleep(3 * (attempt + 1))
        except Exception as e:
            if attempt == 2:
                return f"__ERR__ {e}", None
            time.sleep(3 * (attempt + 1))

items = json.loads((BASE / "testset.json").read_text(encoding="utf-8"))
jobs = [(m, pk, it) for m in MODELS for pk in PROMPTS for it in items]
print(f"호출 {len(jobs)}건 시작", flush=True)

def work(job):
    model, pk, it = job
    out, usage = call(model, PROMPTS[pk].format(s=it["wrong"]))
    o = norm(out)
    rec = {"model": model, "prompt": pk, **{k: it[k] for k in ("id", "cat", "wrong", "right", "err", "fix")},
           "out": o, "err_api": o.startswith("__ERR__")}
    if it["cat"] == "정상":
        rec["untouched"] = (o == norm(it["wrong"]))
    else:
        rec["missed"] = it["err"] in o
        rec["fixed"] = (it["fix"] in o) and (it["err"] not in o)
        rec["wrong_fix"] = not rec["fixed"] and not rec["missed"]
        rec["exact"] = (o == norm(it["right"]))
    return rec

with ThreadPoolExecutor(8) as ex:
    res = list(ex.map(work, jobs))

(BASE / "results.json").write_text(json.dumps(res, ensure_ascii=False, indent=1), encoding="utf-8")
errs = [r for r in res if r["err_api"]]
print(f"완료. API 실패 {len(errs)}건")
for e in errs[:3]:
    print("  ", e["model"], e["out"][:110])

알려진 한계

  • 26번(울었든지)의 정답 키가 좁았습니다. 모델이 고른 울었는지도 성립하는 문장이라 실패로 세면 안 됩니다.
  • OpenAI 계열만 측정했습니다. 클로드·제미나이, 전용 맞춤법 검사기와의 비교는 없습니다.
  • 60문항은 표본이 작고 문항 선택에 제 편향이 있습니다.

문항에 오류가 있거나 정답 키가 틀렸다면 문의하기로 알려 주세요. 수정 정책에 따라 고치고 그 사실을 남깁니다.