Команда AI for Devs подготовила перевод статьи о том, как на самом деле устроены AI-агенты для программирования. Автор шаг за шагом показывает, что за Claude Code не стоит магия: это последовательный агентный цикл, инструменты, контроль разрешений и работа с контекстом.


Что делает Claude Code мощным, на удивление просто: это цикл, который позволяет ИИ читать файлы, запускать команды и итеративно работать, пока задача не будет выполнена.

Сложность начинается там, где нужно разрулить пограничные случаи, сделать хороший UX и встроиться в реальные процессы разработки.

В этом посте я начну с нуля и шаг за шагом подведу вас к архитектуре Claude Code, показывая, как вы могли бы «изобрести» её сами — от первых принципов, имея лишь терминал, API LLM и желание сделать ИИ действительно полезным.

Конечная цель: понять, как работают мощные агенты, чтобы вы могли собрать своего

Для начала зафиксируем проблему, которую мы пытаемся решить.

Когда вы пользуетесь ChatGPT или Claude в браузере, вы делаете массу ручной работы:

  • Копируете и вставляете код из чата в файлы

  • Запускаете команды сами, затем копируете ошибки обратно

  • Даете контекст, загружая файлы или вставляя содержимое

  • Вручную проходите цикл «исправил — проверил — отладил» снова и снова

По сути, вы выступаете руками ИИ. ИИ думает — вы исполняете.

А что если ИИ мог бы исполнять тоже?

Представьте, что вы говорите ИИ: «Исправь баг в auth.py» — и уходите. Возвращаетесь, а баг уже исправлен. ИИ прочитал файл, понял, что происходит, попробовал исправление, запустил тесты, увидел падение, попробовал другой подход — и в итоге добился успеха.

Вот что делает агент. Это ИИ, который умеет:

  1. Совершать действия в реальном мире (читать файлы, запускать команды)

  2. Наблюдать результаты

  3. Решать, что делать дальше

  4. Повторять, пока задача не будет завершена

Давайте соберем такого с нуля.

Самый простой агент

Начнём с абсолютного минимума: ИИ, который умеет выполнить одну bash-команду.

#!/bin/bash
# agent-v0.sh - The simplest possible agent

PROMPT="$1"

# Ask Claude what command to run
RESPONSE=$(curl -s https://api.anthropic.com/v1/messages \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "content-type: application/json" \
  -H "anthropic-version: 2023-06-01" \
  -d '{
    "model": "claude-opus-4-5-20251101",
    "max_tokens": 1024,
    "messages": [{"role": "user", "content": "'"$PROMPT"'\n\nRespond with ONLY a bash command. No markdown, no explanation, no code blocks."}]
  }')

# Extract the command from response
COMMAND=$(echo "$RESPONSE" | jq -r '.content[0].text')

echo "AI suggests: $COMMAND"
read -r -p "Run this command? (y/n) " CONFIRM

if [ "$CONFIRM" = "y" ]; then
  eval "$COMMAND"
fi

Использование

bash agent-v0.sh "list all Python files in this directory"
# AI suggests: ls *.py
# Run this command? (y/n)

Это… не слишком полезно. ИИ может предложить одну команду, а дальше вы снова делаете всё вручную.

Но вот ключевая мысль: а что если завернуть это в цикл?

Цель: собрать цикл агента

Ключевая идея, лежащая в основе всех ИИ-агентов, — это цикл агента:

while (task not complete):
    1. AI decides what to do next
    2. Execute that action
    3. Show AI the result
    4. Go back to step 1

Давайте реализуем ровно это. ИИ должен сообщать нам:

  • Какое действие выполнить

  • Завершена ли задача

Используем простой JSON-формат:

#!/bin/bash
# agent-v1.sh - Agent with a loop

SYSTEM_PROMPT='You are a helpful assistant that can run bash commands.

When the user gives you a task, respond with JSON in this exact format:
{"action": "bash", "command": "your command here"}

