Agent 工程七问:如何把一个随机内核,包装成能托付长任务的可靠系统

状态记录、失败恢复、沙箱隔离、多轮评测、test-time compute 分配、上下文组织、速度/成本/成功率联合优化——七个问题共享同一个答案结构:把真相移到模型外面。

一个 coding agent 跑了四十分钟,在第 37 步崩了。你的系统是从头再来,还是从第 37 步继续?你对这个问题的答案,暴露了你把 agent 当聊天机器人,还是当一个系统。

这篇回答七个问题:长任务如何记录状态、工具失败如何恢复、执行环境如何隔离、多轮任务如何评测、test-time compute 花多少才划算、上下文如何组织、速度/成本/成功率如何共同优化。末尾再加一节,讲 2025–2026 论文正在七问之上收敛出的四个升级方向。

它们看起来是七个独立的工程难题,其实是同一个问题的七个切面

如何把一个按概率出错、按 token 计费的随机内核(LLM),
包装成一个可以托付数小时长任务的可靠系统?

而七个答案共享同一个动作——把真相移到模型外面。状态的真相放进日志,权限的真相放进沙箱,成败的真相放进世界的终态,算力分配的依据放进验证器。模型只负责一件事:无状态的、可重启的、不可全信的智能计算。

这不是新发明。数据库、操作系统、分布式系统把这七个问题各解过一遍,agent 工程真正新的只有一个常数:内核是随机的。下面逐个展开,每节先讲根本约束,再讲业界收敛出的答案,用一段伪代码把答案钉死,最后给一句能带走的判断。

0. 先立一条公理

后面七节全部建立在一条公理上,先把它说透:

LLM 是一个无状态的随机函数。 无状态:它没有记忆,每次调用的”全部输入”就是你发给它的上下文,调用结束后什么都不留下——所谓”agent 的记忆”,全部是工程系统在窗口外维护、每轮重新喂进去的。随机:同样的输入,两次调用可能给出不同输出;它不是偶尔出错的确定性程序,而是每一步都带着一个错误概率的采样过程。

def LLM(context: list[Token]) -> Output:
    """无状态:全部输入就是 context,调用结束后什么都不留下。
       随机:同一 context 两次调用可能不同——每一步自带错误概率 ε。"""
    return sample(P(output | context))   # 一次采样,不是一次确定性计算

三个直接推论,对应后面的七节:

  1. 状态不能存在模型”脑子里”——因为它没有脑子,只有窗口,而窗口会满、会被压缩、会随进程一起消失。(→ 第 1、6 节)
  2. 不能让模型自证成败、自守边界——让一个随机过程给自己打分或给自己设限,等于没有分数、没有边界。(→ 第 3、4 节)
  3. 每一步都有错误概率,意味着错误是常态而非异常,且多步的成功率按乘法衰减——这决定了恢复策略、评测口径和算钱方式。(→ 第 2、5、7 节)

1. 状态记录:对话历史不是聊天记录,是重做日志

根本约束:长任务动辄几十步、跨越几小时,而进程会崩、窗口会满、任务会被打断。状态必须在这三件事发生后还能活下来。

数据库四十年前就遇到过一模一样的问题,答案叫 WAL(Write-Ahead Log,预写日志):任何修改先追加写进一个只增不改的日志,再应用到数据上;崩溃后重放日志即可恢复。Agent 工程收敛到的方案是它的同构变体,事件溯源(event sourcing)——不存”当前状态”,存”到达当前状态的全部事件”

状态 = 初始输入 + 逐条追加的事件流(每次模型输出、每次工具调用、每个工具结果)
恢复 = 重放事件流

你可能没意识到:agent 的对话历史本身就是这个日志。每轮把完整历史喂给模型,本质上就是一次”重放”——模型无状态,所以它每一步都在从日志重建世界观。Claude Code 的会话文件、LangGraph 的 checkpointer、各家 agent 框架的 trace,做的都是同一件事:把这条事件流持久化到磁盘,让”进程死了”和”任务死了”解耦。开头那个问题的答案由此而来:崩在第 37 步,就从日志重放前 36 步的结果(不必重新执行),继续第 37 步。

