프롬프트 변수 시스템 — instruction-variables 코드레벨 동작 흐름

마지막 업데이트 2026-07-22

프롬프트 변수 replaceAllVariables @{} / #{} / ${} / {{}} 한국어 조사 자동 getKoreanParticle (받침) openai-create-call 중첩 금지 백로그 P0 #6

TL;DR

AI 지시문(activity.instruction)에 박힌 변수 토큰을 실제 값으로 치환해 OpenAI Realtime에 보내는 시스템. 네 가지 문법이 공존한다 — #{}(프롬프트 변수, DB 전역), @{}(프리셋/정체성 변수, 아동·핑퐁이 이름 + 한국어 조사 자동), ${}(커스텀 변수, 활동별), {{}}(대화 흐름 사용자 변수). 런타임 펼침은 replaceAllVariables#{} → @{}/${} → {{}} 3단으로 처리한다.

핵심 묘미는 한국어 조사 처리다 — @{아동명아}는 받침 유무를 보고 "지우야" vs "현우아"를 만든다. 그리고 미해결 변수는 원문 그대로 남는다(매핑 실패 디버깅의 단서). 1,000줄 넘는 instruction-variables.ts가 파싱·조사·특수변수·치환을 담당한다.

네 가지 변수 문법

문법type값 출처
#{name}promptDB 전역 프롬프트 변수(getPromptVariables) → content#{공통규칙}
@{name}preset아동·핑퐁이 정체성(getPresetVariables) + 특수 조사 변수@{아동명} · @{아동명아}
${name}custom활동별 사용자 변수(activity.variables)${목표단어}
{{name}}대화 흐름conversation flow 사용자 변수{{지난수업}}

런타임 펼침 — replaceAllVariables

수업 진입 시 openai-create-call.ts가 지시문을 펼쳐 OpenAI Realtime call의 instruction으로 보낸다. lib/openai-create-call.ts:64-79:

const promptVars = await getPromptVariables();                        // DB #{}
const promptVarMap = Object.fromEntries(promptVars.map(v => [v.name, v.content]));
const presetVars = getPresetVariables(user, avatar, subAvatars);    // @{} 정체성
const userVars = activity.variables || {};                          // ${}
finalInstruction = replaceAllVariables(activity.instruction, promptVarMap, presetVars, userVars);
1
resolvePromptVariables — #{}
instruction-variables.ts:1034
#{name}promptVarMap[name](DB content)로 치환. 전역 재사용 블록을 먼저 펼친다.
2
replaceInstructionVariables — @{} · ${}
:246
프리셋/특수 변수(@{})와 커스텀 변수(${})를 치환. 한국어 조사·특수변수 맵을 적용.
3
replaceConversationFlowUserVariables — {{}}
replaceAllVariables :1036
대화 흐름 사용자 변수({{}})를 마지막으로 치환.

파싱 — parseInstructionVariables

instruction-variables.ts:25-67. 세 정규식을 각각 돌려 토큰을 수집하고 위치순 정렬한다.

const presetVarRegex = /@\{([^}]+)\}/g;   // type: "preset"
const userVarRegex   = /\$\{([^}]+)\}/g;  // type: "custom"
const promptVarRegex = /#\{([^}]+)\}/g;   // type: "prompt"
// 각 토큰: { type, name, fullMatch, startIndex, endIndex } → startIndex 기준 정렬

preset 이름은 normalizeVariableName으로 정규화돼 한글·영문 별칭이 모두 동작한다(@{아동명} == @{childName}).

★ 한국어 조사 자동 — getKoreanParticle

핵심 기능. 이름 끝 글자의 받침(종성) 유무로 조사를 고른다. instruction-variables.ts:69-91:

const lastCharCode = name[name.length - 1].charCodeAt(0);
// 한글 음절 영역(0xAC00~0xD7A3) 밖이면 기본값(야/랑)
const hasBatchim = (lastCharCode - 0xac00) % 28 !== 0;   // 받침 있음?
return particleType === "아"
  ? (hasBatchim ? "아" : "야")      // 현우+아, 지우+야
  : (hasBatchim ? "이랑" : "랑"); // 현우+이랑, 지우+랑

