父子 Agent 的沟通契约
XML 包装
一、原理:结构化数据怎么在 LLM 之间传递
5.3 章我们看到——子 agent 完成任务后输出一段 response。这段 response 被主 agent 看到、整合到主 agent 的工作。
但这里有个细节问题——子 agent 的 response 怎么"塞进"主 agent 的 messages 历史?
直观做法是——直接塞 raw text。子 agent 输出 "I found auth validation in src/auth/login.ts at line 45"——主 agent 看到这段文字。
但这有问题:
问题 1:主 agent 分不清来源
主 agent 在 messages 历史里看到这段文字——它怎么知道这是"子 agent 的回答"而不是"自己之前说过的话"或者"用户输入的内容"?
LLM 的 messages 历史只有 user / assistant 两种 role。子 agent 的 response 塞进去——按哪种 role?
塞 user role——主 agent 以为"用户在说这件事"。 塞 assistant role——主 agent 以为"我自己之前说过这件事"。
两种都错——子 agent 是个特殊来源,应该有明确标记。
问题 2:主 agent 不知道结果是"成功"还是"失败"
子 agent 跑了可能:
- 成功完成
- 部分完成
- 完全失败
- 正在跑(background mode)
主 agent 需要知道结果状态——决定怎么响应。
如果只是 raw text——主 agent 要靠语义判断("看起来是成功"),可能错。
问题 3:复杂结果难表达
子 agent 的结果可能包含多种信息:
- 找到的事实
- 给主 agent 的建议
- subagent session ID(如果想 resume)
- 警告或风险
把这些堆成 raw text——主 agent 难解析。
解决方案是——用结构化标记包装。子 agent 的 response 不直接塞进主 agent 的 messages,而是包装成一个明确的"特殊数据块"——标记来源、状态、内容、metadata。
最常用的包装格式是 XML——LLM 学过大量 XML 数据、识别标签很准确、容易扩展。
opencode 选了 XML 包装。我们看具体怎么做。
二、案例:opencode 的 <task> / <task_result> / <task_error> 包装
R5-B / R8-06 阶段的研究告诉我们——opencode 在 packages/opencode/src/tool/task.ts 里有个 renderOutput() 函数。它生成的包装格式大概是:
成功完成时:
<task id="sess_abc123xyz" state="completed">
<task_result>
{子 agent 返回的完整文本}
</task_result>
</task>
带摘要时:
<task id="sess_abc123xyz" state="completed">
<summary>找到 auth validation 在 3 个文件</summary>
<task_result>
{完整文本}
</task_result>
</task>
失败时:
<task id="sess_abc123xyz" state="error">
<task_error>
{错误信息}
</task_error>
</task>
Background 启动时(异步):
<task id="sess_abc123xyz" state="running">
<summary>Background task started</summary>
<task_result>
The task is working in the background. You will be notified automatically when it finishes.
DO NOT sleep, poll for progress, ask the task for status, or duplicate this task's work...
</task_result>
</task>
这段被以 user message role 塞进主 agent 的 messages 历史。主 agent 看到——明确知道:
- 这是 task 工具的输出(XML 标签)
- 这是哪次调用(id 字段)
- 状态是什么(state 字段)
- 内容是什么(task_result / task_error)
我们看几个具体设计细节。
细节 1:用 XML 而不是 JSON
为什么 XML 不 JSON?两个理由:
理由 a:LLM 对 XML 更友好
LLM 训练数据里 XML 跟自然文本混在一起——它学过的格式。看到 <task>...</task> 它自然识别"这是个特殊数据块"。
JSON 在 LLM 训练数据里更多用在"结构化输出"——LLM 看到 JSON 倾向认为"我应该生成 JSON",可能模仿。
XML 不会触发这种模仿——LLM 看到 XML 块知道是 metadata、不会去生成。
理由 b:XML 包含的文本可以是任何格式
<task_result>...</task_result> 里面可以是 markdown、可以是代码、可以是表格、可以是图——什么都行。
JSON 的字段值需要 escape——特殊字符(引号、换行)要转义。复杂内容很麻烦。
XML 不需要 escape(除了 < > &)——内容直接放进去。
细节 2:state 字段明确分类
completed / error / running 三个值——清楚。
LLM 看到 state="error" 知道是失败、state="running" 知道是异步、state="completed" 知道是成功。
不需要语义解读——直接判断。
细节 3:summary 是可选的摘要
如果子 agent 的 task_result 很长——加一个 summary 字段,主 agent 先看 summary 决定要不要看 detail。
这是 token 经济学的考量——主 agent 可能只看 summary 就够决策、不需要完整 result。
细节 4:id 让任务可追溯
每个 task 调用有 ID(其实就是 subagent 的 sessionId)。这个 ID 可以用作 task_id 参数 resume subagent。
主 agent 想"再问那个 expert 一个问题"——传相同 task_id 即可。
细节 5:Background 模式的特殊提示
Background 启动时,task_result 里包含一段告诉主 agent 怎么做:
"DO NOT sleep, poll for progress, ask the task for status, or duplicate this task's work — avoid working with the same files or topics it is using."
这段是给主 agent 的行为指引——不要傻等、不要重复做、避开 background 在做的事。
如果不加这段——主 agent 看到"任务在 running"可能犯傻("那我等一下吧")。明确指引避免这种。
细节 6:以 user role 塞回
虽然是 task 工具的输出——按 LLM API 协议应该是 tool result。但 opencode 把它包装成 XML 后塞进 user message。
为什么?因为 user message 是 LLM 最关注的——位置在 messages 末尾、attention 强。如果塞进 tool result——可能被 LLM 忽略中间段。
塞 user message + XML 标签——既明确语义又利用位置 attention。
三、设计启示:用 XML 暗示"这是特殊数据"
这一章的核心论点:LLM 之间的通信需要明确的"包装契约"——XML 是最适合的载体。
设计 multi-agent 通信时,下面几条原则有用:
1. 用 XML 标签包装跨 agent 数据
不要 raw text——LLM 分不清来源。
XML 标签让来源、类型、状态明确。
2. 标签命名有语义
<task> / <task_result> / <task_error> 比 <data> / <output> 强。
让 LLM 看标签就知道含义——不需要再读内容判断。
3. 用属性表达 metadata
<task id="..." state="..."> —— ID 和状态用属性,内容用子标签。
LLM 学过 XML——这种结构识别准确。
4. 包装数据放 user message
虽然是工具输出——但塞 user message 让 LLM 注意。
不要 tool result role——可能位置上被淡化。
5. 状态枚举要明确
成功 / 失败 / 异步 / 部分完成——每种用明确的 state 值。
不要"靠语义判断"——LLM 可能错。
6. 给主 agent 行为指引
不只塞数据——告诉主 agent "看到这个该做什么"。
特别是异步、失败、部分完成的场景。
7. summary 字段加进高频场景
如果跨 agent 的数据经常很长——加 summary 字段。
主 agent 先看 summary 决定要不要看完整 result。
8. 包装格式要稳定
一旦确定了 <task> 这种格式——不要随便改。LLM 学了你的格式(in-context learning),改格式让 LLM 困惑。
如果要升级——加新字段(向后兼容),不删旧字段。
最后一个观察。XML 包装这种LLM 之间的通信契约是 AI Agent 工程的"协议层"——跟 HTTP / RPC 在分布式系统里的角色类似。
设计协议有几个普适原则:
- 明确(标签、属性、值)
- 可扩展(加字段不破坏旧版本)
- 自描述(看到协议能理解含义)
- 容错(部分缺失也能 fallback)
XML 满足这些原则——LLM 之间的"分布式系统"用 XML 协议是个不错选择。
opencode 在 task 工具的输出上做了这种协议设计——细致、稳定、扩展性好。这是个值得抄的工程模板。
下一章 5.5 我们看 task 工具的另一个维度——Foreground vs Background。同步等待和异步通知是两种完全不同的产品形态。