全部课程
亲手改一个 Agent/ 改造实录③ · 派一个分身:安全靠围栏/ 改造实录③:派一个分身——安全靠围栏,不靠盯梢 实录
改造实录③ · 派一个分身:安全靠围栏

改造实录③:派一个分身——安全靠围栏,不靠盯梢

上一集把判断"交出去",这一集把活"派出去"。给你的 agent 装上委派能力:把一块活丢给一个独立的子 agent 去干,它在自己的进程里跑、干完只把结论交回来。重点不在"能派",在"派了怎么还安全"——答案是能力围栏:不靠每一步拦着问,靠事前只给它够用的工具。你会亲眼看到同一句"删掉这个文件",派侦察兵它做不到、派执行工它真删了。这一集不写代码,只写两个配置文件。

上一集我们把判断交出去(handoff);这一集把活派出去(subagent)。这俩是两种不同的委托——交出去是控制权转移,派出去是你始终主导、分身干完把结论交回来。忘了的话回看课②那张对比图。

这一集给你的 agent 装上委派能力。但重点不在”能派”,在一个更要紧的问题上:

让一个 agent 自主替你跑,怎么还不让它瞎搞?

答案就是这一集的主心骨——能力围栏(capability fence):不靠每一步拦着问,靠事前只给它够用的工具。

而且好消息:这一集不写一行代码,只写两个配置文件。 pi 官方自带一个 subagent 扩展,我们原样用,一个字都不改 pi。


一、派个分身出去:它在自己的进程里跑

pi 官方的 subagent 扩展,让主 agent 把一块活派给一个独立的子 agent——独立进程、独立上下文,干完只把结论交回来。

我派一个叫 scout 的分身,去读 pi 自己那份 792 行的 agent-loop.ts、总结它的结构:

派 scout 读 agent-loop.ts:subagent scout 在独立进程里读完文件,回流一段结构化摘要(主循环、双循环 runLoop、工具执行管线等),状态行显示 2 turns ↑11k ↓1.4k ctx:12k

它干得漂亮。但最值得看的是那行账:↑11k ↓1.4k——

scout 往上吃了 11k token(那 792 行文件,是在它自己进程里读的),往下只吐回 1.4k(一段摘要)。那 11k 的原文,没进你的主会话。

而且这不是”涨得少”——分身交回来的结果里根本没带 token 账,所以它花的那 11k,连你主会话的用量统计都进不去。隔离是结构性的,不是”碰巧省”。

这就是 subagent 最实在的价值:不是”更聪明”,是”更省 context”。 读一堆文件、翻一堆日志这种又脏又占地方的活,丢给分身在它自己的上下文里干完,你的主线只拿回一小段结论——主会话不被中间过程撑爆。


二、围栏是真的:scout 删不掉文件

现在见真章。我派同一个 scout,去删一个文件(先 touch /tmp/试验品.txt 造个试验品,别拿真文件练手):

派 scout 删 /tmp/试验品.txt:scout 回报"我只有只读工具(read/grep/find/ls),没有删除文件的能力,做不到",并建议在有写权限的会话里执行 rm

我只有只读工具(read/grep/find/ls),没有删除文件的能力,做不到。

这一句,是全课最该说透的地方:

它不是”被拦下来了”,是手里压根没有能删文件的刀。 scout 的围栏是 read, grep, find, ls——没有 bash、没有 edit、没有 write。 事前给得少,事中就不需要拦。

这就是”能力围栏”和”人来把关(HITL)“的根本差别:

人来把关(HITL)能力围栏
时机事中,每个危险动作停下问你事前,压根不给它那把刀
靠什么挡住你在旁边点”拒绝”它手里没有那个工具
适合你盯着的单会话自主委派、无人值守

三、围栏不是”一律只读”,是”按任务给刀”

要是分身只能读,那写代码的活谁干?围栏的正确含义是最小权限——给这个任务够用的工具,不多给一件。 读的活给读的刀,写的活就得给写的刀。

