본문 바로가기

시스템/Devops

AI 런북을 숫자 계약으로 교정하는 법

필드 상한과 기대 출력 검사를 생성 후 계약으로 고정한다. 9개 섹션은 맞지만 결과가 긴 기술 에세이처럼 보인다. 기대 출력에 관측값 대신 성공 조건이 적힌다.

전제 환경 — n8n / Python 3 / JavaScript / HTML

구조

런북 생성과 숫자 계약 검사 흐름

[작업 기록]
     |
     v
[LLM 런북 생성] --X temperature: 0.4
     |
     v
[9개 섹션 구조화]
     |
     v
[HTML 렌더링]
     |
     v
[글자 수 검사] --X 상한 초과
     |
     v
[기대 출력 검사] --X 한다/된다
     |
     v
[저장]

정리하면

  • temperature: 0.4 제거
  • 프롬프트를 5.7KB로 축소
  • 필드별 글자 수 상한 적용
  • 서술형 기대 출력 차단
  • 3회 연속 형태 유지 확인

무엇이 문제였나

기존 결과는 9개 섹션을 정확히 갖췄지만 각 필드가 설명문으로 채워졌다. 구조 검증은 통과했어도 절차 6단계의 산문이 1,772자, 조치표 6행이 787자였고 전체 산문은 4,492자였다. 특히 expect가 화면 출력이 아니라 “동일한 16개 좌석표를 계산한다” 같은 성공 조건을 담았다. 섹션 수만 검사하면 문장의 형태와 길이는 제한되지 않으므로, 올바른 뼈대가 긴 기술 에세이를 그대로 수용하는 상태였다.

처음에는 모델 변동성을 줄이려고 temperature: 0.4를 넣었지만 두 실행 모두 “Unsupported value: 'temperature' does not support 0.4 with this model.”로 실패했다. 실제 충돌 지점은 모델 파라미터보다 9.9KB, 약 50줄에 이른 프롬프트 규칙이었다. 규칙을 5.7KB로 줄이고 필드별 숫자 상한과 서술형 종결 검사를 계약으로 두자 산문은 2,300자로 감소했다. 수정 초안은 상한 초과 0건과 서술형 기대 출력 0개를 기록했고, 같은 형태가 3회 연속 유지됐다.

주요 설정값

이름 무엇을 정하는가
temperature 0.4 제거 모델 호출 변동성 설정
tldr 45자 결론 항목 최대 길이
expect 70자 관측 가능한 기대 출력 최대 길이
check 40자 완료 확인 항목 최대 길이
cause 45자 조치표 원인 최대 길이
action 55자 조치표 조치 최대 길이

처리 순서

1. 기존 초안의 산문량을 실측한다

구조 문제가 아니라 필드 과잉인지 수치로 구분한다.

진단 — 생성물 디렉터리에서 최신 HTML 측정

: "${DRAFT_DIR:?<생성물 디렉터리>}"
cd "$DRAFT_DIR"
F=$(ls -t *.html | head -1)
python3 - "$F" <<'PYEOF'
import re,sys
s=open(sys.argv[1],encoding='utf-8').read()
art=s.split('<article>',1)[-1].split('</article>')[0]
sections=re.split(r'<h2>',art)[1:]
total=0
for sec in sections:
    name=re.sub('<[^>]+>','',sec.split('</h2>')[0]).strip()
    body=re.sub(r'<pre[\s\S]*?</pre>','',sec)
    size=len(re.sub(r'\s+',' ',re.sub('<[^>]+>','',body)).strip())
    total += size
    if name in ('절차','증상별 조치표'):
        print(f'{name}={size}')
print(f'total={total}')
PYEOF

이렇게 나오면 정상

절차=1772
증상별 조치표=787
total=4492

실패하면 — 최신 HTML 선택과 article 구간 추출 여부를 확인한다.

2. 지원하지 않는 파라미터를 제거한다

모델 호출 실패를 문체 문제와 분리한다.

진단 — 모델 호출 설정에서 temperature 검색

: "${MODEL_CONFIG:?<모델 설정 파일>}"
MATCH=$(grep -E "temperature[^0-9]*0\.4" "$MODEL_CONFIG" | head -1)
printf '%s\n' "$MATCH"

