본문 바로가기
Agentic AI/AI_AGENT

하니스엔지니어링(Harness Engineering) 로직 구현

by 아톨 2026. 10. 4.

이번 코드는 앞서 공부한 MCP + Skill 구조에서 한 단계 더 발전한 형태입니다.

이전에는 우리가 다음 구조를 공부했습니다.

SKILL.md → LLM → MCP Tool → MCP Server → 실제 데이터 → LLM → 답변

이번에는 여기에 Harness Engineering이 추가됩니다.

즉,

“LLM에게 일을 시키는 것”에서 끝나는 것이 아니라, LLM이 여러 작업을 안정적으로 수행하도록 바깥에서 계획·상태·저장·재실행을 관리하는 구조

입니다.

특히 이번 코드에서 꼭 이해해야 할 핵심은 다음 5가지입니다.

① SKILL.md       → AI에게 "어떻게 일할지" 가르친다.
② Tool           → AI가 실제 데이터를 가져오게 한다.
③ Agent Loop     → Tool 호출 → 결과 전달 → 최종 답변을 만든다.
④ Harness        → 여러 작업의 계획/상태/저장/재실행을 관리한다.
⑤ Persistence    → 작업 결과와 진행 상태를 파일에 남긴다.
 

1. 먼저 전체 그림부터 보겠습니다

이번 코드는 다음과 같은 구조입니다.

                    사용자 / 프로그램
                           │
                           │
                    ["서울","도쿄",
                     "뉴욕","런던","파리"]
                           │
                           ▼
                ┌────────────────────┐
                │      Harness       │
                │                    │
                │  plan.json 관리    │
                │  작업 순서 관리    │
                │  상태 관리         │
                │  결과 저장         │
                │  재실행 가능       │
                └─────────┬──────────┘
                          │
                    도시 하나씩 처리
                          │
                          ▼
                ┌────────────────────┐
                │     SKILL.md       │
                │                    │
                │ "기상 전문가로서   │
                │  이렇게 판단하라"  │
                └─────────┬──────────┘
                          │
                          ▼
                ┌────────────────────┐
                │        LLM         │
                │                    │
                │ Tool이 필요하다고  │
                │ 판단               │
                └─────────┬──────────┘
                          │
                    Tool Call
                          │
                          ▼
                ┌────────────────────┐
                │ get_weather_info()  │
                │                    │
                │ 실제 날씨 데이터   │
                │ 조회                │
                └─────────┬──────────┘
                          │
                          ▼
                    날씨 JSON
                          │
                          ▼
                ┌────────────────────┐
                │        LLM         │
                │                    │
                │ Skill의 정책 적용  │
                │ 최종 리포트 작성   │
                └─────────┬──────────┘
                          │
                          ▼
                ┌────────────────────┐
                │ reports/서울.md    │
                │ reports/도쿄.md    │
                │ reports/뉴욕.md    │
                │ ...                │
                └────────────────────┘
                          │
                          ▼
                ┌────────────────────┐
                │     plan.json      │
                │                    │
                │ 서울: completed    │
                │ 도쿄: completed    │
                │ ...                │
                └────────────────────┘
 

이 그림을 먼저 머릿속에 넣고 코드를 보시면 훨씬 쉽습니다.


2. 그런데 Harness가 정확히 무엇인가?

여기가 이번 공부의 핵심입니다.

Harness를 우리말로 단순하게 표현하면:

AI Agent가 일을 제대로 수행하도록 주변에서 관리·통제해주는 실행 환경

정도로 이해하면 좋습니다.

AI Agent 자체가:

생각한다
↓
Tool 사용한다
↓
결과를 본다
↓
답한다
 

를 담당한다면,

Harness는:

무슨 일을 할 것인가?
↓
어떤 순서로 할 것인가?
↓
어디까지 했는가?
↓
결과를 어디에 저장할 것인가?
↓
중간에 실패하면 다시 할 수 있는가?
 

를 관리합니다.


3. Agent와 Harness의 차이

이것을 회사 조직으로 비유하면 굉장히 쉽습니다.

Agent = 직원

직원에게:

"서울 날씨를 분석해서 보고서를 작성하세요."

라고 하면,

Skill을 참고하고 Tool을 사용해서 보고서를 만듭니다.


Harness = 팀장/PM/업무관리 시스템

Harness는 직원에게:

오늘 처리할 도시가 서울, 도쿄, 뉴욕, 런던, 파리입니다.

라고 업무를 배정합니다.

그리고:

서울      → 완료
도쿄      → 완료
뉴욕      → 진행 중
런던      → 대기
파리      → 대기
 

를 관리합니다.

그리고 보고서를 파일로 저장합니다.

따라서:

Agent는 일을 수행하고, Harness는 일을 운영합니다.

이렇게 이해하면 좋습니다.


4. 이제 코드의 첫 부분부터 보겠습니다

 
import json
import os
import pathlib
from dotenv import load_dotenv
from openai import OpenAI
 

앞서 공부했던 것과 거의 같습니다.


json

JSON 데이터를 다루기 위해 필요합니다.

이번 프로젝트에서는 특히:

plan.json
 

을 관리합니다.


os

파일 존재 여부 확인:

 
os.path.exists(...)
 

환경변수:

 
os.getenv(...)
 

등에 사용합니다.


pathlib

파일 경로를 편리하게 관리합니다.


OpenAI

LLM을 호출하기 위한 객체입니다.


5. 환경설정

 
load_dotenv(override=True)
 

.env 파일의 환경변수를 읽습니다.

예를 들어:

OPENAI_API_KEY=...
OPENAI_MODEL=gpt-5.4-mini
 

등을 읽게 됩니다.


6. 프로젝트의 기본 경로

 
BASE_DIR = pathlib.Path(__file__).parent
 

현재 Python 파일이 있는 디렉터리를 의미합니다.

예를 들어:

C:\Dev\book_agentic_ai\harness\
 

에서 실행한다면:

BASE_DIR
=
C:\Dev\book_agentic_ai\harness
 