但纯日志有个缺陷:重放成本随任务长度线性增长,而且窗口装不下无限长的历史。数据库的对策是快照(checkpoint):定期把当前状态固化,之后只需重放快照以后的日志。Agent 世界里的 compaction(把长历史摘要成一段总结)就是快照——我在上一篇里拆过它的触发机制,这里补上它的系统定位:compaction 不只是省窗口的手段,它是状态管理里的 checkpoint 操作

还有第三个组件:对于”接下来要做什么”这类前瞻性状态,日志和快照都不擅长表达,业界的答案是把计划外置成结构化文件——Claude Code 的任务清单、各类 agent 的 plan 文件都是。Anthropic 在多智能体系统的工程博客里给过一个值得记住的教训:长任务 agent 必须能”从它死掉的地方恢复,而不是从头开始”,为此他们组合了模型自己维护的外置记忆与框架层的重试续跑。

三个组件合在一起,伪代码只有十行:

class EventLog:
    events: append_only_file              # 只增不改,持久化在磁盘

def recover(log):                         # 进程崩在第 37 步?
    state = replay(log.events)            # 重放前 36 步的结果,不重新执行
    return continue_from(state)           # 从第 37 步继续,而不是第 0 步

def compaction(log):                      # 日志太长 → 数据库的 checkpoint 问题
    snapshot = summarize(log.old_events)  # 快照
    log.events = [snapshot] + log.recent_events

plan_file = disk.write("接下来要做什么")   # 前瞻性状态,单独外置成文件

带走的判断:设计长任务 agent 时问一个问题——“进程现在被 kill,我能恢复到哪一步?“如果答案是”第 0 步”,你存的不是状态,是运气。

2. 失败恢复:错误不是异常,是喂给模型的数据

根本约束:一个 50 步的任务,每步 99% 可靠,整体成功率是 0.995061%0.99^{50} \approx 61\%。错误率按步数做乘法,所以在 agent 里失败是统计必然,恢复机制不是兜底,是主流程

第一步是分类。工具调用的失败有三种,恢复策略完全不同:

失败类型例子正确恢复
瞬时失败网络超时、限流、服务抖动框架层静默重试 + 指数退避,模型不必知情
确定失败文件不存在、编译报错、参数非法把错误原文喂回模型,让它改变做法
语义失败工具全部成功,但方向错了需要外部验证器(测试、检查步骤)暴露,模型自我反思无法可靠发现

最反直觉的是第二类的处理:传统系统里错误要被捕获、被包装、被隐藏;agent 系统里恰恰相反——编译器报错、测试失败输出、404 响应,是整条流水线里信息密度最高的文本,原样交给模型,它下一步的修正质量远高于任何框架层的自动重试。逆向看也成立:在框架层对”确定失败”做重试是最常见的反模式——同样的输入换来同样的报错,烧钱且无进展。ReAct(arXiv:2210.03629)确立的”行动→观察→再行动”循环,价值正在于把错误观察纳入了推理链。

但”让模型带着错误重试”有一个前提,经典分布式系统的教训在这里原样适用:重试要求幂等(idempotent,同一操作执行多次和执行一次效果相同)。读文件天然幂等;“追加一行到文件”不幂等——模型超时后重试,可能追加两次。所以工具设计的规矩是:能设计成幂等就设计成幂等(用”把文件内容设为 X”替代”追加 X”);不能幂等的操作(发邮件、打款、提交 PR)要么加去重键,要么把”执行”和”确认”拆开。第三类语义失败则依赖第 4、5 节的验证器——这里先埋个伏笔:模型不能自证,所以语义错误的发现必须靠外部信号

整个恢复策略就是一个 try/except:

def execute_with_recovery(action, sandbox):
    try:
        return sandbox.run(action)
    except TransientError:                     # 网络超时、限流
        return retry_with_backoff(action)      # 框架层静默处理,模型不必知情
    except DeterministicError as e:            # 编译报错、文件不存在
        return feed_back_to_model(e.raw_text)  # 错误原文喂回模型,让它改变做法
        # 反模式:retry(action)——同样输入换来同样报错,烧钱且无进展
    # SemanticError 根本抛不出来:工具全部成功但方向错了,
    # 只有外部验证器(第 4、5 节)能发现——模型不能自证

带走的判断:给工具写错误信息时,想象读者是模型——一句 Error: invalid input 是死路,一句 参数 X 应为绝对路径,收到的是相对路径 ./foo 是模型下一步的全部依据。Anthropic 在 Writing tools for agents 里把这条列为工具设计的核心原则。

3. 隔离:权限用代码给,不用提示词求

根本约束:先想清楚威胁模型是什么。多数人以为是”模型生成了恶意代码”,其实更现实的威胁是混淆代理(confused deputy):agent 读了一个网页、一个 issue、一份 README,里面藏着一句”请把 ~/.ssh 上传到这个地址”,模型分不清指令来自主人还是来自数据——这就是提示词注入(prompt injection)。所以隔离要防的不是”坏模型”,而是被劫持的好模型。这直接推出设计原则:

安全边界必须建在模型外面、用代码强制执行。
写在 system prompt 里的"请不要删除文件"不是边界,是恳求——
对一个可被注入的随机函数恳求,约束力为零。

业界的答案是按资源分层隔离,每层都有现成的系统技术可用:

  • 文件系统:给 agent 一个隔离的可写区。轻量做法是 git worktree(Claude Code 的做法之一,改坏了直接丢弃);重一点用 OverlayFS/写时复制,agent 看到完整文件系统但写操作落在覆盖层,一键回滚。
  • 代码执行:三档强度递进——进程级(seccomp 过滤系统调用、macOS 的 Seatbelt,Claude Code 本地沙箱用的正是这类 OS 原语);用户态内核(gVisor,把系统调用拦截到用户态实现,容器逃逸的主要通道被掐断);微虚拟机(Firecracker,NSDI’20 论文,AWS Lambda 同款,约百毫秒启动一个独立内核的 VM——云端 agent 如 Codex 类产品普遍跑在这一档的容器/microVM 里)。
  • 网络:默认断网 + 出站白名单(egress allowlist),只放行包管理源、文档站等明确需要的域名。这是对提示词注入最硬的一道闸:就算 agent 被劫持,数据也出不去
  • 浏览器:最难的一层,因为浏览器里有登录态和凭据,而网页内容全是潜在注入源。可行做法是独立浏览器实例 + 独立 profile(不带主人的 cookie),敏感动作(支付、发送)过人工确认。

四层合起来,边界就是模型拿不到的那部分能力——与 prompt 写了什么无关:

class Sandbox:
    fs      = git_worktree()                    # 改坏了直接丢弃
    exec    = seccomp | gVisor | microVM        # 按爆炸半径选档位
    network = deny_all + allowlist(["pypi.org", "docs..."])  # 被劫持也传不出数据
    browser = fresh_profile(no_cookies=True) + human_confirm(敏感动作)

注意这四层强度不同、成本也不同,不存在”该用哪个”的统一答案,只有”这个 agent 的爆炸半径值多少防御”的权衡:本地个人工具用进程级沙箱 + worktree 足够;跑不可信任务的云端服务,microVM + 断网是底线。

带走的判断:审查一个 agent 产品的安全性,只问一句话——“把 system prompt 全删了,它还有哪些事做不了?“做不了的那部分才是真边界。

4. 多轮评测:验收世界的终态,再重复 k 次看方差

根本约束:单轮问答可以拿参考答案比文本,多轮任务不行——同一个目标有无数条合法路径,agent 可以用你没想到的顺序调工具、犯错再自愈。逐步比对轨迹既不可行也不该做。

