乐于分享
好东西不私藏

AI Agent 跑不久,不是模型不够强,而是系统没设计对

AI Agent 跑不久,不是模型不够强,而是系统没设计对

大多数 AI agent 都只是一个单一循环:进程一重启、context window 填满,或者某个 API 调用失败,它就死掉了。用于快速聊天时这没问题,但面对需要运行数天或数周的长周期任务时,它会彻底崩溃。在这篇博客中,我们会构建一个能在整个时间跨度内持续运行的 agent。它会规划自己的工作,构建并测试真实软件,并从失败中学习,以便在下一次运行中改进自己。为此,模型本身不需要变得更聪明。承担重活的是模型周围的系统。

在开始构建之前,先看一下整体形态。人类可以通过持久信号进行审批和引导。一个 durable control plane 负责调度工作。一个 agent team 执行工作,其中有一位 lead engineer 和若干 helper。真实状态保存在 git 加分层 memory 中,而一个 deterministic verifier 是唯一被允许判定某项工作完成的东西。

  1. 我想看看我们能在相反方向上推进多远。

  2. 如果我们把长周期自主性当作工程问题,而不是模型能力,会怎样?

  3. 模型以短 burst 的方式思考,而系统通过把三件无聊的事情做得非常好来运行数周。它把真实状态保存在模型之外,记录每一步 journal,并用真实测试验证进展。

在整篇博客中,我会跟随一个具体任务,以便让这些想法落地。这个任务是从零开始构建一个小型 Python language server(一个 LSP server),系统大约需要一周时间完成。下面的 trace 是该任务的一次代表性运行,到文章末尾,你会理解其中每一行。

2026-06-02T08:44:51.880Z INFO  mission.complete items=14/14 cycles=615 head=9d8c7b6 status=DONE2026-06-02T08:44:51.900Z INFO  governor.final cum_usd=178.60 ceiling=400.00 crashes_survived=1                               loop_trips=1 reviewer_blocking=2 scope_cuts=1 skills_learned=2

这个单一任务经历了一次主机重启,触发过一次 loop detector,被独立 reviewer 阻塞过两次,在过度规划后自行缩减 scope,并在过程中学会了两个可复用 skill。所有这些 token 成本是 178 美元 60 美分,而预算上限是 400 美元。让我们逐步构建产生这行日志的系统。

代码概览

所有代码都位于名为 lra 的 Python package 中(Long Running Agents 的缩写)。你可以在 GitHub 上找到完整项目:

https://github.com/FareedKhan-dev/long-running-agent

代码库很大,所以这里不直接展示完整目录树,而是给出重要部分的地图。每个文件夹负责一项职责,我们会大致按这个顺序讲解它们。

src/lra/├── durable/        # the Temporal control plane: workflows, activities, replay, claim-check├── state/          # the git saved-state files: the real state on disk├── contracts/      # the typed interfaces (state, model, tools, verify, memory, hitl)├── agent/          # the inner loop: think -> act -> verify -> checkpoint, plus compaction├── agents/         # the organization: planner, lead, researchers, reviewer, evolver, judge├── coordination/   # ownership map, decision log, blackboard, tickets├── model/          # pluggable LLM backends: stub, ollama, openai_compat, claude├── execution/      # the tool dispatcher and the sandbox (local, docker, e2b)├── safety/         # default-deny egress + the rule of two├── verify/         # the deterministic verifier, mutation testing, trust bootstrap├── governor/       # the budget governor + loop detector├── hitl/           # human-in-the-loop gates├── memory/         # tiered memory, embeddings, hybrid retrieval, skills└── evals/          # the offline eval harness for self-improvement

模型层是可插拔的,所以你可以用零成本运行整个系统。本地 Ollama 模型或免费层云模型不产生费用,测试中使用 stub 模型。只有当你把它指向 Claude 这样的付费 API 时,才需要为 token 付费。下面是 quickstart:

# install (this provisions Python 3.12 for you)uv syncuv run lra versionuv run lra config        # show the resolved config, with secrets redacted# run a mission locally, no servers needed (uses the stub model at $0)uv run lra mission --task "Create a hello.py that prints hello and a test for it" \                   --workdir .lra/workspaces/demo

这个 lra mission 命令会把任务规划成 checklist,然后逐项工作,直到每一项都通过验证。在命令和日志中,你会看到它被称为 mission,例如 lra mission,这只是系统对“一次针对一个任务的运行”的叫法。它所做的一切都是真实的 git history,因此之后你可以用 git -C .lra/workspaces/demo log --oneline 检查工作内容。现在,让我们理解为什么这个系统要这样构建。

目录

  • 核心思想:假设一定会被中断

    ∘ Context Window 是有损缓存

    ∘ 真实状态位于 Window 之外

  • Saved State

    ∘ Checklist

    ∘ 决策与事件

    ∘ 读取当前进展

    ∘ 提交 Checkpoint

  • Agent Cycle

    ∘ Think, Act, Verify, Checkpoint

    ∘ Tool Loop

    ∘ Context Compaction

  • 模型层

    ∘ 一个接口,多种 Backend

    ∘ 真实 Token,真实成本

    ∘ Failover

  • Tools 与 Sandbox

    ∘ Allow-List Dispatcher

    ∘ Sandbox

  • Deterministic Verification

    ∘ 由 Exit Code 决定

    ∘ 信任 Gate

    ∘ Flaky Quarantine

  • Durable Backbone

    ∘ Deterministic Scheduler

    ∘ Journaled Activities 与 Replay

    ∘ Durable Sleep 与 Continue-As-New

    ∘ Claim-Check Codec

    ∘ Sagas 与 Compensation

  • 不均衡的团队

    ∘ 为什么 Multi-Agent 是一种成本

    ∘ 角色与模型层级

    ∘ Planner

    ∘ Single-Writer-Per-File Ownership

    ∘ Researcher Fan-Out

    ∘ Fresh-Context Reviewer

    ∘ Decision Log

  • 保持安全并控制预算

    ∘ Budget Governor

    ∘ Loop Detector∘ Default-Deny Egress

    ∘ Rule of Two

    ∘ Human-in-the-Loop Gates

  • 能跨越数周的 Memory

    ∘ 三层 Memory

    ∘ Embeddings

    ∘ Hybrid Retrieval

    ∘ Consolidation∘ Skill Library

  • 让经验变成永久改进

    ∘ 提出更好的 Prompt

    ∘ Eval Harness 与 Judge

    ∘ Promotion Gate

  • 一个任务的一周生命周期

    ∘ Day Zero:研究与计划

    ∘ 困难部分

    ∘ Crash 与 Replay

    ∘ 摆脱卡住状态

    ∘ Fan-Out 与最终 Gate

  • 哪些已被证明,哪些仍是 Frontier

  • 下一步

核心思想:假设一定会被中断

在写任何代码之前,我们需要理解支撑一切的一个理念。如果一个 agent 必须运行一周,那么在这一周里它一定会被中断。机器会重启,API 会 rate limit,context window 会溢出,worker process 会被部署杀掉。中断在这里不是边缘情况,而是常态。所以我们从一开始就为它设计,而遵循的规则很简单:假设一定会被中断。

Context Window 是有损缓存

最常见的错误是把模型的 context window 当作 agent 的 memory。它不是。Context window 是一个小型、有损缓存。它最多只能容纳几十万 token,填满时会被 summarize 或 truncate,进程重启时会完全消失。如果我们的 agent 对“我在这个任务中进行到哪里了”的理解只存在于 context window,那么第一次中断就会把它抹掉。

因此,在数千轮之后,context window 会被填满,而 crash 或 reboot 会把它丢掉。发生这种情况时,天真的 agent 会完全失去对当前状态的感知。它不知道自己已经写了哪些文件,哪些测试已经通过,或者已经做过哪些设计决策。这正是大多数长时间 agent run 崩溃的原因。我们要通过把真实状态移动到不会消失的地方来解决这个问题。

真实状态位于 Window 之外

修复方式是把任务的真实状态保存在模型之外,保存在一个能经受任何情况的地方。我们使用一个 git repository,再加上其中几个结构化文件。模型在每个 cycle 开始时读取这个状态来赶上进度,完成一小段工作,然后把结果 commit 回去。Context window 变得可丢弃,因为状态从来不在里面。

这张图概括了整个思想。真实状态是一个 git repo 加上一小组 saved state files。模型每个 cycle 都重新读取它,所以一个全新的、context window 为空的模型可以在几秒内重建完整认知。然后它把结果 commit 回同一个状态。Cycle 之间的 crash 不会丢失任何东西,因为唯一持久的状态就是我们已经 commit 的部分。现在,让我们构建这个 saved state。

Saved State

Saved state 是一组保存所有重要信息的文件。它位于我们正在处理的 repository 内部的 .lra/ 文件夹,并与实际代码变更一起提交到 git。这样,一个 commit 同时捕获了工作本身和工作进度。我们先看数据,因为数据就是设计。

Checklist

Saved state 的中心是 checklist。每个 item 是一个小工作单元,而且 item 只能处于四种状态之一:todoin_progressblocked 或 done。我们稍后会强制执行的重要规则是:只有真实测试证明后,item 才能翻转为 done。下面是用 Pydantic 写的数据模型。

# src/lra/contracts/state.pyItemStatus = Literal["todo", "in_progress", "blocked", "done"]class ChecklistItem(BaseModel):    """One mission work-unit; flips to `done` only after deterministic verification."""    id: str    description: str    status: ItemStatus = "todo"    # IDs of the checks (tests/lint) that actually proved this item done.    verified_by: list[str] = Field(default_factory=list)    depends_on: list[str] = Field(default_factory=list)  # other item ids    attempts: int = 0                                     # how many tries so far    notes: str = ""    schema_version: int = 1                               # for safe migrations later    @property    def is_open(self) -> bool:        return self.status in ("todo", "in_progress", "blocked")

每个 item 都带有自己的 dependency list 和 attempt counter。verified_by 字段是一条 audit trail。它记录证明该 item 完成的 check 名称,因此之后我们可以用具体答案回答“我们怎么知道它能工作?”,而不是靠猜。Checklist 本身只是这些 item 的有序列表,加上一个非常重要的方法。

class Checklist(BaseModel):    items: list[ChecklistItem] = Field(default_factory=list)    schema_version: int = 1    def next_actionable(self) -> ChecklistItem | None:        """Return the first open item whose dependencies are all done, else None."""        done = {i.id for i in self.items if i.status == "done"}        for item in self.items:            if item.is_open and all(dep in done for dep in item.depends_on):                return item        return None    @property    def all_done(self) -> bool:        return bool(self.items) and all(i.status == "done" for i in self.items)

next_actionable 方法让 agent 总是知道下一步该做什么。它遍历列表,返回第一个 dependencies 都已完成的 open item。这使得一个没有任何 memory 的 fresh agent 也能准确接上上一个 agent 停下的位置。它不需要记住任何东西,只需要询问 checklist。

决策与事件

Checklist 告诉你还剩什么,但不会告诉你早先为什么做出某些选择,也不会给你逐步发生的历史。为此,我们在 checklist 旁边保存两个 append-only log。一个用于 decisions,一个用于 events。

class DecisionRecord(BaseModel):    """A durable, never-compacted record of an implicit design decision (anti-drift)."""    decision: str               # e.g. "use a synchronous read loop, not asyncio"    rationale: str    alternatives_rejected: str = ""    affected: list[str] = Field(default_factory=list)   # item ids this touches    cycle_id: str = ""class EventRecord(BaseModel):    """One entry in the append-only episodic event log."""    kind: str                   # "cycle", "tool_call", "verification", ...    cycle_id: str = ""    payload: dict[str, object] = Field(default_factory=dict)    # Pointer to a large blob in the object store, to keep the log compact.    payload_ref: str | None = None

Decision log 比看起来更重要。当 context window 被 compact 时,最先被丢掉的往往是那些小的隐式决策,比如“我们选择直接读取 bytes,而不是使用 input()”。而这些恰恰是后来导致 drift 的决策:未来某个 cycle 悄悄违背了过去的选择。通过把它们写进一个永不 compact 的 append-only 文件,我们总能用旧决策检查新工作。

读取当前进展

现在是关键操作。在每一个 cycle 开始时,agent 都会从这些文件加 git log 中重建完整认知。这就是“假设一定会被中断”这条规则变成方法后的样子。下面是来自 GitMissionAnchor class 的 read path。

