Skip to main content

W&B Weave로 MCP 및 A2A 에이전트 평가하기

이 글은 AI 번역본입니다. 오역이 있으면 댓글로 알려주세요.
Created on September 12|Last edited on September 12
~할 때 AI 기반 에이전트 점점 더 정교해지고 상호 연결성이 높아짐에 따라, 개발자들은 다양한 플랫폼과 벤더 전반에서 원활한 협업, 가시성, 통합을 보장하는 과제에 직면합니다. 두 가지 주요 프로토콜—Anthropic의 Model Context Protocol (MCP) 그리고 Google의 Agent2Agent (A2A)—표준화되고 상호 운용 가능한 기반을 마련하고 있으며 에이전트 간 통신과 컨텍스트 공유. 그러나, 구축하고 견고한 에이전트형 시스템 모니터링 여전히 복잡한 과제로 남아 있습니다.
W&B Weave 이 과제를 획기적으로 단순화합니다. 통합된 트레이싱 그리고 MCP용 관측 가능성(A2A는 곧 지원 예정)까지 제공하는 Weave는 에이전트 시스템에 대한 즉시 사용 가능한 가시성을 제공하여 에이전트형 AI 애플리케이션을 평가, 모니터링, 디버깅하고 반복 개선할 수 있게 해줍니다.
이 글에서는 전체적인 개요를 제공합니다 MCP 그리고 A2A에이전트 시스템을 구축할 때 마주하는 어려움을 논의하고, Weave가 에이전트 개발에 구조와 인사이트를 어떻게 제공하는지 보여드립니다.


목차



Model Context Protocol(MCP)란 무엇인가요?

Model Context Protocol은 다음 기관에서 개발한 오픈 표준입니다 앤트로픽 방식을 통합하기 위해 LLM 애플리케이션 외부 도구, 데이터, 프롬프트와 상호작용합니다. MCP는 AI 에이전트를 위한 USB 포트처럼 동작하여, 실시간 데이터 소스, API, 외부 액션에 안전하게 접근할 수 있는 범용 인터페이스를 제공합니다. 각 서비스마다 커스텀 통합을 만들 필요가 없습니다.
MCP는 세 가지 핵심 원시 구성 요소를 중심으로 컨텍스트를 구성합니다:
  • 도구: 모델이 실행할 수 있는 외부 함수(예: 코드 실행, 이메일 전송, API 조회).
  • 리소스: 모델이 참조할 수 있는 구조화된 데이터(예: 문서, 데이터베이스 레코드, 로그).
  • 프롬프트LLM의 동작을 일관되고 효율적으로 만들기 위해 구조를 제공하고 지침을 제시하는 사전 정의된 템플릿.
이러한 리소스의 기술 방식과 접근 방식을 표준화함으로써, MCP는 LLM 기반 애플리케이션이 공통 인프라를 공유하도록 하여 복잡성과 중복을 줄입니다. 결국 MCP 규격을 준수하는 어떤 클라이언트든 어떤 MCP 서버의 기능도 활용할 수 있게 되어, 에이전트형 AI가 고립된 섬에서 연결된 생태계로 확장되는 과정에서 상호운용성, 보안, 확장성이 향상됩니다.

Agent2Agent이란 무엇이며 MCP와는 어떻게 함께 작동하나요?