When the task is complete, respond with:
{"action": "done", "message": "explanation of what was accomplished"}

Only respond with JSON. No other text.'

# We'll build messages as a JSON array (using jq for proper escaping)
MESSAGES="[]"

run_agent() {
    local USER_MSG="$1"
    
    # Add initial user message using jq to handle escaping
    MESSAGES=$(echo "$MESSAGES" | jq --arg msg "$USER_MSG" '. + [{"role": "user", "content": $msg}]')
    
    while true; do
        # Build the request body properly with jq
        REQUEST_BODY=$(jq -n \
            --arg model "claude-opus-4-5-20251101" \
            --arg system "$SYSTEM_PROMPT" \
            --argjson messages "$MESSAGES" \
            '{model: $model, max_tokens: 1024, system: $system, messages: $messages}')
        
        # Call the API
        RESPONSE=$(curl -s https://api.anthropic.com/v1/messages \
          -H "x-api-key: $ANTHROPIC_API_KEY" \
          -H "content-type: application/json" \
          -H "anthropic-version: 2023-06-01" \
          -d "$REQUEST_BODY")
        # Echo the response for debugging
        AI_TEXT=$(echo "$RESPONSE" | jq -r '.content[0].text')
        
        # Add assistant message to history
        MESSAGES=$(echo "$MESSAGES" | jq --arg msg "$AI_TEXT" '. + [{"role": "assistant", "content": $msg}]')
        
        # Parse the action from the JSON response
        ACTION=$(echo "$AI_TEXT" | jq -r '.action // empty')
        
        if [ -z "$ACTION" ]; then
            echo "❌ Could not parse response: $AI_TEXT"
            break
        elif [ "$ACTION" = "done" ]; then
            echo "✅ $(echo "$AI_TEXT" | jq -r '.message')"
            break
        elif [ "$ACTION" = "bash" ]; then
            COMMAND=$(echo "$AI_TEXT" | jq -r '.command')
            echo "? Running: $COMMAND"
            
            # Execute and capture output
            OUTPUT=$(eval "$COMMAND" 2>&1)
            echo "$OUTPUT"
            
            # Feed result back to AI
            MESSAGES=$(echo "$MESSAGES" | jq --arg msg "Command output: $OUTPUT" '. + [{"role": "user", "content": $msg}]')
        else
            echo "❌ Unknown action: $ACTION"
            break
        fi
    done
}

run_agent "$1"

Теперь у нас есть штука, которая действительно умеет итеративно работать:

bash agent-v1.sh "Create a file called hello.py that prints hello world, then run it"

# ? Running: echo 'print("hello world")' > hello.py
# ? Running: python hello.py
# hello world
# ✅ Created hello.py and executed it successfully. It prints "hello world".

ИИ выполнил две команды и затем сообщил, что задача завершена. Мы собрали цикл агента!

Но погодите. Мы выполняем произвольные команды без каких-либо проверок безопасности. ИИ может предложить rm -rf /, и мы послушно это исполним.

Цель: добавить контроль разрешений

Давайте добавим человека в контур для опасных операций. Сначала определим функцию, которая оборачивает выполнение команды проверкой безопасности:

# Add this function BEFORE run_agent() in your script
execute_with_permission() {
    local COMMAND="$1"
    
    # Check if command seems dangerous
    if echo "$COMMAND" | grep -qE 'rm |sudo |chmod |curl.*\|.*sh'; then
        # Use >&2 to print to stderr, so prompts display immediately
        # (stdout gets captured by the $(...) in the agent loop)
        echo "⚠️  Potentially dangerous command: $COMMAND" >&2
        echo "Allow? (y/n)" >&2
        read CONFIRM
        if [ "$CONFIRM" != "y" ]; then
            echo "DENIED BY USER"
            return 1
        fi
    fi
    
    eval "$COMMAND" 2>&1
}

Затем внутри цикла агента заменяем прямой вызов eval на нашу новую функцию:

        # BEFORE:
        OUTPUT=$(eval "$COMMAND" 2>&1)
        
        # AFTER (with permission check):
        OUTPUT=$(execute_with_permission "$COMMAND")

