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.sessionspan이 생성되고 - 하위에
run_pytesttool 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.retrievedecision.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_IRRELEVANTPLAN_DRIFTCONTEXT_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)