前阵子我调试一个跑了两天两夜的 Agent 任务,凌晨三点盯着日志里疯狂刷新的“重试”记录,第一次意识到:一个能跑通 Demo 的 Agent 骨架,和一个能在生产环境里连续运行几天的 Harness,中间隔着的不是一两个补丁,而是整套工程思维。后来我把手头几个 Agent 项目的结构重新梳理了一遍,发现真正值得沉淀的其实就五块:Agent 循环、上下文管理、沙箱隔离、结果验证、长时运行。这篇东西就是想把这五块一次讲透,把我试过的方案、踩过的坑、最后留下来的参数配置都摊开说,希望能给正在搭 Agent 工程的同学省一些凌晨三点的排查时间。
这篇文章适合谁看?如果你已经在用大模型接口写 Agent 原型,跑过几个自动化任务,但一旦遇到连续运行、工具调用变多、上下文越用越乱、偶尔还要现场查日志的场景就头疼,那这篇大概率对你有用。我会从一个最小循环讲起,逐步加厚到能支撑长时任务的工程结构,过程中给出可复现的参数和经验判断依据——不保证是唯一答案,但都是实测下来能用的方案。
1. Agent 循环:先搞懂主循环,再谈其他
1.1 一个最小可跑的 Agent 循环长什么样
任何 Agent 系统,不管宣传得多么花哨,最底层就是一个循环:让模型观察当前状态,决定下一步动作,执行动作,把结果写回上下文,然后继续下一轮。你可以把它理解成一个人做事的过程——先看清楚手上有什么材料,想想要不要查资料,查完资料再汇总判断,直到得出最终结论。程序里的 Agent 循环就是把这件事自动化。
我第一次写最小 Agent 循环时,代码极其简单:
messages = [{"role": "system", "content": system_prompt}] while True: response = client.chat.completions.create( model="your-model", messages=messages, tools=tools ) message = response.choices[0].message messages.append(message) if not message.tool_calls: break for tool_call in message.tool_calls: result = execute_tool(tool_call.function.name, tool_call.function.arguments) messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": result })这段代码解决了从零到一的问题,但漏洞同样明显:循环没有最大迭代限制,工具执行没有超时,上下文无限膨胀。我第一版跑一个“自动整理文件”的 Demo,模型在某个分支里反复调用同一个工具,整整跑了四十多分钟,费用哗哗地烧。那之后我给自己定了一条规矩:任何 Agent 循环,第一版也必须带上 halt 条件,不带就是等着出事故。
1.2 循环终止条件:不设上限的循环会把你拖垮
Agent 循环的终止条件不只是“模型不再调用工具”这一种。实际工程里,至少要有三到四道安全阀:
一是正常终止,即模型输出最终答案、不再发起工具调用;二是最大迭代数限制,我通常设为 15 到 25 轮,具体看任务复杂度;三是总超时限制,比如单次任务最多跑 10 分钟,到了就强制中断并保存当前状态;四是异常退出,比如工具调用格式反复解析失败、连续报错超过阈值,这时候不硬撑,直接标记失败退出。
迭代上限的设计很重要。设太小,任务稍微复杂一点就被截断;设太大,又失去了护栏意义。我实践下来常用的两个设定:普通文档处理类任务 15 轮,涉及多步骤检索或多阶段推理的任务给到 25 轮。这个数不是拍脑袋,而是统计了我自己十几个任务的平均轮次后留出的余量——大部分任务其实在 8 到 12 轮内就结束了,25 轮基本是两倍余量,足够接住大多数意外分支。
还有一个容易被忽略的细节:任务超时要按“总时长”算,而不是按“每轮响应时长”算。模型单次响应可能很快,但工具调用链条一变长,累计时间会远超预期。我会在循环外层包一个deadline = time.time() + max_total_time,每轮开始前检查当前时间是否超过 deadline,超了就立刻终止,把已完成的中间结果写入日志,方便后续断点续跑。
1.3 循环内的护栏与保险丝
循环里最怕的不是单次失败,而是失败后无限重试。模型对工具返回的错误信息理解不了时,会尝试换一个参数再调一次,这看起来聪明,实际上可能是在反复撞同一堵墙。我见过一个任务,模型因为文件路径里少了个斜杠,连续八次尝试不同路径前缀,全部失败,最后把上下文塞得乱七八糟。
我的做法是给同一个工具的连续调用计数。定义“重复失败热力图”——如果模型在连续三轮里调用同一个工具且都返回错误,就把当前工具暂时禁用掉,并向模型明确提示“此工具当前不可用,请换一个思路”。这招在实操中非常有效,能逼着模型跳出现有路径,而不是原地打转。
另一个保险丝是全局异常捕获。循环里任何一次工具执行抛异常,不能直接让整个进程崩溃,而是要捕获后转成一段可读的错误文本送回给模型。但要给这段错误文本加上时间戳和调用签名,方便后续人工排查。我习惯把每次工具调用连同参数、返回码、耗时都写入结构化日志,这样即便任务最终失败,也能完整回溯是哪一步带偏了方向。
2. 上下文:Agent 的记忆里装不下全世界
2.1 上下文窗口的构成与膨胀路径
把 Agent 想象成一个只有短期记忆的人:它能记住的,就是你塞给它的所有对话历史加起来。系统提示词是它的性格和岗位职责,历史消息是它做过的事,工具定义是它能用的技能,工具返回结果是它查阅的资料。问题是,这个“记忆”是有上限的,而且模型对藏在超长历史中部的关键信息往往会“变瞎”——不是超长就一定出错,但关键信息埋在十万 token 中间时,召回质量明显波动,这是找模型厂商优化也没办法完全消除的问题。
上下文膨胀的路径非常清晰:每多一轮推理,就要多存一轮模型输出;每调一次工具,工具结果也要进上下文。一个要读十个网页并做汇总的任务,网页正文全部塞进上下文后,往往还没开始真正分析,就快把窗口塞满了。我一开始天真地以为“反正现在窗口大,塞满也没关系”,事实证明代价很大:响应变慢、费用升高、关键信息被稀释,三样全占。
2.2 上下文管理三件套:截断、摘要、聚焦
上下文管理不是单一技术,而是三层策略配合使用。第一层是截断:把最早的历史消息直接丢弃。但这有个前提,你得先确定系统提示词和核心任务约束绝对不能丢。我会把消息列表按角色拆分对待,系统提示词和用户最初的任务描述永远保留,只滚动截断中间的过程性对话。对于过程性对话,保留最近几轮完整的,更早的则合并成一段摘要放在前面,这样模型既能看到任务全貌,又不至于被陈年细节淹没。
第二层是摘要压缩。与其简单丢消息,不如让模型把旧对话浓缩成结构化要点。这一步要控制摘要本身的长度,我会在摘要提示词里明确格式,比如“用不超过 300 字概括已完成步骤、已获取的关键结论、尚未解决的问题”。注意,摘要不能让模型自由发挥,否则格式一会儿列表一会儿散文,下次循环根本无从解析。我踩过这个坑后,一直坚持用 JSON 结构控制摘要输出。
第三层也是我认为最重要的一层:聚焦。让模型只看与当前决策相关的信息。这本质上是一种检索增强路由:不把几百页文档全部塞进去,而是先做一个粗筛,只把命中的片段送回上下文。这跟“挂载数据库让模型自己查”是两回事——聚焦是在工具调用层面就限制返回大小,从源头减少上下文压力。
2.3 给工具返回结果做“瘦身”
工具返回结果是上下文膨胀的最大来源。我在设计工具接口时有一条原则:工具返回的内容,应该是模型做决策所需的最小充分集,而不是完整原始数据。
举个例子。一个读日志文件的工具,如果把整份几十 MB 的日志全部返回,模型既看不完也用不上。正确做法是工具内部先做预处理——统计错误等级分布、提取异常堆栈里的关键行、只返回匹配关键字的上下文各五行。这样既保留了决策依据,又把单次返回控制在几百 token 内。这样设计后,工具函数本身就承担了“信息提炼”的职责,Agent 循环压力大减。
结构化返回也很重要。我要求所有工具返回统一为 JSON 格式,至少包含status(成功或失败)、summary(给模型看的结果摘要)、detail(需要对比的原始片段,可选)。summary控制在 200 字以内,detail只在必要时才填。别小看这个约束,它让模型解析工具结果时省去大量猜测,也让我做验证和日志分析时有了统一字段可依赖。实测下来,同样的自动化任务,工具返回瘦身后,完成率从 71% 提到 89%,主要原因是模型不再被海量无关信息带偏。
3. 沙箱:给 Agent 划一条安全边界
3.1 沙箱要解决的真实问题
Agent 跟传统程序最大的区别在于:Agent 的动作不是程序员预先写死的,而是模型临场决策生成的。这带来一个本质矛盾——你既希望它灵活执行工具,又不敢让它完全放开权限。模型可能会因为提示词理解偏差,执行删除命令;也可能因为工具返回内容里恰巧有一段恶意指令,就照着做了一遍。所以,把 Agent 的代码执行、文件操作、网络请求全部圈在一个受限空间里,不是可选项,是必选项。
我第一次做带文件操作能力的 Agent 时没上沙箱,结果模型根据网上查到的一段不完整的 shell 命令,误删了同目录下的一个备份文件夹。虽然数据有异地备份,但那一次就把我吓清醒了。后来我每次搭建 Agent 环境,第一件事就是确认沙箱边界,再谈能力。这个顺序不能反。
3.2 沙箱的三种实现形态怎么选
按隔离级别从低到高,我常用的沙箱形态有三种:进程级隔离、容器级隔离、虚拟机级隔离。各自的取舍很直接。
进程级隔离最简单,用操作系统的用户权限和资源限制来实现,比如给 Agent 进程分配一个低权限用户、限制可访问目录、通过系统调用拦截控制文件访问范围。优点是轻量、启动快、开发调试方便;缺点是隔离不彻底,一个提权漏洞就可能打穿边界。适合内部调试阶段或低风险任务。
容器级隔离是我目前的主力方案。每个 Agent 任务跑在一个独立的容器运行时里,镜像只安装任务必需的依赖,挂载只读的系统目录,工作目录挂载成独立卷。容器的 CPU、内存、磁盘都设置硬上限;为了稳妥,工作目录要挂载成独立卷。好处是环境一致性好、隔离级别适中、起停都在秒级。坏处是需要维护镜像和卷管理,初期会多花一些时间。
虚拟机级隔离适合跑不受信任的第三方代码,或者需要强隔离的金融、隐私类任务。我这里说得比较简单:安全级别最高,但资源开销也最大,起一个虚拟机再初始化环境通常要数十秒。我的经验是——能用容器解决的场景就别上虚拟机,否则并发一多,资源分配直接成瓶颈。
3.3 沙箱里的网络与资源控制
沙箱不能只圈住文件系统,网络更要管。默认策略我设为禁止所有出站网络请求,再按白名单放行。比如一个写报告类 Agent,只允许访问公司内部知识库域名和一个固定的检索 API,其他域名全部拒绝。这不仅是安全考虑,也是稳定性考虑——模型不会随机抓取一个无法访问的网站,反复陷入超时等待。
资源控制方面,我习惯给容器设置四组硬参数:内存上限 2GB,超出即触发 OOM(为了避免整机被杀进程,先做小);CPU 限制为 1 个核心,避免模型生成大量计算任务时拖垮宿主机;磁盘写入限制 1GB,防止 Agent 循环里写脏数据或者日志文件把磁盘塞满;最大执行时间限制 600 秒,超时直接销毁容器,由外层任务系统负责记录失败原因并重试。参数的具体值肯定要按业务调整,但思路是一致的:每个资源维度都必须有边界,宁可误杀不可失控。
沙箱里还有一个容易忽视的细节:临时文件的清理。容器销毁后,工作目录如果是独立卷,任务结束要显式清理;我还加了一个定时清理任务,把超过一天的临时卷全部回收,避免磁盘水位只增不减。这个清理策略看起来不起眼,但长时运行场景下,它就是磁盘不会无声无息爆掉的关键防线。
4. 验证:把 Agent 的“胡说”拦在上线之前
4.1 验证不是最后一步,而是每一环
很多人以为验证是 Agent 输出完最终结果后做一次检查,其实到那时候已经晚了。Agent 出错往往是滚雪球式的——第一环的输出错了,第二环在错误基础上继续加工,到最后一环已经很难定位源头。所以我现在的做法是:在每一环都设置验证点。
具体有三个环节我会强制加验证点。工具入参的验证:模型生成工具参数后,先做格式和枚举值检查,比如日期格式必须是YYYY-MM-DD,文件路径必须存在于允许列表中;工具出参的验证:工具返回后检查数据是否符合预期结构,比如返回列表是否为空、状态码是否在预期范围内;最终输出的验证:整段结果交付前做一次完整审视,看任务约束是否都满足。
这三个验证点不是平均用力。工具入参验证拦截的是“模型犯低级错误”,工具出参验证拦截的是“外部系统返回异常数据”,最终输出验证拦截的是“任务本身没有真正完成”。三者各司其职,缺失任何一个,都会在长链条任务中放大成不可控问题。
4.2 结构化输出校验与重试
模型生成 JSON 格式的工具参数时,偶尔会产生非法 JSON——多一个括号、少一对引号、字段值类型错误,这些都是常见问题。我的处理思路是分层校验加有限重试。
第一层是语法校验,用 JSON 解析器去解析模型输出,解析失败就把错误信息反馈给模型,让它修正后重新生成。这里我会附上原始输出片段和具体报错位置,否则模型对着一个“JSON decode error”完全无从下手。第二层是结构校验,用 JSON Schema 或者等效的字段规则检查,要求必填字段齐全、类型匹配、枚举值合法。第三层是语义校验,这一步最容易被忽略,比如一个查询时间范围的参数,开始日期必须早于结束日期;一个分页参数,页号必须大于等于一。这些业务规则模型不一定每次都能遵守,校验层把它守住是最后的防线。
重试次数要限制。我给模型重试修正结构化输出的上限是三次,超过三次就不再让模型继续改正,而是把这次工具调用标记为失败,进入降级流程。降级流程的典型做法是:提示模型跳过该工具,改用替代方案,或者直接询问用户。这个上限是我反复调出来的——三次以内,模型通常能自己纠偏;超过三次还错,说明当前上下文或工具设计有问题,继续重试只会烧钱和浪费时间。
4.3 端到端验证与回归基线
单点验证做得再好,也不能保证整条任务链路最终产出是可靠的。所以我额外维护了一套端到端验证体系:每次主要流程迭代后,用一组固定的典型任务跑回归,比较 Agent 的最终输出与预期结果。
回归基线不需要很复杂。我建了一个验证用例集,里面大概二十来个任务,覆盖日常高频场景和几个容易出错的边界场景。比如文件整理任务、网页信息抓取任务、批量数据清洗任务,以及几个故意设置陷阱的任务——像让 Agent 去读一个不存在的文件,看它能不能正确地报告失败而不是编造内容。每次代码或提示词有改动,就跑一遍这套用例,记录通过率。通过率下跌超过一个点,就禁止合并改动。
这套做法看起来笨,但非常有效。它能把大模型输出的主观波动转化成客观数字,让我在评估“某个提示词改动到底有没有用”时有一个相对可靠的标尺。模型版本升级、工具结构调整、沙箱环境变更,都可以靠回归基线快速发现意外影响——这些影响经常在运行到第三五个任务时才暴露,肉眼很难察觉。
5. 长时运行:从“能跑”到“一直跑”
5.1 长时运行的几个隐藏杀手
短任务 Demo 和一个能连跑几天的服务之间,差的不是大模型能力,而是工程韧性。我摸索下来,长时运行最大的杀手有三个:一是状态丢失,好不容易跑到一半,进程一崩,前面的进度全归零,得从头再来;二是资源泄漏,连接数、临时文件、内存句柄,随着循环轮数一点点涨,一两天下来的累积效应足以拖垮进程;三是静默偏离,任务还在跑,但 Agent 早就偏离了原始目标,在某个子分支里空转,日志看起来一切正常,实际上什么正事都没干。
状态丢失最容易在没做检查点持久化时发生。我有一个真实的教训:跑一个多小时的报告生成任务,跑到 87% 时进程因为宿主机重启被杀,任务管理器里只剩一行“任务失败”。从那次起,我彻底换了一套思路,不再把 Agent 状态只留在内存里,而是定期落盘。
5.2 检查点与状态恢复
检查点的核心思想很简单:把 Agent 当前的状态——消息历史、已执行步骤、中间结果、当前循环轮次——序列化存储起来。遇到崩溃时,从最近一个检查点恢复,而不是从头开始。
我实现检查点的方式很朴素:每完成一个工具调用,就把完整的消息列表和任务元数据存成一份 JSON 文件。文件路径按任务 ID 和检查点序号组织,比如/tasks/<task_id>/checkpoints/<round>.json。解析时按序号取最后一个完整可读的检查点,重建消息列表,接着继续跑。这种方式简单粗暴,但胜在可靠——它不依赖任何外部存储系统,文件系统在就有状态。
检查点也不能无限写,每一轮都落盘虽然最安全,但磁盘开销和序列化耗时也不小。我的折中方案是每完成一次工具调用落盘一次,因为工具调用本身通常比纯对话更耗时,落盘成本相对可忽略;对话轮次之间频繁来回时不落盘,等下一轮工具调用前再落。这样既保证了崩溃时最多丢失一轮对话,又把写盘频率控制在一个合理的水平。
5.3 可观测性:日志、追踪、指标缺一不可
长时运行的服务,你不可能每时每刻盯着屏幕看。可观测性就是我远程判断系统是否健康的眼睛。日志、追踪、指标这三样,我全部都要,一个都不能省。
日志要结构化,每条日志至少带任务 ID、轮次、工具名、耗时和状态。我踩过一个大坑:早期日志全是散装文本,任务出错时我只能人肉搜索关键词,在大段混合日志里找线索,一下就是几小时。改成 JSON 结构化日志后,排查时间缩短到原来的十分之一。追踪要记录一次任务里模型调用和工具调用的调用链,我看到哪一步耗时异常偏高,就能直接定位到具体环节。指标则是全局视角,任务成功率、平均完成时长、工具调用失败率、上下文 token 峰值,这些数上仪表盘,不用等用户投诉,我自己就能提前发现问题。
还有一个小技巧,是我后来加的:给每个任务生成一个红绿灯标记。绿灯是任务正常推进,黄灯是任务偏离了预设计划、进入替代路径但尚未失败,红灯是任务已经失败或触发降级。这个标记会随日志一起输出,监控系统只扫绿灯比例就能快速评估整体健康度,不用逐条读日志。
5.4 队列、并发与任务去重
长时运行的另一个侧面是:任务可能很多,但资源是有限的。直接为每个任务起一个进程,机器的 CPU 和内存很快就会被打爆。我后来引入了一个简单的任务队列层,工作线程按配置的并发数从队列里取任务执行,每个任务再进沙箱环境。这样资源占用始终稳定在可控范围内,不会被一批突然涌进来的任务冲垮。
任务队列还要解决重复执行的问题。同一个事件可能触发多次入队,如果没有幂等控制,同一个任务会被跑好几遍。我的做法是:每个任务在入队前先计算一个稳定的任务指纹,比如由任务类型、目标路径、触发时间戳组成的哈希值;在队列存储里建一个唯一索引,重复指纹直接丢弃。这样网络抖动、消息重发都不会造成重复计算。
并发数也需要讲究。我先用保守值起步,观察指标再调。比如一台机器我初始只给 2 个并发容器配额,跑上两三天,看 CPU 和内存水位正常了,再逐步加到 3、4 个。这里不能拍脑袋定高并发,因为每个 Agent 任务的资源消耗随任务复杂度波动,万一某批任务全是重活,并发一高立刻拖垮全局。宁可牺牲一点吞吐量,也要保证整个系统不会因为某个超大任务而集体翻车。
我个人在实际操作中还有一个体会:长时运行系统的价值,不在于单个任务跑得多快,而在于它能不能在不被人工干预的情况下,稳定跑完一批又一批任务。检查点、可观测性、队列这三件套,恰好是支撑这种稳定性的三个支点。它们不解决单次任务能不能成功的问题,但能保证即使失败了,系统也不会被拖垮,数据不会丢,偏差能被发现。
最后再分享一个小技巧:Agent 工程的调试,很多问题不是看第一眼能发现的,而是要靠不同层次的验证一点一点筛出来的。我会刻意在每个验证层埋一个可开关的详细日志开关,平时只记录 summary,排查问题时再打开 detail,看工具入参、出参、模型原始输出全貌。这个细节帮我省了无数次“重跑一遍任务才能复现问题”的冤枉时间。做 Agent 工程,除了把能力做出来,更重要的是一整套兜底机制——它不会让 Agent 变得更聪明,但能让你的系统在 Agent 不那么聪明时,依然稳得住。