前面两篇讨论了两个问题:什么情况下需要多个 Agent,以及任务执行过程中由谁决定下一步。
如果继续往前深入 Multi-Agent 的设计,需要解决一个新的问题:一个 Agent 完成工作后,应该把什么交给下一个 Agent?
最简单的方式,是把前一个 Agent 的回答直接放进下一个 Agent 的上下文。
Agent 数量少、任务简单时,这种方式通常还能工作。
随着任务变长,问题会逐渐显现:前一个 Agent 的结果是否已经完成?哪些结论已经验证?事实来源还能不能追溯?如果只完成了一部分,后续 Agent 应该继续、重试还是等待补充?
如果结果本身很大,后面的 Agent 是否还要把所有内容完整带进上下文?
这些问题最终都会落到同一件事上:Agent 之间需要明确的工作契约。
一、消息与接口的职责边界
讨论 Agent 之间的数据交接,首先要区分三个概念:
| 对象 | 作用 | 典型内容 |
|---|---|---|
| Message | 沟通 | 请求、说明、追问、进度 |
| Task | 表达一项工作 | 任务 ID、状态、生命周期 |
| Artifact | 保存任务成果 | 报告、代码、数据、图片、结构化结果 |
A2A(Agent2Agent Protocol)规范明确区分了 Message、Task 和 Artifact。
Task 表示一项有状态的工作,会经历自己的生命周期;Message 表示通信中的一次交互;Artifact 则表示任务执行过程中产生的结果。A2A 当前规范还明确建议,处于 completed 状态的 Task 应使用 Artifact 返回生成结果。
例如 Research Agent 完成研究后,可以形成这样的交接:
这里的三部分承担着不同职责。
research completed 是一条通知,告诉 Writer 研究阶段已经结束。
completed 是任务状态,程序可以据此知道这项工作已经进入完成状态。
真正供 Writer 使用的研究成果,则放在 ResearchBundle 这个 Artifact 中。
这种区分很重要。
Message 更适合承载交流过程中的信息,例如:
正在查询第二个数据源。
或者:
目前缺少 2025 年的数据,是否继续?
其价值主要体现在交互过程。
正式的研究报告、代码文件、结构化数据等结果,则更适合放入 Artifact。
此外,还有一个工程上的原因。
A2A 中的 Task.history 本身就是可选的,客户端获取 Task 时也不应假设所有历史消息都会被永久、完整保存。实时流式连接发生中断时,当前连接上的增量更新同样可能停止接收。相比之下,Artifact 属于 Task 的输出。只要服务端仍然保留对应 Task,客户端就可以重新获取任务,并读取其中已经生成的 Artifact。
上述三者的职责可概括为:
Message 负责交流过程
Task 负责工作生命周期
Artifact 负责工作成果
因此,需要继续处理、重复读取或者作为正式成果保存的数据,不应该只存在于聊天消息里。
二、Agent 之间的工作契约
假设系统中有这样一条流程:
Research → Writer → Reviewer
这里暂时只讨论 Research 和 Writer 之间如何交接。
一种常见写法是:
Research Agent:
我查到了这些资料……
Writer Agent:
请根据 Research Agent 的结果写文章。
这种设计的问题在于,Writer 还要重新解释上一段自然语言。
它需要自己判断:
哪些内容属于事实?
哪些属于 Research Agent 的分析?
哪些信息有可靠来源?
哪些问题还没有解决?
研究是否已经完整结束?
每经过一个 Agent,都要重新做一次语义判断。
Agent 越多,这些隐含信息越容易在传递过程中发生变化。
更稳定的方式,是让 Research 返回一个固定结构:
{
"schema_version": "1.0",
"task_id": "research-42",
"result_status": "completed",
"facts": [
{
"claim": "...",
"source_id": "src-7",
"confidence": 0.94
}
],
"sources": [
"src-7",
"src-9"
],
"open_questions": [
"..."
],
"artifact_refs": [
"artifact://research/raw-notes-42"
]
}
这里可以把整个对象定义为:
ResearchBundle
Bundle 表示一组已经组织好的相关数据。
Research 和 Writer 可以提前约定各个字段的含义。比如:
facts 保存可以交给后续阶段使用的事实。
sources 保存资料来源。
open_questions 记录当前没有解决的问题。
artifact_refs 指向完整研究笔记、原始数据或者其他体积较大的结果。
schema_version 则表示当前采用哪一版接口结构。
有了这些约定,Writer 不再需要从几千字自然语言中猜测哪些内容可以直接使用。
Agent 之间开始拥有类似普通软件模块之间的接口边界。
所谓 Agent 的工作契约,本质上就是提前约定:
我要给你什么?
每个字段是什么意思?
哪些字段必须存在?
出现异常时怎样表达?
哪些结果可以继续使用?
大数据放在哪里?
Prompt 解决 Agent 如何完成工作。
契约解决 Agent 如何把工作交出去。
三、结构化数据的适用边界
有了契约,并不意味着所有内容都应该变成严格字段。
实际开发中,更适合结构化的通常是程序需要直接判断的数据。
例如:
{
"result_status": "partial",
"error_type": "SOURCE_UNAVAILABLE",
"retryable": true
}
程序可能根据这些字段决定是否继续执行,因此状态最好使用固定值。
相比下面这句话:
资料好像没有完全找齐,之后可能需要再尝试一次。
结构化数据更容易直接在代码控制逻辑中使用。例如:
if result["result_status"] == "partial" and result["retryable"]:
retry_failed_sources()
这里不需要模型再次理解“好像没有完全找齐”到底代表什么。
类似的还有:
task_id
schema_version
source_id
artifact_id
timestamp
error_code
retryable
它们都比较适合采用固定格式。
自然语言则适合表达真正需要语义的信息:
{
"claim": "...",
"summary": "...",
"reason": "..."
}
例如 summary 很难通过枚举表示,因为它本身就是一段需要模型理解和生成的内容。
因此,我们记住一个原则:程序需要判断的内容尽量结构化,模型负责理解和生成的内容保留自然语言。
这和 Tool 的设计思路很接近。
例如一个搜索工具可以规定输入:
{
"type": "object",
"properties": {
"query": {
"type": "string"
},
"depth": {
"type": "string",
"enum": [
"quick",
"standard",
"deep"
]
}
},
"required": [
"query",
"depth"
]
}
这里:
required
表示必须提供哪些字段。
enum
限制字段只能采用指定值。
收到数据以后,程序可以按照 Schema 检查字段是否存在、类型是否正确、值是否合法。
Agent 之间的数据交接同样适合采用这种思路。
四、事实来源的追溯链路
Multi-Agent 系统还有一个很实际的问题:信息会被反复改写。
Research Agent 找到的数据可能是:
某项指标同比增长 37%。
Writer 为了让文章更自然,写成:
该指标出现明显增长。
Reviewer 又调整成:
该领域正在快速增长。
文字经过几次加工以后,原来的“同比增长 37%”已经变成了范围更大的判断。
这时再问:
这个结论来自哪里?
系统可能已经回答不出来。
因此,用于 Agent 协作的数据结构中,最好同时保存 Provenance。
Provenance 可以理解为数据的来源关系以及产生过程。
它主要解决几个问题:
这条信息来自哪里?
什么时候获取?
由哪个 Agent 产生?
对应哪一份原始材料?
例如:
{
"id": "claim-12",
"claim": "某项指标同比增长 37%",
"source_id": "src-7",
"source_uri": "https://example.com/report",
"retrieved_at": "2026-09-23T08:00:00Z",
"producer": "research-agent",
"confidence": 0.94
}
Writer 可以重新组织文字,但仍然保留:
claim-12 → src-7
Reviewer 对某个结论有疑问时,可以沿着 source_id 找回原始资料。
这样就不需要重新执行整轮 Research。
confidence 则需要谨慎处理。
如果 0.94 只是模型根据感觉生成的数字,就不能把它理解成统计意义上的“94% 正确概率”。
更合理的方式,是提前定义置信度规则,例如:
high
medium
low
或者根据来源数量、来源质量、交叉验证结果计算。
真正重要的并不是这个数字本身。重要的是:事实经过多个 Agent 以后,来源关系仍然存在。
五、异常状态的契约表达
真实系统里,一个 Agent 很可能只能完成部分工作。
Research Agent 计划查询 10 个来源,其中两个网站超时。
Coding Agent 完成了主要功能,但还有三个测试没有通过。
数据 Agent 缺少部分字段,只生成了当前能够计算的结果。
这时,仅仅使用:
success
failed
往往不够。
应用自己的结果契约可以进一步描述:
{
"result_status": "partial",
"retryable": true,
"missing_inputs": [],
"errors": [
{
"code": "SOURCE_TIMEOUT",
"source_id": "src-9"
}
],
"artifact_refs": [
"artifact://research/partial-result-42"
]
}
这里有一个概念需要区分。
partial 是我们自己的业务结果状态,表示:
已经产生有效结果,但结果并不完整。
它并不是 A2A 的标准 TaskState。
A2A 当前的任务状态包括:
submitted
working
input-required
auth-required
completed
failed
canceled
rejected
以及用于未知状态的表示。
其中:
working 表示任务仍在处理。
input-required 表示需要额外输入才能继续。
auth-required 表示需要完成认证。
completed 表示任务正常结束。
failed 表示任务因错误结束。
业务结果和协议状态属于两个层次,不需要强行做成同一套枚举。
例如:
{
"task_state": "completed",
"result_status": "partial"
}
可以表达:
任务本身已经结束,但最终只拿到了部分可用结果。
具体是否允许这种组合,应由应用自己的语义决定。
关键在于,部分完成、错误原因以及是否能够重试,都应该成为契约的一部分。
如果成功时返回 JSON:
{
"status": "completed"
}
失败时却只返回一句:
这次好像出了点问题。
后面的 Agent 依然很难稳定处理。
六、大型结果的引用传递
另一个容易被忽略的问题是上下文长度。
假设 Research Agent 产生了一万字研究资料:
Research
↓
10,000 字资料
↓
Writer
↓
10,000 字资料
+ 5,000 字草稿
↓
Reviewer
随着 Agent 数量增加,后面的上下文会越来越长。
如果每个 Agent 还继续总结前一个 Agent 的结果,又会出现另一种问题:
原始资料
↓
Research Summary
↓
Writer Summary
↓
Reviewer Summary
每经过一层,都可能损失一些信息。
更适合工程系统的方式,是把体积较大的结果保存起来,只传轻量引用。
例如完整研究报告可以放入:
文件系统
对象存储
数据库
Artifact Store
Agent 之间只传:
{
"artifact_id": "research-42",
"uri": "artifact://research/report-42",
"media_type": "text/markdown",
"sha256": "a83f...",
"schema_version": "1.0"
}
这里:
artifact_id 用于识别结果。
uri 表示内容在哪里。
media_type 表示数据格式。
sha256 可以用于确认读取到的内容是不是预期版本。
这样,无论 Artifact 是几 KB 的 Markdown,还是几百 MB 的数据文件,Agent 之间都只需要传递一个很小的引用对象。
还有一个直接收益:并不是所有 Agent 都需要读取完整结果。
例如:
Research
↓
ResearchBundle
├── facts
├── sources
└── raw_notes_ref
Writer 可能只读取 facts 和 sources。
只有发现信息不足时,才加载 raw_notes_ref 对应的完整笔记。
这比一开始就把所有内容塞进上下文更合理。
七、协作链路中的数据交接
现在重新看:
Research → Writer → Reviewer
Research 完成工作后,可以先形成:
Task
state = completed
同时产生:
ResearchBundle v1
例如:
{
"schema_version": "1.0",
"task_id": "research-42",
"result_status": "completed",
"facts": [
{
"id": "claim-1",
"claim": "...",
"source_id": "src-7"
}
],
"sources": [
"src-7",
"src-9"
],
"open_questions": [],
"artifact_refs": [
"artifact://research/raw-notes-42"
]
}
Writer 收到后,先检查:
schema_version 是否支持
必要字段是否存在
result_status 是否满足写作条件
Writer 自己的消费契约还可以规定:
只有带 source_id 的 facts 可以直接作为事实使用。
open_questions 中的信息仍然属于待确认内容,Writer 不能自行补全。
Writer 完成以后,再输出:
DraftArticle v1
Reviewer 读取 DraftArticle,以及必要的 ResearchBundle。
它不需要重新接收 Research 和 Writer 之间的所有聊天内容。
审校完成后可以返回:
{
"accepted_claims": [
"claim-1",
"claim-3"
],
"rejected_claims": [
"claim-2"
],
"required_changes": [
{
"claim_id": "claim-2",
"reason": "source does not support this conclusion"
}
]
}
三个 Agent 仍然可以通过 Message 交流。
但真正推动任务向前发展的,已经是一组明确的数据对象。
这时系统能够回答很多具体问题:
这一阶段是否完成?
正式结果在哪里?
这条事实是谁产生的?
来源是什么?
当前结果是否完整?
失败部分能不能重试?
现在使用哪个 Schema 版本?
如果 Agent 之间只传一段不断增长的 Conversation History,这些问题通常很难准确回答。
Task、Artifact、Schema 和 Provenance 的价值,就体现在这里。
八、LangGraph 中的契约实现
前面的设计并不依赖某一种 Agent 框架。
只要多个 Agent 之间存在长期、稳定的数据交接,都可以建立类似的契约。
放到 LangGraph 中,这种关系尤其直观。
LangGraph 的 StateGraph 围绕 State 运行:节点读取当前 State,完成处理以后返回需要更新的数据。
因此,可以直接把 Agent 之间的工作契约放进 State。
仍然使用这个示例:
Research → Writer → Reviewer
首先定义 Research Agent 的结果模型。
这里使用 Pydantic:
from typing import Literal, TypedDict
from pydantic import BaseModel, Field
class Fact(BaseModel):
id: str
claim: str
source_id: str
# 如果使用置信度,需要给它明确的业务定义
confidence: float | None = Field(
default=None,
ge=0,
le=1,
)
class ArtifactRef(BaseModel):
artifact_id: str
uri: str
media_type: str
sha256: str
class ResearchBundle(BaseModel):
schema_version: Literal["1.0"] = "1.0"
task_id: str
# 这是应用自己的结果状态,
# 不等同于 A2A TaskState
result_status: Literal[
"completed",
"partial",
"failed",
]
facts: list[Fact]
sources: list[str]
open_questions: list[str]
artifact_refs: list[ArtifactRef]
这里的 ResearchBundle 就是 Research 和 Writer 之间的接口。
它规定 Research 必须交出哪些内容。
工作流 State 可以继续定义为:
class WorkflowState(TypedDict, total=False):
research: dict
draft: str
review: dict
三个字段分别对应不同阶段的结果:
research
Research Agent 的结构化结果
draft
Writer Agent 生成的文章
review
Reviewer Agent 的审校结果
接下来实现 Research 节点:
def research_agent(state: WorkflowState):
bundle = ResearchBundle(
task_id="research-42",
result_status="completed",
facts=[
Fact(
id="claim-1",
claim=(
"A2A 将 Task、Message 和 Artifact "
"定义为不同的数据对象"
),
source_id="src-a2a-1",
confidence=0.95,
)
],
sources=[
"src-a2a-1"
],
open_questions=[],
artifact_refs=[
ArtifactRef(
artifact_id="raw-notes-42",
uri="artifact://research/raw-notes-42",
media_type="text/markdown",
sha256="a83f...",
)
],
)
return {
"research": bundle.model_dump()
}
这里发生了一个很重要的变化。
Research Agent 不再返回:
我研究完了,下面是完整结果……
然后附上一大段自然语言。
它返回的是一个符合 ResearchBundle 契约的数据对象。
完整研究笔记也没有直接放进 State。
State 中只保存:
artifact://research/raw-notes-42
这样的引用。
Writer 节点收到数据以后,可以先验证契约:
def writer_agent(state: WorkflowState):
research = ResearchBundle.model_validate(
state["research"]
)
if research.result_status != "completed":
raise ValueError(
"research result is not completed"
)
usable_facts = [
fact
for fact in research.facts
if fact.source_id
]
draft = "\n".join(
fact.claim
for fact in usable_facts
)
return {
"draft": draft
}
model_validate() 是 Pydantic 提供的模型验证方法。
它会检查输入是否符合 ResearchBundle。
例如:
task_id 是否存在
result_status 是否属于允许值
facts 是否符合 Fact 结构
confidence 是否处于允许范围
如果上游输出已经发生结构变化,可以在 Writer 执行主要逻辑之前发现。
Writer 还实现了一条自己的消费规则:
if fact.source_id
只有存在来源的事实才能进入后续处理。
原本只能写在 Prompt 里的约束:
请只使用有来源的信息。
现在有一部分已经进入程序逻辑。
Reviewer 也可以采用同样的方式:
def reviewer_agent(state: WorkflowState):
research = ResearchBundle.model_validate(
state["research"]
)
draft = state["draft"]
source_ids = {
fact.source_id
for fact in research.facts
}
return {
"review": {
"result_status": "completed",
"checked_sources": sorted(source_ids),
"draft_length": len(draft),
"required_changes": [],
}
}
最后建立 LangGraph:
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import InMemorySaver
builder = StateGraph(WorkflowState)
builder.add_node(
"research",
research_agent,
)
builder.add_node(
"writer",
writer_agent,
)
builder.add_node(
"reviewer",
reviewer_agent,
)
builder.add_edge(
START,
"research",
)
builder.add_edge(
"research",
"writer",
)
builder.add_edge(
"writer",
"reviewer",
)
builder.add_edge(
"reviewer",
END,
)
graph = builder.compile(
checkpointer=InMemorySaver()
)
调用时,再为这次工作流指定 thread_id:
result = graph.invoke(
{},
{
"configurable": {
"thread_id": "article-42"
}
},
)
LangGraph 的 Checkpointer 会保存工作流 State 的 checkpoint。
checkpoint 按 thread 组织,并在 graph 的 super-step 边界保存 State 快照,可用于状态恢复、Human-in-the-loop、调试和故障恢复。
示例使用 InMemorySaver,它把 checkpoint 放在内存里,进程重启以后数据会丢失。生产环境需要跨进程保存状态时,应使用 PostgreSQL、SQLite 等持久化 Checkpointer。
现在整个工作流变成:
Research Agent
│
│ ResearchBundle
▼
Writer Agent
│
│ Draft
▼
Reviewer Agent
│
│ ReviewResult
▼
END
节点之间讨论的问题也发生了变化。
不再只是:
上一个 Agent 说了什么?
而是:
上一个阶段产生了什么数据?
这些数据符合哪个 Schema?
哪些字段可以直接消费?
哪些结果仍然不完整?
大结果保存在哪里?
来源能不能追溯?
当前工作流能不能恢复?
这也是 LangGraph State 很适合承担的一项职责。
State 不必只存 Conversation History。
它同样可以成为多个 Agent 之间的数据接口。
不过,LangGraph State 和 A2A Artifact 仍然属于不同层次。
LangGraph State 解决的是:
一个工作流内部怎样传递和保存状态?
A2A 解决的是:
独立 Agent 或 Agent 服务之间怎样交换
Message、Task 和 Artifact?
如果 Research、Writer 和 Reviewer 都运行在同一个 LangGraph 应用中,可以直接通过 State 传递 ResearchBundle。
如果 Research Agent 是独立服务,而 Writer Agent 运行在另一套系统里,那么 ResearchBundle 可以作为 Artifact,通过 A2A 交给 Writer。
框架负责运行 Agent。
契约负责让 Agent 之间稳定交接工作。
总结
本文讨论了 Multi-Agent 系统中一个很基础、也很容易被忽略的问题:Agent 之间究竟应该如何交接数据。
Message 适合沟通和状态通知,Task 用来描述工作的生命周期,Artifact 承载正式结果;在此基础上,再通过 Schema 约束结构、Provenance 保留来源、Error Contract 表达异常,并用 Artifact Reference 传递较大的结果。
当这些信息形成明确的工作契约后,后续 Agent 就不必反复解释上一轮自然语言。系统也能更清楚地判断任务是否完成、结果是否可靠、错误能否恢复,以及数据是否可以继续使用。对于长期运行的 Multi-Agent 系统,这些接口设计和 Prompt 本身同样重要。
社区讨论
参与讨论
有问题或想法?欢迎继续讨论。