扩展系统的哲学:每一个扩展都在能力与复杂度之间做交易
这是《写一个 Coding Agent Harness and TUI 》系列的最终篇。 前面 7 篇搭好了一个功能完备的 coding agent。最后一篇加上 6 个扩展—— 但重点不是"怎么加",是"加之前要回答哪个问题"。
一个能用的 agent 和一个好用的 agent 之间,隔着这 6 个扩展。
但"加扩展"不是"加功能"。“加功能"是你知道需要什么,你知道它怎么工作,你只需要实现。“加扩展"是你在跟复杂度做交易——每个扩展在给你新能力的同时,也在拿走一些东西。Skills 给你可复用知识,同时让你的 system prompt 更长。Subagent 给你并行执行,同时让你的 agent 需要学会"判断什么时候 spawn 值得”。Memory 给你跨会话记忆,同时让你的 agent 需要学会"判断什么值得记住”。
这篇不是"6 个扩展的实现文档"。这篇是"6 个交易的分析"——每个扩展的交易内容、交易代价、以及什么情况下这个交易是划算的。
〇、元问题:扩展的边际成本——为什么"能做就做"是错误的
扩展不是免费的
你加一个 Skills 系统。agent 的 system prompt 里现在多了每个 skill 的内容——从 200 token 增加到 800 token。这 600 token 是每一轮 LLM 调用的固定开销。一次对话可能有 10 轮——600 × 10 = 6000 额外 token。一个月的使用——6000 × 30 × 20 次对话 ≈ 360 万额外 token。
token 是成本,是延迟,是注意力稀释。 Skills 给你"agent 自动知道你的 coding 规范"的能力。代价是每个 session、每轮对话都消耗额外 token。这个交易划不划算,取决于你是否有足够多的 skil 让 600 token 的开销产生超值回报。
判断标准:不用这个扩展,agent 能不能完成当前任务
- 用户每次都要手写同样的 coding 规范 → 加 Ski ls(一次定义,长期复用)
- 单任务耗时超过 30 秒且步骤可并立 → 加 Subagent(并行替换串行)
- agent 需要访问你没有写的数据库或 API → 加 MCP(外部工具协议的现成生态)
- 用户反复纠正 agent 同样的偏好问题 → 加 Memory(自动化偏好记忆)
- 需要定时执行、CI 集成 → 加 Hooks + Sched ler(从被动到主动)
**反过来:**如果用户只有一两个简单偏好,Memory 的存储和检索成本 > 收益——在 system prompt 里手写一条规则更便宜。如果所有 ML 流程都是串行依赖的——Subagent 不仅不能加速,还引入了不必要的通信开销。
扩展的边际收益递减,边际成本递增。 第 1 个扩展(Skills)收益最高——因为它解决了"agent 不知道我的规范"这个最高频的痛点。第 6 个扩展(Scheduler)收益最低——因为它只在特定场景下产生价值。
一、Skills:为什么是 Markdown——不是格式选择,是创作者群体的选择
两种 Skills 系统
代码插件:每个 Skill 是一个 Rust crate 或 .so。实现 trait Skill { fn modify_prompt(&self, base: &str) -> String; }。编译、加载、管理版本。
Markdown 文件:每个 Skill 是一个 .md 文件。YAML frontmatter 定义触发条件,正文是注入 system prompt 的文本。
代码插件为什么是错误的——不是技术上,是生态上
技术上说,代码插件更好。它可以做任意复杂的逻辑——不只是文本注入,还可以修改工具列表、添加自定义 tool、修改 Turn Loop 行为。它有编译期保证——不会因为一个 typo 导致 agent 行为异常。
但代码插件有一个不可逾越的门槛:只有程序员能写。
一个团队 lead 想写一个"代码审查 skill"——他应该是这个 domain 的专家,他理解什么代码需要审查、审查标准是什么、常见问题模式是什么。但他可能不懂 Rust,不知道 trait 怎么写,不清楚编译和加载流程。代码插件的门槛直接排除了他——这个最有资格定义 skill 的人。
Skills 系统的设计目标不是"能做什么"——是"谁能为 agent 定义能力"。 如果只有写代码的人能扩展 agent 的能力,agent 的能力边界受限于程序员的数量。如果任何一个团队成员都能写——就像写文档一样——agent 的能力边界受限于团队集体知识的增长速度。
Markdown 的可访问性 = Skills 系统的可扩展性。不是技术原因。是人的原因。
grok-build 的实现
grok-build 的 skills 是 Markdown 文件 + YAML frontmatter。conditional activation——只在特定项目(project: "my-crate")或特定文件类型(file_pattern: "*.rs")时加载。不是"所有 skill 全部注入"——是"只注入当前上下文相关的 skill"。这就是前面说的"边际成本控制"——600 token 的开销通过按需加载降到了 200 token。
grok-build:
tools/src/implementations/skills/
二、Subagent:独立上下文 vs 共享上下文——不只是一个实现选择
共享上下文的诱惑
父 agent 的对话历史里有所有之前累积的信息:项目结构、用户偏好、之前工具调用的结果。子 agent 启动时直接继承这些信息——不需要额外传参,不需要重复查询。
这是"方便",不是"正确"。 共享上下文的问题不是性能——是信息污染。
一个具体场景
父 agent 正在处理用户的任务:“我之前让你帮我重构了数据库 layer,现在帮我看看 API 层的调用是否一致。”
在"重构数据库 layer"那个 Turn 里,工具调用了 8 次——read_file(db.rs)、search_replace(3 次)、cargo check(2 次)、cargo test(2 次)。这些工具调用的结果都在对话历史里。LLM 需要这些信息来判断"数据库 layer 被重构成了什么样子"。
但现在父 agent 决定 spawn 一个子 agent 去"check API layer"。如果子 agent 共享上下文——它会看到所有数据库重构的日志。它需要"忽略"这些不相关的 8 轮工具调用,专注于 API layer 的问题。
LLM 不擅长"选择性忽略"。 给它的上下文中 80% 是无关信息、20% 是任务信息——它的注意力被稀释到那 80% 上,它对 20% 的专注度下降。这是 transformer 注意力机制的本质限制——self-attention 是全局的,不能"跳过"不相关的 region。
独立上下文的代价和收益
独立上下文 = 子 agent 收到的是一条干净的任务 goal + 弹小的 system prompt。没有父 agent 的历史包袱。子 agent 聚焦在当前任务的范围内。
代价:子 agent 不能直接看到父 agent 之前的工具调用结果。但这可以通过显式传参弥补——父 agent 在 scatter goal 里包含必要的上下文信息(“database layer 已经被重构为使用 sqlx 0.8,连接池配置在 db.rs:45-67”)。
显式传参 > 共享上下文。因为显式传参是你主动选择的"子 agent 需要知道的"。共享上下文是子 agent 被动接受到的——里面 80% 是它不该知道的。 信息的选择性传递 = agent 的"职权范围"。
grok-build:
agent/src/prompt/subagent_prompts.rs
三、MCP:为什么是 stdio transport——而不是"因为 Anthropic 选的"
MCP 的 transport 选择
| stdio | SSE/HTTP | |
|---|---|---|
| 启动方式 | agent spawn MCP server 为子进程 | MCP server 独立运行,agent 通过 HTTP 连接 |
| 进程生命周期 | 和 agent 绑定——agent 退出,server 退出 | 独立——agent 重启不影响 server |
| 发现机制 | agent 启动时 spawn → 通过 stdin/stdout 通信 | agent 启动时连接 URL → 通过 HTTP/SSE 通信 |
| 部署复杂度 | 零——子进程自动管理 | 需要端口管理 + server 进程管理 |
为什么选 stdio——不是因为技术优势,是因为"简单"
MCP server 的生命周期绑定 agent 的生命周期。这是正确的——MCP server 是 agent 的工具池的一部分。 agent 重启了,工具池就应该重建。不需要面临"上次 agent 连接的 MCP server 还在运行吗?端口被占了吗?版本一致吗?“的问题。
HTTP/SSE 的优势在远程 MCP server 场景。 多个 agent 实例共享一个 MCP server;MCP server 在独立服务器上,有独立的 scaling 策略。本机 coding agent 场景没有这些需求——MCP server 是本地工具(数据库客户端、浏览器控制、文件管理器),不需要远程部署。
grok-build:
shell/src/session/mcp_servers/
四、Memory:SQLite 的选择——不是"数据库 vs JSON 文件”
访问模式决定了存储格式
Memory 的访问模式不是"读整份数据"——是"查询相关记忆"。
用户说过"我喜欢 2 空格缩进"。这条记忆的 key 是 user_preference:indent。下次会话开始时,agent 需要"加载所有 user_preference 开头的记忆"。
用 JSON 文件: 每次会话开始 → 读整个 memory.json → 解析 → 遍历所有条目 → 过滤 key 前缀 → 注入 system prompt。
用 SQLite: SELECT value FROM memories WHERE key LIKE 'user_preference:%' → 返回相关记忆。
300 条记忆:JSON 方案读 300 条 → 过滤 → 可能只有 5 条相关。SQLite 方案只读 5 条。
这里又回到了第 4 篇的信噪比原则。 给 LLM 的 system prompt 里注入 300 条记忆(其中 295 条跟当前会话无关)是噪音。注入 5 条相关记忆是信号。SQLite 支持按 key 前缀精确查询 = 让注入 LLM 上下文的记忆是信号而不是噪音。
更重要的:并发写入
agent 在 Turn Loop 里积累新记忆(“用户这次用 clippy 而不是 cargo check”)。同时,会话开始时 agent 在加载已有记忆。SQLite 的 WAL(Write-Ahead Logging)模式天然支持一写多读并发——Turn Loop 在写,会话初始化在读,不会锁表。
JSON 文件没有并发控制。Turn Loop 写文件和会话初始化读文件可能竞态——读到一半写入的文件 = 损坏的 JSON → 解析失败 → 所有记忆丢失。
grok-build:
tools/src/types/memory_backend.rs
五、Hooks 和 Scheduler——从被动 agent 到主动 agent 的关键一跃
Hooks 为什么是外部脚本
Hooks 在 agent 的生命周期事件(pre-turn、post-tool、post-turn)上挂载自定义逻辑。外部脚本 vs 内置 Rust 函数——选择脚本的理由与 Skills 选 Markdown 一致:降低创作者门槛。
一个 shell 脚本:
#!/bin/bash
# post-tool hook: tool 执行后如果失败,自动记录到日志
if [ "$TOOL_EXIT_CODE" != "0" ]; then
echo "[$(date)] TOOL FAILED: $TOOL_NAME (exit $TOOL_EXIT_CODE)" >> tool_errors.log
fi
同样功能用 Rust trait 实现:需要建 crate、写 impl、编译、重启 agent。复杂度门槛决定扩展生态的活跃度。
Scheduler 为什么不是 cron
Scheduler 让 agent 在后台定时执行任务。系统 cron 可以触发任何命令——包括 agent run "check for updates"。
但系统 cron 的 stdout 只是一段文本——你不知道怎么把它注入 agent 的上下文。 Agent 内置的 Scheduler 在任务触发时直接作为一条"系统通知"加入对话历史——就像用户在会话中发了一条消息。Agent 看到这条消息,用自己的推理引擎决定怎么处理。
这是 ReAct 模型从"被动"到"主动"的关键跃迁。被动 agent 需要用户触发。主动 agent 可以自己触发自己——定时检查代码质量、定时跑测试、定时提醒 review PR。
六、最终组装:7 条原则融会贯通
从 200 行到 ~1760 行,从零到一个完整的终端 AI 编程代理:
Part 1: LLM 调用层 (200行) — 元问题:确定性代码 vs 概率系统
Part 2: 工具系统 (120行) — 元问题:LLM 的信息通道宽度决定你的设计上限
Part 3: Agent 循环 (90行) — 元问题:控制权转移后,代码变成护栏
Part 4: 文件编辑 (200行) — 元问题:fail-safe vs best-effort — 工具不替 LLM 决策
Part 5: TUI 界面 (350行) — 元问题:透明度是信任的基础设施
Part 6: 通讯协议 (300行) — 元问题:agent 的状态不属于 UI,属于持久的 Agent Server
Part 7: 安全体系 (150行) — 元问题:有效拦截率 > 拦截数量;安全是"定义人机信任边界"
Part 8: 扩展系统 (350行) — 元问题:能力与复杂度交易;扩展的边际收益递减
──────────────────────────────────────
总计: ~1760 行代码
+ 8 个元问题的深度分析
+ 30+ 个"方案 A 被淘汰,选方案 B"的决策推演
7 条核心原则
| # | 原则 | 来自 |
|---|---|---|
| 1 | LLM 是推理引擎,不是函数——它的输出是决策 | 第 1 篇 |
| 2 | 工具是 LLM 的 UI——通道宽度决定 agent 正确调用率的上限 | 第 2 篇 |
| 3 | Agent 的控制流是 Observe→Reason→Act→Observe——你设置护栏,不是编排路径 | 第 3 篇 |
| 4 | 文件编辑遵循 fail-safe——“拒绝并告诉原因"比"帮你猜"更安全 | 第 4 篇 |
| 5 | TUI 是透明度仪表盘——用户对 agent 的信任 = 理解它的过程 | 第 5 篇 |
| 6 | Agent 的生命周期 > 交互界面——它是一个持久的服务,不是一次性程序 | 第 6 篇 |
| 7 | 安全 = 定义人机信任的动态边界——“什么条件下信任 agent 做什么” | 第 7 篇 |
你学会了 1760 行 Rust。你也学会了在这些 Rust 代码背后的 30+ 个设计决策中,每一个为什么选了这条路而不是那条路。 下一次你面对一个新的架构问题——不管是 coding agent 还是任何其他系统——这套分析方法可以直接复用。
grok-build 的 3 万多行 Rust,不过是这 7 条原则的工程化放大。你现在能读懂它了。
完整代码在 tutorial-code/part-08-advanced/,cargo run 即用。
📬 系列完结
你花了 8 篇文章,从 200 行到 1760 行,写出了一个完整的终端 Coding Agent。
| 下一步 | 内容 | 扫码获取 |
|---|---|---|
| 🎓 进阶课程 | 视频讲解 + 完整源码(含测试、CI、Docker)+ 1v1 代码走读 | 回复"agent进阶” |
| 🏗️ 企业咨询 | 技术选型、安全方案、落地路线图 | 回复"企业咨询" |
| 💬 社群交流 | 微信学习群——和同样在写 Agent 的开发者交流 | 回复"agent"获取本系列 PDF |
📮 扫码关注「全栈之巅 - 梦兽编程」,回复"agent"获取本系列 PDF 合集 + 全部源码打包。
关于作者
梦兽编程,专注于 Rust 系统编程与 AI Agent 架构设计。曾深度逆向分析 grok-build、Claude Code、OpenAI Codex 等开源 Agent 项目的实现,将三万多行 Rust 代码提炼为可复用的工程原则。
公众号每周推送一篇深度技术文章——不讲"怎么做",只讲"为什么这么做"。

