LLM은 왜 돌아갈 곳을 잃는가 — Bogyie
← Writing

LLM은 왜 돌아갈 곳을 잃는가

긴 작업에서 에이전트가 놓치는 것은 기억보다 복귀 위치일 수 있다. Call Stack의 은유에서 출발해 Goal, Plan, CallFrame, Event Log를 갖춘 Agent Runtime을 설계한다.

LLM이 복잡한 문제를 푸는 모습을 보고 있으면 종종 깊이 우선 탐색이 떠오른다. 하나의 문제를 붙잡고 그 안에서 발견한 하위 문제로 내려간다. 도구를 사용하다 새 오류를 만나면 다시 그 원인을 파고든다. 그러다 보면 처음 해결하려던 문제보다 몇 단계 아래의 작업에 오래 머물기도 한다.

처음에는 탐색 전략의 문제라고 생각했다. 깊이 들어가는 대신 여러 가능성을 먼저 펼치는 너비 우선 모드를 제공하면 되지 않을까. 하지만 생각을 이어갈수록 핵심은 DFS 자체가 아니라 복귀 위치에 있다는 쪽으로 판단이 바뀌었다.

전통적인 프로그램이 함수 호출을 따라 내려갈 때는 Call Stack이 함께 움직인다. 각 Stack Frame에는 호출 인자, 지역 상태, 복귀 주소, 반환값을 받을 위치가 남는다. 가장 깊은 작업이 끝나면 어디로 돌아가 무엇을 이어갈지 구조적으로 정해져 있다.

반면 많은 LLM Agent의 실행 기록은 하나의 Context Window 안에 자연어와 도구 호출의 연속으로 놓인다. 왜 이 하위 작업을 시작했는지, 끝나면 어느 계획으로 복귀해야 하는지, 호출자가 어떤 결과를 기다리는지도 대화 속 의미로 암묵적으로 남는다.

Context에 여유가 있을 때는 제법 잘 작동한다. 문제는 작업이 길어져 과거 내용을 압축하거나 새 Context로 넘길 때다. 요약은 대략적인 의미를 보존할 수 있지만 Stack Pointer나 Return Address 같은 제어 상태까지 정확히 보존한다고 보장하지 않는다.

이때 사라지는 것은 사실 몇 개가 아닐 수 있다.

  • 아직 완료하지 않은 의무
  • 현재 작업을 시작하게 만든 상위 목적
  • 결과를 돌려줘야 할 호출자와 반환 형식
  • 이미 시도하고 실패한 경로
  • 잠시 보류했지만 다시 검증해야 할 가설
  • 작업이 끝났다고 판단할 조건

즉 LLM이 잃는 것은 기억보다 continuation, 다시 말해 “다음에 어디로 돌아가 무엇을 계속해야 하는가”일 수 있다.

여기서 선을 하나 그어야 한다. LLM이 내부에서 실제로 DFS 알고리즘을 실행한다거나, 신경망 안에 손상되기 쉬운 Call Stack이 있다는 주장은 아니다. 더 정확히 말하면 다음과 같다.

LLM Agent의 실행 경로는 DFS처럼 깊어질 수 있지만, 그 실행을 지탱하는 복귀 위치와 미완료 의무가 명시적인 Runtime 상태로 관리되지 않는 경우가 많다.

이 글에서는 이 문제를 긴 Prompt가 아니라 Runtime 설계로 풀 수 있는지 살펴본다.

긴 Context와 안전한 실행 상태는 다르다

긴 Context Window는 압축 시점을 늦추고 더 많은 기록을 담는다. 그러나 “Context 안에 있다”와 “필요한 순간에 정확히 복원한다”는 같은 말이 아니다. Lost in the Middle은 필요한 정보가 긴 입력 안에 있어도 그 위치에 따라 모델의 활용 성능이 달라질 수 있음을 보였다.

장기 실행 Agent를 만든 팀들도 이 문제를 Context 크기만으로 풀지 않는다. Anthropic은 장기 개발 실험에서 Agent가 한 번에 너무 많은 일을 시도하거나, 다음 세션이 이전 진행 상황을 추측하거나, 일부 기능만 완성한 채 전체 작업이 끝났다고 선언하는 실패를 관찰했다. 이를 줄이기 위해 진행 파일, Git history, 기능 목록과 세션 간 handoff를 함께 사용했다. 장기 실행 Agent를 위한 Harness

