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

Прекратите заставлять вашего агента для программирования перечитывать кодбейс: шаблон LLM Wiki

Идея Wiki для LLM от Karpathy и openwiki от LangChain: исходные данные, вики, ориентированная на агентов, и указатель AGENTS.md, плюс задачи обновления, оценка и отслеживание.

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

Источники: Stop Making Your AI Agent Re-Read Your Entire Codebase Every Session

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

Андрей Карпати описал эту проблему и предложил решение, названное им шаблоном LLM Wiki. LangChain выпустил его реализацию с открытым исходным кодом — openwiki. Ниже шаблон и его реализация рассматриваются параллельно: что представляет собой эта идея, почему она важна и как работает код демо-приложения. В результате получается умственная модель, которую можно применять к любой кодовой базе, которую необходимо понять агенту.

Агенты с амнезией

Во-первых, способ сбоя в точных терминах. Обычный ответ на вопрос «Как агент понимает кодовую базу?» — это RAG. Файлы исходного кода встраиваются, помещаются в векторную базу данных, а при запросе извлекаются соответствующие фрагменты.

RAG подходит для ответов на вопросы. Однако для агентов-программистов он имеет три недостатка.

Понимание пересчитывается каждый раз. Предположим, агенту нужна связь между agent.py и memory.py. Он извлекает фрагменты, модель снова находит ответ, после чего результат удаляется.

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

Он становится устаревшим бесшумно. Хранилище векторов даже не подозревает, что memory.py был переписан в прошлый вторник, и с полной уверенностью предоставляет старые версии данных.

Идея Карпати была проста: пусть сама модель пишет документацию в формате, подходящем для её собственного чтения, и автоматически обновляет её. Речь идет о документации для агентов, а не для людей.

Три уровня

Каждый элемент openwiki относится к одному из трех уровней в концепции Карпати.