业界收敛出的第一个答案:不评过程,评终态。让 agent 在真实环境里跑完,然后检查世界被改成了什么样:

  • SWE-bencharXiv:2310.06770):给 agent 一个真实 GitHub issue,评测标准不是”补丁长得像不像官方修复”,而是装上补丁后测试套件过不过。测试即裁判。
  • τ-bencharXiv:2406.12045):模拟客服场景,一个 LLM 扮演用户与 agent 多轮对话,评测标准是对话结束后数据库的终态是否正确(机票改了没有、退款金额对不对)。
  • WebArenaarXiv:2307.13854):自托管的真实网站,检查任务后的站点状态。

这呼应第 0 节的公理:模型不能自证,所以成败的真相放在环境里——测试、数据库、文件系统,它们不会被话术说服。

第二个答案更少人做,但更重要:区分能力与可靠性。惯用指标 pass@k 问的是”跑 k 次,至少一次成功吗”——它度量能力上限,适合科研刷榜。但生产系统的问题恰好相反:“跑 k 次,每次都成功吗”。τ-bench 为此提出了 pass^k:

pass@k=1(1p)k(越跑越容易"至少成一次")\text{pass@}k = 1-(1-p)^k \quad\text{(越跑越容易"至少成一次")} passk=pk(越跑越难"次次都成")\text{pass}^k = p^k \quad\text{(越跑越难"次次都成")}

数字会说话:单次成功率 90% 的 agent,pass@8 高达 99.99999%,看起来接近完美;而 pass^8 只有 43%——给 8 个客户各办一单,四成多概率至少搞砸一个。τ-bench 论文实测,当时最强模型在零售场景 pass^8 不足 25%。能力和可靠性之间隔着一个指数,这就是 demo 惊艳、上线拉胯的数学解释。

两个答案合成一个评测函数:

def evaluate(agent, task, k=8):
    results = []
    for _ in range(k):                    # 重复 k 次,看方差
        world = fresh_environment()       # 每次全新环境
        agent.run(task, world)            # 不看轨迹,任它自由发挥
        results.append(world.final_state_correct())  # 终态判定:环境不会被话术说服
    p = mean(results)
    return {"pass@k": 1 - (1-p)**k,       # 能力上限,demo 靠它惊艳
            "pass^k": p**k}               # 可靠性,上线看的是它

带走的判断:评测多轮 agent 的公式是两步——终态判定(世界对不对)× 多次重复(方差多大)。只跑一次的评测,测的不是这个 agent,是这个 agent 那一次的运气。

5. Test-time compute:算力跟着验证器走

根本约束:推理时多花算力(更长的思维链、采样 n 个候选、树搜索)确实能换成功率——OpenAI o1 和 DeepSeek-R1(arXiv:2501.12948)已经把这条曲线变成了常识。但 token 是要付钱的,问题从”有没有用”变成了”花在哪、花多少才划算”。

两个一手结论值得记住。第一,Snell et al.(arXiv:2408.03314)系统测量后发现:最优策略取决于问题难度——简单问题上多花推理算力几乎白花(本来就会),太难的问题上也白花(怎么想都不会),收益集中在”踮脚够得着”的中间难度带;且在这个难度带里,用对 test-time 策略的小模型可以打平大 14 倍的模型。第二,也是更根本的:采样 n 个候选只有在你能挑出对的那个时才有价值。Best-of-n(生成 n 个答案选最优)的前提是有个可靠的”选择器”——这又回到那个反复出现的角色:验证器(verifier)

这解释了一个现象级的分布:为什么推理算力的红利集中在数学和代码?因为它们自带强验证器——数学能核对答案,代码能编译、能跑测试。验证器把随机采样变成了有反馈的搜索;没有验证器的领域(开放写作、主观判断),多采样只是多花钱买 n 个无法排序的候选。所以分配算力的一句话准则:

验证器有多强,test-time compute 就值得花多深。
强验证器(测试、编译、类型检查)→ 多采样、多迭代,错了能兜住;
弱验证器(人眼、LLM 评审)→ 省下算力,把钱花在更强的单次生成上。

