-
생성된 위키 문서에서 틀린 줄을 발견했다 — 고치지 말고, 입력을 바꿔라IT 2026. 6. 5. 21:00
코드를 읽어 위키 문서를 LLM이 통째로 써 주는 시스템을 굴리다 보면, 어느 날 반드시 이 순간이 온다. 자동 생성된
architecture.md를 읽다가 틀린 한 줄, 혹은 어색한 표현, "여기는 이렇게 서술됐으면" 싶은 문장을 발견한다. 손이 자연스럽게 그 파일로 간다. 고치고 저장한다. 그리고 언젠가 그 코드가 바뀌어 페이지가 다시 쓰이는 순간, 그 수정은 흔적도 없이 사라진다.이게 직관에 반하는 첫 사실이다. 문서를 더 정확하게 만들려고 한 손질이, 시스템 입장에선 그냥 덮어쓸 대상이었다. 이유는 단순하다 — 이 위키의 생성물은 원본 코드가 바뀔 때마다 코드에서 다시 쓰인다. (매일 밤 자동 파이프라인이 돌긴 하지만, 전체를 다시 쓰는 게 아니다. 그사이 git HEAD가 움직인 — 즉 코드가 바뀐 — repo의 페이지만 골라 재생성하고, 바뀌지 않은 repo는 건드리지 않는다.) 코드가 진실의 원천(ground truth, 검증된 사실의 기준점)이고, 위키는 거기서 파생된 산출물이라, 산출물을 직접 고치는 건 강물 하류에서 물을 퍼내 상류로 거슬러 붓는 것과 같다. 코드가 다음에 바뀌어 재생성되는 순간 쓸려 내려간다.
그럼 사람의 판단은 어디로 들어가야 하나? 이 글은 처음 떠올린 "사람 편집을 보존하자"는 길이 왜 막다른 골목이었는지, 그리고 발상을 뒤집어 "출력을 고치지 말고 입력을 바꾼다"로 옮겨 간 과정 — 구체적으로 steering note와 pin override라는 두 입력 경로를 어떻게 설계했는지를 정리한다.
1. 처음 생각한 길 — "사람 편집을 비파괴적으로 보존하자"
가장 먼저 떠오르는 답은 "사람이 고친 건 절대 안 덮어쓰면 되잖아"다. 자동화가 사람 손길 위에서 비켜서게 만들면 된다. 구체적으로는 이렇게 그려진다. 생성 블록마다 지문(provenance 해시, 그 블록을 마지막으로 누가 어떤 내용으로 남겼는지 식별하는 짧은 해시값)을 새겨 둔다. 재생성할 때 지금 블록의 실제 해시와 새겨진 지문을 비교해서, 같으면 "사람이 안 건드림"이니 안전하게 갈아끼우고, 다르면 "사람이 건드림"이니 덮어쓰지 않고 드리프트(drift, 원본과 생성본이 어긋난 상태)로 표시만 한다.
이론적으로는 우아하다. 사람 수정도 안 죽고 최신 사실도 반영된다. 그런데 이 길을 끝까지 따라가 보면 두 가지가 무너진다.
- 코드가 바뀔 때마다 충돌이 쌓인다. 사람이 고친 문서는 재생성 때마다 "지문 불일치"로 잡혀 보류된다. 코드가 그 사이 또 바뀌었으면 보류된 사람 버전은 점점 stale(낡아서 현실과 어긋난 상태)해진다. 결국 누군가 그 충돌을 매번 손으로 풀어야 한다. 1인 운영에서 가장 비싼 자원인 사람의 주의력이 코드를 고칠 때마다 충돌 해소에 빨려 들어간다.
- "코드가 ground truth"라는 원칙에 금이 간다. 사람이 출력을 고치고 그게 보존되는 순간, 그 문서는 더 이상 코드의 파생물이 아니다. 코드와 문서가 두 개의 진실을 갖게 되고, 시간이 갈수록 둘은 벌어진다. 애초에 위키를 코드에서 생성하기로 한 이유(드리프트를 재생성으로 없앤다)가 무력화된다.
핵심 깨달음은 여기서 나왔다. 이 모든 복잡함 — 지문, 드리프트 감지, 보류 큐, 사이드카 파일 — 은 전부 "사람이 출력을 고친다"는 전제 하나에서 파생된다. 그 전제를 버리면, 딸려 오는 기계장치 전체가 통째로 사라진다.
2. 발상의 전환 — 출력이 아니라 입력에 개입한다
다이어그램 설명. 왼쪽(빨강)은 막다른 길이다 — 사람이 생성물을 직접 고치면 다음 재생성에 덮어써지고, 그 경험을 한 번 한 사람은 다시 고치지 않는다. 오른쪽(초록)이 택한 길이다. 사람은 생성물에 손대지 않는다. 대신
wiki-steering/이라는 버전관리되는 입력 폴더에 자기 의도를 적어 둔다. 생성 파이프라인이 매번 그 입력을 읽어 반영하므로, 재생성이 몇 번을 돌든 사람의 의도는 매번 다시 반영된다. 출력을 보존하려 애쓰는 대신, 보존할 필요가 없는 자리(입력)에 의도를 두는 것이다.전환의 요점은 한 줄로 압축된다. 생성물은 어차피 버려지고 다시 만들어진다(우리 시스템에서 출력 폴더는 git에서 제외돼 있다). 버려지는 것을 고치는 건 의미가 없다. 사람의 판단이 살아남으려면 버려지지 않는 곳, 즉 생성의 입력에 들어가야 한다. 그러면 충돌 처리도, 지문도, 드리프트 큐도 필요 없다 — 합칠 두 버전이 애초에 생기지 않으니까.
3. 두 입력 경로 — 같은 폴더, 다른 발행 규칙
사람이 위키에 개입하고 싶은 강도는 두 가지로 나뉜다. 하나는 "이 페이지에서 이 점을 강조/수정해 줘" 정도의 연성 조정이고, 다른 하나는 "이 페이지는 내가 직접 쓴 걸 그대로 내보내 줘"라는 완전한 소유다. 이 둘을 같은 입력 폴더 안에서 파일 확장자로만 구분한다.
다이어그램 설명. 두 경로는 입력 폴더는 같고 파일 확장자만 다르다. steering note(파란 열)는 자유 텍스트 가이드다 — 생성 프롬프트에 "이 점을 반영하라"고 끼워 넣을 뿐, 본문은 여전히 LLM이 쓴다. 그래서 tier(문서의 신뢰 등급·출처 태그)는
llm-generated로 남고, LLM 출력을 대상으로 하는 환각 검사와 앵커 검증을 그대로 통과해야 한다. pin override(보라 열)는 사람이 본문을 직접 박는다 — LLM을 건너뛰고 그 글을 그대로 발행하며, tier가human-pinned이 된다. 이 태그 하나가 두 가지를 동시에 한다. 사람이 쓴 글을 LLM 환각 검사에서 자동으로 빼 주고(사람 글을 기계 환각 검사에 거는 건 무의미하다), 동시에 코드 인용의 정확성 책임을 사람에게 넘긴다. 대부분의 개입은 왼쪽(steering)으로 충분하고, pin은 "이 페이지만큼은 내가 통째로 책임진다"는 드문 탈출구로 좁게 쓴다.4. 구현 — 입력을 읽는 두 함수와, tier를 바꿔 박는 한 줄
코드는 의외로 작다. 출력 보존 방식의 복잡함(지문 비교, 드리프트 큐, 사이드카)이 통째로 증발했기 때문이다. 핵심은 입력 폴더에서 파일을 읽는 두 함수와, 발행 직전에 tier를 결정하는 분기뿐이다.
# scripts/generate_repo_narrative_pages.py — 사람 입력을 읽는 두 함수 # wiki-steering/ 는 git으로 추적된다 (출력 폴더 wiki-output/ 은 gitignored). STEERING_ROOT = Path(__file__).resolve().parent.parent / "wiki-steering" def load_steering(repo: str, page: str) -> str: # 1) 연성 가이드: 자유 텍스트를 프롬프트에 주입한다. # LLM이 여전히 grounded 산문을 쓰므로 앵커 검증은 그대로 적용된다. path = STEERING_ROOT / repo / f"{page}.md" try: return path.read_text(encoding="utf-8").strip() except OSError: return "" # 파일 없으면 빈 문자열 — 평소처럼 LLM이 알아서 생성 def pinned_override(repo: str, page: str) -> str | None: # 2) 완전 소유: 사람이 박은 본문을 그대로 반환하고, LLM은 호출조차 안 한다. path = STEERING_ROOT / repo / f"{page}.override.md" try: return path.read_text(encoding="utf-8") except OSError: return None # 파일 없으면 None — LLM 생성 경로로 진행코드 설명. 두 함수 모두 "파일이 있으면 사람 입력, 없으면 평소대로"라는 단순한 분기다.
load_steering은 가이드 텍스트를(없으면 빈 문자열),pinned_override는 사람이 박은 본문을(없으면None) 돌려준다. 본문 생성기는 가장 먼저pinned_override를 확인해서, 값이 있으면 LLM을 건너뛰고 그대로 반환한다. 값이 없을 때만load_steering의 가이드를 프롬프트에 끼워 LLM 생성으로 넘어간다. 이 "있으면 사람, 없으면 기계" 패턴 덕에 입력 폴더가 비어 있으면 시스템은 종전과 100% 동일하게 동작한다 — 사람 개입은 순수하게 추가적(opt-in)이다.# scripts/generate_repo_pages.py — 발행 직전, pin이면 tier를 바꿔 박는다 body = generate_narrative_body(page, repo, conn) # pin이면 override를 verbatim 반환 if body: # 같은 (repo, page)에 대해 pin이 있으면 사람 소유 → tier를 바꾼다. pinned = pinned_override(repo, page) is not None write_page( out_dir, page, body, # human-pinned 으로 태그하면 환각 검사기가 "이건 사람 글"이라 보고 건너뛴다. tier_override="human-pinned" if pinned else None, )코드 설명. 발행 단계의 분기는 한 줄짜리 결정이다. 본문이 pin override에서 온 것이면 frontmatter(문서 맨 위 메타데이터 블록)의 tier를
human-pinned으로 바꿔 박는다. 왜 이 한 줄이 중요한가 — 별도의 검증 단계인 환각 검사기가 "tier가 llm-generated인 문서만 검사한다"는 규칙을 갖고 있기 때문이다. tier를 바꿔 박는 것만으로 사람이 쓴 글은 자동으로 그 검사 대상에서 빠진다. 새로운 예외 처리 코드를 환각 검사기에 추가할 필요가 없다 — 이미 있던 규칙이 새 tier 값을 자연스럽게 받아 준다. 작은 결정 하나가 검증 파이프라인 전체와 깔끔하게 맞물리는 자리다.5. 왜 이게 옳은가 — 사라진 복잡함과 명확해진 경계
충돌 처리 코드가 0줄이다. 출력 보존 방식에서 필요했던 지문 비교, 드리프트 큐, 사이드카 파일, 코드를 고칠 때마다의 충돌 해소 — 이 전부가 사라졌다. 합쳐야 할 두 버전이 애초에 생기지 않기 때문이다. 사람 입력과 기계 출력은 서로 다른 자리(입력 폴더 vs 출력 폴더)에 살고, 출력은 매번 입력에서 새로 만들어진다. 머지를 더 똑똑하게 만든 게 아니라 머지가 일어날 상황 자체를 없앤 것이다.
"코드가 ground truth"가 깨지지 않는다. steering note는 코드 기반 생성의 방향만 살짝 틀 뿐 여전히 LLM이 코드를 근거로 쓴다. pin override만이 코드를 우회하는데, 그조차
human-pinned이라는 태그로 "여기는 사람이 책임지는 명시적 예외"라고 못 박혀 있다. 검증되지 않은 산문이 슬그머니 섞이는 게 아니라, 누가 어디를 책임지는지가 메타데이터에 드러난다.책임 경계가 자기-문서화된다. 문서를 읽는 사람도, 검증 도구도 tier만 보면 안다 —
llm-generated면 기계가 코드에서 뽑은 것이라 환각 검사를 거친 것이고,human-pinned이면 사람이 직접 쓰고 직접 책임진 것이다. 예전 같으면 "이 문장은 사람이 고친 건가 기계가 쓴 건가"를 알 길이 없었는데, 이제 출처가 태그 한 줄로 노출된다.트레이드오프도 정직하게 적는다. 첫째, steering/pin은 다음 재생성 때 반영된다 — 즉각 반영이 아니다. 가이드를 적고 나서 결과를 보려면 생성을 한 번 돌려야 한다(수동으로 재생성을 트리거하거나, 그 repo 코드가 다음에 바뀌어 야간 파이프라인이 그 페이지를 재생성할 때까지 기다린다). 출력을 직접 고치는 것보다 피드백 루프가 한 박자 느리다. 둘째, pin override는 사람이 grounding 책임을 진다. 코드를 인용하면 정확한 앵커를 직접 달거나, 검증을 면제하려면 본문에 명시적 skip 마커를 넣어야 한다. 이걸 남용해 pin을 남발하면 검증 안 된 산문이 쌓일 수 있으므로, pin은 어디까지나 좁은 예외로 쓰는 규율이 필요하다. 다만 이 두 비용은 "코드를 고칠 때마다 충돌을 손으로 푸는" 비용에 비하면 비교가 안 되게 작다.
마무리 — 보존하지 말고, 보존할 필요가 없게 하라
사람과 기계가 함께 만드는 문서를 유지하는 문제는 흔히 "어떻게 두 버전을 잘 합칠까(merge)"로 접근한다. 나도 처음엔 그랬다 — 지문으로 사람 편집을 식별하고 비파괴적으로 보존하는, 꽤 정교한 설계까지 그렸다. 그런데 그 정교함 전체가 "사람이 출력을 고친다"는 전제 하나에 매달려 있었다. 전제를 버리니 설계도 통째로 증발했다.
진짜 해법은 보존을 잘하는 게 아니라 보존이 필요 없게 만드는 것이었다. 생성물이 매번 다시 만들어진다면, 사람의 의도는 생성물이 아니라 생성의 입력에 둬야 한다 — 그러면 몇 번을 재생성하든 의도는 매번 다시 반영되고, 합칠 두 버전은 생기지 않는다. 강물 하류에서 물을 퍼 올리지 말고, 상류의 수원에 색을 풀면 흐름을 따라 매번 색이 입혀지는 것과 같다. 출력을 고치고 싶을 때, 입력을 바꾼다. 코드가 진실의 원천인 시스템에서 사람이 말하는 올바른 방법은, 출력에 손대는 게 아니라 입력으로 말하는 것이다.
이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.
'IT' 카테고리의 다른 글
에이전트 루프의 실행력, 행동(Action) — 생각에서 변화로 나아가는 도구 호출 (0) 2026.06.07 에이전트 루프의 나침반, 계획(Planning) — 복잡함을 나눌 때 시작되는 문제 해결 (0) 2026.06.06 에이전트 루프의 첫 단추, 관찰(Observation) — 세상의 상태를 맥락으로 번역하기 (0) 2026.06.06 에이전트의 다섯 가지 본질 — 루프·도구·맥락·정지·신뢰 (0) 2026.06.06 내 코드 위키를 남의 코딩 에이전트가 쓰게 하려면 — SKILL.md 한 장으론 부족하다 (0) 2026.06.05 같은 사실도 신뢰 레벨이 다르다 — 코드 위키의 Trust Gradient (0) 2026.06.04 불필요한 껍데기 걸러내기 — vulture를 이용한 데드 코드 탐지 및 지식 정제 (0) 2026.06.04 코드의 어두운 구석을 드러내기 — radon을 통한 순환 복잡도 지표 수집 (0) 2026.06.04 LSP를 메모리 안으로 — jedi를 통한 초고속 함수 단위 CALLS 그래프 구축 (0) 2026.06.03 tree-sitter Stage A — 어려웠던 건 빠른 파싱이 아니라 캐시 무효화였다 (0) 2026.06.03