feat: v2.2.83 → v2.2.91 — info prompt 강화 + 사용자 노출 설정 + 답변 포맷 정리

[v2.2.83] /youtube info 프롬프트 강화
- 비유 방향 보존 룰 (Hugging Face=자료실 같은 짝 뒤집기 방지)
- 신뢰도 라벨 4종 ([근거 명시] / [화자 주장] / [가정] / [정리자 추론])
- 타임스탬프 fail 룰 (인용·구간 요약 모두 mm:ss 필수)
- "정리자 노트" 별도 섹션으로 추론 격리

[v2.2.85] polishPersona self-check 5가지
- 정리·리뷰·요약 답변 출력 직전 머릿속 체크:
  (1) 사실 오류  (2) 없는 내용 추가  (3) 뉘앙스 유지
  (4) 중요도 비례  (5) 중복 제거

[v2.2.86] chunkedSwitchTokens 절대 임계값 게이트
- 입력 < 50k 토큰이면 키워드·길이 트리거 무시하고 단일 호출
- 큰 컨텍스트 모델(131k+)에서 chunked 과잉 발동 방지

[v2.2.87] MAX_SECTIONS 5→3 cap
- 총 호출 7회 → 5회 (outline + 3 section + polish)
- 사용자 피드백 "6+회는 과하다"

[v2.2.88] 이모지 사용 금지 룰
- polishPersona / directPersona / sectionPersona 모두 적용
- 사용자 피드백 "이모지는 시각 노이즈"

[v2.2.89] 사용자 노출 설정 두 항목
- chunkedMaxSections config 신규 (default 3, 1~10 clamp)
- MAX_SECTIONS_HARD_CEILING (10) 으로 안전망 격상
- Astra Settings 패널 "고급" 섹션에 두 슬라이더 노출

[v2.2.90] 가이드 문구 단순화
- "작은 모델은 낮추라" 문구 빼고 일관되게 50000 권장으로

[v2.2.91] 답변 포맷 가독성 fix
- persona 의 "TL;DR" 표현 전부 "한 줄 요약" 으로 단일화
- stripMarkdownFormatting 에 헤더 후 빈 줄 강제 삽입
  (marked.parse 가 라벨·본문을 별도 단락으로 인식 → 시각 분리)

[테스트] 400/400 통과 (resilience_stress + chunked flow + MAX_SECTIONS cap 등)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
g1nation
2026-05-24 14:12:56 +09:00
parent ded3eea7ce
commit 4153f640c2
22 changed files with 425 additions and 204 deletions
+28 -6
View File
@@ -456,8 +456,14 @@ export class CacheManager {
* - Error Recovery Matrix 기반의 Transient/Permanent 오류 자동 분류 및 복구
*/
export class AgentEngine {
/** Outline LLM이 제안한 N을 강제로 1..MAX_SECTIONS 로 clamp 한다. */
static readonly MAX_SECTIONS = 5;
/**
* Hard ceiling — *사용자 config 가 어떤 값이든 절대 넘을 수 없다*. 안전망.
* 실제 사용 상한은 `getConfig().chunkedMaxSections` (default 3). 사용자가
* Astra Settings 에서 1~10 사이 조정.
*
* factory.ts ChunkedWriter.MAX_SECTIONS_HARD_CEILING 와 일치.
*/
static readonly MAX_SECTIONS_HARD_CEILING = 10;
/**
* 단일 writer agent — 같은 모델이 outline / section / polish 역할을 번갈아
@@ -526,18 +532,28 @@ export class AgentEngine {
// --- Phase 1: Outline ---
// 1번의 LLM 호출로 답변을 몇 개 섹션으로 쪼갤지 결정. JSON 배열 반환.
// 사용자 config 의 chunkedMaxSections 를 outline persona 에 전달 — outline
// LLM 이 그 상한을 지키도록 prompt 에 박힘. parseOutline 의 cap 도 같은
// 값 사용해서 LLM 이 룰 어겨도 강제로 자름.
const cfgMaxSections = (() => {
try {
const { getConfig } = require('../config') as typeof import('../config');
const v = getConfig().chunkedMaxSections;
return Math.max(1, Math.min(AgentEngine.MAX_SECTIONS_HARD_CEILING, v ?? 3));
} catch { return 3; } // 안전 fallback
})();
const outlineRaw = await this.executeStep(
state, 'outline', '답변 구조 잡는 중...',
() => this.resilientExecute(state, this.writer, 'Outline', prompt, brainContext, signal, onProgress, {
...options,
context: brainContext,
signal,
config: { ...options?.config, role: 'outline' },
config: { ...options?.config, role: 'outline', maxSections: cfgMaxSections },
}),
`outline::${prompt}`, brainContext, signal, onProgress
);
const outline = this.parseOutline(outlineRaw);
const outline = this.parseOutline(outlineRaw, cfgMaxSections);
const sections = outline.sections;
// outline 이 빈 배열(`reason === 'empty'`)을 반환했다면 LLM 이
@@ -920,10 +936,16 @@ export class AgentEngine {
* (옛 버전엔 길이로만 구분이 안 돼서 empty 와 fallback 이 혼동돼
* parse 실패가 우발적 single-pass 전환을 일으켰음).
*/
private parseOutline(raw: string): {
private parseOutline(raw: string, cap?: number): {
sections: Array<{ heading: string; scope: string }>;
reason: 'ok' | 'empty' | 'fallback';
} {
// cap 미지정 시 hard ceiling 으로 안전 보호. 정상 호출 경로에선 호출자가 사용자
// config 값 (chunkedMaxSections) 을 전달함.
const effectiveCap = Math.max(1, Math.min(
AgentEngine.MAX_SECTIONS_HARD_CEILING,
cap ?? AgentEngine.MAX_SECTIONS_HARD_CEILING,
));
const fallbackSections = [{ heading: '본문', scope: '사용자 요청 전체를 다루는 단일 섹션' }];
if (!raw || !raw.trim()) {
return { sections: fallbackSections, reason: 'fallback' };
@@ -949,7 +971,7 @@ export class AgentEngine {
}))
.filter((o) => o.heading.length > 0);
if (cleaned.length === 0) return null;
return { kind: 'sections', list: cleaned.slice(0, AgentEngine.MAX_SECTIONS) };
return { kind: 'sections', list: cleaned.slice(0, effectiveCap) };
} catch { return null; }
};