# src/lra/state/mission_anchor.pydef _read_sync(self) -> SituationSnapshot:    checklist = self._read_checklist()    progress = ""    progress_path = self._path(PROGRESS_FILE)    if progress_path.exists():        progress = progress_path.read_text(encoding="utf-8")    # The active item is whatever is in_progress, else the next actionable one.    active = next((i for i in checklist.items if i.status == "in_progress"), None)    if active is None:        active = checklist.next_actionable()    return SituationSnapshot(        head_sha=git_ops.head_sha(self.workdir),        recent_commits=git_ops.log_oneline(self.workdir, 10),        progress_summary=progress,        open_items=[i for i in checklist.items if i.is_open],        last_decisions=self._read_recent_decisions(5),        active_item=active,        is_complete=checklist.all_done,    )

请慢慢读这个方法,因为它是整个系统的核心。它打开 checklist 文件,读取 progress narrative,找到 active item,拉取最近十个 git commits 和最近五个 decisions,并把它们打包成一个 SituationSnapshot。这个过程以毫秒计,而且对一个从未见过该任务的全新进程也有效。这就是让 crash 变成非事件的原因。下一个 worker 只需读取 snapshot 并继续。

提交 Checkpoint

另一半是写入。当一个 cycle 结束时,我们写入更新后的文件,并用单个原子 git commit 提交它们。如果 commit 成功,真实状态就前进了。如果在 commit 之前发生任何 crash,真实状态就没有前进,下一个 cycle 会重试同一个 item。不存在半完成状态。

def _commit_sync(self, checkpoint: Checkpoint) -> str:    self.anchor.mkdir(parents=True, exist_ok=True)    self._write_checklist(checkpoint.checklist)              # checklist.json    if checkpoint.progress_summary:        self._path(PROGRESS_FILE).write_text(checkpoint.progress_summary, encoding="utf-8")    if checkpoint.decisions:                                 # append, never overwrite        with self._path(DECISIONS_FILE).open("a", encoding="utf-8") as fh:            for decision in checkpoint.decisions:                fh.write(decision.model_dump_json() + "\n")    if checkpoint.events:                                    # append, never overwrite        with self._path(EVENTS_FILE).open("a", encoding="utf-8") as fh:            for event in checkpoint.events:                fh.write(event.model_dump_json() + "\n")    message = checkpoint.commit_message or f"lra: checkpoint {checkpoint.cycle_id}"    return git_ops.commit_all(self.workdir, message)         # git add -A && git commit

注意,decisions 和 events 是 append,而不是 overwrite,而 checklist 会完整重写。整个过程以一个 git commit 结束。由于 commit message 是 deterministic 且可读的,git history 本身就变成了人类可读的 progress report。下面是一个三项小任务完成后的 history 示例。

#### OUTPUT ####$ git -C .lra/workspaces/demo log --oneline9d8c7b6 lra: complete 03 (task 3)b3c4d5e lra: complete 02 (task 2)a1f2e3d lra: complete 01 (task 1)0c1d2e3 lra: initialize mission anchor

每个完成的 item 一个 commit,再加上创建 saved state 的初始 commit。你无需任何特殊工具,只读 git log 就能理解整个任务。这就是把真实状态放入 git 的好处。现在我们有了存储进度的地方。接下来需要真正推动进度的循环。

Agent Cycle

我们已经有了保存真实状态的地方。现在需要一个循环来读取它、做一点工作、再写回去。我把一个这样的工作单元称为 cycle。Cycle 被刻意设计得很小。它处理一个 checklist item,让模型运行几轮,验证结果,然后 commit。保持工作单元小,是系统具有弹性的原因:因为 crash 最多只会让我们丢失一个未完成的 cycle。

Think, Act, Verify, Checkpoint

每个 cycle 都遵循同样四步。它从 git 读取当前进展,通过模型和 tools 采取行动,用真实 checks 验证结果,并把结果 checkpoint 回 git。然后重复。

下面是来自 AgentLoop class 的 cycle 开头。它做的第一件事是读取 snapshot,如果任务已经完成,就立即返回。这个 early return 让整个 loop 具备 idempotent 特性。

# src/lra/agent/loop.pyasync def run_cycle(self, *, ctx, mission_id, cycle_id, anchor_text, checks=None):    checks = checks or []    snapshot = await self._anchor.read_situational_awareness()    # Idempotent guard: if there is nothing left to do, this cycle is a no-op.    if snapshot.is_complete or snapshot.active_item is None:        return CycleOutcome(            item_id=None, advanced=False, verified=False,            is_complete=True, head_sha=snapshot.head_sha, tool_calls=0, turns=0,        )    item = snapshot.active_item  # the one thing we will work on this cycle    messages = build_messages(        anchor_text=anchor_text, snapshot=snapshot, item=item,        specs=self._dispatcher.specs(),   # the tools this agent is allowed to use    )

Cycle 只选择一个 item,即 snapshot 中的 active item,并构建一个 prompt,其中包含目前的进度、open items,以及它被允许调用的 tools。注意,它从不相信自己的 memory 来决定要做什么。它总是询问刚刚读取的 snapshot。接下来进入真正思考和行动的部分。

Tool Loop

在一个 cycle 内部,模型和 tools 轮流执行。模型要么产生一个 tool call,要么发出“done”信号。如果是 tool call,我们运行 tool,把结果作为 observation 反馈回去,然后再次询问模型。我们最多重复固定轮数,因此困惑的模型永远无法在单个 cycle 内无限循环。

tool_calls = 0for turns in range(1, self._max_turns + 1):    result = await self._model.complete(messages)     # one real model turn    if self._ledger is not None:                      # record real token cost        self._ledger.record(            cycle_id=cycle_id, usage=result.usage,            usd=self._model.estimate_cost_usd(result.usage),        )    action = parse_action(result.text, result.tool_calls)    if action.done or action.tool is None:            # the model says it is finished        messages.append(ModelMessage(role="assistant", content=result.text))        break    # Run the requested tool through the safety gate (more on that gate later).    call = ToolCall(id=f"{cycle_id}-{turns}", name=action.tool, arguments=action.arguments)    tool_result = await self._dispatcher.dispatch(call, ctx)    tool_calls += 1    observation = (tool_result.content or tool_result.error or "")[:4000]    messages.append(ModelMessage(role="assistant", content=result.text))    messages.append(ModelMessage(role="user", content=f"OBSERVATION ({action.tool}): {observation}"))

每个模型 turn 都会把真实 token usage 记录到 cost ledger,稍后我们会用它来执行预算。我们反馈回去的 observation 被限制在四千字符,因此巨大的 tool output 不会撑爆 context。当模型最终发出完成信号时,我们跳出循环并进入 verification。下面是真实 LSP 任务中的一个 cycle,lead engineer 写入 transport 文件并运行测试。

#### OUTPUT ####07:30:04 INFO  lead.turn cycle=c1 model=claude-opus-4-8 tok_in=8112 tok_out=905 cache_read=5900 cost=0.071207:30:16 INFO  tool.exec cycle=c1 name=write_file path=pylsp_mini/transport.py bytes=81208:01:59 INFO  tool.exec cycle=c1 name=run_command cmd="uv run pytest tests/test_initialize.py -q" -> exit=1    tests/_fakeclient.py:55: in _read_response        header = self._read_until(b"\r\n\r\n")    E   Failed: Timeout >10.0s08:02:10 WARN  verify.fail cycle=c1 attempt=1 reason=timeout tests=0/1

一个模型 turn,一个文件写入,一个测试运行,然后它因 timeout 失败。这是一个完全正常的 cycle。工作没有通过,所以该 item 不会被标记为 done。相反,它会被记录为一次失败尝试,下一个 cycle 会继续尝试。系统正在完全按设计运行,我们稍后会继续追踪这个 bug 的结局。

Context Compaction

单个 cycle 可以运行很多 turn,message list 会随着每次 tool call 和 observation 增长。如果某个 cycle 探索得很多,这个 list 可能长到给 window 带来压力。所以我们会 compact 它。我们完整保留 system prompt 和最近几轮,把更早的所有内容总结成一条 compact note。

# src/lra/agent/compaction.pyasync def compact_messages(*, model, messages, keep_last=4):    """Return system + a summary-of-older-turns + the last `keep_last` turns."""    system = [m for m in messages if m.role == "system"][:1]    # never compact this    body = [m for m in messages if m.role != "system"]    if len(body) <= keep_last:        return messages                                         # nothing to do yet    to_summarize = body[:-keep_last]                            # older turns    recent = body[-keep_last:]                                  # keep these verbatim    transcript = "\n".join(f"{m.role}: {m.content}" for m in to_summarize)[:12000]    result = await model.complete([        ModelMessage(role="system", content=_SUMMARY_INSTRUCTIONS),        ModelMessage(role="user", content=transcript),    ])    summary = ModelMessage(role="user", content=f"[compacted summary]\n{result.text[:4000]}")    return [*system, summary, *recent]

Summary instruction 很具体。它要求模型保留任务、关键决策及其原因、发现内容包括失败方法,以及下一步。System prompt 永远不会被 compact,因为它至关重要。这让长探索可以保持在 window 内,同时不丢掉重要事实。值得注意的是,这种 in-context compaction 是 git 中 durable state 的一个更小、更快的亲戚。Git saved state 跨 cycle 和 crash 存活,而 compaction 只是让一个长 cycle 保持整洁。

模型层

Cycle 调用 self._model.complete(...),但它从不关心具体是哪一个模型。这种间接层是有意设计的。整个系统只与一个接口对话,我们可以通过一次 config 修改替换其背后的实际模型。这让同一套代码可以在测试中用本地模型零成本运行,也可以在生产中用 Claude 运行。

一个接口,多种 Backend

接口是一个小型 Protocol。模型接收一组 messages,并返回包含真实 token usage 的真实结果。仅此而已。

# src/lra/contracts/model.py@runtime_checkableclass ModelProvider(Protocol):    """A pluggable LLM backend. Implementations live in src/lra/model/."""    name: str    async def complete(self, messages, *, tools=None, max_tokens=None) -> "TurnResult":        """Run one real model turn and return its real output + usage."""        ...    def estimate_cost_usd(self, usage: "Usage") -> float:        """Compute real USD cost from real token counts (0.0 for local/free backends)."""        ...class Usage(BaseModel):    """Real token accounting as reported by the provider."""    input_tokens: int = 0    output_tokens: int = 0    cache_read_input_tokens: int = 0    cache_creation_input_tokens: int = 0    model: str = ""

这个接口有四种实现。stub 模型是 deterministic 且免费的,用于测试,这样整个系统无需网络或 key 就可以被 exercise。ollama 和 openai_compat backend 与本地或免费层云模型通信。claude backend 与 Anthropic 的 Messages API 通信。它们都返回同样的带真实 Usage 的 TurnResult,所以系统其他部分永远无需知道差异。Claude backend 甚至会单独报告 prompt cache tokens,这对长时间运行的成本非常重要。

# src/lra/model/claude.py  (the price table, dollars per million tokens)_PRICES: dict[str, tuple[float, float]] = {    "claude-opus-4-8": (15.0, 75.0),     # (input, output)    "claude-sonnet-4-6": (3.0, 15.0),    "claude-haiku-4-5-20251001": (1.0, 5.0),}

真实 Token,真实成本

每个模型 turn 都返回真实 token count,我们把它转换成真实美元金额并追加到 ledger。关键点是这个数字是真实的。我们从 provider 自己的 response 中读取它,从不估算或伪造成本。Ledger 很小。

# src/lra/governor/cost.pyclass CostLedger(BaseModel):    entries: list[CostEntry] = Field(default_factory=list)    def record(self, *, cycle_id: str, usage: Usage, usd: float) -> CostEntry:        entry = CostEntry(            cycle_id=cycle_id, model=usage.model,            input_tokens=usage.input_tokens, output_tokens=usage.output_tokens, usd=usd,        )        self.entries.append(entry)        return entry    @property    def total_usd(self) -> float:        return sum(e.usd for e in self.entries)    def mean_usd_per_cycle(self) -> float:        cycles = {e.cycle_id for e in self.entries}        return self.total_usd / len(cycles) if cycles else 0.0

