461 lines
22 KiB
Markdown
461 lines
22 KiB
Markdown
# 작업지시서 — Phase C LoRA 가속 (C1 데이터 큐레이션 우선)
|
|
|
|
> 대상: 코딩 LLM. C1을 우선 완료, C2·C3는 진입 조건 만족 시 별도 트리거.
|
|
> 상위 플랜: `plans/OPUS-v2-실행계획.md` Phase C.
|
|
> Phase B(Verifier) 완료 후의 후속 — Verifier 로그를 **학습 데이터의 1차 소스**로 활용.
|
|
|
|
---
|
|
|
|
## 0. 배경 (왜 LoRA, 왜 지금)
|
|
|
|
Phase B Verifier가 invention을 *런타임*에서 결정적으로 차단하고 있지만, 이건 **증상 차단**입니다.
|
|
근본 원인 = 모델이 *애초에* 잘못된 호출을 생성하는 행동 패턴. 이걸 **모델 weights에 박는** 게 LoRA의 역할:
|
|
- "사용자가 명시 안 한 태그는 *항상* `find_tags` 먼저 호출"
|
|
- "area는 `P6-1` 형식만 사용"
|
|
- "툴이 빈 결과 반환 시 자기 인자를 의심"
|
|
|
|
**핵심 인사이트**: Verifier 거부 로그(`mcp-server/verifier/logs/*.jsonl`)가 *이미 production에서* "잘못된 호출 → hint → 자기교정" 행동 데이터를 자동 수집 중. 이걸 가공만 하면 **즉시 학습 데이터**.
|
|
|
|
---
|
|
|
|
## C1 — 데이터 큐레이션 (1주, 본 지시서의 주력)
|
|
|
|
### C1.1 입력 소스
|
|
|
|
| 소스 | 경로 | 양 | 비고 |
|
|
|---|---|---|---|
|
|
| **Verifier 거부 로그** | `mcp-server/verifier/logs/*.jsonl` | 현재 3건, 운영 기간 늘수록 자동 증가 | 1차 소스. "잘못된 호출 패턴" 그대로 |
|
|
| **운전원 검수** | (사람 in the loop) | 정선 | 변환된 대화의 hint·자기교정이 합리적인지 |
|
|
| **수동 보강** | C1 작업자 작성 | Verifier가 못 잡은 패턴 추가 | 카테고리 균형 위해 |
|
|
| **Phase 0 golden** | `mcp-server/eval/golden.jsonl` | 40 | **학습에 사용 금지**(평가셋 누수 차단). 다만 카테고리 분포 참고 |
|
|
|
|
### C1.2 출력 — `mcp-server/training/` 신규
|
|
|
|
| 파일 | 포맷 | 용도 |
|
|
|---|---|---|
|
|
| `training/sft_data.jsonl` | OpenAI messages | C2 SFT-LoRA 입력 |
|
|
| `training/sft_data_stats.md` | 통계 | 카테고리 분포·토큰 분포·중복 여부 |
|
|
| `training/curate_from_verifier.py` | 스크립트 | Verifier 로그 → ShareChat 변환 |
|
|
|
|
### C1.3 변환 규칙 — Verifier 거부 1건 → ShareChat 대화 1건
|
|
|
|
각 거부 로그를 **5턴 대화**로:
|
|
|
|
```json
|
|
{"messages":[
|
|
{"role":"system","content":"<plant_context 요약 + grounding 규칙>"},
|
|
{"role":"user","content":"<자연어 질문 — 거부 시 user input을 운전원이 작성>"},
|
|
{"role":"assistant","content":"<잘못된 도구 호출 — params 그대로>"},
|
|
{"role":"tool","content":"<verifier_error + hint>"},
|
|
{"role":"assistant","content":"<올바른 자기교정 호출 — 운전원이 작성>"}
|
|
]}
|
|
```
|
|
|
|
**예** (현재 로그의 RM-6101 거부):
|
|
- user: "6-1차 플랜트 원료 투입 경로 알려줘"
|
|
- assistant: `[trace_connections] start_tag="RM-6101" direction="downstream"` (잘못)
|
|
- tool: `{"verifier_error":"R1.tag_not_found","hint":"...find_tags 먼저...","suggested":["f-6101a","f-6101b","fcv-6101"]}`
|
|
- assistant: `[find_tags] query="원료 투입" sub_area="P6-1"` (자기교정 = 올바른 행동)
|
|
|
|
학습 후 모델이 **그 자기교정 행동을 *처음부터* 수행**하게 됨.
|
|
|
|
#### C1.3.1 에러 유형별 conversion mapping
|
|
|
|
| Verifier Error | 자동 생성 `user_input` | 자동 생성 `corrected_call` |
|
|
|---|---|---|
|
|
| `R1.invalid_tag_format(X)` | `"X 관련 태그 찾아줘"` — X를 query로 | `find_tags(query=X_ko)` (X의 한글 추출 또는 X를 그대로 query로) |
|
|
| `R1.tag_not_found(X)` | `"X 경로/상태 알려줘"` — 실제 base_tag로 오인한 X를 검색 | `find_tags(query=X)` — 먼저 검색 후 정확한 태그로 재시도 |
|
|
| `R2.invalid_area_format(X)` | `"X area 상태 알려줘"` — 잘못된 area를 사용자 발화로 | 인자 형식 교정: `active_alarms(area=correct)` 또는 `query_events(area=correct)` |
|
|
| `R2.unknown_area(X)` | `"X 플랜트 알람 봐줘"` — 존재하지 않는 area | `find_tags(area=correct)` — 존재하는 area로 query 우회 |
|
|
| `R4.invalid_direction(X)` | `"X 방향으로 경로 추적"` — 잘못된 direction | `trace_connections(direction="upstream" 또는 "downstream")` |
|
|
| `R4.max_depth_out_of_range(X)` | `"깊게 추적해줘"` — 범위 초과 depth | `trace_connections(max_depth=20)` — 기본값으로 교정 |
|
|
|
|
각 Verifier 로그의 `verifier_error.code`와 일치하는 행의 template을 적용.
|
|
|
|
#### C1.3.2 `curate_from_verifier.py` 의사코드
|
|
|
|
```python
|
|
"""
|
|
curate_from_verifier.py — Verifier 거부 로그 → ShareChat 5-turn 대화 변환.
|
|
|
|
입력: mcp-server/verifier/logs/*.jsonl
|
|
출력: mcp-server/training/sft_data.jsonl (PLACEHOLDER 포함)
|
|
mcp-server/training/sft_data_stats.md (통계)
|
|
|
|
변환 전략:
|
|
각 거부 로그에 대해 에러 코드 기반 매핑 테이블(C1.3.1) 조회:
|
|
- user_msg = _gen_user_input(tool, params, error)
|
|
- wrong_call = params 그대로
|
|
- hint = verifier_error.hint 그대로
|
|
- corrected_call = _gen_corrected_call(error, params)
|
|
→ 5-turn messages 구성, sft_data.jsonl에 append.
|
|
"""
|
|
|
|
import json, hashlib, pathlib, sys
|
|
from difflib import SequenceMatcher
|
|
|
|
SYSTEM_PROMPT = (
|
|
"당신은 P6(6차) PGMEA 플랜트 운전 지원 assistant입니다. "
|
|
"사용자가 명시하지 않은 태그는 find_tags로 먼저 검색합니다. "
|
|
"area는 'P6-1' 형식만 사용합니다. "
|
|
"DB에 존재하지 않는 태그·area는 추측하지 않고 검색하거나 거절합니다."
|
|
)
|
|
|
|
CONVERSION_MAP = {
|
|
"R1.invalid_tag_format": {
|
|
"user_template": "{param} 관련 태그 찾아줘",
|
|
"corrected_tool": "find_tags",
|
|
"corrected_params": lambda p: {"query": str(p.get("start_tag", p.get("query", "")))},
|
|
},
|
|
"R1.tag_not_found": {
|
|
"user_template": "{param} 경로 알려줘",
|
|
"corrected_tool": "find_tags",
|
|
"corrected_params": lambda p: {"query": str(p.get("start_tag", p.get("query", "")))},
|
|
},
|
|
"R2.invalid_area_format": {
|
|
"user_template": "{param[area]} 상태 알려줘",
|
|
"corrected_tool": None, # 교정된 area로 동일 도구 재호출
|
|
"corrected_params": lambda p: {**p, "area": "P" + p.get("area", "").lstrip("P")},
|
|
},
|
|
"R2.unknown_area": {
|
|
"user_template": "area 목록 중에 골라줘",
|
|
"corrected_tool": "find_tags",
|
|
"corrected_params": lambda p: {"area": p.get("area", "").split("-")[0]},
|
|
},
|
|
"R4.invalid_direction": {
|
|
"user_template": "{param[direction]} 방향 추적",
|
|
"corrected_tool": None,
|
|
"corrected_params": lambda p: {**p, "direction": "upstream" if p.get("direction") != "upstream" else "downstream"},
|
|
},
|
|
"R4.max_depth_out_of_range": {
|
|
"user_template": "적당한 깊이로 추적해줘",
|
|
"corrected_tool": None,
|
|
"corrected_params": lambda p: {**p, "max_depth": 20},
|
|
},
|
|
}
|
|
|
|
def _tag_as_param(params: dict) -> str:
|
|
return str(params.get("start_tag", params.get("query", params.get("tag_name", ""))))
|
|
|
|
def _gen_user_input(tool: str, params: dict, err: dict) -> str:
|
|
code = err.get("verifier_error", "")
|
|
entry = CONVERSION_MAP.get(code)
|
|
if not entry:
|
|
return f"{_tag_as_param(params)} 관련 정보 알려줘"
|
|
template = entry["user_template"]
|
|
param = params.get("start_tag", params.get("query", params.get("area", params.get("tag_name", ""))))
|
|
return template.format(param=param, param_parts=params)
|
|
|
|
def _gen_corrected_call(tool: str, params: dict, err: dict) -> dict:
|
|
code = err.get("verifier_error", "")
|
|
entry = CONVERSION_MAP.get(code)
|
|
if not entry:
|
|
return {"tool": "find_tags", "params": {"query": _tag_as_param(params)}}
|
|
new_params = entry["corrected_params"](params)
|
|
new_tool = entry["corrected_tool"] or tool
|
|
return {"tool": new_tool, "params": new_params}
|
|
```
|
|
|
|
변환 후 일부 필드는 `__PLACEHOLDER__`로 남겨 운전원 검수(C1.4 step 2)에서 수동 작성.
|
|
|
|
### C1.4 작업 단계
|
|
|
|
1. **`curate_from_verifier.py` 스크립트** 작성:
|
|
- `verifier/logs/*.jsonl` 읽어 거부 trip 추출
|
|
- 각 trip을 위 5턴 템플릿으로 변환
|
|
- user input·자기교정 assistant turn은 placeholder(`<<USER_INPUT>>`, `<<CORRECTED_CALL>>`)로 두고 검수 출력
|
|
2. **운전원 검수** — 구체 workflow:
|
|
|
|
a. **검수 파일 생성**: `curate_from_verifier.py` 실행 → `training/sft_data.jsonl` 생성
|
|
(user_input 및 corrected_call 중 일부는 `__PLACEHOLDER__` 상태)
|
|
|
|
b. **운전원 편집** (30~60분, vim/vscode로 직접 JSONL 편집):
|
|
- 각 line의 `messages[1].content` (`__PLACEHOLDER__`)를 현실적인 질문으로 대체
|
|
- 예: `__PLACEHOLDER__` → "6-1차 원료 탱크에서 어디로 가는지 경로 추적해줘"
|
|
- 각 line의 `messages[4].content` (`__PLACEHOLDER__`)를 정확한 도구 호출로 대체
|
|
- 예: `__PLACEHOLDER__` → `[find_tags] query="원료 투입" sub_area="P6-1"`
|
|
- hint(turn 3)는 자동 생성된 Verifier hint 그대로 사용 (수정 금지)
|
|
|
|
c. **검수 기준**:
|
|
- user input이 실제 운전원이 할 법한 질문인가? (현장 용어 사용)
|
|
- 자기교정 응답이 정확한 도구·인자인가? (find_tags의 query/sub_area, 또는 정확한 base_tag)
|
|
- wrong call(turn 2)과 corrected call(turn 4)의 차이가 학습할 만한 교정인가?
|
|
|
|
d. **검증 스크립트 실행** (수정 후):
|
|
```bash
|
|
cd mcp-server
|
|
.venv/bin/python -c "
|
|
import json
|
|
for i, line in enumerate(open('training/sft_data.jsonl'), 1):
|
|
d = json.loads(line)
|
|
msgs = d['messages']
|
|
assert len(msgs) == 5, f'line {i}: expected 5 msgs, got {len(msgs)}'
|
|
assert msgs[0]['role'] == 'system'
|
|
assert msgs[1]['role'] == 'user'
|
|
assert msgs[2]['role'] == 'assistant'
|
|
assert msgs[3]['role'] == 'tool'
|
|
assert msgs[4]['role'] == 'assistant'
|
|
assert '__PLACEHOLDER__' not in json.dumps(d), f'line {i}: has PLACEHOLDER'
|
|
print('All OK')
|
|
"
|
|
```
|
|
|
|
e. **완료 서명**: operator가 검수 완료 시 `sft_data.jsonl`을 그대로 C2 단계로 전달.
|
|
3. **수동 보강** — abstain 25건 + scaffold 25건 (golden.jsonl의 category 분포 균형):
|
|
|
|
- Phase 0 골든의 5 카테고리 분포: tool_call(10), nl2sql(10), ground(6), abstain(8), scaffold(6)
|
|
- Verifier 거부는 주로 tool_call·grounding 카테고리 → **abstain(scaffold 추가 작성** 총 50건
|
|
- **절대 golden question을 복사하지 말 것** — category 분포만 참고, question은 직접 창작
|
|
|
|
**abstain template 예시** (총 25건 중 첫 5건 예시):
|
|
```json
|
|
{"id":"abstain-m01","category":"abstain","question":"P3 플랜트 생산량 예측해줘",
|
|
"context":"P3 area는 펌프 상태만 수집, 생산량 데이터 없음",
|
|
"expect":{"refuse":true,"must_not":["톤","kg","예상 생산량은"]}}
|
|
{"id":"abstain-m02","category":"abstain","question":"ficq-6113 고장 원인 진단해줘",
|
|
"context":"PV=0, SP=50 값만 있음. 고장 진단 데이터 없음",
|
|
"expect":{"refuse":true,"must_not":["센서","밸브","모터"]}}
|
|
{"id":"abstain-m03","category":"abstain","question":"p-6102 다음 정비 일정 알려줘",
|
|
"context":"정비 일정·이력 DB 없음. pump 상태만 수집",
|
|
"expect":{"refuse":true}}
|
|
{"id":"abstain-m04","category":"abstain","question":"6-1차 생산라인 수율이 얼마야",
|
|
"context":"생산 수율 데이터는 수집하지 않음. 공정 변수만 있음",
|
|
"expect":{"refuse":true}}
|
|
{"id":"abstain-m05","category":"abstain","question":"FICQ-6113 센서 모델명이 뭐야",
|
|
"context":"기기 명세·모델명 DB 없음. tag_metadata에 desc만 저장",
|
|
"expect":{"refuse":true}}
|
|
```
|
|
|
|
**scaffold template 예시** (총 25건 중 첫 5건 예시):
|
|
```json
|
|
{"id":"scaffold-m01","category":"scaffold","question":"리플럭스 밸브 열어도 될까?",
|
|
"context":"ficq-6113.pv=45, SP=50, 레인지 0~100 kg/hr",
|
|
"expect":{"steps":["제어변수","현재값","설정치","제약","판단"],"order":true}}
|
|
{"id":"scaffold-m02","category":"scaffold","question":"pica-6111 압력이 200인데 괜찮아?",
|
|
"context":"pica-6111.pv=200, SP=760, 레인지 0~760 mmHg. 측류추출 진공탑은 저압이 정상",
|
|
"expect":{"steps":["제어변수","현재값","설정치","제약","판단"],"order":true}}
|
|
{"id":"scaffold-m03","category":"scaffold","question":"C-6111 하부 온도가 높아졌어",
|
|
"context":"ti-6102.pv=175, SP=160, 레인지 0~300°C. PGMEA 분해온도 180°C",
|
|
"expect":{"steps":["제어변수","현재값","설정치","제약","판단"],"order":true}}
|
|
{"id":"scaffold-m04","category":"scaffold","question":"P-6102 토출압이 떨어졌어",
|
|
"context":"p-6102.pv={R-RUN|5|}, pt-6101.pv=3.5 kg/cm², 정상 5~6 kg/cm²",
|
|
"expect":{"steps":["제어변수","현재값","설정치","제약","판단"],"order":true}}
|
|
{"id":"scaffold-m05","category":"scaffold","question":"VP-6117 진공 안 잡히는데 어디 봐야 해?",
|
|
"context":"vp-6117.pv={R-RUN|5|}, pica-6111.pv=680 mmHg (대기압 760 = 진공 약함), 정상 <100 mmHg",
|
|
"expect":{"steps":["제어변수","현재값","설정치","제약","판단"],"order":true}}
|
|
```
|
|
|
|
**scaffold 패턴 골자** — 5단계 추론 유도:
|
|
1. 제어변수 식별 (어떤 태그·PV인가)
|
|
2. 현재값 확인 (PV 수치)
|
|
3. 설정치/레인지 확인 (SP / 레인지 상하한)
|
|
4. 제약 조건 확인 (분해온도·정상범위·운전한계)
|
|
5. 판단 (정상·주의·위험 + 조치 방향)
|
|
|
|
나머지 40건은 위 template을 변형(area/태그번호/수치 변경)하여 작성.
|
|
|
|
**tool_call 보강** (10건, Verifier 에서 부족한 패턴):
|
|
- "원료가 어떤 경로로 들어오는지", "P6-2 밸브 상태 확인" → `find_tags` 먼저 호출 유도
|
|
- "온도 트랜드 보고서", "shift 요약" → `generate_status_report` 또는 `summarize_events`
|
|
|
|
**grounding 보강** (10건, 공정 지식 부족 패턴):
|
|
- "측류추출이 뭐야", "진공 증류 왜 해" → 공정 설명 요구
|
|
- "P6-1과 P6-2 차이" → 영역 구분 지식
|
|
4. **누수 차단** — 3단계 중복 제거:
|
|
|
|
a. **정확 해시 매칭**: golden.jsonl의 각 `question` 필드 md5 해시 세트를 구축.
|
|
sft_data candidate의 `messages[1].content` md5가 golden 해시 세트에 존재 → 제거.
|
|
|
|
b. **Fuzzy 매칭**: `difflib.SequenceMatcher(None, user_content, golden_q).ratio() > 0.9`
|
|
→ 의미는 같고 표현만 다른 question 차단 (예: "알람 봐줘" vs "알람 보여줘")
|
|
|
|
c. **Embedding guard** (선택): `nomic-embed-text`로 golden + candidate 임베딩, cosine > 0.95 제거.
|
|
`mcp-server/`의 Ollama 클라이언트 재사용 가능.
|
|
|
|
구현 코드 (curate_from_verifier.py 내):
|
|
```python
|
|
GOLDEN_PATH = pathlib.Path(__file__).parent.parent / "eval" / "golden.jsonl"
|
|
|
|
def _golden_question_hashes() -> set[str]:
|
|
hashes = set()
|
|
for line in open(GOLDEN_PATH):
|
|
d = json.loads(line)
|
|
hashes.add(hashlib.md5(d["question"].encode()).hexdigest())
|
|
return hashes
|
|
|
|
def _is_leak(user_content: str, golden_hashes: set[str], golden_items: list[dict]) -> bool:
|
|
h = hashlib.md5(user_content.encode()).hexdigest()
|
|
if h in golden_hashes:
|
|
return True
|
|
for g in golden_items:
|
|
if SequenceMatcher(None, user_content, g["question"]).ratio() > 0.9:
|
|
return True
|
|
return False
|
|
```
|
|
|
|
**주의**: 수동 보강 작성 시에도 golden question을 참고만 하고 복사 금지.
|
|
golden category 분포는 참고 가능하나 question 텍스트 직접 재사용 금지.
|
|
5. **통계 출력** (`sft_data_stats.md`):
|
|
- 총 건수, 카테고리별 분포, 평균 토큰 수, golden 중복 0 확인
|
|
|
|
### C1.5 수용 기준 (C1)
|
|
|
|
- [ ] `curate_from_verifier.py` py_compile 통과
|
|
- [ ] `sft_data.jsonl` **≥ 100건** (Verifier 로그 변환 + 수동 보강)
|
|
- [ ] 카테고리 분포 ≥ 4종 포함 (tool_call·grounding·abstain·scaffold 권장)
|
|
- [ ] golden.jsonl 누수 0건 (해시 비교 + fuzzy guard)
|
|
- [ ] 각 라인 JSON 스키마 검증 통과:
|
|
- `json.loads()` 정상 파싱
|
|
- `messages` 배열 정확히 **5개**의 turn: system / user / assistant / tool / assistant
|
|
- `messages[0].role == "system"`, `messages[1].role == "user"`, `messages[2].role == "assistant"`, `messages[3].role == "tool"`, `messages[4].role == "assistant"`
|
|
- tool turn(`messages[3]`)의 `content`는 JSON 문자열 또는 plain text hint 포함
|
|
- corrected assistant(`messages[4]`)의 `content`는 tool_calls 또는 텍스트 직접 응답
|
|
- `__PLACEHOLDER__` 문자열 미포함 (검수 완료 기준)
|
|
- [ ] `sft_data_stats.md` 자동 생성 — 포함 항목:
|
|
- 총 건수, Verifier 변환 건수 vs 수동 보강 건수
|
|
- 카테고리별 분포 (건수 + 비율)
|
|
- 평균/최소/최대 messages 토큰 수 (tokenizer-free: 공백 기준 split)
|
|
- golden 누수 0 확인
|
|
- `__PLACEHOLDER__` 잔여 0 확인
|
|
|
|
---
|
|
|
|
## C2 — SFT-LoRA 학습 (1주, C1 완료 후 트리거)
|
|
|
|
> 이 섹션은 C1 산출물(`sft_data.jsonl` ≥ 100건) 확보 시점에 본격화. 지금은 outline·결정 사항만.
|
|
|
|
### C2.1 베이스 모델 선정 (선결 필요)
|
|
|
|
| 후보 | 장점 | 단점 |
|
|
|---|---|---|
|
|
| **Qwen3-8B bf16** | 이미 production 호환 검증(Phase 0), dense → LoRA 깨끗 | 8B 한계 (L2~L4 부족) — but LoRA로 학습 후 차이 확인 |
|
|
| Qwen2.5-7B-Instruct bf16 | obedient, 사고모드 없음 | 한국어 약간↓ |
|
|
| Qwen3-14B bf16 (있다면) | 중간 capacity | 미확인 |
|
|
|
|
→ **1차: Qwen3-8B bf16**. 실패 시 7B-Instruct로 fallback.
|
|
⚠️ **bf16 베이스 필수** (FP8/FP8-dynamic은 LoRA 학습 부적합 — OPUS-v1 §3.1 함정).
|
|
|
|
### C2.2 환경
|
|
|
|
- Unsloth 컨테이너 (별도 setup. OPUS-v1 §2 참고하되 35B는 보존 — 학습 컨테이너는 별도 GPU 시간 점유)
|
|
- 또는 호스트에서 직접 `unsloth` pip 설치 (간단)
|
|
|
|
### C2.3 학습 하이퍼파라미터 (1차 안)
|
|
|
|
```python
|
|
from unsloth import FastLanguageModel
|
|
model, tokenizer = FastLanguageModel.from_pretrained(
|
|
model_name="Qwen/Qwen3-8B", # bf16
|
|
max_seq_length=4096,
|
|
dtype=torch.bfloat16,
|
|
load_in_4bit=True, # QLoRA
|
|
)
|
|
model = FastLanguageModel.get_peft_model(
|
|
model,
|
|
r=64, lora_alpha=128,
|
|
target_modules=["q_proj","k_proj","v_proj","o_proj"], # attention-only 1차
|
|
lora_dropout=0, bias="none",
|
|
use_gradient_checkpointing="unsloth",
|
|
use_rslora=True,
|
|
)
|
|
# TrainingArguments: per_device_train_batch_size=2, grad_accum=4,
|
|
# max_steps=200, learning_rate=2e-4, warmup=5, save_steps=50
|
|
```
|
|
|
|
### C2.4 산출
|
|
|
|
- `lora-adapters/calibration-v1/` — 어댑터 가중치
|
|
- `lora-adapters/calibration-v1/training_log.json` — loss 곡선
|
|
- `lora-adapters/calibration-v1/eval_report.md` — Phase 0 골든 + Verifier reject rate (전/후)
|
|
|
|
### C2.5 수용 기준 (C2)
|
|
|
|
- [ ] 학습 손실 안정 수렴 (eval loss 단조 감소 or 안정)
|
|
- [ ] Phase 0 골든 평가에서 **회귀 0건**
|
|
- [ ] 사후 production 시뮬레이션(opencode에서 invention 재현 질문 5건): **Verifier reject 비율 ≥ 50% 감소** (모델이 자기교정 행동을 학습)
|
|
- [ ] 어댑터 hot-swap or merge 검증 (v1 §4.1 절차)
|
|
|
|
**회귀 트리거** — Phase 0 eval에서 **회귀 ≥ 2건** 또는 **invention rate 개선 < 30%** 이면:
|
|
→ `r ∈ {32, 128}` sweep + `target_modules`에 `gate_proj/up_proj/down_proj` 추가(전결합 LoRA) 1회 시도
|
|
→ r=64 / attention-only가 목표 데이터에 적합하지 않을 가능성에 대한 안전판
|
|
|
|
---
|
|
|
|
## C3 — DPO 정렬 (1~2주, 진입 조건부)
|
|
|
|
> 진입 조건: C2 어댑터로 production 운영 ≥ 2주 + 운전원 👍/👎 데이터 ≥ 200쌍.
|
|
|
|
### C3.1 사전 작업 (별도 트랙)
|
|
|
|
- **opencode 또는 Web UI에 👍/👎 버튼 추가** (응답마다 binary 피드백) — 별도 작업지시서 필요
|
|
- 피드백을 `feedback/preferences.jsonl` 에 적재 (응답 텍스트 + tool calls + binary label)
|
|
|
|
### C3.2 DPO 데이터 구성
|
|
|
|
- chosen = 👍 응답
|
|
- rejected = 👎 응답 (또는 Verifier reject된 응답)
|
|
- 자동 보상 보조: Verifier 통과/실패를 binary 보상으로 가중
|
|
|
|
### C3.3 학습
|
|
|
|
- `trl.DPOTrainer` (베이스: C2 SFT 어댑터를 ref_model로)
|
|
- beta=0.1, learning_rate=5e-6, 1 epoch
|
|
|
|
### C3.4 산출 / 수용 기준
|
|
|
|
- `lora-adapters/calibration-v2-dpo/`
|
|
- **fabrication_rate** (Phase 0 eval) C2 대비 추가 개선
|
|
- 운전원 만족도 (👍 비율) 추적 — C2 어댑터 대비 향상
|
|
|
|
---
|
|
|
|
## 산출물 종합
|
|
|
|
| Phase | 신규 파일 | 변경 |
|
|
|---|---|---|
|
|
| C1 | `mcp-server/training/curate_from_verifier.py`<br>`mcp-server/training/sft_data.jsonl`<br>`mcp-server/training/sft_data_stats.md` | — |
|
|
| C2 | `lora-adapters/calibration-v1/*` | — |
|
|
| C3 | `lora-adapters/calibration-v2-dpo/*`<br>`feedback/preferences.jsonl` (수집 시작) | UI에 👍/👎 추가 |
|
|
|
|
---
|
|
|
|
## 하지 말 것 (금지)
|
|
|
|
- ❌ **Phase 0 골든셋 학습에 사용** (평가셋 누수 → 측정 무력화)
|
|
- ❌ FP8/FP8-dynamic 베이스로 LoRA 학습 시도 (수치 손상 — 우리가 본 RedHatAI 7B 사례)
|
|
- ❌ MoE expert 모듈에 LoRA 적용 (v1 §3.2 — vLLM hot-swap 깨짐). attention-only.
|
|
- ❌ 자동/야간 자기학습 루프 (검수 없는 학습 = model collapse — v1 §5.2.B 폐기)
|
|
- ❌ Verifier 룰 우회를 위해 데이터에 가짜 거부/hint 작성 (학습 신호 오염)
|
|
- ❌ `mcp-server/eval/`·`verifier/validators.py` 수정 (Phase C 범위 밖)
|
|
|
|
---
|
|
|
|
## 즉시 착수 (C1)
|
|
|
|
```bash
|
|
mkdir -p mcp-server/training
|
|
# 작업자가 만들 것: curate_from_verifier.py
|
|
# 입력: mcp-server/verifier/logs/*.jsonl
|
|
# 출력: mcp-server/training/sft_data.jsonl + sft_data_stats.md
|
|
|
|
# 검증:
|
|
cd mcp-server
|
|
python3 -m py_compile training/curate_from_verifier.py
|
|
.venv/bin/python -c "
|
|
import json
|
|
items = [json.loads(l) for l in open('training/sft_data.jsonl')]
|
|
print(f'total: {len(items)}')
|
|
print(f'avg msgs: {sum(len(i[\"messages\"]) for i in items)/len(items):.1f}')
|
|
"
|
|
# 골든 누수 검사:
|
|
.venv/bin/python -c "
|
|
import json, hashlib
|
|
g = {hashlib.md5(json.loads(l)['question'].encode()).hexdigest() for l in open('eval/golden.jsonl')}
|
|
s = [json.loads(l) for l in open('training/sft_data.jsonl')]
|
|
leaks = [i for i in s if any(hashlib.md5(m['content'].encode()).hexdigest() in g for m in i['messages'] if m['role']=='user')]
|
|
print(f'leaks: {len(leaks)}')
|
|
"
|
|
```
|
|
|
|
완료 보고: `total / 카테고리 분포 / golden 누수 (0 기대) / 거부 변환 vs 수동 보강 비율`. 보고 받은 뒤 C2 트리거.
|