用 OpenWiki 索引你的代码库

如今大多数编码 Agent 的工作方式存在一个缺陷。

每次你的 Agent 启动一个会话,它都会重新读取文件、重新推导关系、重新弄清楚哪个模块做什么。你花费 token——以及金钱——去获取 Agent 五分钟前就有、但在上下文窗口关闭时就丢弃的知识。

Andrej Karpathy 命名了这个问题并提出了一个修复方案。他称之为 LLM Wiki 模式。LangChain 发布了一个名为 openwiki 的开源实现。在这篇文章中,我将详细介绍这两者——这个模式是什么、为什么重要,以及代码到底是如何工作的。

读完本文,你将获得一个可以应用于任何你的 Agent 需要理解的代码库的思维模型。

1、问题:Agent 有健忘症

在我们深入解决方案之前,让我们精确描述失败模式。

"Agent 如何理解代码库"的标准答案是 RAG——检索增强生成。你嵌入源文件,将它们存储在向量数据库中,并在查询时检索相关片段。

RAG 对于回答问题很有效。对于编码 Agent,它会因三个原因而崩溃:

它在每次调用时重新推导知识。 每次你的 Agent 想了解 agent.py 如何与 memory.py 关联时,它都会检索片段并让 LLM 重新弄清楚。没有持久化。

检索质量取决于嵌入。 如果编写代码的工程师没有使用与 Agent 查询相同的词汇,这些片段就不会出现。嵌入是近似的;架构理解是精确的。

它会无声地过时。 你的向量数据库不知道 memory.py 上周二被重构了。它自信地提供旧片段。

Karpathy 的洞察很简单:如果 LLM 自己编写文档,以针对 LLM 阅读优化的格式,并且我们自动保持其最新状态呢?

不是面向人类的文档。是面向 Agent 的文档。

2、三层架构

Karpathy 模式有三层。openwiki 中的所有内容都映射到其中一层。

