유창함이 충실함을 의미하지는 않는다: 더 안전한 AI 패러프레이징 (Paraphrasing) 파이프라인 구축하기
요약
LLM을 활용한 패러프레이징 과정에서 발생할 수 있는 의미 왜곡 문제를 방지하기 위한 구조화된 파이프라인 구축 방법을 소개합니다. 재작성 요청을 구조화된 데이터로 취급하고, 모호한 스타일 레이블을 명시적인 지시 사항으로 변환하여 충실도를 높이는 전략을 다룹니다.
핵심 포인트
- 재작성 요청을 단일 프롬프트가 아닌 구조화된 데이터(RewritePayload)로 관리
- 모드, 강도, 유지 단어 등 제약 조건을 분리하여 모델의 추론 용이성 확보
- 입력값 검증을 통해 컨텍스트 예산 초과 및 잘못된 지시 사항 주입 방지
- 모호한 스타일 레이블을 구체적이고 명시적인 지시 사항으로 변환하여 일관성 유지
대규모 언어 모델 (Large language models)은 유창한 텍스트를 생성하는 데 매우 능숙합니다. 하지만 모든 유창한 재작성 (rewrite)이 충실한 (faithful) 재작성을 의미하는 것은 아닙니다.
Paraphraser AI를 구축하면서, 저는 기만적일 정도로 단순한 요구 사항에 계속해서 직면했습니다:
주장을 변경하지 않고 표현만 바꾸십시오.
사용자가 더 강력한 편집, 다른 어조 (tone), 여러 개의 출력 버전, 또는 제품명과 SEO 키워드의 정확한 보존을 요구하는 순간 이 요구 사항은 어려워집니다. 모델은 스타일 요청을 충족하면서도, 한편으로는 한정어 (qualifier)를 조용히 누락시키거나, 숫자를 변경하거나, 조심스러운 주장을 강화하거나, 원문에서 지원되지 않는 더 매끄러운 전환 (transition)을 꾸며낼 수 있습니다.
이 글에서는 제가 현재 사용하고 있는 파이프라인과 그것이 무엇을 잡아내는지, 그리고 더 중요한 점은—그것이 여전히 무엇을 증명할 수 없는지에 대해 설명합니다.
재작성 계약 (rewrite contract)으로 시작하기
저는 재작성 요청을 단일 텍스트 프롬프트가 아닌 구조화된 데이터로 취급합니다:
type RewritePayload = {
text: string;
mode: "Standard" | "Natural" | "Formal" | "Academic" | "SEO" | "Humanize";
...
각 필드는 별도의 제약 조건 (constraint)을 나타냅니다:
text는 반드시 유지되어야 하는 사실과 의미를 포함합니다.mode는 의도된 목소리 (voice)를 설명합니다.strength는 구문 (syntax)과 어휘 (vocabulary)가 얼마나 멀리 이동할 수 있는지를 제어합니다.keepWords는 변경되지 않고 유지되어야 하는 문자열을 포함합니다.outputCount는 독립적으로 사용 가능한 하나 이상의 결과를 요청합니다.
이러한 관심사들을 분리함으로써 API 검증이 쉬워지고 프롬프트에 대한 추론이 용이해집니다. 또한 “SEO 모드”와 같은 UI 레이블이 모델 호출마다 의미가 변하는 정의되지 않은 명령어가 되는 것을 방지합니다.
모델 토큰을 소비하기 전에 검증하기
공개 엔드포인트 (public endpoint)는 제공자 (provider)를 호출하기 전에 요청을 검증합니다:
const rewriteRequestSchema = z.object({
text: z.string().trim().min(1).max(12_000),
mode: z.enum([
...
이러한 제한 사항들은 그 자체로 의미론적 안전장치 (semantic safeguards)인 것은 아니지만, 피할 수 있는 여러 실패 모드 (failure modes)를 줄여줍니다:
- 극도로 긴 입력이 유용한 컨텍스트 예산 (context budget)을 조용히 초과하지 않도록 합니다.
- 알 수 없는 모드 (unknown modes)가 임의의 지시 사항을 주입할 수 없습니다.
- 하나의 요청이 수백 개의 비용이 많이 드는 출력 변형 (output variants)을 요구할 수 없습니다.
- 보호된 문구 (protected phrases)는 크기와 개수가 제한됩니다.
또한 모드별 규칙도 존재합니다: SEO 모드는 최소 하나 이상의 보호된 키워드 (protected keyword)를 요구합니다. 이 규칙이 없다면, 제품은 어떤 용어가 중요한지 알지 못한 채 키워드를 보존한다고 주장하게 될 것입니다.
모호한 스타일 레이블을 명시적인 지시 사항으로 변환하기
모드가 구체적인 지시 사항 (instruction)으로 변환될 때 모델은 더 일관되게 반응합니다:
const modeInstruction = {
Standard: "명확하고 중립적인 패러프레이징 (paraphrase)을 작성하세요.",
Natural: "텍스트가 유창하고 인간적이며 대화하듯 들리게 만드세요.",
...
강도 (Strength) 역시 동일한 처리를 받습니다:
const strengthInstruction = {
Low: "가벼운 편집을 수행하되, 모든 문장의 어휘는 변경하세요.",
Medium: "의미는 유지하면서 문장 구조와 어휘를 변경하세요.",
...
최종 프롬프트는 이러한 지시 사항들을 여러 불변 사항 (invariants)과 결합합니다:
의미와 사실을 보존하세요.
원문을 변경 없이 그대로 반환하지 마세요.
"패러프레이징된 텍스트:"와 같은 레이블을 단순히 추가하지 마세요.
...
이것이 준수 (compliance)를 보장하지는 않습니다. 대신 생성 후에 확인할 수 있는 계약 (contract)을 만드는 것입니다.
구조화된 출력을 요청하되, 이를 신뢰하지 마세요
제공자 (provider)는 작은 JSON 객체를 반환합니다:
{
"outputs": ["재작성된 텍스트가 여기에 들어갑니다."],
"meta": {
...
구조화된 출력 (structured output)은 다운스트림 프로세싱 (downstream processing)을 용이하게 만들지만, 메타데이터 (metadata)가 독립적인 평가인 것은 아닙니다. 만약 동일한 모델이 재작성문을 작성하고 meaningPreserved: true라고 보고한다면, 해당 불리언 (boolean) 값은 단지 모델의 주장일 뿐입니다.
따라서 애플리케이션은 생성된 메타데이터를 권고 사항 (advisory)으로 취급합니다. 출력이 사용 가능한지 여부는 결정론적 검사 (deterministic checks)를 통해 결정됩니다.
코드에서 보호된 용어 강제하기
키워드 보존 (keyword preservation)은 정확하게 확인할 수 있는 제약 조건 중 하나입니다:
function hasKeepWords(text: string, keepWords: string[]) {
const normalized = text.toLowerCase();
return keepWords.every((word) =>
...
다른 부분은 유창하더라도 보호된 문구 (protected phrase)를 누락한 출력은 거부됩니다.
이 간단한 체크 방식에는 한계가 있습니다. 굴절 (inflection), 유니코드 정규화 (Unicode normalization), 또는 헤딩 (heading) 내에서만 용어를 보존해야 하는 것과 같은 요구 사항은 처리하지 못합니다. 현재 제품의 경우, UI에서 문구의 문자 그대로의 보존 (literal phrase preservation)을 약속하고 있으므로, 문자 그대로의 검증 (literal validation)이 가장 정직한 구현 방식입니다.
중요한 설계 선택 사항은 결정론적 검증기 (deterministic validator)가 작업을 수행할 수 있을 때, 모델이 스스로의 준수 여부를 평가하게 하지 않는 것입니다.
가장 흔한 두 가지 '재작성 실패' 사례 거부하기
모델은 때때로 원문을 변경하지 않고 그대로 반환합니다. 또한 때때로 레이블 (label)을 앞에 붙이고 그것을 재작성이라고 부르기도 합니다:
Professional version: [원문]
첫 번째 정제 단계는 흔히 쓰이는 레이블을 제거합니다:
function stripRewriteLabel(text: string) {
return text.replace(
/^(paraphrased text|professional version|rewritten version|rewrite|output|version)\s*[::-]\s*/i,
...
다음 단계는 정규화된 단어 중복도 (normalized word overlap)를 비교합니다:
function isTooSimilar(output: string, input: string) {
const outputWords = normalize(output).split(" ").filter(Boolean);
const inputWords = normalize(input).split(" ").filter(Boolean);
...
이것은 의도적으로 거친 가드레일 (guardrail)로 설계된 것이지, 품질 지표 (quality metric)가 아닙니다. 단어 중복도만으로는 변경된 출력이 좋은지 여부를 판단할 수 없습니다. 단지 요청된 재작성 (rewrite) 수행에 명백히 실패한 출력을 잡아낼 뿐입니다.
프로덕션 버전에서는 위의 단순화된 멤버십 테스트 (membership test) 대신 토큰 빈도 (token frequencies)나 n-gram을 사용해야 합니다. 그렇지 않으면 반복되는 단어가 중복도 점수를 왜곡할 수 있습니다.
잘못된 형식의 "JSON" 대비하기
엄격한 JSON을 반환하도록 지시받더라도, 모델은 다음과 같은 결과를 생성할 수 있습니다:
- 마크다운 코드 펜스 (Markdown code fences)
- 객체 (object) 앞의 설명
- 잘린 배열 (truncated arrays)
- 출력 문자열 내부에 인코딩된 JSON 객체
- JSON 대신 유효한 산문 (prose)
파서는 먼저 코드 펜스 (code fences)를 제거하고 가장 바깥쪽 객체를 추출합니다. 그런 다음 outputs가 비어 있지 않은 배열인지 검증합니다. 잘못된 형식의 응답은 UI로 직접 전달되지 않습니다.
function extractJsonObject(text: string) {
const trimmed = text
.trim()
.replace(/^```
(?:json)?\s*/i, "")
.replace(/\s*```$/i, "");
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
return trimmed;
...
복구 작업은 보수적이어야 합니다. 의도된 구조를 공격적으로 추측하는 것은 제공자 (provider)의 실패를 겉보기에는 유효하지만 부정확한 텍스트로 바꿀 수 있습니다. 파싱 (parsing) 또는 검증 (validation)이 실패할 경우, 응답이 신뢰할 수 있는 것처럼 가장하기보다는 명확하게 표시된 폴백 (fallback)을 반환하는 것이 더 안전합니다.
제공자 가용성과 제품 동작의 분리
애플리케이션은 Cloudflare Workers AI 바인딩을 사용할 수 있으며, 필요할 때 OpenAI 호환 제공자로 폴백할 수 있습니다:
async function rewriteWithAIProvider(env, payload) {
if (env.AI) {
try {
...
제공자 폴백은 가용성을 향상시키지만, 서로 다른 모델은 동일한 수준의 재작성 (rewrites)을 생성하지 않습니다. 제공자 전환은 어조 (tone), 형식의 신뢰성, 그리고 의미론적 표류 (semantic drift)를 변화시킬 수 있습니다.
이는 평가 스위트 (evaluation suite)가 구성된 모든 모델에 대해 실행되어야 함을 의미합니다. 생성형 기능에서 "엔드포인트가 200을 반환했다"는 것만으로는 충분하지 않습니다.
누락된 검증기: 의미론적 충실도 (semantic faithfulness)
결정론적 파이프라인 (deterministic pipeline)은 스키마 (schema), 길이, 필수 문구, 그리고 출력이 실제로 변경되었는지 여부를 확인할 수 있습니다. 하지만 재작성이 의미를 보존했는지는 증명할 수 없습니다.
다음 소스를 고려해 보십시오:
The treatment may reduce symptoms in some patients.
유창하지만 안전하지 않은 재작성은 다음과 같이 말할 수 있습니다:
The treatment reduces symptoms in patients.
대부분의 단어와 일반적인 주제는 유사하게 유지되지만, "may"와 "some"이라는 두 가지 중요한 한정어 (qualifiers)가 사라졌습니다. 이제 주장이 더 강해졌습니다.
유용한 의미론적 평가 세트에는 다음과 같은 사례들이 포함되어야 합니다:
- 부정 (Negation)
- “some,” “most,” “all”과 같은 양화사 (Quantifiers)
- “may” 및 “suggests”와 같은 불확실성 단어 (Uncertainty words)
- 날짜, 가격, 측정값 및 백분율
- 고유 명사 (Named entities) 및 제품명
- “less than” 및 “at least”와 같은 비교 (Comparisons)
- 인과관계 (Cause) 대 상관관계 (Correlation)
- 인용구 및 출처가 명시된 주장 (Attributed claims)
저는 각 사례를 최소 네 가지 차원에서 평가할 계획입니다:
- 필수 용어 유지 (Required-term retention) — 결정론적인 합격/불합격 (pass/fail) 체크.
- 사실적 불변량 유지 (Factual invariant retention) — 숫자, 엔티티, 날짜 및 부정 (negations).
- 의미론적 충실도 (Semantic faithfulness) — 문서화된 루브릭 (rubric)을 사용한 인간 검토.
- 재작성 유용성 (Rewrite usefulness) — 텍스트가 요청을 충족할 만큼 충분히 변경되었는지 여부.
임베딩 유사도 (Embedding similarity)는 의심스러운 사례의 우선순위를 정하는 데 도움이 될 수 있지만, 유일한 판단 기준이 되어서는 안 됩니다. 두 문장은 벡터 공간 (vector space)에서 가깝게 위치하면서도, 정작 중요한 특정 양화사 (qualifier)에 대해서는 서로 상충할 수 있습니다.
다음에 변경할 사항
현재의 파이프라인은 유용한 베이스라인 (baseline)일 뿐, 완성된 솔루션은 아닙니다. 제가 계획 중인 다음 개선 사항들은 의도적으로 좁은 범위에 집중되어 있습니다:
- 생성 (generation) 전에 사실적 불변량 (factual invariants)을 추출하고 생성 후에 이를 비교합니다.
- 부정, 숫자, 불확실성에 대한 적대적 테스트 (adversarial tests)를 추가합니다.
- 단순한 중첩 계산 (overlap calculation)을 n-gram 기반의 변경 점수 (change score)로 대체합니다.
- 평가 결과와 함께 제공자 및 모델 버전을 기록합니다.
- 모델이 생성한
meaningPreserved값을 마치 증거인 것처럼 제시하는 것을 중단합니다. - 입력, 출력, 실패 사례 및 채점 루브릭 (scoring rubric)을 포함한 작은 벤치마크 (benchmark)를 공개합니다.
더 넓은 관점에서의 교훈은 생성형 기능 (generative feature)에는 두 가지 설계가 필요하다는 것입니다: 생성 설계 (generation design)와 거부 설계 (rejection design). 프롬프트 엔지니어링 (Prompt engineering)은 우리가 무엇을 요청할지를 제어합니다. 검증 (Validation)은 우리가 무엇을 보여줄 용의가 있는지를 제어합니다.
유창함 (Fluency)은 유용합니다. 하지만 충실도 (Faithfulness)가 실제 제품 요구사항입니다.
여러분은 테스트 세트에 어떤 의미론적 드리프트 (semantic-drift) 사례를 추가하시겠습니까?
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기