Field note · 2026-05-26

AI 에이전트는 왜 "한 번 잘 됐다"고 믿으면 안 되는가

Skill과 Hook으로 만드는 ProviderFree 하네스 AI 에이전트를 운영할 때 가장 위험한 착각은 "한 번 잘 됐으니 이제 된다"는 믿음이다. 에이전트는 같은 입력에도 다른 경로를 택하고, 도구를 호출하며, 중간 상태를 만들고, 때로는 사람의 승인을 기다린다. 그래서 에이전트 품질은 모델 성능만으로 결정되지 않

AI 에이전트는 왜 "한 번 잘 됐다"고 믿으면 안 되는가

Skill과 Hook으로 만드는 Provider-Free 하네스


요약

AI 에이전트를 운영할 때 가장 위험한 착각은 "한 번 잘 됐으니 이제 된다"는 믿음이다. 에이전트는 같은 입력에도 다른 경로를 택하고, 도구를 호출하며, 중간 상태를 만들고, 때로는 사람의 승인을 기다린다. 그래서 에이전트 품질은 모델 성능만으로 결정되지 않는다. 무엇을 할 줄 아는지(Skill), 언제 개입할지(Hook), 어떻게 기록하고 판정할지(Harness) 가 함께 있어야 한다.

예전에는 hook이 특정 도구, 특히 Claude Code 쪽의 편의 기능처럼 보였다. 그래서 provider-free 구성을 지향하는 입장에서는 hook에 너무 의존하는 것이 꺼려질 수 있었다. 그런데 Codex 계열에서도 hook과 유사한 실행 개입점이 등장하고, 여러 런타임이 lifecycle hook, middleware, guardrail, approval, tracing을 공통 기능으로 다루기 시작하면서 그림이 바뀌었다. 이제 hook은 특정 provider의 특수 기능이라기보다 에이전트 런타임의 표준 인터페이스에 가까워지고 있다.

이 글의 관점은 단순하다.

구성 요소역할provider-free로 보는 법
Skill에이전트가 수행할 업무 지식과 절차모델이 바뀌어도 재사용되는 작업 설명서
Hook실행 전후에 끼어드는 정책·검사·기록 지점Claude/Codex/LangGraph/Temporal로 옮길 수 있는 이벤트 계약
HarnessSkill과 Hook을 묶어 실행·검증·감사하는 구조provider adapter만 갈아끼우는 운영 골격

결국 중요한 것은 "어느 모델이 더 똑똑한가"가 아니라, 업무 지식은 Skill로 분리하고, 런타임 통제는 Hook으로 분리하며, 평가와 기록은 Harness로 표준화하는 것이다.


왜 Skill과 Hook인가

에이전트 시스템은 보통 두 가지가 뒤섞이면서 망가진다.

첫째, 프롬프트 안에 업무 지식이 계속 쌓인다. "이 문서는 이렇게 읽고, 저 고객은 이렇게 분류하고, 이 도구는 이런 경우에만 쓰고, 이런 결과는 실패로 봐라"가 한 프롬프트 안에 들어간다. 나중에는 모델을 바꾸기도 어렵고, 같은 업무를 다른 에이전트에게 이식하기도 어렵다.

둘째, 런타임 정책이 모델의 선의에 맡겨진다. "위험하면 물어봐", "비용 많이 쓰지 마", "파일을 지우기 전에는 확인해" 같은 지시를 프롬프트에 써두지만, 실제 실행 시점에 강제되지는 않는다.

Skill과 Hook은 이 둘을 분리한다.

문제프롬프트 중심 접근Skill + Hook 접근
업무 지식긴 시스템 프롬프트에 누적SKILL.md 같은 독립 단위로 관리
실행 정책모델이 기억해주길 기대hook이 실행 전후에 검사
평가 기준답변 후 사람이 감으로 판단oracle/evaluator hook으로 기록
재사용성provider를 바꾸면 다시 작성skill과 hook 계약은 유지, adapter만 교체
감사성대화 로그를 뒤져야 함run/event log로 남김

이렇게 보면 하네스는 거대한 플랫폼이 아니다. Skill 파일, Hook 이벤트, 평가 스크립트, 실행 로그를 묶는 작은 운영 규약에서 시작할 수 있다.


최소 구조

Provider-free 하네스의 최소 디렉터리는 이런 모양이면 충분하다.

agent-harness/
  skills/
    research-brief/SKILL.md
    kb-rewrite/SKILL.md
    customer-risk-review/SKILL.md
  hooks/
    pre_tool_use.yaml
    post_tool_use.yaml
    pre_response.yaml
    on_run_end.yaml
  evaluators/
    citation_check.yaml
    cost_budget.yaml
    policy_rubric.yaml
  fixtures/
    research_cases.yaml
    kb_rewrite_cases.yaml
  runs/
    2026-05-11T090000Z/
      trace.jsonl
      result.md
      eval.json