Agent2Agent(A2A)는 보완적인 과제를 해결하기 위해 Google이 도입한 개방형 표준으로, 다음을 가능하게 합니다: 자율형 AI 에이전트 서로 다른 플랫폼과 다양한 제공업체 위에서 구축된 에이전트들이 손쉽게 발견하고, 소통하며, 협업할 수 있도록 합니다. MCP가 LLM을 위한 컨텍스트 접근과 구조화된 도구 사용에 초점을 맞추는 반면, A2A는 오케스트레이션멀티 에이전트 워크플로우, 보안 통신, 그리고 8 MCP와 A2A?"
Anthropic의 Model Context Protocol(MCP)과 Google의 Agent2Agent(A2A)는 모두 AI 에이전트의 상호 운용성을 확보하기 위해 설계된 개방형 표준��지만, 서로 다른 과제를 다루며 에이전트 스택의 서로 다른 계층에서 작동합니다.
MCP는 단일 에이전트를 연결하는 데 초점을 맞춥니다 (종종 언어 모델로 구동되는) 어시스턴트를 외부 도구, 구조화된 데이터, 미리 정의된 프롬프트에 연결합니다. 이는 API, 파일, 함수와 같은 리소스에 접근하고 실행하기 위한 표준화된 프레임워크를 제공합니다. MCP를 사용하면 AI 애플리케이션은 실시간 정보를 얻거나 외부 작업을 수행하기 위해 개별 맞춤 커넥터의 누더기 대신 보편적인 프로토콜을 통해 기존 인프라를 손쉽게 활용할 수 있습니다.
대조적으로 A2A는 자율 에이전트들 간의 소통과 협업을 가능하게 하는 데 초점을 둡니다., 잠재적으로 서로 다른 팀이 만들었거나 별도의 플랫폼에서 실행되는 에이전트들까지 포함합니다. A2A의 핵심 혁신 중 하나는 Agent Card를 통한 에이전트 검색 가능성입니다. 모든 에이전트는 자신의 기술, 지원하는 모달리티, 인증 요구 사항, 연결 세부 정보를 알리는 기계가 읽을 수 있는 “Agent Card”를 공개합니다. 다른 에이전트들은 이러한 Agent Card를 조회하고 이해하여 특정 작업에 적합한 파트너를 동적으로 찾고, 통신 매개변수를 협상하며, 업무 위임을 조율할 수 있습니다. 이는 에이전트가 조직이나 벤더의 경계를 넘어 원활하게 발견하고, 상호 작용하며, 협력할 수 있도록 하는 공통 언어와 프로토콜 집합을 형성합니다.
요약하면, MCP는 에이전트를 도구와 데이터 소스에 연결하는 범용 “어댑터” 역할을 하며, A2A는 Agent Card를 통한 프로토콜과 자기 서술 메커니즘을 제공해 에이전트들이 서로를 안전하게 찾아 협업할 수 있도록 합니다. 두 표준을 함께 사용하면, 개별적으로는 확장 가능하고 팀 단위로는 상호 운용 가능한, 풍부한 컨텍스트 인식형 에이전트 애플리케이션을 구현할 수 있습니다.

W&B Weave의 MCP 통합

W&B Weave는 MCP 프로토콜을 기본적으로 지원하여 Anthropic 표준을 기반으로 한 에이전트형 애플리케이션을 손쉽게 평가하고 모니터링하며 반복 개선할 수 있게 해줍니다. MCP 서버와 클라이언트를 위한 즉시 사용 가능한 트레이싱을 통해 별도의 커스텀 계측 없이도 MCP로 구축된 에이전트에서 엔드 투 엔드 트레이스를 자동으로 수집할 수 있습니다.
보통 개발자는 개별 함수나 엔드포인트를 수동으로 래핑해야 합니다 @weave.op 자세한 트레이스와 텔레메트리를 Weave 안에서 수집하려면 데코레이터가 필요합니다. 예를 들어 Python에서 MCP 기반 서버를 실행하는 경우, 일반적으로 Weave를 임포트하고 초기화한 뒤, 추적하려는 함수—예를 들어 외부 도구 호출이나 리소스 페치—에 데코레이터를 적용하여 에이전트 워크플로 전반에 걸친 포괄적인 가시성을 보장합니다.
하지만 Weave의 사전 구축된 MCP 통합을 사용하면 이 과정이 간소화됩니다. MCP 서버와 MCP 클라이언트의 시작 지점에서 Weave를 임포트하고 초기화하기만 하면 됩니다. 한 번 활성화하면 Weave가 모든 MCP 호출과 상호 작용을 자동으로 추적하여, 에이전트형 워크플로 전반에 걸친 실행 흐름, 입·출력 파라미터, 성능 데이터를 끊김 없이 수집합니다. 즉, MCP 규격을 준수하는 에이전트가 사용하는 모든 도구 호출, 리소스 접근, 프롬프트가 Weave에서 확인 가능해지며, 추가 데코레이터나 트레이싱 로직으로 코드를 어지럽히지 않고도 깊이 있는 인사이트를 얻을 수 있습니다.
이 간소화된 방식은 수작업을 없애줄 뿐만 아니라 에이전트형 시스템 전반에서 일관되고 세밀한 관측 가능성을 보장합니다. MCP 기반 에이전트를 반복·개선·확장하는 과정에서 Weave는 실제 동작 모니터링, 문제 디버깅, 성능 최적화를 손쉽게 해주며, 동시에 시스템 변경 이력도 명확하게 유지합니다. 이전 버전과 현재 버전의 트레이스를 비교함으로써 진행 상황을 추적하고, 개선 효과를 검증하며, 에이전트형 애플리케이션을 자신 있게 발전시킬 수 있습니다. 요약하면, Weave는 이제 모든 MCP 에이전트에 마찰 없는 가시성과 실행 가능한 인사이트를 제공하여, 현대적인 AI 시스템에서 관측 가능성과 거버넌스의 기준을 한층 끌어올립니다.

