Припиніть змушувати вашого агента для програмування перечитувати кодову базу: шаблон Wiki LLM
Ідея Wiki для LLM від Karpathy та openwiki від LangChain: необроблені джерела, wiki, створене спеціально для агентів, та посилання AGENTS.md, а також завдання на оновлення, оцінку та відстеження.
Джерела: 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 — це навмисний вибір. CLI-інструмент openwiki сканує ту директорію, з якої він запускається. Встановлення робочої директорії на sample_agent/ обмежує сканування лише цим кодовим базисом, а не всім проектом. Якщо проігнорувати цей параметр, інструмент буде сканувати з кореня проекту, що призведе до створення wiki, яке включатиме тести, конфігурацію CI та папку node_modules.
По-друге, shell=True у Windows. Там глобальні пакети npm встановлюються у вигляді скриптів-обгорток .cmd. Коли викликається subprocess.run(["openwiki", ...]), шукається бінарний файл під назвою openwiki, його не знаходять, і виникає помилка FileNotFoundError: [WinError 2]. Якщо передати shell=sys.platform == "win32", виклик буде направлений через cmd.exe, який правильно обробляє файли .cmd. Спочатку це може збити з пантелику, але потім стає зрозумілим.
Що означає форматування „agent-native“
Людська документація пишеться так, щоб її читали зверху вниз. Ви відкриваєте README.md, починаєте з верхньої частини та поступово збираєте необхідну інформацію. Агенти не читають у такий спосіб. Вони шукають відповідні розділи серед заголовків, беруть необхідні факти та переходять далі.
Документація у форматі „agent-native“ створюється з урахуванням такої поведінки:
- Інтенсивні заголовки на кожному рівні, щоб можна було ознайомитися зі структурою без читання прозового тексту.
- Чіткі переліки посилань, такі як
Див.: 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: схема, яка завершує процес
Створення wiki — це лише половина роботи. Інша половина полягає у тому, щоб переконатися, що кодувальний агент знає про існування wiki.
Саме для цього існують файли 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 subprocess. Де б ви не розгортали цей продукт, виконуйте команду --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")
Оцінювач з використанням LLM як судді
Разом із агентом існує оцінювач з використанням LLM як судді — техніка, яка заслуговує окремої уваги.
# 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 у повідомленні показує саме те, що повернула модель, що дозволяє швидко виправляти проблеми.
Інструкція для оцінювача окремо оцінює точність, корисність та зрозумілість, оскільки вони можуть бути невдалими окремо. Відповідь може бути правильною, але не відповідати справжньому запитанню, або вказувати правильний напрямок, але заплутати читача. Розділення їх дає бали, на основі яких можна діяти.
Перегляд вмісту за допомогою 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.