Вот и всё. Функция вклинивается между запросом ИИ и реальным выполнением, давая вам возможность заблокировать опасные команды. Если вы запретили выполнение, можно отдать это обратно ИИ, чтобы он попробовал другой подход.

Попробуйте:

# Create a test file
echo 'print("hello world")' > hello.py

# Ask the agent to delete it
bash agent-v1.sh "delete the file hello.py"

# ? Running: rm hello.py
# ⚠️  Potentially dangerous command: rm hello.py
# Allow? (y/n)

Нажмите y, чтобы разрешить удаление, или n, чтобы заблокировать.

Это начало системы разрешений. Claude Code развивает эту идею гораздо дальше:

  • Разрешения по типам инструментов (правки файлов vs. bash-команды)

  • Аллоулисты по шаблонам (Bash(npm test:*) разрешает любую команду npm test)

  • Режимы «принять всё» на уровне сессии, когда вы доверяете ИИ

Ключевая мысль: человек должен контролировать, что именно ИИ может делать, но с такой детализацией, чтобы это не бесило.

Цель: выйти за рамки bash — добавить инструменты

Запуск bash-команд — это мощно, но это также:

  • Опасно: неограниченный доступ к системе

  • Неэффективно: чтобы прочитать файл, не стоит поднимать отдельный процесс

  • Неточно: парсинг вывода хрупкий

Что если вместо этого дать ИИ структурированные инструменты?

Дальше перейдём на Python, потому что там проще и чище работать с JSON и API-вызовами:

# agent-v2.py - Agent with structured tools
import anthropic
import json
import os

client = anthropic.Anthropic()

TOOLS = [
    {
        "name": "read_file",
        "description": "Read the contents of a file",
        "input_schema": {
            "type": "object",
            "properties": {
                "path": {"type": "string", "description": "Path to the file"}
            },
            "required": ["path"]
        }
    },
    {
        "name": "write_file",
        "description": "Write content to a file",
        "input_schema": {
            "type": "object",
            "properties": {
                "path": {"type": "string", "description": "Path to the file"},
                "content": {"type": "string", "description": "Content to write"}
            },
            "required": ["path", "content"]
        }
    },
    {
        "name": "run_bash",
        "description": "Run a bash command",
        "input_schema": {
            "type": "object",
            "properties": {
                "command": {"type": "string", "description": "The command to run"}
            },
            "required": ["command"]
        }
    }
]

def execute_tool(name, input):
    """Execute a tool and return the result."""
    if name == "read_file":
        try:
            with open(input["path"], "r") as f:
                return f.read()
        except Exception as e:
            return f"Error: {e}"
    
    elif name == "write_file":
        try:
            with open(input["path"], "w") as f:
                f.write(input["content"])
            return f"Successfully wrote to {input['path']}"
        except Exception as e:
            return f"Error: {e}"
    
    elif name == "run_bash":
        import subprocess
        result = subprocess.run(
            input["command"], 
            shell=True, 
            capture_output=True, 
            text=True
        )
        return result.stdout + result.stderr

def run_agent(task):
    """Main agent loop."""
    messages = [{"role": "user", "content": task}]
    
    while True:
        response = client.messages.create(
            model="claude-opus-4-5-20251101",
            max_tokens=4096,
            tools=TOOLS,
            messages=messages
        )
        
        # Check if we're done
        if response.stop_reason == "end_turn":
            # Extract final text response
            for block in response.content:
                if hasattr(block, "text"):
                    print(f"✅ {block.text}")
            break
        
        # Process tool uses
        if response.stop_reason == "tool_use":
            # Add assistant's response to history
            messages.append({"role": "assistant", "content": response.content})
            
            tool_results = []
            for block in response.content:
                if block.type == "tool_use":
                    print(f"? {block.name}: {json.dumps(block.input)}")
                    result = execute_tool(block.name, block.input)
                    print(f"   → {result[:200]}...")  # Truncate for display
                    
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": result
                    })
            
            # Add results to conversation
            messages.append({"role": "user", "content": tool_results})