데모: Weave로 MCP와 A2A 결합하기

이제 Weave의 MCP 통합을 활용하면서 A2A와 MCP의 강점을 함께 결합하는 방법을 시연하겠습니다. 이 데모에서는 A2A와 MCP의 동작 원리를 상세히 다루지 않으므로, 두 프로토콜이 처음이라면 제가 제작한 다른 튜토리얼에서 사용 방법을 확인해 보세요. MCP 그리고 A2A. 이 튜토리얼에서는 데모 중 하나를 실행했다는 전제하에 진행합니다 A2A 리포지토리에 있는 통화 변환 에이전트 여기에이전트는 다음 명령으로 실행할 수 있습니다 (내부에서). A2A/samples/python/agents A2A 디렉터리) :"
uv run . --host 0.0.0.0 --port 1001
포트 1001에서 A2A 에이전트를 실행 중이라면, 다음 단계는 FastMCP 라이브러리를 사용해 MCP 서버를 구축하는 것입니다. 이 MCP 서버는 브리지 역할을 하며, MCP 클라이언트(예: Claude Desktop 또는 기타 LLM 앱)에게 하나의 툴을 노출합니다. 이 툴은 내부적으로 사용 가능한 A2A 에이전트를 검색하고, 각 작업에 가장 적합한 에이전트를 선택한 뒤, 요청과 응답을 적절히 중계합니다. 이 서버에서 Weave를 초기화하면 모든 MCP 툴 호출이 자동으로 트레이싱되어, 에이전트가 어떻게 동작하는지 가시성을 제공합니다!
다음은 Weave를 활용하면서 A2A와 MCP를 통합하는 방법을 보여주는 간단한 스크립트입니다! 스크립트의 다음 줄에서 api 키를 반드시 본인의 키로 교체하세요: os.environ["GEMINI_API_KEY"] = "your_gemini_api_key"
import asyncio
import os
import uuid
import logging
import base64
from typing import Optional, List, Dict

import httpx
from litellm import completion

from mcp.server.fastmcp import FastMCP
import weave

weave.init("wv_mcp")

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

AGENT_CARD_PATH = "/.well-known/agent.json"
HOST = "localhost"
START_PORT = 1000
END_PORT = 1010
MODEL_NAME = "gemini/gemini-2.0-flash"



# --- export/set API key for downstream libraries (if not already set elsewhere) ---
os.environ["GEMINI_API_KEY"] = "your_gemini_api_key"

