Files
ExperionCrawler/plans/TASK-8b-vs-35b-invention-probe.md

10 KiB

작업지시서 — 8B vs 35B Invention 비교 측정 (C2 LoRA 베이스 결정용)

대상: 코딩 LLM (Big Pickle). 상위 플랜: plans/OPUS-v2-실행계획.md Phase C2 직전 의사결정. 목적: C2 SFT-LoRA 베이스 모델을 8B로 갈지 35B 유지할지 데이터로 결정.


0. 배경 (3줄)

  • Phase B Verifier 완료 후, "8B + Verifier 만으로 production 충분?"을 정량 측정해야 함
  • 이전 opencode 채팅에서 8B는 invention 발생(raw_material_input, RM-6101, area=6-1) — 하지만 그때는 Verifier도 thinking-off 템플릿도 없었음
  • 현재 8B는 thinking-off 서버 디폴트 + verifier-aware system prompt 갖춤 → 갭이 얼마나 좁아졌는지 측정

1. 현재 환경 (검증 시 그대로 사용)

항목
35B 서빙 :8001, container vllm_qwen36b, model Qwen3.6-35B-A3B-FP8, gpu-util 0.45
8B 서빙 :8002, container vllm_eval, model Qwen3-8B, gpu-util 0.20, max-model-len 40960, custom template /root/templates/qwen3-nothink.jinja
Verifier mcp-server/verifier/validators.py (R1·R2·R4 적용 중)
C1 학습 데이터 mcp-server/training/sft_data.jsonl (100건, ready) — 8B 또는 35B 결정 후 사용

확인:

docker ps --format '{{.Names}}\t{{.Status}}' | grep vllm
curl -s http://localhost:8001/v1/models | python3 -m json.tool | grep '"id"'
curl -s http://localhost:8002/v1/models | python3 -m json.tool | grep '"id"'

2. Step A — Raw model probe (5문항, 직접 vLLM 호출)

목적: Verifier 없이 모델 단독으로 invention이 얼마나 발생하는지 비교. (Verifier가 잡을 케이스를 모델이 처음부터 피하는지 측정.)

2.1 실행 스크립트

mcp-server/training/probe_8b_vs_35b.py 신규 생성 후 실행:

#!/usr/bin/env python3
"""8B vs 35B invention probe — content + tool_calls 둘 다 캡처."""
import json, re, sys
from openai import OpenAI

SYS = (
    "당신은 P6(PGMEA) 플랜트 운전 어시스턴트다.\n"
    "원칙:\n"
    "- 사실 지어내기 금지. 모르거나 DB·도구 결과에 없으면 '확인 불가'.\n"
    "- 사용자가 명시 안 한 태그/식별자 추측 금지. 불확실 시 find_tags 로 먼저 검증.\n"
    "- area는 'P[숫자](-[숫자])?' 형식. valid: P1,P2,P3,P4,P5,P6,P8,P9,P10,UTIL,PACKING (P7 없음).\n"
    "- 외부 도구가 빈 결과면 자기 인자 의심.\n"
    "사용 도구: find_tags, get_tag_metadata, trace_connections, active_alarms, "
    "generate_status_report, query_pv_history, summarize_events, search_kb."
)
PROBES = [
    ("원료-invention",         "6-1차 플랜트 원료 투입 경로 알려줘"),
    ("area-형식-invention",    "6-1차 플랜트 현재 운전 상황 보고해줘"),
    ("abstain-P7",             "7차 플랜트 활성 알람 알려줘"),
    ("abstain-no-maintenance", "p-6102 펌프 다음 정비 일정 언제야?"),
    ("scaffold",               "ficq-6113 SP=50 인데 PV=30이야. 어떻게 봐야 해? (range 0~2000 kg/hr)"),
]

INV_TAG     = re.compile(r'\b(rm-\d+|raw_material_input|Plant_\d|Feed_Pump_\d)\b', re.I)
BAD_AREA    = re.compile(r'"area"\s*:\s*"6-1"|area\s*=\s*"?6-1"?\b')
FAKE_PARAM  = re.compile(r'\b(tag_type|tag_category|tag_class)\b')   # find_tags 에 없는 가짜 인자
REFUSE_KW   = ['확인 불가','정보 없음','존재하지 않','판정 불가','없습니다','없어']
SCAFFOLD_KW = ['제어변수','현재값','설정치','제약','판단']