여기서 핵심은 파일명이 아니다. 핵심은 세 가지 계약이다.

계약질문
Skill contract이 에이전트는 어떤 업무를 어떤 절차로 수행하는가?
Hook contract실행 중 어떤 사건이 발생하면 무엇을 검사하거나 기록하는가?
Evaluation contract성공/실패/보류를 어떤 기준으로 판정하는가?

이 계약만 유지되면 Claude Code, Codex, OpenAI Agents SDK, LangGraph, Temporal, 자체 worker 사이를 옮겨 다닐 수 있다. 도구별 문법은 달라도, 개념은 거의 같다.


Skill은 업무 지식의 이식 단위다

Skill은 "모델에게 주는 긴 설명"이 아니다. 특정 업무를 수행하기 위한 목적, 입력, 절차, 산출물, 금지사항, 검증 기준을 담은 작업 단위다.

예를 들어 리서치 브리프 Skill은 이렇게 쓸 수 있다.

# research-brief

## Purpose
사용자의 질문을 근거 기반 리서치 브리프로 정리한다.

## Inputs
- research_question
- scope
- allowed_sources
- output_format

## Procedure
1. 질문을 하위 쟁점으로 분해한다.
2. 각 쟁점마다 1차 출처를 우선 수집한다.
3. 주장마다 출처를 연결한다.
4. 근거가 약한 주장은 별도 표시한다.
5. 최종 브리프에는 결론, 근거, 한계, 다음 질문을 포함한다.

## Output
- `brief.md`
- `sources.json`
- `open_questions.md`

## Failure Conditions
- 출처 없는 핵심 주장
- 오래된 자료를 최신 사실처럼 사용
- 사용자 범위를 벗어난 조사

## Evaluation
- citation_check
- freshness_check
- claim_support_check

이 Skill은 특정 모델에 종속되지 않는다. Claude가 읽어도 되고, Codex가 읽어도 되고, 별도의 agent worker가 읽어도 된다. 중요한 것은 Skill이 업무의 구조를 담고 있다는 점이다.

Skill을 잘 만들면 다음 효과가 생긴다.

효과설명
재사용성같은 업무를 여러 provider에서 실행할 수 있다
테스트 가능성Skill 단위로 fixture와 evaluator를 붙일 수 있다
버전 관리research-brief@v1, @v2처럼 변경 이력을 남길 수 있다
온보딩새 에이전트가 업무 맥락을 빠르게 읽을 수 있다
감사성"그때 어떤 절차를 따랐는가"를 확인할 수 있다

Hook은 런타임의 개입 지점이다

Hook은 에이전트가 실행되는 중간에 런타임이 끼어드는 지점이다. 도구마다 이름은 다르다. 어떤 곳은 hook이라 부르고, 어떤 곳은 middleware, lifecycle event, guardrail, callback, interceptor라고 부른다. 하지만 실무적으로는 같은 문제를 푼다.

사용자 요청
→ skill load
→ plan 생성
→ pre_tool_use hook
→ tool 실행
→ post_tool_use hook
→ state update
→ evaluator hook
→ pre_response hook
→ 최종 응답
→ on_run_end hook

Hook이 중요한 이유는 모델의 판단을 믿지 않아도 되는 통제 지점이 생기기 때문이다. 예를 들어 "삭제 전에는 확인해"를 프롬프트에 적는 대신, pre_tool_use hook에서 삭제 명령을 막거나 승인 요청으로 바꿀 수 있다.

Provider-free 관점에서는 hook을 특정 제품 문법으로 먼저 생각하지 않는 편이 좋다. 먼저 이벤트 계약을 만든다.

event: pre_tool_use
input:
  run_id: string
  skill_id: string
  tool_name: string
  arguments: object
  risk_level: low | medium | high
  estimated_cost: number
output:
  decision: allow | block | require_approval | rewrite
  reason: string
  approval_packet: object?

이 계약을 Claude Code hook, Codex hook, LangGraph middleware, Temporal activity wrapper, 자체 CLI wrapper에 각각 맞춰 연결하면 된다. 문법은 달라도 운영 원칙은 유지된다.


예제 1: 삭제 작업을 막는 Hook

가장 단순한 hook은 위험한 도구 호출을 막는 것이다.

id: block_destructive_file_ops
event: pre_tool_use
when:
  tool_name: shell
  command_matches:
    - "rm -rf"
    - "git reset --hard"
    - "git clean -fd"
