Przestań zmuszać swojego agenta do kodowania do ponownego czytania bazy kodu: wzorzec LLM Wiki
Idea Wiki LLM od Karpathy’ego oraz openwiki od LangChain: surowe źródła, wiki dostosowane do agentów oraz odnośnik AGENTS.md, plus zadania aktualizacji, ocena i śledzenie.
Źródła: Stop Making Your AI Agent Re-Read Your Entire Codebase Every Session
Większość agentów do kodowania ma irytujący nawyk – każda sesja zaczyna się od zera: ponownie otwierają pliki, na nowo odkrywają, który moduł zależy od którego, i na nowo uczą się, do czego służy każda część bazy kodu. Płacisz tokenami, a więc pieniędzmi, za wiedzę, którą agent posiadał pięć minut wcześniej, ale stracił, gdy zamknęło się okno kontekstowe.
Andrej Karpathy opisał ten problem i zaproponował rozwiązanie, które nazwał wzorcem LLM Wiki. LangChain udostępnił jego implementację o otwartym kodzie, openwiki. Poniżej wzorzec i implementacja są omawiane obok siebie: jaka jest idea, dlaczego jest ważna oraz jak działa kod w aplikacji demonstracyjnej. Rezultatem jest model mentalny, który można zastosować do dowolnej bazy kodu, którą agent musi zrozumieć.
Agenci z amnezją
Najpierw, w precyzyjnych słowach, opis trybu awarii. Zwykłą odpowiedzią na pytanie „jak agent rozumie bazę kodu?” jest RAG. Wgruje się pliki źródłowe, umieści je w bazie danych wektorowej i pobiera odpowiednie fragmenty podczas wyszukiwania.
RAG nadaje się do odpowiadania na pytania. Jednak w przypadku agentów programistycznych ma trzy wady.
Rozumienie jest obliczane na nowo za każdym razem. Załóżmy, że agent potrzebuje powiązania pomiędzy plikami agent.py a memory.py. Pobiera fragmenty, model ponownie dochodzi do odpowiedzi, a wynik jest odrzucany.
Wyszukiwanie zależy od embeddingów. Jeśli osoba, która napisała kod, użyła innego słownictwa niż w zapytaniu agenta, odpowiednie fragmenty nigdy się nie pojawią. Embeddingi są przybliżone, natomiast architektura jest dokładna.
Staje się przestarzały bez żadnego sygnału. Serwer wektorowy nie ma pojęcia, że memory.py został przeprojektowany w zeszły wtorek, i z pełnym przekonaniem serwuje stare dane.
Idea Karpathy’ego była prosta: niech model sam tworzy dokumentację w formacie dostosowanym do czytania przez modele i automatycznie ją aktualizuje. Chodzi tu o dokumentację dla agentów, a nie dla ludzi.
Trzy warstwy
Każdy element openwiki należy do jednej z trzech warstw w projekcie Karpathy’ego.
┌─────────────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────────────┘
Każda warstwa spoczywa na tej poniżej niej.
Warstwa 1: surowe źródła
Surowe źródła to po prostu twoja baza kodu. Z punktu widzenia wiki są niezmienne: OpenWiki je czyta i nigdy ich nie zmienia.
Projekt demonstracyjny przechowuje swoje źródła w katalogu sample_agent/ – to realistyczny agent LangChain ReAct, który rozpoznają większość inżynierów AI.
# 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,
)
Cztery komponenty są połączone ze sobą: model klienta (ChatOpenAI), szablon promptu, pamięć rozmowy oraz narzędzia. To klasyczny przykład ReAct – tego rodzaju kodu, do którego przeznaczony jest OpenWiki. Narzędzia są celowo proste: symulowana funkcja wyszukiwania oraz bezpieczny kalkulator:
# 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}"
Spójrz na {"__builtins__": {}}. Usuwa on wszystkie wbudowane funkcje Pythona z zakresu eval, aby użytkownik nie mógł przekazać czegoś w rodzaju __import__('os').system('rm -rf /') i sprawić, że to zostanie wy wykonane. To mały szczegół, który ma poważne konsekwencje bezpieczeństwa.
Jeśli chodzi o wzorzec, istotne jest to, że OpenWiki będzie czytał każdy z tych plików, określał, jak są ze sobą powiązane, i raz tworzył stronę wiki wyjaśniającą to wszystko, a nie za każdym razem, gdy agent się uruchamia.
Warstwa 2: sama wiki
Tutaj leży główna idea. Ręcznie pisane dokumenty ulegają zniszczeniu, natomiast RAG przetwarzający surowy kod ponownie wyprowadza wszystko przy każdym zapytaniu, więc wzorzec polega na tym, by model generował strukturyzowany markdown dostosowany do sposobu, w jaki modele przetwarzają kontekst.
Jak OpenWiki to tworzy
W aplikacji Streamlit z demonstracji przycisk „Generate Wiki” uruchamia następujące działanie:
# 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
Dwa szczegóły są ważne.
Po pierwsze, cwd=SAMPLE_AGENT_DIR jest celowe. Interfejs wiersza poleceń openwiki skanuje katalog, z którego jest uruchamiany. Ustawienie katalogu roboczego na sample_agent/ ogranicza wiki do tego kodu, a nie do całego projektu. Jeśli pominąć ten parametr, program będzie skanował katalog główny projektu, tworząc wiki obejmujące testy, konfigurację CI oraz pliki z katalogu node_modules.
Po drugie, shell=True w systemie Windows. Pakiety npm instalowane globalnie są tam tworzone jako skrypty otaczające w formacie .cmd. Wywołanie subprocess.run(["openwiki", ...]) szuka pliku binarnego o nazwie openwiki, nie znajduje go i wywołuje błąd FileNotFoundError: [WinError 2]. Przekazanie parametru shell=sys.platform == "win32" kieruje wywołanie przez cmd.exe, który prawidłowo obsługuje pliki .cmd. Na początku może to być mylące, ale później staje się oczywiste.
Co oznacza formatowanie „agent-native”
Dokumentacja przeznaczona dla ludzi jest pisana tak, by czytać ją od góry do dołu. Otwieramy README.md, zaczynamy od góry i stopniowo budujemy kontekst. Agenci nie czytają w ten sposób – szukają odpowiednich sekcji w nagłówkach, pobierają potrzebne informacje i przechodzą dalej.
Dokumentacja typu „agent-native” jest dostosowana do takiego sposobu działania:
- Gęste nagłówki na każdym poziomie, dzięki czemu można szybko przejrzeć strukturę bez czytania tekstu opisowego.
- Jasne odniesienia w postaci np.
See: patterns.md#memory-strategy, których agent trzyma się mechanicznie, a nie na podstawie intuicji. - Streszczenia w jednym akapicie dla każdego komponentu: wystarczające, by zdecydować, czy czytać dalej, ale nie stanowiące pełnego opisu.
- Brak dodatkowych elementów. Pomijamy wstęp typu „W tym dokumencie przyjrzymy się…” oraz wszelkie zamykające podsumowania.
Wygenerowany plik openwiki/architecture.md dla przykładowego agenta może wyglądać w ten sposób:
# 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)
Umieść to obok typowego ludzkiego README z trzema akapitami wprowadzającymi kontekst, zanim pojawi się cokolwiek konkretnego. Agenci nie potrzebują tych wprowadzeń – potrzebują faktów.
Warstwa 3: schemat, który zamyka pętlę
Stworzenie wiki to połowa pracy. Druga połowa polega na upewnieniu się, że agent kodujący wie o istnieniu wiki.
To właśnie jest rolą plików AGENTS.md i CLAUDE.md – plików z instrukcjami, które Claude Code, Cursor, GitHub Copilot oraz większość nowoczesnych agentów kodujących czytają na początku sesji. OpenWiki wstawia krótką odniesienie do tego z nich, który jest używany w projekcie. Wcześniej:
# 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
A po uruchomieniu 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.
Iniekcja jest celowo krótka. Umieszczenie całej witryny w pliku instrukcji spowodowałoby wyczerpanie okna kontekstowego na początku każdej sesji przy dużym zasobie kodu. Wystarczy wskaźnik. Agent go odczytuje, otwiera plik openwiki/architecture.md tylko wtedy, gdy potrzebny jest kontekst kodu, i dowiaduje się tego, co musi wiedzieć, nie dotykając ani jednego pliku źródłowego.
Czwarty zakładka demonstracji pokazuje to przed i po, symulując iniekcję, aby mechanizm był widoczny:
# 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")
Wybór dostawcy modeli
OpenWiki nie przejmuje się tym, jaki LLM znajduje się za nim. Wykorzystuje ChatOpenAI z LangChain, który ładuje wartość OPENAI_BASE_URL w celu uzyskania dostępu do dostosowanego punktu końcowego, więc zadziała każdy dostawca kompatybilny z OpenAI. Demo obsługuje dwa takie rozwiązania:
# 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 to prosty wybór. Jedno hasło otwiera większość popularnych modeli za pośrednictwem jednego punktu dostępu, a ustawienie OPENWIKI_MODEL_ID=openai/gpt-4o-mini zakończa konfigurację.
Cloudflare Workers AI dostarcza modele z sieci edge. Model GLM firmy ZhipuAI (@cf/zai-org/glm-5.2) działa na własnej infrastrukturze Cloudflare, dzięki czemu opóźnienia zmniejszają się podczas wdrażania w środowisko produkcyjne, jeśli chcesz korzystać z jednego dostawcy chmury. Ponieważ Cloudflare udostępnia kompatybilny z OpenAI punkt dostępu pod adresem /ai/v1, nie jest potrzebny żaden specjalny klient. Wskazuj na niego wartość OPENAI_BASE_URL, a już istniejąca integracja z LangChain zajmie się resztą.
Utrzymywanie wiki aktualnym
Każdy system dokumentacji ostatecznie zawodzi w ten sam sposób: ludzie zapominają go aktualizować, gdy zmienia się kod.
Karpathy zauważył, że nie chodzi tu o problem dyscypliny, lecz o problem ekonomiczny. Koszty konserwacji rosną wraz z rozmiarem bazy kodu, podczas gdy osoba zajmująca się tym nie otrzymuje natychmiastowej nagrody. W końcu dokumentacja tak bardzo odbiega od rzeczywistości, że nikt jej nie ufa, a cykl zaczyna się od nowa.
Rozwiązaniem w OpenWiki jest openwiki --update. Zamiast ponownie skanować wszystko, narzędzie pyta git log o komitety dodane po ostatnim uruchomieniu, używa git diff, aby przeczytać tylko zmodyfikowane pliki, i przepisuje jedynie te strony, które zostały dotknięte tymi zmianami. Akcja GitHub Action wbudowana w projekt uruchamia ją według ustalonego harmonogramu:
# .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
Zwróć uwagę na working-directory: sample_agent – to ta sama opcja zakresu działania co przy wywołaniu Python subprocess. Gdziekolwiek to wdrożysz, uruchom --update z korzenia bazy kodu, a nie z korzenia projektu.
Otwieranie pull request jest właściwym rozwiązaniem projektowym. Automatyczne komitowanie do main umożliwiłoby zmiany w dokumentacji bez żadnej oceny, natomiast PR daje możliwość przejrzenia różnic i odrzucenia złych wyników modelu. Piąta karta w demonstracji symuluje aktualizację lokalnie:
# 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")
Ocena za pomocą LLM jako sędziego
Oprócz agenta występuje również ocena za pomocą LLM jako sędziego – technika, która zasługuje na osobne rozważanie.
# 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 nie jest tutaj opcjonalne. Sędzia powinien być deterministyczny, aby to samo pytanie i odpowiedź zawsze otrzymywały tę samą ocenę. Losowa ocena unieważnia sens istnienia ocen.
json.loads jest otoczone blokiem try/except JSONDecodeError, ponieważ modele czasami umieszczają swój JSON w ramkach kodu markdown, dodają zdanie wprowadzające lub przerywają długie odpowiedzi. Wyraźna obsługa wyjątku za pomocą result.content!r w wiadomości pokazuje dokładnie to, co zwrócił model, co ułatwia debugowanie.
Prompt sędziego ocenia dokładność, pomocność i klarowność oddzielnie, ponieważ te aspekty zawodzą niezależnie od siebie. Odpowiedź może być poprawna, ale nie odpowiadać na rzeczywiste pytanie, albo wskazywać właściwy kierunek, ale mylić czytelnika. Rozdzielenie tych kryteriów pozwala uzyskać oceny, na których można działać.
Przeglądanie treści za pomocą LangSmith
Ustawienie LANGSMITH_API_KEY umożliwia śledzenie każdego kroku podejmowanego przez agenta openwiki: jakie pliki jest czyta, jakie zapytanie wysyła, co zwraca model oraz jak długo trwa każde wezwanie.
# 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
Wystarczą trzy zmienne środowiskowe. LangChain pobiera je automatycznie bez konieczności modyfikacji kodu w openwiki ani w agencie. To właściwy wzorzec obserwowalności dla projektów LangChain: instrumentacja odbywa się poprzez środowisko, a nie kod, dzięki czemu śledzenie można włączyć w środowisku produkcyjnym bez ingerencji w aplikację.
Śledzenie ujawnia również coś przydatnego. Wywołanie --init nie jest pojedynczym wywołaniem modelu LLM. Jest to pętla agenta składająca się z wywołań do planowania, czytania plików i pisania na wiki, a śledzenie pokazuje liczbę tokenów oraz opóźnienie dla każdego z nich. W ten sposób można odpowiedzieć na pytanie „dlaczego --init zajmuje 90 sekund”: otwiera się śledzenie i widać trzy kolejne wywołania modelu trwające po 30 sekund.
Pełny proces
Podsumowując:
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
Wiki rozwija się z upływem czasu. Sześć miesięcy codziennych aktualizacji sprawia, że plik openwiki/patterns.md zawiera już więcej niż tylko obecną strukturę – odnotowuje on również rozwój bazy kodu, w tym wzorce, które pojawiły się podczas refaktoryzacji, oraz powody istnienia określonych konwencji. Nowy agent lub pracownik od razu ma do dyspozycji całą tę informację.
Prawdziwa wskazówka
W gruncie rzeczy chodzi tu nie o dokumentację, lecz o to, kto ponosi koszt zrozumienia bazy kodu.
W modelu RAG każda sesja agenta wiąże się z kosztami: ponownym ustalaniem zależności, ponownym uczeniem się konwencji, ponownym czytaniem wcześniej przeczytanych plików. Koszt ten rośnie wraz z częstotliwością sesji.
W modelu wiki koszt jest ponoszony tylko raz przy każdej zmianie w kodzie, przez zadanie zaplanowane na określony czas, gdy nikt nie musi czekać; koszt ten wynosi zaledwie ułamek centa za stronę, a treść generowana przez ten system stale zyskuje na wartości.
Modeli nie męczy utrzymywanie wiki w aktualnym stanie. Terminy nie przesuwają dokumentacji na koniec listy zadań, a one nigdy nie zostawiają notatki „TODO: udokumentować później” i o tym nie zapominają. W tym braku równowagi polega cała istota sprawy.
Aby to wypróbować, zainstaluj Node.js 18 lub nowszą wersję dla interfejsu wiersza poleceń openwiki i uzyskaj klucz API od OpenRouter lub Cloudflare Workers AI.
Literatura pokrewna
- Samodoskonalące się agenty poprzez umiejętności: ponowne wykorzystanie, selekcja, naprawa i weryfikacja — czteroczęściowy model agentów, które doskonalą własne umiejętności: przechowywanie działającego kodu, udoskonalanie opisów, proponowanie sprawdzonych poprawek oraz weryfikacja wyników przed przekazaniem.
- Agent AI DevOps w Pythonie: LangChain, Claude i narzędzia Bash z zabezpieczeniami — Stwórz agenta, który uruchamia bash, czyta logi i przeprowadza zapytania do Kubernetes: najpierw zasady bezpieczeństwa, potem narzędzia, pętla ReAct, pamięć, otoczka FastAPI oraz rozwiązania dla częstych problemów.
- Notatki praktyczne: Przestań obserwować swojego agenta kodującego – stwórz system, któremu możesz zaufać — Szczegółowy przewodnik po notatkach praktycznych: Przestań obserwować swojego agenta kodującego – stwórz system, któremu możesz zaufać: umowy, sprawdzania oraz gotowe fragmenty kodu dla zespołów wdrażających ten wzorzec.
- Twoje system plików agenta to decyzja: wyjaśnienie backendów Deep Agents — Jak backendy LangChain Deep Agents decydują, co zostanie zachowane: pięć wbudowanych rozwiązań, routowanie przez CompositeBackend, pułapka nazw przestrzeni dla magazynów wielu użytkowników oraz wzorce sandbox.