if __name__ == "__main__":
    import sys
    run_agent(sys.argv[1])

Теперь мы используем нативный API tool use от Anthropic. Это гораздо лучше, потому что:

  1. Типобезопасность: ИИ точно знает, какие параметры принимает каждый инструмент

  2. Явные действия: чтение файла — это вызов read_file, а не cat

  3. Контролируемая поверхность: мы сами решаем, какие инструменты вообще существуют

Попробуйте:

# Create a test file for the agent to work with
cat > main.py << 'EOF'
def calculate(x, y):
    return x + y

def greet(name):
    print(f"Hello, {name}!")
EOF

# Run the agent
uv run --with anthropic python agent-v2.py "Read main.py and add a docstring to the first function"

# ? read_file: {"path": "main.py"}
#    → def calculate(x, y):...
# ? write_file: {"path": "main.py", "content": "def calculate(x, y):\n    \"\"\"Calculate..."}
#    → Successfully wrote to main.py
# ✅ I've added a docstring to the calculate function explaining its purpose.

Цель: сделать правки точными

У нашего инструмента write_file есть проблема: он перезаписывает файл целиком. Если ИИ делает небольшую правку в файле на 1000 строк, ему приходится выводить все 1000 строк. Это:

  • Дорого: больше токенов на вывод — выше стоимость

  • Рискованно: ИИ может случайно «уронить» строки

  • Медленно: генерация такого объёма текста занимает время

А что если сделать инструмент для хирургически точных правок?

{
    "name": "edit_file",
    "description": "Make a precise edit to a file by replacing a unique string",
    "input_schema": {
        "type": "object",
        "properties": {
            "path": {"type": "string"},
            "old_str": {"type": "string", "description": "Exact string to find (must be unique in file)"},
            "new_str": {"type": "string", "description": "String to replace it with"}
        },
        "required": ["path", "old_str", "new_str"]
    }
}

Реализация:

def edit_file(path, old_str, new_str):
    with open(path, "r") as f:
        content = f.read()
    
    # Ensure the string is unique
    count = content.count(old_str)
    if count == 0:
        return f"Error: '{old_str}' not found in file"
    if count > 1:
        return f"Error: '{old_str}' found {count} times. Must be unique."
    
    new_content = content.replace(old_str, new_str)
    with open(path, "w") as f:
        f.write(new_content)
    
    return f"Successfully replaced text in {path}"

Это ровно то, как работает инструмент str_replace в Claude Code. Требование уникальности может показаться раздражающим, но на деле это фича:

  • Заставляет ИИ добавлять достаточно контекста, чтобы правка была однозначной

  • Создаёт естественный «diff», который человеку легко проверить

  • Предотвращает случайные массовые замены

Цель: поиск по кодовой базе

Пока что наш агент умеет читать только те файлы, о которых он уже знает. Но что делать с задачей вроде «найди, где баг в аутентификации»?

ИИ нужно уметь искать по кодовой базе. Давайте добавим для этого инструменты.

SEARCH_TOOLS = [
    {
        "name": "glob",
        "description": "Find files matching a pattern",
        "input_schema": {
            "type": "object",
            "properties": {
                "pattern": {"type": "string", "description": "Glob pattern (e.g., '**/*.py')"}
            },
            "required": ["pattern"]
        }
    },
    {
        "name": "grep",
        "description": "Search for a pattern in files",
        "input_schema": {
            "type": "object",
            "properties": {
                "pattern": {"type": "string", "description": "Regex pattern to search for"},
                "path": {"type": "string", "description": "Directory or file to search in"}
            },
            "required": ["pattern"]
        }
    }
]

Теперь ИИ может:

  • glob("**/*.py") → найти все Python-файлы

  • grep("def authenticate", "src/") → найти код, связанный с аутентификацией

  • read_file("src/auth.py") → прочитать нужный файл

  • edit_file(...) → исправить баг

