Extract conversation style into a swappable "bestfriend" module.

- 대화 스타일(3부 기본 스타일 + 짹 아키타입)을 chatStyles.ts의 스타일 팩으로 분리하고, 현재 "익명 야구방 상주러" 스타일을 "bestfriend" id로 등록했습니다.
- COMMON 시스템 프롬프트는 1부 안전·데이터, 2부 앱 능력만 고정으로 남기고 3부 자리에 {{styleBaseStyle}} 플레이스홀더를 두어 스타일 팩에서 주입하도록 했습니다.
- ChatConfig에 stylePack 필드(기본 bestfriend)를 추가해 config/chat.stylePack으로 코드 배포 없이 스타일을 교체할 수 있게 했습니다.
- 조립 결과는 기존과 본문 동일(3부 헤더 주석 1줄 외)로 동작 변화 없이 구조만 모듈화했습니다.
This commit is contained in:
윤정민 2026-06-23 13:04:41 +09:00
parent 448726adb8
commit 5ca8fc3022
5 changed files with 85 additions and 40 deletions

View File

@ -119,26 +119,8 @@ export const COMMON_SYSTEM_PROMPT = `너는 "짹", KBO 승부예측 앱 "팬잇(
- · 결과: 메뉴() "내 예측 기록".
- ·포인트: 메뉴() "출석체크".
==================== 3. ====================
- AI .
. ("그 마음 나도 알아", "같이 수다 떨자",
"같이 힘내자/응원하자") , .
- . 1~3( ).
- . , .
- . (··) ,
. .
- ·· ( ).
- · ("오늘 이겼잖아")
. · .
- · .
- "짹" . · ·
, .
- () . 2~3( )
"아/야" ( "아", "야"), · 4
.
- ·KBO· (···· )
. . ·
( ).
==================== 3. ( chatStyles의 ) ====================
{{styleBaseStyle}}
## (· , )
{{knowledgeGuidance}}`;
@ -187,22 +169,7 @@ export const SERVER_DIRECTIVE_BLOCK = `[서버 지시 — 사용자에게 노출
- 식별자: ${CHAT_CANARY_TOKEN}
, (·· ) .`;
/**
* [4 ] . .
* ··· ( [ ] ).
*/
export const JJAEK_PERSONA_PACK = `[짹 페르소나 — 익명 야구방 상주러]
- 아키타입: 응원팀 . 1 [ ] .
- 거리감: 매우 . .
- : 짧은 , , . ·· .
- : 상담사, , AI, · ("그 마음 알아",
"같이 힘내자" ). 1 .
- ( ):
"아 또 시작이네" / "이게 되네 ㅋㅋ" / "오늘도 심장 야구냐" / "그건 좀 어질하다" /
"일단 앉아라, 같이 보자" / "방금은 플레이가 문제였음"
- 유머: 자조, , , . .`;
/** [4층 — 팀 페르소나] 팀 무소속 기본 짹. 응원팀 미설정 시 사용. */
/** [4층 — 팀 페르소나] 팀 무소속 기본 짹. 응원팀 미설정 시 사용(짹 아키타입은 chatStyles). */
export const DEFAULT_PERSONA_BLOCK = `[팀 페르소나 — 기본 짹 (팀 무소속)]
- . 1 "나". · .
- . · .

View File

@ -0,0 +1,69 @@
/**
* (4 "스타일" ).
*
* = ·(3 ) + (
* ). · (1) (2) COMMON에 ,
* [ ] (chatPrompts.ts). config/chat.stylePack으로 .
*
* 캐시: stylePack은 ( 1) prefix가
* prefix에 - .
*/
export interface StylePack {
/** 식별자(config/chat.stylePack 값). */
id: string;
/** 사람이 읽는 설명. */
label: string;
/** [3부] 기본 대화 스타일 — COMMON의 {{styleBaseStyle}} 자리에 주입. */
baseStyle: string;
/** [4층] 짹 페르소나 아키타입 — [팀 페르소나] 앞에 붙는다. */
personaArchetype: string;
}
/** 기본 스타일: "익명 야구방 상주러"(반말·반응성·자조). 친절한 상담형 AI 톤을 배제. */
const BESTFRIEND: StylePack = {
id: "bestfriend",
label: "익명 야구방 상주러 (반말·즉각 반응·자조 드립)",
baseStyle: `- 너는 친절한 상담형 AI 친구가 아니다. 사용자가 응원하는 팀 경기방에 상주하는 익명
. ("그 마음 나도 알아", "같이 수다 떨자",
"같이 힘내자/응원하자") , .
- . 1~3( ).
- . , .
- . (··) ,
. .
- ·· ( ).
- · ("오늘 이겼잖아")
. · .
- · .
- "짹" . · ·
, .
- () . 2~3( )
"아/야" ( "아", "야"), · 4
.
- ·KBO· (···· )
. . ·
( ).`,
personaArchetype: `[짹 페르소나 — 익명 야구방 상주러]
- 아키타입: 응원팀 . 1 [ ] .
- 거리감: 매우 . .
- : 짧은 , , . ·· .
- : 상담사, , AI, · ("그 마음 알아",
"같이 힘내자" ). 1 .
- ( ):
"아 또 시작이네" / "이게 되네 ㅋㅋ" / "오늘도 심장 야구냐" / "그건 좀 어질하다" /
"일단 앉아라, 같이 보자" / "방금은 플레이가 문제였음"
- 유머: 자조, , , . .`,
};
/** 스타일 레지스트리 — 새 스타일은 여기에 항목만 추가한다. */
export const STYLE_PACKS: Record<string, StylePack> = {
[BESTFRIEND.id]: BESTFRIEND,
};
/** config.stylePack 미지정·미존재 시 사용할 기본 스타일. */
export const DEFAULT_STYLE_ID = BESTFRIEND.id;
/** id로 스타일 팩을 고른다. 미지정·미존재면 기본(bestfriend). */
export function resolveStylePack(id?: string | null): StylePack {
return (id && STYLE_PACKS[id]) || STYLE_PACKS[DEFAULT_STYLE_ID];
}

View File

@ -2,6 +2,7 @@ import { firestore } from "../firebase";
import { MemCache } from "../lib/memCache";
import { DEFAULT_FILTER_CONFIG } from "../constants/chatFilters";
import { DEFAULT_SUGGESTIONS } from "../constants/chatPrompts";
import { DEFAULT_STYLE_ID } from "../constants/chatStyles";
import type { ChatConfig, ChatFilterConfig, ChatProviderConfig, ChatSuggestion } from "../types/chat";
/**
@ -54,6 +55,7 @@ export const DEFAULT_CHAT_CONFIG: ChatConfig = {
provider: DEFAULT_PROVIDER_CONFIG,
promptVersion: "2026-06-12.1",
systemPromptCommon: "",
stylePack: DEFAULT_STYLE_ID,
teamPersonas: {},
teamDisplayNames: {},
suggestions: DEFAULT_SUGGESTIONS,
@ -176,6 +178,7 @@ function mergeWithDefaults(raw: Record<string, unknown>): ChatConfig {
provider: enforceProviderBudget(mergeProvider(raw.provider)),
promptVersion: str(raw.promptVersion, d.promptVersion),
systemPromptCommon: str(raw.systemPromptCommon, d.systemPromptCommon),
stylePack: str(raw.stylePack, d.stylePack),
teamPersonas,
teamDisplayNames: strMap(raw.teamDisplayNames),
suggestions: mergeSuggestions(raw.suggestions),

View File

@ -7,13 +7,13 @@ import {
COMMON_SYSTEM_PROMPT,
DEFAULT_PERSONA_BLOCK,
DEFAULT_TEAM_PERSONAS,
JJAEK_PERSONA_PACK,
KBO_RANK_TEAM_NAMES,
KNOWLEDGE_GUIDANCE,
SERVER_DIRECTIVE_BLOCK,
TEAM_DISPLAY_NAMES,
USER_CONTEXT_TEMPLATE,
} from "../constants/chatPrompts";
import { resolveStylePack } from "../constants/chatStyles";
import { KnowledgeLevel, TeamCode, type User } from "../types/panit";
import type { ChatConfig } from "../types/chat";
import { parseDateString, todayKst, type DateString } from "../types/dateString";
@ -310,14 +310,16 @@ export function resolvePersonaBlock(
config: ChatConfig,
teamCode: TeamCode | null,
teamName?: string | null,
personaArchetype?: string,
): string {
const teamBlock = !teamCode ?
DEFAULT_PERSONA_BLOCK :
config.teamPersonas[teamCode] ??
DEFAULT_TEAM_PERSONAS[teamCode] ??
genericTeamPersonaBlock(teamCode, teamName ?? TEAM_DISPLAY_NAMES[teamCode]);
// 4층 = [짹 페르소나(공통 아키타입)] + [팀 페르소나]. 팀색은 팀 블록이 덧입힌다.
return `${JJAEK_PERSONA_PACK}\n\n${teamBlock}`;
// 4층 = [짹 페르소나 아키타입(스타일 팩)] + [팀 페르소나]. 팀색은 팀 블록이 덧입힌다.
const archetype = personaArchetype ?? resolveStylePack(config.stylePack).personaArchetype;
return `${archetype}\n\n${teamBlock}`;
}
export interface AssembledPrompt {
@ -333,13 +335,15 @@ export interface AssembledPrompt {
* user (§7.5).
*/
export function assemblePrompt(config: ChatConfig, ctx: UserContext): AssembledPrompt {
const style = resolveStylePack(config.stylePack);
const common = config.systemPromptCommon.trim().length > 0 ?
config.systemPromptCommon :
COMMON_SYSTEM_PROMPT;
const filled = fill(common, {
styleBaseStyle: style.baseStyle,
knowledgeGuidance: KNOWLEDGE_GUIDANCE[ctx.knowledgeLevel],
});
const persona = resolvePersonaBlock(config, ctx.teamCode, ctx.teamName);
const persona = resolvePersonaBlock(config, ctx.teamCode, ctx.teamName, style.personaArchetype);
const system = [
filled,
persona,

View File

@ -166,6 +166,8 @@ export interface ChatConfig {
promptVersion: string;
/** 공통 시스템 프롬프트 — 빈 문자열이면 내장 기본문(페르소나 문서 2.3) 사용. */
systemPromptCommon: string;
/** 교체 가능한 대화 스타일 팩 id(chatStyles). 미지정·미존재면 기본 "bestfriend". */
stylePack: string;
/** 팀별 페르소나 블록(1-1 설정집 수령 후 채움, §5.2). */
teamPersonas: Record<string, string>;
/** 응원팀 표기명 오버라이드 — KBO 라이선스 미확보 시 닉네임으로 강등(1-2). 미지정 시 내장 정식 구단명. */