이렇게 나오면 정상

temperature: 0.4

조치 — 모델 호출 설정에서 temperature 제거

cp "$MODEL_CONFIG" "$MODEL_CONFIG.bak"
python3 - "$MODEL_CONFIG" <<'PYEOF'
import re,sys
p=sys.argv[1]
s=open(p,encoding='utf-8').read()
s,n=re.subn(r'^.*temperature[^\n]*0\.4[^\n]*\n?', '', s, flags=re.M)
open(p,'w',encoding='utf-8').write(s)
print(f'removed={n}')
PYEOF

이렇게 나오면 정상

removed=1

검증 — temperature 키 부재 확인

python3 - "$MODEL_CONFIG" <<'PYEOF'
import re,sys
s=open(sys.argv[1],encoding='utf-8').read()
print('temperature=present' if re.search(r'temperature',s) else 'temperature=absent')
PYEOF

이렇게 나오면 정상

temperature=absent

실패하면 — 호출 설정이 별도 워크플로에 중복됐는지 검색한다.

3. 프롬프트를 숫자 계약으로 재구성한다

서로 경쟁하는 문체 지시를 검증 가능한 규칙으로 바꾼다.

진단 — 기존 프롬프트 크기 측정

: "${PROMPT_FILE:?<프롬프트 파일>}"
python3 - "$PROMPT_FILE" <<'PYEOF'
import sys
b=open(sys.argv[1],'rb').read()
print(f'prompt_kb={len(b)/1000:.1f}')
print(f'prompt_lines={len(b.splitlines())}')
PYEOF

이렇게 나오면 정상

prompt_kb=9.9
prompt_lines=50

조치 — 축약 본문 앞뒤에 숫자 계약 배치

: "${CORE_PROMPT:?<축약한 프롬프트 본문>}"
cp "$PROMPT_FILE" "$PROMPT_FILE.bak"
CONTRACT=$(mktemp)
cat > "$CONTRACT" <<'EOF'
tldr 45자, why 70자, expect 70자, onFail 70자
check 40자, cause 45자, action 55자, takeaway 80자
expect는 출력 문자열·숫자·종료 코드만 허용
expect의 한다·된다 종결 금지
EOF
{
  cat "$CONTRACT"
  printf '\n'
  cat "$CORE_PROMPT"
  printf '\n자기 점검\n'
  cat "$CONTRACT"
} > "$PROMPT_FILE"
rm "$CONTRACT"
printf 'prompt=rebuilt\n'

이렇게 나오면 정상

prompt=rebuilt

검증 — 축약 크기와 계약 위치 확인

python3 - "$PROMPT_FILE" <<'PYEOF'
import sys
s=open(sys.argv[1],encoding='utf-8').read()
print(f'prompt_kb={len(s.encode())/1000:.1f}')
print(f'contract_copies={s.count("tldr 45자") }')
PYEOF

이렇게 나오면 정상

prompt_kb=5.7
contract_copies=2

실패하면 — 축약한 본문에 중복 지시와 긴 예시가 남았는지 확인한다.

4. 필드별 상한 검사를 추가한다

간결성 지시 대신 필드 단위 위반을 차단한다.

진단 — 검사 스크립트 존재 여부 확인

: "${CHECKER:?<검사 스크립트>}"
if test -f "$CHECKER"; then STATE=present; else STATE=absent; fi
printf 'checker=%s\n' "$STATE"

이렇게 나오면 정상

checker=absent

조치 — JSON 필드 상한 검사기 작성

