子 Agent 不知道自己是子 Agent
一个有意的设计
一、原理:自我意识对 LLM 输出的影响
LLM 在生成响应时,它的"自我认知"会影响输出。这是个不为人知但有真实影响的现象。
LLM 学了大量"AI 助手"的训练数据——里面有"我是一个 AI 助手"、"作为一个语言模型"、"我没有意识"这类陈述。这些数据让 LLM 形成了"自我身份"的概念——它知道自己是 AI、是在被使用、是要帮助用户。
但这种"自我认知"是多层的:
- 顶层:我是一个 AI
- 中层:我是一个 coding agent
- 中层:我在被 opencode 这个产品使用
- 中层:我是 opencode 的子 agent,被主 agent 派生来做特定任务
- 底层:我是 Claude 3.5 Sonnet 模型实例
每多一层"自我认知",LLM 的输出会被附加一层心智状态。
具体说——如果 LLM 知道自己是"subagent",它会:
- 偏向简短回答(subagent 应该聚焦)
- 偏向保守(subagent 不应该越界)
- 偏向返回结构化结果(subagent 要被整合)
- 偏向等待父 agent 指示(subagent 是被动的)
听起来这些都是好事——subagent 就该这样。但实际中这种"自我感知"经常带来副作用:
副作用 1:子 agent 过度保守
子 agent 知道"我是 subagent,不能越界"——结果它做事缩手缩脚。该探索的不探索、该建议的不建议——觉得"这超出我职责"。
但其实主 agent 期待它做这些事——只是没明确说。
副作用 2:子 agent 想"协调"
子 agent 知道有"主 agent"——它会想"我应该等主 agent 指示吗?"、"主 agent 要这个答案做什么?"
这种"对父子关系的思考"耗费 LLM 注意力——本来该聚焦任务,结果想协调。
副作用 3:子 agent 假装"汇报"
子 agent 知道"我要返回结果给主 agent"——它的输出语气变成"汇报式":
- "我已完成探索"
- "我发现以下事实"
- "希望对你有帮助"
这种汇报式输出有大量"元话术"——浪费 token,主 agent 也不需要。
把这 3 个副作用合起来——告诉 LLM "你是 subagent" 降低了它的工作质量。
opencode 的解决方案是反直觉的——让子 agent 不知道自己是子 agent。
子 agent 的 system prompt 里完全没有 "you are a subagent"、"you were spawned by another agent"、"please return result to your parent" 这类话。
子 agent 看自己的 system prompt——感觉就像一个独立的 agent——有完整的任务、需要做完它、需要给用户输出结果。
它不知道"输出会被另一个 LLM 看到"——它以为输出是给用户的。
这种"隐瞒身份"让子 agent 像独立 agent 一样工作——不缩手缩脚、不元思考、不汇报式输出。
听起来欺骗 LLM 似乎不对。但实际效果很好——subagent 输出质量高、token 消耗低、行为可预测。
我们看 opencode 怎么实现。
二、案例:opencode 子 agent 的 system prompt 完全没有 "you are a subagent"
打开 packages/opencode/src/session/system.ts,看 subagent 的 system prompt 拼装:
function buildSubagentSystemPrompt(agentType: string, model: Model, ctx: Context) {
return [
// 1. 选 base prompt(agent 类型决定)
...(agentPrompts[agentType] || SystemPrompt.provider(model)),
// 2. AGENTS.md / CLAUDE.md
...instruction.system(),
// 3. 环境信息
SystemPrompt.environment(ctx),
]
}
注意——没有任何 "you are a subagent"。subagent 看到的 system prompt 跟主 agent 几乎一样:
- 5 张脸之一(或者 explore 等专门 prompt)
- AGENTS.md 项目规则
- 环境信息
子 agent 看自己的 prompt——感觉就是个独立 agent。
它收到的"任务"——是主 agent 通过 task 工具传的 prompt 参数。这个 prompt 是主 agent 写的"具体任务描述"——比如:
"Look at the auth implementation in src/auth/ and find any password validation logic. Return the file paths and line numbers where validation happens."
子 agent 看到这个 prompt——以为是用户给的任务(user message)。它聚焦做这件事,最后输出。
子 agent 的输出会被主 agent 看到——但子 agent 不知道这件事。它以为输出是给用户的。
这种"隐瞒"让子 agent 的输出自然、聚焦、不啰嗦——它没有元思考"主 agent 要这个做什么",只关心"用户要这件事,我把它做好"。
我们看 task.ts 里子 agent spawn 的代码:
// 简化版
async function spawnSubagent(agentType: string, prompt: string, ctx: Context) {
// 创建新 session(fresh context)
const subSession = await createSubSession(agentType, ctx)
// 把 prompt 作为 user message 塞进去
await subSession.addUserMessage(prompt)
// 启动 subagent 的 runLoop
const result = await runLoop(subSession)
// 提取 subagent 的最终回复
return extractFinalResponse(result)
}
注意几个细节:
细节 1:subagent 有完整的 session
它不是"轻量函数调用"——它有自己的 session、自己的 messages 历史、自己的 runLoop(可以多轮 LLM 调用)。
这是必要的——subagent 可能要做多步骤(先 read 一个文件、再 grep、再 read 另一个文件、再总结)。它需要自己跑一个完整的 multi-turn 流程。
细节 2:parent session 的信息不传递
subSession 不继承 parent 的 messages、不知道 parent 在做什么、不知道 parent 是谁。它的 context 是 fresh。
唯一的"传递"是主 agent 写的 prompt 参数——但那是作为 user message 传,子 agent 以为是用户说的。
细节 3:subagent 的输出走 runLoop 的正常路径
子 agent 输出最终 response 时,从它视角看就是"我回答了用户"。runLoop 检测到 LLM 不再调用工具、给了最终回复——退出循环。
opencode 在这个时刻提取最终 response 作为"task tool 的返回结果"——塞回主 session。
子 agent 不知道自己的输出会被处理成"task 结果"——它以为输出已经给用户了。
细节 4:metadata 在系统层
opencode 在系统层有 metadata:
sessionId这个 subagent session 的 IDparentSessionId主 session 的 ID
这些是系统跟踪信息——给 opencode 自己管理用、给用户在 UI 上看 multi-agent 结构用。
但这些 metadata 不进入 LLM 的 context。LLM 看不到 parentSessionId——它不知道自己是被另一个 session spawn 的。
这种"系统层 metadata + LLM 层无感知"的设计——让 opencode 自己能管理 multi-agent,但 LLM 不被这些信息分心。
三、设计启示:信息隔离的微妙处
这一章的核心论点:告诉 LLM 的越少越好——LLM 不知道的事不会干扰它的工作。
设计 multi-agent 系统时,关于"子 agent 知道什么",下面几条原则有用:
1. 默认隐瞒身份
子 agent 不需要知道自己是子 agent。它的 system prompt 跟独立 agent 一样——只是任务不同。
只在确实需要时才告诉它。
2. 主 agent 的 context 不传递给子 agent
主 agent 的对话历史、主任务的全貌——这些不传给子 agent。子 agent 只看到"被分配的具体任务"。
理由:
- 信息隔离让子 agent 聚焦
- 主 agent 的 context 巨大,传过去吃 token
- 主 agent 的对话风格可能跟子 agent 不匹配
3. 用 user message 形式塞 task prompt
主 agent 给子 agent 的 task prompt——以 user message 形式塞进子 agent 的 messages。
子 agent 看到的是"用户给我的任务"——而不是"主 agent 给我的指令"。这种伪装让子 agent 自然工作。
4. 子 agent 的输出按正常 runLoop 处理
不要给子 agent 一个"special 返回机制"——让它按正常 runLoop 跑、给最终 response。
主 agent 在系统层提取 response 作为 task 结果——子 agent 不感知。
5. metadata 在系统层,不进 LLM context
parentSessionId、subagentType 这些 metadata——系统跟踪用、UI 显示用——不放进 LLM 的 system prompt。
LLM 不需要知道这些。
6. 命名也要"隐藏"
如果你的 task 工具叫 "spawn_subagent"——LLM 看到这个名字会想"我在 spawn 子 agent"——元思考。
改成 "task" 或 "delegate" 或 "ask_expert"——名字暗示功能但不暴露内部结构。
opencode 的 task 工具叫 task——中性。
7. 不要在子 agent 的 prompt 里说"你的输出会回到主 agent"
如果说了——子 agent 会调整语气(汇报式)、调整内容(节选 highlights)——结果反而不自然。
让子 agent 以为"输出给用户"——它会按完整、清晰、有用的方式写。
8. 测试"隐瞒" vs "明告"的输出质量
如果你不确定哪种好——A/B 测:一组子 agent 的 system prompt 加 "you are a subagent"、一组不加。
跑 50 个任务,对比输出质量。opencode 团队大概率做过这种测试——结论是"不加"更好。
最后一个观察。"让 LLM 不知道自己被框架"——这是 AI 产品工程里深层的设计哲学。
我们已经在 4.7 章看过——LLM 不应该知道自己被压缩、被监控、被限制资源。这一章是同一个原则的另一个体现——LLM 不应该知道自己是子 agent。
共同的判断是——LLM 在"它觉得自己是独立 agent"的状态下工作质量最高。一旦它感知"我是被系统管理的"——质量就下降。
opencode 团队反复实践这个判断——结果是产品体验明显好。子 agent 输出自然、主 agent 整合顺畅、用户感觉"multi-agent 协作流畅"。
这种"隐瞒身份"看起来欺骗 LLM——但产品上是对用户负责。用户拿到的是更好的输出——他不需要知道内部是"主 agent + 假装独立的 subagent"。
下一章 5.4 我们看主 agent 跟子 agent 怎么沟通——XML 包装是这个沟通的契约。