
TypeScript를 사용하여 상태 유지형 AI 채팅을 위한 경계 정책 엔진(Boundary Policy Engine) 구축하기
요약
AI 채팅 모델의 프롬프트 분위기에 의존하지 않고, 결정론적인 방식으로 권한을 제어하는 경계 정책 엔진(Boundary Policy Engine) 구축 방법을 다룹니다. TypeScript를 사용하여 허용, 거부, 확인의 세 가지 상태를 관리하는 엔진 구현 과정을 설명합니다.
핵심 포인트
- AI의 어조나 관계 진전에 따른 민감한 동작을 결정론적으로 제어
- ALLOW, DENY, ASK의 세 가지 결과값을 가진 정책 엔진 설계
- 범위, 규칙 권한, 만료, 취소, 충돌 해결 로직 포함
- 의미론적 유사성보다 명시적인 범위 매칭의 중요성 강조
AI 채팅 모델이 프롬프트의 분위기(vibes)만으로 제품 권한을 결정해서는 안 됩니다.
만약 컴패니언(companion)이 어조를 바꾸거나, 관계를 진전시키거나, 민감한 주제를 도입하거나, 음성 및 이미지를 생성할 수 있다면, 애플리케이션은 생성이 시작되기 전에 결정론적(deterministic)인 결정을 내려야 합니다.
이 튜토리얼에서는 세 가지 결과값을 가진 작은 경계 정책 엔진(boundary policy engine)을 구축합니다:
ALLOW | DENY | ASK
이 엔진은 범위(scope), 규칙 권한(rule authority), 만료(expiry), 취소(revocation), 충돌(conflicts), 그리고 오래된 비동기 작업(stale asynchronous jobs)을 처리합니다.
기능(capabilities) 및 결정(decisions) 정의
type Capability =
| "tone.playful"
| "topic.work"
...
규칙(Rules)은 자유 형식의 대화가 아닌 결정(decisions)을 포함합니다. 제품에 필요한 경우, 별도의 감사 링크(audit link)를 통해 승인된 증거를 가리킬 수 있습니다.
요청 컨텍스트(request context) 표현
type PolicyContext = {
now: Date
userId: string
...
requestedBy가 중요합니다. 사용자가 명시적으로 이미지를 요청하는 것은 자동화(automation)가 이미지를 삽입하기로 결정하는 것과는 다릅니다.
범위를 명시적으로 매칭
function scopeMatches(
scope: PolicyScope,
context: PolicyContext,
...
의미론적 유사성(Semantic similarity)이 범위 불일치(scope mismatch)를 절대 무시해서는 안 됩니다. 데이터베이스에서도 범위에 따라 필터링하더라도, 이 체크는 일반적인 애플리케이션 테스트에 포함되어야 합니다.
비활성 규칙 제거
function isActive(rule: PolicyRule, now: Date): boolean {
if (rule.revokedAt) return false
if (rule.validFrom > now) return false
...
취소(Revocation)와 만료(expiry)는 별개입니다. 취소는 사용자 또는 제품의 결정을 반영하며, 만료는 유효성 경계(validity boundary)를 반영합니다.
권한(authority) 및 구체성(specificity) 순위 지정
const sourceRank: Record<RuleSource, number> = {
"product-default": 0,
"confirmed-inference": 1,
...
최종 ID 비교는 오직 결정론적인 타이 브레이커(tie-breaker) 역할만 합니다. 동일하게 저장된 상태는 모든 워커(worker)에서 동일한 결정을 생성해야 합니다.
보수적으로 충돌 해결하기 (Resolve conflicts conservatively)
type PolicyDecision = {
effect: PolicyEffect
ruleIds: string[]
...
동등한 권위를 가진 규칙들이 충돌할 때는, 허용 범위가 더 넓은 값을 조용히 선택하는 것보다 ask를 사용하는 것이 더 안전합니다.
결정과 생성의 분리 (Separate decision from generation)
async function generateCompanionReply(
request: CapabilityRequest,
context: PolicyContext,
...
콘텐츠를 먼저 생성한 다음 나중에 필터링하지 마세요. 생성 후 필터링(Post-generation filtering) 방식으로는 원치 않는 관계의 전환이나 이미 발송된 음성 작업(voice job)을 되돌릴 수 없습니다.
비동기 작업 재확인 (Re-check asynchronous jobs)
type MediaJob = {
id: string
userId: string
...
프로덕션 워커(production worker)는 단순히 버전만 비교하는 것이 아니라, 현재 규칙을 다시 불러와서 재평가(re-evaluate)해야 합니다. 버전 확인은 빠른 거절 경로(fast rejection path)로 사용되어야 합니다.
까다로운 케이스 테스트하기 (Test the uncomfortable cases)
import { describe, expect, it } from "vitest"
describe("boundary policy engine", () => {
...
또한 장면 만료(scene expiry), 중복 이벤트, 시계 경계(clock boundaries), 오래된 작업(stale jobs), 사용 불가능한 정책 저장소, 그리고 사용자가 규칙을 변경한 후의 재시도(retries) 케이스도 테스트하세요.
개인적인 텍스트를 로깅하지 않고 결정 관찰하기 (Observe decisions without logging private text)
유용한 추적(trace) 정보는 다음과 같습니다:
capability=media.image
decision=ask
reason=no-applicable-rule
...
추적 정보에 개인적인 대화 내용이나 규칙의 원래 메시지 텍스트를 포함할 필요는 없습니다.
최종 요약 (Final takeaway)
상태 유지형(stateful) AI 컴패니언에게는 프롬프트(prompt)가 필요하기 전에 정책 결정(policy decision)이 필요합니다.
범위(scope)를 일치시키세요. 취소되거나 만료된 규칙은 제거하세요. 권위와 구체성(specificity)의 순위를 매기세요. 해결되지 않은 충돌은 ask로 전환하세요. 비동기 출력 전에 정책을 재확인하세요. 결정 사항은 관찰 가능하게 유지하고 대화는 비공개로 유지하세요.
이 방식은 모델이 모든 경계를 추론하도록 내버려 두는 것보다 덜 마법적(magical)입니다. 또한 테스트하고, 설명하고, 되돌리기가 훨씬 더 쉽습니다.
소속 및 AI 지원 공개: 저는 LumiChat 팀과 함께 일하고 있으며, 이곳에서는 캐릭터별 톤(tone), 관계의 속도(relationship pacing), 그리고 생성된 미디어(generated media)로 인해 경계 결정(boundary decisions)이 실질적인 엔지니어링 문제로 다뤄집니다. 이 튜토리얼은 특정 벤더에 종속되지 않으며(vendor-neutral), LumiChat 인프라에 의존하지 않습니다. AI 도구는 편집 및 다이어그램 제작에 도움을 주었으며, 코드와 최종 주장(claims)은 저자가 직접 검토하였습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기