def create_routing_prompt(history: List[Dict], agents: List[Dict]) -> List[Dict]:
agent_list = "\n".join(
f"- {a['card'].get('name', 'Unknown')}: {a['card'].get('description', '')}" for a in agents
)
convo = []
for msg in history:
role = "User" if msg["role"] == "user" else "Agent"
convo.append(f"{role}: {msg['text']}")
convo_str = "\n".join(convo)
content = (
"You are an expert router for user requests. "
"Given the following agents:\n"
f"{agent_list}\n\n"
"Below is the full conversation so far. "
"Pay closest attention to the user's most recent request at the end. "
"Choose the single best matching agent by exact name to handle the user's request, IN CONTEXT of the conversation.\n\n"
f"Conversation:\n{convo_str}\n\nAgent name:"
)
return [{"role": "user", "content": content}]

def make_agent_instruction(history: List[Dict], agent_card: Dict) -> str:
convo = "\n".join(
f"User: {msg['text']}" if msg["role"] == "user" else f"Agent: {msg['text']}"
for msg in history
)
agent_desc = agent_card.get('description', 'No agent description provided.')
agent_name = agent_card.get('name', '[Agent name missing]')
return (
f"You are the agent: {agent_name}.\n"
f"Your job: {agent_desc}\n\n"
"Conversation so far:\n"
f"{convo}\n\n"
"Based on this conversation, take the user's intended action or answer their last request, using all necessary info from the dialog.\n"
"Return only the task/instruction that the agent will accept ."
)

async def fetch_agent_card(host: str, port: int) -> Optional[Dict]:
url = f"http://{host}:{port}{AGENT_CARD_PATH}"
try:
async with httpx.AsyncClient(timeout=3) as client:
resp = await client.get(url)
resp.raise_for_status()
card = resp.json()
logger.info(f"Agent found: {card.get('name', '[no-name]')} on {host}:{port}")
return {"host": host, "port": port, "card": card}
except Exception as e:
logger.debug(f"No agent at {host}:{port} ({e})")
return None


async def discover_agents(host: str, start_port: int, end_port: int) -> List[Dict]:
tasks = [fetch_agent_card(host, port) for port in range(start_port, end_port + 1)]
results = await asyncio.gather(*tasks)
return [r for r in results if r]


async def send_instruction_to_agent(agent: Dict, instruction: str) -> Optional[Dict]:
url = f"http://{agent['host']}:{agent['port']}/"
jsonrpc_id = str(uuid.uuid4())
payload = {
"jsonrpc": "2.0",
"id": jsonrpc_id,
"method": "tasks/send",
"params": {
"id": jsonrpc_id,
"message": {
"role": "user",
"parts": [
{"type": "text", "text": instruction}
]
},
"acceptedOutputModes": ["text/plain",],
}
}
try:
async with httpx.AsyncClient(timeout=30) as client:
resp = await client.post(url, json=payload)
resp.raise_for_status()
data = resp.json()
return data
except Exception as e:
logger.error(f"Error sending query to agent: {e}")
return None

def extract_agent_text(agent_response: Dict) -> str:
"""Extracts text from agent response artifacts."""
result = agent_response.get("result", {})
artifacts = result.get("artifacts", [])
texts = []
for artifact in artifacts:
for part in artifact.get("parts", []):
if part.get("type") == "text":
texts.append(part.get("text", ""))
return "\n".join(texts)

# # ------ MCP TOOL WRAPPER ------


def extract_agent_content(agent_response: Dict) -> Dict:
"""
Extracts texts and images from agent response artifacts.

Returns a dict:
{
"texts": [ ... ],
"images": [ { "filename": ..., "bytes": ..., "type": ... }, ... ]
}
Images are decoded but also stored as bytes (and filenames if saved locally).
"""
result = agent_response.get("result", {})
artifacts = result.get("artifacts", [])
texts = []
images = []

