본문 바로가기

시스템/Devops

기술 초안의 재현성을 정량 검사하는 법

기술 초안에 코드 태그가 존재한다는 사실과 독자가 실행 가능한 재료가 있다는 사실은 다르다. 실제 초안에는 <pre> 블록이 3개 있었지만 모두 코드가 아니라 로그였고, 표와 다이어그램은 각각 0개였다. 태그 개수만 검사했다면 코드가 포함된 기술 글로 판정됐겠지만, 명령·설정·입력값이 없어 같은 작업을 재현할 수 없는 상태였다.

처음에는 글쓰기 프롬프트가 덜 기술적이어서 에세이형 초안이 생성됐다고 판단할 수 있었다. 그러나 결과 HTML을 직접 계수하고 블록 내용을 펼쳐 본 뒤, 상류 수집 단계가 실행 가능한 코드와 구조 재료를 충분히 전달하지 못한 것이 더 직접적인 원인으로 확인됐다. 따라서 검사는 결과 HTML의 형식, 블록의 내용, 상류 기록의 보존량을 분리해 수행해야 한다.

태그 개수로 형식 기준선 만들기

첫 단계는 초안마다 바이트 수와 코드·표·이미지·제목 태그 수를 같은 형식으로 출력하는 것이다. 이 검사는 재현성을 확정하지는 않지만, 표가 0개이거나 코드 후보가 전혀 없는 초안을 빠르게 걸러낸다. 실제 검사에서는 <pre> 3개, <table> 0개, 다이어그램 0개라는 형식 결손이 확인됐다.

초안 디렉터리에서 HTML 태그 수를 계수하는 셸 명령

cd "<초안 디렉터리>" 2>/dev/null || exit 1
for f in *.html; do
  echo "=== $f ==="
  echo -n " bytes: "; wc -c < "$f"
  echo -n " <pre>: "; grep -o '<pre' "$f" | wc -l
  echo -n " <code>: "; grep -o '<code' "$f" | wc -l
  echo -n " <table>: "; grep -o '<table' "$f" | wc -l
  echo -n " <img>: "; grep -o '<img' "$f" | wc -l
  echo -n " <h2>: "; grep -o '<h2' "$f" | wc -l
done

이 명령의 결과는 합격 판정이 아니라 다음 검사의 입력이다. 특히 <pre> 개수는 코드 후보 수일 뿐이다. 로그, 예외 스택, 명령 출력도 같은 태그에 들어갈 수 있으므로 블록 내용의 실행 가능성을 별도로 분류해야 한다.

코드 후보와 로그를 내용으로 구분하기

두 번째 단계에서는 HTML을 텍스트로 펼치되 코드 후보 구간의 경계를 보존한다. 실제 초안은 이 방식으로 확인했을 때 3개 블록이 모두 로그였다. 실행 명령, 설정 키, 입력 파일 형식, 호출 순서 중 어느 것도 제공하지 않는 블록은 코드 태그 수에 포함되더라도 재현 가능한 코드 수에는 포함하지 않는다.

코드 후보 구간을 표시해 로그 여부를 검토하는 Python 명령

import html
import re
import sys

path = sys.argv[1]
source = open(path, encoding="utf-8").read()
marked = re.sub(
    r'<pre[\s\S]*?</pre>',
    lambda match: '\n[[[PRE]]]\n' + match.group(0) + '\n[[[/PRE]]]\n',
    source,
)
marked = re.sub(r'<h2[^>]*>', '\n\n## ', marked)
marked = re.sub(r'<h3[^>]*>', '\n### ', marked)
marked = re.sub(r'</(p|li|h2|h3|div)>', '\n', marked)
marked = re.sub(r'<li>', '- ', marked)
marked = re.sub(r'<[^>]+>', '', marked)
print(html.unescape(marked).strip()[:9000])

