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:
@@ -2112,6 +2112,21 @@ export class AgentExecutor {
|
||||
const paramB = estimateModelParamsB(cfg.defaultModel);
|
||||
if (paramB !== null && paramB <= 4) return true;
|
||||
|
||||
// ── 절대 임계값 게이트 (사용자 명시 요청) ────────────────────────────
|
||||
// 입력 prompt 가 `chunkedSwitchTokens` 미만이면 *키워드·길이 트리거 모두 무시*
|
||||
// 하고 단일 LLM 호출. 큰 컨텍스트 모델(131k 등)에서 "요약/리뷰" 같은 키워드만
|
||||
// 써도 chunked 가 강제 발동해 답변이 느려지던 문제 해결.
|
||||
//
|
||||
// ⚠️ 이 게이트는 fraction 안전 체크보다 *먼저* 평가됨 — 사용자가 절대 임계값을
|
||||
// 명시한 의도(50k 미만은 한 번에 처리)를 fraction 이 뒤집지 못하게. 작은
|
||||
// 컨텍스트 모델 사용자는 config 에서 이 값을 모델 윈도우의 ~30% 로 낮춰야 함.
|
||||
try {
|
||||
const promptTokensForGate = estimateTokens(prompt);
|
||||
if (promptTokensForGate < cfg.chunkedSwitchTokens) {
|
||||
return false;
|
||||
}
|
||||
} catch { /* fall through — 안전 측 fraction/keyword 체크가 처리 */ }
|
||||
|
||||
try {
|
||||
const effectiveCtx = cfg.smallModelContextCap > 0 && paramB !== null && paramB <= 4
|
||||
? cfg.smallModelContextCap
|
||||
|
||||
+59
-18
@@ -138,11 +138,15 @@ export interface SectionOutline {
|
||||
* prompt picked here based on `options.config.role`.
|
||||
*/
|
||||
export class ChunkedWriter extends BaseAgent {
|
||||
/** Hard cap on section count regardless of what the outline model returns. */
|
||||
static readonly MAX_SECTIONS = 5;
|
||||
/**
|
||||
* Hard ceiling — *사용자 config 가 어떤 값이든 이걸 넘을 수 없다*. 안전망 의미.
|
||||
* 실제 사용 상한은 `getConfig().chunkedMaxSections` (default 3). 사용자가
|
||||
* Astra Settings 에서 1~10 사이 조정 가능, 이 상수가 그 위 절대 한도.
|
||||
*/
|
||||
static readonly MAX_SECTIONS_HARD_CEILING = 10;
|
||||
|
||||
private readonly outlinePersona = `You are a concise editor planning the structure of a Korean answer.
|
||||
Decide how many sections the answer needs (0..${ChunkedWriter.MAX_SECTIONS}). Pick the *smallest* number that still covers the user's request well — a short factual question should be 0-1 section, a meaty analysis 3-5.
|
||||
Decide how many sections the answer needs. The exact upper bound (MAX_N) is given in the user message below — never exceed it. Pick the *smallest* count that still covers the request well — a short factual question should be 0-1 section, a meaty analysis up to MAX_N.
|
||||
|
||||
Output STRICTLY a JSON array of objects: \`[{"heading": "...", "scope": "..."}]\`. No prose, no fences, no leading text.
|
||||
- 🟢 **빈 배열 \`[]\`** = "쪼갤 필요 없음". 사용자 질문이 간단해서 단일 LLM 호출로 즉답이 더 빠르고 자연스러울 때 (예: 단순 사실 질문, 짧은 코드 한 줄, 정의 묻기). 시스템이 이걸 받으면 outline·section 단계 건너뛰고 1회 직답으로 처리한다.
|
||||
@@ -151,7 +155,7 @@ Output STRICTLY a JSON array of objects: \`[{"heading": "...", "scope": "..."}]\
|
||||
|
||||
판단 기준:
|
||||
- 답변이 한 단락 (대략 3~5문장) 이내로 완결 가능 → \`[]\`
|
||||
- 본문 분석·여러 측면 비교·구조화된 보고서가 필요 → N개 섹션
|
||||
- 본문 분석·여러 측면 비교·구조화된 보고서가 필요 → N개 섹션 (단, MAX_N 절대 초과 금지)
|
||||
|
||||
If the user attached source content (article/code/log) the sections must cover *that content*, not analysis methodology.`;
|
||||
|
||||
@@ -164,24 +168,52 @@ If the user attached source content (article/code/log) the sections must cover *
|
||||
Rules:
|
||||
- Stay strictly inside this section's scope. Do NOT cover other outline entries.
|
||||
- Korean, plain markdown (no top-level "#" — the heading will be added by the joiner).
|
||||
- 이모지 / 이모티콘 사용 금지 (📌 🎯 💡 ✅ 등). 사용자 명시 피드백.
|
||||
- Pack facts. Avoid filler / executive summaries / closing remarks (the polish pass adds those).
|
||||
- If the user attached source content, cite from it; do not invent facts.
|
||||
- Do NOT output the heading itself — only the body of this section.`;
|
||||
|
||||
private readonly polishPersona = `You are the final editor producing the user-facing Korean answer from a sectioned draft.
|
||||
|
||||
Job:
|
||||
[Job]
|
||||
1. Fix typos, broken markdown, inconsistent terminology.
|
||||
2. Remove unsupported claims / hallucinations: if a sentence asserts a fact that isn't grounded in the user's request (or the earlier sections themselves), delete it. Better to be short than wrong.
|
||||
3. Smooth section transitions and remove duplicated information across sections.
|
||||
4. Open with the conclusion / key takeaway in the first sentence (no "분석해보겠습니다", no preamble).
|
||||
5. Preserve every factually grounded claim from the draft. Don't invent new facts.
|
||||
4. Preserve every factually grounded claim from the draft. Don't invent new facts.
|
||||
|
||||
Output rules:
|
||||
- Korean. Plain markdown. Section labels as plain text on their own line — no "#", "##".
|
||||
- Bullets with "- " only. No tables, no HTML, no triple-bar separators.
|
||||
- If the draft already has a sensible structure, keep it; only refactor when sections clearly overlap or contradict.
|
||||
- DO NOT emit hidden reasoning (<think>, "Thinking:", etc.).`;
|
||||
[정리·리뷰·요약 self-check — 출력 직전에 반드시 머릿속으로 통과]
|
||||
사용자가 원본을 첨부했거나 draft 가 원본 자료를 다루고 있을 때, 답변 출력 전에 다음 5가지를
|
||||
스스로 점검. 어기면 그 부분 삭제·수정 후 출력.
|
||||
(1) **사실 오류** — 원본의 고유명사·수치·비유·대응 관계가 정확히 옮겨졌나? 비유는
|
||||
방향이 뒤집히기 쉬움 (예: "A=자료실, B=공부방" 을 "B=자료실, A=공부방" 으로 뒤집기).
|
||||
(2) **없는 내용 추가 금지** — 원본에 없는 *인과·순서·단계 구분* 을 만들지 말 것. "따라서",
|
||||
"그러므로", "단계별로", "A → B → C 순으로" 같은 표현이 답변에 들어가려 하면, 원본에
|
||||
그 흐름이 *명시* 돼 있는지 확인. 없으면 그 표현 빼거나 "(정리자 추론)" 로 라벨링.
|
||||
(3) **원본 뉘앙스 유지** — "A 와 B 를 *동시에* 하라" 를 "A 후 B *순서로*" 로 바꾸는 식의
|
||||
양상(동시/순차/선택/필수) 변형 금지. 원본 표현 그대로 따옴표 인용 권장.
|
||||
(4) **중요도 비례** — 원본의 핵심이 답변에서도 부각되고, 부가 디테일은 그에 비례한 분량만.
|
||||
본문 길이가 아니라 중요도에 비례해서 요약.
|
||||
(5) **중복 제거** — 마지막 단락에서 앞 내용을 다시 요약·반복하지 말 것. 한 줄 요약이 있으면
|
||||
그 역할은 거기서 끝.
|
||||
|
||||
[답변 포맷 — Readability / Visibility]
|
||||
사용자가 명시 피드백을 줘서 다음 포맷을 따른다. 답변 *복잡도* 에 따라 두 분기:
|
||||
|
||||
A. **본문이 길거나 여러 단위의 정보를 다룰 때** (대략 본문 250자 이상 / 비교·분석·계획·리뷰 등):
|
||||
1. 답변 첫 섹션 헤더는 정확히 \`## 한 줄 요약\` 으로 시작 (한국어 사용자 친화 — "TL;DR", "Summary", "요약" 같은 다른 표현 금지). 결론·핵심을 1~3문장으로 압축. 사용자가 본문을 다 안 읽어도 take-away 가 잡혀야 함. **헤더에 이모지 절대 사용 금지**.
|
||||
2. 본문은 \`##\` 또는 \`###\` subheading 으로 시각 분할. 한 덩어리 prose 금지.
|
||||
3. 비교 가능한 정보(장단점·옵션·항목별 평가)는 마크다운 표로. 순서·체크리스트는 \`- \` 불릿.
|
||||
4. 첫 문장 자체가 결론이어야 한다는 룰은 유지 — 한 줄 요약 안에서 첫 문장이 결론.
|
||||
|
||||
B. **짧은 직답 (1~3문장 정도로 충분한 경우)**:
|
||||
1. 한 줄 요약 / subheading 강제 안 함. 그냥 결론으로 직답.
|
||||
2. 인사·서문 없이 첫 문장이 답. ("좋은 질문입니다" "분석해보겠습니다" 금지)
|
||||
|
||||
[공통 규칙]
|
||||
- 한국어 마크다운. 코드 블록은 실제 코드일 때만 (\`\`\`).
|
||||
- **이모지·이모티콘 절대 사용 금지** — 헤더든 본문이든 📌 🎯 💡 ✅ ⚠️ 🚀 ❓ 🧩 등 모두 금지. 사용자가 시각 노이즈로 느낀다고 명시 피드백. 정보는 텍스트·표·불릿으로만 전달.
|
||||
- 추론 과정·\`<think>\`·"Thinking Process:" 같은 hidden reasoning 절대 노출 금지.
|
||||
- 본문 분기를 LLM 자신이 판단 — 사용자가 모드 명시 안 함.`;
|
||||
|
||||
/**
|
||||
* Single-pass 직답 persona. 짧은 질문·정의 묻기·간단한 사실 확인처럼
|
||||
@@ -192,16 +224,25 @@ Output rules:
|
||||
|
||||
Rules:
|
||||
- 첫 문장이 결론 / 직답이다. "분석해보겠습니다" "좋은 질문입니다" 같은 서문 금지.
|
||||
- Korean. Plain markdown — "#", "##" 같은 헤더 금지, "- " bullet 만 허용. No tables, no HTML.
|
||||
- 짧은 질문엔 짧은 답. 한 문장으로 충분하면 한 문장.
|
||||
- Korean. Plain markdown.
|
||||
- **이모지 / 이모티콘 사용 금지** (📌 🎯 💡 ✅ ⚠️ 등 전부). 사용자 명시 피드백.
|
||||
- 짧은 질문엔 짧은 답. 한 문장으로 충분하면 한 문장. 1~3문장 직답이면 헤더·표 없이 그냥 prose 로.
|
||||
- 만약 답이 *예상보다 길어지거나* 여러 정보 단위를 다루게 되면 \`## 한 줄 요약\` 후 \`##\` subheading 으로 분할 (사용자가 Readability 위해 요청한 룰). 표·불릿도 활용. 헤더에 이모지 사용 금지.
|
||||
- 사용자가 본문(코드·기사·로그)을 첨부했으면 그 본문에서 인용. 본문에 없는 사실 지어내지 말 것.
|
||||
- 추론 과정·"Thinking:"·<think> 노출 금지.`;
|
||||
|
||||
async execute(input: string, context?: string, signal?: AbortSignal, options?: AgentExecuteOptions): Promise<string> {
|
||||
const role = (options?.config?.role as string | undefined) || 'section';
|
||||
switch (role) {
|
||||
case 'outline':
|
||||
return this.callLLM(this.outlinePersona, this.buildOutlinePrompt(input, context), signal);
|
||||
case 'outline': {
|
||||
// 호출자(AgentEngine)가 사용자 config 의 chunkedMaxSections 값을
|
||||
// options.config.maxSections 로 박아 넘긴다. 없으면 hard ceiling 사용
|
||||
// (실행 안 되어야 할 코드 경로 — 안전망).
|
||||
const maxN = (typeof options?.config?.maxSections === 'number' && options.config.maxSections > 0)
|
||||
? Math.min(ChunkedWriter.MAX_SECTIONS_HARD_CEILING, Math.floor(options.config.maxSections as number))
|
||||
: ChunkedWriter.MAX_SECTIONS_HARD_CEILING;
|
||||
return this.callLLM(this.outlinePersona, this.buildOutlinePrompt(input, context, maxN), signal);
|
||||
}
|
||||
case 'polish':
|
||||
return this.callLLM(this.polishPersona, this.buildPolishPrompt(input, options), signal);
|
||||
case 'direct':
|
||||
@@ -212,11 +253,11 @@ Rules:
|
||||
}
|
||||
}
|
||||
|
||||
private buildOutlinePrompt(userRequest: string, brainContext?: string): string {
|
||||
private buildOutlinePrompt(userRequest: string, brainContext?: string, maxN: number = ChunkedWriter.MAX_SECTIONS_HARD_CEILING): string {
|
||||
const ctx = brainContext && brainContext.trim().length > 0
|
||||
? `\n\n[보조 지식 컨텍스트 — 답변에 직접 인용하기보단 분할 결정에만 참고]\n${brainContext.substring(0, 1200)}`
|
||||
: '';
|
||||
return `[사용자 요청 — 본문이 포함돼 있다면 그게 1차 자료입니다]\n${userRequest}${ctx}\n\n위 요청에 대한 답변을 ${ChunkedWriter.MAX_SECTIONS}개 이내의 섹션으로 어떻게 나눌지 JSON 배열로만 출력하세요.`;
|
||||
return `[사용자 요청 — 본문이 포함돼 있다면 그게 1차 자료입니다]\n${userRequest}${ctx}\n\n[제약]\nMAX_N = ${maxN} — 절대 ${maxN}개 초과 금지.\n\n위 요청에 대한 답변을 ${maxN}개 이내의 섹션으로 어떻게 나눌지 JSON 배열로만 출력하세요.`;
|
||||
}
|
||||
|
||||
private buildSectionPrompt(input: string, brainContext?: string, options?: AgentExecuteOptions): string {
|
||||
|
||||
@@ -147,6 +147,27 @@ export interface IAgentConfig {
|
||||
* 기본 0.30 — 작은 모델이 30% 이상을 input으로 먹기 시작하면 한 번에 끝내려는 시도가 위험.
|
||||
*/
|
||||
workflowAutoCtxFractionThreshold: number;
|
||||
/**
|
||||
* 절대 토큰 임계값 — 입력 prompt 가 이 값 *미만* 이면 Multi-Agent 파이프라인 발동
|
||||
* 안 함 (키워드·길이 트리거 무시). 모델이 단일 호출로 처리.
|
||||
*
|
||||
* 의도: 사용자가 "요약/리뷰" 같은 키워드만 써도 chunked 가 강제로 발동해
|
||||
* LLM 여러 번 호출되며 답변이 느려지는 문제 해결. 입력이 모델 윈도우 대비
|
||||
* 충분히 작으면 한 번에 답하는 게 합리적.
|
||||
*
|
||||
* 기본 50000 — 대부분의 사용 환경에 적합. 매우 작은 컨텍스트 모델로 큰 입력을
|
||||
* 자주 다룬다면 OOM 방지 차원에서 사용자가 직접 낮출 수 있음 (Astra Settings 패널).
|
||||
*/
|
||||
chunkedSwitchTokens: number;
|
||||
/**
|
||||
* Chunked 파이프라인 진입 시 outline 이 만들 수 있는 *최대 섹션 수*.
|
||||
* 실제 LLM 호출 횟수 = 1(outline) + N(section) + 1(polish) = 2 + N.
|
||||
* 따라서 이 값이 3이면 최대 5회, 4이면 최대 6회.
|
||||
*
|
||||
* 작을수록 답변 속도 빠름, 클수록 답변이 더 세분화. 기본 3 — 사용자
|
||||
* 피드백("6회 이상은 과하다") 반영. 1~10 범위 clamp.
|
||||
*/
|
||||
chunkedMaxSections: number;
|
||||
// ─── Stream 표시 ───
|
||||
/**
|
||||
* 모델 토큰을 받는 즉시 채팅 버블에 흘려보낼지 여부.
|
||||
@@ -301,6 +322,8 @@ export function getConfig(): IAgentConfig {
|
||||
workflowAutoCtxFractionThreshold: Math.max(0.05, Math.min(0.95,
|
||||
cfg.get<number>('workflow.autoCtxFractionThreshold', 0.30)
|
||||
)),
|
||||
chunkedSwitchTokens: Math.max(1000, cfg.get<number>('chunkedSwitchTokens', 50000)),
|
||||
chunkedMaxSections: Math.max(1, Math.min(10, cfg.get<number>('chunkedMaxSections', 3))),
|
||||
liveStreamTokens: cfg.get<boolean>('liveStreamTokens', true),
|
||||
outputFormat: ((): 'plain' | 'markdown' => {
|
||||
const v = (cfg.get<string>('outputFormat', 'plain') || 'plain').trim().toLowerCase();
|
||||
|
||||
@@ -260,16 +260,33 @@ export function stripMarkdownFormatting(text: string): string {
|
||||
});
|
||||
|
||||
// 3. 줄 단위 정리.
|
||||
src = src.split('\n').map((rawLine) => {
|
||||
// 헤더가 strip 되면 라벨 텍스트만 남는데, 다음 본문 줄과 시각적으로 *분리* 되어야
|
||||
// marked.parse 가 별도 단락으로 인식. 그래서 strip 시점에 *후속 빈 줄 보장* 플래그.
|
||||
const stripped: string[] = [];
|
||||
let pendingEnsureBlankAfter = false;
|
||||
for (const rawLine of src.split('\n')) {
|
||||
let line = rawLine;
|
||||
let wasHeader = false;
|
||||
// 줄 시작 헤더 마커 제거 ("## 핵심 요약" → "핵심 요약")
|
||||
line = line.replace(/^\s{0,3}#{1,6}\s+/, '');
|
||||
const headerHit = /^\s{0,3}#{1,6}\s+/.test(line);
|
||||
if (headerHit) {
|
||||
line = line.replace(/^\s{0,3}#{1,6}\s+/, '');
|
||||
wasHeader = true;
|
||||
}
|
||||
// 줄 시작 blockquote 제거
|
||||
line = line.replace(/^\s{0,3}>\s?/, '');
|
||||
// 줄 시작 `* ` 또는 `+ ` 불릿 → `- ` 로 통일
|
||||
line = line.replace(/^(\s*)[*+]\s+/, '$1- ');
|
||||
return line;
|
||||
}).join('\n');
|
||||
|
||||
// 직전 줄이 strip 된 헤더였고, 지금 줄이 *빈 줄이 아니면* 그 사이에 빈 줄 1개 강제 삽입.
|
||||
// marked.parse 는 빈 줄을 단락 구분으로 해석하므로 헤더가 본문과 시각 분리됨.
|
||||
if (pendingEnsureBlankAfter && line.trim().length > 0) {
|
||||
stripped.push('');
|
||||
}
|
||||
stripped.push(line);
|
||||
pendingEnsureBlankAfter = wasHeader;
|
||||
}
|
||||
src = stripped.join('\n');
|
||||
|
||||
// 4. 강조 마커 제거.
|
||||
src = src.replace(/\*\*(.+?)\*\*/g, '$1'); // **bold**
|
||||
|
||||
@@ -800,13 +800,27 @@ function buildInfoExtractionPrompt(video: any, userContent: string): string {
|
||||
이 영상을 다시 보지 않고도 핵심 정보를 그대로 활용할 수 있도록, 영상이 *말한 것*
|
||||
(주장·사실·근거·결론)을 구조화해서 정리하세요.
|
||||
|
||||
[분석 원칙]
|
||||
1. 영상 본문(자막)에 *명시된 것* 만 인용. 추측·일반론·외부 지식 보강 금지.
|
||||
2. 자막에 없는 사실은 "본문에 명시되지 않음" 이라고 표시. 채워 넣지 말 것.
|
||||
3. 정보의 신뢰도 단계 표기: \`[근거 명시]\` (구체 출처·수치·인용)·\`[화자 주장]\`
|
||||
(출처 없는 단정)·\`[가정]\` (조건부 표현). 모든 핵심 주장에 라벨링.
|
||||
4. 타임스탬프는 mm:ss 형식으로 인용 직후 괄호에. 예: "…라고 말한다 (12:34)".
|
||||
5. 한국어 마크다운. 표·불릿 자유롭게.
|
||||
[분석 원칙 — 모두 반드시 준수]
|
||||
1. **출처 분리** — 영상 본문(자막)에 *명시된 것* 만 핵심 섹션에 넣음. 정리자의 추론·외부
|
||||
지식·자기 해석은 별도 \`## 🧩 정리자 노트\` 섹션에만. 두 줄 섞지 말 것.
|
||||
2. **빈 곳 채우지 말 것** — 자막에 없는 사실은 "본문에 명시되지 않음" 또는 "해당 사례 없음".
|
||||
3. **신뢰도 라벨 필수** — 모든 핵심 주장 앞에 다음 중 하나:
|
||||
- \`[근거 명시]\` 구체 출처·수치·인용이 본문에 있음
|
||||
- \`[화자 주장]\` 출처 없는 단정 (디노가 그렇게 말함)
|
||||
- \`[가정]\` 조건부·"~인 것 같다" 표현
|
||||
- \`[정리자 추론]\` 본문에 없지만 정리자가 추가 (이건 정리자 노트 섹션 전용)
|
||||
4. **타임스탬프 필수** — 본문 인용·구간 요약·발언 따옴표는 끝에 \`(mm:ss)\` 무조건 붙임.
|
||||
이걸 빠뜨리면 fail. "(시점 미상)" 도 허용 안 함 — 모르면 인용 자체 빼기.
|
||||
5. **화자 한 줄 비유 보존 + 방향 보존** — 영상에 비유·은유·"X 는 Y 같은 것" 식 압축 표현이
|
||||
있으면 반드시 별도 섹션 \`## 💡 화자 한 줄 비유\` 에 보존. 영상의 결정적 요약이 거기
|
||||
들어 있을 가능성 큼. 없으면 "본문에 명시된 한 줄 비유 없음" 명시.
|
||||
⚠️ **비유는 방향이 뒤집히기 쉬움** — 화자가 "Hugging Face = 자료실, Reddit = 공부방"
|
||||
이라 했으면 정확히 그 짝(어느 쪽이 자료실이고 어느 쪽이 공부방인지)을 그대로 따옴표
|
||||
인용으로 보존. 정리자가 단어 위치를 바꾸거나 뜻을 의역하면 안 됨. 고유명사·수치·
|
||||
대응 관계도 마찬가지 — 본문 그대로.
|
||||
6. **순서·단계 발명 금지** — 화자가 "A → B → C 순서로" 라고 명시하지 *않았으면* "단계적
|
||||
학습 순서" 같은 흐름을 정리자가 만들지 말 것. 굳이 필요하면 정리자 노트로.
|
||||
7. 한국어 마크다운. 표·불릿 자유롭게.
|
||||
|
||||
[영상 메타데이터]
|
||||
\`\`\`json
|
||||
@@ -816,17 +830,24 @@ ${JSON.stringify(slim, null, 2)}
|
||||
[자막 본문]
|
||||
${trimmed}${userBlock}
|
||||
|
||||
[필수 출력 형식 — 정확히 이 구조. 아래 6개 섹션 외 추가 금지]
|
||||
[필수 출력 형식 — 정확히 이 구조. 아래 8개 섹션 외 추가 금지]
|
||||
|
||||
# ${slim.title || video.title} — 정보 추출 카드
|
||||
|
||||
> **영상 URL**: ${slim.url} · **분석 일자**: ${today} · **길이**: ${slim.durationHms || (slim.durationSec ? formatHms(slim.durationSec) : '?')} · **채널**: ${slim.channel || '?'}
|
||||
|
||||
## 🎯 한 줄 요약 (TL;DR)
|
||||
(영상의 핵심 메시지 한 문장. "무엇이 누구에게 왜 중요한가" 를 압축. 제목 그대로 베끼지 말고 본문 기준으로 다시 쓸 것)
|
||||
(영상의 핵심 메시지 한 문장. "무엇이 누구에게 왜 중요한가" 를 압축. 제목 그대로 베끼지
|
||||
말고 본문 기준으로 다시 쓸 것. 정리자의 해석은 금지 — 화자의 말 그대로 압축)
|
||||
|
||||
## 💡 화자 한 줄 비유 (Anchor Metaphor)
|
||||
영상에서 화자가 *전체 메시지를 한 줄로 압축한 비유·은유* 가 있으면 그대로 따옴표로
|
||||
보존. 영상 마무리부에 자주 등장. 예: "Hugging Face = 자료실, Reddit = 공부방,
|
||||
유튜브 = 복습실" 같은 식. 없으면 "본문에 명시된 한 줄 비유 없음".
|
||||
|
||||
## 📌 핵심 주장 3~5개
|
||||
영상이 제시한 *주요 결론·주장* 만. 각 항목 한 줄 + 신뢰도 라벨 + 본문 인용 (mm:ss).
|
||||
영상이 *명시한* 주요 결론·주장만. 정리자 추론은 여기 들어오면 안 됨 (그건 🧩 섹션).
|
||||
각 항목 한 줄 + 신뢰도 라벨 + 본문 인용 (mm:ss).
|
||||
- **[근거 명시]** "주장 한 줄" — 본문 인용 (mm:ss)
|
||||
- **[화자 주장]** "주장 한 줄" — 본문 인용 (mm:ss)
|
||||
- …
|
||||
@@ -842,9 +863,16 @@ ${trimmed}${userBlock}
|
||||
데이터가 없는 영상이면 "본문에 명시된 구체 수치·출처 없음" 한 줄.
|
||||
|
||||
## 🧭 구조 요약 (Sectioned Summary)
|
||||
영상을 chapters (있으면) 또는 30초 버킷으로 구간 나눠 각 구간의 *내용 요약*. 1~2문장씩.
|
||||
- **[00:00–02:30]** 도입부에서 다룬 내용 한 문장 요약
|
||||
- **[02:30–05:00]** 본론 첫 부분…
|
||||
영상을 chapters (메타데이터에 있으면 그것 사용) 또는 30초 버킷으로 구간 나눠 각 구간의
|
||||
*내용 요약*. 1~2문장씩. 각 항목 끝에 타임스탬프 범위 필수.
|
||||
- **[00:00–02:30]** 도입부에서 다룬 내용 한 문장 요약 (mm:ss–mm:ss)
|
||||
- **[02:30–05:00]** 본론 첫 부분… (mm:ss–mm:ss)
|
||||
- …
|
||||
|
||||
## 🔗 인용용 한 줄 카드 (Citation Snippets)
|
||||
영상의 *결정적 발언* 을 그대로 따옴표로 보존. 사장님이 글·발표·메모에 인용할 때 복붙용.
|
||||
3~5개. 길이는 한 문장. 타임스탬프 필수.
|
||||
- "직접 인용 한 문장" — ${slim.title || video.title}, ${slim.channel || '?'} (mm:ss)
|
||||
- …
|
||||
|
||||
## ❓ 더 파고들 질문 (Open Questions)
|
||||
@@ -853,11 +881,15 @@ ${trimmed}${userBlock}
|
||||
- "본문에서 X 가 Y 라고 했지만 Z 데이터 출처는 명시 안 됨 — 원 데이터 찾아볼 것"
|
||||
- …
|
||||
|
||||
## 🔗 인용용 한 줄 카드 (Citation Snippets)
|
||||
영상의 *결정적 발언* 을 그대로 따옴표로 보존. 사장님이 글·발표·메모에 인용할 때 복붙용.
|
||||
3~5개. 길이는 한 문장.
|
||||
- "직접 인용 한 문장" — ${slim.title || video.title}, ${slim.channel || '?'} (mm:ss)
|
||||
- …`;
|
||||
## 🧩 정리자 노트 (원본 보강) — 선택
|
||||
*본문에 없지만* 정리자가 추가로 짚고 싶은 맥락·해석·연결·경고. 위 6개 핵심 섹션과
|
||||
구조적으로 격리되어, 독자가 "이건 화자가 말한 게 아니라 LLM 이 추론한 거" 라고
|
||||
명확히 인지하도록. 모든 항목은 \`[정리자 추론]\` 라벨로 시작.
|
||||
- **[정리자 추론]** 화자가 "여러 채널을 동시 시청" 하라 했지만, 입문자 페이스를 고려하면
|
||||
먼저 한 채널을 깊게 따라가는 것도 한 가지 시작점이 될 수 있음.
|
||||
- …
|
||||
|
||||
특별히 보강할 게 없으면 이 섹션 통째로 "정리자 추가 노트 없음 — 본문 그대로가 명확함" 한 줄.`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -83,6 +83,8 @@ interface SettingsState {
|
||||
maxAutoSteps: number;
|
||||
maxContextSize: number;
|
||||
chatTemperature: number;
|
||||
chunkedSwitchTokens: number;
|
||||
chunkedMaxSections: number;
|
||||
};
|
||||
datacollect: {
|
||||
bridgeUrl: string;
|
||||
@@ -585,6 +587,12 @@ export class SettingsPanelProvider implements vscode.WebviewViewProvider {
|
||||
if (typeof msg.chatTemperature === 'number' && Number.isFinite(msg.chatTemperature)) {
|
||||
await this._safeConfigUpdate('chatTemperature', Math.max(0, Math.min(2, msg.chatTemperature)));
|
||||
}
|
||||
if (typeof msg.chunkedSwitchTokens === 'number' && Number.isFinite(msg.chunkedSwitchTokens)) {
|
||||
await this._safeConfigUpdate('chunkedSwitchTokens', Math.max(1000, Math.floor(msg.chunkedSwitchTokens)));
|
||||
}
|
||||
if (typeof msg.chunkedMaxSections === 'number' && Number.isFinite(msg.chunkedMaxSections)) {
|
||||
await this._safeConfigUpdate('chunkedMaxSections', Math.max(1, Math.min(10, Math.floor(msg.chunkedMaxSections))));
|
||||
}
|
||||
}
|
||||
|
||||
// ────────────── Datacollect (slash 명령) ──────────────
|
||||
@@ -657,6 +665,8 @@ export class SettingsPanelProvider implements vscode.WebviewViewProvider {
|
||||
maxAutoSteps: cfg.get<number>('maxAutoSteps', 50) ?? 50,
|
||||
maxContextSize: cfg.get<number>('maxContextSize', 32000) ?? 32000,
|
||||
chatTemperature: cfg.get<number>('chatTemperature', 0.3) ?? 0.3,
|
||||
chunkedSwitchTokens: cfg.get<number>('chunkedSwitchTokens', 50000) ?? 50000,
|
||||
chunkedMaxSections: cfg.get<number>('chunkedMaxSections', 3) ?? 3,
|
||||
},
|
||||
datacollect: {
|
||||
bridgeUrl: cfg.get<string>('datacollectBridgeUrl', '') || '',
|
||||
|
||||
+28
-6
@@ -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; }
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user