Вот и весь паттерн: дайте ИИ инструменты для разведки, и он сможет ориентироваться в кодовой базе, которую видит впервые.

Цель: управление контекстом

Вот с какой проблемой вы быстро столкнётесь: окна контекста конечны.

Если вы работаете с большой кодовой базой, диалог может выглядеть так:

  • Пользователь: «Исправь баг в аутентификации»

  • ИИ: читает 10 файлов, запускает 20 команд, пробует 3 подхода

  • ...диалог разрастается до 100 000 токенов

  • ИИ: упирается в лимит контекста и начинает забывать ранние детали

Как с этим справляться?

Вариант 1: суммаризация (уплотнение)

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

def compact_conversation(messages):
    """Summarize the conversation to free up context."""
    summary_prompt = """Summarize this conversation concisely, preserving:
    - The original task
    - Key findings and decisions
    - Current state of the work
    - What still needs to be done"""
    
    summary = client.messages.create(
        model="claude-opus-4-5-20251101",
        max_tokens=2000,
        messages=[
            {"role": "user", "content": f"{messages}\n\n{summary_prompt}"}
        ]
    )
    
    return [{"role": "user", "content": f"Previous work summary:\n{summary}"}]

Вариант 2: подагенты (делегирование)

Для сложных задач можно запускать подагента со своим отдельным контекстом:

def delegate_to_subagent(task, tools_allowed):
    """Spawn a sub-agent for a focused task."""
    result = run_agent(
        task=task,
        tools=tools_allowed,
        max_turns=10  # Prevent infinite loops
    )
    # Only return the result, not the full conversation
    return result.final_answer

Поэтому в Claude Code есть концепция подагентов: специализированные агенты решают узкие подзадачи в собственном контексте и возвращают только итог.

Цель: system prompt

Мы до этого замалчивали одну важную вещь: откуда ИИ вообще знает, как себя вести?

System prompt — это место, где вы кодируете:

  • Идентичность ИИ и его возможности

  • Правила использования инструментов

  • Проектный контекст

  • Поведенческие ограничения

Вот упрощённая версия того, что делает Claude Code эффективным:

SYSTEM_PROMPT = """You are an AI assistant that helps with software development tasks.
You have access to the following tools:
- read_file: Read file contents
- write_file: Create or overwrite files
- edit_file: Make precise edits to existing files
- glob: Find files by pattern
- grep: Search for patterns in files
- bash: Run shell commands

## Guidelines

### Before making changes:
1. Understand the task fully before acting
2. Read relevant files to understand context
3. Plan your approach

### When editing code:
1. Use edit_file for small changes (preferred)
2. Use write_file only for new files or complete rewrites
3. Run tests after changes when possible
4. If tests fail, analyze the error and iterate

### General principles:
- Be concise but thorough
- Explain your reasoning briefly
- Ask for clarification if the task is ambiguous
- If you're stuck, say so instead of guessing

## Current Directory
You are working in: {current_directory}
"""

Но тут возникает проблема: а если у проекта есть специфические соглашения? Что если команда использует конкретный тестовый фреймворк или у репозитория нестандартная структура директорий?

Цель: проектный контекст (CLAUDE.md)

laude Code решает это через CLAUDE.md — файл в корне проекта, который автоматически добавляется в контекст:

# CLAUDE.md

## Project Overview
This is a FastAPI application for user authentication.

## Key Commands
- `make test`: Run all tests
- `make lint`: Run linting
- `make dev`: Start development server

## Architecture
- `src/api/`: API routes
- `src/models/`: Database models
- `src/services/`: Business logic
- `tests/`: Test files (mirror src/ structure)

## Conventions
- All functions must have type hints
- Use pydantic for request/response models
- Write tests before implementing features (TDD)

