Deep Agents生产运行时:持久执行与全栈可观测基础设施
本文由@sydneyrunkle和@Vtrivedy10撰写,最初发表于LangChain博客。
核心要点
一个好的框架能为你的智能体提供正确的提示词、工具和技能。但在生产环境中部署长时间运行的智能体,需要持久化执行、记忆、多租户、人机协同和可观测性。这些基础设施位于框架之下,确保智能体在崩溃、部署和长时间运行任务中可靠运行。
持久化执行是所有其他功能的基础。运行数分钟或数小时的智能体、为等待人工审批而暂停的智能体、或在部署中途仍能存活的智能体,都需要支持检查点执行的机制,使其能够在进程边界间停止、恢复和重试。流式传输、人机协同、定时任务和并发消息处理都建立在此之上。
生产环境中的智能体需要开放且与模型无关的基础设施。Deep Agents采用MIT许可证,智能体通过开放协议(MCP、A2A)暴露,记忆存储在你自己的PostgreSQL中。团队可以完全了解智能体的工作方式,并能在无需重写的情况下进行修改。
在生产环境中部署长周期智能体需要专门构建的基础设施。本指南涵盖持久化执行、记忆、人机协同、可观测性,以及deepagents deploy如何将这一切交付到生产环境。
要构建一个好的智能体,你需要一个好的框架。要部署这个智能体,你需要一个好的运行时。
框架是你围绕模型构建的系统,帮助智能体在其领域内成功运行。它包括提示词、工具、技能,以及任何支持模型和工具调用循环(这是智能体的核心)的其他内容。运行时则是底层的一切:持久化执行、记忆、多租户、可观测性,以及确保智能体在生产环境中运行而无需团队重新发明的机制。
本指南将介绍部署智能体后出现的生产需求、满足这些需求的运行时能力,以及deepagents deploy如何将这些能力打包成可交付的解决方案。
生产环境智能体的运行时能力
在本节中,“运行时”指LangSmith Deployment(LSD)及其Agent Server:LSD在生产环境中运行智能体,而Agent Server是助手、线程、运行、记忆和计划任务的接口。下表将每个生产需求映射到满足该需求的运行时原语。
持久化执行
智能体通过运行一个循环来工作:给定一个提示词,模型进行推理、调用工具、观察结果,然后重复,直到它认为任务完成。
与典型的毫秒级返回的Web请求不同,这个循环可能持续数分钟或数小时。单次运行可能涉及数十次模型调用、生成子智能体,或无限期等待人类审批草稿。循环中任何位置的崩溃、部署或瞬时故障都不应抹去之前已完成的工作。
在实践中,你会在两个地方感受到这一点:
长时间运行需要能够承受基础设施故障。一个花费二十分钟收集资料并综合研究结果的智能体,如果工作进程崩溃,不能从头重新开始:智能体已经支付了token成本并执行了工具调用。你需要的是从最后完成的步骤恢复,并保留所有先前状态。
智能体需要能够停止并等待。一个暂停等待人类审批交易的智能体,不知道人类会在三十秒还是三天后响应。在这整个时间段内占用工作进程或客户端连接是不可行的。智能体需要真正停止:释放资源、释放工作进程,然后从之前中断的确切位置继续执行。
这两个需求都由同一件事解决:持久化执行。
智能体在带有自动检查点功能的管理任务队列上运行,因此任何运行都可以从中断的确切点进行重试、重放或恢复。
图执行的每个超级步骤都会将检查点写入持久化层(默认是PostgreSQL),以thread_id为键,该ID充当运行中的持久化游标。
当工作进程崩溃时,运行的租约被释放,另一个工作进程从最新的检查点接手。
当智能体等待人类输入时,进程交出它的槽位,运行无限期休眠,直到恢复。
可配置的重试策略控制退避、最大尝试次数以及每个节点上哪些异常会触发重试。
持久化是此列表中其余功能的基础。由于执行可以在进程边界间暂停和恢复,智能体可以无限期等待人类输入、在后台运行、在部署中途存活,并在不破坏状态的情况下处理并发输入。
记忆
智能体需要两种不同类型的记忆,这种区别很重要。
短期记忆是智能体在单次对话中积累的内容。交换的消息、进行的工具调用、在运行中构建的中间状态。这些存在于线程的检查点中,作用域为thread_id,并在对话结束时(概念上)消失。同一线程上的后续消息可以看到该线程上之前的所有内容。
长期记忆是智能体跨对话携带的内容。这可以包括跨对话学习的用户偏好、项目约定和最佳实践,或每次新查询都会增强的知识库。这些都不属于任何单个线程。它是用户级或组织级的上下文,应在智能体的每次对话中持续存在。仅靠检查点无法做到这一点,因为检查点状态的作用域是单个线程。
长期记忆正是Agent Server内置存储的用途。它是一个键值接口,记忆按命名空间元组(例如,(user_id, "memories"))组织,并跨线程持久化。你的智能体在一次对话中写入存储,在下次对话中读取。默认由PostgreSQL支持,它通过嵌入配置支持语义搜索,因此智能体可以根据含义而非精确匹配来检索记忆,如果你需要不同的存储特性,可以替换为自定义后端。命名空间结构灵活:可以按用户、助手、组织或任何适合你数据模型的组合来限定作用域。
由于积累数月的记忆是系统产生的最有价值的数据之一,它存储在哪里至关重要。该存储可直接通过API查询,如果你自行托管,它位于你自己的PostgreSQL实例中。将数据保存在你能控制的标准格式中,这让你能够在模型之间迁移、分析数据,或在智能体之外基于它进行构建。
多租户
一旦你的智能体服务多个用户,就会出现单用户模式下不存在的一系列问题。这些问题分为三个不同的关注点,Agent Server用各自的原语处理每个关注点。
隔离一个用户的数据与另一个用户的数据。用户A的运行应只触及用户A的线程,且只读取用户A的记忆。自定义身份验证作为中间件在每个请求上运行:你的@auth.authenticate处理程序验证传入的凭证并返回用户的身份和权限,这些信息会附加到运行上下文中。使用@auth.on.threads、@auth.on.assistants.create等注册的授权处理程序,通过在创建时用所有权元数据标记资源,并在读取时返回过滤字典,来强制执行谁可以查看或修改什么。处理程序从最具体到最不具体进行匹配,因此你可以从一个全局处理程序开始,随着模型增长添加特定于资源的处理程序。
让智能体代表用户行事。智能体通常需要使用用户的凭证调用第三方服务——读取他们的日历、发布到他们的Slack、在他们的仓库中打开PR。Agent Auth为此模式处理OAuth流程和令牌存储,因此智能体在运行时获得用户作用域的凭证,而无需你自行管理刷新流程。用户只需验证一次;智能体可以在后续运行中代表他们行事。
控制谁可以操作系统本身。与最终用户访问分开,还有你的团队中哪些成员可以部署智能体、配置它们、查看追踪或更改身份验证策略的问题。RBAC处理这种操作员级别的访问控制。
这三个层次组合在一起:最终用户通过你的身份验证处理程序进行身份验证,智能体通过Agent Auth调用第三方服务,你的团队在RBAC策略下操作部署。
人机协同(HITL)
智能体通过运行一个循环来工作:给定一个提示词,模型进行推理并决定调用工具、观察结果,然后重复,直到它认为手头的任务完成。大多数时候,你希望这个循环不间断地运行。这就是价值所在。但有时,你需要在关键决策点让人类介入循环中间。
有两种常见情况会出现这种情况:
审查提议的工具调用。在智能体执行重要操作(发送电子邮件、执行金融交易、删除文件)之前,你希望人类确切看到它即将做什么,并决定如何回应。以电子邮件为例:智能体起草一条消息并在发送前暂停。你可以按原样批准、在发送前编辑主题或正文、或附带理由和具体编辑请求拒绝,以便智能体修改后重试。
智能体提出澄清性问题。有时智能体会遇到它无法自行解决的决策点,不是因为缺少工具,而是因为正确答案取决于人类的判断或偏好。智能体无需猜测,可以直接提出问题:“我找到了三个匹配该模式的配置文件。应该修改哪一个?”或“应该部署到staging还是生产环境?”你的回答成为中断的返回值,智能体从它停止的确切位置继续执行。
Agent Server用两个原语处理这个问题:interrupt()暂停执行并向调用者展示一个负载;Command(resume=...)用人类的响应继续执行。它们共同让你能够构建审批关卡、草稿审查循环、输入验证,以及任何需要人类在运行中参与的工作流程。
在底层,interrupt()触发运行时的检查点器将完整的图状态写入持久化存储,以thread_id为键,该ID充当持久化游标。然后进程释放资源并无限期等待。与在特定节点之前或之后暂停的静态断点不同,interrupt()是动态的:将其放置在你的代码中的任何位置,包裹在条件语句中,或嵌入工具函数内部,以便审批逻辑随工具一起移动。当Command(resume=...)在数分钟、数小时或数天后到达时,resume值成为interrupt()调用的返回值,执行从它停止的确切位置继续。由于resume接受任何JSON可序列化的值,响应不限于批准/拒绝:审查者可以返回编辑过的草稿,人类可以提供缺失的上下文,下游系统可以注入计算结果。当并行分支各自调用interrupt()时,所有待处理的中断会一起呈现,可以在一次调用中全部恢复,或随着响应返回逐个恢复。
实时交互
人机协同是一种交互模式,其中执行可以暂停以供人类审查或提供输入——有时立即,有时更晚。另外,当智能体在用户在场时积极工作时,会出现“实时会话”问题:使进度可见(流式传输)和协调并发消息(双重输入)。
流式传输
一个需要三十秒才能产生响应的智能体会让用户盯着旋转图标,无法得知它是在取得进展、卡住还是即将失败。他们也无法在全部完成之前开始阅读答案。流式传输解决了这两个问题:部分输出在智能体生成时流向客户端,因此用户实时看到响应成形。
流式API支持多种模式,取决于你想要的粒度:每个图步骤后的完整状态快照、仅状态更新、逐token的LLM输出、或自定义应用程序事件。你也可以组合它们。运行流式传输(client.runs.stream())的作用域是单次运行;线程流式传输(client.threads.joinStream())打开一个长期连接,传递线程上每次运行的事件,当后续消息、后台运行或HITL恢复都在同一线程上触发活动时,这很有用。
线程流式传输支持通过Last-Event-ID头进行恢复:客户端使用它收到的最后一个事件的ID重新连接,服务器从那里重放,没有间隙。没有这个,每次连接断开都意味着客户端要么错过输出,要么必须重新开始。
双重输入
第二个实时问题:用户在智能体仍在处理上一条消息时发送了一条新消息。这在聊天UI中经常发生。有人输入一个问题,意识到他们本意略有不同,然后在第一次运行完成前发出了更正。我们称之为双重输入,运行时必须决定如何处理它。
有四种策略,正确的选择取决于你的应用程序:
enqueue(默认):新输入等待当前运行完成,然后顺序处理。
reject:在当前运行完成前拒绝任何新输入。
interrupt:暂停当前运行,保留进度,并从该状态处理新输入。当第二条消息建立在第一条消息之上时很有用。
rollback:暂停当前运行,回滚包括原始输入在内的所有进度,并将新消息作为全新运行处理。当第二条消息取代第一条消息时很有用。
interrupt提供最灵敏的聊天体验,但要求你的图干净地处理部分工具调用(中断发生时已启动但未完成的工具调用可能在恢复时需要清理)。enqueue是最安全的默认选项——没有状态损坏,代价是让用户等待。
护栏
并非每个生产问题都可以表述为“持久化运行循环”。有些问题必须塑造循环本身:拦截模型输入、过滤工具输出、对昂贵操作施加限制。这些策略属于代码,而非提示词。它们需要每次运行,而不是模型碰巧记住它们的时候。
两个具体案例说明了这一点:
在模型看到敏感数据之前进行编辑。一个客户支持智能体处理包含PII(姓名、电子邮件、账号)的用户消息。你不希望模型看到它们,你不希望它们出现在追踪中,合规性可能要求在记录前进行编辑。这必须在每次模型调用之前确定性地发生。
限制昂贵操作。一个可以调用付费外部API的智能体需要硬性限制每次运行调用该API的次数,因为一个困惑的模型会愉快地调用它五十次,在午餐前烧光你的预算。
两者都由中间件处理,它包裹了