서론
“AI 에이전트가 실제 환경에서 제대로 동작하지 않는다.”
이것은 지난 2년간 수많은 개발자들이 겪어온 좌절의 순간이다. GPT-4 같은 강력한 LLM을 에이전트 시스템에 통합해보았지만, 막상 실행해보면 도구 호출이 꼬이고, 멀티턴 대화 중 컨텍스트가 유실되며, 복잡한 추론이 필요한 작업에서 엉뚱한 결과를 내뱉는 상황이 반복되었다.
필자 역시 지난 프로젝트에서 이 문제에 직면했다. RAG 시스템과 연동된 에이전트를 구축했는데, 사용자가 세 번째 질문을 던지면 첫 번째 질문의 맥락이 증발했다. 도구 호출 시나리오에서는 더했다—API 호출 순서가 뒤섞이거나, 필요하지 않은 매개변수를 넘기거나, 아예 존재하지 않는 함수를 호출하려 시도했다.
Qwen3.6-Plus는 바로 이 문제를 해결하기 위해 설계되었다. 단순한 텍스트 생성 모델이 아니라, 처음부터 “실제 환경 에이전트(Real-World Agent)” 구현을 염두에 두고 훈련된 모델이다. 이 글에서는 Qwen3.6-Plus의 아키텍처를 분석하고, 실제 에이전트 시스템에 어떻게 통합할 수 있는지 단계별로 살펴본다.
Qwen3.6-Plus의 핵심 혁신
아키텍처 개요
Qwen3.6-Plus는 기존 Qwen 시리즈의 장점을 계승하면서도, 에이전트 워크플로우에 특화된 세 가지 핵심 개선사항을 도입했다.
1
2
3
4
5
6
7
8
9
10
11
12
13
| graph TD
A[User Input] --> B[Intent Analysis]
B --> C{Reasoning Required?}
C -->|Yes| D[Chain-of-Thought]
C -->|No| E[Direct Response]
D --> F[Tool Selection]
F --> G[Parameter Extraction]
G --> H[Tool Execution]
H --> I[Result Integration]
I --> J[Response Generation]
E --> J
J --> K[Context Memory Update]
K --> L[Final Output]
|
위 다이어그램은 Qwen3.6-Plus의 추론 파이프라인을 간략화한 것이다. 핵심은 동적 추론 경로 선택이다. 모든 쿼리에 대해 무조건적인 Chain-of-Thought를 수행하는 것이 아니라, 문제의 복잡도에 따라 적절한 경로를 선택한다.
성능 비교: 주요 벤치마크
Qwen3.6-Plus는 다양한 벤치마크에서 GPT-4급 성능을 보여준다. 특히 에이전트 관련 태스크에서 두드러진 성능을 기록했다.
| 벤치마크 | 측정 영역 | GPT-4o | Claude 3.5 Sonnet | Qwen3.6-Plus | | :— | :— | :— | :— | :— | | BFCL-v2 | Tool Calling | 71.2% | 68.4% | 74.1% | | AgentBench | Multi-turn Agent | 82.3% | 79.6% | 83.7% | | GSM8K | Math Reasoning | 92.0% | 88.7% | 91.4% | | HumanEval | Code Generation | 90.2% | 92.0% | 89.6% | | MMLU-Pro | General Knowledge | 85.7% | 84.2% | 86.1% |
가장 주목할 점은 BFCL-v2 (Berkeley Function Calling Leaderboard) 결과다. Tool Calling 정확도에서 GPT-4o를 3%p 이상 앞서며, 이는 실제 에이전트 구현 시 얼마나 안정적으로 외부 도구를 호출할 수 있는지를 보여주는 핵심 지표다.
Qwen3.6-Plus의 Tool Calling은 단순한 함수 호출을 넘어선다. **의미론적 도구 선택(Semantic Tool Selection)**과 **매개변수 타입 추론(Parameter Type Inference)**을 결합한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
| from transformers import AutoModelForCausalLM, AutoTokenizer
import json
# Qwen3.6-Plus 로드 (실제 배포 시에는 vLLM 권장)
model_name = "Qwen/Qwen3.6-Plus"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype="auto",
device_map="auto"
)
# 도구 정의 (OpenAI Function Calling 형식 호환)
tools = [
{
"type": "function",
"function": {
"name": "search_database",
"description": "Search company database for employee information",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query (name, department, or employee ID)"
},
"filters": {
"type": "object",
"properties": {
"department": {"type": "string"},
"status": {"type": "string", "enum": ["active", "inactive"]}
}
},
"limit": {
"type": "integer",
"description": "Maximum number of results",
"default": 10
}
},
"required": ["query"]
}
}
},
{
"type": "function",
"function": {
"name": "send_email",
"description": "Send an email to specified recipients",
"parameters": {
"type": "object",
"properties": {
"to": {
"type": "array",
"items": {"type": "string"},
"description": "List of email addresses"
},
"subject": {"type": "string"},
"body": {"type":
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| "string"}
},
"required": ["to", "subject", "body"]
}
}
}
]
# 시스템 프롬프트 구성
system_prompt = """You are a helpful assistant with access to the following tools:
{tools}
When a user asks you to perform a task, determine which tools (if any) should be called.
Output your response in the following format if tool calls are needed:
<tool_call)>
{"name": "tool_name", "arguments": {"arg1": "value1", "arg2": "value2"}}
</tool_call)>
"""
# 사용자 쿼리
user_query = "Find all active employees in the Engineering department and send them an email about tomorrow's meeting at 2pm"
# 메시지 구성
messages = [
{"role": "system", "content": system_prompt.format(tools=json.dumps(tools, indent=2))},
{"role": "user", "content": user_query}
]
# 추론
text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = tokenizer(text, return_tensors="pt").to(model.device)
outputs = model.generate(**inputs, max_new_tokens=512, temperature=0.1)
response = tokenizer.decode(outputs[0], skip_special_tokens=True)
print(response)
|
위 코드를 실행하면 Qwen3.6-Plus는 다음과 같은 출력을 생성한다:
1
2
3
4
5
| <tool_call)>
{"name": "search_database", "arguments": {"query": "", "filters": {"department": "Engineering", "status": "active"}, "limit": 100}}
</tool_call)>
After getting the results, I will send an email to all active Engineering employees about tomorrow's meeting at 2pm.
|
모델이 복잡한 사용자 요청을 분석하여: 1. 먼저 search_database를 호출해야 함을 인식 2. filters 파라미터 내에 중첩된 객체 구조를 정확히 구성 3. 이메일 발송이 검색 결과에 의존적임을 파악하고 적절한 순서 계획
실제 에이전트 구현 가이드
Step-by-Step: ReAct 에이전트 구축
ReAct(Reasoning + Acting) 패턴은 현대적 에이전트 아키텍처의 표준이다. Qwen3.6-Plus로 실제 ReAct 에이전트를 구축해보자.
1
2
3
4
5
6
7
| graph LR
A[Query] --> B[Thought]
B --> C[Action]
C --> D[Observation]
D --> E{Complete?}
E -->|No| B
E -->|Yes| F[Final Answer]
|
Step 1: 기본 에이전트 클래스 정의
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
| import re
from typing import Callable, Dict, List, Any
from dataclasses import dataclass
from enum import Enum
class AgentState(Enum):
THINKING = "thinking"
ACTING = "acting"
OBSERVING = "observing"
FINISHED = "finished"
@dataclass
class AgentStep:
thought: str
action: str
action_input: str
observation: str = ""
class QwenReActAgent:
def __init__(
self,
model,
tokenizer,
tools: Dict[str, Callable],
max_iterations: int = 10
):
self.model = model
self.tokenizer = tokenizer
self.tools = tools
self.max_iterations = max_iterations
self.history: List[AgentStep] = []
def _build_prompt(self, query: str) -> str:
"""ReAct 프롬프트 구성"""
tool_descriptions = "
".join([
f"- {name}: {func.__doc__ or 'No description'}"
for name, func in self.tools.items()
])
prompt = f"""You are a reasoning agent. Use the ReAct framework to answer questions.
Available tools:
{tool_descriptions}
Use the following format:
Thought: Think about what to do next
Action: tool_name
Action Input: input for the tool (JSON format)
Observation: tool result will appear here
... (repeat Thought/Action/Action Input/Observation as needed)
Thought: I now know the final answer
Final Answer: the answer to the original question
Previous steps:
"""
# 이전 스텝 추가
for step in self.history:
prompt += f"
Thought: {step.thought}"
prompt += f"
Action: {step.action}"
prompt += f"
Action Input: {step.action_input}"
prompt += f"
Observation: {step.observation}
"
prompt += f"
Question: {query}
"
return prompt
def _parse_response(self, response: str) -> AgentStep:
"""모델 응답 파싱"""
thought_match = re.search(r"Thought:\s*(.+?)(?=
|$)", response)
action_match = re.
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| search(r"Action:\s*(\w+)", response)
action_input_match = re.search(r"Action Input:\s*(.+?)(?=
Observation|
Thought|
Final|$)", response, re.DOTALL)
return AgentStep(
thought=thought_match.group(1) if thought_match else "",
action=action_match.group(1) if action_match else "",
action_input=action_input_match.group(1).strip() if action_input_match else ""
)
def run(self, query: str) -> str:
"""메인 실행 루프"""
for iteration in range(self.max_iterations):
prompt = self._build_prompt(query)
# 모델 추론
inputs = self.tokenizer(prompt, return_tensors="pt").to(self.model.device)
outputs = self.model.generate(
**inputs,
max_new_tokens=256,
temperature=0.7,
do_sample=True
)
response = self.tokenizer.decode(outputs[0], skip_special_tokens=True)
# 응답에서 새로 생성된 부분만 추출
new_response = response[len(prompt):]
step = self._parse_response(new_response)
# Final Answer 확인
if "Final Answer:" in new_response:
final_match = re.search(r"Final Answer:\s*(.+)", new_response, re.DOTALL)
if final_match:
return final_match.group(1).strip()
# 도구 실행
if step.action and step.action in self.tools:
try:
import json
action_input = json.loads(step.action_input) if step.action_input else {}
observation = str(self.tools[step.action](**action_input))
except Exception as e:
observation = f"Error executing {step.action}: {str(e)}"
step.observation = observation
self.history.
|
1
2
3
4
5
6
7
| append(step)
else:
# 도구가 없거나 잘못된 액션
step.observation = f"Unknown tool: {step.action}"
self.history.append(step)
return "Max iterations reached without final answer."
|
Step 2: 도구 정의 및 에이전트 실행
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| # 실제 도구 함수 정의
def web_search(query: str) -> str:
"""Search the web for information. Input: {'query': 'search term'}"""
# 실제 구현에서는 SerperAPI, Tavily 등 사용
mock_results = {
"Python 3.12": "Python 3.12 was released on October 2, 2023 with major performance improvements.",
"Qwen3.6": "Qwen3.6-Plus is an open-source LLM optimized for agent tasks."
}
return mock_results.get(query, f"Search results for: {query}")
def calculator(expression: str) -> str:
"""Evaluate mathematical expressions. Input: {'expression': 'math expression'}"""
try:
result = eval(expression) # Note: 실제 사용 시 보안 검증 필수
return str(result)
except:
return "Invalid expression"
def get_weather(location: str) -> str:
"""Get current weather for a location. Input: {'location': 'city name'}"""
# 실제 구현에서는 OpenWeatherMap 등 사용
mock_weather = {
"Seoul": "Temperature: 18°C, Condition: Partly cloudy",
"New York": "Temperature: 22°C, Condition: Sunny"
}
return mock_weather.get(location, f"Weather data for {location}: Sunny, 20°C")
# 에이전트 인스턴스 생성
tools = {
"web_search": web_search,
"calculator": calculator,
"get_weather": get_weather
}
agent = QwenReActAgent(model, tokenizer, tools)
# 실행 예시
result = agent.run("What's the weather in Seoul? Also calculate 15 * 7 + 3")
print(f"Final Result: {result}")
|
멀티턴 컨텍스트 관리
Qwen3.6-Plus의 또 다른 강점은 **긴 컨텍스트 윈도우(128K 토큰)**와 효율적인 메모리 관리다. 멀티턴 대화에서도 이전 대화의 맥락을 효과적으로 유지한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
| from typing import List, Dict
import tiktoken
class ConversationMemory:
def __init__(self, max_tokens: int = 100000, model_name: str = "cl100k_base"):
self.messages: List[Dict[str, str]] = []
self.max_tokens = max_tokens
self.encoding = tiktoken.get_encoding(model_name)
def add_message(self, role: str, content: str):
"""메시지 추가"""
self.messages.append({"role": role, "content": content})
self._trim_if_needed()
def _trim_if_needed(self):
"""토큰 한도 초과 시 오래된 메시지 제거 (시스템 메시지는 보존)"""
while self._count_tokens() > self.max_tokens and len(self.messages) > 1:
# 시스템 메시지가 아닌 가장 오래된 메시지 제거
for i, msg in enumerate(self.messages):
if msg["role"] != "system":
self.messages.pop(i)
break
def _count_tokens(self) -> int:
"""전체 메시지 토큰 수 계산"""
total = 0
for msg in self.messages:
total += len(self.encoding.encode(msg["content"]))
total += 4 # 역할 포맷팅 오버헤드
return total
def get_context_window(self) -> List[Dict[str, str]]:
"""현재 컨텍스트 반환"""
return self.messages.copy()
def summarize_and_compress(self, llm_callable) -> str:
"""대화 압축 (요약)"""
if len(self.messages) < 5:
return ""
conversation_text = "
".join([
f"{m['role']}: {m['content']}"
for m in self.messages[1:] # 시스템 메시지 제외
])
summary_prompt = f"""Summarize the following conversation, preserving key facts, decisions, and context:
{conversation_text}
Summary:"""
summary = llm_callable(summary_prompt)
# 요약으로 대체
self.messages = [
self.messages[0], # 시스템 메시지 보존
{"role": "assistant", "content": f"[Previous conversation summary: {summary}]"}
]
return summary
|
1
2
3
4
5
6
7
| # 사용 예시
memory = ConversationMemory(max_tokens=100000)
memory.add_message("system", "You are a helpful AI assistant.")
memory.add_message("user", "My name is Kim and I work at a startup.")
memory.add_message("assistant", "Nice to meet you, Kim! How can I help you today?")
memory.add_message("user", "I need help with Python async programming.")
# ... 대화 계속
|
MLOps 관점: 프로덕션 배포
Qwen3.6-Plus를 프로덕션에 배포할 때는 vLLM 또는 TensorRT-LLM을 사용한 고성능 서빙이 권장된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| # vLLM을 사용한 서빙 예시
from vllm import LLM, SamplingParams
# vLLM으로 모델 로드 (자동 배치 처리, PagedAttention)
llm = LLM(
model="Qwen/Qwen3.6-Plus",
tensor_parallel_size=2, # GPU 2개 사용
max_model_len=32768, # 필요에 따라 조정
gpu_memory_utilization=0.9
)
sampling_params = SamplingParams(
temperature=0.7,
top_p=0.9,
max_tokens=512
)
# 배치 추론
prompts = [
"Explain quantum computing in simple terms.",
"Write a Python function to merge two sorted lists.",
"What are the key differences between SQL and NoSQL databases?"
]
outputs = llm.generate(prompts, sampling_params)
for output in outputs:
print(f"Prompt: {output.prompt}")
print(f"Response: {output.outputs[0].text}
")
|
프로덕션 체크리스트:
| 항목 | 권장 설정 | 비고 | | :— | :— | :— | | 추론 엔진 | vLLM 0.6+ | PagedAttention으로 메모리 효율화 | | GPU 메모리 | A100 80GB × 2 | 또는 H100 80GB × 1 | | 최대 시퀀스 | 32K~128K | 사용 사례에 따라 조정 | | 배치 크기 | 동적 (vLLM 자동 관리) | 최대 처리량을 위해 | | 양자화 | AWQ/GPTQ 4bit | 메모리 제약 시 | | 모니터링 | Prometheus + Grafana | 토큰 처리량, 지연시간 추적 |
결론
핵심 요약
Qwen3.6-Plus는 실제 환경 에이전트 구현이라는 구체적인 목표를 위해 설계된 모델이다. 주요 특징을 정리하면:
- Tool Calling 정확도: BFCL-v2에서 74.1%로 GPT-4o 대비 우수 2. 멀티턴 처리: 128K 컨텍스트 윈도우로 긴 대화 유지 3. 추론 최적화: 동적 경로 선택으로 효율적인 추론 4. 오픈소스: Apache 2.0 라이선스로 상업적 활용 가능
전문가 인사이트
Qwen3.6-Plus의 등장은 “에이전트용 LLM"이라는 새로운 카테고리가 형성되고 있음을 시사한다. 범용 언어모델이 아닌, 에이전트 워크플로우에 특화된 훈련이 이뤄진 것이다. 이는 향후 LLM 선택 시 단순한 벤치마크 점수보다는 구축하려는 시스템의 요구사항에 맞는 모델 선택이 중요해짐을 의미한다.
다만, Qwen3.6-Plus도 완벽하지는 않다. 복잡한 멀티 도구 오케스트레이션이나 실시간 스트리밍 응답에서는 여전히 개선 여지가 있으며, 이는 향후 버전에서 기대해볼 만하다.
참고자료
출처: https://qwen.ai/blog?id=qwen3.6