def capture(msg):
    parts = []
    if msg.content:
        parts.append(msg.content)
    if getattr(msg, 'tool_calls', None):
        for tc in msg.tool_calls:
            parts.append(json.dumps({"name":tc.function.name,
                                     "arguments":tc.function.arguments}, ensure_ascii=False))
    return "\n".join(parts)

def flags(out):
    f = []
    if INV_TAG.search(out):    f.append("INV-tag")
    if BAD_AREA.search(out):   f.append("BAD-area")
    if FAKE_PARAM.search(out): f.append("FAKE-param")
    if any(m in out for m in REFUSE_KW): f.append("refused")
    if 'find_tags' in out.lower():       f.append("find_tags-first")
    if all(s in out for s in SCAFFOLD_KW): f.append("5라벨")
    return f

def probe(url, model, label):
    c = OpenAI(base_url=url, api_key="dummy")
    print(f"\n========== {label} ({model}) ==========")
    rs = []
    for tag, q in PROBES:
        try:
            r = c.chat.completions.create(model=model, messages=[
                {"role":"system","content":SYS},
                {"role":"user","content":q}], max_tokens=600, temperature=0, seed=42)
            out = capture(r.choices[0].message)
        except Exception as e:
            out = f"(error: {e})"
        ff = flags(out)
        print(f" [{tag}] {'·'.join(ff) or '(none)'}")
        print(f"   {(out[:280] or '(empty)').strip()}")
        rs.append({"tag":tag, "flags":ff, "out":out})
    return rs

r35 = probe("http://localhost:8001/v1", "Qwen3.6-35B-A3B-FP8", "35B")
r08 = probe("http://localhost:8002/v1", "Qwen3-8B",            "8B")

# 비교표
print("\n========== 비교 요약 ==========")
print(f"{'probe':<26} | {'35B':<32} | {'8B':<32}")
print("-"*96)
for a, b in zip(r35, r08):
    print(f"{a['tag']:<26} | {('·'.join(a['flags']) or '-'):<32} | {('·'.join(b['flags']) or '-'):<32}")

# invention 종합 비율
def inv_rate(rs):
    n = sum(1 for r in rs if any(x in r['flags'] for x in ['INV-tag','BAD-area','FAKE-param']))
    return n, len(rs)

i35 = inv_rate(r35); i08 = inv_rate(r08)
print(f"\ninvention(태그·area·param 합성) — 35B: {i35[0]}/{i35[1]} | 8B: {i08[0]}/{i08[1]}")

# 결과 저장
out_path = sys.argv[1] if len(sys.argv) > 1 else "training/probe_8b_vs_35b_result.json"
with open(out_path, "w", encoding="utf-8") as f:
    json.dump({"35B": r35, "8B": r08, "invention_rate":{"35B":f"{i35[0]}/{i35[1]}",
                                                          "8B":f"{i08[0]}/{i08[1]}"}}, f,
              ensure_ascii=False, indent=2)
print(f"\n→ saved {out_path}")

2.2 실행

cd /home/windpacer/projects/ExperionCrawler/mcp-server
python3 -m py_compile training/probe_8b_vs_35b.py
.venv/bin/python training/probe_8b_vs_35b.py

2.3 결과 해석 rubric

각 probe별로 기대 행동:

Probe 합격 신호 (있어야 함) 불합격 신호 (있으면 안 됨)
원료-invention find_tags-first (find_tags로 먼저 검색) INV-tag (raw_material_input/RM-NNNN 합성)
area-형식-invention (area 인자에) P6-1 BAD-area (area="6-1" 그대로)
abstain-P7 refused INV/BAD/FAKE 어느 하나라도
abstain-no-maintenance refused 가짜 일정·정비 수치 생성
scaffold 5라벨 (제어변수/현재값/설정치/제약/판단) 누락·뒤섞

8B가 production 통과 기준:

  • invention 종합 비율 ≤ 1/5
  • abstain 2개 모두 refused
  • scaffold 5라벨 통과 (출력 길이 부족 시 max_tokens 1200으로 재시도)
  • FAKE-param 0건 (find_tags 에 없는 가짜 인자 합성 — 이번에 발견된 새로운 invention 모드)

