构建智能体,而非流水线——LLM应用的两种范式选择
在计算机程序中使用LLM只有两种方式:作为流水线的一部分,或者作为智能体。换句话说,要么你用代码表达程序的控制流,要么你给LLM提供工具并让它自己管理控制流¹。
下面是一个简单的“汇总一堆信息并通过邮件发送给我”程序作为流水线的样子:
`
context = gather_context(various, data, sources)
llm_response = llm_summarize(context)
summary = parse(llm_response)
email_me(summary, my_email)
`
而作为智能体是这样做的:
`
read_data_tool = build_read_data_tool(various, data, sources)
email_tool = build_email_tool(my_email)
run_agent(tools: [read_data_tool, email_tool])
`
这就像是库和框架之间的区别。当你使用库时,你自己定义程序的结构,并在过程中调用各种库辅助函数。当你使用框架时,程序的主要结构存在于框架中,框架在特定点调用你的代码。两种方法都有其权衡。框架让你更快上手,通常“免费”提供功能,但当你想做框架设计之外的事情时可能会很困难。库给你更多控制权,但要求你编写(和维护)更多样板代码。
在简单情况下,流水线和智能体之间的区别消失了。如果你只有几段可能的上下文,那么一个带有gather_context和email_me工具的智能体将执行与一个将上下文注入提示词并调用推理模型的流水线完全相同的步骤(即智能体会重现你那简单的流水线控制流)。但当你拥有超过单个提示词容量的上下文,或者你想采取行动然后对结果做出反应时,流水线和智能体之间的选择就变得非常重要。
可预测性、灵活性与智能
流水线更可预测,但智能体更灵活。当你把一个问题交给智能体时,当LLM认为它完成时工作就停止了。根据问题的感知难度,这可能需要几轮LLM到数百轮(因此花费从几美分到许多美元不等)。如果你构建的是打算大规模运行的东西,这种不可预测性可能是一场噩梦。用户数据的任何细微变化都可能导致LLM在每个任务上花费两倍的时间,这会使你的延迟²和成本翻倍。
流水线只有在不使用推理模型,或者不允许模型在其输出token中“自言自语”(例如通过使用结构化输出)时才能免受此问题的影响。然而,单个LLM对模型推理的控制要比对智能体循环持续时间的控制严格得多。在所有前沿模型API中,你可以显式设置所需的推理级别。这不会给你完全的控制权,但确实将“花费更长时间”限制在百分之十或二十(而对于智能体,可能是两倍或更多)。
那么为什么要使用智能体呢?智能体更聪明。如果你愿意接受这种不可预测性,智能体系统可以处理更困难的任务,因为它能够循环更长时间,并且在思考问题后收集更多信息。最成功的AI产品(如Claude Code、Codex、Cursor和Copilot³这些编码智能体)都是智能体,这是有原因的:编码是一项足够困难的任务,你根本无法用流水线构建一个功能性的编码智能体。
上下文收集
上下文收集阶段对于流水线来说远比对于智能体更为微妙。如果智能体试图解决一个问题并意识到需要更多数据,它可以简单地去获取。但对于流水线,所有必需的数据都必须已经存在于上下文中,因为LLM只运行一次。
构建流水线的许多工作在于正确地收集上下文。智能体则容易得多。例如,对于编码智能体,你基本上只需提供一个“grep”和“读取文件”工具,让智能体自己找出哪些代码块与当前文件相关。而在流水线中,你必须自己解决这个问题:祝你好运,这是一个尚未解决的技术问题!通常你最终会使用一些巧妙的技巧,比如遍历AST来识别哪些代码部分“贡献”给当前文件,或者用语义嵌入对整个代码库进行索引,并做某种最近邻搜索来构建上下文(称为RAG,即“检索增强生成”)。这些都不如使用智能体效果好。
在2023年和2024年,许多人相信RAG能解决上下文收集问题。每个LLM都会有一个完全索引的上下文库,神奇地浮现出LLM在任意给定时刻所需的精确信息。但这并没有发生。相反,我们后退了,让我们的智能体进行纯文本搜索,像人类一样自己弄清楚。为什么RAG不起作用?这是另一篇文章的主题,但简短的答案是:“找出哪些信息与这个问题相关”通常和实际解决问题一样困难。语义嵌入和余弦相似度根本不足以胜任这项工作。
多模型流水线
多次调用LLM的流水线确实有一个额外的灵活性维度:它们可以为不同任务使用不同的LLM。例如,如果一个LLM在任务A上基准测试更好,或者对于更容易的任务B更便宜,你可以为任务选择合适的模型。而智能体(至少目前)必须全程保持同一个模型,所以你总是被钉在你需要的最高智能水平上。
这很重要吗?我持怀疑态度。我经常看到的一种模式是用一个更便宜的模型来整理或总结数据,供一个更聪明的模型使用。但信号往往就在原始数据本身!我认为这样的设计实际上是在搬起石头砸自己的脚,原因与RAG失败相同:上下文收集比人们预期的更难。
无论如何,如果你确实想将任务分派给不同的模型,你也可以通过精心设计的智能体工具来实现。例如,你可以构建你的web_search工具,让它使用一个便宜的模型来总结网页。
小上下文与面向未来
流水线允许使用较小的上下文,从而可以使用本地模型。智能体自己获取上下文的能力意味着它几乎总是摄取比所需更多的数据。除此之外,智能体以循环方式运行,因此每一轮智能体都会增加上下文的大小。对于构建在前沿模型API之上的系统来说,这不是一个大问题,因为:
- 前沿模型都暴露了大的上下文窗口, - 前沿模型在前20万token内通常表现良好, - KV缓存意味着传递相同的大上下文块出奇地便宜。
然而,这对于本地模型来说是一个大问题。上下文窗口消耗大量VRAM,因此大多数运行本地模型的人将上下文保持在3.2万(甚至6千)token以下。如果你正在编写一个在这种环境下运行的程序,你可能无法给智能体所需的空间,而将被迫使用流水线。
在我看来,智能体更面向未来。部分原因是模型现在被明确构建为更好的智能体,部分原因是智能体将更多工作委托给LLM,从而从LLM改进中获益更多。如果你有一个基于流水线的系统,新模型可能比旧模型表现稍好。如果你有一个智能体系统,新模型可能比旧模型表现好得多(以至于值得为当前太困难的任务构建智能体系统,假设到完成时模型可能已经足够好)。我从2023年开始就一直强调这一点,那时工具调用甚至还不是模型API的一部分。
安全性与可读性
总的来说,我不同意那种认为工作流比智能体更安全的流行建议。工作流在预算上提供了更多控制,但在基于LLM输出采取行动时,你面临的问题完全一样——无论是在工具调用层面检查还是在流水线的下一个阶段检查:要么你通过代码进行某种启发式评估,这可能是错误的;要么你将行动排队等待人类批准,这将很慢。
智能体会让你更容易受到提示注入攻击吗?是的,但流水线也是如此。在这两种情况下,你都将某个人类生成的数据块(例如代码库中的文件,或网络搜索的结果)输入给LLM。这些数据中的任何提示注入都会被LLM同样吸收,无论它们是工具调用的结果还是由流水线直接注入到提示词中。你必须净化用户内容并仔细检查LLM触发的行动,无论你选择哪种设计⁴。
我确实承认流水线稍微更具可读性。你可以追踪流水线的大部分行为,因为你控制了更多的内容。要弄清楚为什么智能体查询了某条特定信息或采取了某个行动则更困难。但即使在流水线中,你也永远无法确切知道LLM为什么以那种方式回应。这就是用LLM编程的本质。
LLM驱动的大规模监控
让我们将这些原则应用于一个真实世界的非平凡例子。假设你是NSA,试图使用LLM来掌控秘密电子邮件监控数据的疯狂洪流⁵⁶。你应该使用流水线还是智能体?好吧,如果你正在构建一个旨在处理美国每一封电子邮件的东西,你可能不应该使用智能体:严格限制性能和成本需要流水线。然而,你显然有足够的资源来普遍使用智能体,而且这个问题也足够困难,可以从额外的智能中受益。我可能会建议两者都使用:一个低上下文、廉价的流水线,可以对每封电子邮件运行一次并进行标记;以及一群智能体,可以深入调查这些标记,进行常规查询,并更像人类分析师那样行动。
流水线必须随着数据总量扩展,这应该基本没问题,因为流水线以可预测的方式扩展。但这群不可预测的智能体可以完全独立地扩展,尽管在实践中它会受到GPU可用性和人类审查必要性的瓶颈。大部分工程工作⁷可能会投入到流水线的上下文组装中:提供足够多的关于邮件会话中涉及的人的数据,以便LLM能够做出是否标记该邮件的明智决定。
总结
总的来说,我建议遵循以下指导原则:
- 当对上下文大小有严格限制时,使用流水线 - 当需要准确预测(或限制)GPU成本时,使用流水线 - 当必须使用本地模型时,使用流水线 - 当你不确定自己能一次性组装所有相关上下文时,使用智能体 - 当问题足够困难,你不确定流水线能否解决时,使用智能体
如有疑问,使用智能体。据我所知,过去一年中有几个AI项目从流水线迁移到了智能体,但没有一个反向迁移。作为软件设计的一般要点:如果你不确定该做什么,选择更容易构建且更有可能解决实际问题的方案。如果你以后想切换到更便宜的基于流水线的系统,至少你可以将其与一个有效的智能体设计进行比较,并做出明智的决定。