别再让编码助手反复阅读代码库了:LLM Wiki模式
Karpathy的LLM Wiki构想与LangChain的openwiki:原始数据源、专为智能体设计的wiki以及AGENTS.md引用文件,此外还包括更新任务、评估与追踪功能。
来源: Stop Making Your AI Agent Re-Read Your Entire Codebase Every Session
大多数编程智能体都有一个令人恼火的习惯:每次会话都得从零开始——它们会重新打开文件,重新确认哪个模块依赖于哪个模块,还要重新了解代码库中每个部分的用途。你得用代币乃至金钱来购买那些智能体五分钟前还掌握、但一旦上下文窗口关闭就立刻忘掉的知识。
Andrej Karpathy指出了这个问题,并提出了一种解决方案,他将其称为“LLM Wiki模式”。LangChain已经发布了该模式的开源实现 openwiki。下文将同时介绍这一模式及其实现方式:其核心理念是什么、为何重要,以及演示应用背后的代码是如何运作的。最终你会得到一个思维模型,可以应用于智能体需要理解的任何代码库。
有“健忘症”的智能体
首先,明确说明故障模式。对于“智能体如何理解代码库?”这一问题的常见答案是RAG——即将源文件嵌入向量数据库,在查询时检索相关片段。
RAG虽适合回答问题,但对于编程智能体而言存在三方面不足。
理解内容每次都需要重新计算。假设智能体需要查找agent.py与memory.py之间的关联,它会先检索相关片段,模型再重新推导答案,最终得到的结果会被丢弃。
检索依赖于嵌入向量。如果编写代码的人使用的词汇与智能体的查询词汇不同,就永远无法找到正确的片段。嵌入向量是近似的,而架构则是精确的。
它会在无声无息中过时。向量存储系统根本不知道memory.py上周二已经过重构,仍会满怀信心地提供旧版本的数据。
Karpathy的思路很简单:让模型自己以适合其阅读的格式编写文档,并自动保持文档的更新。也就是说,这些文档是为模型准备的,而非人类。
三层结构
openwiki中的每个部分都属于Karpathy设计中的三层结构之一。
┌─────────────────────────────────────────────────┐
│ 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__": {}}。它会将 eval 作用域中的所有 Python 内置函数移除,这样用户就无法传递类似 __import__('os').system('rm -rf /') 的代码并使其执行。这虽是个小细节,却会带来严重的安全问题。
对于这种模式而言,关键在于 OpenWiki 会读取所有这些文件,分析它们之间的关联,然后只生成一次完整的维基页面进行说明,而无需在每次代理启动时都重复处理。
第二层:维基本身
这里体现了核心理念。手写的文档会逐渐过时,而基于原始代码的 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 CLI会扫描其运行所在的目录。将工作目录设置为sample_agent/可以确保wiki仅涵盖该代码库,而不会包含整个项目。如果忽略这个参数,它就会从项目根目录开始扫描,从而生成一个包含测试文件、CI配置以及node_modules的wiki。
其次是在 Windows 系统上使用 shell=True。在该系统中,全局 npm 包会以 .cmd 形式的封装脚本形式被安装。当调用 subprocess.run(["openwiki", ...]) 时,系统会寻找名为 openwiki 的二进制文件,由于找不到而抛出 FileNotFoundError: [WinError 2] 错误。通过传递 shell=sys.platform == "win32" 参数,可以让调用通过 cmd.exe 执行,从而正确处理 .cmd 文件。初次使用时可能会感到困惑,但理解之后就很明显了。
“Agent-native”格式的含义
人类编写的文档都是从上到下阅读的。你打开 README.md 后,从顶部开始阅读并逐步了解上下文。而智能体并非如此阅读方式,它们会扫描标题以找到相关部分,提取所需信息后便继续前进。
“Agent-native”文档正是围绕这种行为特点设计的:
- 各级均设有简洁的标题,无需阅读正文即可了解结构。
- 存在明确的交叉引用,如
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旁边——那些README通常会有三段背景介绍,直到出现具体内容。而智能体并不需要这些背景介绍,他们需要的是事实。
第三层:架构,用于形成闭环
生成维基内容只是任务的一半,另一半则是确保编码智能体知道该维基的存在。
这就是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并不关心其背后使用的是哪种大语言模型。它使用LangChain的ChatOpenAI,该组件会从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 能从边缘节点提供模型服务。ZhipuAI 的 GLM 模型(@cf/zai-org/glm-5.2)运行在 Cloudflare 自有的基础设施上,因此当您需要将应用部署到生产环境且希望使用单一云服务提供商时,延迟会显著降低。由于 Cloudflare 在 /ai/v1 处提供了与 OpenAI 兼容的端点,因此无需自定义客户端。只需将 OPENAI_BASE_URL 指向该端点,已有内置的 LangChain 集成便会处理其余工作。
保持维基内容更新
所有的文档系统最终都会以同样的方式出问题:代码发生变化时,人们会忘记更新文档。
Karpathy认为这并非纪律问题,而是经济学问题。维护成本会随着代码库规模的扩大而上升,而执行维护工作的人却无法立即获得回报。最终,文档与现实情况相差过远,以至于没人再信任它们,然后这个循环又会重新开始。
在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")
以大语言模型作为评判者的评估系统
除了智能体之外,还有一种以大语言模型作为评判者的评估技术,值得专门探讨。
# 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调用并非一次单一的LLM调用,而是一个包含规划调用、文件读取调用和维基写入调用的智能体循环,追踪结果会显示每种调用的token数量和延迟时间。这样就能回答“为什么--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:稍后编写文档”这样的提示然后置之不理。这种失衡正是问题的关键所在。
若想尝试,需为 openwiki CLI 安装 Node.js 18 或更高版本,并从 OpenRouter 或 Cloudflare Workers AI 获取 API 密钥。
相关阅读
- 通过技能实现自我提升的智能体:复用、筛选、修复与验证 —— 一种针对能够提升自身技能的智能体的四阶段设计方案:保存可运行的代码、优化描述、提出经过审核的修复方案,并在交接前验证输出结果。