LLM에 전체 스레드 제공하기: 문맥에 맞는 답변을 위한 프롬프트 체이닝 (Prompt Chaining)
요약
LLM이 단일 메시지가 아닌 전체 대화 맥락을 이해하도록 돕는 프롬프트 체이닝 전략을 소개합니다. 스레드 그래프 구축과 문맥 압축을 통해 토큰 효율성을 높이면서도 답변의 품질을 개선하는 방법을 다룹니다.
핵심 포인트
- 단일 트윗만 제공할 경우 LLM의 답변이 공허해질 수 있음
- 부모 트윗, 루트 트윗, 작성자의 이전 메시지 등 다양한 문맥 요소 활용
- 토큰 예산 관리를 위해 스레드 그래프 구축 및 깊이 제한(depth limit) 필요
- 문맥 체이닝을 통해 대화의 톤과 연속성을 유지하는 기술 설명
저는 이전에 우리의 persona engine — 즉, 어떻게 하나의 LLM이 여러 명의 서로 다른 사람처럼 말하게 만드는지에 대해 작성한 적이 있습니다. 하지만 훌륭한 페르소나(persona)라 할지라도 단일 트윗만을 고립된 상태로 보고 답변한다면 여전히 평범한 답변을 내놓게 됩니다. 왜냐하면 트윗은 결코 전체 이야기의 전부가 아니기 때문입니다. 트윗은 대화 속의 하나의 노드(node)입니다. 가장 좋은 답변은 이전에 무엇이 있었는지 참조하고, 다음에 무엇이 올지 예측하며, 자신이 참여하는 스레드(thread)의 어조(register)와 일치해야 합니다.
사람은 답변하기 전에 스레드를 읽습니다. 하지만 LLM은 기본적으로 당신이 건네주는 트윗만을 봅니다. 이 글은 LLM에 필요한 문맥(context) — 주변 스레드, 작성자의 이전 메시지, 대화의 톤(tone) — 을 어떻게 제공하는지, 그리고 고립된 트윗을 답변 준비가 된 문맥으로 바꾸는 프롬프트 체이닝 (Prompt Chaining) 전략에 관한 것입니다.
문제점: 문맥 없는 답변은 공허하다
다음 트윗을 생각해 보세요:
"드디어 출시했습니다. 6개월이 걸렸네요."
답변 A (문맥 없음):
"출시를 축하드립니다! 6개월간의 노력이 결실을 맺다니 정말 멋진 기분일 것 같네요. 🎉"
답변 B (스레드 문맥 포함 — 이전 트윗이 "우리 팀은 이번 릴리스를 위해 6개월 동안 QA 지옥에 갇혀 있었습니다"였을 경우):
"QA 지옥에서 탈출하는 것도 일종의 출시죠. 무엇이 마침내 막힌 부분을 뚫어주었나요 — 테스트 스위트(test suite)인가요, 아니면 프로세스인가요?"
답변 B가 훨씬 더 낫습니다. 답변 B는 이전 문맥("QA 지옥")을 참조하고, 구체적인 질문으로 대화를 진전시키며, 공허한 축하용 미사여구를 걷어냈습니다. 동일한 LLM, 동일한 페르소나입니다. 차이점은 전적으로 제공된 문맥(context)에 있습니다.
페르소나 엔진(persona engine)이 "특정 인물처럼 말하기"를 해결한다면, 문맥 체이닝(context chaining)은 "특정 대화에 적합한 말 하기"를 해결합니다. 당신에게는 이 두 가지가 모두 필요합니다.
우리가 제공하고자 하는 문맥
트윗 T에 답변할 때, 이상적인 문맥에는 다음이 포함됩니다:
- 트윗 T 자체 — 우리가 답변하려는 대상.
- 부모 트윗 (Parent tweet) (T가 답글인 경우) — T가 무엇에 대해 응답하고 있는가.
- 스레드의 루트 트윗 (Root tweet of the thread) — 대화의 기점.
- 스레드 내 동일 작성자의 이전 답글들 — 작성자의 입장을 확립.
- 이 스레드 내 우리 계정의 이전 답글들 (있는 경우) — 연속성 유지 및 모순 방지.
- 참여도(Engagement)가 높은 다른 답글들 — 대화의 톤과 방향성 파악.
상당히 많은 양입니다. 단순히 이 모든 것을 모든 프롬프트에 쏟아붓는다면 토큰 예산(Token budget)을 초과하고 신호(Signal)를 희석시킬 것입니다. 핵심은 어떤 트윗에 대해 어떤 문맥을 포함할지 선택하고, 이를 적절히 압축하는 기술에 있습니다.
1단계: 스레드 그래프 구축하기 (Build the thread graph)
X의 API는 트윗과 (때때로) 그 부모 트윗을 제공합니다. 전체 스레드를 재구성하기 위해, 우리는 부모 체인을 따라 위로 올라가고 답글을 따라 아래로 내려가며 작은 그래프를 구축합니다:
async function buildThreadContext(tweetId, depth = 3) {
const context = {
target: null,
...
깊이 제한(depth limit, 3)은 중요합니다. 50개의 트윗으로 구성된 전체 스레드를 모두 훑는 것은 비용이 많이 들고 대부분 노이즈(Noise)만 추가하게 됩니다. 3단계 정도 위로 올라가는 것은 과도한 데이터 수집 없이 대화의 흐름(Conversational arc)을 포착할 수 있습니다.
2단계: 프롬프트를 위한 문맥 압축하기 (Compress the context for the prompt)
가공되지 않은 스레드 데이터는 장황합니다. 트윗에는 메타데이터, ID, 타임스탬프 등이 포함되어 있는데, 이 중 대부분은 "내가 무엇을 말해야 하는가?"라는 질문과는 무관합니다. 우리는 프롬프트를 구성하기 전에 필수적인 요소로 압축합니다:
function compressThread(context) {
return context.thread.map(t => ({
author: t.author.handle,
...
그리고 이를 읽기 쉬운 대화 형식으로, 오래된 순서부터 렌더링하여 LLM이 자연스러운 순서로 읽을 수 있도록 합니다:
function renderThreadPrompt(compressed) {
const lines = compressed.map(t => {
const marker = t.isTarget ? ' [REPLY TO THIS]' : '';
...
[REPLY TO THIS] 마커는 매우 중요합니다. 이 마커가 없다면 LLM은 스레드 내의 어떤 트윗에 응답해야 하는지 알 수 없습니다. 이 마커는 생성 작업이 올바른 대상에 고정(Anchor)되도록 합니다.
3단계: 전체 프롬프트 조립하기 (Assemble the full prompt)
컨텍스트 블록(context block)은 (페르소나 엔진으로부터 가져온) 페르소나 프롬프트(persona prompt)를 결합하여 전체 시스템+사용자 프롬프트(system+user prompt)를 형성합니다:
function buildContextualPrompt(persona, context) {
const compressed = compressThread(context);
const threadBlock = renderThreadPrompt(compressed);
...
"대화를 자연스럽게 참조하라(reference the conversation naturally)"라는 지침과 "이전 메시지를 그대로 반복하지 마라(do not repeat earlier messages verbatim)"라는 지침은 LLM이 답변 A(공허한 답변)나 부모 트윗을 그대로 따라 하는 실패 모드(failure mode)를 피하고, 답변 B(문맥에 맞는 답변)를 생성하도록 유도합니다.
토큰 예산 문제 (The token budget problem)
여기에 상충 관계(tension)가 존재합니다. 더 풍부한 컨텍스트(context)는 더 나은 답변을 생성하지만, 풍부한 컨텍스트는 더 많은 토큰(tokens) 비용을 발생시키며, 토큰은 곧 비용(및 지연 시간, latency)을 의미합니다. 메타데이터가 포함된 5개의 트윗 스레드는 1,500개 이상의 토큰이 될 수 있으며, 하루에 수백 개의 답변을 생성할 경우 이는 실제 비용 부담으로 이어집니다.
우리는 이를 **적응형 컨텍스트 깊이(adaptive context depth)**를 통해 관리합니다. 즉, 중요할 가능성이 높을 때는 더 많은 컨텍스트를 포함하고, 그렇지 않을 때는 적게 포함합니다:
function selectContextDepth(context, persona) {
// 짧은 단독 트윗 (부모 없음) — 최소한의 컨텍스트 필요
if (context.parents.length === 0) return 'minimal';
...
- minimal (최소): 대상 트윗만 포함합니다. 참조할 스레드가 없는 단독 트윗의 경우에 사용됩니다.
- standard (표준): 대상 트윗 + 직계 부모 트윗. 가장 일반적인 경우입니다.
- full (전체): 재구성된 전체 스레드 + 형제 답변(sibling replies). 내용의 실체가 중요한 깊은 스레드나 단호한 페르소나의 경우에 사용됩니다.
이 방식은 "항상 전체 컨텍스트를 포함한다"는 방식에 비해 측정 가능한 품질 저하 없이 평균 토큰 사용량을 대략 절반으로 줄여줍니다. 대부분의 트윗은 전체 컨텍스트가 필요하지 않으며, 필요한 트윗에는 컨텍스트를 제공하기 때문입니다.
연속성 문제: 스스로 모순되지 않기 (The continuity problem: don't contradict yourself)
미묘한 실패 모드 중 하나는 다음과 같습니다. 한 계정이 트윗에 답변을 남긴 후, 나중에 동일한 스레드에서 다시 답변을 남겼는데 이 두 답변이 서로 모순되는 경우입니다. 인간 독자에게 이는 "자동화되었다 — 자신이 이전에 무엇을 말했는지 기억하지 못한다"라는 인상을 강하게 줍니다.
우리는 컨텍스트에 우리 자신의 이전 답변들을 포함함으로써 이를 방지합니다:
async function getOurPriorReplies(slotId, threadTweetIds) {
// 이 스레드에서 우리가 이미 보낸 답변들을 조회합니다
const priorReplies = await db.getRepliesByUs(slotId, threadTweetIds);
...
이 블록은 스레드 내에 이전 답변이 있는 경우에만 프롬프트(prompt)의 맨 앞에 추가됩니다. 이제 LLM은 자신이 이미 무엇을 말했는지 "알게" 되며, 동일한 대화 내의 여러 상호작용 전반에 걸쳐 일관된 입장을 유지합니다. 이러한 연속성은 계정이 상태가 없는 생성기(stateless generator)가 아니라 일관된 인격을 가진 사람처럼 느껴지게 만듭니다.
톤 매칭(tone-matching) 문제: 스레드의 어조(register)를 반영하기
농담이 가득한 스레드에는 진지한 답변을 원하지 않으며, 기술적인 스레드에는 격식 없는 답변을 원하지 않습니다. 우리는 문맥에서 유도된 가벼운 톤 힌트(tone hint)를 추가합니다:
function inferToneHint(context) {
const texts = context.thread.map(t => t.text).join(' ');
const hasQuestions = (texts.match(/\?/g) || []).length;
...
톤 힌트가 반환되면, 이는 페르소나(persona)를 교체하는 것이 아니라 페르소나의 톤을 "조절(modulate)"합니다. 예를 들어, 진지한 스레드에 있는 캐주얼한 페르소나는 완전히 진지해지는 것이 아니라 톤을 약간 높입니다. 힌트는 살짝 밀어주고(nudges), 페르소나는 중심을 잡습니다(anchors).
측정 항목
우리는 세 가지 구성에 대해 답변 품질(500개의 답변을 블라인드 테스트로 수동 검토하여 점수 매김)을 비교했습니다:
| 구성 (Configuration) | 평균 품질 (1-5) | "문맥적(Contextual)" 답변 |
|---|---|---|
| 대상 트윗만 포함 (문맥 없음) | 2.4 | 11% |
| ... |
"부모 트윗(parent)만 포함"에서 "전체 문맥(full context)"으로 넘어갈 때의 도약은 "문맥 없음"에서 "부모 트윗"으로 넘어갈 때의 도약보다 작지만(수익 체감 법칙), 여전히 유의미하며, "문맥적" 비율(트윗에 고립되어 반응하는 대신 대화에 명확하게 참여하는 답변)은 거의 두 배로 증가합니다. 계정의 모든 가치가 "이 답변은 진짜 같다"는 느낌에서 오는 경우, 이 두 배의 차이는 성장하는 계정과 그냥 지나쳐지는 계정을 가르는 기준이 됩니다.
배운 점
1. 페르소나(Persona)만큼 문맥(Context)도 중요합니다. 문맥이 없는 훌륭한 페르소나는 공허한 답변을 생성합니다. 반면, 훌륭한 문맥을 가진 평범한 페르소나는 관련성 높은 답변을 생성합니다. 두 가지 모두가 필요하지만, 대부분의 AI 답변 시스템은 페르소나에만 투자합니다.
2. 대상을 명시적으로 표시하세요. LLM에 스레드(Thread)를 전달할 때는 어떤 트윗에 답글을 달아야 하는지 알려주어야 합니다. 명시적인 표시가 없으면 모델은 방향을 잃고 표류하게 됩니다.
3. 상황에 따라 문맥의 깊이를 조절하세요. 모든 답변에 전체 문맥을 제공하는 것은 토큰(Token)을 낭비하며, 모든 답변에 최소한의 문맥만 제공하는 것은 품질을 희생시킵니다. 적응형 깊이(Adaptive depth)를 사용하면 중요한 순간에 지속 가능한 비용으로 품질을 확보할 수 있습니다.
4. 자신의 이전 답변을 포함하세요. 상태 비저장성(Statelessness)은 진행 중인 스레드에서 LLM의 가장 큰 특징(Tell)입니다. 이미 말했던 내용을 다시 피드백으로 제공하면 일관성을 유지하고 모순을 방지할 수 있습니다.
5. 프롬프팅 전에 압축하세요. 가공되지 않은 트윗 객체(Tweet objects)는 대부분 노이즈입니다. LLM이 확인하기 전에 작성자 + 텍스트 + 대상 표시(Target marker) 형태로 압축하세요.
6. 톤을 대체하지 말고, 톤을 맞추세요. 대화의 톤이 페르소나를 무시하게 하지 말고, 페르소나를 조절(Modulate)하도록 하세요. 페르소나는 상수(Constant)이고, 문맥은 변수(Variable)입니다.
프롬프트 체이닝(Prompt-chaining) 레이어는 대화에 갑자기 낙하산 타고 내려온 낯선 사람처럼 느껴지는 AI 답변과, 대화를 계속 따라오고 있었던 일반 참여자처럼 느껴지는 AI 답변을 구분 짓는 요소입니다. 둘 다 "AI가 생성한 것"이지만, 오직 하나만이 계정을 성장시킵니다.
HelperX는 전체 스레드 문맥, 페르소나 고정(Persona anchoring), 그리고 자기 연속성(Self-continuity)을 통해 답변을 생성하므로, 모든 답변이 참여 중인 대화에 자연스럽게 어우러집니다. 30일 무료 체험 가능.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기