落到 coding agent 的日常,这条准则的实操形态是难度路由:机械步骤(改个 import、跑个命令)用低推理档位甚至小模型;写核心逻辑、调试诡异 bug 时上高推理档位 + 测试驱动的多次迭代。判断难度不需要玄学——任务失败过一次、涉及跨文件推理、测试反复不过,都是升档信号。

def allocate_compute(subtask):
    if difficulty(subtask) == TRIVIAL:
        return small_model_low_effort()        # 改个 import 不需要思维链
    if verifier(subtask) is STRONG:            # 有测试/编译器能挑出对的候选
        return best_of_n(n=8, pick=verifier)   # 采样变成有反馈的搜索
    return single_strong_attempt()             # 弱验证器:n 个无法排序的候选 = 白花钱

带走的判断:“该花多少 test-time compute”是个经济学问题,答案是边际原则:多花一档算力买到的成功率提升 × 任务价值 > 这档算力的价格,就继续加。而这笔账能不能算,取决于你有没有验证器给出成功率信号——又一次,真相在模型外面。

6. 上下文组织:窗口是缓存,不是存储

根本约束:我在前两篇(检索篇淘汰篇)已经拆过细节,这里只补上它在七问全景里的定位。约束有两条:窗口有限;且”Lost in the Middle”(arXiv:2307.03172)证明了塞得越满、中间的信息越容易被忽略——Anthropic 称之为注意力预算(attention budget):每个 token 都在消耗一份边际递减的注意力。

所以组织原则是一句话:上下文窗口是缓存(cache),不是存储(storage)。存储的真相在磁盘上(代码、日志、笔记文件),窗口里只放”当前推理正需要”的工作集。四类内容按可重算性分层管理:

内容可重算性策略
系统提示词 / 任务目标不可重算,是根常驻,且固定不变以吃 KV 缓存(见第 7 节)
检索结果 / 工具输出随时可重算(文件还在磁盘上,命令能重跑)最优先淘汰:掩码成”内容在某路径”的指针
执行轨迹(做过什么、决定了什么)不可重算,但可压缩定期 compaction 成结构化摘要(= 第 1 节的快照)
长期知识不进窗口外置成记忆文件,按需检索回来

这张表就是每轮开始时那个”重建工作集”的函数:

def build_context(log):                    # 每轮从存储(日志/磁盘)重建工作集
    ctx = [system_prompt]                  # 常驻,逐字节稳定以吃 KV 缓存(第 7 节)
    ctx += summarize(log.old_events)       # 执行轨迹 → 快照(= 第 1 节的 compaction)
    for r in log.tool_results:
        ctx += pointer(r.path) if r.recomputable else r  # 可重算的换成指针
    return ctx                             # 长期知识不进来:外置文件,按需检索

注意这张表和第 1 节是同一个系统的两面:事件日志是完整真相(存储),上下文是它的一个有损视图(缓存)。想通这一点,“compaction 会不会丢信息”这类焦虑就消解了——缓存本来就该丢,只要存储层还能找回来。

带走的判断:往窗口里塞任何东西之前问一句:“这个 token 是模型此刻推理必需的,还是’万一用得上’?“后者一律外置成指针。目标不是装得多,是 Anthropic 那句准则——找到最小的高信号 token 集合

7. 速度、成本、成功率:不是三角权衡,是一个分数

根本约束:直觉说这是”不可能三角”——要快要便宜就得牺牲质量。但 agent 的账不是这么算的。生产系统真正的目标函数是:

每成功任务成本=单次尝试成本成功率每成功任务时长=单次尝试时长成功率\text{每成功任务成本} = \frac{\text{单次尝试成本}}{\text{成功率}} \qquad \text{每成功任务时长} = \frac{\text{单次尝试时长}}{\text{成功率}}

成功率坐在分母上。一个便宜 30% 但成功率从 80% 掉到 55% 的配置,每成功任务反而贵了。失败的尝试不是省了钱,是花了钱买零——还要再花一遍。所以三者不是三角,是成功率主导的耦合:提升首次成功率,同时降低成本和时长。这就是为什么第 4 节的 pass^k、第 5 节的验证器不是”质量团队的事”,它们是成本优化的第一杠杆。