for artifact in artifacts:
for part in artifact.get("parts", []):
ptype = part.get("type")
if ptype == "text":
texts.append(part.get("text", ""))
elif ptype in ("image/png", "image/jpeg", "image/jpg"):
b64data = part.get("text") or part.get("bytes") or ""
if not b64data:
logger.warning("Image artifact part missing data")
continue
try:
image_bytes = base64.b64decode(b64data)
ext = "png" if "png" in ptype else "jpg"
filename = f"agent_image_{uuid.uuid4().hex}.{ext}"
# Optional: save to file
with open(filename, "wb") as f:
f.write(image_bytes)
images.append({"filename": filename, "bytes": image_bytes, "type": ptype})
texts.append(filename)
except Exception as e:
logger.error(f"Failed to decode/save image: {e}")
elif ptype == "file":
file_info = part.get("file", {})
b64data = file_info.get("bytes")
mime = file_info.get("mimeType", "")
if not b64data:
logger.warning("'file' artifact missing 'bytes' data")
continue
try:
file_bytes = base64.b64decode(b64data)
ext = "bin"
if "png" in mime:
ext = "png"
elif "jpeg" in mime or "jpg" in mime:
ext = "jpg"
filename = f"agent_file_{uuid.uuid4().hex}.{ext}"
with open(filename, "wb") as f:
f.write(file_bytes)
images.append({"filename": filename, "bytes": file_bytes, "type": mime})
texts.append(filename)
except Exception as e:
logger.error(f"Failed to decode/save file artifact: {e}")
# (You can add more media/part types as needed)

return {"texts": texts}


# `history`: [{"role": "user"|"agent", "text": "..."} ..]
# Returns: Dict with "agent":..., "response": ..., and updated "history"

async def run_a2a_router(query: str, history: Optional[List[Dict]] = None) -> Dict:
"""
Runs a2a multi-agent router: discovers agents, routes, relays, and returns agent response.
"""
# 1. Discover agents
agents = await discover_agents(HOST, START_PORT, END_PORT)
if not agents:
return {"error": "No agents discovered."}

if history is None:
history = []

history = history.copy()
history.append({"role": "user", "text": query})

# 2. Route to best agent via LLM
prompt_messages = create_routing_prompt(history, agents)
response = completion(
model=MODEL_NAME,
messages=prompt_messages,
max_tokens=16,
temperature=0.0,
)
agent_name = response.get("choices", [{}])[0].get("message", {}).get("content", "").strip()
matched_agent = None
for agent in agents:
if agent["card"].get("name", "").lower() == agent_name.lower():
matched_agent = agent
break
if not matched_agent:
matched_agent = agents[0]

# 3. Build agent instruction and relay
agent_instruction = make_agent_instruction(history, matched_agent["card"])
agent_response = await send_instruction_to_agent(matched_agent, agent_instruction)
if not agent_response:
return {"error": f"Agent '{matched_agent['card'].get('name')}' did not return a valid response."}

reply_text = extract_agent_text(agent_response)
if reply_text:
history.append({"role": "agent", "text": reply_text})

return {
"agent": matched_agent["card"].get("name"),
"response": reply_text,
"history": history,
"raw_response": agent_response,
}



mcp = FastMCP("mcp_server")

@mcp.tool()
async def handle_route_to_agents_with_a2a(query: str, history: Optional[List[Dict]] = None) -> Dict:
"""use this tool for any queries requiring up to date information"""
return await run_a2a_router(query, history)



if __name__ == "__main__":
mcp.run(transport='stdio')

이 설정을 사용하면, MCP 클라이언트(예: Claude Desktop)가 호출할 때마다 handle_route_to_agents_with_a2a 툴을 호출하면, 서버는 네트워크에 등록된 모든 A2A 에이전트를 자동으로 탐색하고, LLM 기반 선택을 통해 최적의 에이전트를 고르기 위한 라우팅 프롬프트를 생성한 뒤, 대화 기록을 포함한 요청을 중계합니다. 선택된 에이전트가 작업을 실행하고, 응답은 MCP 프로토콜을 통해 다시 전달됩니다.
Weave 통합 덕분에, 에 대한 모든 요청은 handle_route_to_agents_with_a2a Weave 플랫폼에서 도구 호출이 추적되고 시각화됩니다. 전체 실행 트레이스를 탐색하고, 에이전트 협업 성능을 모니터링하며, 시간이 지남에 따라 멀티 에이전트 인프라의 다양한 버전을 비교할 수도 있습니다.
Weave의 가시성과 함께 엮인 A2A와 MCP 프로토콜의 융합은 상호운용성이 높고 투명성이 뛰어난 에이전트 시스템을 구축하고 확장하기 위한 매끄러운 경로를 제공합니다. 워크플로 병목을 조사하든, 에이전트 라우팅 결정을 분석하든, 배포 전반의 개선 사항을 추적하든, 이 아키텍처는 강건한 에이전트 애플리케이션을 자신 있게 출시할 수 있도록 통찰과 제어력을 제공합니다.
이제 Claude Desktop 내부에서 시스템을 직접 시험해 볼 수 있습니다.


