三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

面试官:“gather报错全停?”我:“分支照跑”

面试官:“gather报错全停?”我:“分支照跑”

asyncio.gather默认遇到首个异常时,会把异常传播给等待它的任务,但其他 awaitable 不会因此自动取消,仍会继续运行。把它理解成“立刻终止所有子任务”,会直接带偏失败策略。

这不是一个可以忽略的并发细节。并行检索时,一个分支失败后,其他分支究竟继续、取消还是降级,必须由业务规则决定,不能靠误读并发库。为了把边界跑清楚,后面会用一个只依赖 Python 标准库的小例子逐步验证。

为什么并行不等于 Supervisor

先把两个概念拆开。

Fan-out 是把一个任务展开成多个可并行分支,fan-in 是等待分支到达后合并结果。只要子任务、依赖和合并条件能够提前写清,用普通工作流和并发库就能完成,不需要让大模型扮演 Supervisor。

Supervisor 解决的是运行前无法穷举的语义决策。例如初次检索发现一个新实体,需要临时追加调查;两份证据冲突,需要决定请哪个专业角色复核;剩余预算不足,需要在几个合法动作里选一个。它可以负责选择下一步,但不应该越过权限、预算、最大轮次和完成门禁。

如果把“有多个 Agent”“使用并发”“存在 Supervisor”画成等号,中央 Agent 就会同时拆任务、保存全部上下文、判断完成、处理错误和写报告。结果只是把一个大 Agent 换了名字。

更稳的边界是:确定性依赖写成图,分支状态写进结构化 State,代码处理超时、权限、预算和幂等;只有多个合法下一步需要语义判断时,才调用受限 Supervisor。

例如“同时查订单、政策和物流,三路结束后合并”在运行前已经知道分支与依赖,用代码 fan-out 就够了。“政策里出现一个此前未知的产品名称,要不要追加产品专家”才可能需要 Supervisor。判断标准很具体:下一步能不能由稳定字段和规则决定。任务听起来复杂,并不能自动推出需要 Supervisor。

Supervisor 的派发结果也不能只是一段话。至少要输出目标角色、子目标、输入工件引用、期望 Schema、理由和剩余预算,再由代码检查角色是否存在、权限是否允许、任务是否重复。模型负责提出合法选择,编排层负责守边界。

Fan-out、fan-in 与 Supervisor 是三件不同的事

失败策略先看子任务是否关键

“一个子 Agent 失败,不能影响整体”同样不是普遍原则。

如果失败的是补充背景资料,主证据已经齐全,系统可以带着缺口继续,状态应标成degraded,不能假装完整成功。如果失败的是回答必需的政策版本、订单事实或权限校验,继续生成只会把缺证据包装成答案,此时应该阻断。

因此,每个分支在派发前至少要写清五件事:稳定的分支 ID、是否必需、输入版本、期望输出 Schema、失败后的允许动作。fan-in 节点要检查必需工件是否有效、可选工件缺了哪些、有没有未解决冲突,“几个任务结束了”远远不够。

并发原语也要按这条规则选。需要收集所有分支结果并自行分类时,可以让每个分支把异常转换成结构化失败,再用gather汇总。只要一个必需分支失败就必须终止同组任务时,可以考虑TaskGroup的 fail-fast 语义。Python 官方文档说明,TaskGroup 中首个非取消异常会取消其余任务并等待它们结束。

外部取消要继续传播。CancelledError用于通知协程停止,清理资源应放在finally,不要把取消吞掉再返回“成功”。重试也只适合临时网络错误,并且要有上限;带写操作的 Worker 在重试前先查幂等键,不能重复扣款、发信或写两份报告。

失败最好先分类,再决定动作。临时超时可以在总预算内重试;输入缺字段属于确定性错误,重复执行没有意义;资料里本来就没有答案属于业务缺口,应该澄清或拒绝;写接口超时后状态不确定,则先凭幂等键查询执行结果,不能盲目重放。把四类失败都写成retry,只会让系统更慢,也更难审计。

取消范围同样要明确。用户终止整个任务时,父任务应向仍在运行的只读分支传播取消;某个可选分支超时,不代表必需分支也要取消;已经发出的副作用不能只靠取消协程来回滚。这里的决定属于业务事务边界,不属于 Supervisor 的临场发挥。

把分支结果写成统一协议

下面的代码只用 Python 标准库。任务有三个分支:订单事实和政策条款是必需项,行业新闻是可选项。每个分支都把结果归一成同一个结构,fan-in 再决定阻断还是降级。

import asyncio from dataclasses import dataclass @dataclass class Result: name: str required: bool ok: bool value: str = '' error: str = '' async def worker(name, delay, required, fail=False): await asyncio.sleep(delay) if fail: raise RuntimeError(f'{name} failed') return Result(name, required, True, value=f'{name}:evidence') async def safe_run(name, delay, required, fail=False): try: return await worker(name, delay, required, fail) except Exception as exc: return Result(name, required, False, error=str(exc)) async def run_case(required_fails): results = await asyncio.gather( safe_run('order', 0.01, True), safe_run('policy', 0.02, True, required_fails), safe_run('news', 0.03, False, True), ) blockers = [r.name for r in results if r.required and not r.ok] gaps = [r.name for r in results if not r.required and not r.ok] if blockers: return f"BLOCK:{','.join(blockers)}" if gaps: return f"DEGRADED:{','.join(gaps)}" return 'READY' async def main(): print(await run_case(False)) print(await run_case(True)) asyncio.run(main())