따라서 결론은 “Compaction은 실패한다”가 아니다.

Compaction은 Context 관리 기법이지 실행 상태의 정합성을 보장하는 Checkpoint Protocol은 아니다.

모델과 Context가 좋아지면 실패 빈도는 줄 수 있다. 그래도 실행의 정확성이 중요하다면 Goal, Plan, 호출 관계와 완료 조건은 모델의 기억과 별도로 남길 필요가 있다.

Goal은 첫 Prompt가 아니라 완료 계약이다

대부분의 Agent 작업에서 Goal은 최초 사용자 메시지에 있다. 이후 탐색, 수정, 오류 메시지와 도구 출력이 쌓이면 Goal은 과거로 밀려난다. 요약에 목표 문장이 남아도 비목표, 제약, 완료 기준은 약해질 수 있다.

그래서 Goal은 대화의 첫 문단보다 버전이 있는 실행 계약에 가까워야 한다.

GoalSpec
- goal_id
- revision
- outcome
- non_goals
- constraints
- acceptance_criteria
- status: active | paused | satisfied | abandoned

Goal은 일반적인 Compaction 대상에서 제외한다. 변경할 수는 있지만 조용히 덮어쓰지 않고 새 revision으로 기록한다. 그래야 실행 중인 Task가 어느 버전의 목표에서 파생됐는지 추적할 수 있다.

이 구조는 Goal drift도 드러낸다. 작업 중 더 좋아 보이는 아이디어가 생겼을 때 원래 요청을 슬쩍 바꾸는 대신, 목표 변경이 필요한 발견으로 사용자나 상위 Runtime에 제안할 수 있다.

Plan은 다시 시작할 수 있어야 한다

Plan을 단순한 할 일 목록으로 보면 부족하다. 장기 작업에서 Plan은 현재 위치와 다음 위치를 잇는 외부화된 continuation이어야 한다.

OpenAI가 소개한 ExecPlan은 진행에 따라 갱신되는 자기 완결적 문서다. milestone, 검증 기준, 진행 상황, 발견 사항과 결정 기록을 포함하며, 과거 대화 없이 Plan과 작업 트리만으로 실행을 재개할 수 있어야 한다. PLANS.md를 사용한 장기 작업

다만 계획을 자세히 쓰는 것 자체가 목적이 되어서는 안 된다. 아직 코드를 읽지 않은 Planner가 세부 구현까지 고정하면 잘못된 판단이 후속 작업 전체로 전파될 수 있다. 나는 Plan을 다음 두 층으로 나누는 편이 낫다고 본다.

  • Stable Plan: 목표, 비목표, milestone, 의존성, 완료 조건
  • Adaptive Plan: 현재 구현 선택, 새로 발견한 사실, 다음 후보, 예상 비용

Stable Plan은 자주 흔들리지 않아야 하고, Adaptive Plan은 실제 결과와 맞지 않으면 적극적으로 바뀌어야 한다.

Call과 Return을 실행 상태로 만든다

Agent가 하위 Agent나 도구를 호출할 때 자연어 한 문장만 넘기는 경우가 많다. 작은 작업에는 충분하지만 장기 실행에서는 호출의 의미가 지나치게 암묵적이다.

Call에는 최소한 다음 정보가 필요하다.

CallFrame
- call_id, parent_id, task_id
- goal_revision
- objective
- input_refs, required_context_refs
- allowed_tools, constraints
- expected_output_schema
- completion_check
- token/time/cost_budget
- return_to

중요한 점은 Context 전체를 복사하지 않는 것이다. 큰 문서, 소스 코드와 조사 결과는 저장소에 두고 참조만 전달한다. 호출할 때 mutable alias는 가능하면 특정 version이나 content hash로 확정한다. 작업 도중 “최신 문서”가 바뀌어도 이미 시작한 Call의 의미가 바뀌지 않게 하기 위해서다.

Return도 자유로운 자연어 하나보다 명시적인 envelope가 유용하다.

ResultEnvelope
- call_id
- status: completed | partial | blocked | yielded | failed | cancelled
- output_refs, evidence_refs
- side_effects
- unresolved_items
- invalidated_assumptions
- suggested_next_nodes
- resume_token

모든 Call이 호출자에게 값을 돌려줘야 하는 것은 아니다. OpenAI Agents SDK는 Manager가 제어권을 유지하며 전문 Agent를 도구처럼 호출하는 방식과, 전문 Agent에게 대화의 소유권을 넘기는 handoff를 구분한다. Agents SDK의 다중 Agent 오케스트레이션