특수 변수(@{아동명아}·@{아동+는/이는}·@{핑퐁+야/이야} 등)는 buildSpecialVarMap이 프리셋 값으로 조합한다. 단, 의존 프리셋이 비어 있으면 만들지 않는다(:215-220) — 그 경우 토큰은 원문 그대로 남는다.

치환 메커니즘 — replaceInstructionVariables

프롬프트 변수 CRUD (#{})

/api/prompt-variables — DynamoDB 전역 변수. app/main/prompt-variable 관리 페이지.

하이라이팅 — highlightInstructionVariables

lib/instruction-highlight.ts. 편집기에서 변수 토큰을 색으로 구분(preset/custom/prompt/text). 유효하지 않은 프리셋 이름(getValidPresetVariableNames에 없음)은 일반 텍스트로 표시해, 오타를 변수처럼 강조하지 않는다.

코드 맵 — 파일별 역할

파일역할
lib/instruction-variables.ts (~1,048L)파싱·조사·특수변수·치환·replaceAllVariables 통합 엔진
lib/instruction-highlight.ts편집기 변수 하이라이팅 세그먼트
types/instruction-variables.types.tsPRESET_VAR_KEYS·PRESET_VAR_EN(한↔영 별칭)·SPECIAL_VAR_MAP_KEYS
lib/openai-create-call.ts런타임 펼침 — getPromptVariables+getPresetVariablesreplaceAllVariables → finalInstruction
app/api/prompt-variables/*#{} 전역 변수 CRUD + /usage (중복·중첩 검증)
app/main/prompt-variable · app/main/prompt-test변수 관리 / 프롬프트 테스트 UI

읽을 때 주의할 함정

1. 네 문법을 혼동하지 말 것. #{}(DB 전역)·@{}(정체성+조사)·${}(활동 커스텀)·{{}}(대화 흐름)은 출처도 처리 단계도 다르다. 처리 순서는 #{} → @{}/${} → {{}}(:1034-1036). 같은 토큰이 안 펼쳐지면 어느 단계/출처인지부터 확인.

2. 미해결 변수는 원문 그대로 출력된다. 매핑이 없거나 의존 프리셋이 비면 @{아동명}이 그대로 instruction에 남는다(:240). 최종 instruction에 @{}/#{}가 남아 있으면 = 매핑 실패. AI가 "@{아동명}"을 그대로 읽는 증상의 원인.

3. 한국어 조사는 받침으로 결정. (charCode − 0xAC00) % 28 !== 0이면 받침 있음 → "아/이랑", 없으면 "야/랑"(:84). 한글 음절 영역 밖(영문·숫자)이면 기본값(야/랑). 이름이 영문이면 조사가 어색할 수 있다.

4. 치환은 반드시 끝→앞. 토큰을 startIndex 작은 것부터 바꾸면 뒤 토큰들의 인덱스가 밀려 깨진다. 역순(endIndex 큰 것부터)이라 인덱스가 유지된다(:290). 이 로직을 만질 때 방향을 바꾸면 안 된다.

5. 중첩은 한 단계만. 커스텀 변수 값 안의 @{}는 펼쳐지지만(:263), #{} 프롬프트 변수는 content에 #{}를 넣는 것 자체가 저장 단계에서 금지된다(NESTED_PROMPT_VARIABLE_NOT_ALLOWED). 깊은 재귀 펼침은 설계상 없다.

6. #{} 표기가 알림톡과 겹치지만 별개 엔진. 알림톡#{key}를 쓰지만 거긴 단순 replaceAll이다. 이 문서의 프롬프트 변수는 DB 조회 + 파싱 엔진을 거친다. 같은 표기, 다른 시스템.

관련 문서

OpenAI Realtime 세션 매니저 — 펼쳐진 finalInstruction을 받아 세션을 시작하는 곳 Activity 시스템 — instruction·variables의 출처(활동 템플릿) 알림톡 (NCP SENS) — 같은 #{} 표기를 쓰는 별개의 단순 치환 운영·관리·인프라 문서화 백로그 — 이 문서는 P0 #6 항목