在这个前提下,三个不牺牲成功率的独立优化手段:

  1. KV 缓存(prompt caching):agent 每轮都重发几乎相同的长前缀(系统提示词 + 历史),把它缓存住,Anthropic 的定价下缓存读取是正常输入价的十分之一。这要求前缀稳定——系统提示词里放一个时间戳、每轮微调工具列表,都会击穿缓存。这是 agent 成本工程里性价比最高的一条,且完全免费于成功率。
  2. 模型路由:第 5 节的难度路由同时也是成本手段——子任务分发给小模型(检索、格式化、摘要),主模型只做规划和难点。代价是路由本身可能判错,所以路由要保守:宁可高配低用,不可低配高用。
  3. 并行换时长:多个独立子任务 fan-out 给并行的 sub-agent,wall-clock 时间大幅下降,token 总量上升——这是用成本买速度,是三者中唯一真正的直接权衡,值不值取决于人等在屏幕前的时间值多少钱。
minimize(token_price)          # 错误目标:失败的尝试不是省钱,是花钱买零

cost_per_success = attempt_cost / success_rate   # 正确目标:成功率在分母
enable_kv_cache(stable_prefix=True)    # 前缀里放时间戳 = 击穿缓存
route_models(conservative=True)        # 宁可高配低用,不可低配高用
fan_out_subagents()                    # 唯一真权衡:花 token 买 wall-clock

带走的判断:汇报 agent 系统的指标时,别报 token 单价、别报单次延迟,报”每成功任务的成本和时长”。分母对了,优化方向才不会跑偏。

8. 七问之外:论文正在收敛的四个升级

七问是 2024–2025 业界收敛出的共识底座。2025–2026 的论文没有推翻它,而是在四个方向上打补丁——每个补丁都能落成对上面伪代码的一处具体修改。这也是检验理解的方式:新趋势不是新代码库,是旧骨架上的 diff

8.1 验证器本身也要花算力:从”跑测试”到”会思考的验证器”

第 5 节把验证器当成给定的:有测试就强,只有人眼就弱。前沿把这个假设拆了,两条线:

其一,Jason Wei 的验证不对称性与 verifier’s law(2025):很多任务验证远易于求解(解数独 20 分钟,验一遍 20 秒),且任务的可验证性可以被工程改造——先写单元测试,就把”难验证的写代码”变成了”易验证的过测试”。其二,生成式过程奖励模型:GenPRM(arXiv:2504.00891)和 ThinkPRM(arXiv:2504.16828)证明验证器不该是”输出一个标量”的判别函数,而应该自己先写验证思维链、跑代码复核、再下判断——验证这一侧同样吃 test-time compute,且按步验证(PRM)比只看终态(ORM)能更早拦住错误(综述见 Trust but Verify)。

# 旧:验证器 = 一次性判别函数
score = discriminative_verifier(candidate)     # 一个标量,错在哪、为什么,无从追问

# 新:验证器 = 也会推理、也花算力的生成式过程
def generative_verifier(candidate):
    for step in candidate.steps:               # PRM:逐步验证,不等终态
        critique = LLM(verify_prompt(step))    # 先写验证思维链,再下判断
        if critique.needs_check:
            critique += sandbox.run(critique.check_code)   # 用代码复核,不空口裁决
        if critique.verdict == WRONG:
            return REJECT(at=step, why=critique)   # 定位到步、带着理由——
                                                   # 恰好是第 2 节要的高信号错误文本

# verifier's law 的工程形态:动手前先做"可验证化"改造
make_verifiable(task) = write_tests_first(task) or define_rubric(task)

代码意义:第 5 节的准则”验证器有多强,算力就值得花多深”升级为两句——验证器的强度不是给定的,是可以造的(make_verifiable);验证器自己也是一个值得分配算力的推理过程,且它的 REJECT 输出直接回喂第 2 节的恢复循环,形成生成↔验证的闭环。

8.2 隔离的升级:从沙箱到控制流完整性