정도가 됩니다.


7. Skill 파일 위치

 
SKILL_PATH = BASE_DIR / "skills" / "SKILL.md"
 

즉 구조가 대략:

프로젝트
│
├── harness.py
│
├── skills
│   └── SKILL.md
│
├── reports
│
└── plan.json
 

형태라는 뜻입니다.


8. 결과 저장 디렉터리

 
REPORT_DIR = BASE_DIR / "reports"
 

각 도시의 결과를 여기에 저장합니다.

예:

reports/
├── 서울.md
├── 도쿄.md
├── 뉴욕.md
├── 런던.md
└── 파리.md
 

9. 계획 파일

 
PLAN_FILE = BASE_DIR / "plan.json"
 

이 파일이 Harness의 핵심입니다.

예를 들어 처음에는:

 
{
    "서울": "pending",
    "도쿄": "pending",
    "뉴욕": "pending",
    "런던": "pending",
    "파리": "pending"
}
 

가 됩니다.

작업이 끝나면:

 
{
    "서울": "completed",
    "도쿄": "completed",
    "뉴욕": "completed",
    "런던": "completed",
    "파리": "completed"
}
 

가 됩니다.


10. mkdir

 
REPORT_DIR.mkdir(exist_ok=True)
 

reports 폴더가 없으면 만듭니다.

이미 있으면 그대로 둡니다.

즉 프로그램을 처음 실행해도:

reports/
 

폴더를 자동으로 만들어 줍니다.


11. Mock Weather Data

 
MOCK_WEATHER_DATA = {
    "서울": {
        "condition": "맑음",
        "temperature_c": 22,
        "humidity": 45
    },
    ...
}
 

이 부분은 실제 날씨 API가 아닙니다.

가짜 테스트 데이터입니다.

즉 이번 Harness 실습에서는 외부 API나 MCP 서버를 연결하지 않고 Agent 구조 자체를 테스트하는 것입니다.


12. 왜 Mock을 사용하는가?

이것은 AI Agent 개발에서 굉장히 중요한 방법입니다.

실제 Open-Meteo API를 호출하면:

인터넷
↓
API
↓
날씨 데이터
 

가 필요합니다.

그런데 Agent/Harness 로직을 테스트할 때마다 실제 API를 호출할 필요는 없습니다.

따라서:

서울 → 22도 / 맑음 / 습도45%
도쿄 → 18도 / 흐림 / 습도60%
뉴욕 → 12도 / 비 / 습도85%
...
 

처럼 고정된 데이터를 넣어놓고 Agent의 판단 구조만 테스트하는 것입니다.


13. get_weather_info()

 
def get_weather_info(location):
 

이것은 이번 프로젝트의 Tool입니다.

앞서 MCP에서 보았던:

 
get_city_weather()
 

와 역할이 비슷합니다.

다만 이번에는 MCP 서버에 있는 Tool이 아니라 Python 함수 자체를 Tool로 등록해서 OpenAI에 전달합니다.


14. 함수 내부

 
data = MOCK_WEATHER_DATA.get(
    location,
    {
        "condition":"정보 없음",
        "temperature_c":20,
        "humidity":50
    },
)
 

예를 들어:

 
get_weather_info("서울")
 

이면:

 
{
    "condition": "맑음",
    "temperature_c": 22,
    "humidity": 45
}
 

를 가져옵니다.


15. JSON 문자열로 변환

 
return json.dumps(
    data,
    ensure_ascii=False
)
 

Python dictionary를 JSON 문자열로 변환합니다.

예:

 
{
    "condition": "맑음",
    "temperature_c": 22,
    "humidity": 45
}
 

↓

 
{"condition":"맑음","temperature_c":22,"humidity":45}
 

입니다.


16. 그런데 아주 중요한 차이가 있습니다

앞서 MCP 코드에서는:

 
await session.call_tool(...)
 

이 있었습니다.

이번에는 없습니다.

왜냐하면 이번에는 MCP가 없기 때문입니다.

구조가:

이전
LLM
 ↓
MCP Client
 ↓
MCP Server
 ↓
Tool
 

였다면,

이번에는:

LLM
 ↓
Harness
 ↓
Python Tool
 

입니다.

즉 이번 코드는 Harness Engineering 자체에 집중하기 위해 MCP를 제거한 구조라고 이해하면 됩니다.


17. Tool Schema

다음이 중요합니다.

 
tools = [
    {
        "type":"function",
        "name": "get_weather_info",
        ...
    }
]
 

이것은 LLM에게 Tool 사용법을 알려주는 명세입니다.


18. LLM은 Python 코드를 직접 보는 것이 아닙니다

LLM은:

 
def get_weather_info(location):
 

라는 Python 코드를 직접 보고 사용하는 것이 아닙니다.

대신 다음과 같은 Schema를 받습니다.

이런 Tool이 있다.

이름:
get_weather_info

설명:
특정 도시의 현재 기온, 기상 상태, 습도 정보를 가져온다.

입력:
location
문자열
 

그러면 LLM이:

서울 날씨가 필요하니까 get_weather_info(location="서울")를 호출해야겠다.

라고 판단할 수 있습니다.


19. parameters

 
"parameters": {
    "type":"object",
    "properties": {
        "location": {
            "type":"string",
            "description":"도시 이름..."
        }
    },
    "required":["location"]
}
 

이것은 Tool의 입력 규격입니다.

즉:

get_weather_info(
    location = "서울"
)
 

이어야 합니다.


20. 이제 진짜 Harness 구성요소가 시작됩니다

주석을 보면:

 
# [Harness 구성 요소]
 

라고 되어 있습니다.

첫 번째가:

 
def initialize_plan(cities):
 

입니다.


21. initialize_plan()

 
def initialize_plan(cities):
    plan = {city:"pending" for city in cities}
 

예를 들어:

 
cities = ["서울","도쿄","뉴욕","런던","파리"]
 

이면:

 
plan
 

