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:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user