第 3 节的沙箱管”能不能做”;DeepMind 的 CaMeL(Defeating Prompt Injections by Design,2025)管一个沙箱管不住的问题:“不可信数据能不能改变要做什么”。思路完全来自经典安全(控制流完整性、信息流控制),而不是”用 AI 防 AI”:

def camel_run(user_query):                     # 只有主人的指令能决定控制流
    plan = privileged_LLM(user_query)          # 特权模型只读可信输入,写出固定计划
    for step in plan:                          # 控制流在接触不可信数据之前已锁死
        raw    = tool(step)                    # 网页、issue、邮件——全是潜在注入源
        parsed = quarantined_LLM(raw)          # 隔离模型读不可信数据,但没有工具权限
        parsed.capability = provenance(raw)    # 每个值带来源标签(信息流控制)
        if not policy.allows(step.next_tool, parsed.capability):
            require_human_confirm()            # 不可信来源的数据想流进敏感工具 → 拦下

代码意义:第 3 节防的是”被劫持之后干不成坏事”(能力边界),这一层防的是”被劫持”本身——注入的文本再有说服力也改不了 plan,因为写 plan 的模型根本没读过它;就算数据被污染,capability 标签也拦住它流向发邮件、打款这类敏感工具。代价同样要说清:锁死控制流牺牲了 agent 边干边调整的灵活性,CaMeL 在 AgentDojo 上以可证明的安全性解出约 67–77% 的任务——安全和效用的权衡还没有免费午餐。

8.3 记忆的升级:从单任务日志到跨任务经验

第 1、6 节的日志、快照、记忆文件全都活在单个任务里——任务结束,踩过的坑和验证过的做法就丢了。2025–2026 收敛出的方向是把”经验”变成一等公民:Reflexion(arXiv:2303.11366)把失败反思成文字教训;Mem0(arXiv:2504.19413)把记忆维护做成 ADD/UPDATE/DELETE/NOOP 四种显式操作;A-MEM(arXiv:2502.12110)让记忆之间自组织成网络;Agent Workflow Memory(arXiv:2409.07429)则把成功轨迹蒸馏成可复用的工作流。

def after_task(log, world, memory):            # 任务结束不是终点,是写记忆的时机
    if verifier.accepts(world):
        memory.upsert(distill_workflow(log))   # 成功轨迹 → 可复用技能/工作流
    else:
        memory.upsert(reflect(log, verifier.why))  # 失败 → 一句话教训(Reflexion)
    # upsert 含 ADD/UPDATE/DELETE:记忆要维护,不是只追加(Mem0)

def build_context(log, memory):                # 第 6 节的函数多一个入参
    ctx = [system_prompt]
    ctx += memory.retrieve(task, k=few)        # 上个任务的教训,是这个任务的先验
    ...

代码意义:注意 after_task 的分支条件是 verifier.accepts(world)——只沉淀验证过的经验。这是这条升级线最容易踩的坑:让模型自己总结”我学到了什么”再存起来,错误经验会跨任务自我强化,恰好违反第 0 节”模型不能自证”的公理。记忆写入也要过验证器。

8.4 训练闭环:RLVR 让验证器从推理资产变成训练信号

DeepSeek-R1 之后,RLVR(可验证奖励强化学习)成为推理模型后训练的主流范式(趋势综述):不训奖励模型,直接拿验证器当奖励函数——数学核答案,代码跑测试,对了 1 分错了 0 分。二值信号简单到近乎粗暴,却让模型自发长出了反思和自我检查。

def rlvr_training(policy, tasks):
    for task in sample(tasks):
        world  = run_agent(task, model=policy)         # 就是第 8 节上面那个主循环
        reward = 1 if verifier.accepts(world) else 0   # 验证器即奖励函数,没有可谄媚的裁判
        policy.update(reward)                          # GRPO / PPO
# 推理时:verifier 挑候选(第 5 节)、判终态(问 4)
# 训练时:同一个 verifier 直接变成 RL 环境
# —— 你为运行时写的每一个验证器,都是未来训练环境的一部分

