Новости и статьи об искусственном интеллекте и нейросетях. Мы собираем и обрабатываем самую актуальную информацию из мира AI. О проекте

Статьи

Локальный AI с Qwen3.6 и MCP: GitHub-ассистент без облаков

В статье показано, как с помощью модели Qwen3.6-35B-A3B и протокола Model Context Protocol (MCP) построить агента-ассистента для работы с GitHub: анализ issue, редактирование кода и создание pull request полностью локально. Приведены готовые примеры через Qwen-Agent и чистый MCP SDK, а также инструкция по написанию кастомного MCP-сервера.

30 июня 2026 г.
12 мин
85
Building Local AI Systems: Qwen3.6 + MCPs

Каждый разработчик, создающий локальные AI-системы, рано или поздно упирается в ограничения. Модель отлично рассуждает, пишет код, отвечает на сложные вопросы. Но она не может всё. Она не умеет выполнять запросы к базе данных, открывать тикеты в GitHub или вызывать ваше внутреннее API. Приходится писать кастомные Python-обёртки для каждого нужного инструмента, вручную склеивать вывод модели с исполнением, и переписывать эти скрипты при каждом изменении API.

Протокол Model Context Protocol (MCP) создан именно для того, чтобы это исправить. Открытый стандарт от Anthropic — универсальный подключаемый протокол для связи AI-моделей с инструментами. Один раз описываете инструмент как MCP-сервер, и любой MCP-совместимый клиент, любая модель, любой фреймворк может найти и вызвать его без единой строчки интеграционного кода для конкретной модели.

Qwen3.6-35B-A3B — самая мощная локальная модель для подобных задач на сегодня. У неё контекстное окно в 262 144 токена, архитектура Mixture of Experts (MoE), которая активирует лишь 3 млрд из 35 млрд параметров за проход (благодаря чему модель умещается на оборудовании, которое не смогло бы запустить плотную 35B-модель), и она специально обучалась и оценивалась на задачах агентного взаимодействия через MCP.

В этой статье мы построим локального GitHub-ассистента для разработчика: агента, который читает открытые issue репозитория, ищет связанный код, пишет исправление и создаёт pull request. Всё работает на вашем оборудовании, через MCP-серверы, без облачной зависимости.

# Знакомство с MCP

Архитектура Qwen3.6-35B-A3B напрямую объясняет, какое оборудование вам понадобится и почему модель так хорошо показывает себя на агентных задачах.

В названии зашифрован ключевой факт: 35 млрд общих параметров, A3B значит, что на каждом прямом проходе активируются 3 млрд. Это модель MoE с 256 экспертами на слой, маршрутизация 8 плюс 1 общий эксперт на токен. Вы получаете знания 35B-модели при вычислительной стоимости 3B-модели. Именно этот компромисс и позволяет ей работать на оборудовании, которое «рухнуло» бы под плотной 35B.

Строение слоёв — главное отличие Qwen3.6 от других MoE-моделей. В каждом блоке 40-слойного стека чередуются 3 слоя Gated DeltaNet и 1 слой Gated Attention. DeltaNet — это механизм линейного внимания; он обрабатывает последовательности эффективнее, чем полное квадратичное внимание, особенно на длинных контекстах. Вкрапления полного Gated Attention обеспечивают глубокое реляционное рассуждение, которого лишено одно лишь линейное внимание. Для агента, работающего с репозиторием в 500 файлов, такое сочетание критично: эффективная обработка длинного контекста плюс точные рассуждения по релевантным участкам.

Контекстное окно — 262 144 токена нативно, с возможностью расширения до 1 010 000 через YaRN-масштабирование. Для агентной работы длина контекста — не вопрос комфорта, а операционное ограничение. Агенту нужно хранить в памяти содержимое файлов, историю вызовов инструментов, многошаговый план и вставлять результаты инструментов обратно в контекст. Большинство 7B и 13B моделей ограничены 8k или 32k токенов. Исчерпание контекста посреди задачи означает, что агент теряет собственную историю и начинает галлюцинировать результаты вызовов.

Qwen3.6 специально обучалась и тестировалась на MCP-ориентированных агентных бенчмарках. Вот две ключевые особенности:

  • Агентное программирование. Работа с фронтендом и рассуждения на уровне репозитория — модель справляется с рефакторингом множества файлов, рассуждая связно, а не изолированно редактируя отдельные файлы.
  • Сохранение рассуждений (Thinking Preservation). Флаг preserve_thinking сохраняет цепочки рассуждений предыдущих шагов в многоходовом диалоге. Когда агент продумывает план на первом шаге, а затем выполняет вызовы инструментов на шагах со второго по пятый, preserve_thinking=True оставляет рассуждения первого шага в KV-кеше. Каждый последующий шаг использует эти рассуждения без затрат на их повторный вывод.