## Known Issues
- The /auth/refresh endpoint has a race condition (see issue #142)

Теперь ИИ знает:

  • Как запускать тесты в этом проекте

  • Где что лежит

  • Какие соглашения нужно соблюдать

  • Какие подводные камни уже известны

Это одна из самых сильных возможностей Claude Code: проектные знания, которые путешествуют вместе с кодом.

Собираем всё вместе

Посмотрим, что у нас получилось. Ядро любого агентского инструмента для программирования — это цикл:

1. Подготовка (выполняется один раз)

  • Загрузить system prompt с описаниями инструментов, поведенческими правилами и проектным контекстом (CLAUDE.md)

  • Инициализировать пустую историю диалога

2. Цикл агента (повторяется до завершения)

  • Отправить историю диалога в LLM

  • LLM решает: вызвать инструмент или ответить пользователю

  • Если нужен вызов инструмента:

1. Check permissions (prompt user if dangerous)
2.Execute the tool (read_file, edit_file, bash, glob, grep, etc.)
3. Add the result to conversation history
4. Loop back to step 2
  • Если это финальный ответ:

1. Display response to user
2. Done

Вот и всё. Любой ИИ-агент для разработки — от нашего bash-скрипта на 50 строк до Claude Code — следует этому паттерну.

А теперь давайте соберём полноценный, рабочий mini-Claude Code, которым действительно можно пользоваться. Он объединяет всё, что мы изучили: цикл агента, структурированные инструменты, проверки разрешений и интерактивный REPL:

#!/usr/bin/env python3
# mini-claude-code.py - A minimal Claude Code clone

import anthropic
import subprocess
import os
import json

client = anthropic.Anthropic()

TOOLS = [
    {
        "name": "read_file",
        "description": "Read the contents of a file",
        "input_schema": {
            "type": "object",
            "properties": {
                "path": {"type": "string", "description": "Path to the file"}
            },
            "required": ["path"]
        }
    },
    {
        "name": "write_file",
        "description": "Write content to a file (creates or overwrites)",
        "input_schema": {
            "type": "object",
            "properties": {
                "path": {"type": "string", "description": "Path to the file"},
                "content": {"type": "string", "description": "Content to write"}
            },
            "required": ["path", "content"]
        }
    },
    {
        "name": "list_files",
        "description": "List files in a directory",
        "input_schema": {
            "type": "object",
            "properties": {
                "path": {"type": "string", "description": "Directory path (default: current directory)"}
            }
        }
    },
    {
        "name": "run_command",
        "description": "Run a shell command",
        "input_schema": {
            "type": "object",
            "properties": {
                "command": {"type": "string", "description": "The command to run"}
            },
            "required": ["command"]
        }
    }
]

DANGEROUS_PATTERNS = ["rm ", "sudo ", "chmod ", "mv ", "cp ", "> ", ">>"]

def check_permission(tool_name, tool_input):
    """Check if an action requires user permission."""
    if tool_name == "run_command":
        cmd = tool_input.get("command", "")
        if any(p in cmd for p in DANGEROUS_PATTERNS):
            print(f"\n⚠️  Potentially dangerous command: {cmd}")
            response = input("Allow? (y/n): ").strip().lower()
            return response == "y"
    elif tool_name == "write_file":
        path = tool_input.get("path", "")
        print(f"\n? Will write to: {path}")
        response = input("Allow? (y/n): ").strip().lower()
        return response == "y"
    return True

def execute_tool(name, tool_input):
    """Execute a tool and return the result."""
    if name == "read_file":
        path = tool_input["path"]
        try:
            with open(path, "r") as f:
                content = f.read()
            return f"Contents of {path}:\n{content}"
        except Exception as e:
            return f"Error reading file: {e}"

    elif name == "write_file":
        path = tool_input["path"]
        content = tool_input["content"]
        try:
            with open(path, "w") as f:
                f.write(content)
            return f"✅ Successfully wrote to {path}"
        except Exception as e:
            return f"Error writing file: {e}"

    elif name == "list_files":
        path = tool_input.get("path", ".")
        try:
            files = os.listdir(path)
            return f"Files in {path}:\n" + "\n".join(f"  {f}" for f in sorted(files))
        except Exception as e:
            return f"Error listing files: {e}"

    elif name == "run_command":
        cmd = tool_input["command"]
        try:
            result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=30)
            output = result.stdout + result.stderr
            return f"$ {cmd}\n{output}" if output else f"$ {cmd}\n(no output)"
        except subprocess.TimeoutExpired:
            return f"Command timed out after 30 seconds"
        except Exception as e:
            return f"Error running command: {e}"

    return f"Unknown tool: {name}"

