100 份合同批量过审,模型却只收到 1 个变量

0 阅读8分钟

低代码 Agent 平台把一堆相似文档塌成一个变量,模型拿到 list 却不会逐项展开。把「批量」留在编排层、让模型只处理单条,是一条能落地的拆法。

核心判断

  • 塌缩:输入节点天生合并多文档,多个相似文件进同一个变量,平台没有「逐项」开关
  • 不自检:模型节点只收一个 list 变量,不会自己拆开一个个跑,这是设计而非缺陷
  • 外置:真正解法是把循环、计数、收集挪到编排层,模型节点保持无状态、单条进出

一、100 份合同,1 份结果

GitHub 上 bisheng(一个国产低代码 Agent 编排器)的 issue #2465,写得很直白:上传多个文档只适合「不同文档共同喂给下游大模型」,可当你想把一百份相似文档按同一条规则各自处理——比如 100 份合同分别审核出 100 份独立结论——平台的输入节点会把它们全塌成单一结果。哪怕你在代码节点里把它转成 list,模型节点也只收到一个 list 变量,不会从 list 里逐个取出元素、按顺序各自跑一遍。issue 里那位用户为了补齐这个能力,自己写了一个计数节点、再拉一个环、靠自增把 list 里的元素一个一个喂进模型。他给这个 workaround 的评价四个字:多少有些丑陋。

它属于所有「低代码 Agent 编排」范式共有的结构短板,不局限于这一个 issue:平台的抽象单位是「一条工作流」,而真实业务里的「批量」要的是「N 条独立工作流」。中间差的那一层,平台没给。更麻烦的是,大多数人第一反应是去「调教」模型——加一个 critic 节点、写更狠的 prompt、让模型「聪明到」自己迭代——这些都是在用更贵的生成,去解决本该由编排层管的结构问题。

要点:反例我见过最浪费的一条路:把 100 份文档塞进 prompt,要求模型「逐份输出」。模型会给你一份看起来像 100 份、其实是 1 份的合并文本,漏的、混的、张冠李戴的它自己都察觉不到。批量这件事一旦交给生成层,你就失去了「第 37 份到底跑了没有」的确定性。

归根到底,我能带走的核心就一句:**批量,是要交给引擎的控制流;模型只该做单条生成。**平台塌缩了输入、模型不自检,两环合起来就是「N 进 1 出」;要破,就得把「逐项」这件事从模型手里拿出来,交给它外面的那一层。下面顺着这条线,把塌缩拆到具体环节。

二、塌缩发生在哪一环

环节 ① · 输入节点:天生合并

低代码平台的输入节点设计初衷是「收集多路材料,交给下一个大模型节点」,所以多文档进来默认是汇聚而不是枚举。它的变量类型只有一个名字,100 份文件进来就是一个 list。平台没有暴露「按元素展开」的开关,因为从「一条工作流」的心智模型看,根本没有这一层。

100 份合同批量过审,模型却只收到 1 1

环节 ② · 模型节点:list 进不去也吐不出

有人会问:模型节点里写个循环不就行了?不行的关键在于——模型节点是一次生成调用,它的输入契约是「一段 prompt + 一批上下文」,不是「一个可迭代的容器」。你给它一个 list,它要么把 list 整个序列化进 prompt(回到上一条的「合并」),要么只取 list 的第一个元素。逐项是一个控制流概念,它不属于模型节点,属于它外面的那一层。

环节 ③ · 自救:计数节点成环

所以 issue 里那个 workaround 才会长成那样:塞一个计数节点(count,初值 0),拉一个环,每轮把 list[count] 取出来喂模型、模型跑完 count+1,直到 count == len(list)。功能上能用,代价是:一次 100 份就是 100 次模型调用,环越画越长、状态越多、越难加并发、越难在某一环失败时定位是「第几份、哪一步」。把控制流手写进工作流画布,等于把该由引擎干的事,摊到每个使用者的鼠标上。