에이전트가 쿼리를 완료하면 Weave로 이동해 에이전트와의 상호작용에 대한 트레이스를 확인할 수 있습니다. 이제 Weave가 MCP와 직접 통합되었기 때문에 weave를 임포트하고 초기화하기만 하면 MCP 툴 호출이 Weave 내부에 자동으로 기록됩니다.


Weave의 강력함

MCP 서버를 개발하는 분이라면 Weave를 강력히 추천합니다. Weave는 숨은 성능 문제를 드러내는 정교한 트레이싱을 제공하고, 에이전트 선택 로직을 개선하며, 실제 환경에서의 멀티 에이전트 시스템 동작을 훨씬 더 명확하게 파악할 수 있게 해줍니다.
당신이 ~할 때 멀티 에이전트 시스템 구축진짜 중요한 일은 단순히 무언가가 응답하게 만드는 데 그치지 않습니다. 핵심은 설계를 반복 개선하는 것입니다. 에이전트 상호작용 방식을 다듬고, 선택 로직을 바꾸고, 프롬프트 템플릿을 개선하고, 폴백 전략을 조정하고, 네트워크 통신을 최적화한 뒤, 그런 변화가 수많은 실제 대화 전반에서 실제로 어떤 행동 변화를 일으키는지 지켜보는 일입니다.
정교한 트레이싱이 없으면 그 반복 과정은 느리고 추측에 의존하게 됩니다. 무언가를 조금 수정하고, 몇 가지를 수동으로 시험해 보고, 개선되었기를 바랄 뿐이죠. Weave를 사용하면 모든 에이전트 전반의 모든 툴 호출이 전체 맥락과 함께 캡처됩니다. 어떤 에이전트들이 발견되었는지, 왜 특정 에이전트가 선택되었는지, 작업 라우팅에 어떤 프롬프트가 사용되었는지, 각 응답에 걸린 시간은 얼마였는지, 최종 산출물이 무엇이었는지까지 모두 확인할 수 있습니다.
이 덕분에 빠르게 움직일 수 있습니다. 실패했거나 느린 워크플로를 추적하고, 문제가 발생한 지점을 정확히 파고들어(잘못된 선택, 에이전트 오류, 느린 응답) 수정한 뒤 재배포하고, 새로운 동작이 더 나아졌는지 즉시 검증할 수 있습니다. 이제 변경 사항마다 커스텀 로깅을 만들거나 에이전트를 수동으로 모니터링할 필요가 없습니다. 시스템 전체가 기본 설정만으로 바로 관측 가능하기 때문입니다.
여러 작업이나 팀 전반으로 에이전트를 확장할 때, 이런 형태의 피드백 루프는 그저 동작하는 것을 출시하느냐, 신뢰할 수 있고 확장 가능한 것을 출시하느냐를 가르는 핵심 차이입니다. Weave는 직관과 기대에 의존하던 반복을, 눈으로 확인 가능한 실제 엔지니어링 프로세스로 바꿔줍니다.

W&B MCP 서버 데모

이제 Weave와 MCP의 통합을 보여드렸으니, W&B에서 프로젝트를 이해하는 아주 훌륭한 새로운 방법을 소개하고자 합니다. Weights & Biases에는 이제 MCP 서버 프로젝트에 대한 정보를 조회할 수 있도록 지원합니다. 시작하려면 해당 리포지토리에서 서버를 다운로드하고, JSON 구성 파일을 다음에 추가해야 합니다. claude_desktop_config.json 파일. 서버 설정을 마친 뒤에는 Claude Desktop 같은 LLM 클라이언트를 실행해 특정 프로젝트에 대한 질의를 보낼 수 있습니다.
데모를 위해 먼저 W&B 모델을 사용해 몇 번의 테스트 실행을 해보겠습니다. CIFAR-10 데이터셋에서 여러 이미지 분류 모델의 퓨샷 일반화 성능을 간단히 시험해 보겠습니다. 실행에 사용할 코드는 다음과 같습니다.
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
import numpy as np
import wandb
from collections import defaultdict