decision: require_approval
approval_packet:
  risk: high
  reason: "Destructive filesystem or git operation"
  required_fields:
    - command
    - cwd
    - affected_paths
    - rollback_plan

이 hook의 의미는 provider와 무관하다. Claude Code에서 실행되든 Codex에서 실행되든, 또는 자체 agent runner에서 실행되든, 위험한 명령 앞에서 멈추는 정책은 같다.

이 hook 하나만 있어도 하네스의 다섯 관점 중 세 가지가 바로 살아난다.

관점이 hook이 하는 일
궤적위험 명령 시도가 trace에 남는다
비용사고 복구 비용을 줄인다
HITL승인 패킷을 통해 사람에게 넘긴다

예제 2: Skill별 비용 예산 Hook

에이전트 테스트에서 비용은 쉽게 폭발한다. 비용 통제도 hook으로 만들 수 있다.

id: enforce_skill_budget
event: pre_model_call
when:
  skill_id: research-brief
limits:
  max_run_cost_usd: 5.00
  max_model_calls: 25
  max_tool_calls: 40
decision:
  if_over_budget: block
  if_near_budget: require_approval
message:
  near_budget: "Budget is over 80%. Continue?"
  over_budget: "Run blocked because skill budget was exceeded."

이 hook은 "돈 많이 쓰지 마"라는 프롬프트보다 강하다. 모델이 잊어도 런타임이 막는다. 또한 Skill별로 예산을 다르게 줄 수 있다. 리서치 브리프는 5달러, 단순 문서 정리는 0.5달러, 고가치 감사 작업은 20달러처럼 업무 가치에 맞춰 조정할 수 있다.


예제 3: 출처 없는 주장 차단 Hook

리서치나 정책 문서 생성에서는 "그럴듯하지만 근거 없는 문장"이 가장 위험하다. 이때 pre_response hook을 쓴다.

id: require_citations_for_claims
event: pre_response
applies_to:
  - research-brief
  - policy-analysis
checks:
  - name: claim_support_check
    rule: "Every non-trivial factual claim must include a source reference."
  - name: source_freshness_check
    rule: "Time-sensitive claims must include publication or access date."
decision:
  on_fail: rewrite
  max_rewrite_attempts: 2
  after_max_attempts: require_human_review

이 hook은 오라클의 일부다. 최종 응답을 내보내기 전에 품질 기준을 적용한다. 완벽한 판정은 아니더라도, 최소한 "출처 없는 주장"이라는 반복 실패를 런타임에서 잡아낼 수 있다.


예제 4: Knowledge Base 재작성 Skill과 Hook

Obsidian KB를 재작성하는 Skill을 생각해보자.

# kb-rewrite

## Purpose
기존 노트를 의미 단위로 정리하되, 원문의 핵심 주장과 링크 구조를 보존한다.

## Inputs
- target_note
- reference_notes
- rewrite_goal

## Procedure
1. target_note의 핵심 주장과 근거를 추출한다.
2. 중복 문단을 합친다.
3. 관련 개념은 wikilink로 연결한다.
4. 원문에 없던 주장은 추가하지 않는다.
5. 변경 요약을 마지막에 남긴다.

## Output
- updated target_note
- rewrite_summary
- unresolved_questions

## Failure Conditions
- 원문에 없는 주장 추가
- 기존 wikilink 손실
- frontmatter 손상

여기에 hook을 붙인다.

id: protect_obsidian_note_integrity
event: pre_file_write
applies_to:
  - kb-rewrite
checks:
  - preserve_frontmatter
  - preserve_existing_wikilinks
  - detect_unsupported_new_claims
decision:
  on_frontmatter_loss: block
  on_wikilink_loss: require_approval
  on_new_claim: require_rewrite

이 구조는 "에이전트에게 잘 고쳐달라고 부탁"하는 방식과 다르다. Skill은 어떻게 고칠지 알려주고, Hook은 망가뜨리면 안 되는 것을 지킨다. 그래서 provider가 바뀌어도 KB 운영 규칙은 유지된다.


예제 5: Ontology 기반 상태 Hook

Provider-free 하네스에서 ontology/KG는 모든 곳에 필요한 것은 아니다. 하지만 관계, 시간, 승인 이력을 추적해야 하는 업무에서는 hook의 대상이 된다.

예를 들어 고객 정책 검토 에이전트가 있다고 하자.

Customer
→ has_contract
→ Contract
→ governed_by
→ Policy
→ requires
→ HumanApproval

이때 pre_tool_use hook은 도구 호출 전에 그래프 상태를 조회한다.