Ledger 维护 running total 和 mean cost per cycle。这个 mean 让 budget governor 可以在下一步运行前预测成本。本地和免费 backend 记录真正的零成本,同时仍然追踪 tokens,因此即使在免费运行中,成本机制也会被 exercise。下面是 trace 中的几个模型 turn,cache reads 发挥了真实作用。

#### OUTPUT ####07:30:45 INFO  planner.turn cycle=replan-1 model=claude-opus-4-8 tok_in=7740 tok_out=1410 cache_read=6100 cost=0.073107:31:40 INFO  lead.turn   cycle=c201    model=claude-opus-4-8 tok_in=9340 tok_out=1190 cache_read=8100 cost=0.084111:24:40 INFO  governor.tick cum_usd=103.90 tests=9/14

看一下这个 lead turn 中的 cache_read=8100。大部分 input 都来自 prompt cache,这比新 input tokens 便宜得多。在持续一周、反复读取大量相同 context 的 cycles 中,cache 是一次可负担运行和一次昂贵运行之间的差别。而 governor.tick 行显示 running total 正被追踪,此处为 103 美元 90 美分,14 个 item 中有 9 个 green。

Failover

一周时间足够长,某个 provider 总会出现故障。因此我们可以用 FailoverModel 包装多个 providers。它按顺序尝试,并返回第一个成功结果。只有当所有 provider 都失败时才 raise。

# src/lra/model/failover.pyclass FailoverModel(ModelProvider):    """Try providers in order; return the first success, raise only if all fail."""    def __init__(self, providers: list[ModelProvider]) -> None:        if not providers:            raise ValueError("FailoverModel needs at least one provider")        self._providers = providers        self.name = "failover:" + ",".join(p.name for p in providers)

这段代码很小,但正是它让某个 API 的 transient outage 不会终止一个持续一周的任务。任务只是继续在 fallback model 上运行,而且由于模型层是一个接口,系统其他部分都不需要知道发生了这件事。还有一种 operational mode:如果所有模型都 hard down,任务会进入 durable sleep 并提醒人类,而不是消耗 retries。我们很快会看到 durable sleep。

Tools 与 Sandbox

模型不能直接接触世界。它只能请求运行某个 tool,而每个这样的请求都必须通过一个单一 gate。所有安全性都在这个 gate 中,而且它位于模型下方的代码中,而不是放在一个可以被巧妙输入说服绕过的 prompt 里。

Allow-List Dispatcher

AllowListDispatcher 是从模型请求到真实 action 的唯一路径。它在运行任何东西之前检查四件事。Tool 是否已知?是否在 allow list 上?如果该 tool 会 mutate,mutation 是否被允许?如果该 tool 会访问网络,egress 是否被允许?Egress 默认拒绝。

# src/lra/execution/dispatcher.pyasync def dispatch(self, call: ToolCall, ctx: ToolContext) -> ToolResult:    tool = self._tools.get(call.name)    if tool is None:        return ToolResult.failure(f"unknown tool: {call.name!r}")    if not self._permitted(call.name):        return ToolResult.failure(f"tool not allowed: {call.name!r}")    if tool.spec.mutating and not self._allow_mutating:        return ToolResult.failure(f"mutating tools are disabled: {call.name!r}")    if tool.spec.egress and not self._allow_egress:        return ToolResult.failure(f"egress is disabled (default-deny): {call.name!r}")    missing = _missing_required(tool.spec.parameters, call.arguments)    if missing:        return ToolResult.failure(f"missing required args for {call.name!r}: {sorted(missing)}")    try:        return await tool.run(call.arguments, ctx)    except Exception as exc:  # a tool failure must NEVER crash the agent loop        return ToolResult.failure(f"{type(exc).__name__}: {exc}")

这里有两点我想强调。第一,gate 在模型下方的代码中强制执行。Prompt injection 无法授予自己一个 tool,因为模型甚至不会被告知 forbidden tool 的存在。第二,最后的 try/except 意味着抛出异常的 tool 不会 crash cycle。它返回一个 failure result,成为 observation,模型可以对此作出反应。Loop 在 tool 一侧是不可打破的。不同角色拥有不同 dispatcher,因此 read-only researcher 字面意义上无法获得 mutating tool。

Sandbox

运行命令的 tools 不会在 host 上运行。它们在 sandbox 内运行,而 sandbox 只是另一个拥有三种实现的接口。开发用 local subprocess sandbox,CI 用 Docker sandbox,生产用 E2B micro VM sandbox。Agent loop 与接口通信,永远不知道底层是哪一个实现。

# src/lra/contracts/sandbox.py@runtime_checkableclass Sandbox(Protocol):    name: str    async def open(self, *, workdir: str, snapshot_id: str | None = None) -> "SandboxSession":        """Open (or restore from snapshot_id) a session rooted at workdir."""        ...    async def snapshot(self, session: "SandboxSession") -> "Snapshot":        """Persist the session's state and return a restorable handle."""        ...

这里有意思的方法是 snapshot。Sandbox session 可以被冻结,并稍后从 handle 恢复。这对持续一周的运行很重要,因为它意味着不仅 git state,连 working environment 本身也可以在 crash 或长 sleep 后暂停并恢复。我们之前在 trace 中 host reboot 时看到过一行 sandbox.restore。有了 gated 和 sandboxed tools,我们就可以允许模型行动了。但行动不等于成功。为此,我们需要决定什么才算 done 的 gate。

Deterministic Verification

这是博客中最重要的一节,所以我想放慢速度。在数千步中,小错误会复合。如果每一步有百分之九十九的可靠性,那么几百步后你几乎肯定已经坏了。阻止这种复合的唯一方法,是永远不要相信模型自己声称某个东西可用。我们要相信真实测试。

由 Exit Code 决定

DeterministicVerifier 将每个 check 作为真实 subprocess 运行,并查看它的 exit code。只有当每个 gating check 都以 zero 退出时,一个 item 才算 done。不是模型说了算,而是 exit code 说了算。

# src/lra/verify/verifier.pyclass DeterministicVerifier:    async def verify(self, workdir: str, checks: list[Check]) -> VerificationResult:        results: list[CheckResult] = []        for check in checks:            outcome = await run_proc(check.command, cwd=workdir, timeout_s=self._timeout)            results.append(                CheckResult(name=check.name, passed=outcome.ok, exit_code=outcome.exit_code)            )        # The gate: EVERY gating check must pass. Advisory checks are recorded, not gating.        all_green = all(            result.passed            for check, result in zip(checks, results, strict=True)            if check.gating        )        return VerificationResult(all_green=all_green, results=results)

Python project 的默认 gate 是三条真实命令,你在 trace 中已经见过它们。

def default_python_checks() -> list[Check]:    return [        Check(name="ruff", command=["uv", "run", "ruff", "check", "."]),        Check(name="mypy", command=["uv", "run", "mypy"]),        Check(name="pytest", command=["uv", "run", "pytest", "-q"]),    ]

回到 agent loop,这个 verifier 的结果是决定 item 命运的唯一因素。如果 all_green 为 true,item 变为 done,它的 verified_by 被填入 check names。如果不是,item 变为 blocked,attempt counter 增加。下面是 trace 中我们一直跟踪的 transport bug 最终通过的时刻。

#### OUTPUT ####09:10:21 INFO  tool.exec cycle=c201 name=run_command cmd="uv run pytest tests/test_framing.py -q" -> exit=0    collected 7 items    tests/test_framing.py .......                                            [100%]    7 passed in 0.13s09:10:21 INFO  verify.ok cycle=c201 checks=[pytest]   <- FIRST GREEN (cycle 201, ~2.5 days in)09:11:02 INFO  git.commit head=a1f2e3d msg="lra: transport framing (isolated, byte-tested)"

Exit code zero,七个测试通过,只有这时 verifier 才说 verify.ok。这是整个任务的第一个 green,出现在 cycle 201,大约两天半之后。紧随其后的 commit 是第一段真正可信的进展。此前的一切都只是 attempts。

信任 Gate

这里有一个微妙陷阱。只有当测试真正覆盖了代码时,green 才意味着正确。一个没有测试的 repo 也是 green,但毫无价值。因此,在我们高度依赖 gate 之前,特别是在允许 parallel writers 之前,我们会衡量自己能多大程度上信任它。我们使用两个数字:coverage 和 mutation score。

# src/lra/verify/trust_bootstrap.pyclass TrustBootstrap:    def __init__(self, *, coverage_threshold: float = 0.6, timeout_s: int = 900) -> None:        self._threshold = coverage_threshold    async def measure(self, workdir: str, *, command=None) -> "VerifierTrust":        cmd = command or ["uv", "run", "pytest", "--cov", "--cov-report=term-missing", "-q"]        result = await run_proc(cmd, cwd=workdir, timeout_s=self._timeout)        combined = f"{result.stdout}\n{result.stderr}"        pct = parse_coverage_pct(combined)        return VerifierTrust(coverage_pct=pct, trusted=pct >= self._threshold, raw_tail=combined[-2000:])

Coverage 告诉我们哪些行运行过。Mutation testing 更进一步。它故意在代码中引入 bug,并检查测试是否能抓住它们。高 mutation score 是 gate 真正能发现 regression 的最强证据。下面是 LSP 任务后期的 mutation run。

#### OUTPUT ####09:36:02 INFO  run_command cmd="uv run pytest --cov -q"  -> exit=0  "coverage: 91%"09:48:55 INFO  run_command cmd="uv run mutmut run"       -> exit=0  "killed 89/106 (score 0.84)"09:49:00 INFO  gate.trusted coverage=0.91 mutation=0.84

百分之九十一 coverage,测试杀死了一百零六个 mutants 中的八十九个,score 为 0.84。此时系统记录 gate.trusted,这意味着 green checkmark 现在有意义了。这就是获得信任 gate 权利的过程。

Flaky Quarantine

最后一个 verification 问题是 flaky test,也就是随机通过或失败的测试。它会污染 gate。如果 flaky check 能阻塞进度,那么 green 就变得不可靠,agent 会浪费 cycles 去追逐幽灵。因此,我们会 quarantine 已知 flaky checks。它们仍然运行,用于可见性,但会被强制设为 non-gating。

# src/lra/verify/flaky_quarantine.pyclass FlakyQuarantine:    def __init__(self) -> None:        self._flaky: set[str] = set()    def mark_flaky(self, name: str) -> None:        self._flaky.add(name)    def partition(self, checks: list[Check]) -> tuple[list[Check], list[Check]]:        """Split into (gating, quarantined); quarantined checks are forced non-gating."""        gating, quarantined = [], []        for check in checks:            if check.name in self._flaky:                quarantined.append(check.model_copy(update={"gating": False}))            else:                gating.append(check)        return gating, quarantined

被 quarantine 的 check 会被复制并设置 gating=False,所以它永远无法阻塞 item,但它仍然运行,这样我们可以随时间追踪它的 flake rate。这保持了 green 的含义干净。现在我们拥有了一个完整的 inner engine。它读取事实,通过 gated tools 在 sandbox 中行动,并且只有在可信测试通过时才标记进展。下一个问题是让这个 engine 存活一周。

Durable Backbone

Inner loop 很好,但它本身仍然只是一个 Python process。如果这个 process 死掉,loop 也随之死掉。为了运行一周,我们把 loop 包装在一个 durable execution engine 中。我们使用 Temporal。Temporal 的思想是,你的 control flow 被分成两类代码。一类是 workflow code,它必须 deterministic,并会被 journal 以便 replay。另一类是 activity code,它执行所有混乱的 non deterministic 工作,其结果被记录一次,之后 replay 时从 cache 提供。这个拆分让我们免费获得 crash recovery。

Deterministic Scheduler

MissionWorkflow 是 scheduler,它被刻意设计得小而无聊。它不包含模型调用、不包含文件 IO、不包含 randomness。它所做的一切就是一次派发一个 cycle activity,并统计已经运行了多少个。

