开场:一份越写越长的 AGENTS.md
第 17 篇写完项目记忆之后,我尝到了甜头,开始往 AGENTS.md 里加东西。
先是构建命令和踩过的坑,没问题,这些本来就该放在那里。然后是"发布新版本的步骤":打 tag、写 changelog、更新版本号、推送……十几行。然后是"新增一个接口要改哪些地方":路由、handler、参数校验、文档、测试……又是二十几行。然后是"排查线上问题的套路"、"写数据库迁移的规范"……
两周后,AGENTS.md 有四百行。每一次请求都带着它,每一圈都要为它付钱。而在大部分任务里,那些"发布步骤""迁移规范"根本用不上。我让它改个错别字,它的上下文里也躺着一份详细的发布流程。
更糟的是,它开始变笨了:第 16 篇说的 context rot 在一份四百行的记忆文件里同样成立,真正每次都要遵守的那几条约定,被淹没在了一堆流程说明中间。我想要的是:这些详细的操作说明,平时不要放进上下文,需要的时候再拿出来。 但"需要的时候"是什么时候?我不想每次手动告诉它"去读一下发布流程"。
这就是 skills 解决的问题:上下文里只放每个 skill 的名字和一句话描述,模型自己判断什么时候用得上,用得上的时候再去读完整的说明。 这一篇给 mini-agent 加上 skills,再把第 22 篇写的 MCP server 接进来。写完之后,一个能用的迷你 coding agent 就诞生了。读完本篇,你应该能回答这几个问题:
- 什么是"渐进式披露"?skills 是怎么实现它的?
- 一个 skill 长什么样?
- mini-agent 是怎么接入 MCP server 的?
- MCP 工具在权限系统里应该怎么对待?
- skill 和 MCP server 一样,为什么也要审查?
- 写到这里,mini-agent 一共有哪些能力?还缺什么?
一、Skills:渐进式披露
1.1 两层信息
渐进式披露(progressive disclosure)是界面设计里的一个老概念:不要一开始就把所有信息都摆出来,先给概要,用户需要时再展开细节。
skills 把这个思路用在了上下文上:
完整的发布流程"] end A1 -->|"模型判断:这次要发布
调用 load_skill(release)"| B1 style A fill:#e8f5e9,stroke:#2e7d32 style B fill:#fff9c4
- 第一层:名字和描述。所有 skill 的名字和一句话描述,放在 system prompt 里,每次请求都带着。每个 skill 只占几十个 token,有几十个 skill 也不会撑爆上下文;
- 第二层:完整的说明。模型读到描述,判断这次任务用得上,就调用一个
load_skill工具,把完整的说明读进来。
开场那份四百行的 AGENTS.md,就可以拆成:一份短的 AGENTS.md(构建命令、约定、坑),加上几个 skill(发布流程、新增接口、数据库迁移)。改错别字时,上下文里只有几行 skill 的描述;要发布新版本时,模型会自己去读发布流程。
1.2 一个 skill 长什么样
一个 skill 就是一个目录,里面有一份 SKILL.md。文件开头是一段 frontmatter,写名字和描述;后面是正文,写详细的操作说明:
---
name: release
description: 发布新版本时使用:打 tag、写 changelog、更新版本号
---
# 发布流程
1. 确认 main 分支的 CI 是绿的:`gh run list --branch main --limit 1`
2. 更新 `version.go` 里的版本号
3. 在 CHANGELOG.md 顶部加一节,列出这个版本的改动(从上一个 tag 到现在的 git log)
4. 提交:`git commit -am "release: vX.Y.Z"`
5. 打 tag:`git tag vX.Y.Z`
6. 推送:先 `git push`,再 `git push --tags`
注意:不要跳过第 1 步。上次有人在 CI 红着的时候发布,回滚花了一个小时。目录里还可以放别的文件:脚本、模板、示例。正文里可以引用它们,模型读到说明后,会用 read_file 或 run_bash 去用这些文件。
mini-agent 会扫描两个地方的 skills:
- 项目级:工作目录下的
.agents/skills/*/SKILL.md,跟着项目走,提交到 git,团队共享; - 用户级:你家目录下的
~/.agents/skills/*/SKILL.md,你自己的通用技能,所有项目都能用。
同名的 skill,项目级的优先。
(说个小插曲:我第一次在自己的电脑上启动 mini-agent,启动信息里赫然写着"skills 13 个",而项目里一个 skill 都没有。原来我家目录下的 ~/.agents/skills/ 里,早就躺着之前别的工具装进去的十几个 skill,mini-agent 照单全收了。用户级的 skill 对所有项目都生效,这既是方便,也是要留意的地方。)
1.3 实现
agent/skills/skills.go 一共一百多行,做三件事。
加载: 扫描目录,解析每一份 SKILL.md 的 frontmatter:
// Load 扫描若干目录下的 */SKILL.md。同名 skill 以先出现的为准(项目级优先于用户级)。
func Load(dirs ...string) ([]Skill, error) {解析时有一个硬性检查:
if s.Description == "" {
return Skill{}, fmt.Errorf("%s 的 frontmatter 缺少 description——没有描述,模型就不知道什么时候该用它", path)
}description 是 skill 的灵魂。 模型完全是凭这一句话判断要不要加载它的。没有描述的 skill,等于不存在,所以直接报错,而不是悄悄地忽略。
放进 system prompt 的那一段: 只有名字和描述:
// PromptSection 是放进 system prompt 的那一小段:只有名字和描述。
func PromptSection(list []Skill) string {
// ……
sb.WriteString("<skills>\n下面是可用的技能包。当任务和某个技能的描述相符时,先调用 load_skill 读取它的完整说明,再按说明操作。\n")
for _, s := range list {
fmt.Fprintf(&sb, "- %s:%s\n", s.Name, s.Description)
}
sb.WriteString("</skills>")
return sb.String()
}第 24 篇讲过,system prompt 由几段拼成:基础 prompt → 项目记忆 → skills 列表 → 环境信息。skills 列表放在项目记忆之后、环境信息之前,它也是比较稳定的内容。
load_skill 工具: 按名字返回完整的正文:
func (t Tool) Run(_ context.Context, input json.RawMessage) (string, error) {
// ……
for _, s := range t.Skills {
if s.Name == in.Name {
return fmt.Sprintf("<skill name=%q dir=%q>\n%s\n</skill>", s.Name, s.Dir, s.Body), nil
}
}
// 找不到:列出所有可用的 skill
return "", fmt.Errorf("没有名为 %q 的技能。可用的有:%s", in.Name, strings.Join(names, ", "))
}返回的内容里带着 skill 所在的目录 dir,这样正文里引用"同目录下的 template.md"时,模型知道去哪里找。工具定义的参数描述里,还会列出所有 skill 的名字作为可选值。
- release:发布新版本时使用 U->>M: 帮我发布 v1.2.0 M->>L: load_skill(release) L-->>M: 完整的发布流程 M->>M: 按流程:查 CI、改版本号、写 changelog……
1.4 加载一个 skill,也要问你
第一次在 default 模式下用 skill,你可能会有点意外:模型调用 load_skill 时,终端里弹出了一句"允许调用 load_skill 吗?"。
原因在第 27 篇的权限系统里:只读工具的名单里只有 read_file 和 list_files 两个,其他不认识的工具,在 default 模式下一律要问。load_skill 不在名单里,第 30 篇的 task 也不在。选一次 a,本次会话就不会再问了。
要不要把 load_skill 加进只读名单?从功能上看,它确实只读:它只是把一份磁盘上的说明文档返回给模型。但下一节会说到,skill 的内容本身也值得审视。问一次,至少让你知道"它刚刚加载了哪个 skill"。这是一个安全和便利之间的取舍,mini-agent 选了保守的那一边。
1.5 skill 也是一段会进上下文的文字
skill 的正文,会作为 load_skill 的结果原样进入上下文,模型会照着它去做。这意味着:一个 skill,就是一段你预先交给模型的指令。
项目级的 skill 跟着仓库走,改动有 git 记录、有人审查,问题不大。用户级的就要小心一点:1.2 节那个"skills 13 个"的小插曲说明,别的工具可能会往 ~/.agents/skills/ 里装东西,而你未必读过它们。网上分享的 skill 也一样:它的正文可能写着"执行这个脚本",而那个脚本你从没打开过。
所以对待 skill,要像对待第 23 篇的 MCP server 一样:装之前读一遍,尤其是它让模型去运行的那些命令和脚本。 好在 skill 是一份 Markdown,读一遍只要几分钟,比审查一个 MCP server 的代码容易得多。
1.6 skills 越来越多之后
渐进式披露让几十个 skill 也不会撑爆上下文,但它并没有让问题消失,只是换了个形式:
- 描述的总量还是会涨:每个 skill 一行描述,一百个 skill 就是一百行,它们每次请求都在;
- 描述会互相打架:一个"新增接口时使用",另一个"修改 API 时使用",模型面对一个"给接口加个参数"的任务,该加载哪个?第 20 篇说过工具功能重叠的问题,skill 也一样;
- 模型可能不加载:描述写得不够贴切,模型觉得"这事我自己会",就直接上手干了,没有去读你精心写好的流程。
应对的办法和工具设计一样:描述写清楚"什么时候用",功能相近的合并,用不上的删掉。还有一个简单的检验办法:拿几个典型的任务跑一下,看看模型有没有在该加载的时候加载、不该加载的时候不加载。
1.7 skills、项目记忆、MCP 怎么分工
到这里,我们有了三种给 Agent "加知识、加能力"的方式,它们的分工是:
| 放什么 | 什么时候进上下文 | |
|---|---|---|
| 项目记忆(第 17 篇) | 每个任务都需要的:构建命令、约定、坑 | 一直在 |
| skills | 某些任务才需要的:操作流程、规范、套路 | 描述一直在,正文用到时才加载 |
| MCP(第 21-23 篇) | 新的动作:查工单、发消息、收藏书签 | 工具定义一直在 |
第 23 篇最后说"什么时候不需要 MCP:一个 CLI 加一段说明往往就够了"。那段"说明"放在哪里?放进一个 skill 正合适:描述写"需要收藏网址时使用",正文写 bm add 和 bm search 的用法。平时只占一行,用到时才展开,比 MCP 那种"所有工具定义每次都带"省得多。
二、MCP 接入
2.1 配置
第 22 篇写了一个书签 MCP server,第 21 篇手写了一个 MCP 客户端。这一节把它们接进 mini-agent。
mini-agent 读取一个 .mcp.json 格式的配置文件,默认是工作目录下的 .mcp.json,可以用 -mcp 参数指定:
{
"mcpServers": {
"bookmark": {
"command": "/tmp/mcp-bookmark",
"args": ["-db", "/tmp/bookmarks.json"]
}
}
}这是很多 Agent 工具都认的格式,同一份配置可以在不同的工具里共用。mini-agent 只支持 stdio 类型的 server(本地子进程),远程 HTTP 类型的会被跳过并给出提示。
2.2 连接
agent/mcp/mcp.go 的 Connect,对配置里的每一个 server:启动子进程、握手、列出工具。
// Connect 启动配置里的所有 server,握手并拿到工具列表。某个 server 失败不影响其他的。
func Connect(ctx context.Context, cfg *Config, warn func(string)) []*Server {注意那句"某个 server 失败不影响其他的"。你接了三个 server,其中一个的路径写错了,不应该让整个 mini-agent 都启动不了。失败的那个打一行警告,其余的照常连接。
用的就是第 21 篇那个两百来行的 internal/mcpclient:Start 启动子进程,Initialize 握手,ListTools 拿工具列表。
2.3 把 MCP 工具包装成 mini-agent 的工具
关键的一步:每一个 MCP 工具,被包装成一个实现了 tools.Tool 接口的 remoteTool,注册进 mini-agent 的工具注册表:
// remoteTool 把一个 MCP 工具包装成 mini-agent 的 tools.Tool。
type remoteTool struct {
server string
client *mcpclient.Client
def mcpclient.Tool
}
func (t *remoteTool) Definition() llm.Tool {
// ……
return llm.Tool{
Name: ToolName(t.server, t.def.Name),
Description: fmt.Sprintf("[来自 MCP server %s] %s", t.server, t.def.Description),
InputSchema: schema,
}
}
func (t *remoteTool) Run(ctx context.Context, input json.RawMessage) (string, error) {
res, err := t.client.CallTool(ctx, t.def.Name, input)
if err != nil {
return "", fmt.Errorf("MCP 调用失败:%w", err)
}
if res.IsError {
return "", errors.New(res.Text())
}
return res.Text(), nil
}Definition 把 MCP 的工具定义翻译成模型 API 的格式(第 21 篇说过,几乎就是改个字段名);Run 把调用转发成 MCP 的 tools/call。第 21 篇区分过两种错误:协议错误(err)和工具执行出错(res.IsError),在这里都变成了 Run 返回的 error,再由注册表包装成 is_error: true 的 tool_result 交给模型。
第 25 篇把工具设计成接口的回报,在这里兑现了:对注册表、对 Agent 循环来说,remoteTool 和 read_file 没有任何区别。接入 MCP,Agent 的核心代码一行都没改。
mcp__bookmark__add_bookmark"| R["Registry"] R --> RT["remoteTool.Run"] RT -->|"tools/call add_bookmark"| C["mcpclient"] C |"JSON-RPC over stdio"| S["mcp-bookmark 子进程"] style RT fill:#fff3e0,stroke:#e65100
2.4 工具名:mcp__server__tool
MCP 工具注册进 mini-agent 时,名字被改成了 mcp__<server>__<tool> 的形式:
// ToolName 生成 mcp__<server>__<tool> 形式的名字:一眼能看出工具来自哪个 server,也避免和内置工具重名。
func ToolName(server, tool string) string {
n := "mcp__" + invalidChars.ReplaceAllString(server, "_") + "__" + invalidChars.ReplaceAllString(tool, "_")
if len(n) > 64 {
n = n[:64]
}
return n
}三个原因:
- 避免重名:两个 server 都有一个叫
search的工具,或者某个 server 的工具也叫read_file,加上前缀就不会冲突; - 一眼看出来源:模型和用户看到
mcp__bookmark__add_bookmark,就知道这是书签 server 的工具。描述前面也加了[来自 MCP server bookmark]; - 方便权限系统识别:下面马上讲。
名字里不合法的字符替换成下划线,总长度限制在 64 个字符以内,因为很多模型 API 对工具名的格式和长度有限制。
2.5 MCP 工具的权限
第 23 篇讲过,MCP server 是第三方的代码,它的工具描述可能被投毒,它的返回结果也可能藏着指令。所以在权限系统里,MCP 工具被当成"不可信"的来源:
- 它们不在只读白名单里:默认模式下,每次调用 MCP 工具都要用户确认(第 27 篇)。哪怕 server 自己声称这个工具是只读的(第 22 篇的工具注解),mini-agent 也不信;
- 调用过 MCP 工具之后,上下文就被"污染"了:之后任何可能把数据发到外部的操作,都必须由人逐次确认。这是第 38 篇"致命三件套"防护的一部分,在 agent/permission/permission.go 里,所有
mcp__开头的工具,既算"不可信的来源",也算"可能往外发数据的操作"。
mcp__ 这个前缀,让权限系统只用一行 strings.HasPrefix 就能识别出所有的 MCP 工具。
这两条叠在一起,会产生一个你在实验里马上就能看到的效果。第一次调用 MCP 工具时,它和其他工具一样问你,你可以选 a;但这一次调用一完成,上下文就被标记成"被污染了",而 MCP 工具本身又算"可能往外发数据的操作"。于是从第二次开始,每一次 MCP 调用都会走"逐次确认"的那条路,之前选过的 a 不再管用,提示前面还多了一个 ⚠。
这不是 bug,是有意的:第 23 篇的投毒演示里,危险的恰恰是"第二次调用",即模型在读了一个工具的结果之后,把数据塞进另一个工具的参数里。每一次都让人看一眼参数,是代价最低的防线。代价是用 MCP 工具时要多按几次 y,如果这个 server 是你自己写的、完全可信,可以用 -no-guard 关掉这道防护,但第 38 篇会告诉你,为什么最好不要。
2.6 几个没有处理的边角
MCP 接入这部分代码不长,有几种情况它是刻意没有处理的:
- 名字截断后可能撞车:
ToolName把名字截到 64 个字符。如果一个 server 有两个工具,名字前面很长一段都一样,截断之后可能变成同一个名字,而注册表的Add是按名字存的,后注册的会悄悄覆盖先注册的。实际中很少遇到,但如果你接的 server 工具名特别长,留意一下启动时的工具数量对不对; - server 中途崩了,不会自动重连:子进程退出之后,之后每次调用它的工具,都会得到一个"MCP 调用失败"的错误。模型看到错误,通常会换个办法或者告诉你;要恢复,只能重启 mini-agent;
- 只支持 stdio:远程的 HTTP server 会被跳过。第 23 篇说过,远程 server 要处理鉴权,这超出了一个教学用客户端的范围;
- server 的 instructions 没有用:第 21 篇 6.1 节说过,握手时 server 返回的使用说明,mini-agent 读到了,但没有放进 system prompt。
这些都不影响"接一个本地的、可信的 MCP server 来用"这个最核心的场景。如果你要把 mini-agent 用在更复杂的环境里,它们就是你的改进清单。
三、组装:main.go
把所有东西组装起来,是 cmd/mini-agent/main.go 的事。按顺序:
reg := tools.NewRegistry(
tools.ReadFile{WS: ws},
tools.ListFiles{WS: ws},
tools.EditFile{WS: ws},
tools.RunBash{WS: ws},
tools.FetchURL{}, // 第 38 篇
)
reg.MaxOutput = *maxOutput
// 第 31 篇:skills。system prompt 里只放名字和描述,需要时 load_skill 读全文。
home, _ := os.UserHomeDir()
skillList, err := skills.Load(filepath.Join(wd, ".agents", "skills"), filepath.Join(home, ".agents", "skills"))
// ……
if len(skillList) > 0 {
reg.Add(skills.Tool{Skills: skillList})
}
// 第 31 篇:MCP。配置里的每个 server 的工具都注册成 mcp__<server>__<tool>。
mcpCfg, err := mcp.LoadConfig(*mcpConfig)
// ……
servers = mcp.Connect(context.Background(), mcpCfg, func(w string) { fmt.Fprintln(os.Stderr, w) })
defer mcp.Close(servers)
for _, s := range servers {
for _, t := range s.Tools {
reg.Add(t)
}
}
// ……
system := agent.SystemPrompt(wd, agent.ProjectMemory(wd), skills.PromptSection(skillList))
a := agent.New(model, system, reg, ui)内置工具、skill 工具、MCP 工具,全部加进同一个注册表。system prompt 由基础 prompt、项目记忆、skills 列表、环境信息拼成。没有 skill 时不注册 load_skill,没有 MCP 配置时不连任何 server。
defer mcp.Close(servers):mini-agent 退出时,关掉所有 MCP server 的子进程,不留下孤儿进程。
四、里程碑:一个能用的迷你 coding agent
写到这里,第五部分的八篇就完成了。回头看看 mini-agent 现在有什么:
| 能力 | 实现 | 篇目 |
|---|---|---|
| 循环 | Agent.Send:调模型 → 跑工具 → 喂结果,直到模型说完成 | 24、25 |
| 工具 | read_file、list_files、edit_file、run_bash、task、load_skill、MCP 工具 | 25-27、30、31 |
| 权限 | 三种模式、白名单、黑名单、人工确认、限制工作目录 | 25、27 |
| 上下文 | 输出截断、token 预算、自动压缩、子 Agent 隔离 | 28、30 |
| 稳定性 | 流式输出、指数退避重试、截断与中断处理 | 24、29 |
| 扩展 | 项目记忆、skills、MCP | 17、31 |
截断 / 预算 / 压缩"] A --> L["llm:流式 + 重试
DeepSeek / 演示模型"] style A fill:#fff3e0,stroke:#e65100
它能读一个陌生的项目,能定位 bug,能改代码,能跑测试验证,能在上下文快满时自己压缩,能派出分身去调研,能按需加载操作手册,能用别人写的 MCP 工具。所有测试都不需要 API Key:
go test ./agent/... ./internal/...当然,它和 Claude Code、Codex 这样的成品还有很大的差距:没有计划模式,没有待办清单,没有 IDE 集成,没有操作系统级的沙箱,工具也少得多。下一部分我们就去拆解成品,看看这些差距在哪里,以及哪些"最佳实践"其实是从设计里自然推出来的。
这八篇里,我们还顺手攒下了一份"已知的不足",每一条都被写成了某一篇的练习:
| 不足 | 在哪一篇 |
|---|---|
| 按 Ctrl+C 显示成"错误" | 第 24 篇坑 7 |
edit_file 返回的片段可能找错位置 | 第 26 篇 6.1 节 |
run_bash 继承全部环境变量,echo 能打印密钥 | 第 27 篇 1.6 节 |
| 只有整段压缩,没有"只清旧工具结果" | 第 28 篇 4.6 节 |
| 没有空闲超时、没有上下文超长后的自动恢复 | 第 29 篇 2.7、2.8 节 |
子 Agent 有 run_bash,能改文件 | 第 30 篇 2.5 节 |
| MCP 的 instructions 没用上、server 崩了不重连 | 第 21 篇 6.1 节、本篇 2.6 节 |
一个"能用"的 Agent,和一个"没有已知问题"的 Agent,是两回事。 成品 Agent 也一样有它们的已知问题,只是你看不到它们的代码。自己写过一遍,你至少知道该从哪里开始怀疑。
五、动手
要看模型自己决定加载 skill、调用 MCP 工具,需要接真实模型:先 export DEEPSEEK_API_KEY=sk-...。MCP 要启动子进程,在线 Playground 演示不了。实验 1:写一个 skill
在配套仓库的根目录(或者你自己的项目里),新建一个 skill:
mkdir -p .agents/skills/add-example
cat > .agents/skills/add-example/SKILL.md <<'EOF'
---
name: add-example
description: 给某一篇文章新增配套实验时使用
---
# 新增实验
1. 目录名是 examples/chNN-<主题>,NN 是两位数的篇号;
2. 里面放一个 main.go 和一个 README.md;main.go 开头的注释写清楚怎么运行;
3. 如果实验不需要子进程、git、键盘输入,在 web/experiments.js 里加一项,让它能在 Playground 上跑;
4. 改完跑 go vet ./... 和 bash web/build.sh。
EOF启动 mini-agent,看启动信息里的 skills 数量。然后说:"给第 41 篇加一个配套实验,演示一下 prompt caching 的命中率。"看看它是不是先调用了 load_skill,再按说明去做。
(.agents/ 这个目录不想提交的话,记得加进 .gitignore。)
实验 2:接入书签 server
go -C mcp-bookmark build -o /tmp/mcp-bookmark . # 独立模块,用 -C 进它的目录编译
go run ./cmd/mini-agent -mcp agent/mcp/example.mcp.json启动信息里会显示"MCP server 1 个",工具数量也多了 3 个。说:"把 https://go.dev/blog 收藏一下,打上 go 标签。"留意两件事:
- 它调用的工具名是
mcp__bookmark__add_bookmark; - 调用之前,mini-agent 问你"允许调用 mcp__bookmark__add_bookmark 吗?"。MCP 工具每次都要确认。
实验 3:用 skill 代替 MCP
按 1.7 节表格下面那段的思路,试试"CLI 加 skill"这条路:
- 假装你有一个命令行工具
bm(不用真的写,或者用一个打印参数的小脚本代替); - 新建一个 skill,描述写"需要收藏网址或查找已收藏的网址时使用",正文写
bm add <url> --tag <标签>和bm search <关键词>的用法; - 不带
-mcp参数启动 mini-agent,说同样的"把 https://go.dev/blog 收藏一下"。
对比实验 2:启动时的工具数量、每次请求的工具定义大小、模型调用的是 load_skill 加 run_bash,还是 mcp__bookmark__add_bookmark。第 23 篇说过,对只给自己用的工具,后一种往往不是必须的。
实验 4:跑测试
go test ./agent/skills ./agent/mcp -vTestLoadAndProgressiveDisclosure 检查了:项目级的 skill 覆盖了用户级的同名 skill;system prompt 里只有名字和描述,没有正文;load_skill 能返回正文;找不到时会列出可用的 skill。TestMissingDescriptionIsAnError 检查了缺少描述时会报错。
六、踩坑记录:扩展能力的六个坑
坑 1:skill 的描述写得太含糊。
"一些有用的发布相关内容",模型不知道什么时候该加载它。描述要写清楚"什么时候用":发布新版本时使用、新增接口时使用。描述是模型唯一的判断依据。
坑 2:把该放项目记忆的东西放进了 skill。
构建命令、测试命令,几乎每个任务都要用,放进 skill,模型每次都要先加载一次,多绕一圈。每次都需要的,放项目记忆;某些任务才需要的,放 skill。
坑 3:一个 MCP server 连不上,整个 Agent 起不来。
某个 server 的路径写错了,或者它启动时崩了。连接失败要隔离:打一行警告,其他的照常工作。
坑 4:MCP 工具名和内置工具撞了。
某个 server 也有一个 read_file,注册进来,把内置的覆盖了,行为完全变了还不知道。加上 mcp__<server>__ 前缀。
坑 5:信任 MCP server 自己声称的"只读"。
给它的工具开了免确认。第 23 篇的投毒演示,就是利用这一点。对第三方的工具,按"它可能做任何事"来对待。
坑 6:用户级目录里装了一堆没读过的 skill。
别的工具、网上分享的合集,往 ~/.agents/skills/ 里一装就是十几个。它们的描述每次都在你的上下文里,正文随时可能被加载、被照着执行。装之前读一遍,不用的删掉。
一句话总结
skills 是渐进式披露:system prompt 里只放每个 skill 的名字和一句话描述,模型判断用得上时再用 load_skill 加载完整说明,描述是它唯一的判断依据;MCP 接入是把每个远程工具包装成一个 remoteTool,名字加上 mcp__server__ 前缀,权限上按不可信来源对待。至此,一个能读、能改、能跑、能管上下文、能派分身、能扩展的迷你 coding agent 诞生了。
名字 + 描述 → 按需加载正文"] --> R["工具注册表"] M["MCP server
→ mcp__server__tool"] --> R B["内置工具"] --> R R --> A["mini-agent
里程碑达成"] style A fill:#fff3e0,stroke:#e65100
评论
还没有评论,来说点什么吧。