第 5 章 · 巧妙之处、边界与对比
这一章是「带走的精华」:先讲几个值得借鉴的工程巧思,再诚实地划边界、做横向对比,最后给一张代码地图方便你/agent 直接跳源码。
5.1 巧妙之处(可借鉴的技术)
① 流式输出 + 工具结果并发收集,还能随时取消
goose 的循环不是「等模型全说完再跑工具」,而是边流式接收模型输出边处理;多个被批准的工具的输出流用 stream::select_all 合并并发收集(agents/agent.rs:2176)。妙在那个收集循环里挂了个 100ms sleep 分支:
// agents/agent.rs:2184 — biased select:优先收工具结果,但每 100ms 也醒一次查取消
tokio::select! {
biased;
tool_item = combined.next() => { /* 处理工具输出/通知 */ }
_ = tokio::time::sleep(Duration::from_millis(100)) => {} // 让循环能周期性检查取消
}
效果:工具再慢、再沉默,用户按 Ctrl-C 也能及时中断。长任务 agent 的「可中断性」就是这么一点点抠出来的。
② tool_call_id 去重:防恶意/故障 provider 把一个工具跑两次
模型本该给每个工具调用一个唯一 id,但故障或恶意的 provider 可能重复同一个 id。categorize_tool_requests 用一个 HashSet 只保留每个 id 的首次出现(reply_parts.rs:433-437):
// agents/reply_parts.rs:433 — 同一 id 只留第一次,按 provider 给的顺序
let mut seen_ids = std::collections::HashSet::new();
let tool_requests = tool_requests.into_iter()
.filter(|req| seen_ids.insert(req.id.clone()))
.collect();
这避免了同一工具被执行两次,也避免重复 tool_result 污染历史(严格的 provider 会因重复 tool_call_id 直接报错)。把「下游可能不老实」当默认假设,是处理 LLM 输出的好习惯。
③ 思考内容回放抑制:既不重复刷屏,又不丢非流式思考
带 thinking 的模型,流式时会先吐一段思考再吐工具调用。如果工具调用块里又带上完整累积的思考,用户界面会看到思考被「重播」一遍。goose 用 surfaced_thinking_in_turn 标志(agents/agent.rs:2012、2052)记住「本回合是否已经展示过思考」,只在已展示过时才从可见消息里抹掉重复思考(categorize_tool_requests 的 should_suppress_replayed_thinking,reply_parts.rs:440)——这样流式思考不重播,但「只在最后一次性给出的思考」不会被误删。
同时,思考内容在喂回模型的历史里必须保留(Gemini 要求回显 thinking,Kimi/DeepSeek 要求 assistant 工具消息带 reasoning_content,agents/agent.rs:2249-2272)。所以这里是「对用户抑制、对模型保留」——又一次可见性双轨的应用。
④ 不可解析的工具调用:塞一个占位的合法调用,错误走回执
模型偶尔会吐出参数解析不了的工具调用。如果直接把 Err 写进历史,每个 provider 的格式化器都得特判 Err 分支。goose 的取巧:历史里存一个合法的占位工具调用(名为 unparseable_tool_call),真正的解析错误作为配对的工具结果回给模型(agents/agent.rs:2306-2353)。这样历史对每个 provider 的「正常 Ok 路径」都是良构的,不必为每家特判,模型也能看到错误并自我纠正。
⑤ 稳定工具排序 = prompt 缓存命中
工具列表按名字排序(reply_parts.rs:214),注释直白点明是为了多会话 prompt 缓存:系统提示里工具定义那段如果顺序固定,prefix 就能被各家的 prompt cache 命中,省钱省延迟。一个一行的 sort_by,背后是真金白银。
⑥ provider 错误的「可恢复 vs 终止」分类
见第 1 章 §1.7:上下文超限当可恢复(压缩重试),模型拒答当终止(重发只会再被拒)。这种按错误语义而非粗暴重试的处理,是长跑稳定性的关键。
5.2 边界与局限(诚实)
- 强依赖模型的工具调用能力:没有原生 function-calling 的模型只能靠 toolshim(把工具协议塞进文本提示 + 后处理解析,
reply_parts.rs:316-362)模拟,准确率和稳定性都打折。 - 安全检查器是「尽力而为」:prompt-injection / egress 用模式匹配 + 可选 LLM 审查,不是形式化保证;
SecurityInspector默认是否启用取决于配置(security_inspector.rs:84)。它能拦经典攻击(如curl | bash),但对新颖手法无法保证。 - 工具/扩展过多会降准:系统提示自己都在提醒超过 5 扩展 / 50 工具就该精简(
prompt_manager.rs:19-20),因为工具一多,模型选错工具的概率上升。 - 压缩是有损的:整体压缩把历史变摘要,细节会丢;虽保留最近用户消息,但更早的精确内容模型就看不到了。
- 审批模式不适合 headless:Approve/SmartApprove 在非交互模式下会直接报错退出(
goose-cli/src/session/mod.rs:1222),设计如此——无人值守只能用 Auto。 - 本文未深入的部分:各 provider 的具体 wire-format 转换(
formats/)、toolshim 后处理细节、内置 MCP 扩展(computercontroller/memory 等)的内部实现、recipe 完整字段语义、scheduler 的 cron 实现、code-mode(pctx)机制——这些不影响主线理解,但要改对应模块需另读。
5.3 横向对 比(同 shelf 兄弟项目)
goose 在 ai-frontier-reference 的 coding-agents / agent-frameworks 区。和典型兄弟项目的取舍差异:
| 维度 | goose 的取舍 |
|---|---|
| 语言 | Rust(多数同类是 Python/TS)——换性能、可移植性、单二进制分发,代价是上手门槛和迭代速度 |
| 工具接入 | MCP-原生:工具不是内建,而是统一走 MCP server,六种接入类型;扩展生态外置 |
| 定位 | 通用 agent(不只编码),桌面 App + CLI + API 三壳并重 |
| 安全 | 重护栏:执行前检查器流水线 + 四种权限模式 + 生命周期 hooks,是它相对很多框架更用心的差异点 |
| 长任务 | 阈值压缩 + 工具对摘要 + 可见性双轨 + 子 agent + recipe,体系比较完整 |
一句话:如果说很多框架是「Python 写的、工具内建的、编码向的库」,goose 是「Rust 写的、MCP 插槽化的、通用且重安全的运行时」。
5.4 代码地图(导航索引)
agent 用符号名比行号更抗漂移,优先用 grep 符号定位。
| 主题 | 文件 | 符号名 |
|---|---|---|
| Agent 主结构 | crates/goose/src/agents/agent.rs | struct Agent、AgentConfig |
| 公开入口 | crates/goose/src/agents/agent.rs | Agent::reply |
| 中央循环 | crates/goose/src/agents/agent.rs | Agent::reply_internal |
| 事件类型 | crates/goose/src/agents/agent.rs | enum AgentEvent |
| turn context 注入 | crates/goose/src/agents/moim.rs | inject_moim、system_prompt_block |
| 单个工具派发 | crates/goose/src/agents/agent.rs | Agent::dispatch_tool_call |
| 准备工具+提示 | crates/goose/src/agents/reply_parts.rs | prepare_tools_and_prompt |
| 问模型(流式) | crates/goose/src/agents/reply_parts.rs | stream_response_from_provider |
| 工具分类+去重 | crates/goose/src/agents/reply_parts.rs | categorize_tool_requests |
| Provider trait | crates/goose-providers/src/base.rs | trait Provider、MessageStream |
| 权限模式 | crates/goose-providers/src/goose_mode.rs | enum GooseMode |
| 扩展管理器 | crates/goose/src/agents/extension_manager.rs | struct ExtensionManager |
| 工具路由 | crates/goose/src/agents/extension_manager.rs | resolve_tool、dispatch_tool_call |
| 扩展类型 | crates/goose/src/agents/extension.rs | enum ExtensionConfig |
| 检查器 trait/管理 | crates/goose/src/tool_inspection.rs | trait ToolInspector、ToolInspectionManager、apply_inspection_results_to_permissions |
| 安全检查器 | crates/goose/src/security/security_inspector.rs | SecurityInspector |
| egress 检查器 | crates/goose/src/security/egress_inspector.rs | EgressInspector |
| 权限检查器 | crates/goose/src/permission/permission_inspector.rs | PermissionInspector、detect_read_only_tools |
| 上下文压缩 | crates/goose/src/context_mgmt/mod.rs | compact_messages、check_if_compaction_needed |
| 工具对摘要 | crates/goose/src/context_mgmt/mod.rs | maybe_summarize_tool_pairs、compute_tool_call_cutoff |
| 系统提示组装 | crates/goose/src/agents/prompt_manager.rs | PromptManager、SystemPromptBuilder |
| 子 agent | crates/goose/src/agents/subagent_handler.rs | run_subagent_task |
| CLI 消费流 | crates/goose-cli/src/session/mod.rs | (reply 调用 + AgentEvent 匹配,约 :1188-1350) |
| CLI 入口 | crates/goose-cli/src/main.rs | main |
| Server 入口 | crates/goose-server/src/main.rs | main(二进制 goosed) |
5.5 一句话收束
goose = 一个 Rust 写的、MCP-原生的通用 AI agent 运行时:核 心是一个流式的「问模型→安检→跑工具→喂回」循环,工具全部插槽化(MCP),并在动手前串一条可插拔的安全/权限链;长任务靠阈值压缩、工具对摘要、可见性双轨和子 agent 撑住。读懂第 1 章的循环,其余都是挂在这条心跳上的护栏与续航装置。