핵심 한 줄

LLM에만 의존하면 스택트레이스가 명백한데도 “진단만 하고 멈추는” 일이 생긴다(실제 데모에서 겪음). 그래서 LLM을 못 믿고 결정론 레일을 깔았다 — SuspectLocator가 스택트레이스를 직접 파싱해 file:line을 특정해 프롬프트에 주입하고, LLM이 끝내 패치를 못 내면 결정론 carrier PR이 대신 나온다. 이 백스톱은 try/catch 밖이라 분석이 깨져도 살아남는다.

시리즈 — LLM을 장애대응 파이프에 안전하게 엮기


1. 배경 — LLM 기분에 좌우되는 보장

fix-proposal 파이프는 “스택트레이스에 의심 위치가 있으면 draft PR을 보장”하려 했다. 그런데 LLM에만 의존하니:

  • 스택트레이스가 명백히 있는데도 분석만 하고 패치를 안 만드는 경우가 생긴다.
  • 수정제안이 모델의 변덕에 좌우된다 — 보장이 보장이 아니게 된다. (실제로 데모에서 겪은 버그)

“의심 위치가 있으면 반드시 유용한 PR”을 만들려면, LLM 바깥에 결정론 레일이 필요했다.


2. 결정

LLM을 신뢰 대상에서 빼고, 두 겹의 결정론 장치로 감쌌다.

  1. 결정론 특정SuspectLocator가 스택트레이스를 regex로 파싱해 앱 최상위 프레임의 file:line을 특정하고, GitHub git-tree 검색으로 실제 repo 경로를 해석한다.
  2. 결정론 백스톱 — 위치를 프롬프트에 주입해 read→propose를 유도하되, LLM이 끝내 유효 패치를 못 내면 TriageAgent.backstop이 TODO carrier diff로 PR을 보장한다.

3. 대안 비교

방식경로 특정패치 보장위험채택
LLM이 경로까지 추출LLMLLM경로 환각·누락, 보장 깨짐X
결정론 특정 + LLM 패치코드(regex+tree)LLMLLM 못 내면 PR 0
결정론 특정 + LLM 패치 + 결정론 백스톱코드코드가 보장백스톱은 수정 아님(안내)O

4. 선택 이유

LLM이 “경로를 추출할 필요 자체를 없앴다.” 경로를 모델이 뽑게 하면 환각·누락 여지가 생기고, 보장이 다시 모델에 의존한다. 대신 결정론 코드가 file:line을 확정해 프롬프트에 주입하면, LLM은 “이 위치를 읽고 고쳐라”만 하면 된다 — read→propose 도달률이 올라간다. 그래도 LLM이 못 내면 백스톱이 받친다. 핵심 원칙은 “모델은 무엇을, 코드는 어떻게”의 연장이다.


5. 설계 상세

결정론 특정 — SuspectLocator

스택트레이스 프레임을 regex로 훑되, 라이브러리 프레임은 건너뛰고 우리 앱 최상위 프레임만 고른다.

// fixproposal/SuspectLocator.java
private static final Pattern FRAME =
    Pattern.compile("at ([\\w.$]+)\\(([\\w$]+\\.java):(\\d+)\\)");
// ...
while (m.find()) {
  String classFqcn = ...;                          // 프레임의 FQCN
  if (!classFqcn.startsWith(appPrefix)) continue;  // com.example. 아니면 skip (라이브러리 제외)
  String packageRelPath = packageDir + "/" + fileName;
  Optional<String> repoPath = github.findRepoPath(packageRelPath);  // GitHub tree로 실제 경로 확정
  if (repoPath.isPresent()) {
    return Optional.of(new Suspect(repoPath.get(), line, classFqcn));
  }
}
return Optional.empty();   // 스택 없음/앱 프레임 없음/경로 미해석 → 빈 값 (절대 throw 안 함)

appPrefix(기본 com.example.)로 우리 코드 프레임만 잡는 게 핵심 — NPE가 JDK 내부에서 터져도 의심 위치는 우리 앱 최상단이어야 한다. 경로 확정(findRepoPath)도 LLM이 아니라 GitHub tree 검색이 한다.

프롬프트 주입 → read → propose

특정된 위치를 프롬프트에 박아 모델이 경로를 추측할 필요를 없앤다.

Suspect location (from stack trace): <repoPath>:<line>
You MUST readFile this path, inspect the code, then call proposePatch
with a minimal single-file, single-hunk fix near line <line>.

결정론 백스톱 — try 밖, 조건부

LLM이 유효 패치를 못 내고(proposal.isEmpty()) suspect가 있으면, backstop이 TODO carrier diff를 만든다.

// analyzer/TriageAgent.java — try/catch 밖
Optional<FixProposal> proposal = patchTool.proposal();
if (proposal.isEmpty() && suspect.isPresent()) {
  proposal = backstop(suspect.get(), analysis);   // 의심 라인 위에 TODO 주석 1줄 추가
}
  • try 밖이라 분석 JSON이 깨져도(degraded) 백스톱은 평가된다 (이걸 안 지켜서 났던 버그).
  • 조건부 — suspect가 없으면 백스톱도 안 한다. 아무 데나 PR을 만들지 않는다.
  • 백스톱 PR은 “수정을 주장”하지 않는다. 의심 라인 위에 // TODO(oncall): suspected cause — ... 주석을 얹어 사람을 정확한 위치로 안내할 뿐.

근거: fixproposal/SuspectLocator.java, fixproposal/Suspect.java, github/GithubChangeService.findRepoPath, analyzer/TriageAgent.backstop.


6. 결과 / 트레이드오프

보장된 것

  • 스택트레이스에 앱 프레임이 있으면 위치 특정은 결정론이라 모델 변덕과 무관하다.
  • LLM이 패치를 못 내도, 최악의 경우 사람을 정확한 file:line으로 안내하는 PR이 반드시 나온다.
  • 이 경로는 try 밖이라 분석 출력이 깨져도 살아남는다.

감수한 한계

  • 백스톱 PR은 실제 수정이 아니라 TODO 주석 — 그 자체로 문제를 못 고친다.
  • “무조건 코드를 고치는 PR”은 결정론으로 불가능(진단 정확도 비대상)이라, “무조건 유용한 PR”로 목표를 낮췄다.

7. 교훈

LLM 출력을 결정론 레일로 감싸 보장 만들기

LLM은 “유용하면 좋은” 경로지, “반드시 일어나야 하는” 보장을 맡길 대상이 아니다. 보장이 필요한 부분(위치 특정, 최후의 PR)은 결정론 코드로 빼내고, LLM은 그 레일 위에서 “더 나은 수정”을 시도하게 둔다.

면접 한 마디: “LLM이 패치를 못 만들 때를 대비해 결정론 백스톱을 깔았습니다. 스택트레이스를 직접 파싱해 의심 파일:라인을 특정하고, 최악의 경우에도 사람을 정확한 위치로 안내하는 PR이 반드시 나옵니다. LLM 출력이 깨져도 이 경로는 try/catch 밖이라 살아남습니다.”


관련 글