# src/lra/durable/workflows.py@workflow.defnclass MissionWorkflow:    @workflow.run    async def run(self, inp: MissionInput, state: MissionState | None = None) -> MissionResult:        state = state or MissionState()        self._cycles_done = state.cycles_done        while True:            if state.cycles_done >= inp.max_cycles:                return await self._terminal(inp, state, completed=False)            # The ONE activity per loop. All real work happens inside it.            result = await workflow.execute_activity(                run_agent_cycle,                CycleInput(                    mission_id=inp.mission_id, workdir=inp.workdir,                    cycle_id=f"c{state.cycles_done + 1}", check_commands=inp.check_commands,                ),                start_to_close_timeout=timedelta(minutes=10),                retry_policy=_CYCLE_RETRY,           # up to 10 tries, exponential backoff            )            state.cycles_done += 1            self._cycles_done = state.cycles_done            if result.is_complete:                return MissionResult(mission_id=inp.mission_id, completed=True,                                     cycles=state.cycles_done, head_sha=result.head_sha,                                     items_done=result.items_done, items_total=result.items_total)            if state.cycles_done % inp.cycles_before_can == 0:                workflow.continue_as_new(args=[inp, state])    # bound history (see below)

这就是整个 scheduler。读一遍并注意它不包含什么。这里没有业务逻辑。Workflow 只决定何时运行下一个 cycle,以及何时停止。由于 workflow 是 deterministic,Temporal 可以随时 replay 它的 history,并得到完全相同的状态。Retry policy 意味着如果某个 cycle activity 失败,Temporal 会用 backoff 最多重试十次,然后才放弃。真正的智能全部位于那个 activity 内部。

Journaled Activities 与 Replay

Activity 是所有 non deterministic 事情发生的地方。它运行我们之前构建的真实 AgentLoop。关键属性是它是 idempotent。它总是从 git 读取 fresh state,并推进下一个 actionable item,因此运行两次也是安全的。

# src/lra/durable/activities.pyasync def _execute_cycle(inp: CycleInput) -> CycleResult:    """Advance the mission by one verified item via the real agent loop; idempotent."""    anchor = GitMissionAnchor(inp.workdir)    snapshot = await anchor.read_situational_awareness()   # always read truth first    if snapshot.is_complete or snapshot.active_item is None:        # Already done: returning early is what makes a retry a safe no-op.        checklist = await anchor.read_checklist()        done = sum(1 for i in checklist.items if i.status == "done")        return CycleResult(item_id=None, advanced=False, head_sha=snapshot.head_sha,                           is_complete=True, items_done=done, items_total=len(checklist.items),                           note="nothing actionable")    session = await LocalSandbox().open(workdir=inp.workdir)    loop = AgentLoop(model=build_provider(), dispatcher=AllowListDispatcher(default_local_tools()),                     verifier=DeterministicVerifier(), anchor=anchor)    # ... run one cycle, then return a small CycleResult that Temporal journals ...

现在看看运行中途主机重启会发生什么。这来自 LSP trace 的第三天,当一次主机 OS update 在 mid cycle 时重启了机器。

#### OUTPUT ####13:48:12 ERROR worker.disconnect reason="connection reset (host OS update reboot)" inflight_cycle=c19013:55:40 WARN  temporal.replay wf=mission:mission_4c7e21a9 events=631 (completed activities served from cache; 0 tokens re-spent; 0 double commits)13:55:41 INFO  reconcile.in_flight ticket=01 branch=feat/transport -> adopt (branch present, unmerged)13:55:42 DEBUG orchestrator.note "replay restored state EXACTLY -> a still-RED transport. durability protects WORK, not CORRECTNESS."

这是 durability guarantee 的可视化。Worker 死了,一个新的 worker 启动,Temporal replay 了六百三十一个 events。每个已经完成的 activity 都从 cache 提供,因此没有重复花费 token,也没有重复 commit。任务恢复到它所在的精确 cycle。我很喜欢 trace 中最后一行。

Durability 保护的是工作,而不是正确性。Replay 恢复的是一个仍然 failing 的 transport,因为那就是真实状态。系统不会假装一个坏掉的东西已经修好。它只是拒绝丢失进展。

Durable Sleep 与 Continue-As-New

另外两个技巧让持续一周真正可行。第一个是 durable sleep。当没有事情要做时,比如夜间,workflow 会睡在一个 durable timer 上。这不产生成本,并且能经受 reboot。第二个是 Continue-As-New。一周运行否则会堆积巨大 event history,因此每隔若干 cycles,workflow 会滚动到一个 fresh execution,只携带一个很小的 compact state。

# inside MissionWorkflow.run, the rollover line again:if state.cycles_done % inp.cycles_before_can == 0:    # Start a fresh execution carrying only the compact MissionState (pointers),    # never the history. The new run replays prior cycles from the activity cache.    workflow.continue_as_new(args=[inp, state])

被携带过去的 MissionState 只是一个 counter:cycles_done。其他所有内容都在 git 中。因此 rollover 很便宜,新 execution 从干净的小 history 开始。下面是 trace 中一天结束时的安静状态。

#### OUTPUT ####18:32:04 INFO  sleep.durable until=2026-05-27T07:30:00Z reason=idle-eod18:32:04 INFO  consolidate episodes=12 facts=5 ; governor.tick cum_usd=0.094...07:30:00 INFO  wake active=01 reground=git+anchor dur=0.041s

任务从傍晚睡到第二天早晨,成本为零,然后醒来并在四十一毫秒内从 git reground。这个 reground 步骤就是我们之前的 read_situational_awareness。Agent 睡觉,世界继续转动,它醒来时仍然准确知道自己在哪里。当大部分 wall clock time 都在睡眠中度过时,一周就是这样的。

Claim-Check Codec

一周内,workflow 会 journal 数千步,其中一些 payload 很大,例如大型模型 response 或长 test log。如果把它们原样 journal,Temporal history 会膨胀。因此我们使用 claim-check pattern。超过阈值的任何 payload 都存入 object store,journal 中只保存一个短的 content addressed key。

# src/lra/durable/codec.pyclass ClaimCheckCodec(PayloadCodec):    async def encode(self, payloads):        encoded = []        for payload in payloads:            if len(payload.data) > self._threshold:        # default 32 KiB                key = await self._store.put(payload.data)   # store the bytes...                encoded.append(Payload(                    metadata={"encoding": _CLAIMCHECK_ENCODING,                              "lra-orig-encoding": payload.metadata.get("encoding", b"")},                    data=key.encode("utf-8"),               # ...journal only the key                ))            else:                encoded.append(payload)        return encoded

Object store keys 是内容的 SHA256 hash,因此相同 payload 会自动只存一次。一个十 megabyte response 在 history 中会变成六十四字符 hash。你可以在 trace 的第一行看到这一点,data converter 被配置了 claim-check threshold。

#### OUTPUT ####09:00:01 INFO  temporal.connect addr=localhost:7233 ns=default build_id=lra-7f1c               data_converter=ClaimCheckCodec(threshold=32768)

Codec 被接入 data converter 层,因此 activity code 根本不知道它在发生。它只是传递 objects,大型对象会被透明 offload。这让 journal 即使在一周工作后仍然足够小,可以快速 replay。

Sagas 与 Compensation

最后一个 durability 组件用于触达 git repo 外部的 action,例如创建 branch、打开 pull request 或 merge。这些 action 是多步的,中途失败会留下一团糟。因此,每个 forward step 在运行前注册一个 undo action,失败时我们按相反顺序运行 undo。

# src/lra/durable/saga.pyclass Saga:    def add(self, name: str, undo: Callable[[], Awaitable[None]]) -> None:        self._compensations.append(Compensation(name=name, undo=undo))    async def compensate(self) -> list[str]:        """Run compensations newest-first (LIFO); record any undo failures, never stop."""        ran = []        for comp in reversed(self._compensations):            try:                await comp.undo()                ran.append(comp.name)            except Exception as exc:                self.failures.append(f"{comp.name}: {type(exc).__name__}: {exc}")        return ran

所以如果我们创建了 branch 并打开 pull request,然后 merge 失败,saga 会按这个顺序关闭 pull request 并删除 branch。有一个 caveat。真正不可逆的 action,例如 production deploy,不会被假装 rollback。相反,它会路由到 human gate,我们很快会讲到。Durable backbone 就位后,我们的单个 agent 现在可以运行数周。下一个问题是,一个 agent 是否真的是正确形态。

不均衡的团队

到目前为止,只有一个 agent 在做所有事情。显而易见的下一步是往问题上扔一群 agents。我想反驳这个想法,因为更多 agents 并不自动更好。更多 agents 是一种成本,我们只应该在它真正带来收益时才支付它。

为什么 Multi-Agent 是一种成本

当多个 agents 同时写代码时,它们会做出相互冲突的假设,踩到彼此的文件,设计会失去一致性。Parallelism 对真正独立的工作有帮助,例如读取代码库的不同部分,或者用 fresh eyes 审查已完成工作。但对于 coupled writing,它是有害的,因为每个决策都依赖上一个。因此团队是刻意不均衡的。

形态如下。一个 single threaded lead engineer 拥有所有 coupled code writing,因此设计决策保持一致。Ephemeral helpers 只在两类真正可并行化的事情上 fan out。Researchers 并行阅读,reviewer 用 fresh context 检查工作。一个单独的 integrator 是唯一允许写入 main branch 的角色。这不是 swarm。这是一个小而刻意的团队,其中昂贵部分,也就是 coherent writing,保持 serial。

角色与模型层级

每个角色由一个小 spec 描述,说明它使用哪个 model tier,以及被允许做什么。成本纪律就体现在这里。我们只在 high leverage judgment 上使用昂贵的 Opus 模型,而 bulk search 使用便宜模型。

# src/lra/agents/roles.pyclass RoleSpec(BaseModel):    name: str    tier: ModelTier             # OPUS | SONNET | HAIKU    system_prompt: str    allow_mutating: bool = False    allow_egress: bool = False    max_turns: int = 8# the lead is the only role that writes the integration line:"lead": RoleSpec(    name="lead",    tier=ModelTier.OPUS,    system_prompt=(        "You are the Lead Engineer and the SOLE writer to the integration line. Keep design "        "decisions coherent. Mark an item done only when the deterministic checks are green."    ),    allow_mutating=True,),

下面是 tiers 到 roles 的映射。高判断力角色使用最强模型,而便宜的并行工作使用小模型。

  • Planner、lead 和 reviewer 使用 Opus,因为它们的判断会塑造一切。

  • Integrator 和 tester 使用 Sonnet,处理中等重量级工作。

  • Researcher 使用 Haiku,因为阅读和总结很便宜,而且会运行很多次。

这使整个团队的成本与决策价值成正比,而不是与计算量成正比。一个运行五十次的 researcher 几乎不花钱,而做出 coherent design choices 的 lead 使用值得付费的模型。

Planner

任务从 planner 开始。它接收任务描述,并将其转化为一组小型、有序、可独立验证的 checklist steps。它要求模型返回 JSON array,然后防御性地 parse,因为模型并不总是返回干净的 JSON。

# src/lra/agents/planner.pyclass Planner:    async def plan(self, *, title: str, description: str, acceptance: str = "") -> Checklist:        messages = [            ModelMessage(role="system", content=ROLES["planner"].system_prompt),            ModelMessage(role="user", content=(                f"Mission: {title}\n\n{description}\n\n"                f"Definition of done: {acceptance or '(use your judgment)'}\n\n"                f"{_PLANNER_INSTRUCTIONS}")),        ]        result = await self._model.complete(messages)        items = parse_checklist(result.text)        if not items:            # Never fail to produce a plan; degrade to a single item.            items = [ChecklistItem(id="01", description=description.strip() or title.strip())]        return Checklist(items=items)

最后的 fallback 很重要。如果模型返回垃圾,我们不会 crash。我们从 description 生成一个单 item checklist,这样任务总能启动。下面是 planner 在 LSP 任务中的工作,我希望你注意输出里有一个令人不舒服的地方。

#### OUTPUT ####09:47:55 INFO  checklist.write items=31 sha256=4b1c... (01 transport, 02 init, 03 hover,               04 completion, 05 definition, 06 diagnostics, 07 formatting, 08 documentSymbol, 09 rename, ...)09:47:55 INFO  ownership.map lead=[transport,dispatch,index] implementers=[per-feature]

Planner 生成了三十一个 items。事实证明这太多了。在核心甚至还没工作之前规划完整 server,是后续多天浪费工作的种子,我们稍后会看到。Planner 很强大,但它也会 over reach,系统需要其他机制来捕捉这一点。这本身就是一个很好的教训。Plan 是一个假设,不是合同。

Single-Writer-Per-File Ownership

当我们确实允许 parallel implementers 时,会用一个简单 invariant 防止冲突。每个文件只有一个 writer。Planner 将文件分配给 writers,而某些 shared files,例如 build manifests 和 __init__.py,只能永远属于 lead。