# Системные требования

Существует три реальных пути развёртывания, и выбор полностью зависит от вашего оборудования.

  • GPU-инференс (рекомендуется для производственных агентных нагрузок). Qwen3.6-35B-A3B в bfloat16 требует около 70 ГБ видеопамяти. В квантизации Q4 умещается в ~20-24 ГБ. Одна RTX 4090 (24 ГБ) потянет Q4. Две RTX 3090 с параллелизмом по тензорам тоже справятся с Q4. A100 80 ГБ позволит запустить полную bfloat16-модель.
  • CPU/гибридный режим через KTransformers. KTransformers — доступный путь для разработчиков без 24 ГБ GPU. Вычислительно тяжёлые слои выносятся на GPU, если он есть, остальное работает на CPU. С 64 ГБ оперативной памяти можно запустить Qwen3.6-35B-A3B в пригодной (хотя и более медленной) конфигурации. Задержка ответа составит 30–120 секунд на ход в зависимости от процессора, что приемлемо для фонового анализа репозитория, но не для интерактивной работы с кодом.
  • Меньшие модели для тестирования. Вся схема интеграции MCP, описанная в статье, одинакова для моделей любого размера. Если хотите попробовать, не имея оборудования под 35B-модель, используйте Qwen/Qwen2.5-7B-Instruct через Ollama (ollama pull qwen2.5:7b) или модель Qwen3-8B. API-сервер одинаков, код тот же, а на полную 35B-модель вы всегда можете перейти, когда позволит оборудование.

Программные требования:

# Требуется Python 3.11+
python --version
python -m venv qwen-mcp-env
source qwen-mcp-env/bin/activate  # macOS / Linux
qwen-mcp-env\Scripts\activate    # Windows

# Основные пакеты
pip install \
    "openai>=1.30.0" \
    "qwen-agent>=0.0.10" \
    "mcp>=1.0.0" \
    "httpx>=0.27.0"

# Фреймворки для развёртывания (выберите один)
pip install "vllm>=0.19.0"   # NVIDIA GPU
pip install "sglang>=0.5.10" # NVIDIA GPU (быстрее заполняет длинный контекст)
pip install "ktransformers"   # CPU/гибрид

# Требуется Node.js 18+ для готовых MCP-серверов, запускаемых через npx
node --version

# Локальный запуск Qwen3.6 с OpenAI-совместимым API

Прежде чем подключать MCP-серверы, необходимо запустить сервер инференса. И SGLang, и vLLM предоставляют OpenAI-совместимый API, с которым взаимодействует слой интеграции MCP, — та же поверхность API, только адрес указывается на localhost вместо api.openai.com.

// SGLang (рекомендуется для агентных задач с длинным контекстом)

# Установка SGLang со всеми зависимостями
pip install "sglang[all]>=0.5.10"

# Запуск Qwen3.6-35B-A3B с включёнными парсерами рассуждений и вызовов инструментов.
# --reasoning-parser qwen3 корректно обрабатывает блоки <think>...</think>.
# --tool-call-parser qwen3_coder направляет результаты вызовов инструментов в нужный формат.
# --enable-prefix-caching критично для агентных нагрузок — включает повторное использование KV-кеша
# между шагами, что делает preserve_thinking эффективным на практике.
python -m sglang.launch_server \
    --model-path Qwen/Qwen3.6-35B-A3B \
    --host 0.0.0.0 \
    --port 30000 \
    --reasoning-parser qwen3 \
    --tool-call-parser qwen3_coder \
    --enable-prefix-caching \
    --tp 2  # параллелизм по тензорам на 2 GPU; удалите, если GPU один

// vLLM

pip install "vllm>=0.19.0"

# Эквивалент vLLM с теми же критичными флагами
vllm serve Qwen/Qwen3.6-35B-A3B \
    --host 0.0.0.0 \
    --port 8000 \
    --reasoning-parser qwen3 \
    --tool-call-parser qwen3_coder \
    --enable-prefix-caching-v2 \
    --tensor-parallel-size 2

// Меньшая модель через Ollama

ollama pull qwen2.5:7b
ollama serve
# API Ollama совместим с OpenAI по адресу http://localhost:11434/v1

Когда сервер запущен, проверьте его, прежде чем двигаться дальше:

# Проверка состояния — должен вернуть {"status": "ok"} или подобное
curl http://localhost:30000/health