이를 일반화하면 Edge도 하나가 아니다.

Edge 의미 결과 처리
call 제한된 하위 작업 호출자에게 결과 반환
handoff 작업 소유권 이전 이전 호출자로 복귀하지 않을 수 있음
fork 독립 분기 생성 이후 Join에서 수집
join 여러 분기 통합 집계 결과 생성
event 상태 변화 통보 반환값 없음
yield 미완료 상태로 제어권 반환 Resume token 생성
retry 같은 의도의 새 시도 새 attempt와 연결
cancel 작업 종료 종료 사유 기록

반환값이 없는 것과 실행 위치를 잃는 것은 다르다. Handoff나 Event도 Runtime에는 누가 소유권을 넘겨받았고 어떤 전이가 일어났는지 남아야 한다.

DFS와 BFS는 사고방식보다 Scheduling Policy다

처음에는 사용자 설정에 따라 DFS와 BFS 모드를 나누려 했다. 여전히 쓸 수 있는 구분이지만, 이를 LLM의 사고 모드라고 부르면 내부 인지 과정에 대한 과도한 주장이 된다. 외부 Task Graph의 ready queue에서 다음 Node를 고르는 정책이라고 보면 더 명확하다.

  • DFS: 하나의 경로를 빠르게 끝까지 검증한다.
  • BFS: 여러 가능성을 얕게 살펴 조기 고착을 줄인다.
  • Best-first: 가장 유망하거나 정보 가치가 높은 Node를 먼저 고른다.
  • Risk-first: 실패했을 때 영향이 큰 가정부터 검증한다.
  • Cost-aware: 비용 대비 기대 가치가 높은 Node를 우선한다.

Graph와 Scheduler를 분리하면 같은 작업을 다른 정책으로 실행할 수 있다. 실제 기본값은 순수 DFS나 BFS보다 먼저 후보와 위험을 얕게 확인하고, 유망한 분기만 깊게 파는 혼합 전략에 가까울 것이다.

Tree of ThoughtsGraph of Thoughts는 복수 경로 탐색이 일부 문제에서 효과적일 가능성을 보여준다. 그렇다고 모든 작업에서 BFS가 우월한 것은 아니다. 분기가 늘면 비용이 커지고, 공유 상태를 수정하는 Agent 사이의 충돌도 다뤄야 한다.

하나가 아니라 세 종류의 Graph가 필요하다

“Agent를 Graph로 관리한다”는 표현은 범위가 너무 넓다. 최소한 세 관계를 구분해야 한다.

Execution Graph

어떤 작업이 실행 중이고, 무엇이 끝났으며, 어떤 Task를 다음에 실행할 수 있는지를 나타낸다.

Artifact Dependency Graph

코드, 문서, 데이터, 결정과 생성된 산출물이 무엇에 의존하는지 나타낸다. 입력이나 명세가 바뀌었을 때 어떤 결과를 다시 만들어야 하는지 판단한다.

Epistemic Graph

어떤 Claim이 어떤 Evidence, Source와 Assumption의 지지를 받는지 나타낸다. 무엇을 안다고 취급하는지뿐 아니라 왜 그렇게 판단했는지를 기록한다.

세 Graph는 연결되지만 같은 규칙으로 움직이지 않는다. 외부 Source가 갱신됐다고 실행 중인 모든 Task를 무효화할 필요는 없다. Source에서 Evidence, Claim과 Decision으로 이어지는 영향을 계산한 뒤 관련 Task만 다시 검증한다.

Source changed
→ Evidence stale
→ Claim needs_review
→ Decision potentially_affected
→ 관련 Task의 검증 Node 활성화

Execution Graph가 “무엇을 할 것인가”를 관리한다면 Epistemic Graph는 “왜 그렇게 믿는가”를 관리한다.

사실, 가설과 결정은 다른 객체다

Agent가 작업 중 얻는 지식은 성격이 다르다. 원문에서 확인한 사실, 여러 근거를 종합한 해석, 검증 전 가설, 편의를 위한 임시 가정, 사실 여부와 별개인 설계 결정이 모두 자연어 메모 한 덩어리에 들어가면 가설이 사실처럼 재사용되기 쉽다.

그래서 Knowledge Layer의 기본 단위는 문서 Chunk만이 아니라 Claim이어야 한다.