# src/lra/coordination/ownership.pyclass FileOwnershipMap(BaseModel):    owners: dict[str, str] = Field(default_factory=dict)   # path -> writer id    def permits(self, *, writer: str, path: str) -> bool:        """Whether `writer` may write `path` under the single-writer-per-file invariant."""        norm = _normalize(path)        if is_shared(norm):            return writer == LEAD               # shared files are always the lead's        owner = self.owners.get(norm)        if owner is None:            return writer == LEAD               # unassigned space belongs to the serial lead        return writer == owner

规则很容易表述。如果文件是 shared,只有 lead 可以写。如果文件有 owner,只有 owner 可以写。任何未分配区域默认属于 lead。这在 git 层检查,因此 stray write 会大声失败,而不是静默破坏另一个 implementer 的 slice。一个 writer 如果发现自己需要一个不属于自己的文件,会提交 lease request,而不是直接写。这就是我们在少数允许 parallel writing 的场合保持安全的方式。

Researcher Fan-Out

阅读是非常适合并行化的工作,因为它没有副作用。当 lead 需要 context 时,orchestrator 会同时 fan out 几个 researchers,每个研究不同问题,并收集它们的 briefs。

# src/lra/agents/team.pyasync def research_fanout(*, model, dispatcher, ctx, queries, role=None) -> list[SubAgentResult]:    """Investigate `queries` in parallel; return one brief per query (failures filtered out)."""    role = role or ROLES["researcher"]    agents = [SubAgent(role=role, model=model, dispatcher=dispatcher) for _ in queries]    results = await asyncio.gather(        *(agent.run(objective=q, ctx=ctx) for agent, q in zip(agents, queries, strict=True)),        return_exceptions=True,    )    return [r for r in results if isinstance(r, SubAgentResult)]   # keep the successes

Researchers 是 read only,因此它们拿到的是 mutation disabled 的 dispatcher。它们并发运行,任何失败者都会直接从结果中过滤掉。在 LSP 任务的 day zero,lead 不知道协议的 wire format,所以第一步花在 research 上,而不是猜测。

#### OUTPUT ####09:00:08 INFO  research.fanout n=4 task_queue=lra-research rule_of_two=ok caps=209:00:20 INFO  research.brief id=1 "FRAME ON BYTES. Content-Length = len(utf8(body)). headers ASCII + CRLF;               body=utf-8 JSON-RPC 2.0. POSITIONS: 'character' is UTF-16 CODE UNITS (HAZARD-1)."09:00:36 INFO  research.brief id=2 "pygls: read header lines off sys.stdin.BUFFER until blank; read(n) EXACTLY;               write to sys.stdout.BUFFER. NEVER print()/input(). pylsp setmode(O_BINARY) on Windows."

四个 researchers 并行运行,并返回了清晰的 briefs。Brief one 甚至标记了一个几天后才会咬人的 hazard:UTF-16 column encoding。这就是便宜并行阅读的好处。花费几美分的 Haiku tokens,lead 在真正开始工作时已经知道问题形态。Research 没写一行代码,它只是收集事实。

Fresh-Context Reviewer

第二件适合并行化的事情是 review。单个 agent 的陷阱是 author bias。写代码的 agent 是最不适合判断代码的人。因此 reviewer 是一个具有 fresh context 的独立 agent。它只看到 diff 和 acceptance criteria,从不看到 lead 的 reasoning 或早先 attempts。

# src/lra/agents/reviewer.pyclass Reviewer:    async def review(self, *, diff: str, criteria: str, ctx: ToolContext) -> ReviewResult:        result = await self._agent.run(            objective=("Review the diff against the acceptance criteria. Identify correctness, "                       "security, and scope issues. Prefix any blocking issue with 'BLOCK:'."),            ctx=ctx,            extra_context=f"Acceptance criteria: {criteria}\n\nDIFF:\n{diff[:8000]}",        )        blocking = "block:" in result.brief.lower() or "blocking" in result.brief.lower()        return ReviewResult(brief=result.brief, blocking=blocking, tool_calls=result.tool_calls)

Fresh context 是主要思想。因为 reviewer 没见过挣扎过程,所以它不会对某种 chosen approach 产生投入。当 LSP 任务在 transport 上卡住数天时,正是 reviewer 最终指出了真正问题。

#### OUTPUT ####10:15:40 INFO  reviewer.finding sev=blocking kind=architecture    "you are solving THREE problems in one file and they keep colliding: (1) framing,     (2) text-vs-binary streams, (3) concurrency. SEPARATE them. Make framing a pure byte-level     pair read_message/write_message and UNIT-TEST it in isolation. Use sys.std*.buffer only.     A minimal server needs NO concurrency: a synchronous read->dispatch->write loop."10:15:40 INFO  reviewer.finding sev=advisory kind=scope    "a broken core + 31 planned items is the real problem; cut scope until the core is green."

这一次 review 解开了整个任务。Lead 一直在 thrash,因为它把三个问题纠缠在一起。Reviewer 没有对十二次失败尝试中的任何方法产生 attachment,所以立刻看出来并要求拆分它们、缩小 scope。Blocking finding 会暂停工作并强制重新思考。这是整个运行中最清晰的例子,说明为什么独立视角值得付费。

Decision Log

我们之前已经见过 decision record 作为一种数据类型。下面是它在实践中的用法。每个有意义的设计决策都会被 append 到一个永不 compact 的 log 中,这样之后的工作就可以与它对照检查。

# src/lra/coordination/decision_log.pyclass DecisionLog:    """A JSONL file of DecisionRecords. Append-only; never rewritten."""    def append(self, record: DecisionRecord) -> None:        self._path.parent.mkdir(parents=True, exist_ok=True)        with self._path.open("a", encoding="utf-8") as fh:            fh.write(record.model_dump_json() + "\n")    def read(self) -> list[DecisionRecord]:        if not self._path.exists():            return []        lines = [ln for ln in self._path.read_text(encoding="utf-8").splitlines() if ln.strip()]        return [DecisionRecord.model_validate_json(ln) for ln in lines]

在 reviewer 介入之后,LSP 任务的 decisions 读起来就像一段清晰的故事,讲述 approach 如何变化。

#### OUTPUT ####07:31:02 INFO  decisions.append id=3 "narrow->14; transport-first TDD; SYNC loop; binary streams (sys.*.buffer)"

这一行记录了转折点。任务从三十一个 items 缩减到十四个,承诺对 transport 先做 test driven development,并选择 synchronous loop 和 binary streams。因为这被写下来并且永不 compact,未来任何 cycle 都不能悄悄漂移回纠缠的 async approach。Decision log 是团队关于“为什么”的长期 memory,而不只是“做了什么”。

保持安全并控制预算

一个能运行一周、能写代码、运行命令并访问网络的 agent 很强大,而强大的东西需要 guardrails。本节讨论让一次长运行不会悄悄烧钱、无限旋转或做危险事情的 guardrails。这些都不在 prompt 中。它们全部位于模型无法说服绕过的代码中。

Budget Governor

第一个 guardrail 是钱。如果 projected spend 会突破 ceiling,governor 会在下一个 cycle 运行前拒绝它。它不会等到超预算才行动,而是根据 running average 预测下一步成本,并提前停止。

# src/lra/governor/governor.pyclass BudgetGovernor:    """Refuses the next step BEFORE it runs if it would breach the spend/iteration ceiling."""    def authorize_next(self, ledger, *, cycles_done, projected_usd=None) -> GovernorDecision:        spent = ledger.total_usd        # Estimate the next step from history if no explicit projection was given.        estimate = projected_usd if projected_usd is not None else ledger.mean_usd_per_cycle()        projected_total = spent + estimate        if cycles_done >= self._max_cycles:            return self._deny("max cycles reached", projected_total, spent)        if projected_total > self._ceiling:            return self._deny("projected spend exceeds ceiling", projected_total, spent)        return GovernorDecision(allow=True, reason="within budget",                                projected_usd=projected_total, spent_usd=spent,                                ceiling_usd=self._ceiling)

Agent loop 在每个 cycle 顶部调用 authorize_next,如果 decision 不是 allow,任务会干净地停止,而不是 overspending。下面是 governor 授权一个 cycle,并在运行后期 tick running total 的日志。

#### OUTPUT ####07:30:01 INFO  governor.authorize cycle=c1 allow=true projected_usd=0.06 spent=0.094...17:48:02 INFO  governor.tick cum_usd=31.29 tests=0/1 active=01 cycles_today=1 attempts=12

第一行显示一个 cycle 被授权,因为 projected total 远低于 ceiling。第二行晚得多,显示 transport 上艰难一天后的 cumulative spend 为 31 美元 29 美分。这个任务中 governor 从未需要干预,因为它最终以 178 美元成本完成,低于 400 美元 ceiling,但它是 stuck task 不会默默烧出一千美元账单的原因。

Loop Detector

第二个 guardrail 是时间。Agent 可能卡在反复尝试同一个失败问题上。Loop detector 监控相同 state 和 action signature 是否重复出现,超过 threshold 后就 trip 并 escalate。

# src/lra/governor/governor.pyclass LoopDetector:    """Flags oscillation: the same (state, action) signature recurring too often."""    def __init__(self, *, threshold: int = 3) -> None:        self._threshold = threshold        self._counts: Counter[str] = Counter()    def observe(self, signature: str) -> bool:        """Record a signature; return True once it has looped past the threshold."""        self._counts[signature] += 1        return self._counts[signature] >= self._threshold

这正是让 LSP 任务免于永远在 transport 上打转的机制。在相同类型的 edit 不断失败后,detector 先 warn,然后 trip,并将工作 escalate 给我们之前见过的 reviewer。

#### OUTPUT ####17:48:02 WARN  loop_detector signature="01:transport:edit" count=3 action=warn...10:15:09 ERROR loop_detector signature="01:transport:rewrite" count=4 action=TRIP10:15:09 INFO  orchestrator.pause item=01 ; escalate=reviewer reason=loop-trip

Signature 01:transport:rewrite 出现了四次,detector trip,orchestrator 没有让 lead 继续失败,而是暂停 item 并请 reviewer 介入。正是这个 escalation 产生了解开一切的 architecture finding。Loop detector 没有修复 bug。它注意到 agent 无法独自修复它,并请求帮助。

Default-Deny Egress

现在是危险部分:网络。Egress policy 默认拒绝。除非 host 明确在 allow list 上,否则不可访问。而 agent 永远不持有真实 credentials。它只看到 placeholders,broker 在最后一刻,也就是 egress boundary,换入真实 secret。

# src/lra/safety/egress.pyclass EgressPolicy(BaseModel):    """Allow-list of permitted egress hosts (default-deny: empty = nothing allowed)."""    allow_hosts: set[str] = Field(default_factory=set)    def permits(self, url: str) -> bool:        host = urlparse(url).hostname        return host is not None and host in self.allow_hostsclass CredentialBroker:    """Maps placeholder tokens to real secrets, injected only at egress."""    def resolve(self, value: str) -> str:        for placeholder, secret in self._secrets.items():            value = value.replace(placeholder, secret)   # swap at the boundary only        return value

好处很具体。如果 agent 被它读取的恶意网页 hijack,最坏也只能泄漏一个无用的 placeholder string,因为真实 secret 从不进入它的 context。而且它只能访问我们批准的 hosts。在 trace 中,每次 fetch 都会在旁边记录 egress decision。

#### OUTPUT ####09:00:12 INFO  tool.exec agent=researcher-1 name=web_fetch               url=microsoft.github.io/.../specification/#headerPart -> 200 egress=allow(microsoft.github.io)

Fetch 成功,日志显示 egress=allow(microsoft.github.io),说明该 host 在 allow list 上。对任何不在列表上的 host 的请求,在离开机器之前就会被 policy 拒绝。Network access 是 agent 被狭窄授予的 privilege,而不是默认拥有的能力。

Rule of Two

这里还有一个更深层的安全思想,它是结构性的,而不是单个 check。Meta 将其命名为 rule of two。有三种危险能力:摄取 untrusted content、访问 private data,以及能够 external communicate。同时持有三者,是让 prompt injection 变得灾难性的危险组合。因此,一个 session 最多只允许持有三者中的两种。

