手敲三Agent串行流水线,我发现了多Agent协作的隐形杀手
做个实验:让三个AI Agent串行协作(主编写稿→审稿评分→排版润色),你觉得最难的部分是什么?
不是模型能力不够,不是工具调用失败,不是权限控制太复杂------最难的是怎么让Agent之间传递信息既不丢重点,又不让垃圾撑爆上下文。
我上周用LangChain4j跑三Agent代码审查流水线时就踩过这个坑:CodeReviewer输出1800字分析报告,塞进变量传给SecurityScanner,直接504超时------token爆炸了。这周我手敲了一个纯Python版的三Agent串行Workflow,从BaseAgent基类设计到Workflow编排器,从JSON结构化传递到上下文膨胀控制,跑通之后提炼出7条设计原则。
这篇文章把实战过程完整摊开:先讲多Agent协作的核心矛盾(上下文膨胀),再看四大生产级Agent架构怎么解决信息传递问题,最后展示我的手敲代码和踩坑经验。3个结论先放这:
- Agent间传递用JSON比自由文本好------结构化、精简、可解析,1800字全文→200字JSON,token消耗降90%
- 每个Agent必须上下文隔离------只看自己的system_prompt+本次输入,不继承上一个Agent的对话历史
- 上下文膨胀的本质是"信息粒度不对等"------Agent A输出的是"我想说什么",Agent B需要的是"我需要知道什么",两者不是一回事
多Agent协作的核心矛盾:信息传递不是"发快递"
很多教程讲多Agent协作,都停留在"让Agent之间对话"这个层面。但多Agent协作真正的问题不是"能不能对话",是"对话的内容到底是不是对方想要的"。
打个比方:你让三个同事串行写文章------主编写完初稿,把全文甩给审稿人;审稿人看完,甩了一份1800字审稿报告给排版师。排版师拿到的是什么?初稿全文+1800字审稿报告,3000+字的输入砸脸上。
但排版师真正需要啥?原文、评分、修改建议,就这三样。审稿报告里那些论证过程、背景分析、补充说明------对排版师来说全都是噪音。
说白了就是"粒度不对":上一个Agent说的是"我想说什么"(完整思考过程),下一个Agent要的是"我需要知道什么"(关键结论)。把"我想说什么"原样甩过去,上下文就膨胀了------一堆没用的token占满LLM窗口,推理质量直线下降,甚至直接超时。
我在Week5用LangChain4j跑代码审查流水线时,亲历了这个问题的严重性:
- CodeReviewer输出1800字代码分析报告
- 这1800字塞进{{codeReview}}变量传给SecurityScanner
- SecurityScanner的prompt突然膨胀到3000+token
- 直接504超时,LLM推理时间暴涨
解决方案贼简单:让Reviewer输出JSON,别写自由文本。{"score": 7, "issues": "开头没有核心结论", "suggestions": "前200字重写"}------200字搞定,token消耗直接降90%。
这不是我独创的,四大生产级Agent架构都在用不同方式解决这同一个问题。
四大架构怎么解决信息传递问题
我拆解了四个生产级Agent(OpenClaw、Hermes、Claude Code、Codex),发现它们解决"信息传递"的思路各有不同,但目标一致:只传必要的,不传全部的。
OpenClaw:compaction压缩历史
OpenClaw的解决方案是上下文压缩。聊天20轮session历史爆满了?别原样塞给LLM------用compaction把历史消息压成summary,只留最近几轮完整对话。
这思路搬到多Agent协作:别把Agent A的整个对话历史甩给Agent B,只传"Agent A的结论"就行。我的三Agent流水线里,Formatter拿的是"评分+问题+建议"的精简摘要,不是Reviewer的全文审稿报告。
Java直觉:compaction就是日志聚合------别把每条日志原样存,按时间段聚合成摘要就行。Agent间传递同理,别把Agent完整输出原样甩过去,只传聚合后的关键信息。
Hermes:硬性容量管理
Hermes更狠------直接给记忆设硬上限。MEMORY.md最多2200字符/800token,USER.md最多1375字符/500token。不是"能塞多少塞多少",是"只留最重要的"。挺吓人的,800token就是MEMORY.md的全部容量------一个稍微长点的结论就塞满了。
搬到多Agent协作就是:每个Agent的输出也得有容量上限。Reviewer输出3个字段JSON,不是1800字自由文本。这不是"少写点",是"只写下一个Agent需要的"。
Hermes还有个"nudge"机制------Agent会定期提醒自己写笔记,不是被动记日志,是主动挑重要的写。搬到多Agent协作:Agent得主动筛自己的输出,别把中间过程全甩给下一个。Reviewer的职责是"为下一个Agent提炼决策关键信息",不是"记录我的完整思考过程"。
Java直觉:这就像DTO(Data Transfer Object)的设计原则------微服务之间传递DTO,不是传整个Entity。DTO只包含对方需要的字段,Entity包含所有字段。Reviewer的JSON输出就是DTO,1800字自由文本就是Entity。
Claude Code:分层存储
Claude Code的记忆是分层的:CLAUDE.md(用户写的持久指令)+ Auto memory(Agent自己攒的笔记)。CLAUDE.md是"我告诉Agent必须遵守的规矩",Auto memory是"Agent从经验中自己学到的偏好"。
搬到多Agent协作:Agent间传递的信息也得分层。Formatter要的是原文+修改建议,不需要Reviewer的论证过程和思考链。_merge_for_formatter()只提取JSON里的score/issues/suggestions三个字段,其余全扔掉。
Codex:远程压缩
Codex的compact_remote挺有意思------压缩操作可以甩给远程服务器干,不用在本地压。这意味着压缩可以异步、可以分布式、可以按需触发。
搬到多Agent协作的启示:Agent间信息传递也可以异步按需。我现在是同步串行------editor写完立即给reviewer,reviewer审完立即给formatter。但compact_remote提示了另一种玩法:审稿意见太长?先压缩再传;多个Agent需要同一段信息?共享一个压缩后的摘要,别各自拿全文。
四大架构对比总结
| 架构 | 信息传递策略 | 核心思路 | Java直觉 |
|---|---|---|---|
| OpenClaw | compaction压缩 | 只传摘要,不传全文 | 日志聚合 |
| Hermes | 硬性容量管理+nudge策展 | 只存必要的,主动提炼 | DTO vs Entity |
| Claude Code | 分层存储 | 显式知识+隐式知识分离 | 配置文件 vs 运行时数据 |
| Codex | 远程压缩 | 异步按需压缩 | 异步日志处理 |
共同原则:只传必要的,不传全部的。具体实现各有不同,但目标一致------防止上下文膨胀。
手敲三Agent串行流水线:从BaseAgent到Workflow编排
理论看完了,现在看代码。我手敲了一个完整的多Agent Workflow系统,从BaseAgent基类到三Agent定义到Workflow编排器,一共约120行代码。每个设计决策都来自上面的架构对比分析。
BaseAgent基类:Agent不是LLM
很多人把Agent和LLM搞混。Agent不是LLM,Agent = system_prompt + 输入格式 + 输出格式 + LLM客户端。LLM是引擎,Agent是驾驶员------不同驾驶员开同一辆车走不同的路。
python
class BaseAgent:
"""Agent基类 --- 独立prompt + 输入输出约定 + LLM调用"""
def __init__(
self,
name: str,
system_prompt: str,
llm: LLMClient,
output_format: str = "text", # text | json
output_keys: list[str] = None, # JSON模式时的字段名
max_tokens: int = 800,
):
self.name = name
self.system_prompt = system_prompt
self.llm = llm
self.output_format = output_format
self.output_keys = output_keys or []
self.max_tokens = max_tokens
def invoke(self, input_data: str) -> str:
"""调用Agent --- 组装messages→LLM推理→解析输出"""
messages = [
{"role": "system", "content": self.system_prompt},
{"role": "user", "content": input_data},
]
# JSON模式:追加格式约束
if self.output_format == "json" and self.output_keys:
messages[0]["content"] += f"\n\n{self._format_instruction()}"
# LLM推理
response_text = self.llm.chat(messages, max_tokens=self.max_tokens)
# JSON模式:提取结构化输出
if self.output_format == "json":
return self._extract_json(response_text)
return response_text
这个设计有几个关键决策值得展开讲:
决策1:output_format双模式------text和json
为什么Reviewer用JSON而Editor用text?因为Reviewer的输出要被下一个Agent消费------Formatter需要知道"评分是多少"、"问题有哪些"、"建议是什么",这些信息必须结构化、可解析。而Editor的输出是给人类看的文章初稿,自由文本更自然。
Claude Code也是这么干的------结构化JSON和自由文本分开处理。Reviewer的JSON是"对方能直接解析的数据",Editor的自由文本是"对方得全文理解的内容"。
决策2:output_keys字段约束
output_keys告诉LLM"你必须输出这些字段"。这不是多余的设计------没有字段约束,LLM可能输出一个包含20个key的JSON,其中15个是废话。有了字段约束,LLM只输出score/issues/suggestions三个字段,精简且可预测。
Hermes硬性容量管理也是这思路------别"能存多少存多少",只存必要的。output_keys就是Agent输出的容量上限。
决策3:max_tokens按角色差异化
Editor需要max_tokens=2000(写1500字文章),Reviewer只需要800(JSON输出不需要太多token),Formatter需要2000(润色后的文章可能比初稿更长)。这不是随意设的------每个Agent的输出需求不同,token限制也应该不同。
Codex的compact策略也是这思路------别让所有Agent用统一的上下文窗口,按角色差异化配置。
_extract_json:LLM不听话怎么办
LLM经常不听指令。你让它输出JSON,它可能:
- 包裹三个反引号的markdown标记
- 在JSON前后加废话
- 输出无效JSON
python
def _extract_json(self, text: str) -> str:
"""从LLM输出提取JSON"""
cleaned = text.strip()
# 去三个反引号包裹
if cleaned.startswith("```"):
idx = cleaned.find("\n")
if idx > 0:
cleaned = cleaned[idx + 1:]
if cleaned.endswith("```"):
cleaned = cleaned[:-3].strip()
try:
parsed = json.loads(cleaned)
return json.dumps(parsed, ensure_ascii=False)
except json.JSONDecodeError:
print(f"[{self.name}] JSON解析失败,返回原始文本")
return text
这个方法在实战中确实捕获了LLM不听话的情况------GLM-5.1有时候会在JSON前面加一句"好的,以下是我的审稿意见:",导致json.loads失败。_extract_json做的是:先去掉markdown标记,再尝试解析,失败就fallback返回原始文本。
这是生产级Agent必须处理的"LLM输出不确定性"问题。四大架构也都处理了:OpenClaw用compaction兜底(压缩失败就用原始历史),Codex用compact_remote远程重试。
三Agent定义:每个角色做什么
python
EDITOR_PROMPT = """你是一位技术文章主编...写一篇技术文章初稿...
要求:标题要有钩子、开头200字放核心结论、至少1500字、口语化风格"""
REVIEWER_PROMPT = """你是一位严厉的技术审稿人...
审查维度:标题吸引力/开头结论/技术深度/口语化程度/逻辑连贯
你必须以JSON格式输出,包含字段: score, issues, suggestions"""
FORMATTER_PROMPT = """你是一位技术文章排版润色师...
润色要点:根据suggestions逐条修改、去AI味、确保开头200字是核心结论"""
注意Reviewer的prompt最后那句"你必须以JSON格式输出"------这是output_format="json" + output_keys="score", "issues", "suggestions"的prompt层约束。技术上,_format_instruction()方法会自动追加更详细的格式说明,但prompt本身也要先提一遍,双重约束确保LLM输出符合预期。
Hermes的nudge机制也是这思路------别被动等LLM自己决定输出格式,主动约束。nudge是"提醒自己写笔记",output_format是"提醒LLM输出JSON",本质都是主动策展而非被动记录。
Workflow编排器:串行的核心是_merge_for_formatter
整个Workflow最关键的方法不是run(),是_merge_for_formatter():
python
def _merge_for_formatter(self, draft: str, review_json: str) -> str:
"""合并原文+审稿意见 → formatter输入"""
try:
review_data = json.loads(review_json)
score = review_data.get("score", "N/A")
issues = review_data.get("issues", [])
suggestions = review_data.get("suggestions", [])
review_summary = f"评分: {score}/10\n问题: {issues}\n建议: {suggestions}"
except json.JSONDecodeError:
review_summary = review_json # fallback
return f"## 文章初稿\n{draft}\n\n## 审稿意见\n{review_summary}\n\n请根据审稿意见润色这篇文章。"
这个方法做的事情是:把Reviewer的1800字自由文本(如果JSON解析失败的话)或200字JSON摘要,提炼成"评分+问题+建议"的精简摘要,和原文合并后传给Formatter。
对比Week5踩坑:LangChain4j的AgenticScope里,CodeReviewer的1800字输出原样塞进{{codeReview}}变量,SecurityScanner的prompt膨胀到3000+token,直接504超时。而这里,Formatter拿到的输入是:原文 + 评分/10 + 问题列表 + 建议列表------精简、可解析、不会膨胀。
说白了就是OpenClaw compaction思想在Agent间传递的应用------别把所有历史消息原样塞给LLM,压缩后只传摘要。Hermes硬性容量管理同理------Reviewer输出从1800字压到200字JSON,只留"下一个Agent需要的"。
Java直觉:_merge_for_formatter就是DTO组装器------从Entity(Reviewer的完整输出)中提取字段,组装成DTO(score/issues/suggestions),传给下一个微服务(Formatter)。
四个实战踩坑:每个都是"上下文膨胀"的变种
跑通三Agent流水线的过程中踩了4个坑,每个说白了都是上下文膨胀的不同表现形式。
坑1:LLM超时------urllib的同步等待陷阱
最开始用urllib同步调用LLM API,timeout=30秒。EditorAgent写1500字文章,GLM-5.1需要40-50秒才能吐完所有内容------30秒超时直接报错。
第一反应是加timeout到60秒,还是超。后来分析了一下:问题不是网络差,是urllib不支持流式响应。长文章生成要等全部内容吐完才能返回,这就像你让厨师做一道菜,非要等他全部做完才能开始吃------不能边做边吃。
解决方案:
- 动态超时------timeout = max(120, max_tokens * 0.1),editor 2000 token→200秒超时
- 重试机制------最多2次重试,间隔3秒,不再一超时就返回错误
说白了也是上下文膨胀问题------"等待时间膨胀"。四大架构都处理了:OpenClaw用流式输出(streaming replies),Codex用异步生成器(realtime_conversation.rs),Claude Code用/compact手动压缩来缩短响应时间。
坑2:飞书渲染------反引号变成HTML标签
这个坑看起来是"显示问题",但说白了就是"信息在传递过程中被意外修改"------也是上下文膨胀的变种。飞书消息里的三个反引号被渲染成HTML标签,手敲时原样敲入导致Python语法错误。
这说明:Agent间传递的信息在经过不同通道时可能被意外修改。OpenClaw处理这个问题的方式是multi-channel routing------不同通道(飞书/Telegram/Discord)有不同的消息格式处理逻辑,确保信息在传递过程中不被损坏。
坑3:invoke缩进错误------上下文隔离的实现缺陷
漏了一个缩进层级,invoke方法变成模块级函数而不是类方法。这导致"上下文隔离"失效------如果invoke是模块级函数,它就无法访问self.system_prompt、self.output_format等Agent私有属性,每个Agent的"独立身份"就被破坏了。
四大架构都强调子Agent隔离------Claude Code的Subagent有自己的上下文窗口、system prompt和tool access;OpenClaw的sessions_spawn隔离子session的上下文。隔离一旦失效,所有Agent共享同一个上下文,上下文膨胀的老问题就回来了。
坑4:max_tokens太小------"容量管理"配置不当
Editor用max_tokens=800写不出1500字文章。Hermes也遇到过这个容量管理问题------MEMORY.md限制2200字符,USER.md限制1375字符。容量太小写不出东西,太大浪费token和推理时间。
解决方案是按角色差异化:editor 2000、reviewer 800、formatter 2000。Codex的多策略compaction也是这思路------别让所有Agent用统一的上下文窗口,按角色差异化配置。
踩坑总结
| 坑 | 表现 | 本质 | 四大架构的解法 |
|---|---|---|---|
| LLM超时 | urllib 30秒超时 | 等待时间膨胀 | OpenClaw流式输出/Codex异步生成器 |
| 飞书渲染 | 反引号→HTML标签 | 传递过程中信息被损坏 | OpenClaw multi-channel路由 |
| 缩进错误 | invoke变模块函数 | 上下文隔离失效 | Claude Code Subagent隔离/Codex spawn隔离 |
| max_tokens太小 | 800 token写不出1500字 | 容量管理不当 | Hermes硬性容量上限+按角色差异化 |
四个坑,四种表现,但说白了都是同一回事:上下文膨胀的不同变种------时间膨胀、信息损坏、隔离失效、容量不当。
7条可迁移的设计原则
从四大架构拆解到实战踩坑,提炼出7条原则。每条都经过代码验证:
原则1:对话循环必须串行化
多Agent串行Workflow里,editor→reviewer→formatter顺序不可变。这不是因为"并行不好",是因为串行有天然的数据依赖------reviewer必须等editor写完才能审稿,formatter必须等reviewer审完才能润色。
OpenClaw的per-session串行化也是这个逻辑:同一个session同时只处理一条消息,防止竞态条件。Java直觉:Kafka partition顺序消费------一个partition同一时刻只被一个consumer处理。
原则2:权限必须渐进式信任
这个原则在多Agent协作里体现为:不同的Agent有不同的"权限级别"。Editor有"写权限"(max_tokens=2000,允许长输出),Reviewer有"读权限"(max_tokens=800,只输出精简JSON),Formatter有"写权限"(max_tokens=2000)。
Claude Code也是这思路------三层分级权限:Read-only无需批准、Bash需批准但永久记住、File修改需批准且只到session结束。别一刀切,按风险等级区分。
原则3:记忆必须Agent主动策展+容量管理
Hermes的nudge机制------Agent主动提醒自己写笔记,别被动记录。搬到多Agent协作:Reviewer得主动筛自己的输出,只写score/issues/suggestions三个字段,别写1800字完整思考过程。
容量管理也是必须的:MEMORY.md限制2200字符、output_keys限制JSON字段数、max_tokens限制输出长度。没有容量管理,Agent就会"能写多少写多少"------然后下一个Agent的上下文就爆炸了。
原则4:Skill必须自描述可热加载
这条在本次实战中体现为:每个Agent的system_prompt就是它的"Skill定义"------Editor的prompt定义了"写文章"这个Skill,Reviewer的prompt定义了"审稿"这个Skill。这些Skill是自描述的(prompt本身就告诉你这个Agent做什么),也是可热加载的(改prompt就改了Agent的能力)。
OpenClaw和Claude Code的SKILL.md也是这么玩的------Skill用Markdown文件自描述,放特定目录就能被Agent自动加载。
原则5:上下文必须有压缩机制
_merge_for_formatter就是压缩机制------把Reviewer的完整输出压缩成"评分+问题+建议"的精简摘要。OpenClaw的compaction就是这个思路------把session历史压成summary。
关键洞察:压缩不是"丢失信息",是"只保留决策关键信息"。Formatter不需要知道Reviewer为什么觉得标题不够好(论证过程),只需要知道"Reviewer觉得标题不够好,建议改一个"(决策结论)。
原则6:子Agent必须隔离上下文
每个Agent只看自己的system_prompt+本次输入,不继承上一个Agent的对话历史。invoke()方法只传两个message:system和user,不传之前Agent的完整对话记录。
Claude Code的Subagent就是这么干的------每个有自己的上下文窗口、独立system prompt、独立tool access。Codex的spawn_child()也一样------子进程隔离上下文,不共享父进程的session。
原则7:Agent间传递必须精简
这条是我自己踩坑总结的,四大架构的文档里没人直接说,但它们的compaction/容量管理/分层存储说白了都是在解决这同一个问题。
精简别"少写点",只写对方需要的。Reviewer输出JSON不是因为懒,是因为Formatter只需要score/issues/suggestions这三个字段来做出润色决策。1800字自由文本里的论证过程、背景分析、补充说明------这些对Formatter来说是噪音,不是信号。
Java直觉再次出现:DTO vs Entity的差别。微服务之间传DTO,只包含对方需要的字段;Agent之间传JSON,只包含下一个Agent需要的字段。
说白了
多Agent协作的核心矛盾就一句话:怎么让Agent之间传递信息既不丢重点,又不让垃圾撑爆上下文。
四大生产级Agent架构各有各的解法:OpenClaw靠compaction压缩历史、Hermes靠硬性容量管理+nudge策展、Claude Code靠分层存储+渐进式权限、Codex靠远程压缩+平台自适应沙箱。我的实战验证结论:Agent间传递用JSON比自由文本好,上下文隔离是必须的,_merge_for_formatter是整个Workflow最关键的方法。
下一步计划:给Mini Harness加Agent Memory三层模型(短期记忆/工作记忆/长期记忆),对比Hermes的封闭式记忆+MEMORY.md索引和OpenClaw的session+workspace机制。
下周我会拆解Agent Memory的设计------为什么"记忆策展"比"记忆存储"更重要。
有问题评论区聊。