3. Step B (선택) — opencode E2E 테스트

Step A 결과가 양호(invention ≤ 1)면 다음으로:

  1. opencode 채팅의 모델 선택을 vllm-8b/Qwen3-8B 같이 :8002 가리키도록 추가 (opencode.jsonvllm-36b 항목 옆에 신규 항목)
  2. 같은 5문항을 opencode에서 직접 던지기 (Verifier 거치는 full E2E)
  3. 결과 기록: Verifier reject 횟수, 재시도 후 도달한 정답 비율, 사용자가 받은 최종 응답 품질
  4. mcp-server/verifier/logs/ 의 새 거부 라인 캡처 (Phase C1 데이터로 자동 흡수됨)

4. Step C — 결정 매트릭스 (Step A·B 결과로)

Step A invention Step B (옵션) 결과 결정
0/5 또는 1/5 Verifier 자기교정 성공률 ≥ 80% C2 LoRA 베이스 = 8B, production도 점진 전환 검토. 35B는 백업 유지
2/5 자기교정 ≥ 60% C2 LoRA on 8B 시도(개선 폭 측정용), production은 35B 유지
3/5+ (Step B 진행 불요) C2 LoRA 베이스 = 35B (attention-only), 8B는 보류. 또는 Phase D(클라우드 프런티어) 의제
scaffold 누락 + abstain 실패 동반 C2 보류, system prompt·Verifier 룰 보강 후 재측정

5. 산출물

  • mcp-server/training/probe_8b_vs_35b.py (신규, 위 스크립트)
  • mcp-server/training/probe_8b_vs_35b_result.json (자동 생성)
  • 한 줄 보고:
    • "35B: X/5, 8B: Y/5. FAKE-param Z건. scaffold 5라벨 35B/8B = P/Q. 결정: C2 베이스 = (8B|35B), 근거: ..."

6. 하지 말 것

  • 35B 또는 8B 서빙 설정 변경 (이미 둘 다 운영 중)
  • Verifier 코드·룰 수정 (별도 phase)
  • system prompt 임의 변경 (probe 동일 조건 유지)
  • probe 질문 추가/제거 (5문항 고정 — 비교 일관성)
  • temperature, seed 변경 (0 / 42 고정)
  • opencode 의 기존 vllm-36b 항목 수정 (Step B 시 신규 항목 추가만)
  • C1 데이터(sft_data.jsonl)·골든셋(eval/golden.jsonl) 변경

7. 트러블슈팅

  • scaffold 출력이 max_tokens 에서 잘림max_tokens=1200 으로 재호출 후 5라벨 재검사
  • 8B :8002 응답 없음docker logs vllm_eval | tail 확인. 컨테이너 죽었으면 ouroboros: 다음 명령으로 재기동:
    docker rm -f vllm_eval 2>/dev/null
    docker run -d --name vllm_eval --gpus all --network host --ipc host \
      --ulimit memlock=-1 --ulimit stack=67108864 \
      -v /home/windpacer/.cache/huggingface:/root/.cache/huggingface \
      -v /home/windpacer/.cache/vllm:/root/.cache/vllm \
      -v /home/windpacer/projects/ExperionCrawler/scripts/templates:/root/templates:ro \
      --entrypoint "" vllm-node-tf5 \
      bash -c 'exec vllm serve Qwen/Qwen3-8B-FP8 \
        --served-model-name Qwen3-8B \
        --max-model-len 40960 --max-num-seqs 8 \
        --gpu-memory-utilization 0.20 \
        --port 8002 --host 0.0.0.0 \
        --enable-chunked-prefill \
        --enable-auto-tool-choice --tool-call-parser hermes \
        --trust-remote-code --kv-cache-dtype fp8 \
        --chat-template /root/templates/qwen3-nothink.jinja \
        -tp 1'
    
  • FAKE-param 다수 발생 → 새 invention 모드. Verifier R6 후보로 별도 보고: "find_tags 의 허용 인자: query, area, sub_area, top_k 만. 그 외 인자 거부"

완료 보고 받으면 다음 단계:

  • 8B 채택 시 → C2 LoRA 학습지시서 (Qwen3-8B bf16 베이스)
  • 35B 채택 시 → C2 LoRA 학습지시서 (Qwen3.6-35B-A3B attention-only, MoE-safe)