id: check_policy_graph_before_action
event: pre_tool_use
applies_to:
  - customer-risk-review
when:
  tool_name:
    - update_customer_plan
    - send_external_email
    - change_access_level
graph_checks:
  - customer_has_active_contract
  - action_allowed_by_policy
  - required_approval_exists
decision:
  on_missing_contract: block
  on_policy_conflict: require_human_review
  on_missing_approval: require_approval

여기서 ontology는 "똑똑한 검색"이 아니라 실행 제어의 일부다. 에이전트가 어떤 행동을 하기 전에, 현재 상태와 정책 그래프를 확인한다. 이것이 벡터 검색만으로는 부족한 지점이다.


다섯 가지 관점으로 다시 보기

Skill과 Hook 중심으로 봐도 하네스의 핵심 관점은 변하지 않는다. 다만 구현 방식이 더 선명해진다.

1. 비결정성: Skill fixture로 반복 실행한다

비결정성은 모델의 성격이다. 같은 Skill을 같은 입력으로 실행해도 경로가 달라질 수 있다. 그래서 하네스는 Skill별 fixture를 둔다.

skill_id: research-brief
cases:
  - id: case_001
    input:
      research_question: "Agent runtime에서 checkpoint가 필요한 이유"
      scope: "최근 5년 연구와 공식 문서"
    expected:
      min_sources: 5
      required_sections:
        - conclusion
        - evidence
        - limitations
    repeat: 10

이렇게 하면 "한 번 잘 됐다"가 아니라 10번 중 몇 번 성공했는가를 볼 수 있다.

2. 궤적: Hook event를 trace로 남긴다

Trace는 대화 기록이 아니라 hook event의 시퀀스다.

{"event":"skill_loaded","skill_id":"research-brief","version":"v2"}
{"event":"pre_tool_use","tool":"web_search","decision":"allow"}
{"event":"post_tool_use","tool":"web_search","cost":0.12,"status":"ok"}
{"event":"pre_response","check":"claim_support_check","decision":"rewrite"}
{"event":"on_run_end","status":"passed","total_cost":1.84}

이 기록이 있어야 "결과는 맞았지만 어떤 경로로 맞았는가"를 볼 수 있다.

3. 오라클: Evaluator도 Skill처럼 분리한다

오라클을 프롬프트 안에 넣지 말고 evaluator로 분리한다.

id: citation_check
type: hybrid
rules:
  - "Every factual claim needs a source."
  - "Sources must be listed in sources.json."
llm_judge:
  rubric:
    - claim_support
    - source_relevance
    - uncertainty_labeling
human_review:
  required_when:
    - llm_judge_score_below: 0.75
    - policy_sensitive: true

이렇게 하면 모델을 바꿔도 평가 기준은 유지된다. 필요하면 evaluator만 교체하거나 보정하면 된다.

4. 비용: Hook으로 예산을 강제한다

비용 통제는 프롬프트가 아니라 hook의 역할이다.

event: on_cost_update
thresholds:
  warn_at: 0.8
  block_at: 1.0
budget_source:
  - skill.default_budget
  - run.override_budget
actions:
  warn_at: summarize_remaining_plan
  block_at: stop_and_report

이런 구조가 있어야 에이전트가 긴 루프에 빠졌을 때도 운영자가 비용을 예측할 수 있다.

5. HITL: Approval을 Hook decision으로 만든다

사람의 개입은 예외가 아니라 hook의 decision 중 하나다.

decision: require_approval
approval_packet:
  action: "send_external_email"
  risk: medium
  reason: "External communication with customer"
  evidence:
    - draft_email.md
    - customer_policy.md
  options:
    - approve
    - reject
    - edit_and_resume

이렇게 하면 HITL은 "사람에게 물어봄"이 아니라 interrupt → review → approve/reject/edit → resume이라는 명시적 상태 전이가 된다.


Provider-Free 설계 원칙

Provider-free를 지향한다면 특정 provider의 기능을 쓰지 말자는 뜻이 아니다. 오히려 좋은 기능은 쓰되, 그 기능을 표준 계약 뒤에 숨겨야 한다는 뜻에 가깝다.

원칙설명
Skill은 Markdown 또는 YAML 같은 평문으로 둔다특정 SDK 객체에만 갇히지 않게 한다
Hook은 이벤트 계약부터 정의한다Claude/Codex 문법은 adapter에서 처리한다
Tool schema는 provider 밖에 둔다모델 교체 시 tool 의미가 유지된다
Evaluator는 독립 실행 가능해야 한다모델 응답 없이도 결과 파일을 검사할 수 있어야 한다
Trace는 JSONL처럼 단순한 형식으로 남긴다나중에 다른 분석 도구로 읽을 수 있게 한다
Approval packet은 구조화한다사람의 승인을 감사 가능한 데이터로 남긴다
Provider adapter는 얇게 유지한다교체 가능한 부분과 고정할 부분을 분리한다

