Qwen3.6-Plus: 실제 환경 에이전트 구현을 위한 LLM 아키텍처 분석

서론

“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 이상 앞서며, 이는 실제 에이전트 구현 시 얼마나 안정적으로 외부 도구를 호출할 수 있는지를 보여주는 핵심 지표다.

Tool Calling 메커니즘 심층 분석

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는 실제 환경 에이전트 구현이라는 구체적인 목표를 위해 설계된 모델이다. 주요 특징을 정리하면:

  1. 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

Hugo로 만듦
JimmyStack 테마 사용 중