Главная / Статьи

Внутренние механизмы контекста и памяти Claude Code: перепроектированы как агент LangGraph

Как Claude Code формирует вызовы API, запускает сабагентов и сжимает длинные сессии, а затем те же идеи реализованы в LangGraph с использованием механизмов сохранения состояния и файлов памяти.

Издатель: REACTAPP.TOP11 мин чтения2610 слов

Источники: Scaling Agent Harnesses for Long-Running Sessions

Детали утекшего исходного кода Claude Code v2.1.87 показывают, как инструмент обрабатывает контекст и память во время длительных сессий программирования. Он решает типичные проблемы долгосрочной работы агента: контекст, превышающий объем памяти модели, вывод инструмента, который невозможно предсказать, восстановление памяти из предыдущих сессий в нужный момент и сохранение инструкций к проекту после их краткого изложения. Далее описывается механизм работы и затем создается упрощенная версия с использованием LangGraph с встроенными функциями сохранения состояния.

Как Claude Code управляет контекстом

Запрос API состоит из трех частей. Две из них — системное уведомление (идентификатор и контекст Git) и массив инструментов (JSON-схемы для Bash, Read, Edit, Skill, Agent и MCP) — находятся вне массива сообщений, поэтому процесс сжатия к ним не применяется. Остальная часть включается в массив сообщений в виде записей с указанием роли пользователя, заключённых в теги <system-reminder>: файлы с инструкциями в самом верху, ход диалога и результаты работы инструментов в середине, а записи приложений (записи из памяти, списки навыков и агентов, инструкции MCP, результаты выполнения хуков) добавляются после каждого цикла работы инструментов. Когда окно заполняется, процесс сжатия сокращает каждое сообщение и восстанавливает важные приложения из их исходных источников. Затем следуют цикл запросов, субагенты и процесс сжатия.

Цикл запросов

Ввод сообщения запускает цикл, в котором каждая итерация представляет собой один вызов API, и он продолжается до тех пор, пока модель не даст ответ без использования каких-либо инструментов.

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

На каждой итерации массив сообщений в памяти сокращается перед отправкой в API. Сначала список сокращается до элементов, находящихся после последней границы сжатия, причем полный файл JSONL на диске не изменяется. Затем любой слишком большой результат работы инструмента записывается в отдельный файл, а вместо него вставляется превью объемом 2 кБ. Если после этого количество токенов все еще превышает примерно 93% от размера окна контекста, происходит сжатие.

Ответ может быть либо простым текстом, который завершает текущий этап, либо одним или несколькими вызовами инструментов, которые выполняются и вновь запускают цикл. Инструменты делятся на группы: общие, такие как Bash, Read, Edit и Grep; инструмент Skill, который включает в разговор полный файл SKILL.md; инструмент Agent, который запускает изолированный подагента, возвращающий один результат; и ToolSearch, который загружает схему инструмента для следующей итерации. По окончании каждого раунда с использованием инструментов вставляются вложения с изменениями для списков навыков, списков агентов, инструкций MCP и любых завершённых операций предзагрузки памяти, причём передаётся только та информация, которая изменилась с прошлого прохождения.

Подагенты

Использование инструмента «Позвонить агенту» заставляет Claude Code создать изолированный контекст внутри того же процесса. Подагент начинает работу с копией разговора родителя, получает собственные инструменты и системное сообщение, работает с отключенным режимом обдумывания для снижения затрат на токены и самостоятельно выполняет тот же код цикла запросов. Агенты типа «Исследовать» и «Планировать» также теряют доступ к файлу CLAUDE.md и информации о статусе git из этой копии, что позволяет сэкономить дополнительные токены.

Только окончательное сообщение с результатом возвращается родителю. Запросы к инструментам, процессы обдумывания и любая компактация данных внутри подагента записываются в отдельный файл-транскрипт subagents/agent-{id}.jsonl, который родитель никогда не открывает. Таким образом затраты остаются изолированными: десятки внутренних запросов к инструментам могут привести к появлению всего одного сообщения в контексте родителя.

Компактация

В коде существует микрокомпактный механизм для сокращения результатов работы старых инструментов перед полной компактацией, но он отключен и не используется здесь. Сама процедура компактации предусматривает две стратегии, причем вторая выступает в качестве альтернативы, когда первая недоступна. Первая стратегия немедленно заменяет ее подготовленный файл памяти сессии, поэтому компактация не требует отдельного вызова API; фоновый агент постепенно собирает этот файл путем последовательных отдельных вызовов, причем данный путь в настоящее время отключен из-за ограничений функционала. Вторая стратегия, используемая в настоящее время, представляет собой полную компактацию с помощью LLM. Форкованный агент получает текущие сообщения и структурированный запрос на компактацию, сокращая разговор примерно до пяти тысяч токенов. Самые новые сообщения сохраняются без изменений рядом с этим резюме, чтобы модель по-прежнему имела свой непосредственный рабочий контекст, а все более старые сообщения заменяются резюме.

