포스트

LLM으로 “에러를 재현 → 원인 규명 → 수정”까지: 2026년형 AI 디버깅/에러 분석 워크플로 심층 가이드

LLM으로 “에러를 재현 → 원인 규명 → 수정”까지: 2026년형 AI 디버깅/에러 분석 워크플로 심층 가이드

들어가며

프로덕션에서 LLM을 붙인 시스템(코딩 에이전트, RAG, tool-using agent)이 깨질 때 가장 짜증나는 지점은 재현(repro)과 원인 규명(RCA)이 기존 디버깅 방식으로 잘 안 먹힌다는 겁니다. 로그는 길고(프롬프트/컨텍스트/툴 응답), 비결정적이며(temperature, tool timing), 실패는 “코드 예외”가 아니라 “의미적 오류(semantic failure)”로 나타나는 경우가 많습니다. 그래서 2026년 6월 기준 업계 흐름은 한 문장으로 정리됩니다:

“LLM 디버깅은 ‘대화 로그’가 아니라 trace(분산 추적) 기반의 사건 재구성이다.” (opentelemetry.io)

언제 쓰면 좋나

  • LLM/agent가 툴 호출, RAG, 멀티스텝 계획을 수행하고, “어디서 잘못됐는지”가 불분명할 때
  • 장애/오작동이 간헐적이거나, 입력은 같아도 실행 경로가 달라질 때
  • 비용/지연/품질을 함께 봐야 하는 운영 단계(“왜 토큰이 갑자기 2배?” 같은) (tmls.nyc)

언제 쓰면 안 되나

  • 단일 LLM 콜(질문→답변) 정도로 끝나고 실패가 “모델 품질” 문제로만 귀결되는 경우(이때는 trace보다 eval이 먼저)
  • 개인정보/비밀 프롬프트를 원문 그대로 수집할 수 없는 환경인데, 마스킹/샘플링/보관정책 없이 “일단 다 저장”하려는 경우(추적은 되지만 운영 리스크가 더 큼) (tmls.nyc)

🔧 핵심 개념

1) “Agent debugging”은 왜 전통적 디버깅과 다른가

전통 디버깅은 (a) 재현 가능한 입력, (b) 결정적 실행, (c) 스택트레이스/코어덤프가 핵심입니다. 반면 LLM/agent는:

  • 한 번의 요청이 여러 LLM call + 여러 tool call로 쪼개짐
  • 실패가 “exception”이 아니라 “잘못된 판단/계획/툴 인자”로 나타남
  • “정답”이 케이스마다 다르고, 동일 입력도 실행 경로가 흔들림

그래서 2026년 관점의 디버깅은 실행을 사건 트리(Trace/Span)로 구조화하고, 그 위에서 “결정적 이벤트”를 찾는 방식으로 이동 중입니다. (tmls.nyc)

2) Trace/Span으로 보는 디버깅 흐름(내부 작동 방식)

OpenTelemetry(OTel) 기준으로 보면:

  • Trace: 사용자 요청 1건(또는 agent run) 전체
  • Span: 그 안의 단계(예: plan, retrieve, call_tool:db, summarize, execute_shell)
  • Span에는 시간/에러/속성(attribute)이 붙고, 부모-자식 관계로 “실행 트리”가 재구성됩니다. “LLM call”도 span, “tool invocation”도 span으로 잡히면, 장애 분석에서 원인 후보를 구조적으로 좁힐 수 있습니다. (opentelemetry.io)

특히 2026년에는 “GenAI observability”가 LLM 요청/툴 호출/세션을 구조적으로 기록하고, span tree로 보는 방식이 널리 언급됩니다. (opentelemetry.io)

3) “관측(Observability)”과 “개선(Fix)”의 간극

요즘 커뮤니티에서 자주 나오는 불만이 “trace는 쌓이는데, 그래서 뭘 고치지?”입니다. 즉,

  • tracing은 증거(evidence) 수집
  • debugging workflow는 그 증거로 원인 분류 → 재현 → 수정 → 회귀 방지까지 가는 체계

연구 쪽에서도 agent failure trajectory를 분류하고 RCA를 돕는 프레임워크(에러 taxonomy, root-cause identification)를 제안합니다. (openreview.net)

4) OpenAI Agents SDK 관점: “tracing이 내장된 agent 런타임”

2026년 OpenAI Agents SDK는 툴 호출, handoff, guardrails, custom event까지 포함한 tracing을 강조합니다. 디버깅/에러 분석의 기본 재료를 SDK 레벨에서 남기는 방향입니다. (openai.github.io)


💻 실전 코드

아래는 “agent가 CI에서 pytest를 돌리고 실패하면, 실패 로그를 요약+원인 후보를 제시하고, 필요 시 패치를 제안”하는 현실적인 워크플로 예시입니다. 핵심은 (1) trace로 실행을 남기고 (2) 실패 시점(span)에 디버깅 정보를 구조화해 남기며 (3) LLM은 ‘RCA 보조자’로만 쓰는 것입니다.