# src/lra/safety/rule_of_two.pyclass Capability(str, Enum):    UNTRUSTED_CONTENT = "untrusted_content"   # web pages, issue text, ...    PRIVATE_DATA = "private_data"             # secrets, customer data    EXTERNAL_COMMS = "external_comms"         # egress / side effectsdef check_rule_of_two(capabilities: set[Capability]) -> None:    """Raise if a session would hold the full lethal trifecta (all three at once)."""    if len(capabilities) >= 3:        raise RuleOfTwoViolation(            "session would hold untrusted content + private data + external comms "            "(the lethal trifecta); split capabilities across sessions"        )

Orchestrator 在授予 session 能力之前检查这一点。如果某项工作需要全部三种能力,它会被拆分到不同 sessions 中。你在 research fan-out trace 中已经看到了证明,那一行写着 rule_of_two=ok caps=2。Researchers 可以读取 untrusted web content 并访问网络,这是两种能力,但它们不能访问 private data。系统在允许它们运行之前强制执行了这个限制。这是通过结构实现的安全,而不是希望模型表现良好。

Human-in-the-Loop Gates

某些 action 风险太高,永远不应自动执行,例如 merge 到 protected branch 或 deploy。对这些 action,系统会暂停并请求人类。Gate 是 durable 的,因此可以零成本等待数天,而且关键是它有 timeout 和 default action,因此人类缺席不会让任务永远挂起。

# src/lra/hitl/gate.pyclass AutoPolicyGate:    """Resolves gates by policy alone (no human)."""    async def request(self, req: GateRequest) -> GateResolution:        if req.risk is RiskTier.REVERSIBLE and self._auto_approve_reversible:            return GateResolution(gate_id=req.gate_id, decision=GateDecision.APPROVE,                                  resolved_by="auto-policy")        # Irreversible and unattended: apply the request's safe default, and mark it defaulted.        return GateResolution(gate_id=req.gate_id, decision=req.default_action,                              resolved_by="default", defaulted=True)

这个 gate 的 durable 版本位于 workflow 本身中,使用 signal。当需要人类时,workflow 将其 status 设为 WAITING_ON_HUMAN,并停在 condition 上,直到有人 signal 一个 decision 或 timeout 触发。下面是一个代表性的 status query。

#### OUTPUT ####$ uv run lra mission-status mission_4c7e21a9status=WAITING_ON_HUMAN cycles=190$ uv run lra mission-approve mission_4c7e21a9 --decision approvesent decision 'approve' to mission mission_4c7e21a9

健康的 parked task 显示 SLEEPING,但 WAITING_ON_HUMAN 意味着一个 gate 打开了,需要有人行动。Approve command 会 signal workflow,它会立即恢复。如果 timeout 前无人回应,default action 会触发,而对不可逆 action 来说,默认是不继续。因此 unattended run 默认安全。它永远不会悄悄做危险事情,也不会不可见地永远挂起。

能跨越数周的 Memory

我们已经有了一个能运行一周并保持安全的系统。但“存活一周”和“在一周内学习”是两回事。Saved state 让 agent 完美记住当前任务。Memory 则提供更多东西:跨 cycles,最终跨 tasks 携带经验的能力。这就是系统不再只是执行,而开始改进的地方。

三层 Memory

我们把 memory 分成三层,借用了人们讨论记忆的方式。Episodic memory 是发生过什么,也就是原始 event log。Semantic memory 是关于代码和 domain 的 distilled facts。Procedural memory 是可复用 skills,也就是经过验证的真实代码,以及它适用的条件。

# src/lra/contracts/memory.pyclass MemoryRecord(BaseModel):    id: str    kind: str           # "episodic" | "semantic" | "procedural"    text: str    metadata: dict[str, str] = {}    embedding_model: str | None = None    embedding_version: str | None = None    valid: bool = True  # soft-invalidation; we never hard-delete a memory@runtime_checkableclass SemanticIndex(Protocol):    """Stores records and retrieves them by semantic similarity."""    async def add(self, records: list[MemoryRecord]) -> None: ...    async def query(self, text: str, *, k: int = 5) -> list["RetrievalHit"]: ...

注意 valid flag、embedding_model 和 embedding_version 字段。valid flag 意味着我们从不直接删除 memory,只会将其标记为 invalid,稍后会回到这一点。Model 和 version 字段用于避免一个微妙的长运行 bug:比较由不同 embedding models 生成的 vectors。数周内,你可能更换 embedder,而静默混合来自两个模型的 vectors 会给你垃圾 retrieval。

Embeddings

为了按语义搜索 memory,我们把文本转换成 vectors。Embedder 当然也是一个可替换接口。有一个真实版本会调用付费 semantic API,还有一个 deterministic offline 版本,零成本,用于测试和本地运行。

# src/lra/memory/embeddings.pyclass HashEmbedder:    """A deterministic, offline embedder (lexical hashing). For dev/CI/offline only."""    def __init__(self, dim: int = 256) -> None:        self.name, self.version, self.dim = "hash", "1", dim    async def embed(self, texts: list[str]) -> list[list[float]]:        return [self._embed_one(text) for text in texts]    def _embed_one(self, text: str) -> list[float]:        vec = [0.0] * self.dim        for token in _TOKEN.findall(text.lower()):            bucket = int(hashlib.sha1(token.encode("utf-8")).hexdigest(), 16) % self.dim            vec[bucket] += 1.0                       # hash each token into a bucket        norm = math.sqrt(sum(v * v for v in vec)) or 1.0        return [v / norm for v in vec]               # normalize to unit length

Hash embedder 不是 semantic 的,它只是 lexical hashing,但它 deterministic 且免费,因此整个 memory subsystem 可以在无网络、无 key 的情况下运行和测试。生产中你可以在同一接口背后替换成真实 Voyage embedder。重点一如既往:系统其他部分无需改变。

Hybrid Retrieval

纯 vector search 会错过精确 keyword matches,而纯 keyword search 会错过语义。因此我们两者都运行,并融合 rankings。我们将 dense semantic search 与 lexical BM25 search 结合,使用 reciprocal rank fusion,它能融合两个 ranked lists,而不需要它们的 scores 在同一尺度上。

# src/lra/memory/hybrid.pydef reciprocal_rank_fusion(rankings: list[list[str]], *, k: int = 60) -> list[tuple[str, float]]:    """Fuse ranked id-lists via RRF: score = sum of 1/(k + rank)."""    fused: dict[str, float] = {}    for ranking in rankings:        for rank, doc_id in enumerate(ranking, start=1):            fused[doc_id] = fused.get(doc_id, 0.0) + 1.0 / (k + rank)    return sorted(fused.items(), key=lambda pair: pair[1], reverse=True)class HybridRetriever:    async def query(self, text: str, *, k: int = 5, candidate_k: int = 20) -> list[RetrievalHit]:        dense = [h.record.id for h in await self._semantic.query(text, k=candidate_k)]        lexical = [doc_id for doc_id, _ in self._bm25.query(text, k=candidate_k)]        fused = reciprocal_rank_fusion([dense, lexical])     # blend both rankings        # ... map ids back to records and return the top k ...

这种 fusion 很优雅。每个 list 都贡献一个“1 除以常数加 rank”的 score,因此两个方法都高排名的 item 会浮到顶部,而只被一种方法找到的 item 仍然有公平机会。这是标准的 two stage retrieval recipe,并且完全是 pure Python,因此零成本、完全 deterministic。还有一个可选的 cross encoder reranker 用于最后润色,但 noop reranker 是免费的默认值。

Consolidation

Episodic events 增长很快。我们不想永远保存一万条 raw events。因此在 durable sleep windows 期间,一个 librarian 角色会把最近 episodes consolidation 成少数 durable semantic facts。这是模型反思自己做过的事,并写下值得保留内容的过程。

# src/lra/memory/consolidation.pyasync def consolidate(*, model, episodes: list[str], mission_id=None) -> list[MemoryRecord]:    """Distill `episodes` into semantic MemoryRecord facts (model-driven, safe fallback)."""    if not episodes:        return []    joined = "\n".join(f"- {e}" for e in episodes[-200:])    result = await model.complete([        ModelMessage(role="system", content="You are the Librarian; consolidate memory."),        ModelMessage(role="user", content=f"{_INSTRUCTIONS}\n\nEpisodes:\n{joined}"),    ])    facts = _parse_facts(result.text)    if not facts:                                       # deterministic fallback        facts = list(dict.fromkeys(episodes))[-10:]     # keep recent distinct episodes    return [MemoryRecord(id=new_id("fact"), kind="semantic", text=f, metadata=...) for f in facts]

Forgetting 被刻意设计得保守。我们绝不让模型原地 rewrite 一个 fact,因为这可能把 hallucination 变成 remembered truth。我们只通过 soft invalidate 将 facts 标记为 invalid。下面是 LSP 任务最艰难两天后的 consolidation step。

#### OUTPUT ####07:00:05 INFO  librarian.consolidate window=2d episodes=29 -> lessons=407:00:05 INFO  librarian.lesson L1 "wire-protocol framing is byte-level + stream-mode-sensitive -> isolate & unit-test FIRST"07:00:05 INFO  librarian.lesson L2 "never fix 3 entangled problems in one file; split them"07:00:05 INFO  librarian.lesson L3 "stream.read(n) on a pipe can short-read -> loop until n bytes"

一夜之间,二十九个 raw episodes 变成了四条清晰 lessons。读一下这些 lessons。它们正是任务在困难阶段痛苦学到的东西。第二天早上,当 lead 重新规划时,这些 lessons 已经在 context 中,这就是第二次尝试顺利得多的原因。系统睡觉、反思,并醒来时知道了更多。

Skill Library

最高层是 procedural memory,也就是一个可复用 skills 的真实 library。Skill 是代码加它适用的 preconditions。关键规则是:只有当 skill 被验证过,也就是学习时通过 deterministic test gate,才能被收入 library。

# src/lra/memory/skills.pyclass InMemorySkillStore:    async def add(self, skill: Skill) -> None:        if not skill.verified:            raise SkillNotVerifiedError(                f"refusing to admit unverified skill {skill.id!r} (must pass the test gate first)"            )        self._by_id[skill.id] = skill        await self._index.add([MemoryRecord(            id=skill.id, kind="procedural", text=skill.description,            metadata={"namespace": skill.namespace, "name": skill.name},        )])

Store 直接拒绝接纳 unverified skill。这防止 library 被看似合理的 nonsense 填满。Skill 还携带 provenance 和 expiry,因此 lesson 不会永远僵在那里。观察 LSP trace 中的 skill gate,先拒绝再接纳。

#### OUTPUT ####18:35:10 INFO  skill.admit count=0 reason="nothing passed the gate; the system records no progress it didn't earn"...09:11:02 INFO  skill.confirm name=lsp-framing precond=[python,stdio] proven_by=tests/test_framing.py11:50:02 INFO  skill.admit name=lsp-utf16-offsets proven_by=tests/test_positions_unicode.py

第一行是我在整个运行中最喜欢的一行。经过一整天失败尝试后,零个 skills 被接纳,因为没有任何东西通过 gate。系统不记录任何它没有赢得的进展。随后,一旦 framing 最终通过测试,skill lsp-framing 被确认,并以证明它的 test file 作为证据。后来,UTF-16 fix 也赢得了自己的 skill。Library 只包含被证明可工作的东西。这就是 skill 在下一个任务中安全复用的原因。

让经验变成永久改进

我们刚构建的 memory 有助于单个任务内部。但最有趣的学习发生在 tasks 之间。一个任务结束后,系统可以回顾自己的失败,并改进自身 prompts,让下一个同类任务做得更好。这是 self improvement,而且必须非常谨慎,因为如果天真地让 agent rewrite 自己的 instructions 是危险的。

提出更好的 Prompt

第一步完全离线发生,在 tasks 之间,从不发生在 live run 上。Evolver 读取已完成任务的 failure traces,并为某个角色提出改进后的 system prompt。这是一种 reflective optimization,精神上类似 GEPA。重要的是,输出只是 candidate。还没有任何东西被改变。

