序言:在构建使用前沿模型的复杂 AI 系统时,我们该以什么为单位来组织代码?很多人自然的答案是”prompt”、”tool”或”agent”。但 Linus Lee 在这篇文章中提出了一个强有力的替代方案:把抽象边界画在具体的任务和子任务上,而不是画在实现细节上。这个想法不仅适用于 AI 系统设计,还延伸到人机协作到团队管理——本质上,这是一个关于”如何让复杂系统经得起变化”的通用原则。
原文:Compose tasks, not implementations(作者:Linus Lee,发表日期:2025-08-17)
我想讨论一个我在过去几年构建前沿模型的过程中越来越坚信的设计理念:如何设计复杂的复合 AI 系统。但在此之前,我想先铺垫一些关于”组合”这个概念的共同基础。
组合与抽象
在软件设计中,我们通过组合与抽象来管理复杂性。我们把硬件指令或变量组合成函数,把函数组合成库和程序。要实现一个用户界面,我可以把具体的输入和功能概念组抽象为按钮、标签页和面板等组件,然后把它们放在相邻或相互包含的位置来组合出界面。一个复杂的 Web 应用可能由数千个相互通过请求和响应交互的服务组成,每个服务都抽象掉了其他服务不该担心的细节。
组合和抽象共同帮助我们应对软件系统中必然要表达某些本质上复杂的现实世界观念时产生的复杂性。通过找到正确的方法将一个复杂问题分解为小块,每块管理自己的细节并暴露明确定义的交互方式,我们建造出的系统更容易理解,也更容易随时间演进。
在软件工程中,组合模式的基因库面临非常强大的演化压力。如果你的系统无法隔离生产环境中的操作故障,或者无法自信地适应行为随时间变化的需求而不让变化混沌地传播,这种设计模式就会被生态淘汰掉,让位于那些轻松吸纳变化的模式。这就引出了一个问题:我们应该如何组合复合 AI 系统,特别是那些大量使用提示词和微调语言模型的系统?
任务是构建能力的基本单元
在一个没有既定约定的新领域里,一种诱人的做法是去寻找系统实现中我们可以复用的代码区域来减少重复。这往往导致沿着实现细节画抽象边界。我们把这种做法叫”组合实现”。
许多与模型打交道的库和框架都犯了这个错误。它们推动使用者把 prompt、工具、模型,或者它们的某种组合(有时叫”agent”)当作组合更大系统的可复用单元。在这种范式下,应用定义具体的 prompt 或工具,通过为特定任务精心配置一组可用的 prompt 和工具,系统理应能产生期望的行为。
我一直难以用这种方式构建出健壮、可变的系统,我认为这会产出脆弱的系统,难以自信地演进。
当我们通过组合实现细节来构建 AI 系统时,我们在做几个假设:
- 我们假设这种设计能使未来的变更仅限于系统的特定部分。也就是说,我们相信可以通过独立地修改 prompt、工具或模型来应对未来的变更。
- 我们假设在这些组件的层面推理故障是有意义的。换句话说,我们假设把一个系统故障看作一个”prompt 不好”或一个”工具不好”是有建设性的。
在实践中,当一个复合 AI 系统在某项任务上失败,或者需要改变在某项任务上的表现时,我往往需要同时修改所有组件——prompt/上下文、工具和底层模型。反过来,如果一个系统在某个任务上的表现不达标,我们也未必能把故障定位到一个坏的 prompt 或一个坏的模型上;两者都是贡献因素,我们也许可以通过改进其中一个或另一个来解决问题,甚至可能两者都需要改进。
由于”组合实现”背后的假设与现实之间的这种不匹配,在实践中我发现这个范式制造了大量不必要的复杂性,并没有真正让系统更容易理解或演进。
与其组合实现,我们应该让抽象对应到我们希望系统执行的具体的任务和子任务上,通过组合任务来构建更大的系统,就像我们通过组合函数来构建更大的程序一样。
当我说”任务”时,我指的是从输入域到输出域的某种映射,配以输出域上的一组评估标准。对于任何给定的问题,可能有许多不同的方式将问题分解为子任务。以问答系统为例:
- 端到端:作为平凡情况,我们可以把整个端到端问题定义为单个任务,其中输入是用户查询,输出是答案。评估标准可以是简洁性、准确性和相关性的某种组合。作为此定义的一部分,我们也可以定义输入域,把某些问题视为”超范围”的。
- 分解式:许多现实世界中的 QA 系统会把这个任务细分为三个子任务:
- 查询扩展:把用户查询映射为一个或多个重写后针对后端搜索引擎优化的查询
- 检索:将某个内部搜索查询映射为一组排序、过滤后的文档匹配结果
- 生成:将原始用户查询和检索到的文档映射为特定的回复措辞
脱离上下文,一种分解方式并不显然优于另一种。要决定哪种更好,需要了解具体的技术和工程背景。
让我们把组合任务与组合实现做个对比。根据我的经验:
- 未来对 AI 系统的变更往往会仅限于特定的任务。如果我需要为查询扩展升级底层模型,或者为延迟重新设计答案生成子任务——只要该子任务评估表现良好,我就可以相信系统的其他部分能与升级后的版本良好地互操作。变更更好地被隔离在各个子任务内部,意味着抽象边界是真正的边界。
- 在具体任务层面讨论故障,通常比在 prompt/工具/模型层面要容易得多。当我看到一个问答系统输出了不好的答案时,我更倾向于说”如果模型能有正确的上下文,这会好得多”,或”搜索引擎显然没有返回正确的文档”。但要我说”如果 prompt 再好一点就好了”则难得多——也许换个更好的模型也能解决问题。
注意,这种方法与”偏向以端到端方式解决大型问题”并不矛盾。随着技术进步,我可能选择把 QA 问题中的三个子任务打包进一个端到端任务,然后在一个更大的、更复杂的报告生成任务中复用它。我主张的是以输入和输出作为抽象边界,而非管道代码的实现细节;我并不一定主张总是把任务分解成更小的任务。
以组合子任务的方式构建的系统,往往看起来不像传统意义上的”agent”,而更像一个”工作流”或”管道”——每个任务接受某个明确定义的输入,将某个输出返回给下一个任务。在这种设计下,在实现新的工作流时,很自然地会尝试看能否用更小的已有工作流来组装它们。在一个把 prompt 和工具作为抽象单元的世界里,将 prompt 和工具用于新的上下文往往需要调整这些实现细节。相比之下,当用已有任务组装新任务时,抽象边界保持清晰——要么子任务表现良好,对更大任务有益;要么子任务需要表现得更好。如果子任务的输出格式不太对,我们可以把它传递给另一个子任务来获得正确的输出。
这是一种更受函数式编程启发的设计。正如函数式编程的约定往往让复杂逻辑更容易推理一样,按任务组合让复杂 AI 系统更容易推理。
在真实世界中组合任务
当我们以组合任务的方式构建系统时,一个模式会浮现出来:随着底层技术演进并变得更加通用,我们往往需要重新划定子任务之间的边界来获得最佳性能。这是”苦涩的教训”的推论:随着更多的数据和算力可以被用来解决一个问题,用端到端方式解决更多任务变得比将其分解为更小、更容易的任务更有吸引力。
我发现在使用计算机解决软件任务时如此,在与人和软件合作解决团队内部的现实世界任务时也是如此。随着技术进步,通常朝着更通用的能力方向发展,定期检查一组人和软件是否可以通过重新划定任务之间的边界来更好地协作,是有意义的。一种常见的朴素版本是把某个任务从人类执行转向机器执行、人类验证输出,但我认为这丢掉了很多创造力和想象空间。如果某个工作流今天每 N 天发生一次,你可以考虑转向流式设计。如果有许多工作流都依赖于某个看起来非常相似的元素,你或许可以把那个共同的、费力的任务统一到一个程序中,让每个人按需”调用”这个程序,成本大幅降低。这意味着对那个共享子任务的表现和可靠性的任何改进,都会提升所有下游工作流的质量。
当我花更多时间跨 AI、用户界面和理解人群协作的生态系统工作时,我越来越相信这些都是一个更大的系统设计问题中相互关联的小组成部分。AI、UI 和社会协调各自都是一个重要的更广泛问题的一部分:如何设计一个对变化有鲁棒性、且能随时间优雅改进的人机混合系统。当以这种方式看待大型复杂问题时,在一个领域(如界面设计)中运作良好的范式,往往会指向相邻领域(如社会协调)中的类似物。任务组合,感觉就是这样一个原则。
