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 시리즈의 장점을 계승하면서도, 에이전트 워크플로우에 특화된 세 가지 핵심 개선사항을 도입했다.

  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-4oClaude 3.5 SonnetQwen3.6-Plus
BFCL-v2Tool Calling71.2%68.4%74.1%
AgentBenchMulti-turn Agent82.3%79.6%83.7%
GSM8KMath Reasoning92.0%88.7%91.4%
HumanEvalCode Generation90.2%92.0%89.6%
MMLU-ProGeneral Knowledge85.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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
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":
 "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 에이전트를 구축해보자.

  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
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
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 = "\n".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"\nThought: {step.thought}"
            prompt += f"\nAction: {step.action}"
            prompt += f"\nAction Input: {step.action_input}"
            prompt += f"\nObservation: {step.observation}\n"
        
        prompt += f"\nQuestion: {query}\n"
        return prompt
    
    def _parse_response(self, response: str) -> AgentStep:
        """모델 응답 파싱"""
        thought_match = re.search(r"Thought:\s*(.+?)(?=\n|$)", response)
        action_match = re.search(r"Action:\s*(\w+)", response)
        action_input_match = re.search(r"Action Input:\s*(.+?)(?=\nObservation|\nThought|\nFinal|$)", 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.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
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 = "\n".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
# 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}\n")

프로덕션 체크리스트:

항목권장 설정비고
추론 엔진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

이 글은 AI를 사용해 생성 또는 보조 작성되었습니다. 보안·기술 정보는 인용된 원문과 공식 자료에서 다시 확인하세요.