Далее восстанавливаются все вложения, которые ранее находились в messages[] и были упрощены в результате суммирования. Списки агентов, названия отложенных инструментов и инструкции MCP снова публикуются полностью, а скрипты запуска при начале сессии выполняются заново. Содержимое навыков, которое использовалось, возвращается из кэша в памяти объемом примерно 25 000 токенов, а пять самых недавно обрабатываемых файлов снова считываются с диска. Системный промпт, tools[], файл Claude.md и контекст git никогда не находились в messages[], поэтому процесс сжатия не может повлиять на них; их кэши просто очищаются, чтобы в следующем этапе использовались свежие значения.

Намеренно упущена одна деталь: каталог навыков не пересоздается, что позволяет сэкономить примерно 4 000 токенов. Побочным эффектом является то, что после сжатия у модели нет способа найти навыки, которыми она ещё не пользовалась, если только они не указаны в Claude.md. Сжатие также приводит к потере деталей, поскольку предыдущее резюме суммируется вместе со всем остальным, и к третьему проходу исходные факты сжимаются в три раза. Ничего по-настоящему не исчезает, поскольку файл JSONL с возможностью только добавления данных на диске хранит всю историю, а маркер границы сжатия (parentUuid: null) указывает загрузчику, с какого места начать восстановление.

Упрощённая версия LangGraph

С помощью LangGraph можно создать уменьшенную версию той же архитектуры. Он уже предоставляет инструменты для сохранения состояния и функцию RemoveMessage для удаления сообщений. Механизм работы с памятью между сессиями следует тому же принципу, что и в Claude Code: при каждом шаге считываются файлы в формате Markdown с диска. Логика сжатия — это та часть, которую необходимо написать самостоятельно.

На диаграмме сопоставления каждая стрелка соединяет концепцию из Claude Code слева с её эквивалентом в LangGraph справа. Ребро, отмеченное как «улучшение», указывает на то, что LangGraph позволяет хранить инструкции вне структуры messages[], поэтому процесс сжатия никогда не затрагивает их.