# Тест эндпоинта чат-дополнений с простым запросом
curl http://localhost:30000/v1/chat/completions \
    -H "Content-Type: application/json" \
    -d '{
        "model": "Qwen/Qwen3.6-35B-A3B",
        "messages": [{"role": "user", "content": "Ответь: готов"}],
        "max_tokens": 10
    }'

Если получили JSON-ответ с массивом choices, сервер готов. Не приступайте к настройке MCP, пока это не заработает. Все последующие проблемы с интеграцией гораздо легче отлаживать, когда вы уверены в надёжности серверной части.

# Как устроен MCP и почему это меняет архитектуру агента

Прежде чем писать код агента, стоит разобраться, что MCP делает на уровне протокола: это понимание предотвратит целый класс ошибок, возникающих, когда MCP воспринимают просто как более навороченный API вызова функций.

MCP — это протокол JSON-RPC 2.0, работающий поверх stdio или HTTP-транспорта. Когда MCP-клиент подключается к серверу, он первым делом вызывает tools/list, чтобы узнать, какие инструменты предоставляет сервер. Каждый инструмент описывается именем, описанием и схемой ввода в формате JSON Schema. Модель читает эту схему — это контракт между моделью и инструментом.

Когда модель хочет вызвать инструмент, она генерирует структурированный объект вызова. MCP-клиент — не модель — фактически выполняет вызов, отправляя запрос tools/call серверу. Сервер выполняет действие и возвращает результат. Клиент вставляет этот результат обратно в диалог как сообщение с ролью tool. Модель читает результат и принимает решение о следующем шаге.

Такое разделение важно. Модель решает, что вызывать и с какими аргументами. Клиент отвечает за исполнение. Сервер делает реальную работу. Ваш код никогда не зашивает инструмент под конкретную модель; вы просто сообщаете клиенту, какие серверы доступны.

Есть два способа использовать MCP с Qwen3.6:

  • Через Qwen-Agent: официальная библиотека qwen_agent автоматически управляет обнаружением инструментов, разбором вызовов, вставкой результатов и многоходовым диалогом. Меньше кода, меньше контроля. Подходит для большинства сценариев.
  • Напрямую через MCP Python SDK: вы самостоятельно управляете агентным циклом с помощью mcp.ClientSession. Больше кода, полная прозрачность каждого сообщения, полный контроль над обработкой ошибок и логикой повторных попыток. Для production-систем, где нужно отслеживать каждый шаг.

Мы рассмотрим оба подхода, начав с Qwen-Agent.

# Сборка локального GitHub-ассистента

Агент последовательно выполняет четыре действия: читает открытые issue в GitHub-репозитории, находит связанный код, пишет исправление и создаёт pull request. Всё локально, всё через MCP.

// Часть 1: Настройка окружения и MCP-серверов

# Установите персональный токен доступа GitHub
# Требуется GitHub MCP-серверу для вызовов API
export GITHUB_TOKEN=ghp_your_token_here

# Готовые MCP-серверы устанавливаются через npx — отдельный шаг установки не нужен.
# npx сделает это при первом использовании, когда агент запустит серверы.
# Проверьте наличие npx: npx --version

Создайте директорию проекта:

mkdir qwen-github-agent
cd qwen-github-agent

// Часть 2: Реализация через Qwen-Agent

Самый быстрый путь к работающему агенту. Qwen-Agent автоматически зацикливает весь процесс.

# github_agent_qwenagent.py
# Предварительно: pip install qwen-agent openai
# Должны быть установлены npm / npx для MCP-серверов
# Переменная окружения GITHUB_TOKEN должна быть задана
# Локальный сервер инференса должен быть запущен (см. предыдущий раздел)
#
# Запуск:
#   python github_agent_qwenagent.py

from qwen_agent.agents import Assistant

# ── Конфигурация сервера ─────────────────────────────────────────────────────
# Укажите адрес вашего локального сервера.
# Измените base_url в соответствии с тем, какой сервер вы запустили:
#   SGLang: http://localhost:30000/v1
#   vLLM:   http://localhost:8000/v1
#   Ollama: http://localhost:11434/v1
LLM_CONFIG = {
    "model": "Qwen/Qwen3.6-35B-A3B",
    "model_server": "http://localhost:30000/v1",
    "api_key": "EMPTY",  # Локальным серверам не нужен настоящий ключ

    # Параметры сэмплирования для режима рассуждений (из официальных рекомендаций)
    "generate_cfg": {
        "temperature": 0.6,
        "top_p": 0.95,
        "top_k": 20,
        "min_p": 0.0,
        "thought_in_history": True,  # Это флаг preserve_thinking в Qwen-Agent
    },
}