def get_fewshot_loader(dataset, num_per_class, batch_size=32):
"""Returns a DataLoader with num_per_class images per class."""
targets = np.array(dataset.targets)
idxs = []
class_counts = defaultdict(int)
for idx, label in enumerate(targets):
if class_counts[label] < num_per_class:
idxs.append(idx)
class_counts[label] += 1
if all(class_counts[k] >= num_per_class for k in range(10)):
break
sampler = torch.utils.data.Subset(dataset, idxs)
return torch.utils.data.DataLoader(sampler, batch_size=batch_size, shuffle=True, num_workers=2)

def evaluate(model, dataloader, device):
model.eval()
correct, total = 0, 0
with torch.no_grad():
for x, y in dataloader:
x, y = x.to(device), y.to(device)
logits = model(x)
pred = logits.argmax(1)
correct += (pred == y).sum().item()
total += len(y)
return correct / total

def main():
wandb.init(project="cifar10-fewshot-eval")

# Use GPU if available
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

# Pretrained models and their final layer info
models_info = {
'resnet18': {
'model': torchvision.models.resnet18(weights='DEFAULT'),
'final_layer': ('fc', 512)
},
'densenet121': {
'model': torchvision.models.densenet121(weights='DEFAULT'),
'final_layer': ('classifier', 1024)
},
'vgg16': {
'model': torchvision.models.vgg16(weights='DEFAULT'),
'final_layer': ('classifier', 4096, 6) # index 6 in classifier
},
'efficientnet_b0': {
'model': torchvision.models.efficientnet_b0(weights='DEFAULT'),
'final_layer': ('classifier', 1280)
}
}

transform = transforms.Compose([
transforms.Resize(224),
transforms.ToTensor(),
transforms.Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]
)
])

trainset = torchvision.datasets.CIFAR10(
root='./data', train=True, download=True, transform=transform)
testset = torchvision.datasets.CIFAR10(
root='./data', train=False, download=True, transform=transform)
testloader = torch.utils.data.DataLoader(
testset, batch_size=128, shuffle=False, num_workers=2)

fewshot_loader = get_fewshot_loader(trainset, num_per_class=10, batch_size=16)

for name, info in models_info.items():
print(f"\nTraining and evaluating {name} ...")
model = info['model']
# Replace final layer for CIFAR-10
if name == 'vgg16':
clf = model.classifier
clf[6] = nn.Linear(info['final_layer'][1], 10)
model.classifier = clf
else:
setattr(model, info['final_layer'][0], nn.Linear(info['final_layer'][1], 10))
model.to(device)

# Only train the head
for param in model.parameters():
param.requires_grad = False
if name == 'vgg16':
for param in model.classifier[6].parameters():
param.requires_grad = True
else:
for param in getattr(model, info['final_layer'][0]).parameters():
param.requires_grad = True

optimizer = optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr=0.01)
criterion = nn.CrossEntropyLoss()

# Quick training (2 epochs)
model.train()
for epoch in range(2):
total_loss = 0
for x, y in fewshot_loader:
x, y = x.to(device), y.to(device)
optimizer.zero_grad()
logits = model(x)
loss = criterion(logits, y)
loss.backward()
optimizer.step()
total_loss += loss.item() * len(y)
avg_loss = total_loss / len(fewshot_loader.dataset)
print(f" Epoch {epoch+1}: avg loss {avg_loss:.4f}")

# Eval
acc = evaluate(model, testloader, device)
print(f"{name}: Test set accuracy after few-shot training: {acc:.4f}")
wandb.log({f"{name}_fewshot_acc": acc})

