포스트

에러 로그만 던지지 말고 “실험 가능한 디버깅 파이프라인”을 만들자: 2026년 7월 LLM 기반 에러 분석 워크플로 심층 분석

에러 로그만 던지지 말고 “실험 가능한 디버깅 파이프라인”을 만들자: 2026년 7월 LLM 기반 에러 분석 워크플로 심층 분석

들어가며

프로덕션 장애/CI 실패/간헐적 테스트 플래키(flake) 같은 “재현은 어렵고, 로그는 많은” 문제에서 LLM을 그냥 채팅창에 붙여 넣으면 보통 2가지로 끝납니다. (1) 그럴듯한 추측(=hallucination) (2) 과도한 컨텍스트 덤프(=비용/시간 폭발). 2026년 흐름은 LLM을 “추론 엔진”이 아니라 “도구를 호출하며 증거를 수집하는 디버깅 에이전트”로 다루는 쪽으로 굳어지고 있습니다. Claude Code 같은 CLI 에이전트는 bash 실행/파일 편집/세션 기록을 1급 기능으로 두고, MCP(Model Context Protocol)로 외부 시스템(로그/모니터링/티켓)을 도구로 붙이는 패턴이 일반화됐습니다. (code.claude.com)

언제 쓰면 좋나

  • 실패 신호가 분명한데(root symptom) 원인 지점이 넓은 경우: “테스트 한 개가 깨짐”, “특정 API만 500”, “특정 배포 이후 latency spike”
  • 재현/검증 루프가 자동화 가능한 경우: pytest -k ..., npm test, docker compose up, canary 쿼리 등
  • 로그/트레이스/리포지토리/런북 등 “증거 소스”가 여러 군데 흩어져 있는 경우(LLM이 연결·요약·가설검증하기 좋음). Claude Code의 artifact가 이런 incident page 조합을 강조합니다. (claude.com)

언제 쓰면 안 되나

  • 안전/보안/컴플라이언스 상 코드/로그를 외부 모델에 보낼 수 없는 경우(또는 경계가 불명확한 경우)
  • 재현이 안 되는 상태에서 “수정부터” 시키는 경우: 에이전트가 편집은 잘하지만, 검증 루프가 없으면 잘못 고치는 속도가 더 빨라짐
  • IDE/에이전트 확장(브라우저/에디터 플러그인)에 권한을 과하게 주는 경우: 실제로 Claude for Chrome 확장 취약점 이슈가 2026년 7월에도 보도됐습니다. (techradar.com)

🔧 핵심 개념

1) “LLM 디버깅”의 단위는 Prompt가 아니라 Workflow

2026년의 실전은 “좋은 프롬프트”보다 (증거 수집 → 가설 생성 → 실험 실행 → diff 제출) 루프를 얼마나 자동화/추적 가능하게 만드느냐가 성패를 가릅니다. Anthropic은 Claude Code 세션 분석에서 사용이 단순 디버깅에서 더 end-to-end 에이전트 사용으로 이동했다고 보고합니다. (anthropic.com)

2) 내부 작동 흐름: Tool-use + 회귀 루프(Regression Loop)

현대 에이전트형 디버깅의 기본 실행 그래프는 대략 이렇습니다.

  1. Symptom intake: 실패 로그/스택트레이스/지표를 요약하고 “재현 커맨드”를 고정
  2. Repo scan: 관련 파일 탐색(검색/읽기), 최근 변경 추적
  3. Hypothesis set: 원인 후보 2~4개로 좁힘
  4. Experiment: bash로 테스트/재현 실행 → 결과를 tool result로 수집
  5. Patch: 최소 수정으로 diff 생성
  6. Verify: 동일 커맨드 재실행, 회귀 테스트 범위 확대
  7. Explain & handoff: 왜 이게 원인인지, 어떤 리스크가 남는지, 관측/알람까지 정리

여기서 중요한 건 도구 호출이 구조화되어야 한다는 점입니다. Claude의 tool use는 tool_use/tool_result 블록으로 왕복되고, OpenAI도 function calling/Responses API에서 JSON 스키마 기반의 tool 호출을 강조합니다. (platform.claude.com)
즉 “LLM이 글로 설명”하는 게 아니라, 툴 호출 인자/결과가 기록되어야 재현 가능하고, 추후 품질 분석(왜 실패했는지)도 가능합니다.

