에이전트에게 무엇이 사실인가
소스 코드, AST, Code Graph, 실행 결과와 Agent Memory가 각각 무엇을 증명하는지 구분하고, 코드 변경 뒤 기억을 선택적으로 갱신하는 방법을 설계한다.
첫 글에서는 실행 위치를 Context 밖에 보존하는 Runtime을 제안했고, 두 번째 글에서는 Agent, Harness, Loop, Goal, Plan과 Graph의 책임을 나눴다. 실행은 이제 중단된 위치로 돌아갈 수 있다. 그렇다면 그곳에서 다시 꺼낸 지식은 믿어도 될까?
에이전트가 저장소를 분석한 뒤 다음과 같이 기억한다고 해보자.
createSession은 로그인 성공 후에만 호출되고, 반환된 세션은 Redis에 저장된다.
이 문장은 처음 분석했을 때는 맞았을 수 있다. 그러나 코드가 바뀌거나 다른 실행 경로를 놓쳤다면 더 이상 사실이 아니다. Memory가 문장을 정확히 보존한 것과 문장이 현재도 참인 것은 별개의 문제다.
장기 실행 Agent에서 기억의 실패는 두 종류로 나뉜다.
- 필요했던 내용을 잊거나 잘못 복원한다.
- 정확히 기억했지만 현실이 바뀌어 내용이 오래된다.
첫 번째는 검색과 저장 방식의 문제다. 두 번째는 provenance, version과 invalidation의 문제다. 이 글은 두 번째 문제에 집중한다. Code Graph와 AST를 만든다고 진실을 얻는 것이 아니라, 각 표현이 무엇을 근거로 어떤 범위까지 말할 수 있는지 정해야 한다.
코드는 하나의 사실이 아니다
“코드가 그렇다”는 말에는 서로 다른 근거가 섞여 있다.
| 근거 | 직접 알 수 있는 것 | 그대로는 알 수 없는 것 |
|---|---|---|
| 소스 문자열 | 특정 version에 쓰인 문자 | 실제 실행 경로와 의미 |
| AST | 문법 구조, 선언과 표현식 | 동적 호출 결과와 운영 상태 |
| 정적 Code Graph | 분석기가 찾은 관계 | 분석에서 빠진 동적 관계 |
| 타입 검사 | 타입 시스템 규칙의 통과 여부 | 런타임 전체의 정확성 |
| 테스트 결과 | 특정 환경과 입력의 관찰 결과 | 실행하지 않은 모든 경우 |
| Trace·로그 | 실제 실행에서 관찰한 사건 | 관찰하지 않은 경로와 원인 |
| 문서·주석 | 작성자가 설명한 의도 | 현재 구현과의 일치 |
| Agent Memory | 과거 관찰과 판단의 기록 | 현재 저장소에서의 유효성 |
각 항목은 쓸모가 있지만 증명하는 범위가 다르다. AST에 호출 표현식이 있다고 그 호출이 운영 환경에서 실행됐다고 말할 수 없다. 테스트가 통과했다고 모든 입력에서 옳다고 말할 수도 없다. Trace에서 호출을 관찰했다고 다른 경로가 없다고 단정할 수도 없다.
따라서 Knowledge Layer에는 결론만 저장하지 않고 근거의 종류와 증명 범위를 함께 저장해야 한다.
AST는 문법을 보존하는 지도다
AST(Abstract Syntax Tree)는 소스의 문법 구조를 Tree로 표현한다. 예를 들어 함수 선언, 매개변수, 호출 표현식과 조건문을 문자열 검색보다 안정적으로 구분할 수 있다. 주석이나 공백이 달라져도 같은 종류의 Node를 찾을 수 있고, Node의 위치를 원본 코드와 연결할 수 있다.
Tree-sitter처럼 편집 중인 코드도 점진적으로 분석하는 Parser를 사용하면 파일 변경 후 전체 저장소를 처음부터 다시 읽지 않고 바뀐 구문 영역을 갱신할 수 있다.
하지만 AST는 보통 파일 안의 구문에 가깝다. 다음 질문에는 추가 분석이 필요하다.
- 이 식별자가 실제로 어느 선언을 가리키는가?
- import된 함수가 어떤 파일에서 왔는가?
- interface의 구현체 중 런타임에 무엇이 선택되는가?
- reflection, dependency injection과 동적 import를 어떻게 해석하는가?
- 환경 변수와 외부 서비스가 분기를 어떻게 바꾸는가?
AST를 “코드를 이해한 결과”로 부르기보다 더 풍부한 분석을 만들기 위한 구문 근거로 두는 편이 정확하다.
AstEvidence
- repository_revision
- file_path
- language, parser_version
- node_type
- source_range
- content_hash
- extracted_relation
repository_revision, parser_version과 source_range를 남겨야 나중에 같은
근거를 다시 확인할 수 있다. 파일 경로와 자연어 요약만 남기면 줄이 이동하거나
Parser 규칙이 바뀌었을 때 무엇을 봤는지 재현하기 어렵다.
Code Graph는 분석 결과이지 코드 자체가 아니다
Code Graph는 AST와 symbol resolution 등의 결과를 Node와 Edge로 연결한다.
Node에는 파일, symbol, function, type, test와 package가 올 수 있고, Edge에는
declares, imports, calls, implements, reads, writes, tested_by 같은
관계를 둘 수 있다.
route/login
└─calls→ authenticate
├─reads→ UserRepository
└─calls→ createSession
└─writes→ SessionStore
이 Graph가 있으면 Agent는 문자열이 같은 파일을 모으는 데서 그치지 않고 변경 영향과 실행 후보 경로를 좁힐 수 있다.
- 이 함수의 signature를 바꾸면 어떤 caller를 확인해야 하는가?
- 이 type을 구현하는 symbol은 무엇인가?
- 수정한 경로를 검증하는 test는 어디에 있는가?
- 변경된 설정을 읽는 component는 무엇인가?
그러나 A calls B라는 Edge는 사실처럼 보여도 실제 의미는 분석 방법에
의존한다.
Edge
- subject: A
- predicate: calls
- object: B
- repository_revision
- analyzer, analyzer_version
- confidence_kind: resolved | inferred | observed
- evidence_refs
- limitations
정적으로 symbol을 해석한 resolved, 이름과 주변 문맥으로 추정한 inferred,
실행 Trace에서 본 observed는 같은 Edge로 덮어쓰지 않는다. 서로 지지할 수는
있지만 증명 범위가 다르다.
Code Graph는 저장소의 객관적인 복사본이 아니다. 특정 revision을 특정 Analyzer로 읽어 만든 파생 산출물이다. Analyzer가 지원하지 않는 언어 기능이나 생성 코드, runtime wiring은 빠질 수 있다. 그래서 Graph 검색 결과에 “없다”는 사실을 곧바로 코드에 “없다”로 바꾸면 안 된다.
정적 관계와 실행 사실을 연결한다
정적 분석은 가능한 관계를 넓게 찾는 데 유용하고, 실행 관찰은 특정 조건에서 실제로 일어난 일을 확인하는 데 유용하다. 둘 중 하나를 진실의 단일 기준으로 삼기보다 연결해서 사용한다.
예를 들어 인증 경로를 수정한다면 다음 근거를 단계적으로 만들 수 있다.
- AST에서 route handler와 호출 표현식을 찾는다.
- symbol resolution으로 handler에서 session 함수까지 후보 경로를 만든다.
- 타입 검사로 변경이 언어 규칙을 위반하지 않는지 확인한다.
- 재현 테스트에서 만료 세션 입력의 실제 결과를 관찰한다.
- Trace가 필요하면 선택된 구현체와 외부 저장소 호출을 확인한다.
여기서 테스트의 passed도 다음 정보가 있어야 다시 사용할 수 있다.
TestObservation
- repository_revision
- command
- environment_fingerprint
- input_or_fixture
- exit_code
- observed_at
- output_ref
“테스트 통과”라는 Memory만 남기면 어느 commit, 어느 환경, 어떤 명령의 결과인지 알 수 없다. 코드가 바뀐 뒤에도 모델은 그 문장을 자연스럽게 재사용할 수 있다.
Memory는 사실 저장소가 아니라 재검증 대기열이다
Agent Memory는 보통 대화 기록, 요약, vector 검색 문서, key-value 상태와 episodic log 등을 가리킨다. 구현 방식은 달라도 공통된 위험이 있다. 잘 검색된 기억이 현재도 유효하다고 느껴진다는 점이다.
Memory 항목을 결론만 저장하는 문서가 아니라 Claim으로 다루면 이 위험을 드러낼 수 있다.
Claim
- statement
- kind: observation | inference | assumption | decision
- scope
- evidence_refs
- derived_from
- valid_at_revision
- freshness_policy
- verification_status
- invalidated_by
여기서 중요한 구분은 네 가지다.
- Observation: 특정 Source나 실행에서 직접 관찰했다.
- Inference: 여러 Observation으로부터 해석했다.
- Assumption: 작업을 진행하기 위해 임시로 참이라고 두었다.
- Decision: 사실 여부보다 선택의 결과다.
“Redis를 쓴다”는 설정과 코드를 관찰한 결과일 수 있다. “Redis 장애가 로그인 지연의 원인이다”는 Trace를 더 확인해야 할 가설이다. “이번 수정에서는 저장소를 교체하지 않는다”는 설계 결정이다. 세 문장을 같은 Memory에 평면적으로 넣으면 다음 Agent가 결정과 외부 사실을 구분하기 어렵다.
Memory 검색의 출력도 관련 문장 목록보다 현재 Task에서 쓸 수 있는 근거 묶음이어야 한다.
MemoryView
- relevant_claims
- supporting_evidence
- repository_revision_match
- stale_or_conflicting_claims
- required_reverification
잘 기억하는 시스템은 많은 문장을 꺼내는 시스템이 아니다. 지금 사용할 수 없는 기억을 표시하고, 어떤 검증을 해야 다시 사용할 수 있는지 알려주는 시스템이다.
코드가 바뀌면 기억을 모두 버리지 않는다
매 commit마다 모든 Code Graph와 Memory를 폐기하면 안전하지만 비싸다. 아무 것도 폐기하지 않으면 빠르지만 오래된 판단이 쌓인다. 필요한 것은 변경의 영향을 따라가는 선택적 invalidation이다.
파일 변경
→ 바뀐 AST Node 식별
→ 관련 symbol과 Edge 재분석
→ 의존 Claim을 stale로 표시
→ 영향받은 Decision과 Plan Node 찾기
→ 필요한 검증만 다시 실행
예를 들어 createSession 구현이 바뀌었다면 이 함수의 content hash를 근거로
삼은 Claim은 stale이 된다. 이 함수를 호출한다는 Edge만 근거로 한 Claim은
호출 관계가 유지되는지 재분석한다. 로그인 테스트 결과는 변경 영향 범위와
테스트의 입력에 따라 재실행 후보가 된다. “인증 방식을 교체하지 않는다”는
Decision은 자동으로 거짓이 되지 않지만, 새 구현이 그 제약을 위반하는지
검토해야 한다.
이때 invalidation은 삭제와 다르다. 과거 Claim을 남겨야 어떤 근거로 판단했고 무엇이 바뀌어 다시 검증했는지 추적할 수 있다.
active → stale → needs_review → verified
└──────────→ rejected
충돌을 없애지 말고 보존한다
새 근거가 과거 Memory와 다를 때 오래된 문장을 조용히 덮어쓰면 판단의 변화를 설명할 수 없다. 두 Claim을 함께 남기고 관계를 기록한다.
Claim A: createSession은 성공한 인증 뒤에만 호출된다.
evidence: code graph at commit X
Claim B: refresh route도 createSession을 호출한다.
evidence: trace and AST at commit Y
relation: B contradicts A within the current revision
resolution: A is stale; update affected plan nodes
충돌은 Knowledge Layer의 실패만은 아니다. 코드가 바뀌었거나 이전 분석이 불완전했다는 정보다. Source의 revision과 분석 범위를 비교해야 “둘 중 하나를 선택”하는 대신 왜 달라졌는지 판단할 수 있다.
Context에는 결론보다 근거의 투영을 넣는다
Code Graph와 Memory가 커져도 전체를 모델에게 제공할 필요는 없다. 현재 CallFrame을 기준으로 필요한 부분만 ContextPack으로 만든다.
ContextPack
- current_goal_and_constraints
- relevant_symbols_and_source_ranges
- resolved_and_inferred_edges
- recent_test_observations
- active_decisions
- stale_or_conflicting_claims
- verification_required_before_return
여기서 Context Compiler는 편리한 설명만 고르면 안 된다. 결론을 뒤집을 수 있는 충돌, stale 상태와 분석 한계도 포함해야 한다. 그렇지 않으면 저장소에는 불확실성이 남아 있어도 모델에게 도달하는 순간 확정된 이야기로 바뀐다.
최소한의 Knowledge Runtime
처음부터 모든 언어를 이해하는 Code Graph와 범용 Memory를 만들 필요는 없다. 한 저장소에서 다음 순서로 시작할 수 있다.
- 모든 근거에
repository_revision과content_hash를 붙인다. - AST에서 symbol과 source range를 안정적으로 식별한다.
defines,imports,calls,tested_by처럼 필요한 Edge부터 만든다.- Edge에
resolved,inferred,observed를 구분한다. - Agent의 결론을 Observation, Inference, Assumption, Decision으로 나눈다.
- 파일 변경이 의존 Claim을
stale로 바꾸게 한다. - 완료 전에 stale 핵심 Claim과 필요한 테스트가 남았는지 검사한다.
이 구조가 제공하는 것은 정답이 아니다. 어떤 주장이 어느 version의 무엇을 근거로 했고, 지금도 사용할 수 있는지 묻는 인터페이스다.
무엇이 사실인지는 질문과 시점에 달려 있다
코드에서 단 하나의 표현만을 진실의 원천으로 고르기는 어렵다. AST는 문법 구조에 강하고, Code Graph는 관계와 영향 범위를 탐색하기 좋다. 타입 검사는 특정 규칙을 판정하고, 테스트와 Trace는 제한된 조건에서 실행 결과를 관찰한다. Memory는 이 근거와 판단을 다음 작업으로 전달한다.
따라서 Agent가 답해야 할 질문은 “내가 이 내용을 기억하는가?”가 아니다.
- 어느 repository revision에서 관찰했는가?
- Source, AST, Graph, Test와 Trace 중 무엇이 근거인가?
- 직접 관찰인가, 추론인가, 가정인가, 결정인가?
- 그 근거가 증명하지 못하는 범위는 무엇인가?
- 무엇이 바뀌면 이 Claim을 다시 확인해야 하는가?
실행 위치를 외부화하면 Agent는 돌아갈 곳을 잃지 않는다. 근거와 유효 범위를 외부화하면 돌아간 뒤 오래된 기억을 사실로 반복하지 않는다.
기억을 많이 쌓는 것보다 중요한 것은 기억이 현실과 연결된 조건을 보존하는 일이다. Agent에게 필요한 Memory는 과거의 답을 저장하는 창고가 아니라, 현재도 사실인지 다시 물을 수 있는 근거의 구조다.