Claim
- claim_id, statement
- kind: fact | hypothesis | assumption | decision
- evidence_refs, source_refs
- observed_at, valid_time
- freshness_policy, verification_status
- contradicting_claims, derived_from

이 구조는 진실을 보장하지 않는다. 잘못된 Source와 Evidence 연결을 구조화하면 오류가 더 오래 남을 수도 있다. 대신 주장의 출처를 추적하고, 근거 없는 주장과 상충하는 주장을 표시하고, 오래된 정보를 골라 재검증하고, 잘못된 가정에 의존한 결과만 선택적으로 무효화할 수 있다.

이 방향은 출처와 생성 과정을 표현하는 W3C PROV-O와 가정 의존성 및 모순을 관리하는 Truth Maintenance System의 문제의식과 닿아 있다. 모델이 말한 confidence 또한 사실일 확률로 바로 취급하지 않는다. Source의 품질, 직접성, 최신성과 반증 상태를 별도로 보존해야 한다.

Context를 데이터베이스로 쓰지 않는다

Context는 작업 메모리이지 영구 저장소가 아니다. 원본 자료, 코드 분석 결과, 테스트 로그, 중간 산출물, Evidence bundle과 상세 Trace는 외부 저장소에 두고 Context에는 Pointer와 현재 작업에 필요한 요약만 넣는 편이 낫다.

Pointer 역시 식별자 하나로는 부족하다. 대상이 바뀌거나 권한이 사라지고, Source가 오래될 수 있다. identity와 함께 version, schema, freshness, provenance와 access policy가 필요하다.

Anthropic의 Code Execution + MCP 사례도 모든 Tool 정의와 대규모 중간 결과를 모델 Context로 통과시키지 않는다. 필요한 Tool만 점진적으로 읽고, 중간 데이터는 실행 환경에서 처리한 뒤 작은 결과를 모델에 돌려준다. MCP와 Code Execution

저장소만 마련한다고 끝나지는 않는다. 어떤 정보를 언제 Context에 편입할지 결정하는 Context Compiler가 필요하다.

ContextPack =
  GoalSlice
  + CurrentCallFrame
  + RelevantPlanSlice
  + RequiredArtifactViews
  + ActiveConstraints
  + RelevantClaimsAndEvidence
  + ReturnContract

전체 기록을 매번 압축해 넣는 대신 현재 Call에 필요한 투영을 새로 만드는 방식이다.

먼저 만들 최소 Runtime

처음부터 범용 Graph Agent 플랫폼을 만들 필요는 없다. 추상화가 늘수록 새로운 실패 지점도 늘어난다. 첫 단계에서는 상위 흐름을 고정해도 충분하다.

Clarify → Plan → Execute → Verify → Integrate
                     ↑         │
                     └─ Repair ┘

그 위에서 다음 여섯 객체만 명시적으로 관리한다.

  1. GoalSpec: 결과, 비목표, 제약과 완료 기준
  2. ExecutionPlan: Milestone, Task dependency, 진행 상황과 결정 기록
  3. CallFrame: 하위 작업의 입력, Context 참조, 예산과 반환 계약
  4. ResultEnvelope: 완료, 부분 완료, 차단, 중단 상태와 산출물 참조
  5. Event: 생성, 실행, 완료, 실패, Handoff, Retry와 무효화 기록
  6. Claim/Evidence: 지식의 종류, 근거, 최신성과 반증 상태

Runtime은 Event를 append-only로 기록하고 현재 상태를 Event에서 계산하거나 Checkpoint로 저장한다. LLM은 다음 Task와 상태 전이를 제안할 수 있지만, Runtime이 의존성, 권한, 예산과 완료 조건을 검사한 뒤 반영한다.

Context Reset은 Node나 Milestone 경계에서 수행한다. 새 Agent는 긴 대화 요약 대신 Goal, 현재 CallFrame, 관련 Plan, 필요한 Artifact와 Return Contract를 받는다. Compaction이 다소 부정확해도 실행 위치까지 함께 사라질 가능성을 줄이는 것이 목적이다.

어디까지가 근거이고 어디부터가 제안인가

이 생각을 모두 같은 확신으로 말할 수는 없다.