wandb.finish()

if __name__ == "__main__":
main()

하이퍼파라미터가 퓨샷 일반화 성능에 큰 영향을 줄 수 있다는 점을 고려하면, 이 실험이 과학적으로 가장 엄밀하다고 말하기는 어렵다는 것을 먼저 인정합니다. 실험을 실행한 뒤에는 Claude Desktop으로 이동해 해당 프로젝트의 결과를 간단히 조회할 수 있습니다.
다음으로 Claude는 W&B MCP 서버의 도구들을 사용해 프로젝트에 대한 추가 정보를 확인합니다.



내부적으로는 Claude가 MCP 도구 호출을 보내고, 서버가 Weights & Biases의 API에 쿼리를 실행해 프로젝트 메타데이터를 가져옵니다. 이 쿼리 결과는 이후 Claude 채팅 내부에서 자동으로 파싱되고 요약되며, UI나 대시보드를 수동으로 뒤질 필요가 없습니다.
다음으로, 결과를 문서화하는 보고서 작성 과정까지 자동화할 수 있습니다!

Claude가 생성한 W&B 리포트 모델 성능을 요약합니다. 이 리포트에는 각 테스트 모델의 퓨샷 정확도 점수 세부 항목, 결과를 시각화한 막대 차트, 그리고 발견 사항을 바탕으로 한 명확한 다음 단계 권고안이 포함됩니다.


이런 유형의 도구는 확실히 AI 연구의 미래처럼 느껴집니다. 머지않아 AI 에이전트에게 여러 개의 독창적이고 높은 품질의 연구 아이디어를 구상하고, 그 아이디어들을 검증할 실험을 설계하며, 궁극적으로 새로운 AI 돌파구를 만들어 내라고 간단히 지시할 수 있게 되길 바랍니다. 이런 종류의 에이전트가 앞으로 어떻게 진화할지 기대됩니다!

결론

AI 시스템이 더 복잡한 멀티 에이전트 아키텍처로 진화할수록 가시성, 상호운용성, 그리고 빠른 반복의 필요성은 그 어느 때보다 커집니다. MCP와 A2A 같은 표준은 플랫폼 전반에서 에이전트와 도구를 연결하는 기반을 제공하지만, 진정으로 확장 가능하고 이해 가능한 시스템을 구축하려면 여전히 강력한 관측 가능성이 필요합니다.
Weave가 그 간극을 메워 줍니다. MCP 워크플로우 트레이싱, 에이전트 라우팅, 도구 호출, 그리고 이제는 Weights & Biases 프로젝트 쿼리까지 1급으로 지원하여, Weave는 개발자가 견고한 멀티 에이전트 시스템을 더 빠르게 출시하는 데 필요한 통제력을 제공합니다. 자체 텔레메트리 스택을 구축하는 대신, 에이전트 동작 개선, 라우팅 로직 정교화, 실험 가속화에 집중할 수 있으며, 그 과정에서 시스템의 진화를 완전하고 검색 가능한 이력으로 남길 수 있습니다.
Weave의 네이티브 A2A 지원도 곧 제공될 예정이지만, 오늘 당장 A2A 시스템에 Weave를 통합하고 싶다면 애플리케이션 시작 시 Weave를 임포트하고 초기화한 뒤 사용하면 됩니다 @weave.op 추적하고 싶은 모든 함수를 간단히 래핑하면 됩니다. 단순한 LLM 앱이든 완전한 멀티 에이전트 워크플로우든, Weave는 트레이스를 손쉽게 수집하고, 실제 동작을 모니터링하며, 더 빠르게 반복할 수 있게 해줍니다. LLM 기반 에이전트, 자율 워크플로우를 다루거나 MCP와 A2A 위에서 구축하고 있다면, Weave는 스택에 반드시 포함해야 할 관측성 플랫폼입니다.


이 글은 AI로 번역된 기사입니다. 오역이 의심되는 부분은 댓글로 자유롭게 신고해 주세요. 원문 보고서 링크는 다음과 같습니다: 원문 보고서 보기