跳到主要内容

第 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_requestsshould_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.rsstruct AgentAgentConfig
公开入口crates/goose/src/agents/agent.rsAgent::reply
中央循环crates/goose/src/agents/agent.rsAgent::reply_internal
事件类型crates/goose/src/agents/agent.rsenum AgentEvent
turn context 注入crates/goose/src/agents/moim.rsinject_moimsystem_prompt_block
单个工具派发crates/goose/src/agents/agent.rsAgent::dispatch_tool_call
准备工具+提示crates/goose/src/agents/reply_parts.rsprepare_tools_and_prompt
问模型(流式)crates/goose/src/agents/reply_parts.rsstream_response_from_provider
工具分类+去重crates/goose/src/agents/reply_parts.rscategorize_tool_requests
Provider traitcrates/goose-providers/src/base.rstrait ProviderMessageStream
权限模式crates/goose-providers/src/goose_mode.rsenum GooseMode
扩展管理器crates/goose/src/agents/extension_manager.rsstruct ExtensionManager
工具路由crates/goose/src/agents/extension_manager.rsresolve_tooldispatch_tool_call
扩展类型crates/goose/src/agents/extension.rsenum ExtensionConfig
检查器 trait/管理crates/goose/src/tool_inspection.rstrait ToolInspectorToolInspectionManagerapply_inspection_results_to_permissions
安全检查器crates/goose/src/security/security_inspector.rsSecurityInspector
egress 检查器crates/goose/src/security/egress_inspector.rsEgressInspector
权限检查器crates/goose/src/permission/permission_inspector.rsPermissionInspectordetect_read_only_tools
上下文压缩crates/goose/src/context_mgmt/mod.rscompact_messagescheck_if_compaction_needed
工具对摘要crates/goose/src/context_mgmt/mod.rsmaybe_summarize_tool_pairscompute_tool_call_cutoff
系统提示组装crates/goose/src/agents/prompt_manager.rsPromptManagerSystemPromptBuilder
子 agentcrates/goose/src/agents/subagent_handler.rsrun_subagent_task
CLI 消费流crates/goose-cli/src/session/mod.rs(reply 调用 + AgentEvent 匹配,约 :1188-1350)
CLI 入口crates/goose-cli/src/main.rsmain
Server 入口crates/goose-server/src/main.rsmain(二进制 goosed)

5.5 一句话收束

goose = 一个 Rust 写的、MCP-原生的通用 AI agent 运行时:核心是一个流式的「问模型→安检→跑工具→喂回」循环,工具全部插槽化(MCP),并在动手前串一条可插拔的安全/权限链;长任务靠阈值压缩、工具对摘要、可见性双轨和子 agent 撑住。读懂第 1 章的循环,其余都是挂在这条心跳上的护栏与续航装置。