实际运行输出是:

DEGRADED:news BLOCK:policy

第一轮只有可选新闻失败,所以系统可以继续,但必须暴露缺口。第二轮政策条款失败,即使订单事实成功,也不能进入报告生成。

代码里没有直接使用return_exceptions=True,而是在safe_run中把普通异常转换成统一的Result。这样异常、必需性和分支名在同一条记录里,fan-in 不必再靠列表位置猜是谁失败。gather仍会按传入 awaitable 的顺序返回结果,但真实系统最好继续使用稳定分支 ID,因为分支数量可能动态变化。

还要注意,safe_run捕获的是Exception,不会把 Python 当前继承自BaseExceptionCancelledError当成普通失败吃掉。这正是我们想要的:业务失败可以进入结果表,外部取消继续向上传播。若 Worker 持有文件、连接或锁,再用try/finally做清理。

这个例子刻意没有写固定重试次数和超时时间。它们取决于服务 SLO、剩余总预算和故障类型。真正应该固定的是状态含义与退出条件,而不是从别人的示例抄一个数字。

必需分支失败要阻断,可选分支失败可降级

结果冲突应该怎么合并

多个 Worker 返回自然语言长文,最后再让 Writer “自行消除冲突”,是最危险的合并方式。Writer 很可能选一段更顺耳的说法,把分歧悄悄抹掉。

我做吴师兄大模型训练营,也更希望大家先把必需分支、可选分支和冲突状态写进协议,再让 Supervisor 参与语义决策。

分支输出应先变成证据记录,至少包含 claim、来源、观测时间、数据版本、置信状态和工件 ID。合并节点先按工件 ID 去重,再检查同一 claim 是否出现值冲突、时间冲突或来源冲突。没有规则能自动裁定时,状态应保留为conflict,交给补证或人工确认,不能让 LLM 投票。

比如一个分支说“订单已退款”,来源是三天前的客服摘要;另一个分支说“订单仍在处理中”,来源是刚读取的交易状态。合并节点不能因为第一段文字更完整就采用它,而要比较 claim 类型、观测时间和事实系统。若两条来源本来描述不同时间点,它们未必矛盾;若时间相同却值不同,才进入冲突状态。先做这层结构化判断,Writer 才能准确写出限制。

也不要写死“数据库永远比新闻可靠”之类的万能优先级。财务报表可能更适合证明历史金额,交易系统更适合证明当前状态,监管公告更适合证明规则。优先级应绑定 claim 类型、来源版本和时效,而不是绑定 Agent 名字。

fan-in 还要防迟到结果覆盖新状态。每次派发带任务 ID、分支 ID 和输入版本;Worker 返回时校验版本,重复结果通过幂等键去重。若用户已经取消任务,迟到的写操作不能重新把状态改成完成。

最后,完成也要由门禁判断。所有协程结束只代表计算停止,不代表任务完成。必需工件齐全、Schema 校验通过、冲突已解决或被明确保留、副作用有执行凭证,才可以进入completed。让 Supervisor 自己说“已经完成”,与让 Worker 自己给作业打满分没有区别。

所以,多 Agent 设计要回答五个更硬的问题,“有几个角色”反倒最不重要:

谁能并行,谁是必需,失败后谁取消,结果怎样合并,重复与迟到怎样挡住。

这五件事能由代码和状态说明白,Supervisor 才是受控的调度者;说明不白,它只是一个更难排查的单 Agent。

学AI大模型的正确顺序,千万不要搞错了

🤔2026年AI风口已来!各行各业的AI渗透肉眼可见,超多公司要么转型做AI相关产品,要么高薪挖AI技术人才,机遇直接摆在眼前!

有往AI方向发展,或者本身有后端编程基础的朋友,直接冲AI大模型应用开发转岗超合适!

就算暂时不打算转岗,了解大模型、RAG、Prompt、Agent这些热门概念,能上手做简单项目,也绝对是求职加分王🔋

📝给大家整理了超全最新的AI大模型应用开发学习清单和资料,手把手帮你快速入门!👇👇

学习路线:

✅大模型基础认知—大模型核心原理、发展历程、主流模型(GPT、文心一言等)特点解析
✅核心技术模块—RAG检索增强生成、Prompt工程实战、Agent智能体开发逻辑
✅开发基础能力—Python进阶、API接口调用、大模型开发框架(LangChain等)实操
✅应用场景开发—智能问答系统、企业知识库、AIGC内容生成工具、行业定制化大模型应用
✅项目落地流程—需求拆解、技术选型、模型调优、测试上线、运维迭代
✅面试求职冲刺—岗位JD解析、简历AI项目包装、高频面试题汇总、模拟面经

以上6大模块,看似清晰好上手,实则每个部分都有扎实的核心内容需要吃透!

我把大模型的学习全流程已经整理📚好了!抓住AI时代风口,轻松解锁职业新可能,希望大家都能把握机遇,实现薪资/职业跃迁~

这份完整版的大模型 AI 学习资料已经上传CSDN,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费

← 返回列表