def agent_loop(user_message, conversation_history):
    """Run the agent loop until the task is complete."""
    conversation_history.append({"role": "user", "content": user_message})

    while True:
        # Call Claude
        response = client.messages.create(
            model="claude-opus-4-5-20251101",
            max_tokens=4096,
            system=f"You are a helpful coding assistant. Working directory: {os.getcwd()}",
            tools=TOOLS,
            messages=conversation_history
        )

        # Add assistant response to history
        conversation_history.append({"role": "assistant", "content": response.content})

        # Check if we're done (no tool use)
        if response.stop_reason == "end_turn":
            # Print the final text response
            for block in response.content:
                if hasattr(block, "text"):
                    print(f"\n? {block.text}")
            break

        # Process tool calls
        tool_results = []
        for block in response.content:
            if block.type == "tool_use":
                tool_name = block.name
                tool_input = block.input

                print(f"\n? {tool_name}: {json.dumps(tool_input)}")

                # Check permissions
                if not check_permission(tool_name, tool_input):
                    result = "Permission denied by user"
                    print(f"   ? {result}")
                else:
                    result = execute_tool(tool_name, tool_input)
                    # Truncate long output for display
                    display = result[:200] + "..." if len(result) > 200 else result
                    print(f"   → {display}")

                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": result
                })

        # Add tool results to conversation
        conversation_history.append({"role": "user", "content": tool_results})

    return conversation_history

def main():
    print("Mini Claude Code")
    print(" Type your requests, or 'quit' to exit.\n")

    conversation_history = []

    while True:
        try:
            user_input = input("You: ").strip()
        except (EOFError, KeyboardInterrupt):
            print("\nGoodbye!")
            break

        if not user_input:
            continue
        if user_input.lower() in ["quit", "exit", "q"]:
            print("Goodbye!")
            break

        conversation_history = agent_loop(user_input, conversation_history)

if __name__ == "__main__":
    main()

Сохраните это как mini-claude-code.py и запустите:

uv run --with anthropic python mini-claude-code.py

Вот как выглядит сессия:

Mini Claude Code
 Type your requests, or 'quit' to exit.

You: create a python file that prints the fibonacci sequence up to n

? write_file: {"path": "fibonacci.py", "content": "def fibonacci(n):\n    ..."}

? Will write to: fibonacci.py
Allow? (y/n): y
   → ✅ Successfully wrote to fibonacci.py

? I've created fibonacci.py with a function that prints the Fibonacci sequence.
   Would you like me to run it to test it?

You: yes, run it with n=10

? run_command: {"command": "python fibonacci.py 10"}
   → $ python fibonacci.py 10
     0 1 1 2 3 5 8 13 21 34

? The script works correctly! It printed the first 10 Fibonacci numbers.

You: quit
Goodbye!

Это рабочий мини-клон Claude Code примерно на 150 строк. В нём есть:

  • Интерактивный REPL: сохраняет контекст диалога между запросами

  • Несколько инструментов: чтение, запись, листинг файлов, запуск команд

  • Проверки разрешений: спрашивает перед записью файлов или выполнением опасных команд

  • Память диалога: каждый следующий запрос опирается на предыдущий контекст

По сути, это и есть то, что делает Claude Code, плюс:

  • Отполированный терминальный UI

  • Продвинутая система разрешений

  • Уплотнение контекста, когда диалоги становятся длинными

  • Делегирование подагентам для сложных задач

  • Хуки для кастомной автоматизации

  • Интеграция с git и другими инструментами разработки

