-
file.py:LINE anchor가 진짜 그 줄을 가리키는가 — 매일 AST와 대조해서 RAG 거짓말 끊기IT 2026. 5. 30. 22:00
위키에
proxy.py:1247에서 인증 토큰을 검사한다라고 적었다. 그날 1247 줄은def verify_token(req):이었다. 일주일 뒤 누가 그 위에 import 두 줄을 추가하고 함수를 위로 옮겼다. 위키 본문은 그대로 —proxy.py:1247. 그 줄은 이제 빈 줄이다.이 상태가 위험한 건 위키만의 문제가 아니다. AI 에이전트가 RAG로 이 페이지를 받아 "토큰 검증이 어디서 이뤄지나"를 물으면, 에이전트는 위키의 anchor를 인용해 답한다. 코드와 어긋난 좌표를 자신 있게 가리키며. 사용자가 그걸 따라가면 빈 줄을 본다. 위키가 가짜 좌표를 가르치는 순간 RAG의 신뢰가 한 번에 무너진다.
이 글은 위키의 모든
file.py:LINEanchor를 매일 AST와 대조해 stale reference를 잡는 좌표 검증이 어떻게 동작하고 왜 매일 돌아야 하는지를 본다.1. anchor가 stale일 때 RAG가 거짓말한다
위키와 코드를 안전하게 연결하는 가장 단순한 방법은 모든 인용에
file.py:LINE좌표를 박는 것이다. "proxy.py의 인증 함수"가 아니라 "proxy.py:1247". 모호함이 없는 좌표는 AI 에이전트가 더 이상 "어디였는지 기억해 보자"고 추측할 필요가 없게 한다. 그대로 인용하면 된다.그런데 좌표는 깨지기 쉽다. 누가 그 파일에 줄을 한 줄만 추가해도 모든 후속 anchor가 한 줄씩 밀린다. 함수가 통째로 이동하면 anchor가 빈 줄이나 다른 함수 본문 안을 가리킨다. 좌표의 무모호성이 곧 staleness의 취약성이다. 그래서 라인넘버를 유지하려면 좌표를 매일 검증하는 동반 메커니즘이 반드시 같이 있어야 한다.
2. 보는 것 — anchor와 AST의 매칭
이 검증은 위키 본문의 모든
file.py:LINEanchor를 추출하고, 그 파일의 AST를 walk해서 LINE이 의미 있는 노드(함수 정의, 클래스, 메서드)와 일치하는지 본다. 의미 있는 노드 위에 있으면 통과, 빈 줄·주석·다른 함수 본문 가운데 위에 있으면 fail.흐름은 단순하다. 위키 본문에서
file:LINE을 추출하고, 그 파일의 AST를 walk해 LINE을 포함하는 노드 종류를 본다.FunctionDef·ClassDef·AsyncFunctionDef같은 의미 있는 노드 안에 LINE이 들어가 있으면 통과. 빈 줄이나 import 영역, 주석 위면 fail. 모든 anchor에 대해 매일 walk하면서 stale 비율을 측정한다.3. 구현 — ast 모듈 한 번 walk
Python에서는 표준 ast 모듈 한 번 호출이면 끝난다.
import ast, re # 위키 본문의 file.py:LINE 패턴 추출 ANCHOR = re.compile(r"\b([a-zA-Z_/.\-]+\.py):(\d+)\b") # 의미 있는 노드 — 빈 줄·import 위 anchor를 fail로 만드는 핵심 SIGNIFICANT = (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef) def anchor_grounded(wiki_text: str, repo_root: str) -> list[tuple[str, int]]: """위키의 anchor 중 의미 있는 노드 위에 있지 않은 것만 리턴.""" bad = [] for file_path, line_str in ANCHOR.findall(wiki_text): line = int(line_str) try: tree = ast.parse(open(f"{repo_root}/{file_path}").read()) except (FileNotFoundError, SyntaxError): bad.append((file_path, line)) # 파일 자체가 없거나 파싱 실패 continue # LINE이 의미 있는 노드의 lineno-end_lineno 범위 안에 들어가는가 if not any( isinstance(n, SIGNIFICANT) and n.lineno <= line <= (n.end_lineno or n.lineno) for n in ast.walk(tree) ): bad.append((file_path, line)) return bad핵심은
SIGNIFICANT튜플의 정의다. import·전역 변수·주석 위에 anchor가 있으면 fail로 간주한다. "proxy.py:5에서 logger를 설정한다"는 인용은 5번이 의미 있는 노드가 아니므로 fail. anchor를 박을 거면 함수/클래스를 가리키라는 doctrine을 검증 자체가 강제한다.4. drift 5% 임계치 — 왜 0%가 아닌가
위키 전체 anchor 중 stale인 비율이 5%를 넘으면 알림을 보낸다. 0%가 아니다. 이유는 두 가지다.
첫째, 위키 작성과 코드 변경 사이에는 항상 lag가 있다. PR이 머지된 직후, 그 PR이 함수를 옮겼다면 위키 anchor는 일시적으로 stale이다. 그 anchor를 매시간 update하는 시스템은 너무 비싸다. 다음 nightly가 위키를 재생성하면서 anchor가 자동 갱신된다. 5%까지의 일시적 stale은 자연스러운 lag로 본다.
둘째, 0% 임계치는 noise 폭주를 만든다. anchor 하나만 stale이어도 알림이 가면, 정상적인 코드 변경 흐름 자체가 알림 소스가 된다. 운영자가 알림을 끄게 되고, 결국 진짜 누적 drift도 묻힌다. 5%는 "정상 lag는 통과시키고, 누적 drift는 잡는다"의 협상 지점이다.
5. 무엇을 해결하고 어떤 효과가 있나
이 검증의 효과는 명확하다. RAG hallucination의 가장 큰 발판을 매일 끊는다. AI 에이전트가 위키를 RAG로 받아 답을 만들 때, anchor를 그대로 인용한다. 그 anchor가 의미 있는 노드를 가리키면 사용자가 따라갔을 때 실제 함수를 본다. anchor가 stale이면 사용자는 빈 줄을 본다. 신뢰는 한 번 무너지면 회복이 어렵다.
매일 자동으로 anchor를 확인하면 stale은 24시간을 넘기지 않는다. 코드를 옮긴 다음 날 nightly가 drift를 보고하고, 위키가 재생성되며 anchor가 새 좌표로 박힌다. 코드와 위키의 좌표 lag가 항상 1 day 이내로 닫힌다 — 이 보장이 RAG 위키의 본질적 가치다.
이 검증이 잡지 못하는 종류가 하나 남는다. anchor가 의미 있는 노드를 가리키긴 하지만 그 노드의 정체가 바뀌어 있는 경우다. 함수 A가 다른 자리로 이사를 갔고, 우연히 같은 줄에서 새 함수 B의 정의가 시작됐다면 —
def가 있는 줄은 맞으니 좌표 검증은 통과시킨다. 위키 본문은 여전히 "proxy.py:1247의 verify_token이 인증 토큰을 검사한다"라고 적혀 있는데 그 줄은 이제def authenticate(...)다. 좌표는 valid한 노드 위에 있지만 가리키던 의미는 사라졌다. 이건 다음 절에서 닫는다.6. 강화 — anchor에 함수명까지 박는다
좌표 검증의 마지막 false negative를 닫는 방법은 의외로 단순하다. anchor에 함수명까지 박으면 된다. doctrine을 한 칸 키운다.
proxy.py:1247 ← 기존 (좌표만) proxy.py:1247:verify_token ← 강화 (좌표 + 이름)위키 본문에 인용할 때 백틱 안 내용을 한 칸 더 길게 적는다. 그 한 칸이 의미 짝까지 같이 박힌 좌표가 된다. RAG가 이 anchor를 그대로 인용해도 LLM이 함수명을 추가로 추론할 필요가 없어진다 — 좌표와 이름이 같은 토큰 위에 같이 있다.
검증 로직도 한 줄 더 길어지는 수준이다. AST를 walk하면서 그 line을 포함하는 함수·클래스 노드의 이름들을 set으로 모으고, anchor에 박힌 이름이 그 set 안에 있는지 본다.
import ast, re # `path/to/file.py:LINE` 또는 `path:LINE:NAME` 둘 다 매치 ANCHOR = re.compile( r"`(?P<path>[A-Za-z0-9_/.\-]+?\.py)" r":(?P<line>\d+)" r"(?::(?P<name>[A-Za-z_][\w.]*))?`" ) SIGNIFICANT = (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef) def qualified_names_at(tree, line): """LINE을 포함하는 모든 함수·클래스의 정규화된 이름. 메서드는 `Foo.bar`, 중첩은 `Foo.Inner.baz`. """ out = set() def walk(node, prefix): if isinstance(node, SIGNIFICANT): start, end = node.lineno, node.end_lineno or node.lineno if not (start <= line <= end): return name = f"{prefix}{node.name}" out.add(name) for child in ast.iter_child_nodes(node): walk(child, f"{name}.") return for child in ast.iter_child_nodes(node): walk(child, prefix) walk(tree, "") return out def anchor_grounded(wiki_text, repo_root): bad = [] for m in ANCHOR.finditer(wiki_text): path, line, expected = m["path"], int(m["line"]), m["name"] try: tree = ast.parse(open(f"{repo_root}/{path}").read()) except (FileNotFoundError, SyntaxError): bad.append((path, line, expected)); continue names = qualified_names_at(tree, line) if not names: bad.append((path, line, expected)) # 좌표 fail elif expected and expected not in names: bad.append((path, line, expected)) # 이름 fail return bad핵심은
qualified_names_at의 출력이 단일 이름이 아니라 set이라는 점이다. 같은 line에 대해 메서드Foo.bar와 그 외곽 클래스Foo둘 다 후보로 들어간다. 위키 작성자가 어느 추상화 레벨로 인용했든 받아주는 게 자연스럽다 — 클래스 단위 인용도 valid, 메서드 단위 인용도 valid.또 하나의 디테일은 expected가 비어 있을 때의 동작이다. 기존 위키에 박혀 있는
proxy.py:1247형식 anchor를 한 번에 다 바꿀 수는 없다. validator는 expected가 비어 있으면 좌표만 검사하고 이름 비교는 skip한다 — backward compat이다. 마이그레이션 스크립트가 점진적으로 이름을 박아 채워 넣는다. ast로 line의 노드 이름을 lookup해 anchor를:NAMEform으로 in-place 치환하는 30줄짜리 enricher가 nightly에 한 번 돌면 위키 전체가 강화 form으로 수렴한다.이렇게 닫은 결과는 작지 않다. anchor의 좌표가 valid한 노드 위에 있고, 그 노드의 이름까지 위키가 기대한 이름과 같다 — 이걸 매일 검증하면 좌표가 가리키던 정체성까지 24시간 안에 보장된다. 좌표 검증이 좌표를 보증하고, 이름 검증이 정체성을 보증한다. RAG가 anchor를 그대로 인용해도 좌표와 이름 둘 다 맞다는 게 매일 자동으로 증명된다.
7. 정리
위키의 모든
file.py:LINEanchor를 매일 AST와 대조해 stale reference를 잡는 검증. 30ms × anchor 수의 비용, 5% drift 임계치, 매일 nightly. 잡는 신호는 좌표 + 이름 — 인용한 좌표가 실재하는 의미 있는 노드를 가리키고, 그 노드의 이름이 위키가 기대한 이름과 같은가. RAG hallucination의 가장 큰 발판인 stale anchor를 24시간 이내로 닫는 자리다. RAG가 거짓을 가르치지 않게 하는 가장 확실한 메커니즘이 좌표 + 이름 검증의 짝이다.
이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.
'IT' 카테고리의 다른 글
위키 하나에 저장소가 셋인 이유 — 그래프 DB·벡터 DB·파일의 분업 (0) 2026.06.01 agent가 wiki로 task를 풀 수 있느냐가 ground truth — with-wiki vs without-wiki로 측정하기 (0) 2026.05.31 같은 소스에서 매번 같은 위키가 나와야 한다 — 위키 생성 파이프라인의 흔들림 제어 (0) 2026.05.31 외부 API 100%, 내부는 trend — 양적 검증을 두 층으로 가르는 이유 (0) 2026.05.30 위키가 거짓말하지 않게 — 모든 코드 인용에 file:line을 강제하는 doctrine (0) 2026.05.30 코드 위키의 빈 박스와 깨진 다이어그램 — mermaid 검증 2중 안전망 (0) 2026.05.30 LLM이 다른 LLM의 답을 채점하는 법 — judge prompt·rubric.json·3가지 안티패턴 (0) 2026.05.29 LLM 검증을 싸게 — haiku 1차 + sonnet 재검증 + Redis SHA 캐시 (1) 2026.05.29 AI 에이전트가 보는 surface를 8개로 좁히다 — deep-wiki MCP gateway (0) 2026.05.28 Redis — 메모리 안의 작은 사전, 그리고 우리가 그것을 쓰는 자리들 (0) 2026.05.28