3) 관측 가능성(Observability)이 디버깅 성능을 좌우

에이전트 디버깅은 사람이 볼 때 “왜 저 툴을 지금 호출했지?”가 불투명해지기 쉽습니다. 그래서 2026년엔 trace 기반 디버깅이 급부상했고, LangSmith는 서로 다른 에이전트(Codex/Cursor/Claude Code)의 툴 호출을 공통 스키마로 매핑해 비교·리플레이·분석하는 접근을 전면에 내세웁니다. (langchain.com)
이건 단순 로깅이 아니라, 스팬(span) 트리로 “실행의 원인-결과”를 보는 것입니다(어떤 tool output이 어떤 편집으로 이어졌는지).

4) 다른 접근과의 차이점

  • 전통적 디버깅(인간 중심): 브레이크포인트/로컬 재현/직감. 강점은 정확한 맥락 이해, 약점은 탐색 비용.
  • LLM 단독 Q&A: 빠르지만 증거 기반이 약함. “가능한 원인 나열”에서 멈춤.
  • 에이전트형 LLM 디버깅(2026 스타일): 증거 수집과 실험을 자동화. 다만 권한/비용/안정성(루프, 과도한 탐색) 통제가 핵심 과제.
  • 연구 쪽에서도 “trace-driven” 디버깅(실행 트레이스 기반 원인 추정) 접근이 나오고 있습니다. (arxiv.org)

💻 실전 코드

아래는 “CI에서 가끔 터지는 Python 서비스의 테스트 실패를 LLM이 재현→원인 추적→패치→검증”까지 돌리는 현실형 예제입니다.

  • 로컬에서 실행 가능
  • 핵심은 “LLM에게 테스트를 직접 돌릴 수 있는 도구”를 주고, 결과를 구조화해 회귀 루프를 만들며,
  • 실행 전 과도한 권한을 주지 않도록 allowlist 명령만 허용합니다.

0) 의존성/환경

1
2
3
4
5
6
python -m venv .venv
source .venv/bin/activate
pip install "openai>=1.0.0" pydantic rich

# OpenAI 키
export OPENAI_API_KEY="..."

1) 디버깅 에이전트(도구 2개: run_tests, apply_patch)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
from __future__ import annotations

import os
import subprocess
from typing import Literal, Optional

from pydantic import BaseModel, Field
from openai import OpenAI

client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])


# ---- Tool schemas (function calling) ----
class RunTestsArgs(BaseModel):
    cmd: str = Field(
        description="Allowed test command. Must start with `pytest` and may include -k, -q, --maxfail, etc."
    )
    timeout_sec: int = Field(default=180, ge=10, le=900)

class ApplyPatchArgs(BaseModel):
    patch: str = Field(
        description="Unified diff patch to apply at repo root. Keep patch minimal."
    )

def run_tests(args: RunTestsArgs) -> dict:
    cmd = args.cmd.strip()
    if not cmd.startswith("pytest " ) and cmd != "pytest":
        return {"ok": False, "error": "Command not allowed. Only pytest is allowed."}

    try:
        p = subprocess.run(
            cmd,
            shell=True,
            capture_output=True,
            text=True,
            timeout=args.timeout_sec,
        )
        return {
            "ok": p.returncode == 0,
            "returncode": p.returncode,
            "stdout": p.stdout[-8000:],  # truncate
            "stderr": p.stderr[-8000:],
        }
    except subprocess.TimeoutExpired:
        return {"ok": False, "error": f"Timeout after {args.timeout_sec}s"}

def apply_patch(args: ApplyPatchArgs) -> dict:
    # NOTE: prod에서는 반드시 sandbox / code review gate 필요
    patch = args.patch
    p = subprocess.run(
        "git apply -",
        input=patch,
        text=True,
        shell=True,
        capture_output=True,
    )
    return {
        "ok": p.returncode == 0,
        "returncode": p.returncode,
        "stdout": p.stdout[-4000:],
        "stderr": p.stderr[-4000:],
    }


TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "run_tests",
            "description": "Run pytest to reproduce or verify a bug. Only allowlisted pytest commands.",
            "parameters": RunTestsArgs.model_json_schema(),
        },
    },
    {
        "type": "function",
        "function": {
            "name": "apply_patch",
            "description": "Apply a minimal code patch (unified diff) to fix the bug.",
            "parameters": ApplyPatchArgs.model_json_schema(),
        },
    },
]


SYSTEM = """You are a senior engineer debugging a real codebase.
Rules:
- Always reproduce with pytest first. No patch before reproduction unless trivial.
- Keep changes minimal and explain risk.
- After patch, rerun the same pytest command, then broaden to `pytest -q`.
- If evidence is insufficient, ask for one specific missing artifact (file path or command output).
Output: concise, technical.
"""

def tool_dispatch(name: str, arguments: dict) -> dict:
    if name == "run_tests":
        return run_tests(RunTestsArgs(**arguments))
    if name == "apply_patch":
        return apply_patch(ApplyPatchArgs(**arguments))
    return {"ok": False, "error": f"Unknown tool {name}"}


def debug_loop(initial_issue: str, max_steps: int = 10) -> str:
    messages = [
        {"role": "system", "content": SYSTEM},
        {"role": "user", "content": initial_issue},
    ]

    for _ in range(max_steps):
        resp = client.responses.create(
            model="gpt-4.1",   # 조직 표준 모델로 교체
            input=messages,
            tools=TOOLS,
            tool_choice="auto",
        )

        out_text = []
        tool_calls = []

        for item in resp.output:
            if item.type == "message":
                out_text.append(item.content[0].text)
            elif item.type == "tool_call":
                tool_calls.append(item)

        if tool_calls:
            # Execute tools and feed results back
            for tc in tool_calls:
                result = tool_dispatch(tc.name, tc.arguments)
                messages.append({
                    "role": "assistant",
                    "content": out_text[-1] if out_text else "",
                })
                messages.append({
                    "role": "tool",
                    "tool_call_id": tc.id,
                    "content": str(result),
                })
        else:
            return "\n".join(out_text).strip()

    return "Reached max steps without resolution."


if __name__ == "__main__":
    issue = """CI failure:
- intermittently failing test: tests/test_payments.py::test_refund_idempotency
- error snippet: AssertionError: expected status=200 got 500
- stack: KeyError: 'refund_id' in payments/refund.py:87
Reproduce locally and propose a minimal fix."""
    print(debug_loop(issue))

예상 출력(요지)

  • 1단계: run_testspytest -k refund_idempotency -q 실행해 재현
  • 2단계: 실패 스택/관련 파일 탐색을 요구(현재 예제는 “파일 읽기 도구”가 없으니, LLM이 특정 파일 내용을 요청하게 만들거나 Read tool을 추가해야 함)
  • 3단계: 최소 patch 생성 → apply_patch
  • 4단계: 동일 테스트 재실행 → pytest -q로 확장 검증
  • 5단계: “원인/재발 방지(입력 검증, 로그 추가, contract test)” 정리

이 예제의 포인트는 LLM이 디버깅의 ‘절차’를 강제받는다는 겁니다. Claude Code도 “증상만 말하면 조사/수정 제안” 워크플로를 전면에 두고, 세션을 JSONL로 남겨 되감기/재개가 가능하게 설계합니다. (support.claude.com)


⚡ 실전 팁 & 함정

Best Practice

1) 재현 커맨드를 ‘단일 소스 오브 트루스’로 고정

  • “고쳤다”의 정의가 흐려지면 에이전트가 끝없이 편집합니다.
  • 먼저 pytest -k ... 같은 최소 재현을 고정하고, 성공 후에만 범위를 넓히세요(동일 커맨드 재실행은 필수).