这里有个反直觉的点,容易被人忽略:**批量跑得慢、失败定位难,根子往往出在控制流被摊在了画布上,模型本身没变慢,慢的是它前面那层调度。**同一个 100 份,把循环交给引擎(可并发、逐条记 id),哪怕模型一次调用耗时不变,整批的墙钟时间也能砍到 1/8 甚至 1/16;而画布上的计数环是串行的,100 份就是 100 个「模型调用耗时 + 一次画布调度」相加。

实际上,真正的问题还更隐蔽一层:画布上的环一旦失败,你看到的是「工作流停了」,而不是「第 41 份、模型节点、第 3 次重试」。要把它定位出来,得顺着计数节点一个环一个环地倒推。这就是为什么「失败定位成本」在决策表里单独占一行——它就是画布持有控制流时最贵的那个隐性成本,远不只是锦上添花。

100 份合同批量过审,模型却只收到 1 2 把两摊事并排放着看,边界很清楚:

环节症状谁该管不该管
输入节点多文档塌成 1 个 list平台(暴露枚举开关)使用者
模型节点不逐项、只出 1 份——(本就不该管控制流)模型自身
代码节点把 list 换个名字搬过去使用者(临时补救)长期方案
编排引擎fan-out / 并发 / 逐条定位引擎(控制流的家)画布上的计数环

要点:数据注脚这条路径在我自己的日常流水线里反复出现:每天那套「抓 N 个源 → 逐个打分 → 汇总」的痛点日报,就是 100% 的外置 fan-out——循环和收集在编排脚本里,每一步只处理单条、失败时能定位到「第几个源、哪一步」。工作流引擎缺的正是这一层,而它恰好是最容易被「加个模型」骗过去的地方。

三、该拆还是该忍:一张决策表

不是所有「批量」都值得立刻去平台里开 fan-out。先过一遍这张表,再决定:

判断项该外置 fan-out先忍 / 换法
单次要处理几条稳定 ≥ 10 条、且逐条要独立结果个位数、结果可合并
规则是否一致同一规则套 N 份每份规则不同(该走路由不是循环)
能否并发条目间无依赖、要提吞吐条目间有依赖(得串行)
失败定位成本一条失败不能拖累整批整批可丢弃重跑
平台是否已给枚举开关已给 → 直接用未给 → 才需要外置这一层

把「拿走清单」放在中间,是因为它才是真正能第二天上班就用的部分:

  • 拆:把循环、计数、收集从画布上移出去,交给编排脚本(或平台的 fan-out 节点,若有)。
  • 无状态:模型节点保持「单条进、单条出」,不持有任何跨条目的状态。
  • 并发:条目间无依赖就开并发,吞吐是线性堆出来的。
  • 定位:每条记一个稳定的 item_id,失败按 id 重跑那一条,不重跑整批。
  • 兜底:一条失败 → 记 id + 原因 → 整批继续,最后一并补偿,别让一条卡死 100 条。

平台没给枚举开关,就先用「外置编排 + 无状态模型节点」顶住;等平台补齐 fan-out,再把外置的那层收敛回画布。方向是同一个:**控制流在引擎,生成在模型,二者不越界。**归根到底,结论就一句:能由引擎持有的控制流,永远别落到模型或画布上。我更看重「逐条可定位」胜过「跑得快」——一条失败能按 id 单独重跑,比整批吞吐快 2 倍值钱。我不认为这是「等引擎升级」就能绕过去的:即便平台补了 fan-out,前提是你先想清楚「哪些条目该并发、失败怎么记、靠不靠得上」,否则引擎再强也只是把塌缩换了个地方发生。至于这条什么时候不适用——单量个位数、结果可合并、条目间有强依赖时,外置 fan-out 是过度设计,直接「合并喂 1 次」更省事。别为了「批量感」去套一套并发引擎,那是拿架构的复杂度,去换一个根本不存在的问题。

四、把批量留在编排层

回到开头那个 issue:那位用户用计数环绕出来的功能是对的,代价是「丑陋」。丑陋的根源不在他,在平台把「批量」这个本该由引擎持有的控制流,摊到了使用者的鼠标上。


一个业务现场问题

一个业务现场问题 你们线上真正卡住「批量」的,是平台没给枚举开关,还是给开关了、并发一开就雪崩?把「单条跑通了、批量就跑不动」的那个环节发我,我们对着真实的编排图拆。