모델보다 중요한 것은 모델을 둘러싼 실행 구조다
RAG에서 MCP·Harness·Loop·Orchestration까지, 실제 업무를 맡길 수 있는 Agent Runtime을 설계하는 법
Agent를 오래 만들수록 이상한 장면을 자주 보게 된다. 모델은 분명 좋아졌는데, 서비스의 신뢰성은 그만큼 좋아지지 않는다. 짧은 답변은 놀라울 정도로 좋아졌지만 작업이 길어지면 이미 읽은 파일을 다시 찾고, 실패한 테스트를 놓치고, 같은 도구를 반복 호출하고, 때로는 “명령이 실행됐다”는 사실을 “업무가 끝났다”는 뜻으로 착각한다.
이 문제를 프롬프트 부족으로만 보면 해결이 어렵다. 실제 업무는 한 번의 생성이 아니라 상태가 변하는 과정이기 때문이다. 무엇을 알고 있는지, 무엇을 실행했는지, 환경이 무엇을 돌려줬는지, 그 결과가 성공 조건을 만족하는지, 실패했다면 무엇을 다음 시도에 남길지까지 하나의 실행 구조로 연결되어야 한다.
Agent Engineering의 중심은 “모델에게 어떻게 잘 말할 것인가”에서 “확률적인 모델을 어떻게 관찰 가능하고, 검증 가능하고, 중단·복구 가능한 업무 시스템으로 만들 것인가”로 이동했다.
이 글의 목적은 Agent 관련 용어를 많이 소개하는 것이 아니다. Prompt, RAG, Context Engineering, LangGraph, MCP, Skills, Hooks, Memory, Multi-Agent, Human-in-the-loop, Background Agent, Loop Engineering이 왜 필요해졌고 서로 어디에 놓이는지를 하나의 Runtime 관점으로 정리하는 것이다. 이 관점을 잡으면 특정 프레임워크가 바뀌어도 설계 원리는 남는다.
이 글의 핵심을 한 문장으로 줄이면 다음과 같다.
모델은 다음 행동을 제안하는 추론 엔진이고, 실제 업무의 신뢰성은 Context·State·Tool·Permission·Verifier·Retry·Handoff·Trace를 묶는 Runtime이 만든다.
1. 기술의 역사는 모델이 아니라 제어 표면이 넓어진 역사다
초기 LLM 애플리케이션에서는 한 번의 호출이 거의 전부였다. 결과가 나쁘면 Role을 바꾸고, Few-shot 예시를 넣고, 출력 형식을 더 자세히 적었다. 이 시기의 핵심 질문은 단순했다.
Prompt Engineering(프롬프트 엔지니어링) — “이번 호출에서 모델에게 어떻게 말할 것인가?”
RAG(Retrieval-Augmented Generation, 검색 증강 생성)가 본격화되면서 질문이 달라졌다. 모델이 사내 문서나 최신 정보를 원래 학습하지 않았더라도, 호출 직전에 필요한 자료를 검색해 넣을 수 있게 됐다. 이제 품질은 프롬프트뿐 아니라 어떤 자료를 골라 Context에 넣었는지에 좌우됐다.
Context Engineering(컨텍스트 엔지니어링) — “이번 판단에 무엇을 보여줄 것인가?”
Tool Calling이 붙으면서 모델은 답변을 만드는 존재에서 환경을 바꾸는 존재가 됐다. 파일을 읽고, 웹을 검색하고, 코드를 실행하고, API를 호출하고, 데이터베이스를 조회할 수 있게 됐다. 이때부터 모델 주위에 행동 규칙과 권한, 재사용 절차, 실행 격리가 필요해졌다.
Harness Engineering(하네스 엔지니어링) — “모델이 어떤 규칙과 도구와 권한을 갖고 일하게 할 것인가?”
그리고 긴 작업에서는 한 번의 Tool Call로 끝나지 않는다. Tool 결과를 다시 읽고, 실패를 다음 상태에 넣고, 재시도하거나 사람에게 넘기고, 성공 증거가 있을 때만 멈춰야 한다.
Loop Engineering(루프 엔지니어링) — “행동 결과를 어떻게 관찰·검증·갱신하고, 언제 계속하거나 멈출 것인가?”
마지막으로 사람이 매번 호출하지 않아도 Event, Queue, Cron, Schedule에 따라 일이 시작되면 시간축을 관리하는 계층이 필요하다.
Orchestration(오케스트레이션) — “어떤 일을 언제, 어떤 Worker에게, 어떤 우선순위와 동시성으로 맡길 것인가?”
Prompt어떻게 말할까
→
Context무엇을 보여줄까
→
Harness어떻게 일하게 할까
→
Loop어떻게 검증·복구할까
→
Orchestration언제 누구에게 맡길까
여기서 중요한 것은 새 단어가 이전 기술을 없애지 않았다는 점이다. 실제 자료의 흐름도 문서 파싱과 VectorStore에서 Agentic RAG, Tool Calling, Memory, Human-in-the-loop, MCP, Multi-Agent, Harness로 넓어졌다. 하지만 문서 파싱, 평가, 추적, 배포 같은 기반 문제는 계속 남았다. Agent는 RAG를 대체하지 않고, MCP는 API 설계를 대체하지 않으며, Harness는 좋은 모델을 대체하지 않는다. 모두 더 큰 Runtime 안에서 서로 다른 실패를 해결한다.
2. 하나의 Agent Runtime으로 보면 용어가 단순해진다
제품마다 이름이 다르다. LangGraph에는 State·Node·Edge가 있고, Claude Code에는 CLAUDE.md·Skills·Hooks·Subagents가 있고, Deep Agents에는 Filesystem·Planning·Subagents가 있고, MCP에는 Tools·Resources가 있다. 기능 목록으로 외우면 금방 복잡해진다.
대신 “누가 무엇을 소유하는가”로 보면 대부분 여섯 층으로 정리된다.
Model모호한 입력을 해석하고 다음 행동 후보를 만든다. 지능의 중심이지만 시스템 전체는 아니다.
Context / State이번 판단에 필요한 Working Set과 현재 작업 상태를 구성한다.
HarnessRules, Skills, Tools, Sandbox, Hooks로 행동 방식과 권한을 규정한다.
LoopObservation을 Verifier에 연결하고 Retry·Handoff·Stop을 결정한다.
Orchestrator여러 Task와 Worker의 Queue, Schedule, 동시성, 우선순위를 관리한다.
GovernanceApproval, Audit, Version, Eval, Rollback으로 자동화의 경계를 통제한다.
이 구조에서 모델은 “무엇을 할지 생각하는 부분”에 가깝다. 하지만 실제 서비스가 안정적으로 일하려면 무엇을 기억할지, 무엇을 실행할 수 있는지, 무엇을 성공으로 인정할지, 어디서 멈출지를 다른 계층이 책임져야 한다.
그래서 Agent Architecture를 결정할 때 “최고 모델이 무엇인가?”만 묻는 것은 부족하다. 더 중요한 질문은 우리 팀이 State, Permission, Trace, Eval, Termination 중 어디까지 직접 소유할 것인가다.
3. Workflow와 Agent를 먼저 구분해야 한다
Agent의 자율성은 기능이 아니라 비용이다. 자율성이 늘면 예외 대응 능력이 좋아질 수 있지만 Tool Call 수, Latency(지연시간), Token 비용, Side Effect(외부 상태 변경), Debugging 난이도도 함께 늘어난다.
따라서 가장 먼저 해야 할 일은 “이 문제에 정말 Agent가 필요한가?”를 묻는 것이다.
WorkflowAgent
| 경로 |
코드가 미리 정의한다. |
실행 중 모델이 다음 행동을 고른다. |
| 잘 맞는 문제 |
정형·반복·규칙 기반 |
탐색형·개방형·예외가 많은 문제 |
| 장점 |
예측 가능, 테스트 쉬움, 저비용 |
환경 피드백을 보고 유연하게 복구 |
| 주요 위험 |
새로운 예외에 약함 |
비용·지연·오류 전파가 커질 수 있음 |
| 운영 핵심 |
분기, 에러 처리, 회귀 테스트 |
Verifier, Budget, Sandbox, Trace, HITL |
실전에서는 둘을 섞는 경우가 많다. “장애 원인을 찾아라”는 개방형 문제이므로 모델이 조사 순서를 선택할 가치가 있다. 그러나 “Production DB 변경 전에는 승인받아라”, “테스트가 실패하면 Merge하지 마라” 같은 규칙을 모델이 매번 판단하게 할 이유는 없다.
모호함은 모델에게, 불변 규칙은 코드에게.
좋은 Agent Runtime은 모든 것을 LLM에게 넘기는 구조가 아니라, LLM이 잘하는 열린 판단과 코드가 잘하는 결정론적 제어를 의도적으로 분리한 구조다.
복잡도는 보통 다음 순서로 올리는 편이 낫다.
Single Call한 번 생성
→
Tool외부 기능 추가
→
Workflow고정된 제어 흐름
→
Eval + Retry검증 후 재시도
→
Bounded Agent제한된 자율 Loop
4. Context는 저장소가 아니라 지금 추론하는 Working Set이다
긴 Agent 작업에서 가장 먼저 무너지는 것은 추론 능력보다 Context인 경우가 많다. 처음에는 사용자 목표와 관련 파일 몇 개뿐이지만, 시간이 지나면 검색 결과, Shell 출력, 과거 실패, 테스트 로그, Tool 정의, Memory, Subagent 결과가 계속 붙는다.
“Context Window가 크면 전부 넣으면 되지 않을까?”라는 생각이 자연스럽지만, 실제로는 그렇지 않다. Context는 장기 저장소가 아니라 다음 결정을 위해 지금 읽는 작업 메모리다. 오래된 정보와 중복 로그가 늘어날수록 중요한 신호의 밀도가 떨어진다.
Always-on과 On-demand를 분리한다
Always-on보안 규칙, 금지된 경로, 코드 Convention, 필수 테스트, Source of Truth 위치처럼 항상 필요한 계약
On-demandDB Migration Runbook, 특정 Framework 사용법, PDF 처리 절차, 특정 고객 정책처럼 상황이 맞을 때만 필요한 지식
이 패턴을 Progressive Disclosure(점진적 공개)라고 이해하면 쉽다. “이런 Skill이 존재한다”는 짧은 설명은 알려주되, 전체 절차와 Reference는 실제로 필요할 때만 Context에 넣는다.
Context Engineering은 네 동작으로 생각하면 된다
Write — 나중에도 필요한 Plan·Decision·Evidence를 외부 State나 파일로 내린다.
Select — 이번 Step에 필요한 정보만 다시 선택한다.
Compress — 긴 History와 Tool Output을 Summary·Checkpoint로 압축한다.
Isolate — 독립 작업을 별도 Context의 Subagent/Worker로 분리한다.
이 네 가지는 “Context를 많이 확보하는 기술”이 아니라 다음 판단에 필요한 정보 밀도를 높이는 기술이다.
5. RAG의 품질은 Retriever보다 앞단에서 이미 결정되기 시작한다
업로드된 2024년 Parser 자료와 실제 코드가 보여주는 중요한 교훈이 있다. PDF를 “긴 문자열 하나”로 보는 순간 RAG는 이미 정보를 잃을 수 있다.
실제 문서는 Paragraph(문단), Heading(제목), Table(표), Figure(도형/이미지), Chart(차트), Caption(설명), Header/Footer(머리말/꼬리말)가 섞여 있다. 샘플로 포함된 21페이지 시장 보고서도 한 페이지에 시장 코멘터리, 가격표, 시계열 그래프, 지역별 구분이 함께 들어간다. 이런 문서를 텍스트만 일렬로 이어 붙이면 표의 행·열 관계, 차트의 의미, 페이지 출처, 요소의 역할이 사라질 수 있다.
따라서 실전 문서 파이프라인은 다음에 가깝다.
Parse요소 단위 추출
→
NormalizeText·Table·Image 정규화
→
Provenancepage·source·element id
→
IndexChunk·Metadata
→
Retrieve검색·재정렬
→
Context Pack이번 질문용 Working Set
업로드된 export.py도 이 구조를 잘 보여준다. Figure·Chart·Table은 이미지 Artifact로 저장하고, HTML에는 Base64 이미지를 삽입하며, Markdown에서는 이미지 경로와 Table Markdown을 남기고, Table은 별도 CSV로도 변환한다. 핵심은 “한 가지 출력 포맷”이 아니라 원본 구조를 여러 검증 가능한 Artifact로 보존한다는 점이다.
현업에서는 Element Schema부터 명확하게 잡는다
문서 파싱 결과를 RAG와 Trace가 함께 쓸 수 있게 정규화하는 예시
from __future__ import annotations
from dataclasses import dataclass
from enum import StrEnum
from pathlib import Path
class ElementKind(StrEnum):
TEXT = "text"
TABLE = "table"
IMAGE = "image"
CHART = "chart"
@dataclass(frozen=True)
class DocumentElement:
# 어떤 문서의 어떤 요소인지 추적할 수 있어야 한다.
source_id: str
page: int
element_id: str
kind: ElementKind
# 검색에 사용할 의미 표현.
# 표라면 표의 제목/요약, 이미지라면 이미지 설명이 들어갈 수 있다.
text_for_retrieval: str
# 실제 답변에 사용할 상세 Artifact.
markdown: str | None = None
artifact_path: Path | None = None
@dataclass(frozen=True)
class ContextItem:
text: str
citation: str
def to_context_item(element: DocumentElement) -> ContextItem:
"""
Retriever가 찾은 결과를 LLM Context에 넣기 직전 변환한다.
중요한 점:
- 내용만 보내지 않고 provenance(출처 정보)를 함께 보낸다.
- source/page/element_id가 있어야 이후 Citation과 Eval이 가능하다.
"""
if element.kind == ElementKind.TABLE and element.markdown:
body = element.markdown
else:
body = element.text_for_retrieval
citation = (
f"{element.source_id}"
f"#page={element.page}"
f"&element={element.element_id}"
)
return ContextItem(
text=body,
citation=citation,
)
sample = DocumentElement(
source_id="market-report-2024-03-22",
page=1,
element_id="price-table-01",
kind=ElementKind.TABLE,
text_for_retrieval="지역별 제품 가격 범위를 정리한 표",
markdown="| region | low | high |\n|---|---:|---:|\n| A | 100 | 110 |",
)
item = to_context_item(sample)
print(item.text)
print(item.citation)
# 예상 결과
# | region | low | high |
# |---|---:|---:|
# | A | 100 | 110 |
#
# market-report-2024-03-22#page=1&element=price-table-01
이 코드는 Embedding 모델을 보여주려는 예제가 아니다. 더 본질적인 포인트는 검색 결과와 출처를 분리하지 않는 것이다. Citation을 답변 마지막에 억지로 붙이는 것이 아니라, Retrieval 단계부터 Source·Page·Element ID를 데이터 구조에 포함한다.
Dense, Sparse, Hybrid, Reranker는 서로 경쟁하는 기술이 아니다
방식왜 쓰는가주요 설계값
Dense Retrieval 의미 기반 검색 |
표현이 달라도 의미가 비슷한 문서를 찾는다. |
Embedding model, top-k, threshold |
Sparse / Keyword 키워드 검색 |
제품명, 코드, 법령 조항처럼 정확한 문자열이 중요할 때 강하다. |
Tokenizer, BM25 계열 점수 |
Hybrid 혼합 검색 |
의미와 정확 키워드를 함께 놓치지 않기 위해 결합한다. |
가중치, fusion 방식 |
Reranker 재정렬기 |
1차 검색 후보를 더 비싼 모델로 정밀 재평가한다. |
candidate 수, final top-k |
Parent-child 부모-자식 청크 |
작은 청크로 정확히 찾고, 큰 부모 Context로 의미를 복구한다. |
child size, parent boundary |
좋은 RAG는 “Vector DB를 썼다”가 아니라 어떤 종류의 정보 손실을 어떤 단계에서 막았는지 설명할 수 있는 시스템이다.
6. Memory, State, Skill을 같은 저장소로 생각하면 구조가 꼬인다
세 개는 모두 “기억하는 것”처럼 보이지만 저장하는 대상이 다르다.
개념질문예시
State 상태 |
지금 이 실행은 어디까지 왔는가? |
attempt=2, last_failed_check="reconciliation" |
Memory 기억 |
과거 실행이나 사용자에게서 무엇을 배웠는가? |
이 Repository는 Python 3.13을 사용한다. |
Skill 절차적 능력 |
이 종류의 일을 어떻게 반복 수행하는가? |
Migration 사전 점검 → dry-run → 검증 → 승인 |
State를 장기 Memory처럼 쌓으면 실행이 비대해지고, Memory에 매 Step의 로그를 저장하면 잡음이 쌓인다. 반대로 반복 절차를 Memory에 흩어놓으면 조직이 가진 방법론을 재사용하기 어렵다.
장기적으로 조직의 차별화가 쌓이는 곳은 모델 이름보다 Skill, Tool Contract, Eval Set, Runbook인 경우가 많다. 모델은 여러 회사가 비슷하게 사용할 수 있지만 “우리 조직은 이 상황에서 무엇을 확인하고 무엇을 금지하며 어떤 Evidence가 있어야 승인하는가”는 고유한 운영 지식이기 때문이다.
7. Harness는 모델 주변의 코드가 아니라 팀의 일하는 방식을 코드화한 것이다
Agent Harness(에이전트 하네스)는 모델이 장시간 실제 작업을 할 수 있도록 입력, 도구, 상태, 권한, 실행 환경, 검증을 모델 바깥에서 관리하는 계층이다.
구성 요소본질현업 예
| Rules / Instructions |
항상 지켜야 할 불변 계약 |
코딩 규칙, 금지 경로, 테스트 기준 |
| Skills |
필요할 때 불러오는 재사용 절차 |
incident-triage, release-review |
| Tools / MCP |
외부 세계에 대한 Capability |
GitHub, DB, Monitoring, Search |
| Hooks / Middleware |
Lifecycle에 삽입하는 결정론적 Gate |
권한 검사, Audit, Lint |
| Sandbox |
실패의 피해 반경 제한 |
Container, Worktree, Network 제한 |
| Subagents |
별도 Context를 가진 독립 Worker |
대규모 코드 조사, 병렬 리서치 |
좋은 Skill은 “전문가 역할”보다 실행 계약에 가깝다
“당신은 SRE 전문가입니다”는 역할을 설명하지만 작업을 운영 가능하게 만들지는 않는다. 실무 Skill에는 사용 조건, 입력, 작업 순서, 완료 조건, 실패 처리, 권한 경계가 있어야 한다.
Claude Code의 현재 SKILL.md 형식을 응용한 운영 점검 Skill 예시
---
name: incident-triage
description: 배포 직후 오류율이 증가했을 때 원인 후보와 재현 증거를 수집한다.
allowed-tools: Read Grep Glob Bash
---
# 사용 조건
배포 직후 error rate 또는 latency가 기준선을 벗어난 경우 사용한다.
# 절차
1. 이상 징후가 시작된 시각을 확정한다.
2. 같은 시간대의 배포/feature flag 변경을 확인한다.
3. 상위 error signature를 추출한다.
4. 최근 변경과 error signature의 연관성을 조사한다.
5. 가능하면 staging에서 재현한다.
6. 원인 후보뿐 아니라 반증 근거도 함께 기록한다.
# 완료 조건
- 원인 후보와 evidence id가 연결되어 있다.
- 재현 명령 또는 재현 불가 이유가 남아 있다.
- production write가 필요하면 직접 실행하지 않고 Human Review로 넘긴다.
여기서 중요한 것은 문서 형식이 아니라 절차를 버전 관리 가능한 Runtime 자산으로 바꿨다는 것이다. 현재 Claude Code의 Skill은 본문을 실제 사용 시 로드하기 때문에 항상 필요한 CLAUDE.md와 조건부 절차를 분리하는 Progressive Disclosure에도 잘 맞는다.
Hook은 확률적인 지시를 결정론적인 Gate로 바꾼다
Prompt에 “테스트를 꼭 실행하세요”라고 적는 것은 모델이 따를 확률을 높인다. Tool 실행 뒤 Hook에서 테스트를 실행하도록 만드는 것은 특정 시점에 코드가 반드시 실행되도록 한다.
PreToolUse — Tool 실행 전에 인자·권한·Scope를 검사한다.
PostToolUse — Tool 실행 결과를 정규화하거나 Audit Log를 남긴다.
PreCompact — Context 압축 전에 중요한 Decision·Evidence를 외부 State로 보존한다.
Guardrail은 위험한 행동과 가까운 곳에 둔다.
“DB를 함부로 수정하지 마”를 상위 Agent Prompt 한 줄에만 두는 것보다 실제 DB Write Tool 직전에 Tenant, Row 범위, 승인 토큰을 검사하는 편이 강하다.
8. Tool은 기능이 아니라 Capability + Permission + Context Contract다
Tool을 단순히 “모델이 호출하는 함수”라고 보면 설계가 넓어지기 쉽다. 실무에서 Tool은 세 가지 계약을 동시에 가진다.
Capability — 무엇을 할 수 있는가.
Permission — 어디까지 할 수 있는가.
Context Contract — 입력과 결과를 모델에게 어떤 구조로 보여주는가.
예를 들어 run_database_command(sql: str)는 구현하기 쉽지만 Capability가 너무 넓다. 모델에게 읽기, 쓰기, 삭제, 임의 조인까지 한 번에 넘기기 때문이다.
정산 시스템이라면 다음처럼 나누는 편이 낫다.
read_settlement_summary읽기 전용. Batch ID와 제한된 요약만 반환.
preview_repair실제 Write 없이 변경될 Row와 Diff를 미리 계산.
apply_approved_repair승인 토큰이 있는 경우에만 제한된 Write.
verify_repairWrite 후 다시 읽어 Business Invariant를 검증.
MCP는 이 경계를 표준화하는 프로토콜이다
MCP(Model Context Protocol, 모델 컨텍스트 프로토콜)의 본질은 Agent를 더 똑똑하게 만드는 것이 아니라 Tool·Resource와 Agent 사이의 연결 계약을 표준화하는 데 있다.
2026년 현재 MCP Python SDK v2의 고수준 서버는 MCPServer이며, @mcp.tool()에 붙인 함수 이름·Docstring·Type Hint에서 Tool 설명과 입력 Schema를 만든다. 따라서 Tool 함수 자체가 Agent Interface가 된다.
MCP Python SDK v2 — 읽기 전용 업무 Tool 예시
from __future__ import annotations
from typing import Annotated
from mcp.server import MCPServer
from pydantic import Field
mcp = MCPServer("billing-readonly")
# 실제 서비스에서는 Repository/DB Adapter 뒤에서 조회한다.
SETTLEMENTS = {
"batch-2026-09-11": {
"delta_amount": 124_300,
"failed_records": 17,
"currency": "KRW",
}
}
@mcp.tool()
def read_settlement_summary(
batch_id: str,
limit: Annotated[int, Field(ge=1, le=100)] = 20,
) -> dict:
"""
정산 Batch의 읽기 전용 요약을 반환한다.
이 Tool은 Production 데이터를 수정하지 않는다.
batch_id는 immutable id를 사용하고,
상세 레코드는 limit을 넘겨 반환하지 않는다.
"""
row = SETTLEMENTS.get(batch_id)
if row is None:
# 모델이 다른 batch_id로 복구할 수 있도록 구조화된 실패를 반환한다.
return {
"ok": False,
"error_kind": "BatchNotFound",
"batch_id": batch_id,
}
return {
"ok": True,
"batch_id": batch_id,
"delta_amount": row["delta_amount"],
"failed_record_count": min(row["failed_records"], limit),
"currency": row["currency"],
"provenance": {
"source": "billing-ledger",
"mode": "read-only",
},
}
if __name__ == "__main__":
# 원격 Host가 연결하는 Stateless Streamable HTTP 서버.
mcp.run(
transport="streamable-http",
stateless_http=True,
json_response=True,
)
같은 MCP Tool을 in-memory Client로 테스트
import pytest
from mcp import Client
from server import mcp
@pytest.mark.anyio
async def test_read_settlement_summary():
# 실제 포트나 subprocess 없이 서버 객체에 직접 연결한다.
async with Client(mcp) as client:
result = await client.call_tool(
"read_settlement_summary",
{
"batch_id": "batch-2026-09-11",
"limit": 10,
},
)
assert result.is_error is False
assert result.structured_content["ok"] is True
assert result.structured_content["delta_amount"] == 124_300
assert result.structured_content["failed_record_count"] == 10
# 예상 결과
# $ pytest -q
# 1 passed
이 코드에서 중요한 Parameter는 Transport보다 batch_id와 limit다. 불변 ID는 잘못된 대상을 선택할 가능성을 줄이고, limit은 Context 폭주와 과도한 데이터 노출을 막는다. provenance는 다음 Trace와 Evidence를 연결한다.
MCP Server가 많아질수록 자동으로 Agent가 좋아지는 것도 아니다. 이름이 겹치는 Tool, 장황한 Description, 지나치게 긴 Tool Output은 오히려 선택 오류와 Context 비용을 만든다. Tool도 Eval 대상이어야 한다.
9. Loop의 최소 단위는 while문이 아니라 Runtime Contract다
Loop Engineering이라는 말을 들으면 다음 코드를 떠올리기 쉽다.
while not done:
response = model(...)
하지만 같은 State에서 같은 호출을 반복한다고 실패에서 배우는 것은 아니다. 진짜 Loop는 최소한 다음 계약을 가져야 한다.
Goal / State현재 어디인가
→
Policy다음 행동 선택
→
ActionTool·환경 실행
→
Observation실행 결과
→
Verifier성공 증거
↺
Update / Stop복구·승인·종료
계약반드시 답해야 할 질문
| Goal / Spec |
어떤 산출물이 생기면 완료인가? |
| State |
다음 Step까지 무엇을 보존할 것인가? |
| Policy |
다음 행동을 모델이 고를까, 코드가 고를까? |
| Observation |
Raw 결과 중 어떤 Signal을 되돌릴 것인가? |
| Verifier |
성공을 어떤 외부 증거로 판정할 것인가? |
| Budget |
Step·시간·비용의 상한은 어디인가? |
| Handoff |
언제 사람의 판단이 필요한가? |
| Termination |
success / blocked / timeout / budget / policy denied를 어떻게 구분할까? |
Action Success와 Task Success를 반드시 분리한다.
git apply가 성공했다는 것은 Patch가 적용됐다는 뜻이지 버그가 고쳐졌다는 뜻이 아니다. API가 200을 반환했다는 것은 요청이 처리됐다는 뜻이지 업무의 불변식이 회복됐다는 뜻이 아니다.
10. Verifier-first: 모델의 “완료했습니다”는 증거가 아니다
Agent가 가장 위험하게 끝나는 방식은 자신의 문장을 종료 조건으로 쓰는 것이다.
assistant: 작업을 완료했습니다.
이 문장은 Claim(주장)이다. 시스템이 필요한 것은 Evidence(증거)다.
검증은 일반적으로 다음 순서가 안정적이다.
- Deterministic Verifier(결정론적 검증기) — Unit Test, Compiler, Schema, SQL invariant, Static Analysis
- Rule / Rubric — 변경 Scope, 필수 필드, 정책 규칙처럼 코드로 판정 가능한 항목
- LLM Grader — 설명 품질, 의미적 충실도처럼 기계적 판정이 어려운 부분의 보조 평가
- Human Review — 비가역적이거나 고위험·모호한 결정
Verifier는 모델과 같은 실패 모드를 공유하지 않게 만든다
Patch를 만든 모델에게 “이 Patch가 맞아?”라고 다시 묻는 것만으로는 충분하지 않다. 같은 모델은 같은 오해를 반복할 수 있다. Coding Agent라면 실제 테스트와 정적 분석, 실제 Diff 범위가 1차 검증이어야 한다.
현업형 Verifier — 모델과 독립적으로 테스트·Lint·변경 Scope를 검사
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
import subprocess
@dataclass(frozen=True)
class Check:
name: str
passed: bool
detail: str
@dataclass(frozen=True)
class Evidence:
passed: bool
checks: tuple[Check, ...]
def run_check(
name: str,
command: list[str],
cwd: Path,
timeout_s: int = 120,
) -> Check:
"""
모델의 자연어 판단이 아니라 실제 프로세스 Exit Code를 Evidence로 사용한다.
"""
completed = subprocess.run(
command,
cwd=cwd,
capture_output=True,
text=True,
timeout=timeout_s,
check=False,
)
# 전체 로그를 Context에 넣지 않는다.
# 실패 시 마지막 일부만 Trace에 남기고, 원본 로그는 Artifact Store에 보관하는 편이 좋다.
tail = (completed.stdout + completed.stderr)[-1200:]
return Check(
name=name,
passed=completed.returncode == 0,
detail=tail.strip(),
)
def changed_files(workspace: Path) -> list[str]:
completed = subprocess.run(
["git", "diff", "--name-only", "HEAD"],
cwd=workspace,
capture_output=True,
text=True,
check=True,
)
return [
line.strip()
for line in completed.stdout.splitlines()
if line.strip()
]
def verify_workspace(workspace: Path) -> Evidence:
checks = [
run_check(
"unit_test",
["uv", "run", "pytest", "-q", "tests/billing"],
workspace,
),
run_check(
"lint",
["uv", "run", "ruff", "check", "src/billing", "tests/billing"],
workspace,
),
]
allowed_prefixes = ("src/billing/", "tests/billing/")
scope_ok = all(
path.startswith(allowed_prefixes)
for path in changed_files(workspace)
)
checks.append(
Check(
name="scope",
passed=scope_ok,
detail="변경 파일이 billing 범위 안에 있는지 검사",
)
)
return Evidence(
passed=all(check.passed for check in checks),
checks=tuple(checks),
)
# 예상 결과 개념
# Evidence(
# passed=True,
# checks=(
# Check(name="unit_test", passed=True, ...),
# Check(name="lint", passed=True, ...),
# Check(name="scope", passed=True, ...),
# )
# )
이 함수는 Agent가 없어도 가치가 있다. 이것이 Verifier-first의 중요한 장점이다. 먼저 “끝났음을 증명하는 코드”를 만들고, 그 다음 그 증거를 만족시키도록 Agent를 붙인다.
11. LangGraph는 State와 Control Flow를 직접 소유하고 싶을 때 강하다
직접 작성한 while Loop가 커지면 State Transition이 여러 함수와 조건문에 흩어진다. LangGraph의 가치는 “그래프 그림을 그린다”보다 State와 제어 흐름을 Runtime의 명시적인 구성 요소로 만든다는 데 있다.
2026년 현재 공식 Graph API에서 핵심은 StateGraph, START/END, add_node, add_edge, add_conditional_edges, RetryPolicy, Command, Checkpointer, interrupt()다. 현재 문서는 HITL이 있는 실행에서는 Event Streaming을 통해 Interrupt와 State를 관찰하는 패턴도 명확하게 제공한다.
현업 예제: B2B 정산 장애 복구 Agent
새벽 정산 Batch 이후 내부 원장과 결제 대행사 정산 금액이 맞지 않는 상황을 가정하자. Agent는 원인 조사와 Patch 후보 생성은 할 수 있지만, Production 원장 보정은 사람 승인 없이는 실행할 수 없다.
코드를 쓰기 전에 계약부터 적는다.
Goal
정산 불일치의 원인을 찾고 안전한 수정안을 만든다.
Success Evidence
unit / integration test = PASS
reconciliation dry-run delta = 0
변경 파일 = billing 범위 내부
production 직접 write = 없음
Budget
수정 반복 최대 3회
외부 API의 transient error만 제한적으로 retry
Graph recursion limit 설정
Human Required
production 원장 보정
schema 변경
고객 환불/취소
허용 Scope 밖 변경
LangGraph — Retry, Verification, Interrupt, Resume를 하나의 State Machine으로 구성
from __future__ import annotations
from typing import Literal, TypedDict
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import END, START, StateGraph
from langgraph.types import Command, RetryPolicy, interrupt
class IncidentState(TypedDict, total=False):
incident_id: str
attempt: int
# 조사 결과
delta_amount: int
suspected_change: str
# Artifact
patch_ref: str
# Verifier Evidence
tests_passed: bool
reconciliation_delta: int
scope_allowed: bool
# Risk / Human Gate
production_repair_required: bool
status: str
def collect_evidence(state: IncidentState) -> dict:
"""
Monitoring/Git/Log Adapter를 호출하는 읽기 전용 Node라고 생각하면 된다.
외부 API 일시 장애는 RetryPolicy가 처리한다.
Raw 로그 전체 대신 다음 판단에 필요한 Signal만 State에 넣는다.
"""
return {
"delta_amount": 124_300,
"suspected_change": "src/billing/rounding.py@a31f9c2",
}
def propose_patch(state: IncidentState) -> dict:
"""
실제 환경에서는 Coding Model 또는 별도 Coding Worker가 실행된다.
자연어 답변이 아니라 patch_ref라는 Artifact를 남긴다.
다음 Verifier가 '같은 Patch'를 검사하기 위해서다.
"""
attempt = state.get("attempt", 0) + 1
return {
"attempt": attempt,
"patch_ref": (
f"worktree://{state['incident_id']}"
f"/patch-{attempt:03d}"
),
}
def verify_patch(state: IncidentState) -> dict:
"""
실제 서비스에서는 pytest, lint, type check,
정산 dry-run, git diff scope 검사 결과를 읽는다.
"""
return {
"tests_passed": True,
"reconciliation_delta": 0,
"scope_allowed": True,
# 과거 잘못 반영된 운영 원장을 보정해야 한다고 가정.
"production_repair_required": True,
}
def route_after_verify(
state: IncidentState,
) -> Literal["retry_patch", "approval", "done", "handoff"]:
verified = (
state["tests_passed"]
and state["reconciliation_delta"] == 0
and state["scope_allowed"]
)
# 검증 실패는 같은 상태로 무한 반복하지 않는다.
if not verified:
return "retry_patch" if state["attempt"] < 3 else "handoff"
if state["production_repair_required"]:
return "approval"
return "done"
def approval_node(
state: IncidentState,
) -> Command[Literal["apply_repair", "handoff"]]:
"""
interrupt()에서 Graph가 멈추고 외부 입력을 기다린다.
매우 중요:
이 Node는 Resume 때 처음부터 다시 실행될 수 있으므로
interrupt() 앞에 비가역 Side Effect를 두지 않는다.
"""
decision = interrupt(
{
"type": "production_repair",
"incident_id": state["incident_id"],
"patch_ref": state["patch_ref"],
"question": "운영 원장 보정을 승인하시겠습니까?",
}
)
return Command(
goto=(
"apply_repair"
if decision.get("approved")
else "handoff"
)
)
def apply_repair(state: IncidentState) -> dict:
"""
예제에서는 실제 DB Write를 하지 않는다.
현업 구현 시 이 Node에는 최소한:
- idempotency key
- tenant / row scope
- approval audit
- write 후 read-back verification
을 둔다.
"""
return {"status": "repair_applied"}
def done(state: IncidentState) -> dict:
return {"status": "resolved"}
def handoff(state: IncidentState) -> dict:
return {"status": "human_required"}
builder = StateGraph(IncidentState)
builder.add_node(
"collect_evidence",
collect_evidence,
retry_policy=RetryPolicy(
max_attempts=3,
retry_on=ConnectionError,
),
timeout=10.0,
)
builder.add_node("propose_patch", propose_patch)
builder.add_node("verify_patch", verify_patch)
builder.add_node("approval", approval_node)
builder.add_node("apply_repair", apply_repair)
builder.add_node("done", done)
builder.add_node("handoff", handoff)
builder.add_edge(START, "collect_evidence")
builder.add_edge("collect_evidence", "propose_patch")
builder.add_edge("propose_patch", "verify_patch")
builder.add_conditional_edges(
"verify_patch",
route_after_verify,
{
"retry_patch": "propose_patch",
"approval": "approval",
"done": "done",
"handoff": "handoff",
},
)
builder.add_edge("apply_repair", "done")
builder.add_edge("done", END)
builder.add_edge("handoff", END)
# 개발 예제는 InMemorySaver.
# Production에서는 DB-backed durable checkpointer를 사용한다.
graph = builder.compile(
checkpointer=InMemorySaver()
)
config = {
"configurable": {
# 같은 업무를 재개하는 Persistent Cursor.
"thread_id": "incident-INC-482",
},
# 종료 조건 버그가 비용 폭주로 이어지는 것을 막는 최후의 상한.
"recursion_limit": 20,
}
# 1) 첫 실행: approval_node의 interrupt()에서 멈춘다.
stream = graph.stream_events(
{
"incident_id": "INC-482",
"attempt": 0,
"status": "running",
},
config=config,
version="v3",
)
# 실행을 끝(또는 pause)까지 진행시킨다.
_ = stream.output
if stream.interrupted:
request = stream.interrupts[0].value
print("human_review:", request)
# 2) 실제로는 Slack/GitHub/Admin UI의 승인 결과가 들어온다.
resumed = graph.stream_events(
Command(
resume={
"approved": True,
"reviewer": "billing-oncall",
}
),
config=config,
version="v3",
)
final = resumed.output
else:
final = stream.output
print("status:", final["status"])
print("patch:", final["patch_ref"])
print("attempt:", final["attempt"])
print("delta:", final["reconciliation_delta"])
# 예상 결과 개념
#
# human_review: {
# 'type': 'production_repair',
# 'incident_id': 'INC-482',
# 'patch_ref': 'worktree://INC-482/patch-001',
# 'question': '운영 원장 보정을 승인하시겠습니까?'
# }
#
# status: resolved
# patch: worktree://INC-482/patch-001
# attempt: 1
# delta: 0
이 예제에서 LLM은 한 줄도 직접 등장하지 않는다. 일부 Node 안에 모델을 넣으면 된다. 이게 오히려 중요한 점이다.
collect_evidence 안에서 Log와 Git Diff를 모델이 해석할 수 있고, propose_patch 안에서 Coding Model이 Patch를 만들 수 있다. 그러나 Retry 상한, 승인 경계, Verifier, Persistent State는 모델이 아니라 Runtime이 소유한다.
LangGraph API를 기능이 아니라 소유권으로 이해한다
API / 개념소유하는 것현업에서 중요한 점
| StateGraph(State) |
업무 State Schema |
대화 History 외에 도메인 상태를 명시적으로 둔다. |
| add_node |
실행 단위 |
Node마다 Retry·Timeout·Error 정책을 다르게 가져갈 수 있다. |
| add_conditional_edges |
분기와 Loop |
Termination Policy가 코드에 드러난다. |
| RetryPolicy |
재시도 계약 |
논리 오류가 아니라 주로 일시적 외부 장애를 재시도한다. |
| interrupt() |
HITL Pause |
State를 보존한 채 외부 판단을 기다린다. |
| Command(resume=...) |
재개 입력 |
같은 thread_id로 동일 실행을 이어간다. |
| Checkpointer |
Thread Snapshot |
HITL, 장애 복구, Time Travel, Fault Tolerance의 기반. |
| Store |
Cross-thread Memory |
Thread State와 장기 기억을 분리한다. |
| recursion_limit |
Graph 상한 |
잘못된 Loop가 무한 비용으로 이어지는 것을 막는다. |
12. Retry는 “다시 해봐”가 아니라 실패 분류와 State 갱신이다
Retry가 의미 있으려면 두 가지가 필요하다.
첫째, 실패 종류를 구분해야 한다. 네트워크 503, DB Lock 같은 일시적 실패는 동일 입력으로 다시 시도할 가치가 있다. 반면 Test Assertion 실패나 Schema 위반은 입력이나 Patch를 바꾸지 않으면 같은 결과가 반복될 가능성이 높다.
둘째, Failure Evidence가 다음 State에 들어가야 한다. 실패했는데 직전 오류를 지운 채 같은 Prompt를 다시 보내는 것은 Stateless Retry다.
실패권장 처리
| HTTP 5xx / 일시적 연결 오류 |
Exponential Backoff를 포함한 제한된 Retry |
| Tool Argument Validation 실패 |
모델이 인자를 수정할 수 있도록 구조화된 오류 반환 |
| Unit Test 실패 |
실패 Test, 핵심 Stack Trace를 다음 수정 State에 반영 |
| 권한 거부 |
자동 Retry하지 않고 Policy Denied 또는 Human Required |
| Budget 초과 |
Stop. “조금만 더”를 모델이 결정하게 하지 않음 |
13. Dify 같은 Low-code Runtime은 시각적 Workflow가 필요한 곳에 강하다
Dify 자료의 LLM, Knowledge Retrieval, Question Classifier, IF/ELSE, Variable Aggregator, Iteration, Loop, Code, Tool 블록을 각각 별도 기술로 외울 필요는 없다. 이들은 Graph Runtime의 시각적 Primitive로 볼 수 있다.
Iteration은 배열 요소를 반복 처리하는 데이터 순회에 가깝고, Loop는 종료 조건이 만족될 때까지 상태를 반복 갱신하는 제어 구조다. Question Classifier와 IF/ELSE는 Routing, Variable Aggregator는 Fan-in, Tool Block은 외부 Action/Observation 지점이다.
업로드된 dify_pipeline_local.py는 Open WebUI 쪽 요청을 Dify Workflow/Agent/Chat API로 변환하고, Streaming 이벤트를 다시 UI에 흘려보내는 Adapter 역할을 한다. 여기서 오래 남는 설계는 특정 requests 코드가 아니라 외부 Runtime과 내부 애플리케이션 사이에 번역 계층을 둔 것이다.
2026년 현재 Dify Workflow API도 /workflows/run에서 blocking과 SSE streaming을 지원하고, 실행 중에는 task_id, 영속 실행 기록에는 workflow_run_id를 사용한다. Human Input으로 Pause된 실행을 다시 이어가는 API도 별도로 제공한다.
현업 Adapter는 Business Logic과 HTTP 세부사항을 분리한다
현재 Dify Workflow API를 감싸는 비동기 Adapter 예시
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
import httpx
@dataclass(frozen=True)
class WorkflowResult:
run_id: str
status: str
outputs: dict[str, Any]
elapsed_time: float
total_tokens: int
total_steps: int
class DifyWorkflowError(RuntimeError):
pass
class DifyWorkflowClient:
def __init__(
self,
*,
base_url: str,
api_key: str,
timeout_s: float = 30.0,
) -> None:
# API Key는 frontend가 아니라 server-side secret으로만 둔다.
self._base_url = base_url.rstrip("/")
self._api_key = api_key
self._timeout = httpx.Timeout(timeout_s)
async def run_blocking(
self,
*,
inputs: dict[str, Any],
user_id: str,
) -> WorkflowResult:
"""
정형 Workflow의 결과를 한 번에 받아야 할 때 사용한다.
긴 작업은 streaming 또는 background run이 더 적합하다.
"""
headers = {
"Authorization": f"Bearer {self._api_key}",
"Content-Type": "application/json",
}
payload = {
"inputs": inputs,
"response_mode": "blocking",
"user": user_id,
}
async with httpx.AsyncClient(
base_url=self._base_url,
headers=headers,
timeout=self._timeout,
) as client:
response = await client.post(
"/workflows/run",
json=payload,
)
# HTTP 오류를 "빈 답변"으로 삼키지 않는다.
response.raise_for_status()
body = response.json()
data = body["data"]
if data["status"] != "succeeded":
raise DifyWorkflowError(
f"workflow failed: {data.get('error')}"
)
return WorkflowResult(
run_id=body["workflow_run_id"],
status=data["status"],
outputs=data["outputs"],
elapsed_time=float(data["elapsed_time"]),
total_tokens=int(data["total_tokens"]),
total_steps=int(data["total_steps"]),
)
# 사용 예
#
# client = DifyWorkflowClient(
# base_url="https://api.dify.ai/v1",
# api_key=os.environ["DIFY_API_KEY"],
# )
#
# result = await client.run_blocking(
# inputs={"query": "이번 보고서의 핵심 리스크를 정리해줘"},
# user_id="user-42",
# )
#
# print(result.status)
# print(result.outputs)
#
# 예상 결과 개념
# succeeded
# {'result': '...워크플로우가 생성한 결과...'}
이 버전에서 일부러 하지 않은 것도 중요하다. SSL 검증을 끄지 않고, bare except:로 Streaming 파싱 오류를 삼키지 않고, API Key를 로그로 출력하지 않는다. Adapter의 목적은 “Dify를 호출한다”가 아니라 네트워크 실패와 Runtime 실패를 우리 서비스가 이해할 수 있는 상태로 변환한다는 것이다.
14. Agentic RAG의 핵심은 검색 Tool이 아니라 Feedback Loop다
2024년 Agentic RAG 자료에는 pdf_search 후 relevance_check를 실행하고, 관련성이 낮으면 Query를 다시 만들고 재검색하는 패턴이 등장한다. 이 구조의 본질은 지금도 유효하다.
Query질문
→
Retrieve검색
→
Evaluate관련성 평가
↺
Rewrite검색 실패를 반영
다만 “최대 20번 다시 검색” 같은 규칙을 Prompt에만 적는 것과 Runtime Budget으로 관리하는 것은 다르다. 현대적인 구현에서는 검색 시도 횟수, 누적 Token, Latency, 마지막 실패 이유가 State에 있고, Budget 초과는 명시적 Stop Reason이 된다.
Agentic RAG가 일반 RAG보다 좋은 것도 아니다. 질문 하나에 Retriever 한 번으로 충분한 도메인이라면 고정 RAG가 더 빠르고 재현 가능하다. Query Rewrite와 Tool 선택이 실제 Retrieval 품질을 개선할 때만 Loop의 비용을 지불할 가치가 있다.
15. Evaluation은 모델 평가가 아니라 Runtime 평가다
Agent를 “답변이 좋아 보인다”로 평가하면 운영 품질을 알기 어렵다. 평가 단위는 실제 Task Outcome과 Trajectory(행동 궤적)까지 내려가야 한다.
평가 층질문예시 Metric
| Outcome |
업무가 실제로 끝났는가? |
Task Pass Rate, Test Pass Rate |
| Quality |
결과가 운영 가능한 품질인가? |
Defect Escape, Rework Rate |
| Tool |
도구를 맞게 골랐고 인자가 맞았는가? |
Tool Selection Accuracy |
| Trajectory |
불필요한 행동을 반복하지 않았는가? |
Step Count, Retry Count |
| Latency |
사용 가능한 시간 안에 끝났는가? |
P50/P95 Completion Time |
| Cost |
성공 한 건당 비용은 얼마인가? |
Cost per Successful Task |
| Safety |
권한 경계를 지켰는가? |
Denied Calls, Policy Violations |
| Human Load |
사람이 얼마나 자주 개입해야 하나? |
Handoff Rate, Review Minutes |
Eval Set도 하나로 끝나지 않는다.
Capability Eval — 새 시스템이 어려운 일을 얼마나 잘하는가.
Regression Eval — Harness를 바꿔도 기존 성공 사례가 깨지지 않는가.
Production Trace Eval — 실제 운영 실패가 평가 데이터로 다시 들어오는가.
Self-Evolving Harness를 만들려면 특히 Regression Eval이 중요하다. 실패 한 건을 고친 Skill 변경이 다른 열 건의 성공 사례를 망칠 수 있기 때문이다.
16. Trace가 없으면 Agent Debugging은 심리 추측이 된다
Agent가 잘못된 행동을 했을 때 “모델이 왜 그렇게 생각했을까?”부터 묻기 쉽다. 하지만 운영 시스템에서는 먼저 그 결정을 내릴 당시 어떤 State와 Observation이 있었는가를 본다.
최소 Trace는 이 정도면 좋다.
{
"run_id": "agent-run-2193",
"step": 4,
"state_in": {
"incident_id": "INC-482",
"attempt": 2,
"remaining_budget": 1
},
"action": {
"tool": "run_reconciliation",
"scope": "staging"
},
"observation": {
"exit_code": 1,
"error_kind": "MismatchDetected",
"summary": "17 records still differ",
"artifact_id": "log://run-2193/step-4"
},
"verification": {
"passed": false,
"failed_checks": ["reconciliation"]
},
"decision": {
"type": "retry",
"reason": "business invariant failed"
}
}
이 구조가 있으면 다음 질문이 재현 가능해진다. 직전 실패가 다음 State에 들어갔는가. 같은 Tool을 반복한 이유는 무엇인가. 어느 Step에서 Context가 커졌는가. 어느 Tool에서 Latency가 발생했는가. Verifier가 무엇을 실패로 판단했는가.
Agent Trace는 단순 Log가 아니라 향후 Eval과 Harness 개선의 학습 데이터다.
17. Multi-Agent보다 먼저 Ownership을 설계한다
Agent를 여러 개 만들면 자연스럽게 더 똑똑해질 것 같지만, 실제 운영 문제는 Agent 수보다 State와 Artifact의 소유권에서 발생한다.
세 Worker가 같은 Repository를 동시에 수정한다고 하자. Git Conflict를 해결해도 설계 의도가 서로 다르면 Semantic Conflict(의미 충돌)는 남는다.
Multi-Agent 전에 다음 질문에 답해야 한다.
- State Ownership — 현재 작업의 진실은 어디에 저장되는가?
- Artifact Ownership — 어떤 Worker가 어떤 파일/문서를 쓸 수 있는가?
- Write Ownership — 동일 Artifact에 동시 Write가 허용되는가?
- Merge Gate — 결과를 무엇으로 합격/불합격 판정해 합칠 것인가?
- Trace — 다른 Worker의 결정 근거를 재현할 수 있는가?
Coding에서는 의외로 Single Writer + Reviewer + Verifier가 강하다. 반대로 여러 독립 문서 조사나 여러 Repository 검사처럼 하위 Task가 서로 독립적으로 완료되고 검증 가능하면 Fan-out/Fan-in 병렬 Worker가 잘 맞는다.
병렬성은 목표가 아니라 검증 가능한 Task 분해의 결과다.
18. MCP와 A2A는 같은 문제를 풀지 않는다
MCP와 A2A를 모두 “Agent 연결 프로토콜”이라고만 보면 경계가 흐려진다.
MCPA2A
| 중심 관계 |
Agent ↔ Tool / Data |
Agent ↔ Remote Agent |
| 주요 관심사 |
Capability discovery, schema, tool/resource access |
독립 Agent 간 task/message/artifact 교환 |
| 쓰는 이유 |
Integration 경계를 표준화 |
프레임워크/공급업체가 다른 Agent 간 Interoperability |
모든 Tool을 Agent로 만들 필요도 없고, 모든 Agent 협업을 MCP 하나로 해결할 필요도 없다. “함수 하나를 호출하면 되는가?”라면 Tool이다. 별도 Runtime과 State, 독립적인 책임과 Task Lifecycle을 가진 상대라면 Remote Agent에 가깝다.
19. 사람이 Loop에서 빠지는 순간 Orchestrator가 필요해진다
Interactive Agent에서는 사람이 Loop 안에 있다.
Human요청
→
Agent실행
→
Human확인·다음 지시
Background/Ambient Agent에서는 시작 자체가 Event, Queue, Cron, Schedule로 바뀐다.
TriggerEvent·Cron·Queue
→
Dispatcher작업 선택
→
Worker격리 실행
→
EvidenceArtifact·Trace
→
Source of Truth다음 Tick의 입력
여기서 Orchestrator는 가장 똑똑할 필요가 없다. 초기에는 오히려 “한 Tick에 하나의 Dispatch 또는 No-op”처럼 단순할수록 중복 실행, Rate Limit, Audit, 장애 복구를 이해하기 쉽다.
Background Agent에는 Idempotency가 필수다
Queue와 Scheduler는 같은 Event를 두 번 전달할 수 있다. 네트워크 Timeout 때문에 성공한 작업을 실패로 오해하고 다시 보낼 수도 있다. 따라서 “이 요청은 한 번만 온다”는 전제를 버려야 한다.
event_id
↓
이미 처리했는가?
├─ Yes → no-op / 기존 결과 반환
└─ No
↓
실행
↓
결과 저장
↓
processed(event_id) 기록
Cursor, Checkpoint, Decision Log가 중요한 이유다. 사람이 사라진 자리를 “더 똑똑한 모델”이 아니라 영속 상태와 재실행 안전성이 메운다.
20. Human-in-the-loop는 실패가 아니라 정상 Runtime State다
완전 자율을 “사람이 전혀 개입하지 않는 것”으로 정의하면 고위험 시스템은 위험해진다. 더 유용한 목표는 Calibrated Autonomy(보정된 자율성)다.
되돌릴 수 있고, 정책 범위 안이고, 성공을 자동으로 검증할 수 있는 행동은 자동화한다. 비가역적이거나 불확실하거나 조직적 책임이 필요한 판단은 사람에게 올린다.
좋은 HITL 화면에는 승인 버튼만 있는 것이 아니라 다음 정보가 있어야 한다.
- 왜 멈췄는가 — blocker / policy / risk reason
- 무엇을 하려는가 — action과 정확한 대상 scope
- 어떤 근거가 있는가 — test, diff, trace, source
- 승인하면 무엇이 변하는가 — side effect와 rollback 경로
- 어디서 재개하는가 — thread/task/run id와 저장된 state
그리고 사람 개입 후 처음부터 다시 조사하게 만들지 않는다. 중단 시점의 State와 Evidence를 보존하고, 사람은 결정만 내려 같은 실행을 이어가게 한다.
21. Security는 Agent 바깥의 부가 기능이 아니라 Loop의 경계다
Tool을 한 번 호출하는 시스템보다 반복 호출하는 Agent의 공격 표면이 더 넓다. 작은 권한도 Loop 안에서 수십 번 반복되면 피해 반경이 커질 수 있기 때문이다.
Capability권장 기본 경계
| Repository 읽기·검색 |
자동 허용 가능. 민감 경로 제외. |
| Test / Build |
Sandbox 내부 자동 허용. |
| Worktree 파일 수정 |
허용 Path와 Repository 범위 제한. |
| Dependency 변경 |
Policy Check 또는 Review. |
| 외부 API Write |
Idempotency + 최소 권한 + Audit. |
| Production 데이터 변경 |
Human Approval + Read-back Verification. |
| Secret / Credential |
Context에 직접 넣지 않고 별도 Secret Boundary. |
Tool Output도 신뢰 경계다. Shell 출력, 외부 웹 페이지, 문서에는 Secret, 개인정보, Prompt Injection성 텍스트가 들어올 수 있다.
Raw Output외부 데이터
→
RedactSecret·PII 제거
→
Limit크기 제한
→
Classifyerror kind
→
Contextsummary + evidence id
보안 목표는 “Agent가 절대로 실수하지 않게 한다”가 아니다. 확률 시스템에서 더 현실적인 목표는 실수해도 피해 반경이 제한되고, 중요한 행동은 승인되며, 모든 변경이 추적되고 되돌릴 수 있게 하는 것이다.
22. OpenAI Agents SDK 같은 고수준 Runtime은 무엇을 대신 소유해 주는가
모든 팀이 Agent Loop를 직접 구현할 필요는 없다. 고수준 SDK는 자주 반복되는 Runtime Primitive를 대신 제공한다.
2026년 현재 OpenAI Agents SDK의 Agent와 Runner는 모델 호출, Function Tool, Handoff, Guardrail, Session, Tracing을 포함한 기본 Loop를 관리한다. 직접 Responses API를 사용하면 Loop를 더 많이 소유하고, Agents SDK를 사용하면 일반적인 orchestration을 SDK에 맡긴다.
주요 API역할설계 포인트
| Agent(...) |
instructions, tools, handoffs, output type, guardrails 구성 |
한 Agent의 책임을 좁게 정의 |
| Runner.run(...) |
비동기 Agent Loop 실행 |
앱의 Runtime 경계에서 실행 |
| Runner.run_streamed(...) |
Streaming Run |
긴 작업의 UX/관찰성 |
| RunConfig |
Run 단위 설정 |
Tool 동시성, error behavior, tracing 등 |
| Function Tool |
Python 함수를 Tool로 노출 |
Schema·Error·Timeout·Permission |
| Tool Guardrail |
Function Tool 전후 검사 |
Side Effect 가까이에 정책 배치 |
특히 Tool Guardrail이 중요하다. Agent-level Input/Output Guardrail만으로는 중간에 여러 Tool을 호출하는 긴 Workflow의 Side Effect를 모두 통제하기 어렵다. 실제 행동 직전·직후 Guard를 두면 모델의 전체 응답이 아니라 위험한 Capability 자체를 통제할 수 있다.
23. Self-Evolving은 자기수정이 아니라 검증된 Feedback 배포 루프다
Memory를 붙인다고 Agent가 계속 똑똑해지는 것은 아니다. 과거 실패가 다음 실행의 행동 규칙을 바꾸려면 운영 Feedback이 실제 Harness 변경으로 연결돼야 한다.
Runtime실제 실행
→
Trace성공·실패
→
Failure Analysis패턴 분류
→
Eval변경안 비교
→
ReviewVersion·승인
→
Deploy다음 Runtime
바꿀 수 있는 것은 Prompt만이 아니다.
Prompt — 모델의 판단 지침이 불명확했는가.
Skill — 반복 절차 자체가 부족했는가.
Tool — Capability 또는 Schema가 모호했는가.
Rule / Middleware — 모델이 아니라 코드로 고정해야 할 정책이었는가.
Eval — 성공 조건을 잘못 측정하고 있었는가.
운영 실패 하나를 본 Agent가 자기 Skill을 즉시 수정해 Production에 반영하도록 하는 것은 위험하다. 한 케이스에 과적합될 수 있고, 다른 정상 케이스를 깨뜨릴 수 있기 때문이다.
안전한 순서는 실패 수집 → 유사 실패 Cluster → 하나의 변경 후보 → Offline Eval → Holdout/Regression → Human Review → Version → 단계 배포 → Rollback 가능에 가깝다.
24. 조직 도입은 Pilot → Measure → Govern → Scale 순서가 맞다
Agent 도입에서 기술 데모와 운영 가치는 분리해야 한다. “코드 몇 줄 생성”, “AI Suggestion 몇 개”, “데모가 멋있다”는 활동량이지 업무 Outcome이 아니다.
시작점은 좁고 검증 가능한 업무가 좋다. 테스트가 있는 반복 Bug Fix, 정형 Migration, 문서 정합성 검사, Incident Evidence 수집, 표준 Report 생성처럼 Success Criteria를 만들 수 있는 업무다.
단계무엇을 하는가
| Pilot |
범위가 좁고 Verifier가 있는 Task를 고른다. |
| Measure |
도입 전 Completion Time, Review Time, Rework, Defect Baseline을 측정한다. |
| Govern |
Pilot부터 Permission, Secret, Approval, Audit, Rollback을 붙인다. |
| Scale |
Baseline을 넘은 범위만 확장한다. 아니면 단순화하거나 중단한다. |
Coding/Operations에서는 Lead Time, PR Cycle Time, Rework Rate, Change Failure Rate, Defect Escape, Incident Rate, Human Review Minutes, Cost per Successful Task가 유용하다.
외부 Benchmark와 Vendor 사례는 “가능할 수 있다”는 방향 신호다. 실제 ROI는 우리 업무와 우리 Baseline으로 다시 측정해야 한다.
25. 현업에서 반복해서 망가지는 패턴
증상근본 원인먼저 바꿀 것
| “완료”라고 했지만 틀림 |
Verifier 없음 |
외부 Evidence를 종료 조건으로 |
| 같은 실패를 계속 반복 |
Stateless Retry |
Failure Evidence를 다음 State에 반영 |
| 긴 작업에서 품질 급락 |
Context Rot |
Select·Compress·Isolate·Offload |
| 로그를 읽고 헤맴 |
Raw Observation Dump |
summary, error_kind, evidence_id로 구조화 |
| 비용이 계속 증가 |
Bound 없음 |
max step, timeout, cost cap |
| Tool을 자주 잘못 선택 |
이름·Schema·Description 겹침 |
좁은 Capability와 Tool Eval |
| Multi-Agent 결과 충돌 |
Artifact Ownership 없음 |
Single Writer 또는 Merge Gate |
| Self-Evolving 후 회귀 |
Regression Eval 없음 |
Holdout + Version + Rollback |
| 모든 행동에서 승인 요청 |
HITL 과다 |
저위험·가역 Action은 자동화 |
| 위험한 행동도 자동 실행 |
HITL 부재 |
Side Effect 근처에 Policy/Approval Gate |
26. 문제가 생기면 모델보다 Runtime부터 본다
Agent가 이상할 때는 다음 순서로 보는 것이 효율적이다.
- Termination — Done을 무엇으로 판단하는가. 모델 문장이라면 먼저 고친다.
- State Propagation — 직전 실패가 다음 실행에 실제 반영됐는가.
- Budget — Step, Timeout, Cost 상한이 있는가.
- Observation Shape — Raw Dump인가, 다음 판단용 Signal인가.
- Context — 오래된 정보와 중복 Tool Output이 쌓였는가.
- Tool Boundary — Tool이 너무 넓거나 이름·인자가 모호한가.
- Permission — 위험 행동이 Prompt 한 줄에만 의존하는가.
- Trace — 같은 오류를 재현할 수 있는가.
이 순서를 통과한 뒤에 “모델을 바꾸면 나아질까?”를 보는 편이 낫다. 많은 운영 실패는 모델 능력보다 Runtime 계약이 불분명해서 발생한다.
27. 어떤 기술을 언제 써야 하는가
문제먼저 고려할 기술왜
| 한 번의 생성 품질 |
Prompt / Structured Output |
가장 단순하고 비용이 낮다. |
| 사내 문서 Q&A |
Parsing + RAG |
지식 경계와 출처를 통제한다. |
| 검색 결과가 자주 부적합 |
Agentic RAG / Evaluator-Optimizer |
실패한 Retrieval을 Query Rewrite로 복구한다. |
| 정형 업무 자동화 |
Workflow / Dify / Graph |
제어 경로가 명확하고 테스트하기 쉽다. |
| 경로를 사전에 알 수 없는 복잡 작업 |
Bounded Agent Loop |
환경 피드백에 따라 다음 행동을 선택할 가치가 있다. |
| 긴 State와 HITL |
LangGraph + durable persistence |
Pause/Resume와 상태 복구가 핵심이 된다. |
| 외부 Tool/Data 연결 표준화 |
MCP |
Host별 Integration 비용을 낮춘다. |
| 독립 Agent 간 협업 |
A2A류 패턴 |
Tool 호출이 아니라 Task/Artifact 협업이 필요하다. |
| 사람 없이 반복 실행 |
Queue/Scheduler + Orchestrator |
Idempotency, Cursor, Retry, Audit가 필요하다. |
| 운영 실패를 다음 버전에 반영 |
Trace + Eval + Versioned Harness |
Feedback이 검증된 변경으로 이어져야 한다. |
28. 프로젝트를 시작할 때 실제로 정하는 순서
- Goal을 산출물로 정의한다. “잘 해줘”가 아니라 PR, Report, 복구된 정합성처럼 끝을 적는다.
- Verifier를 먼저 정의한다. Test, Business Invariant, Rule, Human Gate를 만든다.
- State Schema를 만든다. Goal, Attempt, Last Failure, Artifact, Evidence, Budget부터 시작한다.
- Tool을 좁게 설계한다. Read/Write를 분리하고 Scope와 Limit을 인자로 제한한다.
- Context를 Always-on과 On-demand로 나눈다.
- 문서는 구조와 Provenance를 보존해 Parse한다.
- 반복 절차만 Skill로 만든다.
- 독립적으로 완료·검증 가능한 일만 Subagent로 분리한다.
- Observation Schema를 정한다. exit_code, summary, error_kind, artifact/evidence id.
- Retry 가능한 실패와 불가능한 실패를 구분한다.
- Max Step, Timeout, Cost Cap을 둔다.
- High-risk Action의 Human Gate를 정한다.
- Trace를 처음부터 남긴다.
- Baseline Eval을 만든 다음 자율성을 올린다.
- Background 실행 전 Idempotency와 Checkpoint를 검증한다.
- Self-Evolving 전에 Version, Regression, Rollback 체계를 만든다.
29. 개발자의 역할은 Prompt Writer에서 Runtime Designer로 이동한다
Agent가 검색, 코드 작성, 반복 Review, 자료 정리를 더 많이 맡게 된다고 해서 개발자의 역할이 단순히 줄어드는 것은 아니다. 사람이 소유해야 할 판단이 더 위쪽으로 이동한다.
Architecture Decision — 어떤 경계를 코드로 고정하고 어디에 모델의 자유도를 줄지 결정한다.
Domain Rule — 무엇이 진짜 올바른 결과인지 정의한다.
Evaluation Design — 어떤 Evidence를 성공으로 인정할지 만든다.
Governance — 어디까지 자동화하고 어디서 승인할지 정한다.
Harness / Skill Design — 반복되는 팀의 판단과 절차를 재사용 가능한 자산으로 만든다.
Product Judgment — 실제 사용자가 언제 AI의 자동화보다 설명·통제·신뢰를 필요로 하는지 판단한다.
모델이 더 강해질수록 이 구조는 더 중요해질 가능성이 높다. 더 강한 모델은 더 긴 작업과 더 많은 Tool을 사용할 수 있기 때문에, 성공할 수 있는 범위와 함께 실패할 수 있는 범위도 넓어진다.
좋은 Agent는 가장 자율적인 Agent가 아니다.
검증된 만큼만 자율성을 갖고, 실패하면 Evidence를 남기고, 위험하면 멈추고, 필요한 순간에 사람에게 정확한 결정을 올리며, 성공했음을 외부 세계에서 증명할 수 있는 Agent가 좋은 Agent다.
Prompt는 한 번의 호출을 잘 지시한다.
Context는 이번 판단에 필요한 정보를 구성한다.
Harness는 모델이 일하는 방식과 권한을 설계한다.
Loop는 실패를 복구하고 성공을 증명한다.
Orchestration은 그 실행을 사람의 시간에서 분리한다.
결국 Agent Engineering의 중심은 모델 호출 자체가 아니라 확률적인 추론을 검증 가능한 업무 시스템으로 바꾸는 Runtime을 소유하는 능력이다.
30. 공식 문서: 무엇을 봐야 하는가
API는 빠르게 바뀐다. 블로그 코드 한 줄을 외우기보다 각 공식 문서에서 어떤 Runtime 원리를 확인해야 하는지 아는 편이 오래간다.
Model Context Protocol — Specificationhttps://modelcontextprotocol.io/볼 것: Tool/Resource/Protocol 계약, Authorization, 현재 버전의 변경점. MCP가 “Agent 지능”이 아니라 Integration Boundary라는 점을 확인한다.