2) 툴 권한을 최소화하고, allowlist로 시작

  • bash를 열어주면 생산성은 올라가지만, 실수/비용/보안 리스크도 같이 커집니다.
  • 위 예제처럼 “pytest만 허용” 같은 좁은 도구부터 시작하고, 필요 시에만 ripgrep, cat(read-only), git diff 등으로 확장하세요.
  • 브라우저/확장 기반 도구는 취약점 이슈가 실제로 발생할 수 있으니, 민감 환경에선 특히 보수적으로. (techradar.com)

3) Trace/세션 기록 없이는 ‘원인 분석’이 아니라 ‘운 좋은 수정’이 된다

  • LangSmith 같은 관측(툴 호출/비용/지연/실패 패턴)을 붙이면 “왜 실패했는지”를 구조적으로 되짚을 수 있습니다. (langchain.com)
  • MCP를 쓴다면 MCP 로그/디버깅 가이드를 따라 tool boundary에서부터 확인하세요. (modelcontextprotocol.io)

흔한 함정/안티패턴

  • 로그 덤프 → 한 방에 고치기: 가장 흔한 실패. “읽기 도구/검색 도구/실행 도구”로 증거를 쪼개서 단계화해야 합니다.
  • 다중 에이전트 fan-out 남발: 병렬 탐색은 유용하지만 비용 폭발/중복 작업을 부릅니다. (커뮤니티에서도 워크플로/서브에이전트는 비용 이슈가 자주 언급됩니다. (reddit.com))
  • 컨텍스트 과다 주입: 성능이 아니라 “혼란”이 늘기도 합니다. Claude Code도 /compact, usage 가이드로 컨텍스트를 의식적으로 줄이라고 안내합니다. (claude.com)

비용/성능/안정성 트레이드오프

  • 성능(해결률)을 올리려면: 더 많은 tool, 더 큰 context, 더 많은 반복이 필요
  • 비용을 줄이려면: 재현 범위를 줄이고, diff를 최소화하고, trace로 병목(불필요 호출)을 제거
  • 안정성을 올리려면: (1) 실행 제한 (2) 세션 리플레이 (3) 실패 시 human gate(코드리뷰/승인 훅) 가 필요
    에이전트가 아직 만능이 아니라는 것도 중요합니다. 예를 들어 SWE-bench Mobile 같은 벤치마크에서는 최고 설정도 성공률이 낮게 보고됩니다(현실 과제의 난이도/환경 의존성을 시사). (arxiv.org)

🚀 마무리

2026년 7월 시점의 “LLM 디버깅”은 요약하면 ChatGPT에 로그 붙여넣기가 아니라, 재현 가능한 실험 루프 + tool-use + 관측(트레이싱)의 조합입니다. Claude Code가 bash/파일 편집/세션 기록을 기본으로 두고, MCP로 외부 시스템을 붙이며, LangSmith 같은 도구가 에이전트 실행을 trace로 표준화하려는 이유도 여기 있습니다. (code.claude.com)

도입 판단 기준(체크리스트)

  • (필수) “재현 커맨드”가 존재하고 CI/로컬에서 자동 실행 가능한가?
  • (필수) 에이전트가 접근할 데이터 경계(코드/로그/모니터링)가 정의되어 있는가?
  • (권장) tool 호출/패치/테스트 결과를 남길 trace/세션 로그가 있는가?
  • (권장) 권한(쓰기/실행)을 단계적으로 열 수 있는가? (read-only → test-run → patch)

다음 학습 추천

  • MCP 기반으로 “로그/모니터링(예: Sentry, Datadog) → incident context → 재현 → 패치”까지 연결하는 사내 Debug MCP server 만들기(공식 MCP 디버깅 가이드부터). (modelcontextprotocol.io)
  • 에이전트 실행을 LangSmith trace로 수집하고, “불필요한 tool call/루프/토큰 폭발”을 규칙 기반으로 잡아내는 evaluator를 붙이기. (langchain.com)

원하면, 당신 팀의 스택(언어/테스트 러너/CI, 관측 도구, 배포 방식)을 알려주면 위 예제를 (1) Read/Search 도구 추가, (2) MCP 연동, (3) PR 자동 생성 + human approval gate까지 포함한 “실제로 굴리는” 워크플로로 확장해 드릴게요.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.