cat > "$CHECKER" <<'JSEOF'
const fs = require('fs');
const doc = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
const root = doc.output || doc;
const limits = {
  tldr:45, why:70, expect:70, onFail:70,
  check:40, cause:45, action:55, takeaway:80
};
const overs = [];
const expects = [];
function inspect(kind, label, value) {
  const text = String(value ?? '').replace(/\s+/g, ' ').trim();
  if (text.length > limits[kind]) {
    overs.push(`${kind}:${label}:${text.length}`);
  }
  if (kind === 'expect' && text) expects.push(text);
}
(root.tldr || []).forEach((v,i) => inspect('tldr',i+1,v));
(root.checks || []).forEach((v,i) => inspect('check',i+1,v));
(root.takeaways || []).forEach((v,i) => inspect('takeaway',i+1,v));
(root.steps || []).forEach((step,i) => {
  inspect('why',i+1,step.why);
  inspect('onFail',i+1,step.onFail);
  ['diagnose','apply','verify'].forEach(part => {
    inspect('expect',`${i+1}.${part}`,step[part]?.expect);
  });
});
(root.troubleshoot || []).forEach((row,i) => {
  inspect('cause',i+1,row.cause);
  inspect('action',i+1,row.action);
});
const narrative = expects.filter(v => /(?:한다|된다)\.?$/.test(v));
console.log(`cap_overs=${overs.length}`);
console.log(`narrative_expect=${narrative.length}`);
if (overs.length || narrative.length) process.exitCode = 1;
JSEOF
printf 'checker=created\n'

이렇게 나오면 정상

checker=created

검증 — 정상 표본으로 검사기 자체 검증

FIXTURE=$(mktemp)
cat > "$FIXTURE" <<'JSONEOF'
{
  "output": {
    "tldr": ["필드별 상한 적용"],
    "checks": ["상한 초과 0건"],
    "takeaways": ["문체 지시를 숫자 계약으로 바꾼다."],
    "steps": [{
      "why": "현재 상태 측정",
      "onFail": "입력 확인",
      "diagnose": {"expect": "11 / 8 / 9 / 8 / 7"},
      "apply": {"expect": "exit 0"},
      "verify": {"expect": "match=true"}
    }],
    "troubleshoot": [{"cause":"규칙 경쟁","action":"프롬프트 축소"}]
  }
}
JSONEOF
node "$CHECKER" "$FIXTURE"
STATUS=$?
rm "$FIXTURE"
exit "$STATUS"

이렇게 나오면 정상

cap_overs=0
narrative_expect=0

실패하면 — 검사 입력의 output 래핑과 배열 필드 존재 여부를 확인한다.

5. 기대 출력을 관측값으로 교체한다

운영자가 화면 출력과 직접 대조할 값을 남긴다.

진단 — 기존 HTML에서 조건 서술 검색

: "${OLD_HTML:?<수정 전 HTML>}"
python3 - "$OLD_HTML" <<'PYEOF'
import re,sys
s=re.sub('<[^>]+>','',open(sys.argv[1],encoding='utf-8').read())
m=re.search(r'동일한 16개 좌석표를 계산한다',s)
print(m.group(0) if m else 'not_found')
PYEOF

이렇게 나오면 정상

동일한 16개 좌석표를 계산한다

조치 — 수정 프롬프트로 새 초안 생성

: "${GENERATE_CMD:?<생성 명령>}"
: "${DRAFT_JSON:?<새 초안 JSON>}"
eval "$GENERATE_CMD" > "$DRAFT_JSON"
printf 'draft=generated\n'

이렇게 나오면 정상

draft=generated

검증 — 관측값과 계약 위반 수 확인

python3 - "$DRAFT_JSON" <<'PYEOF'
import json,sys
root=json.load(open(sys.argv[1],encoding='utf-8'))
values=[]
def walk(v):
    if isinstance(v,dict):
        for k,x in v.items():
            if k=='expect' and isinstance(x,str): values.append(x)
            walk(x)
    elif isinstance(v,list):
        for x in v: walk(x)
walk(root)
print('11 / 8 / 9 / 8 / 7' if '11 / 8 / 9 / 8 / 7' in values else 'missing')
PYEOF
node "$CHECKER" "$DRAFT_JSON"

이렇게 나오면 정상

11 / 8 / 9 / 8 / 7
cap_overs=0
narrative_expect=0

실패하면 — 생성 프롬프트에 조건 서술 예시가 남았는지 확인한다.

6. 세 번 연속 형태를 검증한다

단일 성공이 아닌 출력 계약의 반복 유지 여부를 확인한다.

진단 — 호출 설정과 프롬프트 크기 재확인