자동 판정만으로 언어 문법의 유효성을 확정할 필요는 없다. 우선 블록을 실행 명령, 설정, 소스 코드, 로그, 설명문으로 분류하고 로그와 설명문을 제외한 수를 기록하면 된다. 명령이라면 실행 위치와 자리표시자가 있어야 하고, 설정이라면 어느 노드나 파일에 적용하는지 라벨로 남겨야 한다.

검사 항목실제 증상판정과 조치
코드 후보<pre> 3개가 모두 로그태그 수와 실행 가능 블록 수를 분리
표0개파라미터 또는 증상 대응표 요구
다이어그램0개분기와 실패 지점을 구조로 전달
패처 실행SyntaxError: f-string expression part cannot include a backslash생성물 검사 전에 수집·변환기 실행 검사
경로 처리단일 세그먼트 경로가 잘못 중첩될 가능성실행 전 최소 입력으로 구조 검사

상류 기록에 코드 예산을 별도로 배정하기

원천 기록의 전체 크기와 최종 글의 재현성도 비례하지 않았다. 한 추출 결과에는 프로젝트 3개, 프롬프트 20개, 결론 66개, 오류 36개, 코드 30개가 있었지만 내부 코드 20개가 섞였고 비밀정보 2건과 덤프 128건이 폐기됐다. 다른 결과는 7KB에 프로젝트 2개, 프롬프트 7개, 결론 34개, 오류 18개였으므로, 항목 수나 바이트 수만으로 발행 가능한 코드가 남았다고 판단할 수 없다.

문제가 되는 순서는 전체 기록을 먼저 덤프로 판정한 뒤 코드블록을 찾는 방식이다. 코드에는 긴 출력 형식과 반복 기호가 포함될 수 있어 산문용 덤프 규칙에 걸릴 수 있다. 아래와 같은 순서에서는 코드가 제거되거나 잘린 뒤 추출될 가능성이 생긴다.

코드를 덤프로 오인할 수 있는 문제 순서의 수집기 의사코드

def collect_record(record):
    filtered_text = reject_dump_like_content(record)
    snippets = extract_code_blocks(filtered_text)
    return extract_prose(filtered_text), snippets

수정된 순서는 코드블록을 산문보다 먼저 분리하고, 코드가 제거된 산문에만 덤프 판정을 적용한다. 일반 결론 한 건의 상한은 420자였지만 코드 스니펫은 1,400자와 40줄까지 보존했고 최대 10개를 허용했다. 전체 입력은 150,000바이트, 프롬프트·결론·오류는 각각 12개, 22개, 16개로 제한해 코드 예산이 일반 결론에 흡수되지 않게 했다.

코드 예산을 분리하고 추출 순서를 고친 수집기 설정 의사코드

LIMITS = {
    "prompt_chars": 220,
    "item_chars": 420,
    "prompts": 12,
    "findings": 22,
    "failures": 16,
    "total_bytes": 150_000,
    "snippet_chars": 1400,
    "snippet_lines": 40,
    "snippets": 10,
}

def collect_record(record):
    snippets, prose = extract_code_blocks_first(record)
    filtered_prose = reject_dump_like_prose(prose)
    return limit_prose(filtered_prose, LIMITS), limit_snippets(snippets, LIMITS)

수집 범위와 발행 가능 범위를 분리하기

코드를 많이 수집하는 것만으로는 해결되지 않는다. 실제 추출 결과처럼 내부 코드 20개가 포함되면 코드 수는 증가하지만 외부에 발행할 수 있는 재현 재료는 늘지 않는다. Bash 명령은 프로젝트 종류와 관계없이 수집하고, 파일 편집 내용은 허용된 개인 프로젝트에서만 수집하도록 경로를 분리해야 한다.

내부 스니펫 수집 여부는 별도 설정으로 관리하고, 비밀정보 필터는 코드 보존 한도와 독립적으로 적용한다. 프로젝트 식별자, 실제 소스 경로, 함수·필드·클래스 이름은 발행 단계에서 일반 명사나 의사코드로 바꾼다. 이 분리를 하지 않으면 수집량 통계는 높지만 실제 글에는 삭제된 자리만 남는 결과가 발생한다.

