1 배경
멀티 Agent 프로젝트를 개발 중이다. 현재 POC 단계이며, 3개의 메인 Agent를 개발하고 있다.
- QnA Chatbot: RAG 기반 도메인 Q&A
- Data Standardizer: 용어 추출 및 한/영 논리명 생성
- Code Analyzer: 코드 구조 분석 (개발 예정)
- Code Standardizer: 코드 표준화 (추후 개발 예정)
최종 목표는 이 Agent들을 추상화하여, 특정 계약 아래에 Agent가 찍혀져 나오는 플랫폼으로 확장하는 것이다.
이 글에서는 스킬 역분석에서 추출한 8개 프롬프트 작성 원리를 실제 프로젝트에 적용하는 방법을 다룬다.
2 현재 프롬프트 관리 상태
2.1 프로젝트 구조 (프롬프트 관련)
src/agent/
├── prompts/
│ ├── chatbot_data_standardization_rules.yml # 350줄 단일 파일
│ ├── data_standardizer.yml # 단일 파일
│ └── code_analyzer.yml # 비어있음
├── config/
│ └── settings.py # PromptConfig 포함
└── pages/
├── 1_QnA_Chatbot.py # load_prompt()로 YAML 로드
├── 2_Data_Standardizer.py
└── 3_Code_Analyzer.py
2.2 현재 프롬프트 로딩 패턴
2.3 현재 YAML 프롬프트 구조
_type: "prompt"
domain: "Data Standardization"
name: "chatbot_data_standardization_rules"
template: |
## Role
당신은 PCR 분자진단 전문가입니다.
## Instructions
(350줄에 걸친 모든 지시사항이 하나의 template 안에...)
{chat_history}
{context}
{question}
input_variables: ["question", "context", "chat_history"]핵심 문제: Agent당 YAML 파일 1개, 모든 지시사항이 하나의 template 필드에 들어있는 모놀리식 구조이다.
3 문제 진단: 8개 원리 대입
이전 글에서 추출한 8개 원리를 현재 프로젝트에 대입하면:
| 원리 | 현재 상태 | 문제 |
|---|---|---|
| 1. 트리거 기반 description | 없음 | Agent가 3개 → N개로 늘어나면 라우팅 불가 |
| 2. XML 의미론적 태그 | 없음 | 350줄 프롬프트에서 역할별 구분이 안 됨 |
| 3. WRONG → CORRECT 대조 | 부분적 (few-shot만) | 흔한 실수 교정 패턴 없음 |
| 4. 의사결정 테이블 | 없음 | 분기 판단을 자연어로 서술 |
| 5. boundaries | 없음 | Agent 행동 범위 무제한 |
| 6. 프로그레시브 디스클로저 | 없음 | 모든 지시를 한번에 로드 |
| 7. 최소 완전 예시 | few-shot 존재 | 하지만 과도하게 길음 |
| 8. 컨텍스트별 분기 | 없음 | 모든 상황에 동일 프롬프트 |
4 3단계 진화 전략
4.1 Phase 1: 현재 POC에 즉시 적용
파일 구조를 바꾸지 않고, 기존 YAML 프롬프트의 내용만 개선한다.
4.1.1 Before (현재)
4.1.2 After (개선)
_type: "prompt"
domain: "Data Standardization"
name: "chatbot_data_standardization_rules"
description: "INVOKE when the user asks questions about PCR molecular
diagnostics, data standardization rules, or terminology standards.
Covers domain Q&A, citation-based answers, and standardization
guideline lookup."
template: |
<role>
당신은 PCR 분자진단 데이터 표준화 전문가이다.
</role>
<response-format>
모든 응답은 다음 구조를 따른다:
1. Introduction: 질문 핵심 요약 (1-2문장)
2. Body: 상세 답변 (근거 문서 인용 포함)
3. Conclusion: 핵심 정리
4. Related Questions: 후속 질문 2-3개
</response-format>
<citation-rules>
| 상황 | 처리 |
|------|------|
| 문서에 근거가 있음 | [출처: 문서명, 페이지] 형식으로 인용 |
| 문서에 근거가 없음 | "제공된 문서에서 관련 내용을 찾지 못했다" |
| 부분적 근거 | 근거 있는 부분만 인용, 나머지는 명시 |
</citation-rules>
<fix-hallucination>
# WRONG: 문서에 없는 내용을 생성
"PCR 검사의 민감도는 일반적으로 95% 이상이다."
# CORRECT: 문서 기반으로만 답변
"제공된 문서에 따르면, [문서명]에서 PCR 검사의 민감도에 대해
다음과 같이 기술하고 있다: [인용]"
</fix-hallucination>
<fix-out-of-scope>
# WRONG: 범위 밖 질문에 일반 지식으로 답변
"COVID-19 백신의 효과는..."
# CORRECT: 범위 제한 명시
"이 질문은 PCR 분자진단 데이터 표준화 범위에 포함되지 않는다.
제공된 문서 내에서 관련 내용을 찾지 못했다."
</fix-out-of-scope>
<boundaries>
### 할 수 있는 것
- 제공된 문서 기반 Q&A
- 데이터 표준화 규칙 설명
- 용어/코드 표준 안내
### 할 수 없는 것
- 문서에 없는 의학적 판단
- 환자 진단 관련 조언
- 문서 외 출처의 정보 제공
</boundaries>
#Chat History:
{chat_history}
#Context:
{context}
#Question:
{question}
#Answer:
input_variables: ["question", "context", "chat_history"]4.1.3 적용한 원리
| 변경 사항 | 적용 원리 |
|---|---|
description에 트리거 조건 추가 |
원리 1: 트리거 기반 description |
<role>, <response-format> 등 태그 도입 |
원리 2: 의미론적 XML 태그 |
<fix-hallucination>, <fix-out-of-scope> 추가 |
원리 3: WRONG → CORRECT 대조 |
<citation-rules>에 테이블 사용 |
원리 4: 의사결정 테이블 |
<boundaries> 섹션 추가 |
원리 5: boundaries |
Phase 1은 기존 YAML 파일의 template 내용만 수정하면 된다. 코드 변경 없이 프롬프트 품질을 올릴 수 있는 가장 빠른 방법이다.
4.2 Phase 2: Agent 3~4개 완성 시 — 스킬 분리
Agent가 늘어나면 공통 스킬과 Agent별 스킬을 분리한다.
4.2.1 디렉토리 구조
src/agent/prompts/
├── _base/ # 공통 스킬 (모든 Agent가 공유)
│ ├── response-format.yml # 응답 구조 규칙
│ ├── citation-rules.yml # 인용 규칙
│ └── error-handling.yml # 에러/범위 밖 처리
│
├── chatbot/ # QnA Chatbot 전용
│ ├── _manifest.yml # 스킬 목록 + description (Level 0)
│ ├── domain-qa.yml # 도메인 Q&A 스킬
│ └── terminology-lookup.yml # 용어 조회 스킬
│
├── data-standardizer/ # Data Standardizer 전용
│ ├── _manifest.yml
│ ├── term-extraction.yml # 용어 추출
│ ├── naming-rules-korean.yml # 한국어 논리명 규칙
│ └── naming-rules-english.yml # 영어 논리명 규칙
│
├── code-analyzer/ # Code Analyzer 전용
│ ├── _manifest.yml
│ ├── structure-analysis.yml # 코드 구조 분석
│ ├── docstring-generation.yml # 독스트링 생성
│ └── quality-check.yml # 코드 품질 검사
│
└── code-standardizer/ # Code Standardizer 전용 (예정)
├── _manifest.yml
├── naming-convention.yml
└── refactoring-patterns.yml
4.2.2 _manifest.yml (Level 0)
Agent의 스킬 목록과 트리거 조건을 정의한다. LangChain Skills의 AGENTS.md에 해당한다.
# src/agent/prompts/data-standardizer/_manifest.yml
agent: data-standardizer
description: "데이터 표준화 Agent. 용어 추출, 한/영 논리명 생성, 코드 매핑을 수행한다."
skills:
- name: term-extraction
description: "INVOKE when the user provides raw text, terms, or data
fields that need to be identified and extracted for standardization."
- name: naming-rules-korean
description: "INVOKE when generating Korean logical names (한국어 논리명)
for extracted terms. Covers naming conventions, abbreviation rules."
- name: naming-rules-english
description: "INVOKE when generating English logical names for extracted
terms. Covers camelCase, snake_case, and domain-specific abbreviations."
base_skills:
- response-format
- citation-rules
- error-handling
input_variables: ["question", "context"]4.2.3 개별 스킬 파일 예시
# src/agent/prompts/data-standardizer/term-extraction.yml
name: term-extraction
description: "INVOKE when the user provides raw text, terms, or data fields
that need to be identified and extracted for standardization."
template: |
<overview>
텍스트에서 표준화 대상 용어를 식별하고 추출한다. 추출된 용어에 대해
후보 표준명과 설명을 함께 제공한다.
</overview>
<when-to-use>
| 이 스킬을 사용할 때 | 다른 스킬을 사용할 때 |
|---------------------|---------------------|
| 원문 텍스트에서 용어를 추출할 때 | 이미 추출된 용어의 논리명을 생성할 때 |
| 데이터 필드명을 식별할 때 | 코드에서 변수명을 분석할 때 |
</when-to-use>
<ex-term-extraction>
입력: "환자의 Ct value가 35 이하이면 양성으로 판정한다"
출력:
| 추출 용어 | 유형 | 후보 표준명 |
|-----------|------|-------------|
| Ct value | 측정값 | cycle_threshold_value |
| 양성 | 판정결과 | positive |
</ex-term-extraction>
<fix-over-extraction>
# WRONG: 일반 단어까지 추출
"환자" → 추출 대상 아님 (도메인 표준화 대상이 아닌 일반 명사)
# CORRECT: 도메인 특화 용어만 추출
"Ct value" → 추출 대상 (PCR 진단 도메인 전문 용어)
</fix-over-extraction>
<boundaries>
### 할 수 있는 것
- 텍스트에서 도메인 전문 용어 식별
- 용어 유형 분류 (측정값, 판정결과, 시약명 등)
- 후보 표준명 제안
### 할 수 없는 것
- 최종 표준명 확정 (naming-rules 스킬에서 처리)
- 용어 간 관계 매핑
</boundaries>4.2.4 스킬 로더 구현
# src/agent/prompts/loader.py
import yaml
from pathlib import Path
from langchain_core.prompts import PromptTemplate
PROMPTS_DIR = Path(__file__).parent
def load_manifest(agent_name: str) -> dict:
"""Level 0: Agent의 스킬 목록을 로드한다."""
manifest_path = PROMPTS_DIR / agent_name / "_manifest.yml"
with open(manifest_path, encoding="utf-8") as f:
return yaml.safe_load(f)
def load_skill(agent_name: str, skill_name: str) -> str:
"""Level 2: 특정 스킬의 전체 내용을 로드한다."""
skill_path = PROMPTS_DIR / agent_name / f"{skill_name}.yml"
with open(skill_path, encoding="utf-8") as f:
return yaml.safe_load(f)["template"]
def load_base_skill(skill_name: str) -> str:
"""공통 스킬을 로드한다."""
skill_path = PROMPTS_DIR / "_base" / f"{skill_name}.yml"
with open(skill_path, encoding="utf-8") as f:
return yaml.safe_load(f)["template"]
def compose_prompt(
agent_name: str,
skill_names: list[str],
) -> PromptTemplate:
"""선택된 스킬들을 조합하여 최종 프롬프트를 생성한다."""
manifest = load_manifest(agent_name)
# 공통 스킬 로드
parts = []
for base_skill in manifest.get("base_skills", []):
parts.append(load_base_skill(base_skill))
# Agent 전용 스킬 로드
for skill_name in skill_names:
parts.append(load_skill(agent_name, skill_name))
template = "\n\n".join(parts)
return PromptTemplate(
template=template,
input_variables=manifest.get(
"input_variables", ["question", "context"]
),
)4.2.5 기존 코드에서의 사용
# pages/2_Data_Standardizer.py (변경 후)
from agent.prompts.loader import compose_prompt
# 필요한 스킬만 선택하여 프롬프트 조합
prompt = compose_prompt(
agent_name="data-standardizer",
skill_names=["term-extraction", "naming-rules-korean"],
)
chain = (
RunnablePassthrough.assign(
context=lambda x: retriever.invoke(x["question"]),
question=lambda x: x["question"],
)
| prompt
| llm
| StrOutputParser()
)Phase 2의 핵심은 프롬프트를 기능 단위로 분리하여 재사용하는 것이다. _base/ 공통 스킬은 모든 Agent가 공유하므로, 응답 형식이나 인용 규칙을 한 곳에서 관리할 수 있다.
4.3 Phase 3: 플랫폼 확장 — 계약 기반 Agent 생성
Agent가 “찍혀져 나오는” 플랫폼에서는 스킬 레지스트리 + 계약(contract) 기반 조합이 필요하다.
4.3.1 플랫폼 디렉토리 구조
platform/
├── skill-registry/ # 전체 스킬 저장소
│ ├── _base/ # 공통 스킬
│ │ ├── response-format.yml
│ │ ├── citation-rules.yml
│ │ └── error-handling.yml
│ ├── domain/ # 도메인별 스킬
│ │ ├── pcr-diagnostics/
│ │ │ ├── terminology.yml
│ │ │ └── test-codes.yml
│ │ ├── genomics/
│ │ └── clinical-data/
│ └── capability/ # 기능별 스킬
│ ├── rag-qa/
│ ├── data-standardization/
│ │ ├── term-extraction.yml
│ │ ├── naming-rules-korean.yml
│ │ └── naming-rules-english.yml
│ ├── code-analysis/
│ └── code-standardization/
│
├── contracts/ # 계약 정의
│ ├── client-a-pcr-standardizer.yml
│ ├── client-b-genomics-chatbot.yml
│ └── ...
│
└── agent-factory/ # Agent 생성기
└── factory.py
4.3.2 스킬 레지스트리의 설계 원칙
스킬을 도메인(domain)과 기능(capability)으로 분리한다.
도메인 스킬: "무엇에 대해 아는가"
→ pcr-diagnostics, genomics, clinical-data
기능 스킬: "무엇을 할 수 있는가"
→ rag-qa, data-standardization, code-analysis
이 분리가 중요한 이유:
| 조합 | 결과 |
|---|---|
| pcr-diagnostics + rag-qa | PCR 진단 Q&A 챗봇 |
| pcr-diagnostics + data-standardization | PCR 용어 표준화 Agent |
| genomics + rag-qa | 유전체학 Q&A 챗봇 |
| genomics + code-analysis | 유전체학 코드 분석 Agent |
도메인 x 기능 = Agent. 새로운 고객이 들어오면 도메인 스킬만 추가하고, 기존 기능 스킬을 재사용할 수 있다.
4.3.3 계약(Contract) 파일
계약 파일은 특정 고객의 Agent를 정의하는 명세서이다.
# contracts/client-a-pcr-standardizer.yml
contract:
name: "Client A PCR Data Standardizer"
client: "Client A"
version: "1.0"
agent:
type: data-standardizer
base_skills:
- _base/response-format
- _base/citation-rules
- _base/error-handling
domain_skills:
- domain/pcr-diagnostics/terminology
- domain/pcr-diagnostics/test-codes
capability_skills:
- capability/data-standardization/term-extraction
- capability/data-standardization/naming-rules-korean
- capability/data-standardization/naming-rules-english
boundaries:
can:
- "PCR 분자진단 관련 용어 표준화"
- "한/영 논리명 생성"
- "표준 코드 매핑"
cannot:
- "의학적 판단"
- "환자 데이터 직접 처리"
- "다른 도메인 용어 표준화"
config:
llm:
model: gpt-4.1
temperature: 0.3
retrieval:
k: 6
search_type: hybrid4.3.4 Agent Factory
# platform/agent-factory/factory.py
import yaml
from pathlib import Path
from langchain_core.prompts import PromptTemplate
REGISTRY_DIR = Path("platform/skill-registry")
def load_contract(contract_path: str) -> dict:
with open(contract_path, encoding="utf-8") as f:
return yaml.safe_load(f)
def load_skill_from_registry(skill_ref: str) -> str:
"""레지스트리에서 스킬을 로드한다."""
skill_path = REGISTRY_DIR / f"{skill_ref}.yml"
with open(skill_path, encoding="utf-8") as f:
return yaml.safe_load(f)["template"]
def generate_boundaries_skill(boundaries: dict) -> str:
"""계약의 boundaries를 스킬 형식으로 변환한다."""
can_items = "\n".join(f"- {item}" for item in boundaries["can"])
cannot_items = "\n".join(f"- {item}" for item in boundaries["cannot"])
return f"""<boundaries>
### 할 수 있는 것
{can_items}
### 할 수 없는 것
{cannot_items}
</boundaries>"""
def create_agent_from_contract(contract_path: str):
"""계약 파일로부터 Agent를 생성한다."""
contract = load_contract(contract_path)
agent_spec = contract["agent"]
# 모든 스킬 참조를 수집
all_skill_refs = (
agent_spec.get("base_skills", [])
+ agent_spec.get("domain_skills", [])
+ agent_spec.get("capability_skills", [])
)
# 스킬 로드
parts = [load_skill_from_registry(ref) for ref in all_skill_refs]
# boundaries 추가
if "boundaries" in agent_spec:
parts.append(generate_boundaries_skill(agent_spec["boundaries"]))
# 프롬프트 조합
template = "\n\n".join(parts)
prompt = PromptTemplate(
template=template,
input_variables=["question", "context"],
)
# LLM 설정
llm_config = agent_spec.get("config", {}).get("llm", {})
return {
"prompt": prompt,
"llm_config": llm_config,
"retrieval_config": agent_spec.get("config", {}).get("retrieval", {}),
}4.3.5 사용 예시
5 3단계 진화 요약
Phase 1 (POC) Phase 2 (멀티 Agent) Phase 3 (플랫폼)
┌──────────────┐ ┌──────────────────┐ ┌──────────────────────┐
│ 단일 YAML │ │ _base/ (공통) │ │ skill-registry/ │
│ 파일 내에 │ → │ agent/ (전용) │ → │ _base/ │
│ 태그 구조화 │ │ _manifest.yml │ │ domain/ │
│ │ │ loader.py │ │ capability/ │
│ │ │ │ │ contracts/ │
│ │ │ │ │ agent-factory/ │
└──────────────┘ └──────────────────┘ └──────────────────────┘
코드 변경 없음 스킬 분리 + 로더 도메인 x 기능 = Agent
프롬프트만 개선 공통 스킬 재사용 계약 기반 자동 생성
| Phase | 시점 | 작업 | 효과 |
|---|---|---|---|
| 1 | 즉시 | 기존 YAML에 태그/테이블/boundaries 추가 | 할루시네이션 감소, 범위 제한 |
| 2 | Agent 3~4개 완성 시 | 공통/전용 스킬 분리, 로더 구현 | 스킬 재사용, 일관된 품질 |
| 3 | 플랫폼 확장 시 | 스킬 레지스트리 + 계약 시스템 | 고객별 Agent 자동 생성 |
각 Phase는 이전 Phase의 결과물을 포함한다. Phase 2에서 분리한 스킬 파일이 Phase 3의 레지스트리에 그대로 들어간다. 처음부터 Phase 3를 구축할 필요 없이, Phase 1부터 점진적으로 진화시키면 된다.
6 즉시 실행 가능한 액션
| 우선순위 | 작업 | 적용 원리 | 효과 |
|---|---|---|---|
| 1 | 기존 YAML에 <role>, <boundaries>, <fix-...> 태그 추가 |
원리 2, 3, 5 | 할루시네이션 감소, 범위 이탈 방지 |
| 2 | description을 트리거 기반으로 변경 | 원리 1 | 추후 멀티 Agent 라우팅 준비 |
| 3 | 인용/범위 판단을 의사결정 테이블로 변환 | 원리 4 | 판단 정확도 향상 |
| 4 | WRONG → CORRECT 대조 3~5개 추가 | 원리 3 | 가장 빈번한 실수 교정 |
| 5 | few-shot 예시를 최소 완전 예시로 축소 | 원리 7 | 컨텍스트 윈도우 절약 |
1~5번은 기존 YAML 파일 수정만으로 즉시 적용 가능하다.
7 참고
- LangChain Skills GitHub — 스킬 패턴 원본
- 스킬 역분석: Agent용 시스템 프롬프트 작성 원리 — 8개 원리 상세
- 스킬 기반 Agent 패턴 — 배경 및 설치