Skip to content

第 15 章:AI Agent——把模型流、工具与编辑器事务编排成一轮对话

Zed Agent 的核心不是聊天面板,而是一个可取消、可压缩、可并行调用工具、能把编辑纳入 undo/checkpoint 的执行循环。模型只是其中一个异步生产者。

1. 三层边界

text
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 / GitStore

UI 不直接调用模型,provider 也不直接写文件。Thread 是持久会话与运行中 turn 的事实来源。

2. Thread 保存什么

crates/agent/src/thread.rsThread 不只是 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::sendcrates/agent/src/thread.rs:2487-2522)完成:

  1. 将输入提交为 UserMessage;
  2. 验证当前语言模型可用;
  3. 推进 prompt/message id;
  4. 调用 run_turn

真正的调度发生在 run_turn_internalrun_turn 会先同步 flush 上一条尚未落盘的 pending message, 取消旧 turn,创建新的事件通道和工具集合,然后保存 RunningTurn task。

4. LanguageModel 是流式协议

crates/language_model/src/language_model.rsLanguageModel 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 的状态机

mermaid
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_internalcrates/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 回答“即使获准,进程能接触哪些文件、网络和系统资源”。

text
模型提出 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. 一次编辑工具调用的完整链路

mermaid
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. 可迁移经验

  1. Agent loop 是显式状态机,不是一串递归 callbacks;
  2. 模型事件、工具结果、取消是并发输入,统一 select;
  3. 高频 stream chunk 批量写入响应式状态;
  4. 工具走领域 API,才能获得事务、undo、remote 与协作语义;
  5. 权限决定意图,sandbox 限制能力;
  6. 长工具前释放稀缺模型资源;
  7. compaction 保持语义与 tool pairing,而非机械截断;
  8. 所有副作用都要能归因、展示并尽可能恢复。

独立源码学习笔记 · 文档采用 CC BY-SA 4.0