은:

 
{
    "서울": "pending",
    "도쿄": "pending",
    "뉴욕": "pending",
    "런던": "pending",
    "파리": "pending"
}
 

이 됩니다.


22. 이것이 바로 "Plan"입니다

AI Agent가 일을 할 때:

"무슨 일을 해야 하지?"

를 관리하는 목록입니다.

여기서는 도시 5개가 작업 단위입니다.

Task 1 → 서울
Task 2 → 도쿄
Task 3 → 뉴욕
Task 4 → 런던
Task 5 → 파리
 

23. 파일로 저장

 
with open(PLAN_FILE, "w", encoding="utf-8") as f:
    json.dump(
        plan,
        f,
        indent=4,
        ensure_ascii=False
    )
 

이것이 Persistence, 즉 영속성입니다.

프로그램의 메모리에만 가지고 있는 것이 아니라 디스크에 저장합니다.


24. 왜 굳이 파일로 저장할까요?

이게 Harness Engineering에서 굉장히 중요합니다.

예를 들어 5개 도시 중:

서울  완료
도쿄  완료
뉴욕  완료
런던  실패
파리  대기
 

까지 진행했다고 합시다.

그런데 프로그램이 갑자기 종료되었습니다.

만약 메모리에만 저장했다면:

어디까지 했는지 모릅니다.

하지만 plan.json이 있다면:

 
{
    "서울": "completed",
    "도쿄": "completed",
    "뉴욕": "completed",
    "런던": "pending",
    "파리": "pending"
}
 

를 읽으면 됩니다.

그리고 다시 실행하면:

서울 → 건너뜀
도쿄 → 건너뜀
뉴욕 → 건너뜀
런던 → 다시 실행
파리 → 실행
 

할 수 있습니다.

이것이 Harness의 매우 중요한 특징입니다.


25. run_harness()가 핵심입니다

 
def run_harness():
 

이 함수가 사실상 전체 작업 관리자입니다.


26. 계획 파일이 있는지 확인

 
if not os.path.exists(PLAN_FILE):
    print("계획 파일이 없습니다.")
    return
 

즉 Harness를 실행하려면 먼저 작업 계획이 있어야 합니다.


27. Plan을 읽습니다

 
with open(PLAN_FILE, "r", encoding="utf-8") as f:
    plan = json.load(f)
 

예를 들어:

 
{
    "서울": "completed",
    "도쿄": "pending",
    "뉴욕": "pending"
}
 

가 Python dictionary로 들어옵니다.


28. Skill도 읽습니다

 
with open(SKILL_PATH, "r", encoding="utf-8") as f:
    skill_instruction = f.read()
 

이것은 앞서 우리가 공부했던 것과 동일합니다.

SKILL.md
   ↓
skill_instruction
   ↓
system prompt
   ↓
LLM
 

입니다.


29. 도시별 반복

 
for city, status in plan.items():
 

예를 들어:

서울 → pending
도쿄 → pending
뉴욕 → pending
런던 → pending
파리 → pending
 

을 하나씩 처리합니다.


30. 여기서 중요한 Harness 기능

 
if status == "completed":
    continue
 

이 한 줄이 굉장히 중요합니다.

이미 끝난 작업은 다시 하지 않습니다.

예:

서울 completed
도쿄 completed
뉴욕 pending
런던 pending
파리 pending
 

이면:

서울 → 건너뜀
도쿄 → 건너뜀
뉴욕 → 실행
런던 → 실행
파리 → 실행
 

입니다.


31. 이것이 "재개 가능한 Agent"의 기초입니다

영어로는 흔히:

Resumability

라는 개념으로 생각할 수 있습니다.

작업이 중단되어도 처음부터 다시 하지 않고 마지막 상태에서 이어갈 수 있는 구조입니다.


32. Context Reset

이번 코드에서 제가 특히 중요하다고 보는 부분입니다.

 
messages = [
    {"role":"system", "content":skill_instruction},
    {
        "role":"user",
        "content":f"{city}의 날씨를 조회하고 리포트를 작성해줘."
    },
]
 

그리고 주석에:

각 도시마다 깨끗한 컨텍스트로 시작
(Context Reset)
 

이라고 되어 있습니다.


33. 왜 Context Reset이 필요할까요?

만약 서울 작업을 끝내고:

서울은 22도입니다.
 

라는 대화가 Context에 남아 있는 상태에서 도쿄를 처리하면 어떻게 될까요?

LLM에게:

서울 이야기
+
서울 Tool 결과
+
서울 보고서
+
도쿄 질문
 

이 모두 들어갈 수 있습니다.

그러면 불필요한 정보가 섞입니다.


34. 그래서 도시마다 새 Context를 만듭니다

서울 작업

System: SKILL.md
User: 서울 날씨 분석
Tool: 서울 데이터
LLM: 서울 보고서

          ↓ Context 폐기

도쿄 작업

System: SKILL.md
User: 도쿄 날씨 분석
Tool: 도쿄 데이터
LLM: 도쿄 보고서
 

이렇게 합니다.

이것이:

Context Isolation

입니다.


35. Harness에서 굉장히 중요한 설계입니다

작업 단위가 독립적이라면:

한 작업의 Context가 다른 작업으로 넘어가지 않도록 한다.

이 원칙이 매우 중요합니다.

특히 Agent가 수십, 수백 개의 작업을 처리할 때 중요합니다.


36. 이제 LLM을 호출합니다

 
response = openai_client.responses.create(
    model=model,
    input=messages,
    tools=tools,
    tool_choice="auto",
)
 

LLM에게 네 가지를 줍니다.

① model
② messages
③ tools
④ tool_choice
 

37. messages

여기에는:

System
→ SKILL.md

User
→ 서울 날씨 조회하고 리포트 작성해줘
 

가 들어 있습니다.


38. tools

LLM이 사용할 수 있는 Tool:

get_weather_info(location)
 

을 알려줍니다.


39. tool_choice="auto"

앞서 MCP+Skill 예제에서는:

 
tool_choice="required"
 

