
모델을 교체해도 AI의 동작이 무너지지 않는 운영 설계 — 컨텍스트 합성(Context Synthesis)과 가드레일(Guardrails)
요약
모델 교체 시에도 AI의 일관된 동작을 유지하기 위한 운영 설계 방안을 제시합니다. 컨텍스트 합성, 거절 기억, 가드레일의 3단계 계층 구조를 통해 모델의 성능 변화에 휘둘리지 않는 시스템 구축 방법을 TypeScript 코드로 설명합니다.
핵심 포인트
- 규칙을 대화 이력이 아닌 외부 파일로 관리하여 모델 독립성 확보
- System 프롬프트에 규칙을 매번 합성하여 지시 사항 희석 방지
- 모델 교체 포인트를 추상화하여 운영 안정성 극대화
- 컨텍스트 합성, 거절 기억, 가드레일의 3층 구조 설계
AI 에이전트를 실운영하면서 가장 애를 먹는 것은 모델이 오래되는 것이 아닙니다. 새로워지는 것입니다.
새로운 모델은 확실히 똑똑합니다. 문장도 더 잘 쓰고 추론 능력도 향상됩니다. 하지만 현장에서는 반드시 똑같은 일이 일어납니다.
- 어제까지 지켜지던 톤(Tone)이 갑자기 변한다
- "이것은 쓰지 말 것"이라고 정해두었던 것을 슬그머니 써버린다
- 이전에 거절했던 표현이 다시 돌아온다
이유는 간단합니다. 새로운 모델은 "문장을 쓰는 능력"은 계승하지만, "이 발신처는 무엇을 중요하게 여기고, 무엇을 쓰지 않는가"라는 운영 지식은 계승하지 않습니다. 후자는 모델의 가중치(Weight) 안에 있는 것이 아니라, 우리 측의 시스템이 가지고 있어야 할 영역입니다.
이 기사에서는 모델을 교체해도 동작이 무너지지 않는 운영층을 TypeScript의 동작하는 코드로 설계합니다. 다루는 것은 3개 층입니다.
- 층 1: 컨텍스트 합성 (Context Synthesis) (house rules를 매번 반드시 주입한다)
- 층 2: 거절 기억 (성공 사례보다 "탈락 이유"를 효과적으로 적용한다)
- 층 3: 기계 게이트 + 사람의 육안 확인 (모델이 예상치 못한 방식으로 벗어났을 때의 마지막 보루)
전제로, 모델 호출은 Anthropic의 Messages API와 같은 "system 프롬프트 + messages" 구조를 상정합니다. system을 매번 어떻게 구성하느냐가 운영의 생명선이 됩니다.
처음에 저지르기 쉬운 실수는 "대화 속에서 매번 규칙을 다시 말하는" 방식입니다.
// 안티 패턴: 규칙이 대화 이력에 파묻힘
const messages = [
{
role: "user",
content: "우리 톤은 정중하며, 너무 단정적이지 않아야 함. 과장된 숫자는 금지. 기사를 작성해."
},
...
이 방식이 무너지는 이유는 두 가지입니다.
- 대화가 길어지면 규칙이 희석된다. 이력의 맨 앞에 둔 지시는 대화가 오갈수록 상대적인 가중치가 떨어집니다.
- 모델을 교체하면 사라진다. 새로운 버전은 이전 세션을 알지 못합니다. "우리의 방식"이 어디에도 영속화(Persistence)되어 있지 않으면 매번 제로 베이스에서 시작해야 합니다.
대처법은 하나입니다. 규칙을 대화에서 분리하여, 모델에 의존하지 않는 외부의 "소유물"로 만들고, 호출할 때마다 system으로 합성하는 것입니다. 이것만으로도 "휘둘리는 쪽"이 우리 자신이 되지 않게 할 수 있습니다.
규칙은 코드 안에 하드코딩하지 않습니다. 교체나 리뷰를 git 차이(diff)로 할 수 있도록, **플레인 텍스트(Plain Text) 형태의 "소유물"**로서 외부에 둡니다.
house/
tone.md # 어미, 거리감, 1인칭
forbidden.md # 절대 쓰지 말아야 할 것
...
이를 읽어 들여 system을 구성하는 합성기(Synthesizer)를 만듭니다. 핵심은 "매번 반드시 읽는다"와 "모델 ID에 의존하지 않는다"는 것입니다.
// src/context.ts
import * as fs from "node:fs/promises";
import * as path from "node:path";
...
호출 측은 모델을 추상화해 둡니다. 교체 포인트를 한 곳에 가두는 것이 포인트입니다.
// src/llm.ts
import Anthropic from "@anthropic-ai/sdk";
import { buildSystemPrompt } from "./context.js";
...
여기서 설계 판단이 하나 있습니다. 규칙은 messages(대화)가 아니라 system에 둔다.
system은 모델에게 있어 대화 이력과는 별개의 지시이며, 대화가 길어져도 희석되기 어렵습니다. 그리고 MODEL을 변수 하나로 집약해 두면, Opus 계열에서 Sonnet 계열로 교체하더라도, 혹은 폴백(Fallback) 대상으로 전환하더라도 규칙의 주입 경로는 전혀 변하지 않습니다. 실운영에서의 교훈: 우리는 처음에 규칙을 사용자 메시지의 맨 앞에 매번 붙였습니다. 모델이 새 버전으로 올라간 주에 어미와 톤이 일제히 어긋났습니다. 원인은 "똑똑해진 모델이 희석된 지시보다 자신의 본래 문체를 우선시했기" 때문입니다.
system으로 옮기고 합성을 매번 강제한 이후에는, 교체 시의 톤 무너짐 현상이 거의 사라졌습니다.
의외였던 점은 모범 사례(성공 사례)보다 실패의 기록이 더 효과적이었다는 것입니다.
새로운 모델은 모범 사례가 있으면 잘 흉내 냅니다. 하지만 모범 사례에 없는 지뢰는 아무렇지 않게 밟습니다. 그래서 "밟았던 지뢰"를 구조화하여 남기고, 매번 system에 주입합니다.
// src/rejections.ts
import * as fs from "node:fs/promises";
import * as path from "node:path";
...
운영 규칙은 간단합니다. 리뷰에서 무언가를 거절했다면, 그 즉시 recordRejection을 호출합니다. "농담이 너무 심해서 반려", "내용이 너무 과해서 반려", "톤이 어긋나서 반려" —— 이유가 쌓일수록, 다음에 오는 모델이 같은 실수를 반복하지 않게 됩니다.
"쓸 때마다 똑똑해진다"의 정체는 이것이었습니다. 똑똑해지고 있는 것은 모델 본체가 아니라, **모델 외부에 쌓여가는 거절 기억 (rejection memory)**입니다. 그렇기 때문에 모델을 교체하더라도 그 자산은 통째로 남습니다.
주의사항: 거절 기억은 무한히 늘어납니다.
system 프롬프트에 전부 넣으면 컨텍스트 (context)를 잡아먹고 비용도 상승합니다. 우리는 "최근 N건 + 카테고리별 대표 사례"만 주입하고, 전체 내용은 별도의 스토어에 퇴피시켜 두었습니다. limit를 기계적으로 적용하는 것이 유일하게 작동하는 방법이었습니다. "나중에 정리하겠다"는 결코 오지 않습니다.
솔직히 말하면, 1계층(Layer 1)과 2계층(Layer 2)을 쌓아도 새로운 모델은 예상치 못한 방식으로 실수를 합니다. 그래서 **공개 전 게이트 (gate)**를 반드시 통과시킵니다. 게이트는 2단계로 구성됩니다.
금지어, 과장된 숫자 패턴, 내부 식별자 유출 등 기계적으로 판정할 수 있는 것은 모델에게 판단하게 하지 않고, 결정적인 코드로 검사합니다. 모델의 성능에 의존하지 않기 위해서입니다.
// src/gate.ts
export interface GateResult {
pass: boolean;
...
기계 게이트에서 걸러진 것은 그대로 2계층의 거절 기억에 다시 기록함으로써 루프를 완성합니다. 검사에서 발견된 위반 사항이 다음번 system 프롬프트 주입 재료가 됩니다.
const nowIso = new Date().toISOString();
const result = machineGate(draft);
if (!result.pass) {
...
기계 게이트를 통과하더라도, 시각적인 결과물(썸네일, 이미지, 레이아웃)은 사람이 실제로 화면에 띄워 눈으로 확인하기 전까지는 공개하지 않습니다. 이것은 타협할 수 없는 원칙입니다.
이유는 실무 경험 때문입니다. 본문이 중립적이더라도 썸네일 단독으로 문맥을 왜곡할 수 있습니다. 코드의 존재 확인이나 차이(diff)가 0이라는 것이 "렌더링 결과가 올바르다"는 것을 보장하지는 않습니다. overflow-hidden 처리된 컨테이너의 스크린샷이 첫 화면만 찍혀서, 픽셀 차이(pixel-diff)가 0이라 "문제 없음"으로 오판했던 사고도 있었습니다.
// 기계로 판정해도 되는 것 / 사람의 육안 확인이 필요한 것의 경계를 타입으로 명시
type CheckMode = "machine" | "human-eyeball";
function reviewMode(artifact: "text" | "thumbnail" | "image" | "layout"): CheckMode {
...
시스템으로 9할을 줄이고, 남은 1할(특히 시각적 요소)을 사람이 확인합니다. 전부를 사람이 보는 것도 전부를 모델에 맡기는 것도 아닌, 그 중간 지점을 운영 설계로 만들어내는 것이 현실적인 해답이었습니다.
마지막으로, 모델 교체 자체에 대한 처리입니다. 교체가 사고처럼 갑작스럽게 일어나면 위험하므로, 이를 의도적인 조작으로 격하시킵니다. 해야 할 일은 두 가지뿐입니다.
- 모델 ID를
AGENT_MODEL한 곳에 집약한다 (1계층에서 구현 완료) - 교체 시에는 실전 투입 전에 **동일한 입력으로 구모델과 신모델의 출력을 차분 비교(diff comparison)**한다
// src/migration-check.ts
import { machineGate } from "./gate.js";
/** 구모델/신모델에 동일한 프롬프트를 실행하여, 게이트 결과와 톤의 차이를 육안으로 확인한다 */
...
이로써 모델 교체는 "어느 날 아침 갑자기 동작이 변하는 것"이 아니라 "구모델과 신모델을 나란히 놓고 확인한 뒤 전환하는" 조작이 됩니다. system 프롬프트 주입도 거절 기억도 모델에 의존하지 않으므로, 교체 후에도 운영 규칙은 그대로 적용됩니다.
| 계층 | 해결하는 문제 | 핵심 설계 |
|---|---|---|
| 컨텍스트 합성 (Context Synthesis) | 교체 시 톤앤매너(Tone) 붕괴 | 규칙은 system 프롬프트에 매번 합성 · 모델 ID는 한 곳으로 집약 |
| ... |
3개 계층에 공통적인 것은, "똑똑한 모델을 고르는 것"이 아니라 "어떤 모델에서도 무너지지 않는 운영 계층을 갖는 것"이라는 방향성입니다. 모델의 내부 구조는 앞으로도 멋대로 교체될 것입니다. 그럼에도 불구하고 무너지지 않게 하고 싶다면, 규칙의 위치를 "대화"에서 "모델 외부의 기억"으로 옮기십시오. 그것만으로도 휘둘리는 쪽이 바뀝니다.
코드는 그대로 실행할 수 있습니다. HOUSE_DIR에 자신들의 tone.md / forbidden.md를 두고, AGENT_MODEL을 한 곳에 모으는 것부터 시작해 보시기 바랍니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기