ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 같은 사실도 신뢰 레벨이 다르다 — 코드 위키의 Trust Gradient
    IT 2026. 6. 4. 23:00
    같은 사실도 신뢰 레벨이 다르다 — 코드 위키의 Trust Gradient

    AI 에이전트에게 코드 위키를 들려보낼 때 가장 위험한 가정은 "위키에 적혀 있으면 다 같은 정도로 믿어도 된다"는 것이다. 같은 한 줄 — "함수 handle_chatacquire_gpu를 호출한다" — 도 어떻게 알아낸 사실이냐에 따라 신뢰 정도가 매우 다르다. 코드를 직접 파싱해 호출 구문을 본 것인가, LLM에게 코드를 보여 주고 "어떤 함수를 호출하는 것 같아?"라고 물어본 것인가. 둘 다 같은 위키 페이지에 같은 문장으로 적혀 있어도, 첫 번째는 99%, 두 번째는 70%일 수 있다.

    이 글은 그 차이를 없애는 대신 명시하는 방법의 기록이다 — 코드 분석 도구가 추출한 사실과 LLM이 해석한 사실을 같은 위키에 둘 다 두되, 어느 쪽인지 메타데이터로 표시하는 Trust Gradient.

    1. 코드만 주면 LLM이 추론한다

    단순한 문서화 워크플로우는 코드를 LLM에 넘기고 "이 함수가 무슨 일을 하는지 설명해"라고 묻는 것이다. 여기서 LLM은 두 가지를 섞어 처리한다.

    • 구조적 사실 — "어느 함수가 어느 함수를 호출하는가": 이건 코드에 있다. LLM이 텍스트를 읽어 추론하지만, 소스 파일을 파싱하면 확정할 수 있는 사실이다.
    • 해석적 의도 — "왜 이 설계인가, 어떤 트레이드오프가 있는가": 이건 코드에 없다. LLM만 할 수 있는 추론이다.

    문제는 LLM이 이 둘을 구분하지 않는다는 것이다. 구조적 사실도 텍스트에서 "추론"한다 — 대부분 옳지만, 복잡한 의존 관계나 동명이인 함수 앞에서 틀린다. 그 틀림이 위키에 그대로 적힌다.

    해법은 단순하다. LLM이 추론해야 하는 사실을 먼저 기계가 추출해 LLM에 넘기는 것이다. 그러면 LLM은 구조적 사실을 추론하는 데 인지 자원을 쓰지 않아도 된다. 이미 주어진 사실 위에서 해석과 서술만 하면 된다.

    2. 분석 도구의 출력은 두 갈래로 간다

    deep-wiki에서 사용하는 도구들은 코드에 대해 병렬로 실행되는 독립적인 분석기다. 단, 출력이 가는 곳이 두 갈래로 나뉜다 — LLM에 밀어넣는(push) 구조적 사실과, 노드 속성으로 적재해두고 에이전트가 물어볼 때만 꺼내는(pull) 메트릭.

    diagram

    LLM에 밀어넣는 구조적 사실:

    • tree-sitter — syntactic parse. bar(...)이라는 구문이 foo body 안에 있으면 호출로 기록. 빠르고 결정적, 대신 어느 bar인지(동명이인)는 모름.
    • jedi (LSP) — name resolution. 그 bar가 어느 파일의 어느 줄에 정의된 함수인지 식별. 동명이인 문제를 해소하고, 결과가 있을 때는 확정적이다. 동적 디스패치(getattr, decorator chain)는 못 따라가면 그냥 누락한다.

    노드 속성으로만 적재하는 메트릭:

    • radon — cyclomatic complexity 수치. LLM은 코드를 읽으면 복잡도를 직접 파악할 수 있어 입력으로 줄 이득이 없다. 노드에 complexity: 8, complexity_rank: "B"(A–F)로 붙여둔다.
    • vulture — dead code 의심 신호. false positive가 많아 LLM 입력에 넣으면 잘못된 서술을 유도한다. dead_code_suspect: true로 노드에만 표시해두고, 사람이나 리뷰 도구가 판단하게 둔다.

    tree-sitter와 jedi는 LLM이 혼자 읽으면 틀릴 수 있는 사실을 대신 확정해준다. radon과 vulture는 그 역할이 아니다 — LLM의 추론 부담을 줄이지 않고, 검증 기준도 되지 않는다. 그래서 LLM 입력에는 넣지 않고, 노드 속성으로만 적재한다.

    3. 밀어넣는 사실 vs 물어보는 메트릭

    여기서 핵심 구분이 생긴다. 구조적 사실은 위키를 쓸 때마다 LLM에 강제로 밀어넣는다 — LLM이 추론하면 틀리니까. 반면 메트릭은 밀어넣지 않는다. 대신 graph DB 노드 속성으로 쌓아두고, 에이전트가 필요할 때 MCP 도구로 직접 질의하게 한다.

    이를 위해 graph_metrics MCP 도구를 추가했다. 노드 속성을 필터로 조회한다 — min_complexity로 복잡도 임계, dead_code_only로 dead code 후보, repo로 범위 한정.

    graph_metrics(min_complexity=20, top_k=6)
    →  125 [F] family-chronicle: narrator.py::generate_event_narrative
        57 [F] family-chronicle: server.py::do_GET
        56 [F] gemma:            proxy.py::_run_agent_stream
        51 [F] family-chronicle: narrator.py::narrate_events
        ...
    

    에이전트가 "이 repo에서 리뷰 우선순위 높은 함수는?"이라고 물으면 이 도구가 답한다. 위키 서술 생성에는 끼어들지 않는다. 밀어넣기(push)와 물어보기(pull)의 분리가 이 설계의 핵심이다 — 틀리면 안 되는 사실은 밀어넣고, 신뢰가 낮거나 보조적인 신호는 꺼내 쓰게 둔다.

    vulture를 LLM 경로에서 빼야 하는 이유는 실제 데이터로 곧장 드러났다. 전체 repo에 돌리자 dead_code_suspect로 표시된 것들 중에:

    • conftest.py::isolate_cache — pytest fixture다. @pytest.fixture(autouse=True)로 자동 호출되지만 코드에 명시적 호출이 없어 vulture가 "미사용"으로 본다.
    • server.py::graph_query — MCP 도구다. @mcp.tool() 데코레이터로 런타임 등록되지만 정적 분석으로는 호출처가 안 보인다.

    둘 다 멀쩡히 쓰이는 함수인데 vulture가 오탐했다. 이 신호를 LLM 입력에 넣었다면 "이 함수는 사용되지 않는다"는 틀린 서술이 위키에 박혔을 것이다. 노드 속성으로만 두고 사람이 트리아지하는 자리에서는 — 데코레이터 기반 코드를 한 번 걸러 보는 출발점으로 — 오탐이 해롭지 않다. 같은 신호라도 어디에 두느냐가 해로움을 가른다.

    구현 디테일 하나가 이 "어디에 두느냐"를 단단하게 만든다. radon은 메트릭의 라인을 함수 def 줄로 보고해 tree-sitter가 기록한 노드 라인과 정확히 맞지만, vulture는 데코레이터가 있으면 데코레이터 줄을 보고한다(def보다 앞). 그래서 radon은 라인 정확 매칭, vulture는 "같은 이름 + 그 라인 이상의 가장 가까운 def"로 노드에 붙인다. 전체 21개 repo에 2.9초, 5,548개 함수에 complexity, 92개에 dead-code 후보가 적재됐다.

    4. 도구 결과 + 코드 → LLM 파이프라인

    위키 한 페이지를 쓸 때 LLM이 받는 것은 코드 텍스트만이 아니다. 앞서 추출한 구조적 사실이 함께 들어간다.

    [LLM에게 밀어넣는 것]
    - 코드 (함수 본문)
    - tree-sitter 추출 call graph (이 함수가 호출하는 구문 목록)
    - jedi 해소 결과 (각 호출이 가리키는 정의 파일:라인)
    # radon/vulture 메트릭은 여기 없다 — 노드 속성으로만 적재(3장)
    

    LLM은 이 사실들을 주어진 것으로 받고, 그 위에서 자연어 서술을 쓴다. "이 함수는 GPU 자원을 예약한 뒤 모델에 요청을 보내는 것 같다" 같은 문장은 call graph와 name resolution이 이미 확인해 준 구조 위에 의도 해석을 얹은 것이다.

    실제로 이 파이프라인을 처음 가동한 날, jedi name resolution이 전체 repo에서 19.4초 만에 3,095개 CALLS edge를 만들었다. 예상은 repo 당 1~2분, 전체 30~40분이었다. 100배 빠름의 이유는 두 가지다 — jedi가 in-process로 동작하고, tree-sitter가 미리 함수 목록을 graph DB에 깔아 두었기 때문에 jedi는 file walk 없이 "이 함수 line에서 reference만 찾아라"는 좁은 query만 하면 됐다. 한 도구의 결과가 다음 도구의 탐색 공간을 줄여 줬다.

    이 결과는 nightly pipeline에서 미리 계산되어 graph DB에 저장된다. 코딩 에이전트가 RAG로 위키를 조회할 때는 이미 쌓인 사실을 읽는 것이며, 질문마다 실시간으로 도구를 돌리는 게 아니다.

    5. 검증할 때도 도구 기록이 참조 데이터가 된다

    도구가 추출한 구조적 사실은 문서를 쓸 때만 쓰이지 않는다. 위키 검증의 참조 데이터가 된다. 단, 검증에는 두 층위가 있고 각각 다르게 동작한다.

    첫 번째 층위 — anchor 유효성는 순수하게 기계가 확인한다. LLM이 위키에 module_x.py:42 같은 file:line 앵커를 달면, 스크립트가 파일 존재·라인 번호·심볼 일치를 확인한다. LLM 없이 가능한 유일한 검증이다.

    두 번째 층위 — 자연어 주장 검증은 LLM이 해야 한다. "함수 foomodule_x.pybar를 호출한다"는 문장에서 structured claim을 추출해 call graph와 대조하는 것은 기계가 직접 파싱할 수 없다. 이 작업은 LLM이 담당한다. 단, 이 LLM에게는 AST/jedi 기록이 참조 데이터로 함께 주어진다. 검증 LLM이 묻는 건 하나다 — "이 prose 주장이 기록된 call graph와 일치하는가?"

    이게 "LLM이 쓰고 LLM이 다시 추론하는 것"과 다른 이유가 있다. 생성 단계에서 LLM은 코드에서 구조를 직접 추론하는 부담을 진다. 검증 단계에서 LLM은 이미 결정적으로 기록된 사실과 prose를 대조하는 비교만 한다. 참조 데이터가 명확할수록 LLM의 판단은 안정적이다 — 추론이 아니라 조회에 가까워지기 때문이다.

    반면 "이 함수가 GPU 자원을 예약하는 이유는 OOM을 방지하기 위해서다" 같은 의도 서술은 어느 도구도 검증 기준을 줄 수 없다. 코드에 없는 사실이다. 이 구분이 Trust Gradient의 실제 내용이다 — 도구 기록과 대조 가능한 주장과 그렇지 않은 주장.

    6. 신뢰 레벨 표시

    위키 페이지에서 어느 서술이 도구 기반이고 어느 서술이 LLM 해석인지를 frontmatter에 명시한다.

    ---
    repo: gemma
    node_type: function-graph
    source: tool          # tree-sitter + jedi 기반 구조 정보
    schema_version: 2
    ---
    
    ---
    repo: gemma
    node_type: summary
    source: llm           # 도구 결과를 입력으로 받은 LLM 서술
    schema_version: 2
    ---
    

    source 필드 하나가 그 페이지의 신뢰 좌표다. RAG가 청크를 가져갈 때 이 frontmatter를 함께 가져가, prompt 컨텍스트에 "이 정보는 도구 검증됨" 혹은 "이 정보는 LLM 해석이니 재확인 필요" 같은 지침을 함께 넣을 수 있다.

    한 페이지 안에서도 섹션별로 마커를 둘 수 있다 — <!-- source: tool -->, <!-- source: llm -->. 라우터 시그니처(도구 추출)와 그 핸들러의 의도 설명(LLM 해석)이 같은 api.md에 공존할 수 있고, 에이전트는 어느 쪽을 의심해야 할지 안다.

    7. 정리

    코드 위키를 LLM만으로 쓰는 게 위험한 이유는 구조적 사실(call graph, name resolution)을 LLM이 추론하게 되기 때문이다. 이 사실들은 도구가 더 잘 뽑는다 — 빠르고, 결정적이고, 틀리면 출력을 안 낸다. 그래서 tree-sitter·jedi의 결과는 위키를 쓸 때마다 LLM에 밀어넣는다(push).

    radon·vulture는 다르다. 복잡도와 dead code 신호는 LLM의 서술을 개선하지 않고 검증 기준도 되지 않으며, vulture는 데코레이터·동적 등록에 오탐한다. 그래서 LLM 입력에는 넣지 않고 graph DB 노드 속성으로만 적재해, 에이전트가 graph_metrics MCP 도구로 물어볼 때만 꺼낸다(pull). 밀어넣기와 물어보기의 분리 — 틀리면 안 되는 사실은 강제로, 보조적이고 오탐 가능한 신호는 요청 시에만.

    그리고 같은 도구 기록이 검증 참조 데이터가 된다 — anchor 유효성은 스크립트가, 자연어 주장 검증은 LLM이 담당하되, 그 LLM에게도 도구 기록이 함께 주어진다. 추론이 아니라 대조다.

    Trust Gradient는 복잡하지 않다. 도구가 추출한 사실은 검증 가능하고, LLM이 해석한 의도는 검증 불가하다. 이 경계 하나를 메타데이터로 명시하고, 신뢰가 다른 신호는 LLM에 밀어넣을지 꺼내 쓰게 둘지 자리를 가르면, 에이전트는 어디를 의심해야 할지 안다.


    이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.

Designed by Tistory.