비교적 강하게 뒷받침되는 부분은 다음과 같다.

  • 긴 Context에 정보가 있어도 안정적으로 활용되지 않을 수 있다.
  • Compaction만으로 장기 작업의 연속성이 보장되지는 않는다.
  • Plan, 진행 기록, Git history와 handoff 같은 외부 상태는 복구를 돕는다.
  • 별도 Context를 쓰는 Subagent는 주 Context의 오염을 줄일 수 있다.
  • 테스트, Hook과 별도 검증 단계는 모델의 자기평가를 보완한다.
  • 대규모 Tool 결과를 실행 환경에서 처리하면 Context 사용량을 줄일 수 있다.

반면 아래 내용은 아직 설계 제안에 가깝다.

  • 이 글의 CallFrameResultEnvelope가 최적의 Schema라는 주장
  • DFS와 BFS 정책을 사용자에게 직접 노출하는 것이 좋은 UX라는 주장
  • Execution, Artifact, Epistemic Graph를 하나의 Runtime에서 통합하는 방식
  • Context Reset을 수행할 가장 효율적인 경계
  • 범용 Agent에 content-addressed artifact store가 필요한 범위

“LLM은 본질적으로 DFS로 사고한다”, “Knowledge Layer가 사실성을 보장한다”, “구조화된 출력은 의미적으로도 옳다”, “Graph는 단일 Agent loop보다 항상 낫다” 같은 표현은 여기서 주장할 수 없다.

Graph가 잘못 설계되면 오류를 구조적으로 증폭할 수도 있다. 잘못된 Planner의 판단이 여러 Node에 퍼지고, 잘못 연결된 Claim dependency가 정상 결과를 무효화하며, 자동 Retry가 같은 오류를 비싸게 반복할 수 있다. 이 구조의 가치는 복잡성을 늘리는 데 있지 않다. 모델의 암묵적인 기억에 있던 복잡성을 검증 가능한 Runtime 상태로 옮기는 데 있어야 한다.

비교 실험으로 확인할 것

이 아이디어를 검증하려면 인상적인 데모보다 비교 실험이 필요하다. 같은 장기 작업을 다음 네 조건으로 여러 번 수행해 볼 수 있다.

  1. 전체 대화 Context만 사용하는 단일 Agent
  2. Compaction 요약만 사용하는 단일 Agent
  3. Goal, Plan, CallFrame과 Event Log를 사용하는 Runtime
  4. 구조화 Runtime에 Claim, Evidence와 Freshness Layer까지 추가

최종 성공률만 측정하면 복귀 문제를 분리해 보기 어렵다. 중단 후 정확한 재개율, 잘못된 상위 Task로 복귀한 비율, 제약 유지율, 중복 Tool 호출률, 오래된 정보 사용률, 변경된 Source의 영향 탐지율, 사용자 개입 횟수와 실패 후 복구 비용을 함께 측정해야 한다.

각 구성 요소를 하나씩 제거하는 ablation도 필요하다. Goal revision, Return Contract, Artifact Pointer, Decision log, Verifier, Freshness와 invalidation을 각각 제거했을 때 어떤 실패가 늘어나는지 확인해야 “좋은 은유”를 “쓸 수 있는 Runtime”으로 발전시킬 수 있다.

모델이 기억하게 하지 말고 시스템이 이어가게 하자

처음의 직관은 LLM이 DFS처럼 문제 속으로 깊이 내려간다는 관찰이었다. 그러나 더 중요한 문제는 탐색의 모양보다 복귀의 불확실성이었다.

Call Stack은 현재 위치, 호출 관계, 복귀 주소와 반환값의 목적지를 구조적으로 보존한다. LLM Agent는 이 정보를 자연어 Context에 맡기는 경우가 많다. 작업이 짧을 때는 충분하지만, 길어지고 분기되고 압축되고 여러 Agent로 나뉘면 그 암묵성이 실패 원인이 된다.

그래서 LLM의 Context를 실행 상태의 유일한 저장소로 사용하지 않는 편이 낫다. Goal은 지속되는 계약으로, Plan은 재시작 가능한 문서로, 하위 작업은 CallFrame으로, 결과는 Return Contract로, 실행 위치는 Event Log와 Checkpoint로, 지식은 Claim과 Evidence로 외부화한다.

LLM이 모든 것을 기억하고 올바른 곳으로 돌아오기를 기대하기보다 Runtime이 돌아갈 곳을 알고 있어야 한다. 필요한 것은 더 오래 생각하라는 Prompt가 아니라, 기억을 잃어도 길을 잃지 않는 Agent Runtime이다.