# src/lra/agents/evolver.pyclass PromptEvolver:    """Proposes an improved prompt for a role from its failure traces (offline)."""    async def propose(self, *, role, current_prompt, failure_traces) -> PromptCandidate:        traces = "\n".join(f"- {t}" for t in failure_traces[-50:]) or "(no traces)"        result = await self._model.complete([            ModelMessage(role="system", content=(                "You optimize an agent's SYSTEM PROMPT by reflecting on its failures "                "(GEPA-style). Output ONLY the improved prompt text, no preamble.")),            ModelMessage(role="user", content=(                f"Role: {role}\n\nCurrent prompt:\n{current_prompt}\n\n"                f"Failure traces:\n{traces}\n\nReturn an improved prompt.")),        ])        candidate = result.text.strip() or current_prompt        return PromptCandidate(role=role, prompt=candidate,                               rationale="reflective optimization over failure traces (eval-gated)")

Evolver 读取出了什么问题,并写下它认为会更好的内容。对于 LSP 任务,它注意到几天时间浪费在一个困难的 wire protocol 问题上,并且与 over scoping 纠缠在一起,因此提出了一些 standing rules 来避免下次重蹈覆辙。

#### OUTPUT ####22:14:40 INFO  evolve.candidate diff="+ for stdio/wire-protocol: implement+unit-test byte-level framing    in ISOLATION first (incl. a short-read stream + a multibyte body); use binary streams; start    synchronous. + if an unproven core has >~10 dependent items, cut scope until the core is green."

这个 candidate 是痛苦的一周经验,被压缩成 lead engineer 的几条 standing rules。但 candidate 只是 proposal。我们还不信任它。它必须赢得自己的位置。

Eval Harness 与 Judge

为了判断 candidate prompt 是否真的更好,我们让它在 held out 的 gold cases 上运行并评分。Scoring 使用一个独立 agent 作为 judge,并配有 rubric,因为很多标准是 fuzzy 的,不是简单 pass 或 fail。

# src/lra/evals/harness.pyasync def run_eval_suite(*, model: ModelProvider, cases: list[EvalCase]) -> EvalReport:    """Judge each case and aggregate into a report (pass rate + mean score)."""    judge = AgentAsJudge(model)    results = []    for case in cases:        verdict = await judge.judge(candidate=case.candidate, rubric=case.rubric)        results.append(EvalCaseResult(name=case.name, passed=verdict.passed,                                      score=verdict.score, rationale=verdict.rationale))    return EvalReport(results=results)

Judge 被刻意设计成独立于它所评判的 agents,并且对 unparseable output 默认 fail,因此不会悄悄通过某些东西。下面是 eval 对比旧 prompt 与 evolved prompt 的结果。

#### OUTPUT ####22:14:55 INFO  eval.run set=wire-protocol-gold n=20 baseline=0.71 candidate=0.8522:15:30 INFO  judge.verdict model=claude-opus-4-8 promote=true delta=+0.14 regressions=0

在二十个 gold cases 上,baseline prompt 得分 0.71,evolved prompt 得分 0.85。Judge 建议 promotion,提升 0.14 且 zero regressions。现在 candidate 背后有了证据。但我们仍然不会直接 ship 它。

Promotion Gate

最终 gate 最严格。Candidate 只有在 evals 上击败 baseline,并且通过 recorded task histories 的 replay,证明它不破坏 determinism 后,才会被 promotion;即便如此,它也会以 pull request 形式发出,需要人类批准。Live agent 的 prompt 永远不会被 hot patch。

# src/lra/agents/evolution.pyasync def evolve_and_gate(*, model, role, current_prompt, failure_traces,                          baseline_cases, candidate_cases, min_improvement=0.0) -> EvolutionResult:    """Propose an improved prompt and promote it ONLY if it beats baseline by min_improvement."""    candidate = await PromptEvolver(model).propose(        role=role, current_prompt=current_prompt, failure_traces=failure_traces)    baseline = await run_eval_suite(model=model, cases=baseline_cases)    evolved = await run_eval_suite(model=model, cases=candidate_cases)    promoted = evolved.pass_rate >= baseline.pass_rate + min_improvement    return EvolutionResult(promoted=promoted, candidate=candidate,                           baseline_pass_rate=baseline.pass_rate,                           candidate_pass_rate=evolved.pass_rate,                           reason=("candidate improved" if promoted else "rejected"))

Gate 是一条 independent checks 链:evals、judge 和 replay。下面是 trace 中最终 promotion step。

#### OUTPUT ####22:15:31 INFO  replay.histories n=9 result=9/9 clean22:15:31 INFO  evolve.promote target=prompts/lead.md mode=PR human_approval=required (never hot-patched a live run)

九条 recorded histories 在新 prompt 下 clean replay,只有这时变更才被 promotion,作为 pull request,并要求 human approval。那一行末尾的短语就是 self improvement 的完整安全哲学。系统永远不会 hot patch live run。它提出、证明、请求,然后改进才会落地。下一个 wire protocol 任务会跳过大部分多天浪费工作,因为经验已经被烘进 lead 的 standing instructions 中。这个 loop 值回了票价。

一个任务的一周生命周期

我们已经构建了每个组件。现在让我们看它们如何在完整一周中协同工作,任务就是我一直引用的那个:从零构建一个小型 Python language server。Language server 通过 standard input 和 output 使用特定 wire protocol 通信,而这会变成一个出乎意料棘手的小问题。本节是整个运行,从开始到结束,这样你可以看到我们构建的组件在真实压力下实际如何表现。

Day Zero:研究与计划

任务从一个命令开始。Workflow 启动,saved state 初始化,governor 读取它的 ceiling。然后,lead 没有猜测协议,而是聪明地把第一步花在 research 上。

#### OUTPUT ####$ uv run lra mission-start --title pylsp-mini --workdir .lra/workspaces/pylsp-mini --task @mission.txt09:00:01 INFO  workflow.started wf=mission:mission_4c7e21a9 task_queue=lra-mission09:00:01 INFO  governor.config ceiling_usd=400.00 max_cycles=2000 stall_limit=509:00:02 DEBUG planner.thinking "I do not have the LSP wire format memorised. Guessing will burn cycles.               Spend on READ-research first: base protocol framing, a real server's transport, exact JSON."09:47:55 INFO  checklist.write items=31  (01 transport, 02 init, 03 hover, 04 completion, ...)

Plan 生成了三十一个 items,这太多了。在 transport 甚至还没工作之前规划整个 server,就是我们前面指出的 over scope,也是后续浪费工作的种子。但系统当时还不知道。它 commit plan 并开始工作。这很现实。优秀工程师也会 over plan。关键是系统能否从中恢复,而它可以。

困难部分

Transport 是第一个 item,而且非常棘手。Lead 写了一个 transport,运行测试,失败。再试一次,又以不同方式失败。整整一天里它尝试了十二次,每次修复上一个症状,又暴露一个新症状。没有 header 的 bare JSON,然后是 Windows newline translation 导致的两字节 mismatch,然后是 pipe 上的 short read,然后是 asyncio deadlock。

#### OUTPUT ####09:20:38 verify.fail attempt=3 reason=byte-mismatch delta=+2  # Windows \n -> \r\n on text-mode stdout10:11:40 verify.fail attempt=6 reason=JSONDecodeError "Unterminated string"  # pipe short-read11:11:02 verify.fail attempt=10 reason=Timeout >12s  # asyncio executor sometimes never returns17:48:02 WARN  loop_detector signature="01:transport:edit" count=3 action=warn

这些失败中的每一个都是真实 bug,一个人类在 Windows 上写 language server transport 时也会碰到。重要的是最后一行。Loop detector 已经开始计数。系统注意到同一个 item 正以同样方式反复失败。它尚未干预,但它在观察,而这个观察马上会变得重要。

Crash 与 Replay

第三天,transport 仍然 red,主机机器因为 OS update 重启,正好发生在 cycle 中间。这是会杀死普通 agent 的灾难。在这里,它是非事件。

#### OUTPUT ####13:48:12 ERROR worker.disconnect reason="connection reset (host OS update reboot)" inflight_cycle=c19013:55:40 WARN  temporal.replay events=631 (completed activities served from cache; 0 tokens re-spent; 0 double commits)13:55:41 INFO  reconcile.in_flight ticket=01 branch=feat/transport -> adopt (branch present, unmerged)13:55:42 DEBUG orchestrator.note "replay restored state EXACTLY -> a still-RED transport."

Worker 死了,一个新的起来了。Temporal replay 了 journal,从 cache 提供已完成工作,任务从精确 cycle 继续。没有 token 重花,也没有 duplicate commits。注意,它恢复的是仍然 failing 的 transport,因为那就是真实状态。Durable backbone 保护工作,但不假装 bug 已修复。Crash 前任务卡住,crash 后仍然卡住,这完全正确。

摆脱卡住状态

最终打破 deadlock 的不是一次幸运 edit。而是 loop detector trip,进而 escalate 给 reviewer,reviewer 给出我们之前看到的 architecture finding。夜间,librarian 将失败 consolidation 成 lessons。第二天早上,planner 带着这些 lessons 重新规划,缩小 scope,lead 将 transport 重建为 isolated、tested unit。

#### OUTPUT ####07:00:06 INFO  reflection.write item=01 "the 12 attempts were the same 3 bugs reshuffled. stop editing               transport.py in place; build framing as a tested unit, sync, binary."07:31:02 INFO  state.migrate checklist v1->v2 items=14 (dropped 06-09, plus 4 sub-items)09:10:21 INFO  verify.ok cycle=c201 checks=[pytest]   <- FIRST GREEN (cycle 201, ~2.5 days in)

读一下这个序列。Reflection 指出了真正问题:十二次尝试只是同三个 bug 的重新排列。Checklist 从三十一个 items 迁移到十四个。在 cycle 201,大约两天半后,transport 终于通过了测试。这是整个任务的第一个 green。此前的一切都是 attempts,此后的一切建立在坚实基础上。这次恢复来自系统自己的 machinery:loop detector、reviewer、librarian 和 re-planner 都在各司其职。

Fan-Out 与最终 Gate

一旦 core green,剩下的部分就很快。Features 彼此独立,因此它们 fan out 给 parallel implementers。Day zero research 标记的一个旧 hazard,UTF-16 column encoding,终于咬人并被修复。然后整个系统通过最终 gate。

#### OUTPUT ####07:30:03 INFO  child.spawn implementer:0 ticket={feature:hover, write_set:[features/hover.py, tests/test_hover.py]}07:30:03 INFO  child.spawn implementer:1 ticket={feature:completion, write_set:[features/completion.py, ...]}08:58:12 INFO  child.done implementer-0 branch=feat/hover verify=ok skill_reused=lsp-framing09:31:45 INFO  reviewer.finding sev=blocking "HAZARD-1 from day0: LSP 'character' = UTF-16 CODE UNITS;               you used Python str indices. convert at the boundary."08:43:02 INFO  run_command cmd="make check" -> exit=0   "ruff: clean; mypy: clean; pytest: 58 passed"08:44:51 INFO  mission.complete items=14/14 cycles=615 head=9d8c7b6 status=DONE

这些行里有很多值得喜欢的地方。Implementers 在 disjoint files 上并行运行,其中一个复用了困难阶段学到的 lsp-framing skill,因此无需重新发现 transport。Reviewer 抓住了从 day zero 就潜伏着的 UTF-16 hazard。最终 gate make check 干净通过,五十八个 tests green。任务在六百一十五个 cycles 中完成了全部十四个 items。这就是我们开篇展示的那一行,现在其中每个词都应该有意义了。

哪些已被证明,哪些仍是 Frontier

有必要清楚说明这个系统是什么、不是什么,因为 agent demo 很容易被过度包装。下面是清晰区分。

以下部分已被证明并测试过。它们是工程,今天就能工作。

  • Durability 和 crash recovery。Mid run reboot 不会丢失工作,也不会重复花费 tokens。

  • State 位于模型之外。它保存在 git 中,因此任何 restart 都能在几秒内重建全局图景。

  • Deterministic verification。真实 tests,而不是模型意见,gate 每一段进展。

  • Safety boundaries。Tool gate、default deny egress、budget governor 和 loop detector 都在代码中强制执行。

  • Tiered memory 和 skill reuse,并且 skills 只有通过 gate 后才被接纳。

下面是 frontier,也就是我不会过度声称的部分。完全无人值守、多周自主性,即模型能可靠驱动复杂工作数天而无需任何人类介入,这还不是一个已解决问题,而且今天没有任何模型能可靠做到。我们能高可靠信任的 task horizon 仍然远低于一天。