이를 구조로 그리면 다음과 같다.

skills/           업무 지식
hooks/            실행 정책
evaluators/       성공 판정
fixtures/         반복 테스트 입력
trace/            실행 기록
provider-adapter/ Claude Code, Codex, OpenAI SDK, LangGraph 등 연결부

Provider-free의 핵심은 "모든 도구를 동일하게 쓰자"가 아니다. 바뀌어도 되는 부분과 바뀌면 안 되는 부분을 분리하자는 것이다. 모델과 에이전트 앱은 바뀔 수 있다. 그러나 업무 Skill, 위험 정책, 평가 기준, 승인 기록, 실행 trace는 조직의 자산으로 남아야 한다.


Hook이 표준이 된다는 것의 의미

Hook이 여러 에이전트 도구에 들어오기 시작했다는 것은 중요한 신호다. 에이전트 개발의 중심이 프롬프트에서 런타임으로 이동하고 있다는 뜻이기 때문이다.

프롬프트는 모델에게 "이렇게 해줘"라고 말한다. Hook은 런타임에게 "이 시점에는 반드시 이것을 확인해"라고 말한다. 이 차이가 크다.

프롬프트 지시Hook 정책
위험하면 조심해위험 도구 호출 전 승인 요구
출처를 잘 달아줘출처 없는 주장 차단
너무 오래 돌지 마step/cost/time budget 초과 시 중단
실수하지 마실패 유형을 trace에 기록하고 재시도 제한
사람에게 물어봐approval packet 생성 후 checkpoint에서 대기

Hook이 표준화되면 provider-free 설계가 더 쉬워진다. 각 도구가 hook을 지원한다면, 우리는 같은 정책을 여러 런타임에 이식할 수 있다. Claude Code에서 쓰던 삭제 방지 hook을 Codex에 맞게 옮기고, 나중에는 LangGraph middleware나 Temporal activity wrapper로도 옮길 수 있다.


최소 실행 가능한 Skill-Hook 하네스

처음부터 거대한 시스템을 만들 필요는 없다. 다음 정도면 시작할 수 있다.

1. 자주 반복하는 업무 하나를 Skill로 만든다.
2. 그 Skill의 대표 fixture 10~20개를 만든다.
3. 반드시 막아야 하는 위험 행동 3개를 pre_tool_use hook으로 만든다.
4. 반드시 지켜야 하는 품질 기준 3개를 pre_response evaluator로 만든다.
5. 모든 hook decision을 trace.jsonl에 남긴다.
6. 같은 fixture를 5~10회 반복 실행한다.
7. success rate, cost, hook block, approval count를 본다.
8. 실패가 반복되는 부분을 Skill 또는 Hook으로 승격한다.

여기서 중요한 마지막 문장은 이것이다. 반복되는 프롬프트 지시는 Skill로 승격하고, 반복되는 런타임 사고는 Hook으로 승격한다. 이것이 하네스가 성장하는 방식이다.


정리

에이전트 하네스는 "테스트를 많이 돌리자"는 말이 아니다. Provider가 바뀌어도 유지되는 운영 골격을 만드는 일이다.

다섯 가지 관점으로 압축하면 다음과 같다.

관점Skill-Hook 하네스에서의 구현
비결정성Skill fixture를 반복 실행해 성공률을 본다
궤적Hook event를 trace로 남긴다
오라클Evaluator를 Skill 밖에 분리한다
비용cost/budget hook으로 강제한다
HITLapproval packet을 hook decision으로 만든다

Claude Code에만 hook이 있을 때는 이것이 provider-specific 기능처럼 보였다. 하지만 Codex와 다른 런타임에서도 hook, middleware, guardrail, lifecycle event가 공통 패턴으로 자리 잡고 있다면 이야기가 달라진다. Hook은 특정 도구에 갇힌 기능이 아니라, 에이전트 시스템을 운영 가능한 구조로 바꾸는 표준적인 제어면이 된다.

따라서 provider-free 전략은 hook을 피하는 것이 아니라, hook을 추상화하는 쪽이 맞다. Skill은 업무 지식의 이식 단위로, Hook은 런타임 정책의 이식 단위로, Harness는 그 둘을 검증하고 기록하는 운영 단위로 설계한다. 이 구조가 있으면 모델과 도구는 바뀌어도, 조직의 에이전트 운영 지식은 남는다.