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

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

프롬프트 변수 시스템 — instruction-variables 코드레벨 …: 입력: 이 문서는 이렇게 읽으면 됩니다, 주요 처리 단계: 치환 메커니즘 — replaceInstructionVariables, 결과: 읽을 때 주의할 함정 흐름
동작 흐름 요약
  1. 입력: 이 문서는 이렇게 읽으면 됩니다
  2. 주요 처리 단계: 치환 메커니즘 — replaceInstructionVariables
  3. 결과: 읽을 때 주의할 함정
문서 읽는 법 · 설명식

이 문서는 이렇게 읽으면 됩니다

프롬프트 변수 시스템 — instruction-variables 코드레벨 동작 흐름의 핵심을 설명식으로 먼저 안내합니다. 기술적 결론과 원문 근거는 아래 본문에 보존되어 있습니다.

핵심 흐름 펼쳐 보기
  1. 비유와 핵심 질문으로 먼저 전체 구조를 잡습니다.
  2. 실제 컴포넌트·파일·데이터 흐름을 따라 내려갑니다.
  3. 코드 라인과 주의사항에서 구현 근거를 확인합니다.
  • 네 가지 변수 문법
  • 런타임 펼침 — replaceAllVariables
  • 파싱 — parseInstructionVariables
프롬프트 변수 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 항목