였습니다.

이번에는:

 
tool_choice="auto"
 

입니다.

차이가 중요합니다.

required

반드시 Tool을 사용해라.

auto

필요하면 Tool을 사용해라.

입니다.

이번 Skill에는:

데이터를 확보하려면 get_weather_info를 호출하라.

라고 명시되어 있기 때문에 LLM이 Tool을 호출할 가능성이 높습니다.


40. 첫 번째 LLM 호출의 역할

여기서는 최종 보고서를 만드는 것이 아닙니다.

첫 번째 호출은 주로:

"어떤 Tool을 어떤 인자로 사용할 것인가?"

를 결정하는 단계입니다.

예를 들어:

사용자:
서울 날씨를 조회하고 리포트를 작성해줘.

LLM:
get_weather_info를 사용해야겠다.

arguments:
{
    "location": "서울"
}
 

가 됩니다.


41. Agent Loop 시작

 
messages.extend(response.output)
 

LLM의 Tool Call 내용을 기존 대화에 추가합니다.

그 다음:

 
tool_calls = [
    item
    for item in response.output
    if item.type == "function_call"
]
 

Tool Call만 골라냅니다.


42. Tool Call에서 이름과 인자를 추출

 
tool_name = tool_call.name
 

결과:

get_weather_info
 

그리고:

 
tool_args = json.loads(tool_call.arguments)
 

예:

 
{
    "location": "서울"
}
 

이 됩니다.


43. 실제 Tool 실행

 
function_response = get_weather_info(
    tool_args['location']
)
 

여기서 진짜 Python 함수가 실행됩니다.

즉:

LLM
 │
 │ "get_weather_info 서울 호출"
 ▼
Harness
 │
 ▼
get_weather_info("서울")
 │
 ▼
MOCK_WEATHER_DATA
 

입니다.


44. MCP가 없다는 것을 다시 확인하세요

이 부분은 앞선 프로젝트와 비교하면 아주 중요합니다.

이전 프로젝트

 
result = await session.call_tool(
    tool_name,
    tool_args
)
 

MCP 서버를 호출했습니다.

이번 프로젝트

 
function_response = get_weather_info(
    tool_args['location']
)
 

Python 함수를 직접 호출합니다.

즉:

이번 프로젝트는 MCP를 빼고 Harness Engineering의 핵심 원리를 연습하는 구조입니다.


45. Tool 결과

서울이라면:

 
{
    "condition": "맑음",
    "temperature_c": 22,
    "humidity": 45
}
 

가 반환됩니다.


46. 그리고 Tool 결과를 다시 LLM에게 넣습니다

 
messages.append(
    {
        "type":"function_call_output",
        "call_id":tool_call.call_id,
        "output":result_text,
        "name":tool_name,
    }
)
 

이것이 Agent Loop의 핵심입니다.

LLM
 ↓
Tool Call
 ↓
Tool 실행
 ↓
Tool 결과
 ↓
LLM에게 다시 전달
 ↓
최종 답변
 

47. 이것이 우리가 앞에서 공부했던 Tool Calling과 정확히 연결됩니다

기억하시면 됩니다.

[1단계]

LLM
"Tool을 사용해야겠다."

        ↓

[2단계]

Harness
"그럼 Tool을 실행하겠습니다."

        ↓

[3단계]

Tool
"날씨 데이터입니다."

        ↓

[4단계]

Harness
"LLM에게 결과를 다시 전달하겠습니다."

        ↓

[5단계]

LLM
"이 데이터를 분석해서 최종 보고서를 작성합니다."
 

48. 두 번째 LLM 호출

 
final_response = openai_client.responses.create(
    model=model,
    input=messages,
)
 

여기서는 Tool 결과가 이미 messages에 들어 있습니다.

따라서 LLM은:

SKILL.md
+
사용자 질문
+
Tool 호출
+
Tool 결과
 

를 모두 보고 최종 보고서를 작성합니다.


49. Skill의 Policy가 여기서 작동합니다

예를 들어 서울:

기온 = 22°C
습도 = 45%
상태 = 맑음
 

Skill을 보면:

15~25°C
+
습도 60% 미만
 

이면:

최적

입니다.

따라서 LLM은:

조깅 적합도: 최적
 

으로 판단합니다.


50. 뉴욕은 어떨까요?

Mock 데이터:

뉴욕
기온 12°C
비
습도 85%
 

Skill 정책:

습도 80% 이상 → 주의
비 → 야외 활동 자제
 

따라서 LLM은 단순히:

12도니까 보통

이라고 하면 안 됩니다.

특이사항 정책이 더 중요합니다.

비 감지
↓
조깅 적합도와 상관없이
야외 활동 자제
 

라는 Skill의 규칙을 적용해야 합니다.

이것이 바로:

Skill = 단순한 프롬프트가 아니라 업무 정책을 가진 실행 지침

이라는 것을 보여줍니다.


51. 최종 결과를 가져옵니다

 
report_content = final_response.output_text
 

이제 LLM이 작성한 Markdown 보고서가 문자열로 들어옵니다.

예:

 
### **서울 기상 분석 리포트**

- **현재 날씨:** 22°C, 맑음 (습도: 45%)

- **전문가 분석:** 기온과 습도가 조깅에 적합한 수준입니다.

- **오늘의 권고:** 조깅 적합도: 최적 - 가벼운 운동복과 물을 준비하세요.
 

52. 여기서 Harness가 다시 등장합니다

LLM이 보고서를 만들었다고 끝이 아닙니다.

Harness가:

"이 결과를 저장하자."

라고 합니다.

 
with open(
    f"{REPORT_DIR}/{city}.md",
    "w",
    encoding="utf-8"
) as f:
    f.write(report_content)
 

53. 이것이 Persistence입니다

예를 들어:

reports/
 

에:

서울.md
 

가 생성됩니다.

다음에는:

도쿄.md
뉴욕.md
런던.md
파리.md
 

가 생성됩니다.