0) 의존성/환경

  • Node.js 20+
  • OpenAI Agents SDK(JS)
  • (선택) OTel exporter는 환경에 맞게 붙이세요(예: OTLP → Tempo/Jaeger). 여기서는 “SDK tracing을 켠 상태에서 custom span/event를 남기는 패턴”에 집중합니다. (Agents SDK tracing 가이드 참고) (openai.github.io)
1
2
3
npm init -y
npm i @openai/agents execa zod
# (선택) OTEL exporter/collector는 팀 표준에 맞게 추가

1) 초기 셋업: “테스트 실행”을 tool로 만들고, 실패를 span 에러로 승격

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
import { execa } from "execa";
import { z } from "zod";
import {
  Agent,
  tool,
  getGlobalTraceProvider,
  // 실제 패키지/버전별 import는 문서 확인 필요.
} from "@openai/agents";

const PytestResult = z.object({
  ok: z.boolean(),
  exitCode: z.number(),
  stdout: z.string(),
  stderr: z.string(),
});

const runPytest = tool({
  name: "run_pytest",
  description: "Run pytest in repo and return structured output.",
  parameters: z.object({
    path: z.string().default("."),
    extraArgs: z.array(z.string()).default(["-q"]),
  }),
  execute: async ({ path, extraArgs }) => {
    try {
      const p = await execa("pytest", extraArgs, { cwd: path, all: true });
      return PytestResult.parse({
        ok: true,
        exitCode: p.exitCode ?? 0,
        stdout: p.stdout,
        stderr: p.stderr,
      });
    } catch (e: any) {
      // 실패도 “정상 결과”로 구조화해서 반환 (LLM이 읽기 쉽게)
      return PytestResult.parse({
        ok: false,
        exitCode: e.exitCode ?? 1,
        stdout: e.stdout ?? "",
        stderr: e.stderr ?? String(e),
      });
    }
  },
});

2) 기본 동작: 실패 로그를 “RCA 프롬프트”로 넣되, trace에 핵심 신호를 남긴다

포인트:

  • LLM에게 “고쳐라” 이전에 분류/가설/재현 단계를 먼저 요구
  • 그리고 그 결과(가설, 영향 범위, 다음 액션)를 custom event/span attribute로 trace에 남겨서, 나중에 “비슷한 장애”를 검색 가능하게 만듭니다(관측→학습 루프). Agents SDK는 LLM/tool/handoff 등을 포괄적으로 trace로 수집하는 방향을 제공합니다. (openai.github.io)
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
type RCA = {
  summary: string;
  suspected_root_causes: Array<{ label: string; evidence: string }>;
  next_actions: string[];
  risk: "low" | "medium" | "high";
};

const rcaAgent = new Agent({
  name: "rca_agent",
  instructions: `
You are a senior engineer. Given a failing pytest output, produce:
1) concise failure summary
2) 2-4 suspected root causes with concrete evidence from logs
3) next actions to validate (commands/files to inspect)
Do NOT invent stack traces or files that are not mentioned.
Return JSON only.`,
  tools: [runPytest],
});

export async function debugCiFailure(repoPath: string) {
  // (선택) custom span을 직접 만들고 싶으면 provider를 통해 처리
  const traceProvider = getGlobalTraceProvider(); // SDK tracing에 연결 ([openai.github.io](https://openai.github.io/openai-agents-js/guides/tracing?utm_source=openai))
  // 아래는 “개념 코드”: 실제 span API는 프로젝트 OTEL 구성에 따라 다릅니다.
  const tracer = traceProvider.getTracer("ci-debug");

  return await tracer.startActiveSpan("ci.debug.session", async (span: any) => {
    const test = await runPytest.execute({ path: repoPath, extraArgs: ["-q"] });

    span.setAttribute("ci.test.ok", test.ok);
    span.setAttribute("ci.test.exit_code", test.exitCode);

    if (test.ok) {
      span.addEvent("ci.test.passed");
      span.end();
      return { ok: true as const };
    }

    // 로그는 길 수 있으니 “상한”을 두고, 원문 전체는 별도 저장/마스킹 정책 적용 권장
    const tailStdout = test.stdout.slice(-6000);
    const tailStderr = test.stderr.slice(-6000);

    const prompt = {
      failing_command: `pytest -q`,
      stdout_tail: tailStdout,
      stderr_tail: tailStderr,
    };

    const res = await rcaAgent.run({
      input: JSON.stringify(prompt),
    });

    // res.output이 JSON string이라고 가정(에이전트 instruction으로 강제)
    const rca: RCA = JSON.parse(res.output);

    // “RCA 결과”를 trace에 구조화: 나중에 span 검색/집계가 가능해짐
    span.addEvent("rca.generated", {
      "rca.risk": rca.risk,
      "rca.summary": rca.summary,
      "rca.next_actions": JSON.stringify(rca.next_actions),
    });

    // 장애 triage에 특히 유용한 “분류 라벨”을 attribute로 승격
    span.setAttribute(
      "rca.labels",
      rca.suspected_root_causes.map((x) => x.label).join(",")
    );

    span.end();
    return { ok: false as const, rca };
  });
}

3) 확장: “trace → 재현 → 패치”까지 자동화할 때의 안전장치