python3 - "$MODEL_CONFIG" "$PROMPT_FILE" <<'PYEOF'
import re,sys
config=open(sys.argv[1],encoding='utf-8').read()
prompt=open(sys.argv[2],'rb').read()
print('temperature=absent' if not re.search(r'temperature',config) else 'temperature=present')
print(f'prompt_kb={len(prompt)/1000:.1f}')
PYEOF

이렇게 나오면 정상

temperature=absent
prompt_kb=5.7

조치 — 동일 프롬프트로 3회 생성

: "${RUN_DIR:?<반복 생성물 디렉터리>}"
mkdir -p "$RUN_DIR"
for n in 1 2 3; do
  eval "$GENERATE_CMD" > "$RUN_DIR/run$n.json"
  printf 'run%s=generated\n' "$n"
done

이렇게 나오면 정상

run1=generated
run2=generated
run3=generated

검증 — 3개 결과의 계약 통과 확인

STATUS=0
for n in 1 2 3; do
  if node "$CHECKER" "$RUN_DIR/run$n.json" >/dev/null; then
    printf 'run%s=pass\n' "$n"
  else
    printf 'run%s=fail\n' "$n"
    STATUS=1
  fi
done
exit "$STATUS"

이렇게 나오면 정상

run1=pass
run2=pass
run3=pass

실패하면 — 실패한 회차의 상한 종류와 서술형 expect를 비교한다.

바뀐 것

문체 지시와 숫자 계약 대비

바꾸기 전

const request = {
  prompt: '간결한 런북으로 작성한다',
  sections: 9,
  temperature: 0.4
};
// 문제: 길이 계약이 없고 모델이 temperature 값을 거부

바꾼 뒤

const fieldRules = {
  tldr: 45,
  why: 70,
  expect: 70,
  onFail: 70,
  check: 40,
  cause: 45,
  action: 55,
  takeaway: 80
};
// 변경: 지원하지 않는 값 제거, 필드별 검증 계약 적용

끝났는지 확인하기

  • 산문 총량 2,300자
  • 상한 초과 0건
  • 서술형 기대 출력 0개
  • temperature 키 없음
  • 3회 결과 모두 pass

문제 해결

증상 원인 해결
Unsupported value: 'temperature' does not support 0.4 해당 모델의 미지원 파라미터 값 temperature: 0.4를 호출 설정에서 제거
9개 섹션인데 긴 기술 에세이로 출력 필드 문장 형태·길이 무제한 섹션 구조 대신 필드별 숫자 상한 적용
산문 총량 4,492자 절차와 조치표의 설명문 과다 why·onFail·cause·action 상한 검사
동일한 16개 좌석표를 계산한다 성공 조건을 넣은 기대 출력 11 / 8 / 9 / 8 / 7 같은 관측값으로 교체
실행마다 런북 문체가 흔들림 9.9KB 프롬프트 규칙 경쟁 5.7KB로 축소하고 계약을 앞뒤에 배치
섹션 구성을 바꿔도 과잉 설명 지속 뼈대를 원인으로 본 초기 오판 9개 섹션은 유지하고 필드 계약만 교정

롤백

백업한 모델 설정과 프롬프트를 복원하고 새 검사기를 제거한다.

test -f "$MODEL_CONFIG.bak" && mv "$MODEL_CONFIG.bak" "$MODEL_CONFIG"
test -f "$PROMPT_FILE.bak" && mv "$PROMPT_FILE.bak" "$PROMPT_FILE"
test -n "${CHECKER:-}" && rm -f "$CHECKER"
if test -n "${RUN_DIR:-}"; then
  rm -f "$RUN_DIR"/run1.json
  rm -f "$RUN_DIR"/run2.json
  rm -f "$RUN_DIR"/run3.json
fi
printf 'rollback=complete\n'

마치며

  • 섹션 구조가 맞아도 필드 문장 형태와 길이가 무제한이면 런북이 아니다.
  • 모델 변동성으로 단정하기 전에 지원 파라미터와 프롬프트 규칙 경쟁을 분리한다.
  • 간결하다는 문체 지시보다 필드별 글자 수 상한이 안정적인 출력 계약이다.
  • 기대 출력에는 성공 조건이 아니라 화면에서 대조할 문자열·숫자를 둔다.