즉 LLM의 일회성 응답을 영구적인 산출물(artifact)로 바꾸는 것입니다.


54. 그리고 가장 중요한 상태 업데이트

 
plan[city] = "completed"
 

서울 작업이 끝났다면:

 
{
    "서울": "completed",
    "도쿄": "pending",
    "뉴욕": "pending",
    "런던": "pending",
    "파리": "pending"
}
 

가 됩니다.


55. 그리고 다시 저장합니다

 
with open(PLAN_FILE, "w", encoding="utf-8") as f:
    json.dump(
        plan,
        f,
        indent=4,
        ensure_ascii=False
    )
 

이것이 매우 중요합니다.

작업 완료 상태를 디스크에 기록합니다.


56. 실제 전체 실행 과정을 따라가 보겠습니다

처음 실행하면:

cities =
[
    서울,
    도쿄,
    뉴욕,
    런던,
    파리
]
 

입니다.


① 처음 시작

plan.json이 없습니다.

따라서:

 
initialize_plan(cities)
 

실행.

결과:

 
{
    "서울": "pending",
    "도쿄": "pending",
    "뉴욕": "pending",
    "런던": "pending",
    "파리": "pending"
}
 

57. ② 서울

Harness:

서울 작업 시작
 

Context:

System = SKILL.md
User = 서울 날씨 조회 + 리포트
 

LLM:

get_weather_info("서울")
 

Tool:

 
{
    "condition":"맑음",
    "temperature_c":22,
    "humidity":45
}
 

LLM:

22°C
45%
맑음
→ 조깅 최적
 

보고서 저장:

reports/서울.md
 

상태:

서울 = completed
 

58. ③ 도쿄

새로운 Context가 만들어집니다.

System = SKILL.md
User = 도쿄 날씨 조회 + 리포트
 

서울의 대화 내용은 없습니다.

Tool:

18°C
흐림
60%
 

Skill 적용:

18°C → 최적 범위
습도 60% → 60% 미만은 아니므로 최적 조건에는 엄밀히 부합하지 않음
 

여기서 LLM이 Skill의 조건을 정확하게 적용해 판단합니다.

그리고:

reports/도쿄.md
 

에 저장합니다.


59. ④ 뉴욕

12°C
비
85%
 

Skill:

비 → 야외 활동 자제
습도 80% 이상 → 주의
 

따라서 강하게:

야외 활동 자제

를 권고해야 합니다.

그리고:

reports/뉴욕.md
 

에 저장합니다.


60. ⑤ 런던

15°C
안개
75%
 

기온 자체는 좋은 편입니다.

하지만 Skill에:

안개 감지
→ 야외 활동 자제
 

가 있습니다.

따라서 단순히:

15도니까 조깅 최적

이라고 하면 안 됩니다.

이것이 Policy 우선순위입니다.


61. ⑥ 파리

20°C
구름 조금
50%
 

대체로 최적 조건입니다.

보고서를 저장합니다.


62. 결국 이런 구조가 됩니다

plan.json

서울  → completed
도쿄  → completed
뉴욕  → completed
런던  → completed
파리  → completed
 

그리고:

reports/
│
├── 서울.md
├── 도쿄.md
├── 뉴욕.md
├── 런던.md
└── 파리.md
 

가 생성됩니다.


63. 그런데 Harness의 진짜 힘은 "중단 후 재실행"입니다

예를 들어 뉴욕까지 끝났다고 합시다.

 
{
    "서울": "completed",
    "도쿄": "completed",
    "뉴욕": "completed",
    "런던": "pending",
    "파리": "pending"
}
 

그런데 컴퓨터가 꺼졌습니다.

다시 프로그램을 실행하면:

 
if status == "completed":
    continue
 

때문에:

서울 → SKIP
도쿄 → SKIP
뉴욕 → SKIP
런던 → 실행
파리 → 실행
 

됩니다.

이게 Harness + Persistence의 핵심 가치입니다.


64. 일반적인 단순 Agent와 비교하면

단순 Agent

질문
 ↓
LLM
 ↓
Tool
 ↓
답변
 

입니다.

프로그램이 종료되면 작업 상태가 사라질 수 있습니다.


Harness Agent

Plan
 ↓
Task
 ↓
Context Reset
 ↓
LLM
 ↓
Tool
 ↓
LLM
 ↓
Report 저장
 ↓
Status 업데이트
 ↓
다음 Task
 

입니다.

그리고:

plan.json
+
reports/
 

가 남습니다.


65. 그래서 Harness는 Agent의 "운영 시스템"이라고 생각하면 좋습니다

저라면 이번 프로젝트에서는 이렇게 기억하시라고 말씀드리고 싶습니다.

Agent는 생각하고 행동하는 두뇌이고, Harness는 그 Agent가 일을 안정적으로 수행하도록 감싸는 운영 프레임워크다.


66. 그런데 이번 코드에서 아주 재미있는 부분이 있습니다

Skill.md의 첫 부분:

# Weather Expert Skill

## 1. 페르소나
너는 기상 데이터 분석 전문가이자 라이프스타일 코치다.
 

이것은 Persona입니다.

LLM에게:

"너는 누구인가?"

를 알려줍니다.


그리고:

## 2. 분석 정책
 

은:

"어떤 기준으로 판단할 것인가?"

입니다.


그리고:

## 3. 작업 절차
 

는:

"어떤 순서로 일할 것인가?"

입니다.


그리고:

## 4. 출력 형식
 

은:

"결과를 어떤 형태로 만들 것인가?"

입니다.


그리고:

## 5. 제약 사항
 

은:

"절대로 하면 안 되는 것은 무엇인가?"

입니다.


67. 따라서 SKILL.md는 사실상 Agent의 "업무 명세서"입니다

이렇게 볼 수 있습니다.

SKILL.md
│
├── Persona
│      ↓
│   나는 누구인가?
│
├── Policy
│      ↓
│   어떻게 판단하는가?
│
├── Instructions
│      ↓
│   어떤 순서로 일하는가?
│
├── Output Format
│      ↓
│   어떤 형태로 결과를 내는가?
│
└── Constraints
       ↓
    무엇을 하면 안 되는가?
 

