v2.2.265: /meet 검수 루프·편집·액션 상세·논의 매듭

/meet 파이프라인 대폭 개선:
- 검수 루프: 회의록 검증 후 지적분만 수정, 통과까지 반복 (전체 재작성 X)
- 편집(Editor): 검수 통과 후 보고서 형태로 구조 정리 (환각 방지 재검증)
- 액션 상세 확장: task 노트에 2~3문장 자기완결 설명 자동 생성
- 주요 논의/쟁점: 각 쟁점을 논점→논의→결론으로 그 자리에서 매듭
- 자동 등록 0건 fix: 기한 없는 확정·기한미정을 작성일(오늘)로 등록 (폴백)

신규 설정:
- g1nation.meetVerifyMaxRounds (기본 2)
- g1nation.meetEditorPass (기본 ON)
- g1nation.meetTaskDateFallback (기본 today)
- g1nation.meetTaskDetailExpand (기본 ON)

신규 프롬프트:
- buildMeetRevisePrompt (지적분만 수정)
- buildMeetEditorPrompt (근거 유지 편집)
- buildTaskDetailPrompt (액션 상세 확장)

신규 파서:
- parseTaskDetails (task 상세 맵핑)

신규 테스트:
- classifyAction dateFallback 동작 5건
- parseTaskDetails 파싱 4건

tsc ✓ · jest 715/715 ✓

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 18:22:52 +09:00
parent 40d003635e
commit 8f1ce4d617
6 changed files with 392 additions and 51 deletions
+49 -13
View File
@@ -64,14 +64,26 @@ interface LmOpts {
retries?: number;
/** 기본 샘플링 온도 override. */
temperature?: number;
/** 출력 토큰 상한(max_tokens). 미지정이면 payload 에 넣지 않아 서버 기본값을 따른다.
* 회의록처럼 긴 출력은 크게 지정해 서버 기본 상한에 잘리는 것을 막는다. */
maxTokens?: number;
/** 출력이 length(토큰 상한)로 잘렸을 때 끊긴 지점부터 이어쓰기 최대 횟수(기본 3). 0이면 이어쓰기 안 함.
* 회의록 "누락" 의 실제 원인(출력 잘림)을 보완 — 잘린 부분을 이어붙여 완결성을 확보. */
maxContinuations?: number;
}
/** bridge `/api/lm` 단발 호출 — 샘플링 파라미터까지 받는 내부 코어. */
/** 단발 호출 결과 — 이어쓰기 판단을 위해 finish_reason 을 함께 반환. */
interface LmResult { text: string; finish: string; }
const SELF_REFLECT_RE = /\n*(?:```[\w]*\s*)?\[Self-Reflector Check\][\s\S]*$/i;
/** LM 단발 호출 — 샘플링/토큰상한을 받고 (정제 텍스트 + finish_reason)을 돌려주는 내부 코어. */
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 lmChat(lmUrl, {
maxTokens?: number,
): Promise<LmResult> {
const payload: Record<string, unknown> = {
model,
messages: [
{ role: 'system', content: sys },
@@ -81,15 +93,25 @@ async function callLmOnce(
top_p: 0.85,
top_k: sampling.top_k,
repeat_penalty: sampling.repeat_penalty,
}, 120_000);
const content = res?.choices?.[0]?.message?.content
?? res?.choices?.[0]?.text
?? res?.answer
?? res?.response
?? '';
return String(content)
.replace(/\n*(?:```[\w]*\s*)?\[Self-Reflector Check\][\s\S]*$/i, '')
.trim();
};
// max_tokens 는 지정됐을 때만 넣는다 — 미지정 caller(벤치마크 등)의 기존 동작(서버 기본값)을 바꾸지 않기 위해.
if (maxTokens && maxTokens > 0) payload.max_tokens = maxTokens;
const res = await lmChat(lmUrl, payload, 120_000);
const choice = res?.choices?.[0];
const content = choice?.message?.content ?? choice?.text ?? res?.answer ?? res?.response ?? '';
const finish = String(choice?.finish_reason ?? choice?.native_finish_reason ?? '').toLowerCase();
const text = String(content).replace(SELF_REFLECT_RE, '').trim();
return { text, finish };
}
/** 이어쓰기 프롬프트 — 이미 쓴 뒷부분을 주고, 끊긴 지점부터 중복 없이 이어붙일 텍스트만 요청. */
function buildContinuePrompt(soFarTail: string): string {
return '아래는 지금 작성 중이던 문서(회의록/보고서)의 **마지막 부분**이다. 토큰 한도로 중간에 끊겼다.\n'
+ '끊긴 바로 그 지점부터 **이어서** 계속 작성하라. 규칙:\n'
+ '- 이미 쓴 내용을 반복하거나 요약하지 말 것. 새 서두·인사·머리말 금지.\n'
+ '- 끊긴 문장/표 행/섹션을 자연스럽게 이어붙일 **뒷부분만** 출력한다(중복 헤더 재출력 금지).\n'
+ '- 원문서와 동일한 형식·문체(개조식·명사형 종결)를 유지한다.\n\n'
+ '[지금까지 출력의 마지막 부분]\n' + soFarTail;
}
/**
@@ -114,6 +136,7 @@ export async function callLmSynthesis(prompt: string, systemPrompt?: string, opt
+ '- [Self-Reflector Check], Consistency/Completeness/Accuracy 같은 내부 검증·체크 로그 블록을 출력에 절대 포함하지 마시오. 최종 사용자 결과물만 출력하시오.';
const maxRetries = Math.max(0, opts?.retries ?? 2);
const maxCont = Math.max(0, opts?.maxContinuations ?? 3);
let lastErr: unknown = null;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
// 재시도할수록 반복을 더 강하게 억제: repeat_penalty ↑, top_k ↓.
@@ -123,12 +146,25 @@ export async function callLmSynthesis(prompt: string, systemPrompt?: string, opt
top_k: Math.max(10, 20 - attempt * 5), // 20 → 15 → 10
};
try {
let out = await callLmOnce(lmUrl, model, sys, prompt, sampling);
const first = await callLmOnce(lmUrl, model, sys, prompt, sampling, opts?.maxTokens);
let out = first.text;
if (!out) { lastErr = new Error('LLM 응답이 비어 있습니다.'); continue; }
if (looksDegenerate(out)) {
lastErr = new Error('모델 출력이 붕괴(반복/깨짐)했습니다.');
continue; // 다음 시도에서 반복 억제 강화
}
// ── 이어쓰기: 출력이 토큰 상한(finish=length)으로 잘렸으면 끊긴 지점부터 이어붙인다.
// 회의록 후반부·긴 표가 통째로 사라지던 "누락"을 완결성으로 보완한다.
let finish = first.finish;
for (let c = 0; c < maxCont && finish === 'length'; c++) {
let next: LmResult;
try {
next = await callLmOnce(lmUrl, model, sys, buildContinuePrompt(out.slice(-1600)), sampling, opts?.maxTokens);
} catch { break; } // 이어쓰기 호출 실패 시 지금까지분으로 마감
if (!next.text || looksDegenerate(next.text)) break; // 이어쓰기가 붕괴하면 중단
out = `${out}\n${next.text}`.trim();
finish = next.finish;
}
if (/[가-힣][a-z]{2,}/.test(out)) {
out = await repairKoreanGlitches(out, lmUrl, model);
}