여기서부터가 실무 포인트입니다. 패치 자동 적용을 하려면:

  • sandbox execution(격리 실행)
  • apply patch 같은 제한된 변경 방식
  • PR로만 반영(직접 main에 push 금지)

OpenAI Agents SDK는 안전한 실행/파일 편집 같은 primitive를 확장하는 방향을 공개적으로 언급합니다. (openai.com)

1
2
3
4
5
6
7
8
9
10
11
12
// (개념 예시) 실제로는 “패치 제안만” 생성하고, 적용은 사람이 승인하는 구조가 안전합니다.
const patchAgent = new Agent({
  name: "patch_agent",
  instructions: `
Given RCA and failing logs, propose a minimal patch plan.
Output:
- files_to_change
- patch_steps
- new/updated tests if needed
No code dump; explain diffs at high level.`,
  tools: [],
});

예상 출력(예시)

  • Trace에서 ci.debug.session span이 생성되고
  • 하위에 run_pytest tool span / LLM generation span이 붙으며
  • ci.test.ok=false, rca.labels="ImportError,VersionMismatch" 같은 attribute로 검색이 가능해집니다. (span tree로 보는 방식 자체가 GenAI observability에서 핵심 UX로 언급됩니다.) (opentelemetry.io)

⚡ 실전 팁 & 함정

Best Practice 1) “trace 설계”를 먼저 정하고 LLM은 그 다음

요즘 글/가이드에서 반복되는 메시지가: LLM call tracing만으로는 agent debugging이 부족하다는 것. “어떤 상태 전이(state transition)로 다음 단계로 갔는지”가 남아야 실제 RCA가 됩니다. (reddit.com)
실무적으로는 span 네이밍/속성 표준을 먼저 합의하세요:

  • agent.plan, agent.route, tool.call:<name>, rag.retrieve
  • decision.policy, decision.confidence, context.quality_score 같은 속성

Best Practice 2) “원문 프롬프트/응답”은 무조건 저장하지 말고, 단계별로 정책화

OTel에서도 GenAI 관련 속성이 커지고(JSON 덩어리), 플랫폼이 렌더링을 못해서 오히려 읽기 힘들다는 지적이 있습니다. (opentelemetry.io)
추천:

  • 기본은 메타데이터(토큰/비용/latency/라벨) 위주
  • 디버깅 세션에 한해 샘플링/마스킹 후 content logging
  • “stderr tail 6KB”처럼 상한을 둔 캡처

Best Practice 3) “에러 taxonomy(분류)”가 없으면 trace는 쌓이기만 한다

AgentDebug 류의 연구는 failure trajectory를 분류하고 root-cause를 식별하는 프레임워크를 제안합니다. (openreview.net)
팀 내부적으로라도 라벨을 고정하세요:

  • TOOL_ERROR(툴 자체 실패)
  • BAD_TOOL_ARGS(환각 인자)
  • RETRIEVAL_IRRELEVANT
  • PLAN_DRIFT
  • CONTEXT_INCOMPLETE(툴은 성공했지만 필요한 필드 누락) (reddit.com)

흔한 함정/안티패턴

  • “LLM에게 로그 던지고 고치라 하기”: 재현/검증 계획 없이 패치가 나오면 회귀가 늘어납니다.
  • correlation id 단절: 웹 요청 trace와 agent trace가 안 엮이면 장애 타임라인이 분리됩니다(OTel을 쓰는 이유가 반감).
  • 비용 폭발: “모든 span에 원문 프롬프트 저장 + 긴 retention”은 저장비/쿼리비가 운영을 잡아먹습니다. (trace는 길어지는 경향이 있어 비용/성능 트레이드오프가 큽니다.) (tmls.nyc)

🚀 마무리

2026년 6월 기준 “LLM 디버깅/에러 분석”의 중심은 LLM을 만능 디버거로 쓰는 게 아니라,
1) 실행을 Trace/Span으로 구조화하고(LLM call + tool call + decision을 같은 트리에서) (opentelemetry.io)
2) 실패를 분류(taxonomy) + 재현 절차로 떨어뜨린 뒤 (openreview.net)
3) LLM은 그 과정의 요약/가설/다음 액션 생성기로 제한하는 쪽이 가장 재현성과 유지보수성이 좋습니다.

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

  • 우리 시스템은 “한 요청당 여러 단계”인가? (yes면 tracing 우선)
  • 실패가 exception이 아니라 semantic인가? (yes면 RCA 라벨링/trace attribute 우선)
  • 개인정보/비밀 프롬프트 보관 정책이 있는가? (no면 content logging부터 하면 위험)
  • 장애 후 “같은 유형 재발”이 잦은가? (yes면 taxonomy + trace search가 ROI 큼)

다음 학습 추천

  • OpenTelemetry의 GenAI observability 관점(LLM call을 span tree로 다루는 방법) (opentelemetry.io)
  • OpenAI Agents SDK tracing 구조/확장 포인트(커스텀 이벤트/스팬 설계) (openai.github.io)
  • 에이전트 실패 분석 프레임워크(trajectory 기반 RCA) (openreview.net)
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.