이 구조는 앞으로 Skill을 설계할 때 상당히 중요합니다.


68. 이번 SKILL.md가 이전 것보다 발전한 점

앞서 사용했던 Skill은 대략:

기온 15~25
습도 60% 미만
→ 최적
 

정도의 간단한 규칙이었습니다.

이번에는:

최적
보통
주의
 

가 있고,

특히:

비
안개
황사
 

라는 예외/특이사항까지 있습니다.

즉 단순한 조건문에서 조금 더 전문 업무 정책에 가까워졌습니다.


69. 그리고 이번 Harness 구조가 중요한 이유

실제 기업용 Agent 시스템에서는:

사용자 질문
 

하나만 처리하는 경우보다:

100개의 문서 분석
500개의 고객 데이터 처리
1,000개의 상품 분석
10,000개의 웹페이지 조사
 

처럼 많은 작업을 반복적으로 수행하는 경우가 많습니다.

이때 단순 Agent만으로는 부족합니다.

필요한 것이:

Plan
State
Checkpoint
Persistence
Retry
Context Isolation
Artifact 저장
 

등입니다.

이번 코드에서는 그중 기본적인 것들을 직접 구현해 본 것입니다.


70. 이번 코드에서 구현된 Harness 기능을 정리하면

기능코드역할
Plan plan.json 해야 할 작업 관리
Task 도시 하나 작업 단위
Status pending/completed 진행 상태
Context Reset messages=[] 도시별 독립 Context
Tool get_weather_info() 실제 데이터 획득
Agent Loop 1차→Tool→2차 LLM Tool 결과 기반 추론
Persistence reports/*.md 결과 영구 저장
Checkpoint plan.json 업데이트 완료 지점 기록
Resume completed 건너뜀 중단 후 재개
Constraint SKILL.md Agent 행동 제한

71. 이전 MCP 프로젝트와 이번 프로젝트를 비교하면

이 부분을 꼭 연결해서 보세요.

이전 프로젝트

SKILL.md
    ↓
LLM
    ↓
MCP Client
    ↓
MCP Server
    ↓
MCP Tool
    ↓
외부 API
 

핵심 학습:

Agent가 MCP를 통해 외부 기능을 사용하는 방법


이번 프로젝트

Harness
    ↓
SKILL.md
    ↓
LLM
    ↓
Python Tool
    ↓
Tool 결과
    ↓
LLM
    ↓
Report
    ↓
plan.json
 

핵심 학습:

Agent를 반복 작업에 투입하고, 그 작업을 안정적으로 관리하는 방법


72. 그러면 둘을 합치면 어떻게 될까요?

바로 이것입니다.

                         Harness
                ┌──────────────────────┐
                │ Plan                 │
                │ State                │
                │ Context Reset        │
                │ Persistence          │
                │ Retry                │
                └──────────┬───────────┘
                           │
                           ▼
                       AI Agent
                           │
                     ┌─────┴─────┐
                     │           │
                 SKILL.md       LLM
                     │           │
                     └─────┬─────┘
                           │
                           ▼
                       MCP Client
                           │
                        MCP
                           │
                           ▼
                       MCP Server
                           │
                           ▼
                         Tools
                           │
                           ▼
                    External Systems
 

이것이 앞으로 우리가 만들 수 있는 좀 더 실제적인 Agent Architecture입니다.


73. 그리고 A2A까지 연결하면

지금까지 공부하신 내용을 모두 연결하면:

                         Harness
                            │
                            ▼
                      Manager Agent
                            │
                           A2A
              ┌─────────────┼─────────────┐
              ▼             ▼             ▼
         Weather Agent  Research Agent  Document Agent
              │             │             │
           Skill.md       Skill.md       Skill.md
              │             │             │
             LLM           LLM           LLM
              │             │             │
             MCP           MCP           MCP
              │             │             │
           Tool/API       Tool/API       Tool/API
 

라는 형태로 발전할 수 있습니다.

즉 지금 배우고 있는 각각의 기술이 사실 따로 노는 것이 아닙니다.

Python
 ↓
Tool Calling
 ↓
Agent
 ↓
Memory
 ↓
MCP
 ↓
Skill
 ↓
Harness
 ↓
A2A
 

가 점점 하나의 Agent 시스템 아키텍처로 합쳐지고 있는 것입니다.


74. 코드에서 한 가지 주의해서 볼 부분

학습용 코드로는 아주 좋지만, 실제 시스템으로 발전시킬 때는 몇 가지 개선할 부분이 있습니다.

① 알 수 없는 도시 처리

현재:

 
MOCK_WEATHER_DATA.get(
    location,
    {
        "condition":"정보 없음",
        "temperature_c":20,
        "humidity":50
    }
)
 

입니다.

즉 존재하지 않는 도시를 입력해도:

20°C
습도 50%
정보 없음
 

이라는 데이터가 반환됩니다.

그런데 SKILL.md는:

추측으로 날씨를 말하지 마라. 반드시 도구를 통해 얻은 데이터만 사용하라.

라고 되어 있습니다.

Tool은 호출되었지만 실제 날씨가 아닌 임의의 기본값 20°C/50%를 반환하고 있습니다.

따라서 실제 시스템에서는 이것을:

 
{
    "error": "해당 도시의 날씨 데이터를 찾을 수 없습니다."
}
 

처럼 처리하는 편이 더 안전합니다.


75. 또 하나 재미있는 부분

Skill에서는:

도시 이름(예: 서울, Tokyo...)
 

라고 되어 있지만 Mock 데이터에는:

 
"서울"
"도쿄"
"뉴욕"
"런던"
"파리"
 

가 있습니다.

따라서 LLM이 "Tokyo"라고 Tool을 호출하면:

 
MOCK_WEATHER_DATA.get("Tokyo", ...)
 

가 되어 데이터를 찾지 못합니다.

실제 시스템에서는 도시명을 표준화하거나:

Tokyo → 도쿄
New York → 뉴욕
London → 런던
Paris → 파리
 

같은 mapping을 두는 것이 좋습니다.

이것도 Agent 시스템에서 Tool 입력을 얼마나 엄격하게 설계해야 하는가를 보여주는 좋은 예입니다.


76. 그리고 현재 Harness는 "완전한 자동 복구"까지는 아닙니다

현재 예외처리는:

 
except Exception as e:
    print(...)
 

입니다.

즉 실패하면 오류를 출력합니다.

하지만:

retry
backoff
failed 상태 기록
재시도 횟수
 

등은 없습니다.

따라서 현재는 Harness Engineering의 기본 골격을 학습하는 단계라고 보면 좋습니다.


77. 이번 코드의 본질을 한 문장으로 표현하면

“SKILL.md를 업무 매뉴얼로 사용하는 LLM Agent에게 여러 도시라는 독립적인 작업을 순차적으로 맡기고, Tool Calling으로 데이터를 확보한 뒤, 결과물을 파일로 저장하고 plan.json으로 작업 상태를 관리하는 간단한 Harness를 구현한 코드”

입니다.


78. 마지막으로 꼭 기억해야 할 구조

앞서 배운 MCP와 이번 Harness를 머릿속에서 이렇게 구분하시면 됩니다.

                  ┌───────────────────────┐
                  │       Harness         │
                  │                       │
                  │ "일을 관리한다"       │
                  │                       │
                  │ Plan                  │
                  │ State                 │
                  │ Context               │
                  │ Persistence           │
                  │ Resume                │
                  └───────────┬───────────┘
                              │
                              ▼
                  ┌───────────────────────┐
                  │       AI Agent        │
                  │                       │
                  │ "일을 수행한다"       │
                  │                       │
                  │ SKILL.md              │
                  │ LLM                   │
                  │ Tool Calling          │
                  │ Agent Loop            │
                  └───────────┬───────────┘
                              │
                              ▼
                  ┌───────────────────────┐
                  │         MCP           │
                  │                       │
                  │ "기능을 연결한다"     │
                  │                       │
                  │ Client ↔ Server       │
                  │ Tool / Resource       │
                  └───────────┬───────────┘
                              │
                              ▼
                  ┌───────────────────────┐
                  │ External Systems      │
                  │ API / DB / Files ...  │
                  └───────────────────────┘
 

이 관점으로 보면 지금까지 공부한 것이 상당히 깔끔하게 연결됩니다.

  • Skill = 전문 업무 지침
  • LLM = 판단/추론하는 두뇌
  • Tool = 실제 행동 수단
  • MCP = Tool을 표준 방식으로 연결하는 통신/인터페이스
  • Agent Loop = 생각 → 행동 → 결과 → 다시 생각
  • Harness = Agent에게 여러 일을 안정적으로 시키는 작업 관리/운영 구조
  • Persistence = 작업 결과와 상태를 남겨 중단 후에도 이어가는 장치
  • A2A = 여러 Agent가 서로 협력하도록 연결하는 방식

그리고 이번 코드에서 특히 중요한 것은 plan.json과 reports/가 추가되면서 Agent가 “한 번 질문하고 답하는 프로그램”에서 “여러 작업을 수행하고, 진행 상황을 기억하며, 결과물을 남기는 업무 수행 시스템”으로 한 단계 올라갔다는 점입니다.

 

    # 하네스 엔지니어링 로직 구현

    # import asyncio
    import json
    import os
    import pathlib
    # from mcp import ClientSession
    # from mcp.client.sse import sse_client
    from dotenv import load_dotenv
    from openai import OpenAI

    # 1. 환경설정
    load_dotenv(override=True)


    # 2. 초기 설정
    BASE_DIR = pathlib.Path(__file__).parent
    SKILL_PATH = BASE_DIR / "skills" / "SKILL.md"

    REPORT_DIR = BASE_DIR / "reports"
    PLAN_FILE = BASE_DIR / "plan.json"

    REPORT_DIR.mkdir(exist_ok=True)

    MOCK_WEATHER_DATA ={
        "서울":{"condition":"맑음", "temperature_c":22, "humidity":45},
        "도쿄": {"condition": "흐림", "temperature_c": 18, "humidity": 60},
        "뉴욕": {"condition": "비", "temperature_c": 12, "humidity": 85},
        "런던": {"condition": "안개", "temperature_c": 15, "humidity": 75},
        "파리": {"condition": "구름 조금", "temperature_c": 20, "humidity": 50},
    }
    # ---------------------------------------------------------
    # [Tool 정의] 각 나라/도시의 날씨 정보를 리턴하는 Dummy 함수
    # ---------------------------------------------------------
    # 3. get_weather_info 함수 정의
    def get_weather_info(location):
        """특정 위치의 현재 기온, 날씨 상태, 습도 데이터를 반환합니다."""
        data = MOCK_WEATHER_DATA.get(
            location,
            {"condition":"정보 없음", "temperature_c":20, "humidity":50},
        )
        return json.dumps(data, ensure_ascii=False)

    # OpenAI 모델에게 전달할 도구 명세(Schema)
    tools = [
        {
            "type":"function",
            "name": "get_weather_info",
            "description":"특정 도시의 현재 기온, 기상 상태, 습도 정보를 가져옵니다.",
            "parameters":{
                "type":"object",
                "properties":{
                    "location":{"type":"string","description":"도시 이름(예: 서울, Tokyo...)"},
                },
                "required":["location"]
            },  
        }
    ]

    # ---------------------------------------------------------
    # [Harness 구성 요소]
    # ---------------------------------------------------------
    # 4. initialize_plan 함수 정의
    def initialize_plan(cities):
        plan = {city:"pending" for city in cities}
        with open(PLAN_FILE, "w", encoding="utf-8") as f:
            json.dump(plan, f, indent=4, ensure_ascii=False)

    # 5. run_harness 함수 정의
    def run_harness():
        if not os.path.exists(PLAN_FILE):
            print("계획 파일이 없습니다.")
            return
        with open(PLAN_FILE, "r", encoding="utf-8") as f:
            plan = json.load(f)
        with open(SKILL_PATH, "r", encoding="utf-8") as f:
            skill_instruction = f.read()
       
        for city, status in plan.items():
            if status == "completed": continue
            print(f"\n>> [Harness] {city} 작업 시작...")
           
            # 각 도시마다 깨끗한 컨텍스트로 시작 (Context Reset)
            messages= [
                {"role":"system", "content":skill_instruction},
                {"role":"user", "content":f"{city}의 날씨를 조회하고 리포트를 작성해줘."},
            ]
           
            try:
                # 1단계: 모델에게 질문 (도구 호출 권한 부여)
                # OpenAI 클라이언트 초기화
                openai_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY",""))
                model = os.getenv("OPENAI_MODEL","gpt-5.4-mini")
                response = openai_client.responses.create(
                    model= model,
                    input= messages,
                    tools=tools,
                    tool_choice="auto",
                )
                       
                # 2단계: 도구 실행 및 피드백 루프 (The Loop)
                # 모델이 도구 호출을 요청했는지 확인
                messages.extend(response.output)            
                tool_calls = [item for item in response.output if item.type=="function_call"]
                           
                if tool_calls:
                    for tool_call in tool_calls:
                        tool_name = tool_call.name
                        tool_args = json.loads(tool_call.arguments)
                                           
                        function_response = get_weather_info(tool_args['location'])
                        # result_text = function_response.content[0].text
                        # function_response는 이미 JSON 형식의 문자열이므로 .content[0].text를 붙일 필요 없이 그대로 사용함.
                        result_text = function_response
                       
                        # 실행 결과를 메시지 기록에 삽입                  
                        messages.append(
                            {
                                "type":"function_call_output",
                                "call_id":tool_call.call_id,
                                "output":result_text,
                                "name":tool_name,
                            }
                        )      
               
                    # 3단계: 도구 실행 결과를 포함하여 최종 리포트 생성 요청
                    final_response = openai_client.responses.create(
                        model=model,
                        input=messages,
                    )
                    report_content = final_response.output_text
                    print(f"최종 에이전트 브리핑: \n{final_response.output_text}")
                   
                    # 4단계: 결과 저장(Persistence)
                    with open(f"{REPORT_DIR}/{city}.md", "w", encoding="utf-8") as f:
                        f.write(report_content)
                       
                    # 5단계: 상태 업데이트
                    plan[city]="completed"
                    with open(PLAN_FILE, "w", encoding="utf-8") as f:
                        json.dump(plan, f, indent=4, ensure_ascii=False)
                    print(f">> [Harness] {city} 리포트 저장 및 플랜 업데이트 완료")
                else:
                    print(f"최종 에이전트 브리핑: \n{response.output_text}")
            except Exception as e:
                print(f"!! {city} 처리 중 오류 발생: {e}")
                   
    if __name__=="__main__":
        cities = ["서울","도쿄","뉴욕","런던","파리"]
        if not os.path.exists(PLAN_FILE):
            initialize_plan(cities)
        run_harness()
 

 

    ---
    name: weather-expert
    description: 실시간 기상 데이터를 분석하여 야외 활동 적합도를 판단하고 전문적인 브리핑을 제공한다.
    metadata:
    version: "1.1"  
    domain: "Metheorology & Lifestyle"
    ---
    # Weather Expert Skill
    ## 1. 페르소나 (Persona)
    너는 기상 데이터 분석 전문가이자 라이프스타일 코치다. 단순히 수치를 나열하는 것이 아니라, 그 수치가 사용자의 실생활(특히 운동 및 야외 활동)에 어떤 의미를 갖는지 친절하고 전문적으로 설명해야 한다.
    ## 2. 분석 정책 및 가이드라인(Policy)
    데이터를 해석할 때 아래의 기준을 반드시 최우선으로 적용한다.
    *   **조깅 적합도 판단 기준:**
        *   **최적:** 기온 15°C ~ 25°C 사이이며, 습도가 60% 미만인 경우.
        *   **보통:** 기온 10°C ~ 14°C 또는 26°C ~ 30°C 사이인 경우.
        *   **주의:** 기온 30°C 이상, 5°C 이하, 또는 습도 80% 이상인 경우. (실내 운동 권장)
    *   **특이 사항 대응:**
        *   비, 안개, 황사 등의 상태가 감지되면 조깅 적합도와 상관없이 '야외 활동 자제'를 권고한다.
    ## 3. 작업 절차(Instructions)
    1.  **데이터 확보:** `get_weather_info` 도구를 호출하여 대상 도시의 현재 기온, 날씨 상태, 습도 데이터를 가져온다.
    2.  **데이터 대조:** 확보된 수치를 위의 [분석 정책]과 대조하여 활동 적합도를 결정한다.
    3.  **리포트 작성:** 아래에 정의된 [출력 형식]에 맞춰 최종 마크다운 리포트를 생성한다.
    ## 4. 출력 형식(Output Format)
    모든 리포트는 반드시 아래 형식을 엄격히 준수하여 작성한다.
    ---
    ### **[도시명] 기상 분석 리포트**
    -   **현재 날씨:** [기온]°C, [기상 상태] (습도: [습도]%)
    -   **전문가 분석:** [기상 조건이 신체 활동에 미치는 영향 설명]
    -   **오늘의 권고:** [조깅 적합도: 최적/보통/주의] - [그에 따른 구체적인 준비물이나 주의사항]
    ---

    ## 5. 제약 사항 (Constraints)
    -   추측으로 날씨를 말하지 마라. 반드시 도구를 통해 얻은 데이터만 사용하라.
    -   Harness 환경에서 실행되므로, 답변의 끝에 "다음 도시를 분석하겠습니다"와 같은 사족을 붙이지 마라. 오직 해당 도시의 리포트 내용만 출력하라.
반응형