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:
2026-06-17 17:16:55 +09:00
parent 53953fb5f8
commit 64d8093080
8 changed files with 281 additions and 68 deletions
+80 -20
View File
@@ -14,20 +14,35 @@ import * as vscode from 'vscode';
import { bridgeFetch, BRIDGE_API } from './bridgeClient';
/**
* Bridge `/api/lm` 프록시로 OpenAI 호환 chat completion 1회 호출.
* LLM 서버/모델은 Astra 설정(g1nation.ollamaUrl / defaultModel)을 사용한다.
* 모델 출력 붕괴(degeneration) 감지 — 작은/약한 모델이 긴 한국어 입력에서
* 반복 루프나 토큰 깨짐(예: "톤을 톤을 톤을…", "서비스 기프트 서비스 기프트…",
* 깨진 유니코드)에 빠지는 경우를 잡는다. 이런 출력은 사용 불가이므로 재시도 트리거.
*/
export async function callLmSynthesis(prompt: string, systemPrompt?: string): Promise<string> {
const cfg = vscode.workspace.getConfiguration('g1nation');
const lmUrl = (cfg.get<string>('ollamaUrl', 'http://127.0.0.1:11434') || 'http://127.0.0.1:11434').replace(/\/$/, '');
const model = (cfg.get<string>('defaultModel', '') || 'gemma4:e2b').trim();
const temperature = Math.max(0, Math.min(2, cfg.get<number>('datacollectSynthesisTemperature', 0.1) ?? 0.1));
const baseSys = systemPrompt
|| '당신은 시니어 UX/UI 분석가이자 프론트엔드 아키텍트다. 모든 보고서는 한국어로, 제공된 JSON 스캔 데이터의 구체적 수치를 인용해 작성한다. 원본 레퍼런스 사이트를 처음부터 다시 만들기 위한 명세를 작성하는 것이 미션이며, 다른 서비스로 재해석·확장하지 않는다.';
const sys = baseSys + '\n\n[출력 위생 규칙 — 반드시 준수]\n'
+ '- 자연스러운 한국어로 작성하고, 한 단어 안에 한글영문 알파벳을 섞지 마시오 ("결ently", "인orp" 같은 깨진 합성 표기 절대 금지).\n'
+ '- 외래어·기술 용어는 완전한 한글 표기 또는 완전한 영문 단어 중 하나로 일관되게 쓰시오.\n'
+ '- [Self-Reflector Check], Consistency/Completeness/Accuracy 같은 내부 검증·체크 로그 블록을 출력에 절대 포함하지 마시오. 최종 사용자 결과물만 출력하시오.';
export function looksDegenerate(text: string): boolean {
if (!text) return false;
// 1) 대체문자() — 인코딩/토큰 깨짐의 확실한 신호
if (//.test(text)) return true;
const compact = text.replace(/\s+/g, ' ');
// 2) 같은 구절(3~20자)이 5회 이상 반복(사이 공백 허용) — 단, 표 구분선·기호 반복은 제외
const rep = compact.match(/(.{3,20}?)(?:\s*\1){4,}/);
if (rep && !/^[\s\-|=_.·•*#]+$/.test(rep[1])) return true;
// 3) 한글+영문 깨짐 합성 표기가 과다 (정상 교정 한도를 크게 초과)
if ((text.match(/[가-힣][a-z]{2,}/gi) || []).length > 12) return true;
return false;
}
interface LmOpts {
/** 실패·빈응답·degeneration 시 재시도 횟수 (기본 2 = 최대 3회 시도). */
retries?: number;
/** 기본 샘플링 온도 override. */
temperature?: number;
}
/** bridge `/api/lm` 단발 호출 — 샘플링 파라미터까지 받는 내부 코어. */
async function callLmOnce(
lmUrl: string, model: string, sys: string, prompt: string,
sampling: { temperature: number; repeat_penalty: number; top_k: number },
): Promise<string> {
const res = await bridgeFetch<any>(BRIDGE_API.lm.proxy, {
method: 'POST',
body: JSON.stringify({
@@ -38,10 +53,10 @@ export async function callLmSynthesis(prompt: string, systemPrompt?: string): Pr
{ role: 'system', content: sys },
{ role: 'user', content: prompt },
],
temperature,
temperature: sampling.temperature,
top_p: 0.85,
top_k: 20,
repeat_penalty: 1.1,
top_k: sampling.top_k,
repeat_penalty: sampling.repeat_penalty,
},
}),
}, { timeoutMs: 120_000 });
@@ -50,13 +65,58 @@ export async function callLmSynthesis(prompt: string, systemPrompt?: string): Pr
?? res?.answer
?? res?.response
?? '';
let out = String(content)
return String(content)
.replace(/\n*(?:```[\w]*\s*)?\[Self-Reflector Check\][\s\S]*$/i, '')
.trim();
if (/[가-힣][a-z]{2,}/.test(out)) {
out = await repairKoreanGlitches(out, lmUrl, model);
}
/**
* Bridge `/api/lm` 프록시로 OpenAI 호환 chat completion 호출.
* LLM 서버/모델은 Astra 설정(g1nation.ollamaUrl / defaultModel)을 사용한다.
*
* v2.2.252: 약한 로컬 모델의 출력 붕괴(반복·깨짐)와 LM 서버 일시 오류에 대비해
* 재시도를 내장한다. 재시도할수록 repeat_penalty 를 올리고 top_k 를 좁혀 반복
* 루프를 깬다. 모든 시도가 실패하면 마지막 에러를 그대로 throw (호출부가 처리).
*/
export async function callLmSynthesis(prompt: string, systemPrompt?: string, opts?: LmOpts): Promise<string> {
const cfg = vscode.workspace.getConfiguration('g1nation');
const lmUrl = (cfg.get<string>('ollamaUrl', 'http://127.0.0.1:11434') || 'http://127.0.0.1:11434').replace(/\/$/, '');
const model = (cfg.get<string>('defaultModel', '') || 'gemma4:e2b').trim();
const baseTemp = opts?.temperature ?? Math.max(0, Math.min(2, cfg.get<number>('datacollectSynthesisTemperature', 0.1) ?? 0.1));
const baseSys = systemPrompt
|| '당신은 시니어 UX/UI 분석가이자 프론트엔드 아키텍트다. 모든 보고서는 한국어로, 제공된 JSON 스캔 데이터의 구체적 수치를 인용해 작성한다. 원본 레퍼런스 사이트를 처음부터 다시 만들기 위한 명세를 작성하는 것이 미션이며, 다른 서비스로 재해석·확장하지 않는다.';
const sys = baseSys + '\n\n[출력 위생 규칙 — 반드시 준수]\n'
+ '- 자연스러운 한국어로 작성하고, 한 단어 안에 한글과 영문 알파벳을 섞지 마시오 ("결ently", "인orp" 같은 깨진 합성 표기 절대 금지).\n'
+ '- 외래어·기술 용어는 완전한 한글 표기 또는 완전한 영문 단어 중 하나로 일관되게 쓰시오.\n'
+ '- 같은 단어·구절을 반복하지 말고, 한 항목을 다 쓰면 다음 항목으로 진행하시오 (반복 루프 금지).\n'
+ '- [Self-Reflector Check], Consistency/Completeness/Accuracy 같은 내부 검증·체크 로그 블록을 출력에 절대 포함하지 마시오. 최종 사용자 결과물만 출력하시오.';
const maxRetries = Math.max(0, opts?.retries ?? 2);
let lastErr: unknown = null;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
// 재시도할수록 반복을 더 강하게 억제: repeat_penalty ↑, top_k ↓.
const sampling = {
temperature: baseTemp,
repeat_penalty: 1.1 + attempt * 0.15, // 1.1 → 1.25 → 1.4
top_k: Math.max(10, 20 - attempt * 5), // 20 → 15 → 10
};
try {
let out = await callLmOnce(lmUrl, model, sys, prompt, sampling);
if (!out) { lastErr = new Error('LLM 응답이 비어 있습니다.'); continue; }
if (looksDegenerate(out)) {
lastErr = new Error('모델 출력이 붕괴(반복/깨짐)했습니다.');
continue; // 다음 시도에서 반복 억제 강화
}
if (/[가-힣][a-z]{2,}/.test(out)) {
out = await repairKoreanGlitches(out, lmUrl, model);
}
return out;
} catch (e) {
lastErr = e;
// bridge/LM 서버 오류 — 다음 시도로 (degeneration 으로 인한 서버측 parse 실패 포함)
}
}
return out;
throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
}
/**