因此,这个系统的主张是精确的。系统可以运行数周。它会睡眠、存活、恢复,并且永远不会忘记自己在做什么,因为 durability 和 verification 是系统的工程属性,而不是我们寄希望于模型拥有的能力。模型以短而已验证的 bursts 驱动工作,不可逆 action 由 human gates 把关。这就是这篇博客标题的含义。持久性是一种工程属性,而不是模型能力。我们没有让模型变得更聪明。我们构建了一个系统,让今天的模型可以完成一周的谨慎工作,而不至于崩溃。

下一步

如果你想在此基础上构建,我会建议从小处开始,并让每一层都自己赢得位置。先让 git saved state 和 single cycle loop 跑起来,把 deterministic verifier 作为 gate,并证明 mid run crash 不会丢失任何东西。这个 durable single agent backbone 是困难且有价值的核心,其他一切都是附加层,只有在可证明有帮助时才值得加入。当 author bias 开始让你付出代价时,再加入 reviewer。当阅读成为瓶颈时,再加入 researchers。当你反复运行同类任务时,再加入 memory 和 self improvement。

完整代码,包括 Temporal workflows、agent team、memory tiers 和 offline evolution loop,都在 GitHub 上:

https://github.com/FareedKhan-dev/long-running-agent

感谢你读到这里。我们从一个简单思想——假设一定会被中断——出发,把它发展成一个能在真实软件任务上工作一周、经受 crashes 和 reboots、保持预算、不越过安全边界,甚至从自身失败中学习的系统。模型以短 bursts 的方式完成思考。工程完成了存活。

基本 文件 流程 错误 SQL 调试
  1. 请求信息 : 2026-07-23 16:48:04 HTTP/1.1 GET : https://www.yeyulingfeng.com/a/768404.html
  2. 运行时间 : 0.273479s [ 吞吐率:3.66req/s ] 内存消耗:4,885.88kb 文件加载:145
  3. 缓存信息 : 0 reads,0 writes
  4. 会话信息 : SESSION_ID=998419f5a48d83ca1d7fcdb84227640a
  1. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/public/index.php ( 0.79 KB )
  2. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/autoload.php ( 0.17 KB )
  3. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/composer/autoload_real.php ( 2.49 KB )
  4. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/composer/platform_check.php ( 0.90 KB )
  5. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/composer/ClassLoader.php ( 14.03 KB )
  6. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/composer/autoload_static.php ( 6.05 KB )
  7. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/helper.php ( 8.34 KB )
  8. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-validate/src/helper.php ( 2.19 KB )
  9. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/ralouphie/getallheaders/src/getallheaders.php ( 1.60 KB )
  10. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/helper.php ( 1.47 KB )
  11. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/stubs/load_stubs.php ( 0.16 KB )
  12. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Exception.php ( 1.69 KB )
  13. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-container/src/Facade.php ( 2.71 KB )
  14. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/deprecation-contracts/function.php ( 0.99 KB )
  15. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/polyfill-mbstring/bootstrap.php ( 8.26 KB )
  16. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/polyfill-mbstring/bootstrap80.php ( 9.78 KB )
  17. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/var-dumper/Resources/functions/dump.php ( 1.49 KB )
  18. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-dumper/src/helper.php ( 0.18 KB )
  19. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/var-dumper/VarDumper.php ( 4.30 KB )
  20. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/guzzlehttp/guzzle/src/functions_include.php ( 0.16 KB )
  21. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/guzzlehttp/guzzle/src/functions.php ( 5.54 KB )
  22. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/App.php ( 15.30 KB )
  23. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-container/src/Container.php ( 15.76 KB )
  24. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/psr/container/src/ContainerInterface.php ( 1.02 KB )
  25. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/provider.php ( 0.19 KB )
  26. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Http.php ( 6.04 KB )
  27. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/helper/Str.php ( 7.29 KB )
  28. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Env.php ( 4.68 KB )
  29. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/common.php ( 0.03 KB )
  30. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/helper.php ( 18.78 KB )
  31. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Config.php ( 5.54 KB )
  32. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/alipay.php ( 3.59 KB )
  33. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/facade/Env.php ( 1.67 KB )
  34. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/app.php ( 0.95 KB )
  35. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/cache.php ( 0.78 KB )
  36. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/console.php ( 0.23 KB )
  37. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/cookie.php ( 0.56 KB )
  38. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/database.php ( 2.48 KB )
  39. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/filesystem.php ( 0.61 KB )
  40. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/lang.php ( 0.91 KB )
  41. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/log.php ( 1.35 KB )
  42. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/middleware.php ( 0.19 KB )
  43. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/route.php ( 1.89 KB )
  44. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/session.php ( 0.57 KB )
  45. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/trace.php ( 0.34 KB )
  46. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/view.php ( 0.82 KB )
  47. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/event.php ( 0.25 KB )
  48. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Event.php ( 7.67 KB )
  49. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/service.php ( 0.13 KB )
  50. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/AppService.php ( 0.26 KB )
  51. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Service.php ( 1.64 KB )
  52. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Lang.php ( 7.35 KB )
  53. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/lang/zh-cn.php ( 13.70 KB )
  54. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/initializer/Error.php ( 3.31 KB )
  55. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/initializer/RegisterService.php ( 1.33 KB )
  56. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/services.php ( 0.14 KB )
  57. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/service/PaginatorService.php ( 1.52 KB )
  58. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/service/ValidateService.php ( 0.99 KB )
  59. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/service/ModelService.php ( 2.04 KB )
  60. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-trace/src/Service.php ( 0.77 KB )
  61. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Middleware.php ( 6.72 KB )
  62. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/initializer/BootService.php ( 0.77 KB )
  63. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/Paginator.php ( 11.86 KB )
  64. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-validate/src/Validate.php ( 63.20 KB )
  65. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/Model.php ( 23.55 KB )
  66. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/Attribute.php ( 21.05 KB )
  67. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/AutoWriteData.php ( 4.21 KB )
  68. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/Conversion.php ( 6.44 KB )
  69. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/DbConnect.php ( 5.16 KB )
  70. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/ModelEvent.php ( 2.33 KB )
  71. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/RelationShip.php ( 28.29 KB )
  72. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/contract/Arrayable.php ( 0.09 KB )
  73. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/contract/Jsonable.php ( 0.13 KB )
  74. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/contract/Modelable.php ( 0.09 KB )
  75. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Db.php ( 2.88 KB )
  76. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/DbManager.php ( 8.52 KB )
  77. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Log.php ( 6.28 KB )
  78. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Manager.php ( 3.92 KB )
  79. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/psr/log/src/LoggerTrait.php ( 2.69 KB )
  80. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/psr/log/src/LoggerInterface.php ( 2.71 KB )
  81. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Cache.php ( 4.92 KB )
  82. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/psr/simple-cache/src/CacheInterface.php ( 4.71 KB )
  83. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/helper/Arr.php ( 16.63 KB )
  84. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/cache/driver/File.php ( 7.84 KB )
  85. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/cache/Driver.php ( 9.03 KB )
  86. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/contract/CacheHandlerInterface.php ( 1.99 KB )
  87. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/Request.php ( 0.09 KB )
  88. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Request.php ( 55.78 KB )
  89. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/middleware.php ( 0.25 KB )
  90. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Pipeline.php ( 2.61 KB )
  91. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-trace/src/TraceDebug.php ( 3.40 KB )
  92. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/middleware/SessionInit.php ( 1.94 KB )
  93. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Session.php ( 1.80 KB )
  94. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/session/driver/File.php ( 6.27 KB )
  95. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/contract/SessionHandlerInterface.php ( 0.87 KB )
  96. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/session/Store.php ( 7.12 KB )
  97. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Route.php ( 23.73 KB )
  98. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/RuleName.php ( 5.75 KB )
  99. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/Domain.php ( 2.53 KB )
  100. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/RuleGroup.php ( 22.43 KB )
  101. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/Rule.php ( 26.95 KB )
  102. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/RuleItem.php ( 9.78 KB )
  103. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/route/app.php ( 3.94 KB )
  104. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/facade/Route.php ( 4.70 KB )
  105. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/dispatch/Controller.php ( 4.74 KB )
  106. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/Dispatch.php ( 10.44 KB )
  107. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/controller/Index.php ( 9.87 KB )
  108. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/BaseController.php ( 2.05 KB )
  109. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/facade/Db.php ( 0.93 KB )
  110. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/connector/Mysql.php ( 5.44 KB )
  111. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/PDOConnection.php ( 52.47 KB )
  112. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/Connection.php ( 8.39 KB )
  113. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/ConnectionInterface.php ( 4.57 KB )
  114. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/builder/Mysql.php ( 16.58 KB )
  115. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/Builder.php ( 24.06 KB )
  116. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/BaseBuilder.php ( 27.50 KB )
  117. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/Query.php ( 15.71 KB )
  118. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/BaseQuery.php ( 45.13 KB )
  119. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/TimeFieldQuery.php ( 7.43 KB )
  120. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/AggregateQuery.php ( 3.26 KB )
  121. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php ( 20.07 KB )
  122. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/ParamsBind.php ( 3.66 KB )
  123. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/ResultOperation.php ( 7.01 KB )
  124. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/WhereQuery.php ( 19.37 KB )
  125. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/JoinAndViewQuery.php ( 7.11 KB )
  126. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/TableFieldInfo.php ( 2.63 KB )
  127. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/Transaction.php ( 2.77 KB )
  128. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/log/driver/File.php ( 5.96 KB )
  129. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/contract/LogHandlerInterface.php ( 0.86 KB )
  130. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/log/Channel.php ( 3.89 KB )
  131. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/event/LogRecord.php ( 1.02 KB )
  132. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/Collection.php ( 16.47 KB )
  133. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/facade/View.php ( 1.70 KB )
  134. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/View.php ( 4.39 KB )
  135. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/controller/Es.php ( 3.30 KB )
  136. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Response.php ( 8.81 KB )
  137. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/response/View.php ( 3.29 KB )
  138. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Cookie.php ( 6.06 KB )
  139. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-view/src/Think.php ( 8.38 KB )
  140. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/contract/TemplateHandlerInterface.php ( 1.60 KB )
  141. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-template/src/Template.php ( 46.61 KB )
  142. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-template/src/template/driver/File.php ( 2.41 KB )
  143. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-template/src/template/contract/DriverInterface.php ( 0.86 KB )
  144. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/runtime/temp/c935550e3e8a3a4c27dd94e439343fdf.php ( 31.50 KB )
  145. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-trace/src/Html.php ( 4.42 KB )
  1. CONNECT:[ UseTime:0.001171s ] mysql:host=127.0.0.1;port=3306;dbname=wenku;charset=utf8mb4
  2. SHOW FULL COLUMNS FROM `fenlei` [ RunTime:0.001708s ]
  3. SELECT * FROM `fenlei` WHERE `fid` = 0 [ RunTime:0.000756s ]
  4. SELECT * FROM `fenlei` WHERE `fid` = 63 [ RunTime:0.000754s ]
  5. SHOW FULL COLUMNS FROM `set` [ RunTime:0.001573s ]
  6. SELECT * FROM `set` [ RunTime:0.000657s ]
  7. SHOW FULL COLUMNS FROM `article` [ RunTime:0.001618s ]
  8. SELECT * FROM `article` WHERE `id` = 768404 LIMIT 1 [ RunTime:0.001538s ]
  9. UPDATE `article` SET `lasttime` = 1784796485 WHERE `id` = 768404 [ RunTime:0.010103s ]
  10. SELECT * FROM `fenlei` WHERE `id` = 64 LIMIT 1 [ RunTime:0.001074s ]
  11. SELECT * FROM `article` WHERE `id` < 768404 ORDER BY `id` DESC LIMIT 1 [ RunTime:0.001928s ]
  12. SELECT * FROM `article` WHERE `id` > 768404 ORDER BY `id` ASC LIMIT 1 [ RunTime:0.001420s ]
  13. SELECT * FROM `article` WHERE `id` < 768404 ORDER BY `id` DESC LIMIT 10 [ RunTime:0.002891s ]
  14. SELECT * FROM `article` WHERE `id` < 768404 ORDER BY `id` DESC LIMIT 10,10 [ RunTime:0.005167s ]
  15. SELECT * FROM `article` WHERE `id` < 768404 ORDER BY `id` DESC LIMIT 20,10 [ RunTime:0.012827s ]
0.279353s