1. 从"一问一答"到"跑一整条链路",工程重心到底挪到了哪
很多人第一次接触 Agent,脑子里想的还是"更聪明的聊天框"——问一句,答一句,最多加个工具调用。但真把 Agent 放到生产环境里跑长任务,比如让它自己规划一次数据迁移、自己盯一个持续两小时的爬取流程、自己根据中间结果反复调整策略,你会发现一个很反直觉的事实:模型能力往往不是瓶颈,工程才是。
我最早做 Agent 项目时也踩过这个坑。当时觉得只要 Prompt 写得够好、模型选得够强,任务自然能跑通。结果一个"帮我整理这批文档并生成摘要"的任务,单次调用跑得挺漂亮,一旦改成"分批处理 200 个文件、失败要重试、中间要落盘、最后要汇总",整个系统就开始各种诡异行为:上下文越滚越长导致后面完全跑偏、某个工具调用超时后整个循环卡死、重试逻辑把已经成功的步骤又跑了一遍。这些问题没有一个是模型"不够聪明"造成的,全是工程问题。
所以这篇想聊的核心就一句话:当 Agent 从单次回答走向长任务执行,工程工作发生的位置,从"怎么问"转移到了"怎么管"。管什么?管上下文、管循环、管状态、管失败、管并发、管安全边界。这也是为什么最近 Context Engineering、Loop Engineering、Graph Engineering 这些词会密集出现——它们本质上都是在回答"长任务 Agent 的工程该做在哪一层"。
这篇文章适合两类人看:一类是已经能跑通单次 Agent 调用、但一上长任务就翻车的开发者;另一类是正准备搭 Agent 框架、想提前知道坑在哪里的同学。我会尽量把每个环节"为什么这么做"讲清楚,而不是只丢一堆概念。下面这张表先给个全局印象,后面逐层展开。
| 阶段 | 工程重心 | 典型问题 | 对应工程词 |
|---|---|---|---|
| 单次回答 | Prompt 措辞、格式约束 | 答非所问、格式不对 | Prompt Engineering |
| 多步工具调用 | 工具描述、参数校验 | 调错工具、参数幻觉 | Tool/Function Calling |
| 长任务执行 | 上下文、循环、状态、失败恢复 | 跑偏、卡死、重复执行 | Context / Loop Engineering |
| 多 Agent 协作 | 编排、通信、职责边界 | 互相等待、责任不清 | Graph Engineering |
2. 长任务 Agent 和单次回答,差的不是"步数"而是"状态"
2.1 单次回答为什么简单:它是无状态的
单次回答的本质是无状态函数:输入一段文本,输出一段文本,调用结束,一切归零。你不需要关心上一次说了什么,因为根本没有"上一次"。这种模式下,工程复杂度几乎全部集中在输入端——也就是 Prompt 怎么写、few-shot 怎么给、输出格式怎么约束。这就是 Prompt Engineering 的主场。
无状态带来的好处是巨大的:可预测、可重试、可并行。同一个请求发十次,结果大差不差;失败了直接重发,没有任何副作用。所以单次回答的工程问题,本质上是"输入优化问题"。
2.2 长任务为什么难:它是有状态的,而且状态会累积
一旦任务需要多步执行,Agent 就变成了有状态系统。它要记住前面做了什么、当前在哪一步、下一步该干嘛、哪些中间结果还有用。这些"记忆"要么塞进上下文,要么落到外部存储。而状态一旦存在,所有分布式系统的老问题就全来了:
- 状态一致性:上下文里记的和外部存储里存的对不上怎么办?
- 状态膨胀:上下文越滚越长,模型注意力被稀释,后面开始胡说。
- 状态恢复:跑到一半崩了,怎么从断点续跑而不是从头再来?
我见过太多项目,单次调用效果惊艳,一改成循环执行就原形毕露。原因就是开发者还在用"无状态思维"写"有状态系统"——每次循环都把完整历史塞进去,既不裁剪也不摘要,跑到第十几轮上下文就爆了,模型开始忽略早期指令,任务彻底跑偏。
2.3 一个具体的翻车场景
举个我实际遇到的例子。任务是"遍历一个目录下的所有 Markdown 文件,逐个提取关键信息并汇总成一张表"。听起来简单,但拆开看:
- 先列目录,拿到文件列表(工具调用)
- 对每个文件读取内容(工具调用,可能几十次)
- 对每个文件内容做信息提取(模型调用,可能几十次)
- 汇总所有提取结果(模型调用)
如果每步都把完整历史塞进上下文,到第 20 个文件时,上下文里已经堆了 19 个文件的完整内容加 19 次提取结果。这时候模型面对的是一个几万 token 的"历史包袱",它要么开始偷懒只处理最近几个文件,要么把不同文件的信息串在一起。这不是模型不行,是我没做上下文管理。
正确的做法是:文件内容读完、提取完就丢弃原文,只保留提取结果;提取结果用结构化格式(比如 JSON)存到外部,上下文里只放一个"进度指针"和"已完成的摘要"。这样无论处理 10 个还是 200 个文件,上下文大小都是可控的。
3. Context Engineering:长任务里最容易被低估的一层
3.1 上下文不是"越多越好",而是"越准越好"
新手最容易犯的错,就是觉得上下文给得越全,模型表现越好。这在单次回答里勉强成立,在长任务里是灾难。原因有两个:
第一,注意力是有成本的。上下文越长,模型对每一部分的关注度越被稀释。你塞进去 50 条历史,模型可能只认真看了最近 5 条,前面 45 条变成了"背景噪音",甚至干扰判断。
第二,上下文是花钱的。每一轮循环都带着完整历史,token 消耗是线性甚至平方级增长的。一个跑 100 轮的任务,如果每轮都带全量历史,成本会高到离谱。
所以 Context Engineering 的核心命题是:在每一轮,只给模型"此刻真正需要的信息"。这需要你主动设计,而不是被动累积。
3.2 上下文分层:把信息按"生命周期"分类
我的做法是把上下文里的信息按生命周期分成几层,每层用不同的管理策略:
| 信息类型 | 生命周期 | 管理策略 |
|---|---|---|
| 系统指令/角色设定 | 全程不变 | 固定放在最前,永不裁剪 |
| 任务目标 | 全程不变 | 固定,但可精简表述 |
| 当前进度/状态 | 每轮更新 | 只保留最新,覆盖旧的 |
| 近期操作历史 | 短期 | 滑动窗口,只留最近 N 轮 |
| 早期操作摘要 | 长期 | 定期压缩成摘要,替换原文 |
| 工具返回的大块数据 | 用完即弃 | 提取关键信息后立即丢弃原文 |
这张表是我踩了无数坑之后总结的。关键在于区分"必须常驻"和"用完即弃"。系统指令和任务目标必须常驻,否则模型会忘记自己是谁、要干嘛;而工具返回的原始数据,一旦提取出有用信息,原文就该扔掉。
3.3 上下文压缩:什么时候压、怎么压
上下文压缩是长任务的必修课。我的经验是不要等到快满了才压,而是按轮次定期压。比如每完成 5 轮操作,就把这 5 轮的详细历史压缩成一段摘要。
压缩的方式有几种,各有取舍:
- 规则压缩:按固定模板提取关键字段(比如"第 X 步:调用了 Y 工具,结果是 Z")。优点是稳定、便宜,缺点是可能丢细节。
- 模型压缩:让模型自己总结前几轮做了什么。优点是灵活、能抓重点,缺点是额外消耗一次调用,且可能总结失真。
- 混合压缩:关键步骤用规则精确记录,次要步骤用模型概括。
我一般用混合方式。对于"状态变更类"的操作(比如写文件、改数据库),用规则精确记录,因为这些不能出错;对于"探索类"的操作(比如搜索、试错),用模型概括,因为细节不重要。
提示:压缩摘要本身也要控制长度。我见过有人把 10 轮历史压成一段 2000 字的摘要,那等于没压。摘要应该控制在原文的 10% 到 20%。
3.4 一个反直觉的经验:有时候要主动"遗忘"
这一点很多人想不到。长任务里,主动丢弃某些信息反而能提升表现。比如一个任务前期走了弯路,试了几个错误方案,这些失败尝试如果一直留在上下文里,会持续干扰模型后续判断——它可能反复纠结于"之前为什么失败",而不是专注当前方案。
我的做法是:当确认某个方向彻底放弃后,把相关的失败历史从上下文里清掉,只在摘要里留一句"方案 A 已排除,原因是 X"。这样既保留了教训,又不让细节拖累后续推理。
4. Loop Engineering:让 Agent 循环"转得动"也"停得下"
4.1 循环的本质:一个带退出条件的 while
长任务 Agent 的执行骨架,说白了就是一个循环:
初始化状态 while 任务未完成 and 未超限: 构造当前上下文 调用模型决策 执行工具/动作 更新状态 检查退出条件 返回结果看起来简单,但每一行都是坑。Loop Engineering 要解决的就是这个循环怎么设计才既"转得动"(能推进任务)又"停得下"(不会死循环或提前退出)。
4.2 退出条件:最容易写错的地方
退出条件是循环的命门。写得太松,Agent 会无限循环;写得太紧,任务没完成就退出了。常见的退出条件有几类:
- 任务完成信号:模型明确输出"完成"或返回最终结果。
- 步数上限:硬性限制最多跑 N 步,防止失控。
- 时间上限:超过 X 秒强制退出。
- 无进展检测:连续 K 步状态没有实质变化,判定卡死。
- 错误上限:连续失败 M 次,放弃。
我的经验是这几类要组合使用,不能只靠一种。只靠"任务完成信号"最危险,因为模型可能永远不认为自己完成了;只靠步数上限又太粗暴,可能任务差一步就完成却被砍掉。
实际配置时,我会设一个宽松的步数上限(比如预估需要 20 步,就设 50 步),配合"无进展检测"作为主要退出机制。无进展检测怎么判断?比较连续几步的状态快照,如果关键字段(比如已完成的任务项数量)没变化,就认为卡住了。
4.3 循环里的"防重复执行"
这是长任务里特别隐蔽的坑。Agent 在循环中可能因为上下文丢失或判断失误,把已经做过的操作又做一遍。比如已经写过的文件又写一次、已经发过的请求又发一次。
防重复的核心是幂等性设计 + 操作记录。具体做法:
- 每个有副作用的操作,执行前先查"操作记录",看是否已执行过。
- 操作记录用唯一标识(比如"文件路径 + 操作类型")作为 key。
- 对于无法幂等的操作(比如发消息),要么加去重 ID,要么在执行前做二次确认。
我在一个批量处理项目里就吃过这个亏:Agent 重试时把已经处理成功的文件又处理了一遍,导致结果里出现重复条目。后来加了操作记录表,每次执行前先查表,问题才解决。
4.4 循环的"节奏控制"
还有一个容易被忽略的点:循环跑太快未必是好事。如果 Agent 每一步都立刻执行下一个动作,中间没有"思考间隙",它可能陷入局部最优——比如反复微调同一个参数,而不是跳出来换个思路。
我的做法是在循环里加入阶段性回顾:每跑 N 步,强制插入一次"总结当前进展、评估方向是否正确"的步骤。这一步不执行具体操作,只做反思。实测下来,这能显著减少 Agent "钻牛角尖"的情况。
5. Graph Engineering:当单个循环不够用时的编排
5.1 什么时候需要从"循环"升级到"图"
单个循环能搞定线性任务,但现实任务往往不是线性的。比如一个内容生产 Agent:先调研、再写初稿、再审核、审核不过要回到写作、审核过了要排版、排版后要发布。这里面有分支、有回退、有并行,用单个 while 循环表达会非常别扭。
这时候就需要 Graph Engineering——把任务建模成一张有向图,节点是执行单元,边是流转条件。这其实就是把工作流编排的思路引入 Agent。
5.2 图结构的几个关键设计
设计 Agent 图时,我关注这几个点:
- 节点粒度:一个节点应该是一个"语义完整的动作",比如"调研"是一个节点,"写初稿"是一个节点。粒度太细会导致图过于复杂,太粗又失去灵活性。
- 边的条件:边不是无条件的,而是带判断的。比如"审核通过 → 排版"、"审核不通过 → 回到写作"。
- 状态传递:节点之间传递什么?是整个上下文,还是只传关键状态?我倾向于只传关键状态,避免上下文在图里无限膨胀。
- 终止节点:图必须有明确的终点,否则会绕圈。
5.3 多 Agent 协作:图的一种特殊形态
多 Agent 本质上是图的一种实现——每个 Agent 是图里的一个节点或子图。但多 Agent 引入了新的工程问题:通信和职责边界。
我见过不少多 Agent 项目失败,不是因为 Agent 不够强,而是因为职责没划清。比如两个 Agent 都能"修改文件",结果互相覆盖;或者一个 Agent 等另一个 Agent 的输出,但对方根本没打算产出。
我的经验是:每个 Agent 的职责要能用一句话说清,且不重叠。如果两个 Agent 的职责描述有交集,就该合并或重新划分。另外,Agent 之间的通信最好走结构化消息(比如 JSON),而不是自然语言,减少误解。
| 协作模式 | 适用场景 | 工程要点 |
|---|---|---|
| 串行流水线 | 步骤明确、前后依赖 | 状态传递要精简 |
| 并行分工 | 子任务独立 | 结果合并要处理冲突 |
| 主从调度 | 一个协调者 + 多个执行者 | 协调者不能成为瓶颈 |
| 辩论/评审 | 需要多视角验证 | 控制轮次,防止无限争论 |
6. 长任务 Agent 的失败恢复与并发处理
6.1 失败是常态,不是异常
长任务跑得越久,遇到失败的概率越高。网络抖动、工具超时、模型返回格式错误、外部服务限流……这些在单次回答里是"异常",在长任务里是"日常"。所以失败恢复不是可选项,是必选项。
我的原则是:每个可能失败的操作都要有明确的失败处理策略。策略无非几种:重试、跳过、降级、终止。选哪种取决于这个操作的重要性:
- 关键操作(比如写数据库):失败必须重试,重试失败则终止整个任务。
- 次要操作(比如日志上报):失败可跳过,不影响主流程。
- 可替代操作(比如主模型失败):降级到备用方案。
6.2 断点续跑:让任务能从中间恢复
长任务最怕的是"跑到 90% 崩了,只能从头再来"。解决这个的关键是状态持久化:把任务的当前状态定期落盘,崩溃后能从最近的检查点恢复。
状态里要存什么?至少包括:当前执行到哪一步、已完成的操作记录、中间结果、上下文摘要。恢复时,从这些状态重建上下文,继续往下跑。
这里有个细节:检查点的粒度要合适。太粗(比如整个任务只存一次)等于没存;太细(每步都存)开销太大。我一般按"语义阶段"存,比如每完成一个子任务存一次。
6.3 并发:Agent 怎么扛住同时跑多个任务
单个长任务已经够复杂,如果要同时跑几十上百个任务,问题会指数级放大。核心挑战有三个:
- 资源竞争:多个任务同时调用同一个工具或模型接口,可能触发限流。
- 状态隔离:任务之间不能互相污染状态。
- 成本控制:并发任务多了,token 消耗可能失控。
我的做法是引入任务队列 + 限流器。所有任务进队列,由固定数量的 worker 消费,worker 数量根据下游接口的承载能力设定。每个任务有独立的上下文和状态存储,互不干扰。同时给每个任务设 token 预算上限,超了就暂停或降级。
注意:并发不是越高越好。我见过有人把并发开到几百,结果下游接口直接被打挂,所有任务一起失败。并发数应该由最慢的那个下游环节决定。
7. 安全边界:长任务 Agent 必须有的"刹车"
7.1 为什么长任务的安全问题更严重
单次回答就算答错了,最多是输出一段错误文本,影响有限。但长任务 Agent 是有"手"的——它能调工具、能写文件、能发请求。一旦跑偏,可能造成真实破坏:删错文件、发错消息、改错数据。
所以长任务 Agent 必须有明确的安全边界。这不是"锦上添花",是"保命设计"。
7.2 几道必须设的防线
我的实践里,至少设这几道:
- 权限最小化:Agent 能碰的资源,严格限制在任务需要的范围内。不需要写权限就不给写权限。
- 危险操作确认:删除、覆盖、发送这类不可逆操作,执行前要么人工确认,要么走二次校验。
- 沙箱隔离:让 Agent 在受限环境里执行,即使出错也影响不到主系统。
- 操作审计:所有有副作用的操作都记日志,出问题能追溯。
- 预算熔断:token、时间、调用次数都设上限,超了自动停。
7.3 一个具体的教训
我之前做一个自动整理文件的任务,Agent 有删除权限。测试时一切正常,结果有一次它判断失误,把一批不该删的文件标记成了"临时文件"。幸好我设了"删除前先移到回收目录"的规则,才没造成实际损失。
从那以后我定了个规矩:Agent 的任何不可逆操作,都要先转成可逆操作。删除改成移动,覆盖改成备份后写,发送改成先存草稿。这样即使判断错了,也有挽回余地。
8. 从 Prompt 到 Graph:一个 Agent 工程师的能力地图
8.1 这几层工程不是替代关系,是叠加关系
很多人问:学了 Context Engineering 是不是就不用管 Prompt 了?不是。这几层是叠加的,不是替代的。Prompt Engineering 解决"怎么表达",Context Engineering 解决"给什么信息",Loop Engineering 解决"怎么循环",Graph Engineering 解决"怎么编排"。一个成熟的长任务 Agent,这四层都得有。
从能力成长的角度,我建议的学习顺序是:
- 先把 Prompt 写明白,能稳定拿到想要的输出格式。
- 再学工具调用,让 Agent 能操作外部世界。
- 然后啃上下文管理,这是长任务的分水岭。
- 接着设计循环和失败恢复,让任务能稳定跑完。
- 最后学图编排和多 Agent,处理复杂任务。
8.2 面试和实战里最常被问的点
如果你在准备 Agent 相关的面试,或者想检验自己的理解,这几个问题很能区分水平:
- 长任务里上下文膨胀怎么处理?(考 Context Engineering)
- Agent 陷入死循环怎么办?(考 Loop Engineering)
- 多个 Agent 职责重叠怎么解决?(考 Graph Engineering)
- 任务跑到一半崩了怎么恢复?(考状态管理)
- 怎么防止 Agent 执行危险操作?(考安全边界)
能把这几个问题讲清楚,基本就摸到长任务 Agent 的门槛了。
8.3 我个人的一点体会
做 Agent 这几年,最大的感受是:Agent 工程的难点,从来不在"让模型更聪明",而在"让系统更可靠"。模型能力每年都在涨,但可靠性问题不会因为模型变强就自动消失——因为可靠性是工程问题,不是智能问题。
我见过太多团队把精力全砸在 Prompt 调优上,结果系统一上量就崩。也见过一些团队模型用得不算最强,但工程做得扎实,长任务跑得又稳又省。差距就在这些"看不见"的工程层里。
最后分享一个我常用的自检清单,每次设计长任务 Agent 前过一遍:
- 上下文里有没有"用完即弃"的数据没丢?
- 循环的退出条件是不是组合设置的?
- 有副作用的操作有没有防重复?
- 失败了能不能从断点恢复?
- 危险操作有没有刹车?
- 并发上量后下游扛不扛得住?
这六个问题答不上来,任务大概率跑不稳。答上来了,剩下的就是调优的事了。