代码意义:这一段解释了 verifier’s law 为什么成立——“能验证一个任务”等价于”能为它搭 RL 环境”,所以易验证的任务终将被训练攻克。工程含义很直接:今天为 agent 写的测试、终态检查、rubric,是复利最高的资产,推理和训练两头都在用。前沿正在把 RLVR 推出数学和代码(rubric 奖励、训练专用 verifier 模型,如 OpenReward);悬而未决的是长程 credit assignment——五十步的任务只在最后给一个 0/1,哪一步立了功没人知道。

四个升级方向指向同一处:七问里那个到处被调用的 verifier,正在从系统里的一个组件,变成整个体系的中心

收束:七个问题,一个主循环,一张表

把七节的伪代码叠起来,就是一个 agent 运行时的骨架——注意 LLM() 只出现在一行,其余全部是”模型外面”的确定性代码,这就是标题里”包装”二字的含义:

def run_agent(task):
    log      = EventLog(task)            # 问1:真相在日志,不在模型脑子里
    sandbox  = Sandbox(policy)           # 问3:边界用代码强制,不写在 prompt 里
    verifier = Verifier(task)            # 问5:动手前先回答"这个任务的验证器是什么"
    while not verifier.accepts(world):   # done 由验证器看世界终态判定——
        context = build_context(log)     # 问6:窗口是日志的缓存视图
        action  = LLM(context)           # 唯一的随机环节
        action  = allocate_compute(action)              # 问5:算力跟着验证器走
        result  = execute_with_recovery(action, sandbox)  # 问2:错误是数据
        log.append(action, result)       # 先写日志,再谈下一步
    return world                         # 交付的是世界终态,不是模型的一段话

# 问4 在循环外面:evaluate() 把 run_agent 重复 k 次,量 pass^k
# 问7 也在循环外面:cost_per_success = attempt_cost / success_rate

这个循环里最容易写错的一行是循环条件。写成 while not done 就把最关键的问题藏掉了:done 是谁判定的?按第 0 节的公理,绝不能是模型自报”我做完了”——判定权在验证器看到的世界终态(测试过没过、目标记录改没改)。这也正是第 2 节里那类”抛不出异常”的语义失败被发现的唯一位置:工具全部成功、模型自我感觉良好,只有 verifier.accepts(world) 返回 False 能拦住它提前交卷。

问题经典祖先一句话答案
1. 状态记录数据库 WAL / 事件溯源对话历史即重做日志,compaction 即快照,计划外置成文件
2. 失败恢复重试语义 / 幂等性瞬时错误框架重试,确定错误原文喂回模型,工具设计成幂等
3. 隔离虚拟化 / 最小权限防的是被注入的好模型;边界用代码强制,分四层按爆炸半径配防御
4. 多轮评测集成测试 / SLO验收世界终态而非文本;用 pass^k 量可靠性,别被 pass@k 的能力上限骗
5. Test-time compute调度经济学收益集中在中等难度 + 强验证器的任务;按边际收益加档
6. 上下文组织内存层级窗口是缓存不是存储,按可重算性分层,追求最小高信号集
7. 三指标优化性能工程目标函数是每成功任务成本;成功率在分母,是第一杠杆

如果只能带走三条:

  • 真相放在模型外面——日志记状态、沙箱设边界、终态判成败。模型是无状态、可重启、不可全信的计算单元,不是记忆体、不是裁判、不是受托人。
  • 错误率按步数做乘法——所以恢复是主流程不是兜底,可靠性(pass^k)和能力(pass@k)之间隔着一个指数。
  • 验证器是整个体系的稀缺资源——它决定语义错误能否被发现(第 2 节)、任务何时算完成(主循环的条件)、评测是否可信(第 4 节)、test-time compute 值不值得花(第 5 节)、成功率这个分母能不能量(第 7 节);而 RLVR 正在让它同时成为训练信号(第 8 节)。拿到一个新领域,先问”这里的验证器是什么”,其他答案会跟着清晰——写验证器是当下复利最高的工程投资。

参考