# ── Конфигурация MCP-серверов ────────────────────────────────────────────────
# Ключи словаря — имена серверов, значения — команды запуска через stdio.
# Qwen-Agent запускает каждый сервер как подпроцесс и управляет сессиями MCP.
MCP_SERVERS = {
    "mcpServers": {
        "filesystem": {
            "command": "npx",
            "args": [
                "-y",
                "@modelcontextprotocol/server-filesystem",
                # Даём агенту доступ к текущей рабочей директории
                # В production ограничьте путь конкретным репозиторием
                "."
            ]
        },
        "github": {
            "command": "npx",
            "args": ["-y", "@modelcontextprotocol/server-github"],
            "env": {
                # GitHub MCP-сервер читает эту переменную для аутентификации API
                "GITHUB_TOKEN": "${GITHUB_TOKEN}"
            }
        },
    }
}

# ── Системный промпт ─────────────────────────────────────────────────────────
SYSTEM_PROMPT = """Вы — старший инженер-программист с полным доступом к
GitHub-репозиторию через инструменты MCP. Когда вам дают репозиторий и задачу:
1. Просматривайте открытые issue, чтобы понять, что нужно исправить.
2. Используйте файловые инструменты для чтения нужных исходных файлов и тестов.
3. Определяйте корневую причину на основе кода и описания issue.
4. Пишите точечное исправление — с минимальными изменениями,
   без рефакторинга, не связанного с ошибкой.
5. Создавайте pull request с чётким заголовком и описанием, ссылаясь на issue.
Всегда объясняйте свои рассуждения на каждом шаге. Думайте о граничных случаях,
прежде чем писать код. Если вы не уверены в назначении файла,
прочитайте его, прежде чем модифицировать."""

# ── Настройка агента ─────────────────────────────────────────────────────────
agent = Assistant(
    llm=LLM_CONFIG,
    name="GitHub Developer Assistant",
    description="Читает issue, исправляет ошибки, открывает pull request — локально через MCP.",
    system_message=SYSTEM_PROMPT,
    mcp_servers=MCP_SERVERS,
)

# ── Запуск агента ────────────────────────────────────────────────────────────
def run_agent(task: str):
    """
    Запускает агента с описанием задачи и выводит результат.
    Агент автоматически выполняет вызовы инструментов;
    Qwen-Agent зацикливает весь процесс, включая исполнение инструментов и вставку результатов.
    """
    messages = [{"role": "user", "content": task}]
    print(f"Задача: {task}\n{'─' * 70}")

    # run() возвращает генератор, выдающий промежуточные шаги
    # Каждое значение — список сообщений, представляющий текущее состояние диалога
    for response in agent.run(messages=messages):
        last = response[-1]
        role = last.get("role", "")
        content = last.get("content", "")

        if role == "assistant" and content:
            # Выделяем блок рассуждений для удобства чтения
            import re
            thinking = re.search(r"<think>(.*?)</think>", content, re.DOTALL)
            if thinking:
                print(f"[thinking] {thinking.group(1).strip()[:200]}...")
            clean = re.sub(r"<think>.*?</think>", "", content, flags=re.DOTALL).strip()
            if clean:
                print(f"[agent] {clean}")
        elif role == "tool":
            tool_name = last.get("name", "unknown_tool")
            print(f"[tool:{tool_name}] получен результат")

if __name__ == "__main__":
    run_agent(
        "В репозитории myorg/my-api-project найдите открытую issue о том, "
        "что эндпоинт логина возвращает 200 для невалидных токенов. "
        "Прочитайте соответствующий код и тесты, исправьте ошибку и откройте pull request."
    )

Как запустить:

python github_agent_qwenagent.py

// Часть 3: Реализация через чистый MCP SDK

Для команд, которым нужен полный контроль над каждым сообщением протокола, пользовательской обработкой ошибок, логикой повторных попыток для отдельных инструментов и аудитом каждого вызова:

# github_agent_raw.py
# Предварительно: pip install mcp openai httpx
# Должны быть установлены переменная GITHUB_TOKEN и локальный сервер
#
# Запуск:
#   python github_agent_raw.py

import asyncio
import json
import os
import re

from openai import AsyncOpenAI
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

# ── Клиент локального сервера ───────────────────────────────────────────────
client = AsyncOpenAI(
    base_url="http://localhost:30000/v1",
    api_key="EMPTY",
)
MODEL = "Qwen/Qwen3.6-35B-A3B"