┌─────────────────────────────────────────────────┐
│  第三层:Schema                                  │
│  AGENTS.md / CLAUDE.md — "wiki 在这里"          │
├─────────────────────────────────────────────────┤
│  第二层:Wiki                                    │
│  openwiki/*.md — LLM 生成的结构化文档           │
├─────────────────────────────────────────────────┤
│  第一层:原始源码                                 │
│  你的实际代码库 — 对 wiki 不可变                  │
└─────────────────────────────────────────────────┘

每一层都依赖于下面的一层。让我们按顺序逐一介绍。

3、原始源码:Agent 阅读的内容

原始源码就是你的代码库。从 wiki 的角度来看它们是不可变的——OpenWiki 读取它们,但从不修改它们。

在我们的演示项目中,原始源码位于 sample_agent/:一个 AI 工程师会立即识别的真实 LangChain ReAct Agent。

# 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,
    )

Agent 连接了四个组件:一个 LLM(通过 ChatOpenAI)、一个提示模板、一个记忆缓冲区和一个工具列表。这是标准的 ReAct 模式——它正是 OpenWiki 旨在记录的那种代码。

工具是有意设计的最小化——一个模拟搜索和一个安全计算器:

# sample_agent/tools.py
@tool
def calculator(expression: str) -> str:
    """评估安全的数学表达式并返回结果。"""
    allowed_names: dict = {}
    try:
        result = eval(expression, {"__builtins__": {}}, allowed_names)
        return str(result)
    except Exception as exc:
        return f"评估 '{expression}' 时出错: {exc}"

注意 {"__builtins__": {}} ——这是一个沙箱,从 eval 作用域中移除了所有 Python 内置函数,这样用户就不能传入 __import__('os').system('rm -rf /') 并让它执行。小细节,真实的安全影响。

为什么这对模式很重要: OpenWiki 将读取每个这些文件,理解它们的关系,并生成一个解释所有内容的 wiki 页面——一次性的。而不是每次 Agent 启动时都重新阅读。

4、Wiki:LLM 生成的、Agent 原生的文档

这是 Karpathy 模式的核心。不是人类编写的文档(会腐烂)也不是对原始源码的 RAG(会重新推导),而是 LLM 编写的明确针对 LLM 消费上下文方式优化的结构化 markdown。

4.1 OpenWiki 如何生成它

在我们的 Streamlit 应用中,点击 Generate Wiki 会运行这个:

# 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 文件在 Windows 上需要 shell=True
    )
    return result.stdout + result.stderr, result.returncode

这里有两个值得注意的地方。

cwd=SAMPLE_AGENT_DIR ——这是有意的。openwiki CLI 扫描它运行时所在的目录。通过将工作目录设置为 sample_agent/,我们将 wiki 范围限定为该代码库,而不是整个项目。如果你忘记这个参数,openwiki 会从项目根目录扫描并为所有内容生成 wiki——你的测试、CI 配置、node_modules。

Windows 上的 shell=True ——npm 全局包在 Windows 上安装为 .cmd 包装脚本。subprocess.run(["openwiki", ...]) 查找名为 openwiki 的二进制文件,找不到,然后引发 FileNotFoundError: [WinError 2]。添加 shell=sys.platform == "win32" 通过 cmd.exe 路由,可以正确解析 .cmd 文件。这是那种第一次遇到很困惑、事后想来很明显的问题。

4.2 "Agent 原生"格式到底是什么意思

人类文档是线性编写的。你打开 README.md,从顶部开始,逐步构建上下文。Agent 不是线性阅读的——它们扫描标题以找到相关部分,提取事实,然后继续。

Agent 原生文档围绕这种行为构建:

  • 每个级别的密集标题,这样 Agent 可以扫描结构而无需阅读正文
  • 显式交叉引用See: patterns.md#memory-strategy ——Agent 通过程序化方式遵循这些,而不是凭直觉
  • 一段式组件摘要 ——足够决定是否阅读更多,但不足以成为完整故事
  • 零填充 ——没有"在本文档中我们将探讨"的前言,没有结尾摘要

为我们的示例 Agent 生成的 openwiki/architecture.md 可能看起来像:

# 架构 — sample_agent
## 概述
基于 LangChain AgentExecutor 构建的 ReAct Agent。入口点:agent.py。
四个组件:LLM(通过 OpenRouter 的 ChatOpenAI)、工具、记忆、提示。
## 组件
- agent.py - AgentExecutor 连接。参见:components.md#agent
- tools.py - 搜索(模拟)+ 计算器(沙箱化 eval)。参见:components.md#tools
- memory.py - ConversationBufferWindowMemory,k=5 轮
- prompts.py - 带 agent_scratchpad 占位符的 ReAct ChatPromptTemplate
- evaluator.py - LLM-as-judge 链。参见:patterns.md#evaluation
## 数据流
问题 → AgentExecutor.invoke() → ReAct 循环(最多 10 次迭代)
→ 工具调用 → 最终答案 → evaluator.evaluate()(可选)

将其与典型的带三段上下文设定的人类 README.md 比较,你在了解任何具体信息之前需要阅读大量前言。Agent 不需要上下文设定——它们需要事实。

5、Schema:闭合循环

生成 wiki 只是一半的工作。另一半是确保你的编码 Agent 知道 wiki 的存在

这就是 AGENTS.mdCLAUDE.md 的作用。这些是 Claude Code、Cursor、GitHub Copilot Workspace 和大多数现代编码 Agent 在会话启动时读取的指令文件。OpenWiki 向你项目使用的任何一个注入一个简短的引用。

# sample_agent/AGENTS.md(OpenWiki 之前)
此仓库包含一个 LangChain ReAct Agent。
## 架构
- agent.py - AgentExecutor 入口点
- tools.py - 可用工具(搜索、计算器)
- memory.py - 对话记忆策略
- prompts.py - 系统提示和 ReAct 模板
- evaluator.py - LLM-as-judge 评估链

运行 openwiki --init 之后:

# sample_agent/AGENTS.md(OpenWiki 注入后)
此仓库包含一个 LangChain ReAct Agent。
## 架构
- agent.py - AgentExecutor 入口点
...
## OpenWiki 上下文
搜索代码库上下文时,请先检查 `openwiki/`。
关键文件:
- `openwiki/architecture.md` - 整体结构和文件职责
- `openwiki/patterns.md` - 编码模式和约定
- `openwiki/components.md` - 详细组件文档
wiki 每日自动更新。始终优先使用 wiki 上下文而非重新阅读源文件。

注入是有意简短的。你不想将整个 wiki 放在指令文件中;对于大型代码库,那会在每次会话时耗尽上下文窗口。相反,你放一个指针。Agent 读取指针,在需要代码库上下文时获取 openwiki/architecture.md,并获得所需的确切信息而无需阅读源文件。

Streamlit 应用在 Tab 4 中显示此修改前后。该应用模拟注入以使机制可见:

# app.py — Tab 4
agents_md_content = AGENTS_MD.read_text(encoding="utf-8")
openwiki_injection = """
## OpenWiki 上下文
搜索代码库上下文时,请先检查 `openwiki/`。
...
"""
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")

6、Provider

OpenWiki 是 LLM 无关的——它在底层使用 LangChain 的 ChatOpenAI,读取 OPENAI_BASE_URL 作为自定义端点。这意味着任何兼容 OpenAI 的 provider 都可以工作。

我们的应用支持两个:

# 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 自动读取
    else:
        env["OPENROUTER_API_KEY"] = api_key   # openwiki 的原生密钥名
    return env

OpenRouter 是阻力最小的路径——一个 API 密钥,通过统一端点访问所有主要模型。设置 OPENWIKI_MODEL_ID=openai/gpt-4o-mini 就完成了。

Cloudflare Workers AI 在边缘运行模型。来自智谱 AI 的 GLM-5.2 模型(@cf/zai-org/glm-5.2)直接在 Cloudflare 基础设施上运行,这意味着如果你构建生产级应用并希望在单一云提供商内完成所有工作,延迟会更低。Cloudflare 在 /ai/v1 暴露了兼容 OpenAI 的端点,所以不需要自定义客户端代码——只需设置 OPENAI_BASE_URL,现有的 LangChain 集成就能处理。

7、自维护:人类总是忽略的部分

每个文档系统最终都以相同的方式失败:当代码改变时,人类忘记更新它。

Karpathy 的观察是,这不是纪律问题——而是经济问题。维护负担随代码库规模增长,但执行维护的人没有直接回报。最终文档与现实严重偏离,人们停止信任它们,循环重复。

OpenWiki 通过 openwiki --update 解决了这个问题,它做了一件巧妙的事:调用 git log 找出自上次 wiki 运行以来的提交,git diff 只读取更改的文件,并只重新生成受影响的 wiki 页面。永远不会重新读取整个代码库。

GitHub Action 随项目提供:

# .github/workflows/openwiki-update.yml
name: OpenWiki Update
on:
  schedule:
    - cron: "0 8 * * *"   # 每天 UTC 08:00
  workflow_dispatch:
jobs:
  update-wiki:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0   # 完整 git 历史以便 openwiki 读取 diff
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
      - run: npm install -g openwiki
      - name: Update wiki
        working-directory: sample_agent     # 范围限定到我们的代码库
        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 从代码库根目录运行,而不是项目根目录。

PR 流程是正确的选择。自动提交到 main 意味着你的文档可以在未经审查的情况下更改。PR 给你一个 diff 来检查,并且能够拒绝不良的 LLM 输出。

在我们的应用中,Tab 5 允许你在本地模拟这个:

# app.py — Tab 5
if st.button("🔄 Run openwiki --update", type="primary"):
    with st.spinner("检查更改并更新 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")

8、评估器:闭合质量循环

示例 Agent 包含一个 LLM-as-judge 评估器——一个值得单独理解的模式。

# 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 返回非 JSON 响应: {result.content!r}"
        ) from exc

temperature=0 在这里是不可妥协的。你希望评估器是确定性的——相同的问题和响应应该始终得到相同的分数。随机评估会完全破坏拥有分数的意义。

json.loadstry/except JSONDecodeError 包裹,是因为 LLM 偶尔会用 markdown 代码围栏包裹 JSON(json ...)、添加前言句子,或在长响应时截断。显式异常消息中的 result.content!r 告诉你模型返回了什么,使调试快速。

Judge 提示要求对准确性、有用性和清晰度进行评分——三个维度不会合并成一个,因为它们独立失败。一个响应可以准确但无用(正确但不回答实际问题)。它可以有用但不清楚(方向正确,解释混乱)。将它们分开使分数可操作。

9、LangSmith:内部实际发生了什么

当你设置 LANGSMITH_API_KEY 时,openwiki Agent 的每一步都被追踪——它读取了哪些文件、发送了什么提示、LLM 返回了什么、每次调用花了多长时间。

# 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 或 Agent 的代码。这是 LangChain 项目中可观察性的正确模式:在环境级别进行检测,而不是代码级别,这样你可以在生产中启用追踪而不触碰应用程序。

LangSmith 追踪会告诉你一些有用的东西:openwiki --init 调用不是一次 LLM 调用。它是一个 Agent 循环——规划调用、文件读取调用、wiki 写入调用。追踪使这可见,并为你提供每个步骤的 token 数量和延迟。这就是你回答"为什么 --init 需要 90 秒"的方式——你查看追踪并看到三个 30 秒的 LLM 调用串行运行。

10、端到端流程

将所有内容放在一起,流程是:

开发者提交代码更改
    ↓
GitHub Action 在 UTC 08:00 运行 openwiki --update
    ↓
CLI 读取 git diff → 只读取更改的文件
    ↓
LLM 重新生成受影响的 wiki 页面
    ↓
PR 被创建,包含更新的 openwiki/*.md
    ↓
开发者合并 PR
    ↓
Agent 启动下一个会话 → 读取 AGENTS.md → 看到 wiki 引用
    ↓
Agent 在需要代码库上下文时获取 openwiki/architecture.md
    ↓
Agent 做出知情的更改而无需阅读源文件

wiki 会累积价值。经过六个月的每日更新,openwiki/patterns.md 不仅包含当前结构,还包含代码库如何演进的机构知识——从重构中涌现的模式、为特定原因建立的约定。一个新的 Agent(或新的工程师)在第一个会话中就能获得所有这些。

11、真正的洞察

Karpathy 模式不是关于文档的。它是关于谁承担理解代码库的成本

在 RAG 模型中,每个 Agent 会话都支付这个成本——重新推导关系、重新弄清楚约定、重新阅读已经读过的文件。成本随会话频率增长。

在 wiki 模型中,你每次代码更改只支付一次——一个在没有人等待时运行的每日作业,每页成本不到一分钱,并且产生的输出随时间累积价值。

LLM 不会厌倦维护 wiki。它们不会在有截止日期时降低文档的优先级。它们不会写"TODO:稍后记录这个"然后忘记。

这种不对称性就是整个理念。


原文链接:Stop Making Your AI Agent Re-Read Your Entire Codebase Every Session

汇智网翻译整理,转载请标明出处