上周部署的一个Agent任务跑了一个多小时:从市场资料搜集、竞品对比、初稿撰写到最后的报告输出,中间调了十几次工具、发生了两次重试、触发了一次上下文压缩。它在最后五分钟通过校验的时候,我在监控面板上看着它中间三次进入"待定"状态,心里一直在打鼓。
这个经历让我重新想明白一件事:当AI Agent从单次回答走向长任务执行,"工程工作到底发生在哪里"这个问题,答案比大多数人想象的更系统。它不只是一个Prompt写得好不好的问题,也不只是把模型调用包一层的问题。它涉及状态、编排、记忆、并发、可观测性——是一整套后端系统的复杂度,只是这些复杂度以"Agent"的名义被重新组合了一遍。
本文写给正在把AI Agent应用从Demo推向生产的工程师,也写给准备搭建Agent平台的产品和技术负责人。我会从工程落地的角度拆解长任务Agent的关键设计取舍,结合我实际操作过的框架和踩过的坑,把"工程到底发生在哪"这件事讲透。
1. 从单轮问答走到长任务:三个被打破的工程假设
1.1 单轮问答的工程本质
单轮问答(比如"帮我解释一下什么是RAG")本质上就是一个无状态函数调用:输入prompt,输出completion。工程上你只需要关心三件事:把Prompt组织好、调用模型API、解析响应。这跟一次普通HTTP请求没有本质区别。
很多团队在最开始做Agent Demo时也是这么干的:FastAPI起一个路由,里面调模型,把结果返回给前端。这个阶段根本不需要"Agent工程"——你处理的问题跟任何一个LLM应用都一模一样。有意思的是,几乎所有Agent踩坑的开端,都是因为"我觉得它应该能处理更长的任务"这句话。
单轮问答里,工程角色的比重小到什么程度呢?你甚至可以不开日志中间件,不设超时策略,不做状态管理,因为一次调用最多几秒钟,出错了重来一次就行。可一旦任务开始持续几分钟、几十分钟,这些"因为简单而可以省略"的东西,全部会变成必须正面处理的工程问题。
1.2 长任务打破的三个假设
第一个被打破的假设:时间尺度。单轮问答是秒级的,长任务是分钟级甚至小时级的。秒级任务里,HTTP超时、连接池耗尽、浏览器页面刷新、云函数执行时长上限这些问题都不存在;分钟级任务里,它们全部变成需要认真对待的硬约束。你的API网关默认超时可能只有30秒,前端轮询可能隔一分钟就断,后台Worker的可用区还可能因为发布被重启。
第二个被打破的假设:状态连续性。单轮问答不需要关心上一步是什么,长任务却要求第20步始终知道第3步的结果。可LLM本身是无状态的——每一次调用都是独立请求,不保留任何历史。这意味着"记忆"必须由外部系统承担。谁来写、谁来读、怎么更新,在老后端眼里就是一个典型的多进程、多实例状态管理问题,只是换了一层AI的外衣。
第三个被打破的假设:容错门槛。单轮问答失败了大不了重问一次,成本也就一次模型调用。但长任务如果做到第15步挂了,从头再来,意味着前面几十次模型调用和工具调用全部作废。所以你必须设计checkpoint、恢复、重试策略,平台的可靠性能力将成为核心竞争力,而不是锦上添花。
1.3 思维方式的转变:从"调用链"到"任务系统"
单轮问答的工程是调用链工程:请求进来、响应出去,很简单。长任务的工程是任务系统工程:任务需要实例化、状态需要持久化、执行需要调度。
说白了,你不再是在"写一个函数",而是在"写一个后台任务系统"。这个转变是整个Agent工程的分水岭。很多Agent项目做不大,不是模型能力不够,而是工程上根本没为"任务要跑很久、状态要一直存在"做好准备。项目死在Demo阶段,原因往往不是模型不够聪明,而是产品上线后,那个任务跑到第10分钟开始出现各种基础设施层面的怪问题。
理解了这一点,接下来所有讨论就顺了。长任务Agent的工程工作主要发生在五个地方:状态管理、任务编排、上下文与记忆、异步与可靠性、可观测性。下面逐一展开。
2. 状态管理:长任务最容易翻车的地方
2.1 状态里到底存什么
长任务执行过程中需要维护的状态远比想象中多,我习惯分类来看:
- 对话上下文:用户与Agent的历史消息、Agent每一步的推理过程。
- 任务进度:当前执行到哪个节点、哪些步骤已完成、哪些还在pending。
- 工具中间态:某个API返回的原始数据、临时文件、中间计算结果。
- 外部句柄:调用第三方服务时创建的会话ID、临时Token、上传任务ID。
这些看似零散的东西,本质上就是任务能"恢复"和"继续"的依据。如果没有它们,任务中断后只能从头再来。很多人在设计Agent时只关注"Prompt怎么写",完全忽略状态模型,结果就是项目一跑起来,各个节点之间信息传递靠拼字符串,混乱到没法维护。
2.2 状态放在哪里:存储选型对比
按工程复杂度从低到高,我梳理了四个主流选择,它们之间的取舍很清晰:
| 存储方案 | 适用场景 | 主要风险 |
|---|---|---|
| 进程内存 | 本地验证、单机调试 | 进程一重启就丢失,多副本无法共享 |
| Redis | 短期热状态、分布式锁、任务队列 | 大JSON塞进去会拖垮性能和带宽 |
| 关系数据库 | 任务元数据、审计日志、精确查询 | 嵌套结构的灵活状态建模费劲 |
| 事件溯源 | 强审计合规、全量回放 | 理解和实现成本高,非必要不引入 |
我比较推荐的核心组合是:任务的基础元数据和关键状态字段放关系库,热点数据(比如当前节点缓存)放Redis,事件日志单独落一份存储。不要试图用一个存储解决所有问题。Redis不是万能缓存,关系库也不是JSON桶,每种存储都有它擅长的场景。
2.3 Checkpoint:断点续跑的关键设计
Checkpoint本质上是周期性把任务状态序列化。设计要点在于打点的时机。我的做法是在两类节点打checkpoint:一类是每个工具调用的前后,因为工具调用通常有副作用,是出错概率最高的点;另一类是每次LLM调用之前,因为模型调用贵、慢、还可能重复执行。在这两类节点保存完整状态,恢复时从最近的checkpoint重放,能最大限度减少浪费。
还有一个细节很容易被忽略:checkpoint恢复之后,LLM的决策结果可能和崩溃之前不一样。模型本身是不确定的,你用同样的prompt再调一次,出来的结果可能不同。所以我会额外记录一份"决策日志",把当时的prompt、模型名、参数、输出全部钉在任务记录里。这样即使重放后结果变了,你也有据可查,能判断是模型漂移还是状态损坏。
2.4 状态设计的三条排坑经验
第一,状态一定要版本化。长任务跑很久,你很可能中途改状态结构。用JSONB字段加版本号,解析时按版本做兼容迁移,比把schema规范化到死要可靠得多。第二,明确状态的所有权。并行分支、父子任务之间,谁写哪个key要提前约定,不然就会面对大量"同一个状态被两个节点同时改写"的数据竞争问题。第三,只持久化必要内容。很多初写Agent的人喜欢把LLM的完整输出原封不动塞进状态,任务跑几十步后状态体积爆炸。我的习惯是:状态里存结构化摘要,原始完整输出放到对象存储或日志里,按需取用。
3. 任务编排:为什么线性Prompt链在长任务里撑不住
3.1 线性链的死穴:条件、循环与并行
一开始大家都习惯把任务拆成"先做A再做B再做C"的线性链。这个思路在两三步的小任务里完全够用。但真实的长任务充满控制流:
- 条件分支:搜索结果不充分,要不要换数据源再查一次。
- 循环:写完代码跑测试,测试失败要改代码再测,这个动作往往要重复好几轮。
- 并行:同时查三份资料再汇总,比串行省一半时间。
- 补救分支:某个工具调用失败,是等重试,还是换一条路径。
线性链没有能力表达这些控制流。你当然可以让LLM"看着前面的历史自己决定",但这等于把所有工程风险都押在概率上。哪怕模型每一步有99%的概率做对,几十步跑下来整体成功率也会以指数级速度衰减。这不是你的模型写得不够好,这是概率的基本规律。
3.2 从DAG到状态机:图编排的核心思想
现在行业里主流的做法——不管是LangGraph、Temporal还是自研引擎——本质都是把任务建模成一张有向图:节点是执行单元(LLM调用、工具调用、逻辑判断),边是依赖关系。执行引擎沿着图走,每到一个节点就执行对应函数,根据返回结果决定下一步去哪。
与线性链最大的不同是,控制流被显式声明了。这意味着两件事:取消和重试可以精确到节点级别;执行过程可以在任意节点停下、查看、恢复。这两点在单轮问答时代是不可想象的。我看LangGraph的StateGraph时最大的感受是:它本质上就是一个有状态的状态机,state在图里流转,节点函数读取并更新state。理解这个本质之后,用什么框架都不慌了,甚至自己写一个也不难。
3.3 节点状态机:每个节点都不是"一锤子买卖"
工程上一个容易忽略的细节是:每个节点在执行过程中本身也是有状态的。我习惯给节点定义几个状态:pending(还没轮到)、running(正在执行)、success(正常完成)、failed(执行失败)、retrying(失败后等待重试)、canceled(被取消)。
节点状态的转移逻辑是核心工程点。LLM类节点失败,多数情况直接重试即可;工具类节点失败,要看是网络层失败还是业务层失败——网络层可以重试,业务层(比如权限错误)重试一万次也没用,直接failed并走fallback路径。这是我强烈建议自己手写一遍状态机的原因:你能在真实失败中体会到"该在哪个位置做判断"。
3.4 并行分支:快了三倍,也难了三倍
并行是长任务提速最直接的手段,比如同时搜多个数据源。但它引入的麻烦是状态合并——多个分支都往同一个state里写数据,不控制就会互相覆盖。
我的经验是:每个并行分支只操作自己名字空间下的state key(例如research_source_a、research_source_b),等所有分支结束后,由归约节点把各自的输出合并进主状态。合并规则要提前定义,比如追加到列表、取并集、还是覆盖。不要指望模型去"理解"怎么合并,这是纯工程问题,就该用代码保证。
4. 上下文与记忆:长任务执行的分寸拿捏
4.1 上下文窗口:物理边界还是工程边界
128k的上下文看起来很大,但长任务跑十几个步骤之后,把每轮对白、工具返回、中间结果都堆进去,也撑不了多久。而且更长上下文带来两个实际问题:token成本按量涨、模型的有效注意力会下降——业内常说的lost in the middle问题。
所以工程目标不是"尽量塞进去",而是"刚好够用"地把最相关的信息放进prompt。这个思路会引导出一套记忆的分层体系。
4.2 记忆分层:工作记忆、情景记忆、语义记忆
我把长任务涉及的记忆分成三层:
- 工作记忆:当前步骤需要的那一小段信息。比如"我正在生成第三章节的初稿",类似函数栈上的局部变量。
- 情景记忆:整个任务到目前为止发生了什么,以摘要或关键决策日志保存,类似会话历史。
- 语义记忆:长期稳定的知识,比如用户偏好、领域知识、历史项目资料,类似知识库。
每次调用LLM前,我会从三层分别取样,拼装出一个上下文包。这个拼装过程就是长任务Agent工程里最费心思的部分——既不能把模型饿死,也不能让它被无关信息淹没。
4.3 摘要、向量检索与结构化记忆的组合
三种常用手段各有分工:
滚动摘要(Summarization):每执行N步就把历史压缩成一段摘要,旧对话内容不再进prompt。这是成本最低、最常用的手段。向量检索(Vector Retrieval):把已完成步骤的结果向量化,后续需要细节时按语义检索top-k片段放回上下文,适合"过去某个具体数字或结论"这种场景。结构化记忆(Structured Memory):把任务进度、决策点、工具输出整理成固定字段,直接放在state里供代码读取,也拼一小段关键字段进prompt。
我的推荐组合是:summary字段加少量检索片段,再加上当前步骤所需的结构化字段。每次拼装前先按token预算估算。举个例子:假设当前预算上限是8000 token,summary占2000,检索片段占1000,工具上下文占500,留给模型思考的也就5000多。这个预算就是每次调用前要心里有数的数。
4.4 记忆污染:长任务最容易犯的错
这是我在生产环境观察到的最普遍问题:Agent把过时的、不相关的信息一直留在上下文里,导致后续决策被"污染"。
一个经典场景:用户中途改了需求,Agent把旧需求的分析结论继续带进新步骤,越跑越偏。解决办法是在每个关键节点做一个"信息清理"动作——标记哪些信息已过期、哪些是临时中间量、哪些需要归档。类似你在写代码时管理变量生命周期,而不是把所有变量统统放在全局作用域里。做过大型后端系统的人对这个模式应该很熟悉,它只是换了个名字出现在Agent里。
5. 到这一步才算生产:异步、并发与可观测性
5.1 超过1分钟的任务,先解决"等待"问题
从单轮问答到长任务,第一个物理障碍是等待。同步HTTP请求最多撑几十秒,浏览器用户等不了,云函数也有执行时长上限。
所以生产环境的长任务基本上必然走向异步任务架构:用户提交任务后立即拿到task_id,后台用Worker进程慢慢跑,前端轮询或通过SSE推送进度。实际流程就是:用户POST /tasks,主进程把任务写入队列,立即返回task_id;Worker从队列拉任务,创建任务状态,开始执行图;前端通过GET /tasks/{id}轮询进度。整个过程中没有任何环节需要保持HTTP长连接。
我通常用FastAPI接一个任务队列(Redis Stream、Celery或云平台的任务服务),Worker里做长任务执行。这个架构思路跟传统的异步任务系统没有区别。不少Agent项目迟迟上不了生产,卡的就是这一步——大家都在研究怎么设计Prompt,没人处理"任务提交后谁去执行它"这个最基础的问题。
5.2 并发控制:多用户、多任务与LLM限流
并发有两层含义:一是平台上有多个用户同时提交任务,二是单个任务内部有多个并行分支。两层都要管。
用户隔离是第一原则。任务是最基本的隔离单元,状态表、日志表、缓存key都必须带task_id和user_id前缀,不能混用。其次是任务级限流:LLM服务商有QPS限制和每秒token限制,并发任务太多会触发429。需要在任务队列之上再加一层信号量或令牌桶,控制"同时进行中的LLM调用总数"。
关于并发参数的调整,我自己走过的路径是:先用压测脚本测出单任务内的并发上限和单次LLM调用的平均耗时,再反推平台同时执行的合理任务数和单任务并行度。比如你的LLM QPS上限是10,单任务串行执行大概需要20次调用,任务提交高峰是每秒2个,那你需要把同时执行的任务数限制在20个以内,否则必触发限流。这个数字不要拍脑袋定,一定要有压测数据撑着。
5.3 重试与幂等:不确定性是怎么被兜住的
LLM调用天然不稳定:可能超时、可能限流、可能返回JSON解析不了、可能输出一段莫名其妙的文本。所以重试是必须的,但必须讲究策略。
超时和限流:用指数退避加随机抖动重试,通常两三轮就能过去。格式错误:可以重试一次,但重试时最好在prompt里附上"上次解析失败,请严格按模板输出"这类提示。重头戏是幂等:有副作用的工具调用绝对不能盲目重试。发消息、扣库存、写数据库这类操作一旦重复执行,会造成严重事故。
正确做法是为每个外部操作生成一个幂等键(idempotency key),在任务状态里记录"已成功执行的操作列表",重试前先检查这个操作是否已经成功过。这是我见过最多的事故源头:为了对抗不确定性做了重试,结果把不确定性变成了重复执行。工程上的原则应当是:尽量让失败可恢复,但永远不要让同一操作被执行两次。
5.4 可观测性:长任务没有Trace就是摸黑开车
单轮问答看日志就够了,长任务必须上Tracing。我的埋点方案是这样的:
trace_id直接等于task_id,把整条任务链路串起来;每个LLM调用是一个span,记录模型名、输入输出token数、温度、耗时、成本估算;每个工具调用是一个span,记录请求参数、响应状态码、耗时、重试次数;每个节点状态转移都打结构化日志,便于复盘"这个节点为什么会走到failed分支"。
配合结构化日志(每条日志都带task_id,可以一键grep)和节点状态面板,任何一次运行都能被完整回放。我在生产环境排查超时问题、理解模型异常行为时,几乎没有一次不是靠这套Trace定位到具体节点和具体参数的。没有Trace的长任务Agent,出了问题只能靠猜,根本没法救。
6. 从Demo到生产,我的工程检查清单
6.1 先用最小引擎跑通逻辑,再看框架
如果你正在研究LangGraph、CrewAI或者准备自研引擎,我建议先自己写一遍最小执行循环:
def run_workflow(graph, state): node = graph.entry while node != "END": # 执行当前节点函数,传入共享状态 result = graph.nodes[node](state) # 根据节点返回决定下一个节点 node = result.get("next", "END") return state这段不到十行的小循环能帮你彻底理解状态流转:节点函数拿到state,返回增量更新;引擎根据返回值决定走哪条边。等你理解了本质,再去看任何框架的文档都会轻松很多,因为你已经知道它在底层干什么了。
6.2 我至今还记着的三个事故
事故一:上下文塞爆。早期版本把每轮完整记录都往state里堆,任务跑到第30步左右就开始变慢、变贵,最后模型输出质量急剧下降。后来改成summary加检索的组合,问题解决。
事故二:重试导致重复发消息。某个Agent在调用通知服务时超时,重试逻辑直接把同一个通知发了两遍,用户收到了两条一模一样的消息。从那以后,所有带副作用的调用都加了幂等检查和人工确认环节。
事故三:checkpoint存了不完整状态。某次改状态结构时漏了迁移逻辑,恢复任务后节点函数读取到空字段,任务在恢复后三分钟内崩溃。从那以后,每次变更状态结构都会同时更新checkpoint的版本号与迁移脚本,并且把"恢复路径"放进自动化回归测试。
这三个事故本质上都不是模型不够聪明,而是工程系统没兜住。Agent应用上线后出问题,90%以上是这类工程问题,不是模型推理错误。
6.3 一条从简单到复杂的落地路径
我建议按四个阶段演进,每个阶段都要有明确的验收标准:
- 阶段一:单机验证。脚本里写死状态、顺序跑完整个图,验证业务逻辑成立。验收点:业务流程跑通,结果符合预期。
- 阶段二:状态外置。把状态迁到Redis和数据库,实现checkpoint与断点恢复。验收点:中途杀掉进程,重启后能续跑。
- 阶段三:异步与并发。引入任务队列和Worker,实现任务提交、进度查询、并发控制与限流。验收点:多用户同时提交,平台稳定不崩。
- 阶段四:生产加固。补可观测性、审计日志、卡控节点(比如涉及外部写操作前必须人工确认)、权限隔离。验收点:线上运行一周,无未兜住的事故。
多数团队死在第二阶段和第三阶段之间。很多人觉得Demo已经验证过了,生产环境自然而然也能跑——这是最大的幻觉。长任务Agent的生产化,本质上就是把这四个阶段全部补满的过程。
我把一个检索报告Agent从Demo推进到生产的过程中改了三个版本,最深的感觉是:Agent的智能在模型里,但Agent的可靠在工程里。模型的推理能力决定任务的天花板,而状态、编排、记忆、并发、可观测性这些工程细节,决定了你实际能飞多高。希望这篇能把"工程工作到底发生在哪里"这个问题说透,给大家在架构选型和排障时一点实在的参考。