# ── Обработка ответов ────────────────────────────────────────────────────────
def strip_thinking(text: str) -> str:
    """Удаляет блоки <think>...</think>. Используется, когда нужно только действие."""
    return re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL).strip()

def extract_thinking(text: str) -> str:
    """Извлекает содержимое блока thinking для логирования."""
    m = re.search(r"<think>(.*?)</think>", text, re.DOTALL)
    return m.group(1).strip() if m else ""

def process_response(response, preserve_thinking: bool = True) -> dict:
    """
    Обрабатывает ответ чат-дополнения от Qwen3.6.
    Поддерживает два формата вывода:
    1. Вызов инструмента через поля function_call / tool_calls API
       (когда активен --tool-call-parser).
    2. Вызов инструмента, встроенный в содержимое сообщения как <tool_call>JSON</tool_call>.

    Аргументы:
        response: OpenAI-совместимый ответ дополнения.
        preserve_thinking: Если True, сохраняет содержимое thinking в выводе
            для выгоды KV-кеша на следующем шаге.

    Возвращает:
        dict с ключами thinking, tool_calls, final_answer, has_tool_calls, is_terminal.
    """
    choice = response.choices[0]
    message = choice.message

    # Путь 1: вызовы инструментов в структурированном поле (предпочтительный — требует флаг tool-call-parser)
    if message.tool_calls:
        tool_calls = [
            {
                "name": tc.function.name,
                "arguments": json.loads(tc.function.arguments),
                "call_id": tc.id,
            }
            for tc in message.tool_calls
        ]
        thinking = extract_thinking(message.content or "")
        return {
            "thinking": thinking if preserve_thinking else "",
            "tool_calls": tool_calls,
            "final_answer": "",
            "has_tool_calls": True,
            "is_terminal": False,
        }

    # Путь 2: вызовы инструментов в тексте сообщения (запасной)
    content = message.content or ""
    tag_matches = re.findall(r"<tool_call>(.*?)</tool_call>", content, re.DOTALL)
    tool_calls = []
    for m in tag_matches:
        try:
            tool_calls.append(json.loads(m.strip()))
        except json.JSONDecodeError:
            pass

    thinking = extract_thinking(content)
    final_answer = re.sub(r"<think>.*?</think>", "", content, flags=re.DOTALL)
    final_answer = re.sub(r"<tool_call>.*?</tool_call>", "", final_answer, flags=re.DOTALL).strip()

    return {
        "thinking": thinking if preserve_thinking else "",
        "tool_calls": tool_calls,
        "final_answer": final_answer,
        "has_tool_calls": len(tool_calls) > 0,
        "is_terminal": len(tool_calls) == 0 and bool(final_answer),
    }

