Instruction file imported from ChangooLee/opas (
.cursor/rules/agent-development.mdc). Copyright stays with the author.
Agent Development Rules (legacy product reference — not an adapter)
Not authoritative.
AGENTS.mdis the canonical contract (precedence: AGENTS.md > adaptersproduct refs). This is opt-in product guidance only, inherited from agent-portal. Some examples below reference agents removed under milestone M2/SCOPE (dart, legislation) — treat those as historical. For system facts see
agentops/docs/system-reference.md.
Rules for developing AI agents with proper GenAI monitoring patterns.
Required Reference Documents
⚠️ CRITICAL: 에이전트 개발 시 반드시 다음 문서를 참조하세요:
- GenAI 모니터링 표준: docs/OTEL_GENAI_METRICS.md
- 에이전트 통합 가이드: docs/AGENT_INTEGRATION_GUIDE.md
- 에이전트 모니터링 가이드: docs/AGENT_MONITORING.md
GenAI Monitoring Pattern (MANDATORY)
1. Root Agent Session Span
모든 에이전트는 반드시 gen_ai.session span으로 시작해야 합니다:
from opentelemetry import trace
from opentelemetry.propagate import inject
# Root span 생성
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span(
"gen_ai.session", # GenAI 표준: root agent session
attributes={
"service.name": "agent-{agent_type}",
"gen_ai.agent.id": agent_id,
"gen_ai.agent.name": agent_name,
"gen_ai.agent.type": agent_type,
"agent.id": agent_id, # 호환성
"agent.name": agent_name, # 호환성
}
) as root_span:
# Context를 carrier에 주입하여 하위 span에 전달
carrier = {}
inject(carrier)
# State에 carrier 저장 (LangGraph의 경우)
state["otel_carrier"] = carrier
# 에이전트 실행
result = await agent.run(...)
2. LLM Calls with GenAI Standard
LLM 호출 시 반드시 다음 패턴을 사용하세요:
from app.services.litellm_service import litellm_service
from opentelemetry.propagate import inject
# 1. Parent context를 carrier로 변환
trace_headers = {}
if state.get("otel_carrier"):
trace_headers = state.get("otel_carrier", {})
else:
# Root span이 없으면 새로 생성
inject(trace_headers)
# 2. LiteLLM 호출 시 trace_headers 전달
response = await litellm_service.chat_completion_sync(
model=model,
messages=messages,
trace_headers=trace_headers, # Context propagation
metadata={
"agent_id": agent_id,
"agent_name": agent_name,
"node": node_name,
"trace_id": trace_id
}
)
중요: LiteLLM은 자동으로 gen_ai.content.completion span을 생성하고, trace_headers를 통해 parent context를 연결합니다.
3. Agent Node Spans
LangGraph 노드에서 span을 생성할 때:
from contextlib import contextmanager
from opentelemetry import trace
from opentelemetry.propagate import extract, inject
from opentelemetry.context import attach, detach
@contextmanager
def start_agent_span(
name: str,
attributes: Optional[Dict[str, Any]] = None,
parent_carrier: Optional[Dict[str, str]] = None
):
"""Agent 노드용 span 생성 (Context Propagation 지원)"""
tracer = trace.get_tracer(__name__)
span_attributes = {
"service.name": "agent-{agent_type}",
"gen_ai.agent.id": agent_id,
"gen_ai.agent.name": agent_name,
"gen_ai.agent.type": agent_type,
}
if attributes:
span_attributes.update(attributes)
# GenAI 표준 속성 자동 추가
if "gen_ai.agent.id" not in span_attributes:
span_attributes["gen_ai.agent.id"] = span_attributes.get("agent.id", agent_id)
if "gen_ai.agent.name" not in span_attributes:
span_attributes["gen_ai.agent.name"] = span_attributes.get("agent.name", agent_name)
try:
from opentelemetry.context import attach, detach
# Parent context 추출 및 attach
token = None
if parent_carrier:
parent_context = extract(parent_carrier)
if parent_context:
token = attach(parent_context)
try:
with tracer.start_as_current_span(name, attributes=span_attributes) as span:
yield span
finally:
if token is not None:
detach(token)
except ImportError:
yield _NoOpSpan()
except Exception as e:
logger.warning(f"Failed to create span {name}: {e}")
yield _NoOpSpan()
4. Tool Call Spans
도구 호출 시:
with tracer.start_as_current_span(
"gen_ai.tool.call", # GenAI 표준
attributes={
"gen_ai.tool.name": tool_name,
"gen_ai.operation.name": "tool_call",
"tool.arguments": json.dumps(arguments),
"gen_ai.agent.id": agent_id,
"gen_ai.agent.name": agent_name,
}
) as span:
# 도구 호출
result = await tool.call(arguments)
# 결과 기록
span.set_attribute("tool.result", json.dumps(result))
span.set_attribute("tool.success", True)
LangGraph Agent Pattern
LangGraph 기반 에이전트 개발 시:
# graph.py
class MyGraphAgent:
async def run(self, question: str, trace_id: Optional[str] = None):
initial_state = create_initial_state(question=question)
# Root span 생성
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span(
"gen_ai.session",
attributes={
"service.name": "agent-my-agent",
"gen_ai.agent.id": "my-agent",
"gen_ai.agent.name": "My Agent",
"gen_ai.agent.type": "my-agent",
}
) as root_span:
# Context를 state에 저장
carrier = {}
inject(carrier)
initial_state["otel_carrier"] = carrier
# 그래프 실행
final_state = await self.graph.ainvoke(initial_state)
return final_state
# nodes.py
async def my_node(state: MyState) -> MyState:
# Parent carrier 사용
with start_agent_span(
"gen_ai.agent.my_node",
attributes={"node": "my_node"},
parent_carrier=state.get("otel_carrier")
):
# LLM 호출 시 trace_headers 전달
trace_headers = state.get("otel_carrier", {})
response = await litellm_service.chat_completion_sync(
model=model,
messages=messages,
trace_headers=trace_headers,
metadata={"agent_id": "my-agent", "node": "my_node"}
)
# ...
Checklist (MANDATORY)
에이전트 개발 시 반드시 확인:
- Root span이
gen_ai.session으로 생성되는가? -
gen_ai.agent.id,gen_ai.agent.name,gen_ai.agent.type속성이 설정되는가? - LLM 호출 시
trace_headers를 전달하여 context propagation이 되는가? - 모든 노드 span이 parent carrier를 사용하여 같은 trace_id를 공유하는가?
- 도구 호출 시
gen_ai.tool.callspan을 사용하는가? - 모니터링 화면에서 하나의 요청이 하나의 trace로 표시되는가?
Common Mistakes
❌ WRONG: Root span 없이 시작
# ❌ 잘못된 예
async def run(self, question: str):
# Root span 없이 바로 노드 실행
result = await self.graph.ainvoke(state)
❌ WRONG: Context propagation 없이 LLM 호출
# ❌ 잘못된 예
response = await litellm_service.chat_completion_sync(
model=model,
messages=messages
# trace_headers 없음 → 다른 trace_id로 분리됨
)
❌ WRONG: GenAI 표준 속성 누락
# ❌ 잘못된 예
with tracer.start_as_current_span(
"my_agent.run", # gen_ai.session이 아님
attributes={
"agent.id": "my-agent" # gen_ai.agent.id 누락
}
) as span:
pass
✅ CORRECT: GenAI 표준 패턴
# ✅ 올바른 예
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span(
"gen_ai.session", # GenAI 표준
attributes={
"service.name": "agent-my-agent",
"gen_ai.agent.id": "my-agent",
"gen_ai.agent.name": "My Agent",
"gen_ai.agent.type": "my-agent",
}
) as root_span:
carrier = {}
inject(carrier)
state["otel_carrier"] = carrier
# LLM 호출 시 context propagation
response = await litellm_service.chat_completion_sync(
model=model,
messages=messages,
trace_headers=carrier, # Context propagation
metadata={"agent_id": "my-agent"}
)
Reference Examples
- Slide Studio:
backend/app/services/slide_studio/orchestrator/metrics.py - Text2SQL:
backend/app/agents/text2sql/metrics.py - DART Agent:
backend/app/agents/dart_agent/metrics.py - Legislation Agent:
backend/app/agents/legislation_agent/metrics.py
Why LLM Calls Were Missing
원인 분석:
- LiteLLM은 자동으로
litellm_requestspan을 생성하지만, parent context가 없으면 별도의 trace_id를 생성 trace_headers를 전달하지 않으면 context propagation이 실패- 결과적으로 에이전트 span과 LLM span이 다른 trace_id를 가지게 됨
해결책:
- Root span에서
inject(carrier)로 context를 추출 - LLM 호출 시
trace_headers=carrier전달 - LiteLLM이 자동으로 parent context를 인식하여 같은 trace_id 사용
Last Updated: 2025-12-23 Reference: docs/OTEL_GENAI_METRICS.md