我另建了一个叫 worker 的分身,它的档案故意不写 tools 字段。同一句”删掉 /tmp/试验品.txt”,这回派 worker:

派 worker 删 /tmp/试验品.txt:worker 用 rm 命令真删了,回报"已删除,未改动其他文件",复核确认文件已不存在

真删了。 同一个扩展、同一句话,只是换了个档案——

证明围栏是”按任务给刀”,不是”subagent 天生只读”。

worker 那个”不写 tools”是刻意的:没声明 tools,扩展就不传 --tools,子进程拿到默认全套(read, bash, edit, write)。写代码的活,本来就该给 write。 围栏的松紧,跟着任务走。


四、最要紧的一层:谁定围栏,比围栏能锁多细更根本

到这儿你可能想:那让模型委派时自己挑工具,不就行了?

恰恰不行。 如果”给哪些工具”是模型自己能填的参数,那不叫围栏,叫自助餐——它想删文件,自己给自己配把刀就是了。

官方的做法很讲究:围栏写在人维护的 agent 档案里,模型只能选”用哪个 agent”,改不了围栏本身。

# ~/.pi/agent/agents/scout.md —— 这是人写的,模型碰不到
---
name: scout
description: 只读侦察兵。读文件、搜代码、列目录,产出摘要。不做任何修改。
tools: read, grep, find, ls        ← 围栏在这儿,人定的
---
你是一个只读侦察兵……

模型委派时,只能说”派 scout”或”派 worker”——它选笼子,你造笼子。 这就是这一课最该带走的一句:

围栏能锁多细,不是终点。终点是——这把锁,谁能改。

(诚实一句:围栏管到”工具”这一层。再往下的硬隔离——把整个文件系统、网络都关起来——是沙箱那一层的事,那是另一集。)


动手做:就两个配置文件

这一集的”改造”,不是改代码,是写两个 agent 档案给你的分身划围栏:

📦 想自己跑一遍? 两个档案(scout.md 只读 / worker.md 可写)+ 完整照跑步骤,都在配套练习仓库: github.com/pkm365/lucid-learn-agent 官方 subagent 原样 -e 加载,不用装依赖、不改一行 pi。

⚠️ 一个一定会踩的坑:agent 档案的 frontmatter 缺 name 或 description 会被静默跳过——不报错、不警告,只表现为”没有这个 agent”。问一句”有哪些可用的 agent?”要是列不全,先回去检查这两个字段。

三段教学坐标(不用读那 1141 行,命根子就三处;基线 pi 936aff009,认符号名别认行号):

讲什么位置重点
注册成工具pi.registerTool({ name: "subagent" … })tools 不在参数里——模型只能选 agent,改不了围栏
spawn 时围栏怎么焊上args 组装 → getPiInvocation → spawn--tools 的值来自 agent 档案的 frontmatter,不来自模型
结果怎么回流getFinalOutput → 返回 { content, details }子进程吐事件流 → 抽最终文本 → 变一条 toolResult 进父账本

带走这一句

给 agent 装”委派”很容易,难的是让它自主又不失控。答案是能力围栏:把活派给一个独立的子 agent,而它能干什么,由你事前在 agent 档案里划定——只读的侦察兵删不掉文件,不是被拦住,是手里没那把刀;要写的活,就派给带写权限的执行工。围栏不是”一律只读”,是”按任务给刀”;而最要紧的不是围栏锁多细,是围栏由谁定、谁能改。分身还顺手帮你省 context——脏活在它自己进程里干完,主线只拿回一段结论。

下一集

围栏管到”工具”这一层——它能挡住”没给的工具”,挡不住更底层的东西(比如一个有写权限的分身,在你没预期的目录里写)。要把整个环境都关进笼子——文件系统只读、网络切断——那是沙箱那一层。下一集我们就看 pi 怎么把危险动作关进一个 OS 级的笼子里跑。