
확인한 것인가, 추측한 것인가? LLM이 작성한 출력을 생성하는 도구 테스트하기
요약
AI 도구가 생성한 결과물의 신뢰성을 확보하기 위해 '확인된 사실'과 '추측된 판단'을 구분하는 설계 패턴을 다룹니다. Claude Code 스킬을 활용한 인증 시스템 감사 도구 사례를 통해, 검증 가능한 증거와 사용자 인터뷰를 결합하여 신뢰할 수 있는 보고서를 만드는 방법을 설명합니다.
핵심 포인트
- AI 출력의 신뢰성을 위해 사실(Fact)과 추측(Judgment)을 명확히 구분해야 함
- 검증 가능한 데이터(코드, 문서)를 기반으로 한 인용(Citation) 시스템 구축 필요
- 기계적 발견과 인간의 판단을 결합하여 도구의 답변 품질을 높이는 설계 패턴 제시
나의 첫 번째 AI 도구가 완성되었고, 전형적인 AI 시대의 방식대로 나는 서둘러 이를 게시했습니다. 어쨌든 작동했으니까요. 적어도 내가 실행했을 때는 출력이 제대로 맞아 보였습니다.
하지만 한 가지 시나리오가 계속 나를 괴롭혔습니다. 내가 고객에게 이 도구를 추천했는데, 고객이 이를 실행한 후 발견된 결과 중 하나에 의문을 제기하는 상황입니다. 그러고 나서 그들은 이렇게 묻습니다. "이것을 어떻게 신뢰할 수 있습니까? 확인한 것인가요, 아니면 추측한 것인가요? 어떻게 확신합니까?"
나에게는 좋은 답변이 없었습니다. "제 프롬프트(Prompt)가 정말 좋았거든요" — 이는 시니어 아키텍트(Senior Architect)에게 기대하는 답변이 아니었습니다.
이 글은 더 나은 답변을 구축하는 방법, 그리고 그 답변을 구축하는 과정이 도구 자체를 어떻게 변화시키는지에 관한 것입니다.
주제: 하나의 니치(Niche) 도구, 하나의 일반적인 패턴
나는 운영 중인 인증(Auth) 시스템의 경직화(Calcification)를 감사하기 위한 AI 도구를 만들었습니다. 이 도구의 목적은 내가 대규모 플랫폼에서 작업하며 직면했던 실제 문제를 바탕으로 하며, 그 내용은 Part 1에서 설명했습니다. 그 문제를 다룬 후, 나는 Part 2에서 몇 가지 일반화된 원칙을 정립했고, 그 원칙들을 바탕으로 Part 3에서 도구를 구축했습니다.
요약하자면: 이 도구는 Claude Code를 위해 게시된 스킬(Skill)입니다. 이 도구는 코드베이스를 감사하고, 여러 축(토큰 저장, 리프레시 처리, 제공자 결합, 권한 부여)에 걸쳐 인증 구현이 벤더의 기본값에 얼마나 강하게 결합(Hard-wired)되어 있는지를 평가합니다. 그런 다음 판단이 필요한 부분에 대해 사용자와 인터뷰를 진행하고, 모든 것을 결합하여 최종 결과물을 만들어냅니다. 이 이야기가 시작될 당시, 그 결과물은 발견 사항, 리스크 및 권장 다음 단계(Next steps)를 제시하는 전체 보고서와 한 화면 요약본이라는 2개의 마크다운(Markdown) 파일이었습니다.
이 도구와 이전 3개의 기사가 문맥을 파악하는 데 도움이 될 수는 있지만, 본 기사는 AI 도구와 기술을 테스트할 때 통상적으로 마주하게 되는 문제들에 대해 다루는 독립적인 글입니다. Auditor(감사 도구)는 AI 테스트를 어렵게 만드는 모든 요소인 기계적 발견(mechanical findings), 판단(judgment calls), 그리고 산문(prose)이 혼합된 결과물을 내놓기 때문에 단지 편리한 예시로 사용되었습니다.
만약 여러분의 도구가 검증 가능한 무언가(코드베이스 (codebase), 문서 베이스 (document base), 데이터셋 (dataset))에 대해 주장한다면, 그 패턴은 동일하며 용어도 그대로 적용됩니다. 즉, 저의 열거형 (enums)은 여러분의 분류 필드 (classification fields)가 되고, 저의 파일 및 라인 증거 (file-and-line evidence)는 여러분의 인용 (citations)이 되며, 저의 커버리지 (coverage) 섹션은 여러분의 범위 정의 (scope statement)가 됩니다.
v1이 테스트를 남겨둔 지점
초기 버전에서 저는 재현율 (recall, 존재하는 것을 찾아내는가?), 정밀도 (precision, 아무것도 없을 때 침묵을 유지하는가?), 일반화 (generalization, 방법론이 벤더 간에 통용되는가?), 그리고 에지 케이스 (edge cases)를 테스트하기 위한 4가지 적대적 피스처 (adversarial fixtures)를 구축했습니다. 이에 대한 자세한 설명은 여기에서 확인할 수 있습니다.
저는 이 피스처들을 대상으로 도구를 실행하고 결과를 수동으로 검증했습니다. 이는 제가 반복 개선할 수 있는 좋은 피드백 루프 (feedback loop)를 이미 제공해 주었습니다. 초기 수정 사항들을 구현한 후, 저는 실제 코드베이스 (codebases)를 대상으로 해당 기술을 실행해 보았고, 실제 코드는 실제 코드답게 행동했습니다. 즉, 제가 피스처로 상상하지 못했던 빈틈들을 찾아낸 것입니다.
저는 이 기술이 발견 사항에 대해 자신 있게 거짓말을 하는 것을 포착했습니다. 가장 기억에 남는 사례는 다음과 같습니다. 도구의 벤더 지식(vendor knowledge)에는 해당 벤더 라이브러리의 v6 버전에서 토큰 저장(token storage)이 어떻게 구성되는지 기록되어 있었으나, 실제 코드베이스는 v5를 실행 중이었으며, v5에서는 완전히 다른 형태(shape)로 저장소가 구성됩니다. 모델은 v6 패턴을 검색했고, 아무것도 찾지 못하자 "기본 저장소: localStorage"라고 보고했습니다. 정작 쿠키 설정은 도구가 배운 적 없는 형태로 눈에 띄게 존재하고 있었습니다. 도구가 보고하는 가장 기본적인 축에서 부재를 확신하며 내린 잘못된 주장입니다 (전체 이야기는 여기에서 확인 가능합니다).
또한 실행할 때마다, 특히 모델을 전환할 때 다르게 행동하는 모습도 포착했습니다. 예를 들어, 동일한 피스처(fixture)에 대해 한 번의 실행에서는 경계 신호(boundary signal)를 "존재함(present)"으로 평가하고 다음 실행에서는 "부분적(partial)"이라고 평가하거나, 더 큰 모델이 포착한 저장 패턴을 더 작은 모델이 놓치는 경우 등이 있었습니다.
더 철저한 테스트 레이어를 도입하는 방안이 이미 논의 중이었으나, 이러한 실행 결과들은 그 필요성을 더욱 명확하게 만들어 주었습니다.
테스트 가능성(testability)에 대한 고민이 도구의 결과물을 바꾸다
첫 번째 다이어그램을 떠올려 보십시오. 이 도구는 처음에 일관된 구조를 따르되 자유 형식의 텍스트(freeform text)로 작성된 2개의 마크다운(markdown) 파일을 생성하도록 설계되었습니다. 이는 기계적인 테스트(mechanical testing)를 거의 불가능하게 만듭니다. 관련된 모든 검증에는 즉시 또 다른 LLM 호출(LLM-as-judge)이 필요하며, 이는 매 테스트 실행마다 비용을 지불해야 함을 의미합니다.
저는 무료로 가능한 기계적 테스트의 가치를 극대화하고 싶었고, 이는 기술의 출력물을 더 테스트 가능한 형태로 변경해야 함을 의미했습니다. 즉각적인 해결책은 기본 출력물로 구조화된 JSON을 사용하는 것이었습니다. 이제 이 기술은 3개의 산출물(artifacts)을 전달하며, JSON이 표준(canonical)이 되고 2개의 마크다운은 오직 JSON으로부터만 렌더링됩니다.
이러한 순서에는 저렴한 테스트 외에도 두 번째 이점이 있습니다. 모델은 이제 서사(narrative)를 작성하기 전에 자신의 주장(상태, 분류, 증거)을 엄격한 필드(fields)에 먼저 확정(commit)해야 합니다. 확신에 찬 이야기가 검증되지 않은 주장을 더 이상 얼버무릴 수 없게 되는데, 왜냐하면 주장이 먼저 확인 가능한 형식으로 고정되었기 때문입니다.
또한 렌더링 설정을 완성하는 규칙을 도입했습니다. 만약 모델이 렌더링 도중에 산문(prose)이 JSON이 지원하지 않는 내용을 말하려 한다는 것을 인지하면, 수정 사항은 먼저 JSON에 반영되고 뷰(view)는 JSON으로부터 다시 렌더링됩니다. 뷰는 결코 소스(source)로부터 분기(fork)되지 않습니다.
하지만 이것만으로는 "JSON으로부터만 렌더링된다"는 규칙을 강제하기에 충분하지 않았습니다. 렌더링 단계는 분석을 생성한 것과 동일한 컨텍스트(context)에서 실행되므로, 세부 사항이 JSON을 넘어 산문으로 새어 나가는 것을 물리적으로 막을 방법이 없기 때문입니다. 완벽한 버전이라면 JSON만을 전달받는 새로운 세션에서 마크다운을 렌더링해야 합니다. 저는 이를 미루고 대신 동일한 위험을 감시하는 더 저렴한 메커니즘을 선택했습니다. 바로 테스트 하네스(harness)를 통해 마크다운과 JSON 사이의 일치 여부를 확인하는 것입니다.
구조가 가치를 떨어뜨리지는 않을까?
던져볼 만한 진지한 질문입니다. AI의 독특한 가치는 현실 세계의 맥락에 초개인화(hyper-personalized)된 답변을 생성하는 능력에 있습니다. 명확한 구조는 AI를 테스트하기 어렵게 만드는 비결정성(non-determinism)을 일부 제거하지만, 답변을 읽을 가치가 있게 만들었던 바로 그 구체성을 평면화(flattening)할 위험도 있습니다. 비결은 이러한 위험을 고려하여 JSON을 어떻게 설계하느냐에 달려 있습니다.
잘못된 구조는 현실이 정의된 틀을 벗어날 수 있는지 고려하지 않은 채(스포일러: 현실은 거의 항상 틀을 벗어납니다), AI가 현실을 미리 정의된 상자 안에 강제로 밀어 넣게 만듭니다. 더 나은 구조는 출력을 레지스터(registers)로 나눕니다:
- 일부는 **주장(claims)**입니다: 상태, 분류, 증거 참조 등. 엄격한 형태에 맞춰져서 단언할 수 있는 사실들입니다.
- 일부는 **설명(explanation)**입니다: 특정 발견이 왜 중요한지, 만약 그대로 둔다면 어떤 비용이 발생하는지에 대한 내용입니다. 이 부분은 자유 형식으로 유지되며, 테스트가 이를 단언하는 경우는 없습니다.
- 일부는 **종합(synthesis)**입니다: 헤드라인이나 전반적인 태도 같은 것입니다. 자유롭게 작성되지만, 근거로 삼는 주장들 옆의 JSON에 기록되어 편집 계층조차 주장에 고정됩니다.
계약은 다음과 같습니다:
마크다운(markdowns)이 JSON보다 더 많은 내용을 담을 수는 있지만, 결코 다른 내용은 담지 않아야 한다.
상자를 설계하는 방법, 그리고 그 상자가 정당화되는 경우
JSON 구조는 다음 단계를 가능하게 하며, 이는 실제로 AI 출력을 테스트 가능하게 만듭니다: 단언 가능한 열거형 필드(assertable enum fields)를 사용하는 것입니다. 출력의 일부 부분은 나중에 해당 도구의 지침을 변경한 후 정확성을 검증하는 데 사용할 수 있는 상자에 들어갈 수 있습니다.
예를 들어, 인증 감사 도구(auth auditing tool)의 경우, 인증 경계가 존재할 수도 있고(present), 부분적일 수도 있으며(partial), 아예 없을 수도 있다고 말할 수 있고, 이는 JSON 필드 authBoundary: "present" | "partial" | "absent"로 번역됩니다. 마찬가지로, 인증 토큰 저장소는 분류되어 상자에 들어갈 수 있습니다: tokenStorage: "vendor_default" | "builtin_selector" | "custom_adapter". (필드 이름은 전체적으로 단순화되었으며; 실제 스키마에서는 공급업체별로 중첩됩니다.)
이 지점에서 중요한 구분이 필요합니다. 상자(또는 열거형)는 두 가지 유형일 수 있습니다:
- 논리적으로 완전한(Logically exhaustive) 열거형: 구성상 모든 경우를 포괄하는 것들입니다: `
이러한 위험을 완화하기 위해, 두 가지 새로운 값이 도움이 됩니다:
"other": "이 패턴을 이해했지만, 옵션 목록에 없을 뿐입니다"라는 의미를 가집니다. 분류(hatch)의 정직함을 유지하기 위해,"other"에는 의무 사항이 따릅니다. 즉, 모델이 해당 패턴을 자신의 언어로 설명하는 필수note필드와, (모든 확정된 판결과 마찬가지로) 최소 하나 이상의 인용된 근거(cited finding)가 포함되어야 합니다."undetermined": "판단할 수 없었습니다"라는 의미를 담고 있습니다. 이 값은 분석되지 않은 항목과 그 이유를 나열하는 보고서의 섹션인 커버리지(coverage) 섹션에 필수 항목으로 결합됩니다. 커버리지 섹션에 나타나지 않는 "판단할 수 없음"은 무책임한 회피(silent shrug)가 되겠지만, 커버리지 공백과 결합되면 독자가 조치를 취할 수 있는 신호가 됩니다.
강제 적용의 첫 번째 단계는 스키마(schema) 자체에 위치하며, 여기서 의무 사항을 위반하는 것은 구조적 오류(shape error)가 됩니다:
"classification": { "enum": ["vendor_default", "builtin_selector", "custom_adapter", "other", "undetermined"] },
"allOf": [
{ "if": { "properties": { "classification": { "const": "undetermined" } } },
...
두 분류(hatches)는 서로 다른 실패 모드(failure modes)를 다루기 때문에 서로 다른 수준에서 결합됩니다. "other"는 어휘적 실패(vocabulary failure)를 다룹니다. 즉, 모델은 패턴을 이해했으나 나의 열거형(enum)이 이를 담지 못한 경우입니다. 이는 경험적 분류 체계(empirical taxonomies)에만 해당됩니다. "undetermined"는 인식론적 실패(epistemic failure)를 다룹니다. 즉, 조사 행위 자체가 실패한 경우입니다. 이는 모델이 탐지해야 하는 모든 판결에 포함되어야 하며, 논리적으로 완전한(logically exhaustive) 경우조차 마찬가지입니다 (예를 들어, 경계 상태(boundary status)는 '존재함(present)', '부분적임(partial)', '없음(absent)'이라는 완전한 어휘를 갖추고 있습니다. 하지만 여전히 "undetermined"를 포함해야 합니다. 왜냐하면 완전한 어휘가 있다고 해서 모델이 항상 판결에 도달할 수 있다는 것을 보장하지는 않기 때문입니다).
따라서, 우리의 이전 사례는 이제 다음과 같이 변합니다: tokenStorage: "vendor_default" | "builtin_selector" | "custom_adapter" | "other" | "undetermined". 이러한 방식을 통해, 우리의 도구는 고정된 테스트 데이터(fixtures)에 대해 테스트 가능성을 유지하면서(우리는 고정된 fixture가 항상 "vendor_default"를 생성해야 함을 알고 있습니다. fixture는 알려진 패턴으로부터 구축되므로, 거기서 "other"가 나오는 것은 테스트 실패입니다), 동시에 예측 불가능한 실제 웹 환경(wild web)에 대해서도 대비할 수 있습니다.
영수증, 검색 기록, 그리고 DRY 필드
세 가지의 추가적인 구조적 규칙이 JSON을 완성하며, 각 규칙은 특정한 종류의 검증 가능성 (checkability)을 확보해 줍니다.
존재 여부 주장 (Presence claims)은 **축자 인용된 참조 (verbatim-quoted references)**를 포함합니다. 이러한 참조는 모델을 분석 중인 실제 상황에 고정할 뿐만 아니라, 추가적인 LLM 호출 없이도 기계적으로 검증할 수 있습니다: 파일을 열고, 해당 라인으로 이동하여, 인용문을 비교하면 됩니다.
{
"id": "boundary-cognito-authport-interface",
"claim": "presence",
...
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기