# ── Основной цикл агента ────────────────────────────────────────────────────
async def run_github_agent(task: str, repo: str, max_turns: int = 20):
    """
    Запускает агента-ассистента GitHub.
    Подключается к файловому и GitHub MCP-серверам, получает их инструменты
    и выполняет цикл агента Qwen3.6, пока задача не будет выполнена или не достигнут лимит ходов.
    """
    # Запускаем оба MCP-сервера и устанавливаем сессии
    fs_params = StdioServerParameters(
        command="npx",
        args=["-y", "@modelcontextprotocol/server-filesystem", "."],
    )
    gh_params = StdioServerParameters(
        command="npx",
        args=["-y", "@modelcontextprotocol/server-github"],
        env={**os.environ, "GITHUB_TOKEN": os.environ.get("GITHUB_TOKEN", "")},
    )

    async with stdio_client(fs_params) as (fs_read, fs_write), \
            ClientSession(fs_read, fs_write) as fs_session, \
            stdio_client(gh_params) as (gh_read, gh_write), \
            ClientSession(gh_read, gh_write) as gh_session:

        # Инициализируем обе сессии
        await fs_session.initialize()
        await gh_session.initialize()

        # Получаем списки всех доступных инструментов
        fs_tools_result = await fs_session.list_tools()
        gh_tools_result = await gh_session.list_tools()

        # Формируем список инструментов в формате OpenAI и словарь маршрутизации
        all_tools = []
        tool_to_session = {}  # Сопоставляет имя инструмента с его MCP-сессией

        for tool in fs_tools_result.tools:
            all_tools.append({
                "type": "function",
                "function": {
                    "name": tool.name,
                    "description": tool.description,
                    "parameters": tool.inputSchema,
                }
            })
            tool_to_session[tool.name] = fs_session

        for tool in gh_tools_result.tools:
            all_tools.append({
                "type": "function",
                "function": {
                    "name": tool.name,
                    "description": tool.description,
                    "parameters": tool.inputSchema,
                }
            })
            tool_to_session[tool.name] = gh_session

        print(f"Доступно инструментов: {len(all_tools)} "
              f"({len(fs_tools_result.tools)} файловых, {len(gh_tools_result.tools)} GitHub)")

        # Формируем историю диалога
        system_prompt = f"""Вы — старший инженер-программист с доступом к репозиторию {repo}.
Используйте доступные инструменты для исследования проблем, чтения кода, написания исправлений
и создания pull request. Думайте пошагово. Читайте перед тем, как модифицировать.
Только минимальные изменения."""

        messages = [
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": task},
        ]

        # ── Цикл агента ─────────────────────────────────────────────────────
        for turn in range(max_turns):
            print(f"\n[Ход {turn + 1}]")

            # Вызов модели
            response = await client.chat.completions.create(
                model=MODEL,
                messages=messages,
                tools=all_tools if all_tools else None,
                tool_choice="auto",
                # Параметры сэмплирования для режима рассуждений
                temperature=0.6,
                top_p=0.95,
                top_k=20,
                min_p=0.0,
                max_tokens=4096,
                extra_body={
                    # preserve_thinking сохраняет контекст рассуждений между ходами
                    # для эффективности KV-кеша в длинных агентных сессиях
                    "preserve_thinking": True,
                }
            )

            result = process_response(response, preserve_thinking=True)
            if result["thinking"]:
                print(f"[thinking] {result['thinking'][:200]}...")

            # Терминальное состояние — агент выдал финальный ответ
            if result["is_terminal"]:
                print(f"\n[ГОТОВО]\n{result['final_answer']}")
                return result["final_answer"]

            # Состояние вызова инструментов — выполняем каждый и вставляем результаты
            if result["has_tool_calls"]:
                # Добавляем сообщение ассистента с вызовами инструментов в историю
                messages.append({
                    "role": "assistant",
                    "content": response.choices[0].message.content or "",
                    "tool_calls": response.choices[0].message.tool_calls or [],
                })

                for call in result["tool_calls"]:
                    tool_name = call["name"]
                    tool_args = call.get("arguments", {})
                    call_id = call.get("call_id", "")
                    print(f"[tool] {tool_name}({json.dumps(tool_args)[:80]}...)")

                    session = tool_to_session.get(tool_name)
                    if not session:
                        result_content = f"Ошибка: инструмент '{tool_name}' не найден"
                    else:
                        try:
                            tool_result = await session.call_tool(tool_name, tool_args)
                            result_content = str(tool_result.content)
                            # Обрезаем очень длинные результаты для экономии контекста
                            if len(result_content) > 12000:
                                result_content = result_content[:12000] + "\n...[обрезано]"
                        except Exception as e:
                            result_content = f"Ошибка: {e}"

                    print(f"[result] {result_content[:150]}...")
                    messages.append({
                        "role": "tool",
                        "content": result_content,
                        "tool_call_id": call_id,
                        "name": tool_name,
                    })

        print(f"[ПРЕДУПРЕЖДЕНИЕ] Достигнут лимит ходов ({max_turns}) без терминального состояния")

# ── Точка входа ─────────────────────────────────────────────────────────────
if __name__ == "__main__":
    asyncio.run(run_github_agent(
        task=(
            "Найдите открытую issue о том, что эндпоинт логина возвращает 200 для невалидных токенов. "
            "Прочитайте src/auth.py и tests/test_auth.py, чтобы понять ошибку. "
            "Исправьте функцию verify_token и откройте pull request с вашими изменениями."
        ),
        repo="myorg/my-api-project",
    ))

Как запустить:

python github_agent_raw.py

Чистый SDK показывает то, что Qwen-Agent скрывает за абстракцией: вы видите каждый вызов инструмента, каждый результат и каждое сообщение, добавленное в историю диалога. Словарь tool_to_session — ключевой механизм: он сопоставляет имя инструмента с владеющей им MCP-сессией, благодаря чему агент может вызывать любой инструмент любого подключённого сервера, не зная, какой именно сервер его предоставляет.

# Пишем собственный MCP-сервер

Готовые MCP-серверы покрывают работу с файловой системой и GitHub. Когда нужного инструмента нет — например, запросы к внутренней базе данных, обёртка CI/CD API, запуск анализаторов кода — вы пишете MCP-сервер сами. Вот готовый сервер code_quality, предоставляющий ruff и pytest как инструменты MCP.