┌─────────────────────────────────────────────────┐
│  Layer 3: The Schema                            │
│  AGENTS.md / CLAUDE.md — "the wiki exists here" │
├─────────────────────────────────────────────────┤
│  Layer 2: The Wiki                              │
│  openwiki/*.md — LLM-generated structured docs  │
├─────────────────────────────────────────────────┤
│  Layer 1: Raw Sources                           │
│  Your actual codebase — immutable to the wiki   │
└─────────────────────────────────────────────────┘

Каждый слой располагается на том, который находится под ним.

Слой 1: исходные источники

Исходные источники — это просто ваш кодовый базис. С точки зрения вики они являются неизменяемыми: OpenWiki читает их, но никогда не изменяет.

В демонстрационном проекте исходники хранятся в каталоге sample_agent/ — это реалистичный агент LangChain ReAct, который узнают большинство инженеров в области ИИ.

# sample_agent/agent.py
from langchain.agents import AgentExecutor, create_react_agent
from langchain_openai import ChatOpenAI
from .memory import build_memory
from .prompts import build_prompt
from .tools import TOOLS
def build_agent(
    model: str = "openai/gpt-4o-mini",
    base_url: str = "https://openrouter.ai/api/v1",
    api_key: str | None = None,
    temperature: float = 0.0,
) -> AgentExecutor:
    llm = ChatOpenAI(model=model, base_url=base_url, api_key=api_key, temperature=temperature)
    prompt = build_prompt()
    memory = build_memory()
    agent = create_react_agent(llm=llm, tools=TOOLS, prompt=prompt)
    return AgentExecutor(
        agent=agent,
        tools=TOOLS,
        memory=memory,
        verbose=True,
        handle_parsing_errors=True,
        max_iterations=10,
    )

Вместе соединены четыре компонента: клиент-модель (ChatOpenAI), шаблон запроса, память разговора и инструменты. Это типичный пример архитектуры ReAct — именно такой код предназначен для OpenWiki. Инструменты преднамеренно оставлены простыми: имитация поиска и безопасный калькулятор:

# sample_agent/tools.py
@tool
def calculator(expression: str) -> str:
    """Evaluate a safe mathematical expression and return the result."""
    allowed_names: dict = {}
    try:
        result = eval(expression, {"__builtins__": {}}, allowed_names)
        return str(result)
    except Exception as exc:
        return f"Error evaluating '{expression}': {exc}"

Посмотрите на {"__builtins__": {}}. Он удаляет все встроенные функции Python из области видимости функции eval, чтобы пользователь не мог передать что-то вроде __import__('os').system('rm -rf /') и заставить его выполниться. Небольшая деталь с реальными последствиями для безопасности.

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

Уровень 2: сама вики

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

Как OpenWiki это создаёт

В приложении Streamlit демо кнопка «Сгенерировать вики» запускает следующее:

# app.py
def run_openwiki(
    args: list[str],
    provider: str,
    api_key: str,
    model: str,
    langsmith: str,
    cf_account_id: str = "",
) -> tuple[str, int]:
    import sys
    env = _build_openwiki_env(provider, api_key, model, langsmith, cf_account_id)
    result = subprocess.run(
        ["openwiki", *args],
        cwd=str(SAMPLE_AGENT_DIR),
        env=env,
        capture_output=True,
        text=True,
        timeout=300,
        shell=sys.platform == "win32",  # npm .cmd files need shell=True on Windows
    )
    return result.stdout + result.stderr, result.returncode

Важны два момента.

Во-первых, значение cwd=SAMPLE_AGENT_DIR выбрано намеренно. Интерфейс командной строки openwiki сканирует ту папку, из которой запущен. Установка рабочей директории в sample_agent/ ограничивает сканирование только этим кодовым репозиторием, а не всим проектом. Если этот параметр игнорировать, программа будет сканировать корневую директорию проекта, в результате чего будет сгенерирована вики-страница, охватывающая тесты, настройки CI и папку node_modules.

Во-вторых, параметр shell=True в Windows. Пакеты npm устанавливаются там в виде оберточных скриптов формата .cmd. При вызове subprocess.run(["openwiki", ...]) ищется бинарный файл с именем openwiki, который не находится, в результате чего возникает ошибка FileNotFoundError: [WinError 2]. Если передать параметр shell=sys.platform == "win32", вызов будет направлен через cmd.exe, который правильно обрабатывает файлы .cmd. Сначала это может вызвать путаницу, но потом становится очевидным.

Что означает форматирование «агент-ориентированное»

Человеческая документация пишется так, чтобы её читали сверху вниз. Вы открываете файл README.md, начинаете с верхней части и постепенно собираете необходимую информацию. Агенты не читают так. Они сканируют заголовки в поисках соответствующих разделов, извлекают необходимые факты и переходят дальше.

Документация, ориентированная на агентов, создается с учётом этого поведения:

  • Плотные заголовки на каждом уровне, что позволяет быстро ознакомиться со структурой без необходимости читать простой текст.
  • Явные переходы, такие как See: patterns.md#memory-strategy, которые агент выполняет механически, а не на основе интуиции.
  • Краткие резюме в одном абзаце для каждого компонента: достаточно, чтобы решить, стоит ли продолжать чтение, но не предназначены для полного описания.
  • Отсутствие лишнего текста. Игнорируйте вступительные фразы вроде «В этом документе мы рассмотрим…» и любые заключительные сводки.

Генерированный файл openwiki/architecture.md для примера агента может выглядеть следующим образом:

# Architecture — sample_agent
## Overview
ReAct agent built on LangChain AgentExecutor. Entry point: agent.py.
Four components: LLM (ChatOpenAI via OpenRouter), tools, memory, prompt.
## Components
- agent.py - AgentExecutor wiring. See: components.md#agent
- tools.py - search (simulated) + calculator (sandboxed eval). See: components.md#tools
- memory.py - ConversationBufferWindowMemory, k=5 turns
- prompts.py - ReAct ChatPromptTemplate with agent_scratchpad placeholder
- evaluator.py - LLM-as-judge chain. See: patterns.md#evaluation
## Data Flow
question → AgentExecutor.invoke() → ReAct loop (max 10 iter)
→ tool calls → final answer → evaluator.evaluate() (optional)

Поместите это рядом с типичным человеческим README, содержащим три абзаца с введением перед тем, как появятся конкретные данные. Агентам не нужно введение — им нужны факты.

Уровень 3: схема, которая замыкает цикл

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

В этой роли находятся файлы AGENTS.md и CLAUDE.md» — инструкционные файлы, которые Claude Code, Cursor, GitHub Copilot и большинство современных кодирующих агентов читают в начале сессии. OpenWiki вставляет краткую ссылку в тот из этих файлов, который используется в вашем проекте. До этого:

# sample_agent/AGENTS.md (before OpenWiki)
This repository contains a LangChain ReAct agent.
## Architecture
- agent.py - AgentExecutor entry point
- tools.py - available tools (search, calculator)
- memory.py - conversation memory strategy
- prompts.py - system prompt and ReAct template
- evaluator.py - LLM-as-judge evaluation chain

После запуска openwiki --init:

# sample_agent/AGENTS.md (after OpenWiki injection)
This repository contains a LangChain ReAct agent.
## Architecture
- agent.py - AgentExecutor entry point
...
## OpenWiki Context
When searching for codebase context, check `openwiki/` first.
Key files:
- `openwiki/architecture.md` - overall structure and file responsibilities
- `openwiki/patterns.md` - coding patterns and conventions
- `openwiki/components.md` - detailed component documentation
The wiki is auto-updated daily. Always prefer wiki context over re-reading source files.

Инъекция делается намеренно короткой. Если поместить всю вики-информацию в файл с инструкциями, это приведёт к исчерпанию окна контекста при каждой сессии при работе с большой базой кода. Достаточно указателя — агент читает его, открывает openwiki/architecture.md только тогда, когда требуется контекст базы кода, и узнаёт необходимую информацию, не трогая ни одного исходного файла.

Четвёртая вкладка демо-версии показывает ситуацию до и после, имитируя процесс инъекции, чтобы механизм был виден:

# app.py — Tab 4
agents_md_content = AGENTS_MD.read_text(encoding="utf-8")
openwiki_injection = """
## OpenWiki Context
When searching for codebase context, check `openwiki/` first.
...
"""
col_before, col_after = st.columns(2)
with col_before:
    st.code(agents_md_content, language="markdown")
with col_after:
    st.code(agents_md_content + openwiki_injection, language="markdown")

Выбор поставщика модели

OpenWiki не обращает внимания на то, какая LLM находится у неё за спиной. Она использует ChatOpenAI от LangChain, который загружает данные с OPENAI_BASE_URL для кастомного конечного пункта, поэтому подойдёт любой поставщик, совместимый с OpenAI. В демо-версии поддерживаются два таких поставщика:

# app.py
def _build_openwiki_env(
    provider: str,
    api_key: str,
    model: str,
    langsmith: str,
    cf_account_id: str = "",
) -> dict:
    env = {**os.environ, "OPENWIKI_MODEL_ID": model}
if provider == "Cloudflare Workers AI" and cf_account_id:
        cf_base = f"https://api.cloudflare.com/client/v4/accounts/{cf_account_id}/ai/v1"
        env["OPENAI_API_KEY"] = api_key
        env["OPENAI_BASE_URL"] = cf_base      # LangChain reads this automatically
    else:
        env["OPENROUTER_API_KEY"] = api_key   # openwiki's native key name
    return env

OpenRouter — это простой вариант. Одна клавиша позволяет запустить большинство основных моделей через один конечный пункт, а установка параметра OPENWIKI_MODEL_ID=openai/gpt-4o-mini завершает настройку.

Cloudflare Workers AI обслуживает модели непосредственно с края сети. Модель GLM от ZhipuAI (@cf/zai-org/glm-5.2) работает на собственной инфраструктуре Cloudflare, поэтому задержки снижаются при переходе на производственную среду, если требуется использование только одного облачного провайдера. Поскольку Cloudflare предоставляет конечный пункт, совместимый с OpenAI, по адресу /ai/v1, не требуется специальный клиент. Направьте на него параметр OPENAI_BASE_URL, и уже существующая интеграция LangChain выполнит остальное.

Обновление вики

Любая система документации в конечном итоге терпит неудачу по одной и той же причине: люди забывают её обновлять при изменении кода.

По мнению Карпати, речь идет не о проблеме дисциплины, а о проблеме экономики. Расходы на техническое обслуживание растут по мере увеличения объема кода, в то время как человек, занимающийся этим, не получает немедленной награды. В конечном итоге документация сильно отклоняется от реальности, и никто ей не верит, после чего цикл начинается заново.

В OpenWiki ответ — openwiki --update. Вместо повторного сканирования всего контента он запрашивает информацию у git log о коммитах, сделанных после последнего запуска, использует git diff, чтобы прочитать только измененные файлы, и перезаписывает лишь те страницы, которые затронуты этими изменениями. GitHub Action, встроенный в проект, запускает эту процедуру по расписанию:

# .github/workflows/openwiki-update.yml
name: OpenWiki Update
on:
  schedule:
    - cron: "0 8 * * *"   # 08:00 UTC daily
  workflow_dispatch:
jobs:
  update-wiki:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0   # full git history so openwiki can read diffs
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
      - run: npm install -g openwiki
      - name: Update wiki
        working-directory: sample_agent     # scope to our codebase
        run: openwiki --update --print
        env:
          OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
          OPENWIKI_MODEL_ID: openai/gpt-4o-mini
      - uses: peter-evans/create-pull-request@v7
        with:
          commit-message: "docs: update openwiki pages"
          title: "OpenWiki: daily documentation update"
          branch: openwiki/auto-update

Обратите внимание на параметр working-directory: sample_agent — он соответствует тому же выбору области действия, что и при вызове подпроцесса в Python. Где бы вы ни развернули этот код, запускайте команду --update из корня кодовой базы, а не из корня проекта.

Правильным подходом является создание pull request. Автоматическое коммитирование в репозиторий main позволит изменять документацию без проверки, тогда как PR предоставляет возможность просмотреть различия и отклонить некорректные результаты работы модели. Пятая вкладка демо-приложения имитирует процесс обновления локально:

# app.py — Tab 5
if st.button("🔄 Run openwiki --update", type="primary"):
    with st.spinner("Checking for changes and updating wiki…"):
        output, returncode = run_openwiki(
            ["--update", "--print"],
            provider,
            api_key if provider == "Cloudflare Workers AI" else openrouter_key,
            model_id,
            langsmith_key,
            cf_account_id,
        )
    st.code(output, language="bash")

Оценщик в виде ЯЗЫКа-судьи

Помимо агента существует оценщик в виде ЯЗЫКа-судьи — техника, которая заслуживает отдельного рассмотрения.

# sample_agent/evaluator.py
def evaluate(
    question: str,
    response: str,
    model: str = "openai/gpt-4o-mini",
    base_url: str = "https://openrouter.ai/api/v1",
    api_key: str | None = None,
) -> dict[str, int | str]:
    llm = ChatOpenAI(model=model, base_url=base_url, api_key=api_key, temperature=0)
    chain = _JUDGE_PROMPT | llm
    result = chain.invoke({"question": question, "response": response})
    try:
        return json.loads(result.content)
    except json.JSONDecodeError as exc:
        raise ValueError(
            f"LLM returned non-JSON response: {result.content!r}"
        ) from exc

temperature=0 здесь не является необязательным параметром. Оценщик должен работать детерминистично, чтобы один и тот же вопрос и ответ всегда получали одинаковый балл. Случайная оценка сводит на нет смысл наличия баллов.

json.loads оборачивается в блок try/except JSONDecodeError, поскольку модели иногда помещают свой JSON в кодовые рамки Markdown, добавляют вступительное предложение или прерывают ответ из-за его длины. Явное обработка исключения с использованием result.content!r в сообщении показывает точно то, что вернула модель, что упрощает отладку.

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

Просмотр содержимого с помощью LangSmith

При установке LANGSMITH_API_KEY отслеживается каждый шаг, который выполняет агент openwiki: какие файлы он читает, какой запрос он отправляет, что возвращает модель и сколько времени длится каждый вызов.

# app.py
def _build_openwiki_env(...) -> dict:
    ...
    if langsmith:
        env["LANGSMITH_API_KEY"] = langsmith
        env["LANGCHAIN_TRACING_V2"] = "true"
        env["LANGCHAIN_PROJECT"] = "openwiki-demo"
    return env

Для этого достаточно трёх переменных среды. LangChain автоматически их обнаруживает без необходимости изменять код в openwiki или агенте. Именно такой подход к наблюдаемости подходит для проектов LangChain: инструментализация осуществляется через среду, а не через код, что позволяет включать отслеживание в производственной среде без изменения самого приложения.

Отслеживание также раскрывает полезную информацию. Вызов --init — это не один вызов большой языковой модели. Это цикл агента, включающий вызовы для планирования, чтения файлов и записи в wiki, причём отслеживание показывает количество токенов и время задержки для каждого из них. Именно так можно ответить на вопрос «почему --init занимает 90 секунд»: достаточно просмотреть отчёт отслеживания и увидеть три последовательных вызова модели по 30 секунд каждый.

Полный процесс

Подводя итог:

Developer commits code change
    ↓
GitHub Action runs openwiki --update at 08:00 UTC
    ↓
CLI reads git diff → only changed files
    ↓
LLM regenerates affected wiki pages
    ↓
PR opened with updated openwiki/*.md
    ↓
Developer merges PR
    ↓
Agent starts next session → reads AGENTS.md → sees wiki reference
    ↓
Agent fetches openwiki/architecture.md when it needs codebase context
    ↓
Agent makes informed change without reading source files

Вики постепенно пополняется. Шесть месяцев ежедневных обновлений приводят к тому, что файл openwiki/patterns.md содержит не только текущую структуру, но и информацию о том, как развивалась база кода: здесь записаны шаблоны, появившиеся в ходе рефакторингов, а также причины существования определенных правил. Новый агент или сотрудник сразу получает всю эту информацию.

Настоящая суть

По сути, речь идет не о документации. Вопрос заключается в том, кто несет расходы на понимание базы кода.

В рамках модели RAG каждая сессия агента сопряжена с затратами: необходимо снова устанавливать связи между элементами, заново изучать правила и перечитывать уже прочитанные файлы. Расходы растут пропорционально частоте сессий.

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

Модели не устают от постоянного обновления вики. Крайние сроки не смещают документацию в конец их списка, и они никогда не оставляют запись «TODO: документировать позже» и забывают о ней. Именно в этом несоответствии и заключается суть.

Чтобы попробовать, установите Node.js 18 или новее для CLI-инструмента openwiki и получите ключ API у OpenRouter или Cloudflare Workers AI.