第 15 章:AI Agent——把模型流、工具与编辑器事务编排成一轮对话
Zed Agent 的核心不是聊天面板,而是一个可取消、可压缩、可并行调用工具、能把编辑纳入 undo/checkpoint 的执行循环。模型只是其中一个异步生产者。
1. 三层边界
agent_ui
ThreadView / message editor / tool cards / permission UI
↓
agent::Thread
messages / turn loop / tools / compaction / checkpoints
↓
language_model + Project capabilities
provider streaming API / Buffer / terminal / search / GitStoreUI 不直接调用模型,provider 也不直接写文件。Thread 是持久会话与运行中 turn 的事实来源。
2. Thread 保存什么
crates/agent/src/thread.rs 的 Thread 不只是 Vec<Message>,还保存:
- AgentMessage、UserMessage 与 tool use/result;
- 当前 model、profile、上下文与 token 使用;
- running turn、cancel handle 和事件 channel;
- tool registry、权限与 sandbox 策略;
- action log、Git checkpoint、undo 信息;
- compaction summary 和会话统计。
把这些放在一个 Entity 中,UI 可以订阅 ThreadEvent 增量更新;会话持久化也能重建“为什么执行了这个 工具”,而不只重建最终聊天文本。
3. Send 只是启动入口
Thread::send(crates/agent/src/thread.rs:2487-2522)完成:
- 将输入提交为 UserMessage;
- 验证当前语言模型可用;
- 推进 prompt/message id;
- 调用
run_turn。
真正的调度发生在 run_turn_internal。run_turn 会先同步 flush 上一条尚未落盘的 pending message, 取消旧 turn,创建新的事件通道和工具集合,然后保存 RunningTurn task。
4. LanguageModel 是流式协议
crates/language_model/src/language_model.rs 的 LanguageModel trait 以 stream_completion 为核心, 把不同 provider 统一成事件流。事件包括:
- Queued、Started、StartMessage;
- Text、Thinking、RedactedThinking、ReasoningDetails;
- ToolUse、ToolUseJsonParseError;
- UsageUpdate、Stop、Compaction。
UI 因此不依赖某家 provider 的 chunk 格式;Thread 把 provider delta 转换为 AgentMessage chunk 和 ThreadEvent。
5. 一轮 Agent 的状态机
stateDiagram-v2
[*] --> Prepare
Prepare --> Compact: context 需要压缩
Prepare --> StreamModel: context 足够
Compact --> StreamModel
StreamModel --> AppendText: text/thinking event
AppendText --> StreamModel
StreamModel --> RunTools: tool use
RunTools --> StreamModel: tool results 加入下一次请求
StreamModel --> Retry: 可重试错误
Retry --> StreamModel
StreamModel --> Done: stop 且没有工具
Prepare --> Cancelled: 用户取消
StreamModel --> Cancelled: 用户取消
RunTools --> Cancelled: 用户取消
Done --> [*]
Cancelled --> [*]run_turn_internal(crates/agent/src/thread.rs:2719-3068)是一个 loop:每次迭代重新读取 model 和 enabled tools,构造请求,消费模型流,等待工具结果;只要产生工具调用,就把结果追加进上下文并进入 下一次 completion。
6. 为什么每轮迭代重新读取模型与工具
用户可能在 turn 中途切换 model、profile 或禁用工具。若在 run_turn 开头永久捕获配置,后续工具回合 仍使用旧值。循环每次重读 Entity 状态,下一次 completion 立即采用最新配置,同时已经发出的请求保持 一致。
这是“运行配置快照”的边界:一次 provider request 内固定,请求之间允许更新。
7. 模型事件与工具结果是并发生产者
模型可能继续流式输出多个 tool call,而较早的工具已经在执行。Thread 使用 FuturesUnordered 管理工具 task,并用 select 同时等待:
- 下一个 model event;
- 已完成的 tool result;
- cancellation。
可以安全提前运行的工具不必等模型整条消息结束。例如模型已完整给出一次只读搜索参数,就能开始 grep; 随后另一个独立读取也可并行。
8. 为什么批量应用流事件
token 可能以很小 chunk 高频到达。若每个 token 都执行一次 Entity update、layout 和 redraw,会把 UI 线程淹没。循环会先吸收当前已经 ready 的一批 model events,再在一次 cx.update 中追加消息并 emit。
源码关键区间:crates/agent/src/thread.rs:2883-2923。这与 Scene 批处理、LSP change 合并是同一条 性能原则:外部细粒度事件先在边界聚合,再触发响应式更新。
9. 工具不是任意闭包
Tool 定义至少包含:
- 名称、描述和 JSON input schema;
- 输入流式解析能力;
- 是否可提前/并行执行;
- 运行所需 Project/Workspace capability;
- 权限、sandbox 与输出格式;
- cancellation 和错误语义。
内置工具覆盖文件读取/编辑、路径查找、grep、definitions/references、diagnostics、terminal、web search、 skill、subagent/thread 等。注册入口在 crates/agent/src/tools.rs 及相邻 tool 模块。
10. 工具通过 Project 工作,而不是绕过编辑器
读写工具尽量使用 Project、BufferStore 与 Workspace:
- 读取能看到未保存 Buffer;
- edit 生成 Buffer transaction,可 undo;
- path 经过 Worktree/ignore/remote 语义;
- diagnostics/references 复用 LSP store;
- terminal 复用 task/terminal 管理;
- remote project 在 host 执行,无需模型感知 SSH。
如果 Agent 直接用 std::fs 修改文件,Editor snapshot、dirty state、collaboration operation、undo 与远端 模式都会被绕开。
11. 权限与 sandbox 是两道门
权限层回答“用户是否允许这次动作”,可表现为 allow once、allow for thread、always allow 或 deny;sandbox 回答“即使获准,进程能接触哪些文件、网络和系统资源”。
模型提出 tool use
→ 解析并验证 schema
→ 根据 tool/input 计算 permission request
→ 用户/持久策略作决定
→ 在指定 sandbox level 中执行
→ 对输出做长度/结构化处理
→ ToolResult 回到 Thread只做提示框而不隔离命令,无法限制误操作;只做 sandbox 而没有语义权限,又无法让用户理解意图。
12. 为什么释放 model stream semaphore
模型流通常受 provider 并发 semaphore 限制。当响应结束、接下来只剩长工具时,Thread 会主动释放 stream permit,再等待工具。否则工具若启动 subagent,而 subagent 也需要同一模型 permit,就会形成资源死锁。
关键注释和实现位于 crates/agent/src/thread.rs:2938-2943。这是异步 Agent 系统中很容易遗漏的“跨层 锁顺序”问题。
13. 流式工具输入的失败收尾
ToolUse 参数可能分多个模型 chunk 到达。每个工具输入有 sender/receiver;模型流异常结束时必须 drop 仍未完成的 sender,使解析或执行 task 得到 EOF,而不是永久等待下一段 JSON。
源码:crates/agent/src/thread.rs:2945-2955。任何 channel-based streaming API 都应显式设计 producer 异常退出时的关闭路径。
14. Compaction 不是删除旧消息
上下文接近模型限制时,perform_compaction_if_needed 选择旧消息区间,请模型流式生成 summary,最终 插入 Message::Compaction。后续 prompt 用 summary 替代被覆盖的详细历史,但 UI 与持久记录仍可以保留 原始消息。
压缩必须保留:目标、已完成工作、关键文件/符号、工具结果、未解决问题和用户约束。仅按字符截断会破坏 tool call/result 配对,也可能丢失安全约束。
入口:crates/agent/src/thread.rs:3100-3190。
15. Checkpoint、reject 与 undo
Agent 的 edit 不是不可逆副作用。执行修改前可通过 GitStore 创建 checkpoint;工具 action log 记录变更与 关联消息;用户 reject 或 restore 时协调 Git/file 与 Buffer transaction。
边界比简单 git checkout 更复杂:未跟踪文件、未提交的人类编辑、多个 worktree、未保存 Buffer 都不能 被误删。恢复动作必须只覆盖 Agent 实际拥有的 change set。
16. ACP:把 Agent UI 与实现解耦
crates/acp_thread/src/acp_thread.rs 定义 UI 可消费的通用 thread/message/tool-call/permission/checkpoint 表示。NativeAgentConnection 再把内置 Thread 适配成 ACP AgentConnection。
这让同一 ThreadView 能承载:
- Zed 内置 Native Agent;
- 外部 ACP agent;
- 不同模型 provider;
- 不同 tool capability 与权限选项。
协议表达不了的本地细节放入 meta,而不是污染 UI 对某个 agent 的特判。
17. 一次编辑工具调用的完整链路
sequenceDiagram
participant M as Language Model
participant T as Agent Thread
participant P as Permission UI
participant Tool as Edit Tool
participant Proj as Project / Buffer
participant E as Editor
M->>T: ToolUse(edit_file, JSON chunks)
T->>T: schema 校验、组装输入
T->>P: 请求权限
P-->>T: allow once
T->>Tool: run(input, sandbox, cancel token)
Tool->>Proj: open buffer + transaction edits
Proj->>E: BufferEvent / redraw
Tool-->>T: structured ToolResult
T->>M: 下一次 completion 携带结果
M-->>T: 文本总结或下一组工具18. 失败与取消清单
- provider rate limit:按可重试类型 backoff,不重复已完成工具;
- JSON 不完整:展示 parse error,关闭 streaming input;
- 用户取消:停止 model stream,并把 cancel token 传播给工具;
- 工具超长输出:截断/摘要并保留可追踪元数据;
- permission denied:作为结构化结果回给模型,不伪装为系统错误;
- model 中途切换:当前 request 结束,下一 loop 使用新 model;
- Buffer 在执行前变化:Anchor/version 重新验证,避免改错位置;
- remote 断线:Project 工具返回可辨识连接错误,不退化为本地路径。
19. 可迁移经验
- Agent loop 是显式状态机,不是一串递归 callbacks;
- 模型事件、工具结果、取消是并发输入,统一 select;
- 高频 stream chunk 批量写入响应式状态;
- 工具走领域 API,才能获得事务、undo、remote 与协作语义;
- 权限决定意图,sandbox 限制能力;
- 长工具前释放稀缺模型资源;
- compaction 保持语义与 tool pairing,而非机械截断;
- 所有副作用都要能归因、展示并尽可能恢复。