接手这个系列的时候,我就一直有预感,第五篇会是一个分水岭。前面几篇讲工具调用、单轮记忆维护的时候,很多人还能靠“堆上下文”硬扛过去,但一旦任务复杂起来——比如“看完这20份文件,按竞品维度写一份分析报告”——单个Agent的上下文窗口和注意力机制就集体失守了。要么写到一半忘了最初的问题,要么收集资料阶段就把上下文撑爆,要么把两个竞品的内容搅在一起。我这一篇要聊的,就是解决这类问题的机制:子会话(Sub-agent)和任务编排。
子会话不是一个“多开聊天窗口”这么简单的东西。它本质上是一种任务边界的划分方式——把一次复杂任务拆成多个带有独立上下文、独立指令、独立运行边界的执行单元,再由一个主控方把它们组织起来。做得好,你的Agent系统就是一个可以并行、可以回滚、可以局部重试的工程系统;做得不好,就是一场上下文失控的烟花秀。这篇的内容按标题编号来说是“05”,但如果你没看过前面四篇,也可以直接读——我会把子会话的原理、编排模式、代码示例、踩坑经验全部讲透。
1. 子会话不是“多开窗口”:它在解决任务边界问题
1.1 单会话为什么总会翻车
我在没有引入子会话之前,做Agent任务分解的方式非常粗暴:把所有要求塞进一个System Prompt,然后让Agent“一步一步想”。简单任务还好,任务一多就出问题。
举个真实场景:一个“竞品动态分析”任务,我需要让Agent收集三家公司近三个月的新品动态,然后交叉对比,最后产出一份PDF报告。如果用一个会话从头跑到尾,会发生三件事:
- 收集资料阶段会累积大量网页正文、笔记、临时摘要,这些内容会逐渐把上下文窗口占满。等到真正需要写报告的时候,Agent能记住的只剩最后两三家来源,早期的信息被“挤”出了有效注意力范围。
- 阶段性任务之间会互相干扰。Agent在“收集竞品A”的时候,脑子里还残留着“竞品B”的大量细节,写出来的分析会串味。
- 任何一个环节出错,比如某个网页抓取失败,Agent可能重试到把预算烧光,因为单一会话里没有一个清晰的“任务终点”。
这个问题的根源不是模型不够聪明,而是任务边界不清晰。一个会话承载了太多职责,模型需要在“广泛收集”和“聚焦写作”这两种认知模式之间反复横跳,结果两头都不讨好。
1.2 子会话的本质是“一个带独立上下文的执行单元”
子会话的做法,是把不同职责的干活过程拆开,每个子会话拥有自己的System Prompt、自己的人设、自己的输入输出协议,以及自己的运行预算。
用一句话概括:子会话 = 一个只负责一件事、只能看到局部信息、按约定格式交结果的独立执行单元。
它跟“一个Agent跑很多轮”最大的区别,就是上下文隔离。子会话内部不管聊了多少轮、积攒了多少中间信息,结束之后只把最终结果吐出来。主控会话拿到的是一份“干净的交付物”,而不是一大坨对话历史。
我之前在线上项目里用过一个比喻:单会话像一个全能员工坐在一个堆满文件的大办公室里干活,资料越堆越多,他越来越难找到关键文件;子会话则像一个项目组,每个人坐在独立的小隔间里,分别负责一块任务,最后把各自的交付物交到项目经理手上。项目经理不需要看每个人的草稿,只需要看他们提交的最终结论。
1.3 什么任务值得拆成子会话
子会话不是银弹,拆得过碎反而会带来额外的调度开销。我自己的判断标准是三条,至少满足两条我才会拆:
- 任务涉及多个不同的知识领域或信息来源,比如“既要做技术调研,又要做市场分析”;
- 任务存在明显的阶段性,后一段的结果依赖前一段的产出;
- 单次执行的信息量可能超过上下文窗口的合理承载范围,比如要读几十份长文档。
如果任务只是“写一封邮件”“做一份会议纪要”,单会话直接做完就好,拆子系统反而得不偿失。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 四种编排模式的选择逻辑:串行、并行、汇合、动态路由
任务编排就是把多个子会话按一定顺序和结构组织起来。这里没有万能的银弹,只有四种基础模式,理解了它们的取舍,你就能自由组合。
2.1 串行编排:前一个的输出,是后一个的输入
串行是最自然的编排方式:子会话A处理原始输入,产出中间结果;子会话B拿到这个结果继续处理;子会话C再基于B的产出做最终输出。数据流是一条线。
典型场景是内容生产流水线:“资料收集子会话”先提取事实,传给“数据分析子会话”做归纳,再传给“文案子会话”写报告。每个环节任务单一,Prompt可以写得非常具体。
串行的优点是好理解、好调试、好加缓存,任何一个环节失败,重跑那个环节就行。缺点是总耗时会累加,如果每个子会话都要跑几十秒,串行链路就会显得很慢。
我在实际项目中会在两种条件下优先选串行:一是任务本身有严格的因果依赖,必须等上一步结果;二是为了省成本,很多中间结果可以缓存复用。
2.2 并行编排:能并行的任务别串着跑
并行编排适合那些彼此没有依赖的子任务。比如竞品分析场景里,“调研竞品A”和“调研竞品B”之间没有任何依赖,它们完全可以同时跑。
并行带来两个直接收益。第一是延迟下降,如果三个子任务各自需要15秒,并行执行总耗时就只有15秒,而不是45秒。第二是隔离性更好,一个子任务失败不会影响其他子任务的状态,最后在汇总阶段统一处理部分失败的情况。
但并行也有代价。最大的代价是并发限流,很多模型API接口有每分钟请求次数限制(RPM),同时开8个子会话很容易触发429限流。我在生产环境里的经验是:默认并发控制在3到5个,根据上游API配额动态调整。
2.3 汇合编排:什么时候必须停下来等一等
并行子任务全部跑完后,必须有一个“汇合点”(join),把各个子会话的结果汇总起来。可以汇合后直接输出,也可以再交给一个新的子会话做综合整理。
汇合点要注意一个问题:结果一致性。子会话并行的过程中,可能会出现一个子会话提前结束、另一个还在跑的情况。如果你在主控逻辑里用了“先到先得”的取数方式,就会拿到不完整的数据。正确的做法是设置一个屏障(barrier),必须等所有子会话都返回,或者拿到明确的部分失败标记,再进入下一阶段。
我早期犯过一个错误:为了让用户感觉响应快,我让主会话先返回已经完成的子结果,结果后期综合分析时缺了两块数据,整个报告结论站不住脚。后来才明白,汇合点是质量保障的关键位置,宁可多等几秒,也不要让一个不完整的结果进入下游。
2.4 动态路由:让主Agent自己决定下一步拆什么
前三种编排模式都是“静态编排”,也就是说任务拆法在代码里写死。动态路由则是把“怎么拆、下一步做什么”交给主Agent——主Agent先看任务,规划出一组子会话,逐个执行,根据中间结果再决定是否追加新的子会话。
这种方式非常适合探索型任务,比如“帮我制定一份市场推广方案”。主Agent一开始可能只需要拆出“目标用户调研”“竞品分析”“渠道调研”三个子会话,跑完后发现还缺“预算测算”,于是再启动第四个子会话。
动态路由的优点是灵活,能应对无法预料的输入;缺点是失控风险更高,因为子会话的产生数量不受控制。我建议在这种情况下必须设置硬性上限:最多只能创建N个子会话,否则复杂任务可能会让Agent“兴奋”到拆出几十个任务,预算直接爆炸。
下面用一张表总结四种模式的适用场景:
| 模式 | 依赖关系 | 延迟 | 成本 | 适用场景 |
|---|---|---|---|---|
| 串行编排 | 强依赖 | 累加 | 中等 | 内容生产流水线、多阶段转换 |
| 并行编排 | 无依赖 | 取最大 | 中等 | 多源资料收集、并列对比分析 |
| 汇合编排 | 等所有子任务 | 取最大 | 中等 | 多路调研后统一汇总 |
| 动态路由 | 运行时确定 | 不定 | 偏高且需设限 | 开放式探索、自动规划 |
3. 动手写一个多子会话编排示例:报告生成Agent的代码拆解
理论聊完,直接进入代码。我在这里写一个最小但完整的示例,场景是“基于三个资料来源,生成一份竞品对比报告”。整个系统由三部分组成:子会话封装类、主控编排逻辑、子会话指令模板。
3.1 整体结构与数据流
整个系统的数据流分四步:
- 主控接收入口任务,这里假设入口已经解析出“要调研哪三家竞品”。
- 主控为三家竞品分别创建一个子会话,并行执行,每个子会话负责收集对应竞品的公开信息并输出结构化结果。
- 所有子会话执行完毕后,主控把结构化结果汇总,传给一个“报告撰写子会话”。
- 报告撰写子会话输出最终报告。
子会话和主控通过结构化JSON传递结果,而不是传对话记录。这样做的好处是主控上下文不会被大量原始资料撑爆,同时下游可以精准读取关键字段。
3.2 核心代码:子会话封装、并行调度与结果校验
下面是我在项目中反复调整后沉淀下来的精简版本。为了可读性,模型调用用一个抽象函数 call_llm 代替,你可以根据自己的实际API接入。
python复制import json
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
class SubResult:
"""子会话的统一返回结构"""
def __init__(self, name: str, status: str, data: dict, error: str = None):
self.name = name
self.status = status # success / failed
self.data = data
self.error = error
def to_json(self) -> str:
return json.dumps(
{"name": self.name, "status": self.status,
"data": self.data, "error": self.error},
ensure_ascii=False
)
class SubAgent:
"""
子会话封装:每个实例拥有独立的指令和运行预算。
"""
def __init__(self, name: str, instruction: str,
model: str = "claude-3-5-sonnet", max_steps: int = 3):
self.name = name
self.instruction = instruction
self.model = model
self.max_steps = max_steps
self.messages = []
def run(self, task_input: str) -> SubResult:
self.messages = [
{"role": "system", "content": self.instruction},
{"role": "user", "content": task_input},
]
step = 0
while step < self.max_steps:
try:
# call_llm 是对底层模型 API 的统一封装,
# 传入 messages,返回字符串 output
output = call_llm(
model=self.model,
messages=self.messages,
temperature=0.2,
max_tokens=2000,
)
# 尝试解析为 JSON;如果解析失败,则直接进入下一轮
try:
parsed = json.loads(output)
if "status" in parsed and "data" in parsed:
return SubResult(
name=self.name,
status="success",
data=parsed["data"],
)
except json.JSONDecodeError:
pass
# 没有拿到协议要求的 JSON,把它作为消息追加,要求模型修正
self.messages.append({"role": "assistant", "content": output})
self.messages.append({
"role": "user",
"content": "请严格按协议输出 JSON,包含 status 和 data 字段。",
})
except Exception as e:
return SubResult(name=self.name, status="failed",
data={}, error=str(e))
step += 1
return SubResult(name=self.name, status="failed",
data={}, error="max_steps exceeded")
def run_parallel_subagents(tasks: list[tuple[str, str, str]]) -> list[SubResult]:
"""
并行执行多个子会话。
tasks 的元素是 (子会话名称, 指令, 输入内容)
"""
results = []
with ThreadPoolExecutor(max_workers=3) as executor:
future_map = {
executor.submit(SubAgent(name, instruction).run, task_input): name
for name, instruction, task_input in tasks
}
for future in as_completed(future_map):
result = future.result()
results.append(result)
return results
def build_report(compiled_data: list[dict]) -> str:
"""
报告撰写子会话:把多个子会话的产出汇总成最终报告。
"""
report_instruction = (
"你是一名资深行业分析师。用户会给你一组结构化竞品调研数据,"
"请基于这些数据写一份对比分析报告。"
"报告必须包含:整体趋势、各家优劣势对比、潜在机会点。"
"语言专业、简洁。"
)
reporter = SubAgent(
name="report_writer",
instruction=report_instruction,
model="claude-3-5-sonnet",
max_steps=2,
)
result = reporter.run(json.dumps(compiled_data, ensure_ascii=False))
if result.status == "success":
return result.data.get("report", "")
return f"报告生成失败: {result.error}"
这段代码里有一个关键设计:子会话不是做一次LLM调用就完事,它有一个小循环,会自动解析模型的输出。如果输出不是约定的JSON结构,它会要求模型“按协议重新输出”。这个机制在实际生产里极其重要,可以大幅降低因为模型“自由发挥”导致的下游解析失败率。
3.3 主控编排:把子会话组织起来
主控逻辑只负责一件事:生成任务清单、调用并行调度器、收集结果、交给报告子会话。
python复制# 主控逻辑:演示用,直接构造三个并行子任务
def main():
competitors = [
("alpha", "企业A", "收集企业A近三个月发布的公开产品动态、融资动向、关键人事变化"),
("beta", "企业B", "收集企业B近三个月发布的公开产品动态、融资动向、关键人事变化"),
("gamma", "企业C", "收集企业C近三个月发布的公开产品动态、融资动向、关键人事变化"),
]
base_instruction = (
"你是一个行业信息研究员。你会收到一个调研主题,"
"请从知识库或互联网搜索中提取事实信息。"
"输出必须为 JSON,包含 data 字段,"
"data 里面至少有 three_items 数组,每个元素包含 title、detail、source。"
)
tasks = [
(name, base_instruction, f"调研目标:{company_name}。{focus}")
for name, company_name, focus in competitors
]
# Step 1: 并行收集
collect_start = time.time()
sub_results = run_parallel_subagents(tasks)
if not all(r.status == "success" for r in sub_results):
failed = [r.name for r in sub_results if r.status != "success"]
print(f"以下子会话失败: {failed}")
# 生产环境这里应该走局部重试或标记降级
sub_results = [r for r in sub_results if r.status == "success"]
compiled_data = [
{"competitor": r.name, "items": r.data.get("three_items", [])}
for r in sub_results
]
print(f"并行收集耗时: {time.time() - collect_start:.2f}s")
# Step 2: 生成最终报告
report = build_report(compiled_data)
print(report)
if __name__ == "__main__":
main()
我在生产版本里还会加一个“结果合法性校验”函数,比如检查 three_items 是否至少有一个元素、每个元素是否有 title。不要只靠模型自觉,校验逻辑越严格,下游越省心。
4. 子会话指令模板:这是编排质量的命门
代码调度只是骨架,真正决定子会话质量的是指令模板。同一个任务,指令写得好不好,结果差距能有两三倍。我总结了几个必须覆盖的维度。
4.1 基础指令模板:角色、任务、输入、输出
我给每一个子会话都用同一套四段式模板,常年在项目里复用:
- 角色:说明这个子会话的身份定位,让它知道自己是研究员、分析师还是写手。
- 任务目标:用一句话说清楚要完成什么,越具体越好。
- 输入说明:告诉子会话用户会提供什么内容、字段是什么结构。
- 输出协议:规定返回格式,尤其是JSON结构,字段名和含义要写死。
下面是模板示例,你可以直接抄:
code复制你是{角色}。你的职责是{任务目标}。
用户会输入:{输入说明}。
你必须严格按以下JSON结构输出:
{
"status": "success",
"data": {
"conclusion": "一句话结论",
"details": [
{"title": "要点标题", "content": "要点内容", "source": "来源"}
]
}
}
如果输入信息不足,请在 data 内增加 "missing_info": true,并说明缺什么。
我在这个模板里特意加了 missing_info 字段,让子会话有“坦诚说自己不知道”的出口。不加这个出口,很多子会话会为了凑答案编造内容,下游再拿到错误数据就会酿成大祸。
4.2 好指令和烂指令的对比
很多时候,子会话表现不好,不是模型不行,是指令写得不像人话。下面是我在评审团队Prompt时经常引用的对比:
| 维度 | 烂指令 | 好指令 |
|---|---|---|
| 角色 | “你是助手” | “你是主攻企业服务的行业研究员,有五年SaaS市场分析经验” |
| 任务 | “调研一下这家公司” | “从官网、公开新闻、招聘信息三个渠道收集企业A近三个月的信息,只收录可核验的事实” |
| 输出 | “整理结果” | “输出JSON,data.three_items数组中每个元素必须包含title、detail、source三个字段” |
| 边界 | 无 | “无法从可靠来源确认的信息,不要编造,在missing_info中标记” |
烂指令的问题在于没有给模型足够的“约束条件”,模型会在泛化能力范围内自由发挥,结果千奇百怪。而好指令把角色、动作、来源、结构、边界全部写清楚,子会话的输出稳定性和可解析性都会大幅提升。
4.3 给子会话设置“安全阀”
子会话在设计上有一种隐含风险:它是一个独立的智能体,可能会“过于努力”。比如我见过一个子会话为了收集公司动态,编造了“某公司已发布量子计算芯片”,还贴了一个假来源。模型不会主动承认自己不知道,除非你给它一个体面的台阶。
安全阀的常见做法有:
- 在指令中明确写:不确定的信息必须标记
missing_info: true,不得猜测。 - 设置
max_steps重试上限,防止子会话在格式解析失败后无限循环。 - 对输出结果做正则或JSON Schema校验,不满足结构就触发重写或降级处理。
- 记录每个子会话的token消耗,超预算时自动终止,并返回“任务超限”而不是继续跑。
5. 实测踩过的坑:预算失控、并行上限和结果串台
前面讲的都是理想路径。真实跑起来,每个坑都是拿时间和钱换来的。下面这几个是我在多个项目里遇到频率最高的问题,每一个都配有排查思路。
5.1 预算失控:子Agent越跑越多
动态路由模式下,主Agent觉得“信息不足”,可能会连续创建新的子会话。如果每创建一个子会话都要调用一次模型,成本会指数级上升。
我之前有一个线上任务,原本预计跑10个以内的子会话,结果某天用户输入了一个非常开放的问题,主Agent一口气生成了37个子会话,一小时的API账单直接翻了几十倍。
排查链路很清晰:先看调用日志,发现子会话数量异常;再看创建子会话的决策逻辑,发现主Agent的规划Prompt里没有设置数量上限;最后修复方案是加了两道保险——第一,在主控代码里写死 MAX_SUBAGENTS = 10,超过就不允许再创建;第二,在规划Prompt里要求Agent说清楚“总共要拆几个子任务,为什么是这几个”,强制它在一开始就做完整的规划。
5.2 并行上限:同时开20个任务,被限流打崩
并行虽然快,但API并发配额是硬约束。第一次做并行编排时,我把8个资料收集子会话同时丢出去,结果瞬间触发限流,返回大量429错误。
排查链路:错误日志里全是HTTP 429,说明不是代码逻辑问题,是配额问题。调整方式有两种:一是降低 ThreadPoolExecutor(max_workers=N) 的N值,二是实现带退避的重试逻辑。
我现在的默认值是 max_workers=3,如果某个API套餐的RPM很低,我会直接用1或2。并行不是目的,稳定跑完才是目的。
5.3 结果串台:A子会话的结果被当成B的
这是最隐蔽的坑。多个子会话并行执行时,如果返回值只带一个短名称(比如 competitor_a),而下游汇总逻辑里用列表顺序对应竞品顺序,一旦某个子会话超时重排,就会把A的结果安到B头上。
我遇到过一次:报告里把企业A的融资额写到了企业B名下,客户差点投诉。排查链路比较曲折,先怀疑是模型幻觉,后来发现是代码里 as_completed 返回的顺序不确定性导致赋值错位。
修复方案很简单:每个子会话返回时,必须在JSON里带上自己的标识字段(比如 agent_name),下游按 agent_name 去匹配,而不是按位置。我在上文代码里已经用了 r.name 作为标识,这个习惯希望大家一开始就养成。
5.4 主控上下文被撑爆
子会话做完后,如果我把所有原始结果一股脑塞回主控,主控上下文一样会被撑爆,子会话的隔离效果就白费了。
解决办法是我在上文中提到的“结构化交付物”原则。子会话只返回数据摘要和结论,不带完整的对话历史。主控的上下文里永远只有一组紧凑的JSON对象,而不是几万字原始笔记。
如果确实需要保留大量中间过程用于审计或迭代,正确的做法是存到外部存储(比如数据库或文件),而不是放回主控上下文。
写在最后的实际体会
子会话和任务编排这块,我踩过的坑远比我写出来的多。回到开头说的那个分水岭:单会话能解决的复杂度是有一个天花板的,越过天花板再硬写Prompt、硬塞上下文,效果只会越来越差。子会话的思路本质上是一种工程化思维——把模型当作可调度的执行单元,而不是无所不能的万事通。
我自己现在的习惯是:接到一个新任务,先不问“怎么让它一次跑通”,而是问“这个任务应该拆成几个执行单元,每个单元的输入输出是什么”。拆清楚之后,串行、并行、汇合、动态路由都有了落地的基础。代码只是把这种思维固化下来而已。
如果你正准备在自己的项目里引入子会话,我的建议是先从一个最小的并行示例开始,比如把三个互相独立的调研任务拆开跑,然后把结果汇总成一个输出。跑通之后再加异常处理、预算控制、结果校验。别一上来就写动态路由,路由自由度越高,调试成本越大。先求稳,再求野。