Граф состоит из пяти узлов. Пунктирные ребра, исходящие из call_model, являются условными: они передают вызовы инструментов обратно через tools, а текстовые ответы — в post_turn. В функции load_context файлы памяти (.agent/memory/*.md) считываются с диска, затем включаются в объект SystemMessage в call_model. Это позволяет полностью исключить их из массива messages[], так что они остаются нетронутыми в процессе сжатия.

Создание графа

Узлы выполняются по порядку, и после call_model условная ветвь либо переходит к использованию инструментов, либо направляется в post_turn. Показатель состояния SQLite сохраняет всю информацию о состоянии при перезапуске процесса.

def build_graph():
    builder = StateGraph(AgentState)

    builder.add_node("load_context", load_context)
    builder.add_node("prune_and_compact", prune_and_compact)
    builder.add_node("call_model", call_model)
    builder.add_node("tools", ToolNode(TOOLS))
    builder.add_node("post_turn", post_turn)

    builder.add_edge(START, "load_context")
    builder.add_edge("load_context", "prune_and_compact")
    builder.add_edge("prune_and_compact", "call_model")
    builder.add_conditional_edges(
        "call_model", route_after_model,
        {"tools": "tools", "post_turn": "post_turn"},
    )
    builder.add_edge("tools", "call_model")
    builder.add_edge("post_turn", END)

    checkpointer = SqliteSaver(sqlite3.connect("checkpoints.sqlite"))
    return builder.compile(checkpointer=checkpointer)

# Subagent: separate StateGraph with own call_model + tools loop
subagent_builder = StateGraph(SubagentState)
subagent_builder.add_node("call_model", subagent_call_model)
subagent_builder.add_node("tools", ToolNode([bash, read_file, write_file]))
subagent_builder.add_edge(START, "call_model")
subagent_builder.add_conditional_edges("call_model", subagent_route,
    {"tools": "tools", "__end__": END})
subagent_builder.add_edge("tools", "call_model")
subagent_graph = subagent_builder.compile()

Связь между двумя графиками находится внутри инструмента run_subagent:

@tool
def run_subagent(task: str) -> str:
    """Run an isolated subagent for a specific task."""
    result = subagent_graph.invoke({"messages": [HumanMessage(content=task)]})
    return result["messages"][-1].content  # only final result

Как только модель вызывает функцию run_subagent, основной узел tools выполняет эту функцию, что приводит к запуску графа субагента, который отдельно выполняет свои циклы call_model и tools. Возвращается только окончательный ответ в виде строки, поэтому основной граф воспринимает его как обычный результат работы инструмента. У субагента нет инструкций, файлов памяти или механизмов контроля, поэтому его контекст ограничен, и каждый вызов является автономным. В примере представлен один тип субагента. Claude Code на этапе выполнения выбирает из нескольких вариантов (инструмент Explore для поиска, инструмент Plan для планирования, универсальный тип с доступом ко всем инструментам, а также плагин-агенты вроде vercel:ai-architect); у каждого из них свои инструменты, разрешения и системные подсказки.

Подсказки

Это упрощённые версии, используемые в репликации LangGraph; настоящие подсказки Claude Code более детальны.

Собственный компактный шаблон Claude Code гораздо объемнее: он включает девять разделов, область для заметок <analysis>, вступление с указанием «КРИТИЧЕСКО ВАЖНО: ОтвечайТЕ ТОЛЬКО ТЕКСТОМ» и пример блока формата. Упрощенная версия ниже сохраняет основные элементы:

Your task is to create a detailed summary of the conversation so far,
paying close attention to the user's explicit requests and your previous actions.
This summary should capture technical details, code patterns, and decisions
that would be essential for continuing the work without losing context.

Before writing the summary, analyze the conversation chronologically in <analysis> tags.
Then provide the summary in <summary> tags with these sections:

1. Primary Request and Intent: The user's explicit requests in detail.
2. Key Technical Concepts: Important technologies, frameworks, and patterns discussed.
3. Files and Code Sections: Specific files examined, modified, or created, with paths
   and why each was important.
4. Errors and Fixes: Errors encountered, how they were fixed, and any user corrections
   or feedback (pay special attention to "don't do X" or "actually I meant Y").
5. Problem Solving: Problems solved and any ongoing troubleshooting.
6. Pending Tasks: Tasks explicitly requested but not yet completed.
7. Current Work: What was being worked on immediately before this summary,
   with specific file names and details from the most recent messages.

В коде сначала происходит разделение разговора: функция trim_messages определяет последнюю часть, которую необходимо сохранить. Только более старая часть передается в модель с соответствующим запросом, и эта модель видит всё содержимое (сообщения пользователя, ответы ассистента, результаты работы инструментов) и сжимает его. Полная реализация приведена в разделе, посвящённом сжатию, ниже.

Запрос на обновление памяти сессии в Claude Code использует шаблон из десяти разделов с ограничениями по количеству токенов в каждом разделе, замену {{variableName}} и строгими правилами по сохранению заголовков разделов. Упрощённая версия отражает основную идею:

Based on the conversation above, update the session notes.

Update each section below with specific, actionable details from the conversation.
Write info-dense content: file paths, function names, error messages, exact commands.
Skip a section if there are no new insights to add. Do not add filler.
Keep each section under 500 words. If approaching the limit, cycle out less important
details while preserving the most critical information.
Always update Current State to reflect the most recent work.

Sections:
# Session Title
# Current State
# Task Specification
# Files and Functions
# Errors and Corrections
# Key Results
# Next Steps

Шаблон отчёта включает разделы для описания рабочего процесса, документации кодовой базы, выводов из опыта, ключевых результатов и записей о выполненной работе; каждый раздел ограничен 2 000 токенами, а весь файл — 12 000 токенами. Для редактирования разделов используется фоновый агент с инструментом FileEdit без полной перезаписи файла.

Состояние: инструкции вне сообщений

При каждом вызове API инструкции и данные памяти загружаются с диска и помещаются в объект SystemMessage, как это делает функция prependUserContext() в Claude Code. Они не хранятся в массиве messages[], поэтому процесс сжатия их игнорирует. Содержимое вызванных навыков и память сессии хранятся в полях состояния, которые сохраняются при сжатии:

class AgentState(MessagesState):
    summary: str                    # compaction summary
    instructions: str               # CLAUDE.md equivalent (never compacted)
    invoked_skills: dict[str, str]  # skill name → content (survives compaction)
    session_memory: str | None      # background summary for instant compaction

# Cross-session memory: .agent/memory/*.md files read from disk every turn.
# Survives compaction (re-read from disk, never in messages[]).
# Agent creates memories via write_file tool. For production at scale,
# use a lightweight LLM call to select relevant files (Claude Code's approach).

Сжатие: краткое изложение старых сообщений, сохранение новых

Дизайн соответствует принципам Claude Code. Новые сообщения остаются нетронутыми, поскольку модель только что работала с ними, а краткое изложение получают только более старые сообщения.

def compact(state: AgentState) -> dict:
    messages = state["messages"]

    # Keep recent messages (Claude Code uses calculateMessagesToKeepIndex)
    kept = trim_messages(
        messages, strategy="last",
        token_counter=count_tokens_approximately,
        max_tokens=COMPACT_THRESHOLD // 5,
        start_on="human", end_on=("human", "tool"),
    )
    kept_ids = {id(m) for m in kept}

    # Summarise only the OLDER messages
    to_summarise = [m for m in messages if id(m) not in kept_ids]
    if not to_summarise:
        return {}

    response = model.invoke(
        to_summarise + [HumanMessage(content=COMPACT_PROMPT)]
    )

    # Delete old, keep recent, inject summary + re-inject skills
    delete = [RemoveMessage(id=m.id) for m in to_summarise]
    summary = HumanMessage(content=f"[Summary]\n{response.content}")
    skill_msgs = [
        HumanMessage(content=f"[Skill: {n}]\n{c[:5000]}")
        for n, c in state.get("invoked_skills", {}).items()
    ]
    return {"messages": [*delete, summary, *skill_msgs], "summary": response.content}

Результат представляет собой краткое резюме, затем сохранённые недавние сообщения, после чего восстанавливаются навыки; резюме не заменяет всё остальное. Вызов функции RemoveMessage приводит к созданию новой точки контроля без старых сообщений, в то время как более ранние точки контроля остаются в базе данных для восстановления, точно так же как формат JSONL в Claude Code сохраняет записи до их сжатия.

Память: на основе файлов, читается при каждом ходе

В примере показано, как на самом деле работает Claude Code. Файлы памяти хранятся на диске по пути .agent/memory/*.md; функция call_model читает их при каждом ходе, и они никогда не попадают в массив messages[]. Поскольку они снова читаются с диска, а не воссоздаются на основе хода разговора, сжатие не может привести к их потере.

# In call_model — read memory files from disk every turn
MEMORY_DIR = Path(".agent/memory")
if MEMORY_DIR.exists():
    for mem_file in MEMORY_DIR.glob("*.md"):
        content = mem_file.read_text()[:4096]
        system_parts.append(f"Memory ({mem_file.stem}):\n{content}")

# Agent creates memories by writing files:
# write_file(".agent/memory/user_prefs.md", "User prefers concise responses.")

Claude Code тратит отдельный вызов Sonnet за ход на выбор соответствующих файлов памяти — до 5 из потенциально большого числа: быстрая модель получает вопрос вместе с списком имен и описаний файлов и возвращает наилучшие результаты. В примере загружаются все файлы, что упрощает процесс, но не позволяет масштабироваться. В производственных средах с большим количеством источников памяти следует придерживаться первоначального подхода: легковесный вызов LLM с вопросом и индексом памяти определяет, что загружать.

Навыки: инструмент с постепенным раскрытием информации

@tool
def load_skill(skill_name: str) -> str:
    """Load a specialised skill prompt by name."""
    return SKILL_REGISTRY.get(skill_name, f"Unknown skill: {skill_name}")

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

Что сохраняется после сессии

Рассмотрим ситуацию, когда пользователь задает несколько вопросов, запускает процедуру сжатия, а затем возвращается позже. Сохраненные данные агента будут выглядеть следующим образом:

.agent-data/
  checkpoints.sqlite       ~5 MB   (all state snapshots, grows with session)
  tool-results/
    bash-84ffc64a.txt      15 KB   (large bash output persisted with preview)

.agent/
  memory/
    project_goal.md        206 B   (cross-session, survives compaction)
    user_prefs.md          125 B   (read from disk every turn)

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

~/.claude/projects/{cwd}/
  {sessionId}.jsonl        22 MB   (full transcript, never deleted)
  {sessionId}/
    tool-results/          12 files
    subagents/             96 files (subagent transcripts + compact backups)
    session-memory/        (disabled)
  history.jsonl            13k lines (prompt recall, UI only)
  memory/                  4 files (auto-memory)

В LangGraph чекпоинты хранят снимки состояния вместо журнала событий, что снижает размер использования ресурсов. Более ранние чекпоинты остаются и могут быть просмотрены с помощью метода graph.get_state_history(config) во время отладки или восстановления.

Что обрабатывает реплика

Реплика LangGraph отвечает за основные этапы работы: загрузку инструкций с диска, сжатие старых сообщений при сохранении свежих, сохранение результатов работы инструментов в файлы и повторное чтение памяти между сессиями. В производственной системе добавляется функция выбора памяти с использованием LLM через быстрый вызов Sonnet, дельта-файлы, описывающие навыки и агентов, изолированные подагенты, хуки из плагинов и обработка кэша промптов. Благодаря такой структуре это удобный справочник для тех, кто создаёт собственных агентов.