feat(meet): 회의록 가이드 v2 반영 + 모델 출력 붕괴 복원력 (v2.2.253)
회의록 출력물 개선 (실무 회의록 가이드 v2): - 섹션 우선순위 재정렬(①결정 ②액션 ③오픈이슈 ④리스크 ⑤논의) - 논의사항 주제별 bullet 간결화, 오픈 이슈 섹션 복원 - 액션 아이템 산출물 컬럼 추가(담당·작업·기한·산출물 4요소), 담당자 개인 우선 - Executive Summary 결과 중심, 결정사항은 확정된 것만 모델 출력 붕괴(degeneration) 대응: - callLmSynthesis 재시도 내장(repeat_penalty↑/top_k↓로 반복 억제 강화) + looksDegenerate 감지 - 긴 녹취 조각 실패 시 절반 분할 재귀 재시도(12K→6K→3.5K) - 부분 회의록 fallback(한 조각 실패해도 전체 중단 안 함) 하위호환: 액션 표 파서 신6컬럼/구5컬럼 모두 파싱, 섹션 번호 무관 탐지, 회의일 추출 일시/날짜 둘 다 인식. 테스트 +13건(전체 659 통과). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -619,16 +619,56 @@ async function runMeet(arg: string, view: Webview | undefined, context?: vscode.
|
||||
chunk(view, `🧩 **긴 녹취록 — 2단계 합성** (조각 ${segments.length}개 × ~${(SEG_SIZE / 1000) | 0}K자, 모델 \`${model}\`)\n`);
|
||||
const extractSystem = '당신은 회의 녹취 사실 추출기입니다. 제공된 조각에 명시된 내용만 형식대로 추출하고, 없는 사실을 만들지 않습니다. 모든 출력은 한국어입니다.';
|
||||
const notes: string[] = [];
|
||||
let failedSegs = 0;
|
||||
// 약한 모델이 큰 조각에서 출력 붕괴(반복/깨짐)할 때, 그 조각을 줄 경계로
|
||||
// 절반씩 쪼개 재귀 재시도한다. 입력을 줄이면 붕괴 확률이 떨어지므로 모델
|
||||
// 교체 없이도 성공률이 오른다. MIN_SEG 이하인데도 실패하면 그 구간만 포기.
|
||||
const MIN_SEG = 3500;
|
||||
const extractSeg = async (seg: string, label: string, idx: number, total: number): Promise<string | null> => {
|
||||
try {
|
||||
// callLmSynthesis 가 내부적으로 재시도(반복 억제 강화)까지 수행한다.
|
||||
const note = await callLmSynthesis(buildMeetExtractPrompt(seg, metadata, idx, total), extractSystem);
|
||||
if (!note) throw new Error('추출 결과가 비어 있습니다.');
|
||||
return note.trim();
|
||||
} catch (segErr: any) {
|
||||
if (seg.length <= MIN_SEG) {
|
||||
chunk(view, ` ⚠️ 조각 ${label} 추출 실패(최소 크기 도달, 건너뜀): ${segErr?.message || String(segErr)}\n`);
|
||||
return null;
|
||||
}
|
||||
chunk(view, ` ↩︎ 조각 ${label}(${seg.length.toLocaleString()}자) 출력 붕괴 → 절반으로 쪼개 재시도\n`);
|
||||
const lines = seg.split('\n');
|
||||
let cut = 0, acc = 0;
|
||||
for (; cut < lines.length - 1; cut++) { acc += lines[cut].length + 1; if (acc >= seg.length / 2) break; }
|
||||
const left = lines.slice(0, cut + 1).join('\n');
|
||||
const right = lines.slice(cut + 1).join('\n');
|
||||
if (!left.trim() || !right.trim()) return null; // 더는 못 쪼갬
|
||||
const parts = [
|
||||
await extractSeg(left, `${label}a`, idx, total),
|
||||
await extractSeg(right, `${label}b`, idx, total),
|
||||
].filter(Boolean) as string[];
|
||||
return parts.length ? parts.join('\n\n') : null;
|
||||
}
|
||||
};
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
chunk(view, ` ⏳ 조각 ${i + 1}/${segments.length} 추출 중…`);
|
||||
chunk(view, ` ⏳ 조각 ${i + 1}/${segments.length} 추출 중…\n`);
|
||||
const t0 = Date.now();
|
||||
const note = await callLmSynthesis(
|
||||
buildMeetExtractPrompt(segments[i], metadata, i + 1, segments.length),
|
||||
extractSystem,
|
||||
);
|
||||
if (!note) throw new Error(`조각 ${i + 1} 추출 결과가 비어 있습니다.`);
|
||||
notes.push(`### ─── 조각 ${i + 1}/${segments.length} 노트 ───\n${note.trim()}`);
|
||||
chunk(view, ` ✓ (${Math.round((Date.now() - t0) / 1000)}s)\n`);
|
||||
const note = await extractSeg(segments[i], String(i + 1), i + 1, segments.length);
|
||||
if (note) {
|
||||
notes.push(`### ─── 조각 ${i + 1}/${segments.length} 노트 ───\n${note}`);
|
||||
chunk(view, ` ✓ 조각 ${i + 1} 완료 (${Math.round((Date.now() - t0) / 1000)}s)\n`);
|
||||
} else {
|
||||
// 한 조각이 끝내 실패해도 전체 회의록을 포기하지 않는다 — 누락을
|
||||
// 명시하고 나머지 조각으로 부분 회의록을 만든다.
|
||||
failedSegs++;
|
||||
notes.push(`### ─── 조각 ${i + 1}/${segments.length} 노트 ───\n(이 구간은 모델 출력 오류로 추출하지 못했습니다.)`);
|
||||
chunk(view, ` ⚠️ 조각 ${i + 1} 추출 실패(건너뜀)\n`);
|
||||
}
|
||||
}
|
||||
if (failedSegs === segments.length) {
|
||||
throw new Error(`모든 조각(${segments.length}개) 추출에 실패했습니다 — 모델 출력이 계속 붕괴합니다.`);
|
||||
}
|
||||
if (failedSegs > 0) {
|
||||
chunk(view, `\n⚠️ ${segments.length}개 중 ${failedSegs}개 구간을 추출하지 못해 **부분 회의록**으로 진행합니다. 더 큰 모델(예: 27B+) 사용을 권장합니다.\n`);
|
||||
}
|
||||
groundingNotes = notes.join('\n\n');
|
||||
// ── Reduce: 노트 병합 → 최종 회의록 ──
|
||||
@@ -639,7 +679,17 @@ async function runMeet(arg: string, view: Webview | undefined, context?: vscode.
|
||||
chunk(view, ` ✓ (${Math.round((Date.now() - t1) / 1000)}s)\n\n`);
|
||||
}
|
||||
} catch (e: any) {
|
||||
chunk(view, `\n\n⚠️ 회의록 합성 실패: ${e?.message || String(e)}\n(LM 서버가 떠 있는지, \`g1nation.ollamaUrl\` / \`defaultModel\` 설정을 확인하세요.)\n\n`);
|
||||
const msg = e?.message || String(e);
|
||||
const degen = /붕괴|반복|깨|parse input/i.test(msg);
|
||||
chunk(view, `\n\n⚠️ 회의록 합성 실패: ${msg}\n`);
|
||||
if (degen) {
|
||||
chunk(view, `\n💡 모델 출력이 반복/깨짐(degeneration)으로 붕괴한 것으로 보입니다. 재시도(반복 억제 강화)에도 풀리지 않았습니다.\n`
|
||||
+ ` · 현재 모델 \`${model}\` 이 긴 한국어 녹취록에는 약할 수 있습니다 — **더 큰 모델(27B+급)** 로 \`g1nation.defaultModel\` 변경을 권장합니다.\n`
|
||||
+ ` · 또는 녹취록을 더 짧게 나눠 여러 번 \`/meet\` 하면 성공률이 올라갑니다.\n`);
|
||||
} else {
|
||||
chunk(view, `(LM 서버가 떠 있는지, \`g1nation.ollamaUrl\` / \`defaultModel\` 설정을 확인하세요.)\n`);
|
||||
}
|
||||
chunk(view, '\n');
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -729,7 +779,7 @@ async function runMeet(arg: string, view: Webview | undefined, context?: vscode.
|
||||
if (cls.route === 'hold') {
|
||||
holds.push({
|
||||
idx: holds.length + 1,
|
||||
owner: task.owner, work: task.work, detail: task.detail, due: task.due,
|
||||
owner: task.owner, work: task.work, detail: task.detail, deliverable: task.deliverable, due: task.due,
|
||||
kind: cls.kind, condition: cls.condition, suggestedDate: cls.suggestedDate,
|
||||
});
|
||||
continue;
|
||||
@@ -742,7 +792,7 @@ async function runMeet(arg: string, view: Webview | undefined, context?: vscode.
|
||||
if (past) extra.push('⚠️ 과거 자료 기반 등록 — 이미 완료되었는지 확인이 필요합니다.');
|
||||
if (cls.recurNote) extra.push(`↻ 반복 업무 언급(${cls.recurNote}) — 정책상 첫 1회만 등록합니다.`);
|
||||
const notes = buildNotes({
|
||||
detail: task.detail, meetTitle, owner: task.owner,
|
||||
detail: task.detail, meetTitle, owner: task.owner, deliverable: task.deliverable,
|
||||
dueRaw: task.due, dateLabel: cls.date, extra,
|
||||
});
|
||||
const r = await registerAction(context, {
|
||||
|
||||
Reference in New Issue
Block a user