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:
parent
1ab744e660
commit
d3c6fddb58
@ -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(야린이): 야구 용어를 쓰면 반드시 한 줄로 풀어서 같이 말한다.
|
||||||
(예: "병살, 그러니까 공 하나로 아웃 두 개 잡히는 거") 규칙 설명은 비유와
|
(예: "병살, 그러니까 공 하나로 아웃 두 개 잡히는 거") 규칙 설명은 비유와
|
||||||
예시 중심으로. 숫자·기록은 최소한만.
|
예시 중심으로. 숫자·기록은 최소한만.
|
||||||
|
|||||||
@ -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 = {
|
||||||
|
|||||||
@ -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)", () => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user