# code_quality_server.py
# Пользовательский MCP-сервер, предоставляющий инструменты контроля качества кода для Qwen3.6.
#
# Предварительно: pip install mcp ruff pytest
#
# Автономный запуск (для тестирования):
#   python code_quality_server.py
#
# Добавление в конфигурацию Qwen-Agent:
#   "code_quality": {
#       "command": "python",
#       "args": ["/absolute/path/to/code_quality_server.py"]
#   }

import asyncio
import json
import subprocess
import sys

from mcp.server.fastmcp import FastMCP

# FastMCP — высокоуровневый фреймворк для MCP-серверов, уменьшает количество шаблонного кода
mcp = FastMCP("code_quality")

@mcp.tool()
def run_linter(file_path: str, fix: bool = False) -> str:
    """
    Запускает ruff-линтер для Python-файла и возвращает структурированные результаты.
    Используйте перед модификацией файла, чтобы понять его текущее состояние качества,
    и после внесения изменений, чтобы убедиться, что исправление не добавило новых проблем.

    Аргументы:
        file_path: Абсолютный или относительный путь к файлу Python.
        fix: Если True, автоматически исправляет безопасные проблемы на месте.

    Возвращает:
        Строку JSON со списком проблем, их количеством и информацией об изменённых файлах.
    """
    cmd = ["python", "-m", "ruff", "check", file_path, "--output-format=json"]
    if fix:
        cmd.append("--fix")
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
        # ruff возвращает код выхода 1, когда проблемы найдены — это не ошибка
        output = result.stdout or result.stderr

        # Парсим JSON-вывод ruff
        try:
            issues = json.loads(output) if output.strip() else []
        except json.JSONDecodeError:
            issues = []

        formatted = [
            {
                "line": issue.get("location", {}).get("row", 0),
                "col": issue.get("location", {}).get("column", 0),
                "code": issue.get("code", ""),
                "message": issue.get("message", ""),
                "fix_available": issue.get("fix") is not None,
            }
            for issue in issues if isinstance(issue, dict)
        ]

        return json.dumps({
            "file": file_path,
            "issues": formatted,
            "total_issues": len(formatted),
            "fixed": "автоисправление применено" if fix else "автоисправлений не было",
        }, indent=2)

    except subprocess.TimeoutExpired:
        return json.dumps({"error": "Линтер превысил время ожидания (30 с)", "file": file_path})
    except FileNotFoundError:
        return json.dumps({"error": "ruff не найден — установите: pip install ruff"})

@mcp.tool()
def run_tests(target: str, verbose: bool = False) -> str:
    """
    Запускает pytest для модуля или директории и возвращает структурированные результаты.
    Используйте после написания исправления, чтобы убедиться, что падавшие тесты проходят,
    а остальные не сломались.

    Аргументы:
        target: Путь к тестовому файлу или директории (например, tests/, tests/test_auth.py)
        verbose: Если True, включить полный вывод pytest.

    Возвращает:
        Строку JSON с количеством пройденных, упавших, ошибок, деталями падений и длительностью.
    """
    cmd = ["python", "-m", "pytest", target, "--json-report", "--json-report-file=-", "-q"]
    if verbose:
        cmd.append("-v")
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
        output = result.stdout

        # Парсим вывод pytest-json-report, если он есть
        try:
            report = json.loads(output)
            summary = report.get("summary", {})
            failures = [
                {
                    "test": t["nodeid"],
                    "message": t.get("call", {}).get("longrepr", "")[:500],
                }
                for t in report.get("tests", []) if t.get("outcome") == "failed"
            ]
            return json.dumps({
                "target": target,
                "passed": summary.get("passed", 0),
                "failed": summary.get("failed", 0),
                "errors": summary.get("error", 0),
                "total": summary.get("total", 0),
                "duration": summary.get("duration", 0),
                "failures": failures,
                "stdout": result.stdout[:2000] if verbose else "",
            }, indent=2)
        except json.JSONDecodeError:
            # Запасной вариант: возвращаем сырой вывод, если JSON-отчёт не получен
            return json.dumps({
                "target": target,
                "stdout": result.stdout[:3000],
                "stderr": result.stderr[:1000],
                "exit_code": result.returncode,
            })
    except subprocess.TimeoutExpired:
        return json.dumps({"error": f"Тесты превысили время ожидания (120 с) для цели: {target}"})
    except FileNotFoundError:
        return json.dumps({"error": "pytest не найден — установите: pip install pytest"})

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

Добавьте сервер в конфигурацию любого из двух вариантов агента:

# В словарь MCP_SERVERS Qwen-Agent:
"code_quality": {
    "command": "python",
    "args": ["/absolute/path/to/code_quality_server.py"]
}

# В чистом SDK — добавьте третий StdioServerParameters:
cq_params = StdioServerParameters(
    command="python",
    args=["/absolute/path/to/code_quality_server.py"],
)

Протестируйте сервер автономно, прежде чем подключать к агенту:

# Тестирование в режиме MCP Inspector
npx @modelcontextprotocol/inspector python code_quality_server.py
# Откроется браузерный интерфейс, где можно напрямую вызывать run_linter и run_tests

# Настройка режима размышлений и сохранения рассуждений

Решение о включении режима размышлений существенно влияет на задержку, поэтому его стоит принимать осознанно, а не оставлять на потом.

В режиме размышлений Qwen3.6 генерирует цепочку рассуждений внутри тегов <think>...</think>, прежде чем выполнить действие. Для задачи из 5 шагов такая цепочка добавляет от 1 000 до 5 000 токенов за ход в зависимости от сложности. Эти токены требуют времени на генерацию и расходуют бюджет контекста.

Когда эти затраты оправданы:

  • На этапах планирования, когда агент решает, что делать дальше.
  • При отладке по-настоящему неоднозначных проблем.
  • При многофайловом рефакторинге, когда нужно продумать побочные эффекты в разных файлах.

Цепочка рассуждений отлавливает ошибки до того, как они превратятся в вызовы инструментов с неверными аргументами. Когда тратиться не стоит: в механических циклах вызовов, где каждый шаг однозначен — посмотреть директорию → прочитать файл → записать файл → закоммитить. Модели не нужно напряжённо думать над такими шагами. Без режима размышлений быстрее и с тем же качеством результата.

Переключайте режим для каждого запроса, а не глобально:

# Режим размышлений (планирование, отладка, сложные многофайловые задачи)
THINKING_PARAMS = {
    "temperature": 0.6,
    "top_p": 0.95,
    "top_k": 20,
    "min_p": 0.0,
}

# Режим без размышлений (механические циклы, быстрые проверки состояния)
# Передайте enable_thinking=False в шаблон чата или добавьте /no_think в системный промпт.
NON_THINKING_PARAMS = {
    "temperature": 0.7,
    "top_p": 0.8,
    "top_k": 20,
    "min_p": 0.0,
}

Флаг preserve_thinking — специфичная для Qwen3.6 возможность сохранять контекст рассуждений между ходами — напрямую влияет на эффективность инференса при активном кешировании префиксов. Вот почему это важно на практике: в агентной сессии из 10 ходов у каждого хода общий префикс истории диалога. Когда preserve_thinking=True, полная цепочка рассуждений предыдущих ходов остаётся в истории. KV-кеш на стороне сервера распознаёт общий префикс и не пересчитывает его. Эффективная скорость в токенах в секунду для длинных сессий ощутимо выше, чем без этой возможности, особенно когда серверная инфраструктура вроде SGLang запущена с --enable-prefix-caching.

Практическое правило: используйте preserve_thinking=True для агентных сессий, которые длятся больше 5 ходов. Используйте preserve_thinking=False (или режим без размышлений) для однократных запросов и коротких пайплайнов, где эти накладные расходы излишни.

# Заключение

MoE-архитектура Qwen3.6-35B-A3B даёт качество 35B-модели при стоимости активации 3B. Контекстное окно на 262 тысячи токенов позволяет держать в памяти целую сессию код-ревью. Явная тренировка на MCP-ориентированных агентных бенчмарках означает, что модель умеет правильно пользоваться инструментами, а не просто вызывать их.

MCP служит связующей тканью. Один раз описываете инструмент как MCP-сервер. Любая сессия Qwen3.6 и любая другая MCP-совместимая модель может найти и вызвать его без самодельного клея. Серверы GitHub и файловой системы из этой статьи — лишь два из сотен готовых серверов в экосистеме MCP. Пользовательский сервер code_quality показывает шаблон для всего, чего ещё нет в готовом виде.

GitHub-ассистент из этой статьи — одно применение схемы. Та же архитектура — локальная модель, инструменты MCP и агентный цикл — работает для научного ассистента, который ищет в академических базах и составляет обзоры литературы, для DevOps-агента, читающего CloudWatch-логи и заводящего инциденты, или для агента пайплайнов данных, изучающего SQL-схемы, пишущего код трансформации и проверяющего выходные данные. Экосистема MCP стремительно растёт. Возможности локальных моделей уже здесь.

Горячее

Загружаем популярные статьи...