为什么 Agent 的每个请求,都要先拍一张快照

拆解 pi(DeepSeek 开源终端 Coding Agent)v0.83.0 的 turn snapshot 机制:一次请求的输入必须是稳定的一整份,而不是从字段里零散读取。源码依据:pi-internals 20 章

长任务里最常见的撕裂

Agent 跑一个长任务时,模型在变、工具在变、资源在变、思考级别在变。这些变化不是坏事------长任务本来就要在轮次之间调整。坏的是:一个请求发出去了,读到的输入却是新旧混合的

比如系统提示来自旧版资源、调用文本却来自新版资源;或者请求开始时模型是 A,发到一半配置被改成 B,请求却按 B 的期望返回。这种撕裂是最难排查的 bug 之一:不是逻辑错,是状态错。

pi 的答案是:每个请求开始前,先拍一张完整的快照。

turn 的边界

先明确 turn 是什么。它不是用户在界面上看到的整条 prompt,也不是整个 runAgentLoop()。它更接近「一次 assistant response,加上它要求的完整工具批次」。

这个单元结束后,Harness 才把这一轮攒下的 pending writes 排到 transcript 尾部,再为下一次 provider 请求重建状态。

所以「快照」天然与 turn 绑定:一个 turn 一份快照,turn 之间状态可以随便变,turn 内部必须稳定。

快照里装着什么

createTurnState() 做的事很具体:先调用 session.buildContext() 读取当前 active branch 的消息和派生配置,再读资源、session metadata、tool context。工具注册表被展开,active tool name 被映射成具体工具。系统提示如果是回调,也在这里执行一次,得到本轮真正使用的字符串。

返回值包含 messages、resources、toolContext、streamOptions、sessionId、systemPrompt、model、thinkingLevel、全部工具和 active tools。

createContext() 再对 messages 做一次顶层浅拷贝,把 active tool 绑定到本轮解析出的 toolContext。一个 provider 请求需要的可变输入,从此不再从 Harness 字段里零散读取------它是一份自洽的整体。

快照不是深拷贝

这里有个容易被误读的细节:快照不等于冻结整个对象图。resources 的容器和 stream options 会被复制,但 skill、prompt template、headers 里更深的对象仍然共享引用,tools 也是具体对象。

这个实现依赖调用方遵守不可变更新的习惯------用 setResources() 换一组值,而不是悄悄改写旧的 skill 对象。源码保证的是 Harness 自己不会在同一 turn 内重新选择这些对象,不是 JavaScript 世界里的深冻结。

这个取舍是诚实的:深拷贝一个对象图很贵,而且对「快照稳定性」的收益有限。真正需要保证的是「这一轮不会读到被改了一半的配置」,而不是「这一轮读到的对象永远不会变」。

三个入口共用同一份快照

prompt()、skill()、promptFromTemplate() 三种用户发起的运行,都先调用 createTurnState()。skill 和 template 也从这份 state 的 resources 里找。

为什么要强调这一点?因为如果 skill 入口自己另读一份 resources,就可能出现「调用文本来自新版资源、系统提示却来自旧版资源」的撕裂。统一入口 + 统一快照,把这个可能性直接关掉。

保存点在工具结果之后

低层 runLoop() 的时序给出了 turn 的完整边界:先 stream assistant response;如果响应含工具调用,就执行完整工具批次并把结果追加到 context;然后发出 turn_end。

Harness 在 turn_end 里规定更细的时序:先等待普通 subscriber;记录 flush 前是否存在 pending mutation;按接收顺序 flush;若 subscriber 失败再抛出错误;最后发出 save_point。

保存点之后,prepareNextTurn() 保证 pending writes 已清空,调用 createTurnState(),把新鲜的 context、model、thinking level 交回低层 loop------下一轮开始。

拆完这章,我的感受是:Agent 框架的可靠性,很大一部分藏在「输入的稳定性」里。模型调用本身是不可靠的,但框架可以保证它收到的输入是一份自洽的、属于当前时刻的快照------把「状态变了」和「请求发出去了」这两件事严格分开。做到这一点,很多诡异的中途 bug 根本不会出现。

想看完整拆解(turn_end/save_point/prepareNextTurn 源码与时序实验),这里有 pi-internals 小册子

源码依据:packages/agent/src/harness/agent-harness.ts L354-L397、L538-L565、L442-L497、L658-L704;packages/agent/src/agent-loop.ts L192-L245,pi v0.83.0,commit 845d6ff1。

相关推荐
烬羽1 小时前
组件拆了,逻辑没拆——自定义 Hook 才是 React 业务逻辑的正当归属
react.js·架构·typescript
小月土星1 小时前
自定义业务 Hook:从零解剖一个 React + TypeScript Todo 应用:用金字塔思维看透前端架构
react.js·架构·前端框架
AI 小老六2 小时前
Brainstorming 与 grill-me 在 AI 产品设计和工程决策中的分工边界
人工智能·ai·架构·创业创新
莫逸风3 小时前
【AgentScope 2.0】05-文件系统(Filesystem)详解
java·ai·agent·springai·agentscope
苏灿烤鱼3 小时前
今日 GitHub 热门|自改进 Agent 首日登顶,+2,180 项目却只排第四
agent
重庆小透明3 小时前
深入探寻微服务【第一篇微服务的坏】
微服务·云原生·架构
eric-sjq3 小时前
仅0.6B参数如何锁住超长记忆?Xiaothink-T17-RWKV5-MLA 架构深度解析:RWKV-v5 × MLA 的“降维打击“
python·架构
国科安芯3 小时前
抗辐照精密运放怎么选:ASL8522S 的斩波稳零与微安级功耗
架构·信息与通信·深空探测·抗辐射加固·极端环境电源
苏灿烤鱼4 小时前
GitHub #1 拆解|它说自己在进化,但 worker 跑的是你本机权限,不是沙箱
人工智能·typescript·agent