Claude Agent SDK

Если вы хотите развивать эту основу, не изобретая всё заново, Anthropic предлагает Claude Agent SDK. Это тот же движок, на котором работает Claude Code, но в виде библиотеки.

Вот как выглядит наш простой агент с использованием SDK:

import { query } from "@anthropic-ai/claude-agent-sdk";

for await (const message of query({
  prompt: "Fix the bug in auth.py",
  options: {
    model: "claude-opus-4-5-20251101",
    allowedTools: ["Read", "Edit", "Bash", "Glob", "Grep"],
    maxTurns: 50
  }
})) {
  if (message.type === "assistant") {
    for (const block of message.message.content) {
      if ("text" in block) {
        console.log(block.text);
      } else if ("name" in block) {
        console.log(`Using tool: ${block.name}`);
      }
    }
  }
}

SDK берёт на себя:

  • Цикл агента (вам не нужно реализовывать его вручную)

  • Все встроенные инструменты (Read, Write, Edit, Bash, Glob, Grep и т. д.)

  • Управление разрешениями

  • Отслеживание контекста

  • Координацию подагентов

Чему мы научились

Начиная с простого bash-скрипта, мы пришли к следующим выводам:

  • Цикл агента: ИИ решает → выполняет → наблюдает → повторяет

  • Структурированные инструменты: лучше, чем чистый bash, с точки зрения безопасности и точности

  • Точечные правки: str_replace лучше, чем полная перезапись файлов

  • Инструменты поиска: позволяют ИИ исследовать кодовые базы

  • Управление контекстом: уплотнение и делегирование решают проблему длинных задач

  • Проектные знания: CLAUDE.md даёт проектно-специфичный контекст

Каждый из этих шагов появился из практической боли:

  • «Как заставить ИИ делать больше одной вещи?» → цикл агента

  • «Как не дать ему угробить систему?» → система разрешений

  • «Как делать правки эффективно?» → инструмент str_replace

  • «Как ему находить код, о котором он ничего не знает?» → инструменты поиска

  • «Что делать, когда заканчивается контекст?» → уплотнение

  • «Откуда ему знать соглашения моего проекта?» → CLAUDE.md

Вот так вы и могли бы изобрести Claude Code. Базовые идеи на удивление просты.

Как и прежде, сложность появляется при работе с пограничными случаями, создании хорошего UX и интеграции с реальными процессами разработки.

Следующие шаги

Если вы хотите писать собственных агентов:

  • Начинайте с простого: базовый цикл агента и 2–3 инструмента

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

  • Аккуратно обрабатывайте ошибки: инструменты ломаются, агент должен уметь восстанавливаться

  • Тестируйте на реальных задачах: именно пограничные случаи покажут, чего не хватает

  • Рассмотрите Claude Agent SDK: зачем изобретать велосипед?

Будущее разработки — за агентами, которые действительно умеют что-то делать. Теперь мы понимаем, как они устроены.

Ресурсы

Русскоязычное сообщество про AI в разработке

Друзья! Эту статью подготовила команда ТГК «AI for Devs» — канала, где мы рассказываем про AI-ассистентов, плагины для IDE, делимся практическими кейсами и свежими новостями из мира ИИ. Подписывайтесь, чтобы быть в курсе и ничего не упустить!

Комментарии (1)


  1. Markgresilov
    13.01.2026 17:58

    А если ещё туда какой-нибудь жпт приделать для автогенерации этих запросов агенту, то разработчик (а это уже не разработчик) ничего не будет делать. Выходит, ИИ будет писать всё, а мы только пользоваться на уровне потребителя, который нифига не знает об устройстве этого кода? Как по-моему, надо наоборот - пусть ИИ будет делать рутину, люди - писать код. Вот это называется ВАЙБ-кодинг. Когда не ИИ берёт всю волокиту, а разработчик целиком отдан качественному коду.