자동화 워크플로우에 자체검토 기능을 붙인 이야기
자동화 워크플로우에 자체검토 기능을 붙인 이야기
ttapp의 자동화 기능을 개선하면서 겪은 과정을 기록해둔다.
배경
ttapp에는 노드 기반 자동화 기능이 있다. 사용자가 채팅으로 원하는 자동화를 설명하면 Claude가 워크플로우를 설계해주는 방식이다. 트리거, 스크립트, AI 노드, 조건 분기 같은 것들을 조합해서 "매일 오전 9시에 뉴스 크롤링 후 요약해서 푸시 알림"같은 워크플로우를 만들 수 있다.
처음 구현은 단순했다. Claude에게 워크플로우 JSON 스펙을 설명하고, 사용자 요청을 받으면 완성된 JSON을 출력하게 하는 방식. 그런데 이 방식에는 문제가 있었다.
문제: Claude가 만든 워크플로우의 오류
Claude가 워크플로우를 설계할 때 자주 발생하는 오류들이 있었다.
- edge 참조 오류: 존재하지 않는 노드 ID를 연결하는 경우
- outputKey 누락:
{{n3.summary}}를 참조하는데 n3 노드에outputKey: "summary"가 없는 경우 - trigger 개수 오류: trigger 노드가 0개이거나 2개인 경우
실행해보기 전까지는 오류를 알 수 없었고, 실행하면 그냥 실패했다.
구조를 바꾸기 전에: 토론부터
어떻게 개선할지 방향을 잡기 전에 먼저 트레이드오프를 따져봤다. 크게 4가지 방향이 있었다.
- A안: 시스템 프롬프트에 자체검토 규칙만 추가 (비용 제로, 하지만 Claude가 항상 지키진 않음)
- B안: 항상 2-pass (설계 pass + 검토 pass, 품질은 높지만 응답시간 2배)
- C안: JS 구조 검증만 (명백한 구조 오류는 잡지만 논리 오류는 포기)
- D안: B+C 조합, 조건부 2nd pass (오류 발견 시에만 추가 pass)
결론은 A + D안 조합으로, 단계적으로 구현하기로 했다.
구현 1: 시스템 프롬프트 체크리스트 (A안)
Claude에게 워크플로우 작성 후 반드시 스스로 점검하도록 지시를 추가했다.
## Self-Review Checklist:
After writing or modifying workflow.json, you MUST review it yourself:
- [ ] All edge from/to values reference actual node.id values
- [ ] All {{nodeId.customKey}} references match actual outputKey values
- [ ] Every node has required fields: id, type, name, config
- [ ] Exactly 1 trigger node exists
- [ ] If a node is referenced by custom key, that node has outputKey set
비용이 거의 없는 개선이다. Claude가 항상 완벽하게 지키진 않지만, 명백한 실수는 많이 줄었다.
구현 2: JS 구조 검증 레이어 (C안)
Claude 응답이 끝난 후 JS에서 워크플로우 파일을 직접 파싱해서 검증한다.
검증하는 항목:
- 모든 노드에 필수 필드(id, type, name, config) 존재 여부
- trigger 노드가 정확히 1개인지
- edge의 from/to가 실제 존재하는 node.id를 가리키는지
{{nodeId.customKey}}변수 참조가 실제 outputKey와 매칭되는지
function validateWorkflowJson(parsed) {
const errors = [];
const nodes = parsed.nodes || [];
const edges = parsed.edges || [];
const nodeIds = new Set(nodes.map(n => n.id).filter(Boolean));
const outputKeys = {};
// 필수 필드 검증
for (const node of nodes) {
if (!node.id) { errors.push('Node missing id'); continue; }
if (!node.type) errors.push(`Node "${node.id}" missing type`);
if (!node.name) errors.push(`Node "${node.id}" missing name`);
if (!node.config) errors.push(`Node "${node.id}" missing config`);
if (node.config?.outputKey) outputKeys[node.id] = node.config.outputKey;
}
// trigger 노드 개수
const triggerCount = nodes.filter(n => n.type === 'trigger').length;
if (nodes.length > 0 && triggerCount !== 1)
errors.push(`Expected 1 trigger node, found ${triggerCount}`);
// edge 참조 무결성
for (const edge of edges) {
if (edge.from && !nodeIds.has(edge.from))
errors.push(`Edge from "${edge.from}" not in nodes`);
if (edge.to && !nodeIds.has(edge.to))
errors.push(`Edge to "${edge.to}" not in nodes`);
}
// {{nodeId.key}} → outputKey 매칭
const BUILTINS = new Set(['trigger', 'loop', 'item', 'prev']);
const varRe = /\{\{([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]+)\}\}/g;
let m;
while ((m = varRe.exec(JSON.stringify(parsed))) !== null) {
const [, refId, key] = m;
if (BUILTINS.has(refId) || key === 'output') continue;
if (!nodeIds.has(refId)) {
errors.push(`Variable {{${refId}.${key}}} references unknown node`);
} else if (outputKeys[refId] !== key) {
errors.push(`Variable {{${refId}.${key}}} but outputKey is "${outputKeys[refId] || 'none'}"`);
}
}
return errors;
}
구현 3: 오류 발견 시 자동 수정 (D안)
검증에서 오류가 발견되면 Claude에게 수정을 요청한다.
[Auto-fix request] Found 2 structural errors:
1. Edge from "n3" not in nodes
2. Variable {{n4.summary}} but outputKey is "none"
Read workflow.json, fix all issues, and write the corrected version.
이 수정 요청은 별도의 Claude 호출(2nd pass)로 처리된다. 오류가 없으면 2nd pass는 건너뛴다. 수정 후 재검증해서 결과를 사용자에게 알려준다.
- 오류 없음 → 그냥 완료
- 오류 발견 후 수정 성공 →
✅ 2개 오류 수정됨 - 수정 후에도 남은 오류 →
⚠️ 1개 미해결 이슈 남음
부수적으로 수정한 것들
구현 과정에서 코드 리뷰를 통해 몇 가지 추가 문제를 발견했다.
파일 watcher interval 누수: 워크플로우 편집 중 파일 변경을 감지하는 폴링이 에러 발생 시 cleanup되지 않는 문제. finally 블록에서 항상 정리하도록 수정했다.
통합 설정 민감값 노출: Gmail, Slack 등 외부 서비스 연동 설정을 Claude에게 컨텍스트로 전달할 때 API 키나 패스워드가 포함될 수 있었다. Claude CLI는 Anthropic 서버와 통신하므로 민감한 정보는 마스킹 처리했다.
const SENSITIVE_KEY_RE = /api[_-]?key|secret|password|token|credential|auth/i;
const isSensitive = SENSITIVE_KEY_RE.test(key)
|| (typeof v === 'string' && v.length >= 40)
|| (typeof v === 'string' && /^(sk-|ghp_|Bearer )/.test(v));
return `${key}: ${isSensitive ? '[REDACTED]' : v}`;
2nd pass 오류 처리: 수정 요청 중 실패가 발생하면 전체 플로우가 에러로 빠지는 문제. try-catch를 분리해서 2nd pass 실패는 독립적으로 처리하고 사용자에게 안내 메시지를 보여주도록 했다.
결과
지금은 이런 흐름이다.
사용자 메시지
→ Claude가 workflow.json 작성 (최대 10턴)
→ JS 구조 검증
→ 오류 없음: 완료
→ 오류 있음: Claude에게 수정 요청 (최대 5턴)
→ 재검증 → 결과 표시
완벽하진 않다. 논리적 오류(조건 분기 방향이 반대거나, 잘못된 데이터 변환 등)는 JS 검증으로 잡을 수 없다. 하지만 가장 빈번하게 발생하던 구조적 오류들은 이제 자동으로 감지되고 수정된다.
다음 단계는 2~4주 운영해보면서 실제 오류 발생 빈도를 측정하는 것이다. 오류가 자주 나온다면 항상 2-pass로 바꾸는 것도 고려할 수 있다.
이 글 공유하기
// SPONSORED
[>]댓글
아직 댓글이 없어요. 첫 댓글을 남겨보세요!