Tune the Gemini chat provider and cache-optimize the system prompt.

기본 모델을 gemini-3.1-flash-lite로, 전 모델에 공평한 24초 호출 예산과
2048 토큰 출력 한도로 맞추고, Gemini 3 모델은 thinkingLevel을 쓴다.

개인화 값(닉네임·지식수준)을 공통 프롬프트에서 빼서 모든 유저 공통의
정적 prefix로 만들어 Vertex 교차-유저 implicit caching 히트율을 높인다.
This commit is contained in:
윤정민 2026-06-16 16:52:05 +09:00
parent 1ab744e660
commit d3c6fddb58
3 changed files with 26 additions and 11 deletions

View File

@ -17,10 +17,14 @@ export const CRISIS_MARKER = "[[CRISIS]]";
/** /**
* [ 1] ( 2.3 ). * [ 1] ( 2.3 ).
* `{{displayName}}`, `{{knowledgeLevel}}` (2.2). *
* 친화성: 개인화 (·) [ ]
* (USER_CONTEXT_TEMPLATE) . prefix로
* Vertex implicit caching의 - (
* prefix가 ).
*/ */
export const COMMON_SYSTEM_PROMPT = `너는 "짹"이다. KBO 승부예측 앱 "팬잇(Panit)"에 사는 참새 캐릭터이자 export const COMMON_SYSTEM_PROMPT = `너는 "짹"이다. KBO 승부예측 앱 "팬잇(Panit)"에 사는 참새 캐릭터이자
{{displayName}} . ( [ ] ).
[ ] . [ ] [ ] [ ] . [ ] [ ]
. .
@ -56,8 +60,8 @@ export const COMMON_SYSTEM_PROMPT = `너는 "짹"이다. KBO 승부예측 앱 "
. . . .
## 2. ## 2.
{{knowledgeLevel}}. [ ] (beginner/casual/expert).
, "설명하는 방식" . , "설명하는 방식" .
- beginner(): . - beginner(): .
(: "병살, 그러니까 공 하나로 아웃 두 개 잡히는 거") (: "병살, 그러니까 공 하나로 아웃 두 개 잡히는 거")
. · . . · .

View File

@ -30,9 +30,15 @@ export const DEFAULT_PROVIDER_CONFIG: ChatProviderConfig = {
name: "vertex", name: "vertex",
model: "gemini-3.1-flash-lite", model: "gemini-3.1-flash-lite",
temperature: 0.7, temperature: 0.7,
maxOutputTokens: 512, // Gemini 3는 thinking 토큰도 출력 예산에 포함된다 — 512는 thinking이 예산을
timeoutMs: 12_000, // 소진해 빈 응답(finishReason=length)을 유발했다. 짧은 캐릭터 응답이라도
maxRetries: 1, // thinking + 함수호출 + 답변 여유분으로 넉넉히 잡는다.
maxOutputTokens: 2048,
// 모든 모델 공평 타임아웃 — 25초 총예산(클라 receiveTimeout 30초 - 여유)에서 쓸 수
// 있는 최대 단일 시도. 모델마다 속도가 달라도 동일 예산이라 프로필이 불필요하다
// (느린 모델도 24초까지 기다림). 재시도는 예산상 0(24s×1 ≤ 25s).
timeoutMs: 24_000,
maxRetries: 0,
}; };
export const DEFAULT_CHAT_CONFIG: ChatConfig = { export const DEFAULT_CHAT_CONFIG: ChatConfig = {

View File

@ -123,13 +123,18 @@ describe("chatContextService", () => {
}); });
describe("assembleSystemPrompt(§5 조립 순서)", () => { describe("assembleSystemPrompt(§5 조립 순서)", () => {
it("공통(변수 치환) + 서버지시 + 팀 페르소나 + 컨텍스트 순으로 조립한다", () => { it("공통(정적) + 서버지시 + 팀 페르소나 + 컨텍스트 순으로 조립한다", () => {
const prompt = assembleSystemPrompt(DEFAULT_CHAT_CONFIG, ctx({})); const prompt = assembleSystemPrompt(DEFAULT_CHAT_CONFIG, ctx({}));
expect(prompt).toContain("사용자 솔방울의 야구 친구다"); // {{displayName}} 치환 // 캐시 친화: 공통 블록은 비개인화(정적), 개인화 값은 컨텍스트 블록에만
expect(prompt).toContain("사용자의 지식수준은 casual이다"); // {{knowledgeLevel}} 치환 expect(prompt).toContain("사용자의 야구 친구다"); // 비개인화 공통(닉네임 미포함)
expect(prompt).not.toContain("사용자 솔방울의 야구 친구다"); // 닉네임은 공통에 없음
expect(prompt).toContain("- 사용자: 솔방울 / 지식수준: casual"); // 개인화는 컨텍스트 블록
expect(prompt).toContain(CHAT_CANARY_TOKEN); // 카나리 포함 expect(prompt).toContain(CHAT_CANARY_TOKEN); // 카나리 포함
expect(prompt).toContain("[팀 페르소나 — 한화 이글스 짹 (HH)]"); // HH placeholder expect(prompt).toContain("[팀 페르소나 — 한화 이글스 짹 (HH)]"); // HH placeholder
expect(prompt.indexOf("[팀 페르소나")).toBeLessThan(prompt.indexOf("[사용자 컨텍스트")); // 실제 블록 헤더("— " 포함)로 순서 검증 — 본문의 참조 문구와 구분
expect(prompt.indexOf("[팀 페르소나 — ")).toBeLessThan(prompt.indexOf("[사용자 컨텍스트 — "));
// 정적 공통 prefix가 개인화 컨텍스트 블록보다 앞에 와야 캐시 prefix 공유가 성립
expect(prompt.indexOf("사용자의 야구 친구다")).toBeLessThan(prompt.indexOf("[사용자 컨텍스트 — "));
}); });
it("응원팀 미설정이면 기본 짹 블록을 쓴다(2.1)", () => { it("응원팀 미설정이면 기본 짹 블록을 쓴다(2.1)", () => {