작업 기록에서 초안 검사까지의 흐름과 실패 지점

+------------------+
| 세션 작업 기록   |
+--------+---------+
         |
         v
+--------------------------+
| 프롬프트·결론·오류 추출  |
+------------+-------------+
             |
             v
+--------------------------+
| 코드블록·도구 입력 분리  |
+------+-------------------+
       |                         \
       | 올바른 순서              \ 잘못된 순서
       v                           v
+------------------+       +------------------+
| 산문 덤프 판정   |       | 전체 덤프 판정   |
+--------+---------+       +--------+---------+
         |                          |
         v                          v
+------------------+       +------------------+
| 비밀·내부 필터   |       | 코드 오인·폐기   |
+--------+---------+       +------------------+
         |
         v
+------------------+
| 노트 -> n8n 모델 |
+--------+---------+
         |
         v
+--------------------------+
| HTML 초안 형식 계수      |
+------------+-------------+
             |
             +----> 로그만 존재 -> 재현성 실패
             |
             v
+--------------------------+
| 코드 내용·표·구조 확인   |
+--------------------------+

모델 노드의 추측은 구현으로 확인하기

n8n 모델 노드의 options는 빈 객체였고 maxTokens 기본값은 -1이었다. 따라서 n8n이 Azure 요청에 별도 토큰 상한을 보내 코드가 잘렸다는 설명은 확인된 상태와 맞지 않았다. 토큰 제한을 원인으로 기록하려면 모델 노드 밖의 제한이나 실제 요청 값을 추가로 확인해야 한다.

n8n 모델 노드에서 확인한 옵션과 토큰 상한 상태

{
  "options": {},
  "maxTokens": -1
}

코드의 중괄호가 LangChain 템플릿 변수로 처리될 가능성도 처음에는 원인 후보였다. 그러나 n8n 소스에는 .replace(/[{}]/g, (match) => match + match)가 있었고, 템플릿 구성 전에 중괄호를 이스케이프하고 있었다. 추측에 근거한 중복 이스케이프는 추가하지 않고, 해당 가설을 원인 목록에서 제외해야 한다.

재현성 합격 조건을 단계별로 고정하기

검사 순서는 형식 계수, 블록 내용 분류, 상류 보존량 확인, 필터링 결과 확인, 모델 노드 설정 확인으로 고정할 수 있다. 형식 단계에서는 코드 후보·표·다이어그램의 존재를 보고, 내용 단계에서는 로그를 제외한 명령·설정·소스 코드 수를 센다. 상류 단계에서는 코드가 산문보다 먼저 추출됐는지와 별도 길이·줄 수 예산이 적용됐는지를 확인한다.

  1. 초안별 바이트 수와 코드 후보, 표, 제목 태그를 계수한다.
  2. 코드 후보를 펼쳐 로그와 실행 가능한 재료를 분리한다.
  3. 코드블록 추출이 덤프 판정보다 먼저 실행되는지 확인한다.
  4. 코드 예산 1,400자·40줄·10개와 전체 150,000바이트 상한을 각각 검사한다.
  5. 내부 코드, 비밀정보, 덤프의 폐기 건수를 별도 지표로 남긴다.
  6. 모델 설정과 템플릿 처리는 추측하지 않고 실제 옵션과 구현을 확인한다.

패처 수정 중 발생한 SyntaxError: f-string expression part cannot include a backslash는 오류 문구와 발생 사실만 기록에 남아 있고 수정 코드는 남아 있지 않다. 따라서 이 오류의 구체적인 수정안을 재현 코드로 제시해서는 안 된다. 검사 결과와 근거 코드가 함께 남아 있는 항목만 실행 절차로 승격해야 초안의 재현성 점수가 형식적 태그 수로 부풀려지지 않는다.