☰
Agent工程栈分层设计与最小可用实现指南
2026/10/15 22:59:52 网站建设 项目流程

1. 从五条动态说起:Agent 工程栈到底在解决什么问题

10 月 7 日那天,我刷到五条动态,来自不同背景的开发者,有做前端工具的,有搞数据管道的,也有在传统后端摸爬滚打多年的。五条内容表面上各说各的,但串起来看,指向的是同一件事:Agent 不再是一个“提示词加循环调用”的玩具,它正在变成一套有明确分层、有工程约束、有交付标准的系统。换句话说,Agent 工程栈开始成形了。

这个判断为什么重要?因为过去大半年,大多数人做 Agent 的路径是:写一个系统提示词,接几个工具函数,套一个 while 循环,能跑通就上线。跑不通就加提示词,再加提示词。最后提示词膨胀到几千字,行为还是不稳定。这不是工程,这是碰运气。而 10 月 7 日那几条动态透露出的信号是,一批人已经开始把 Agent 当成一个正经的软件系统来拆解:哪些层负责决策,哪些层负责执行,哪些层负责记忆,哪些层负责观测和回滚。

我自己的体会是,Agent 工程栈这个概念,本质上回答的是三个问题。第一,一个 Agent 从接收到任务到产出结果,中间经过哪些确定的阶段。第二,每个阶段的输入输出边界在哪里,怎么保证上一个阶段的错误不会无声地污染下一个阶段。第三,当 Agent 行为不符合预期时,你从哪里开始排查,而不是对着整个黑盒干瞪眼。

这套东西适合谁来参考?如果你只是用现成的 Agent 产品做日常任务,那暂时不需要关心这些。但如果你正在自己搭 Agent,或者团队里要把 Agent 能力集成到现有系统里,那这套分层思路会帮你省掉大量返工。尤其是那些已经踩过“提示词越写越长、效果越来越玄学”这个坑的人,下面的内容应该会对你有直接帮助。

2. 拆解 Agent 工程栈的核心分层与选型逻辑

2.1 为什么不能把 Agent 当成一个函数来写

很多人第一次写 Agent,脑子里想的是:输入一个任务描述,输出一个结果。于是代码结构就是一个大函数,里面塞了提示词拼接、模型调用、工具解析、结果判断。这种写法在 demo 阶段没问题,但一旦任务变复杂,问题就来了。模型调用失败怎么办?工具返回了预期外的格式怎么办?任务执行到一半发现方向错了要不要回退?这些在单函数结构里都没有位置放。

Agent 工程栈的第一个核心认知是:Agent 不是一个函数,而是一条流水线。流水线上有多个工位,每个工位有明确的职责。10 月 7 日那几条动态里,有一条提到“把规划器和执行器拆开之后,调试时间从半天缩短到二十分钟”,这就是分层的直接收益。规划器只负责把任务拆成步骤,执行器只负责按步骤调用工具,两者通过一个结构化的中间表示来通信。规划错了,你看规划器的输出就知道;执行错了,你看执行器的日志就知道。混在一起,你只能猜。

2.2 规划层:从“让模型自己想”到“给模型一个框架”

规划层的职责是把一个模糊的用户目标,转化成一组可执行的步骤。早期做法是直接在提示词里写“请一步步思考”,然后让模型自由发挥。这种做法的问题在于,模型每次的拆解粒度不一样,有时候拆得太粗,有时候拆得太细,而且没有固定的格式,下游很难稳定解析。

现在更常见的做法是给规划层一个结构化的输出模板。比如要求模型输出一个 JSON 数组,每个元素包含步骤编号、步骤描述、依赖的前置步骤编号、预期产出。这样做的好处是,规划结果可以被程序直接消费,而不是靠正则表达式去猜。我试过在同一个任务上对比两种方式,自由发挥的规划结果有大约三成需要人工修正,而结构化模板的规划结果修正率降到不到一成。

注意:结构化模板不是越细越好。步骤描述如果限制得太死,模型会为了填满字段而编造内容。我的经验是,步骤描述保持自然语言,但步骤编号和依赖关系必须强制结构化。

2.3 执行层:工具调用的确定性与容错设计

执行层是 Agent 真正“动手”的地方。这里的关键词是确定性。同一个工具,同样的输入,应该返回同样的输出格式。但现实是,很多工具本身就不稳定,比如网页抓取可能超时,数据库查询可能返回空结果,外部 API 可能限流。执行层如果不处理这些,整个 Agent 就会卡住或者产出垃圾。

我在实际项目里的做法是,给每个工具定义一个返回信封。信封里包含三个固定字段:状态码、数据体、错误信息。状态码只有成功、可重试失败、不可重试失败三种。执行器拿到信封后,根据状态码决定是继续、重试还是终止并上报。这个设计看起来简单,但它把“工具调用失败”从一个异常变成了一个正常的分支,整个流程的可控性提升非常明显。

另外,执行层还需要一个超时和重试策略。我的经验值是,对于网络类工具,单次超时设 10 秒,最多重试 2 次,重试间隔用指数退避。对于本地计算类工具,超时设 30 秒,不重试,因为本地计算超时通常意味着逻辑有问题,重试也是白搭。

2.4 记忆层:短期上下文与长期知识的分离

记忆层是很多人容易忽略的一层。早期做法是把所有历史对话都塞进上下文窗口,结果就是 token 消耗飞快,而且模型会被无关的历史信息干扰。10 月 7 日有一条动态提到“把会话记忆和任务记忆分开存之后,上下文长度降了六成,任务成功率反而升了”,这个观察很准确。

我的做法是分两个存储。会话记忆存最近几轮对话的摘要,用于维持对话连贯性。任务记忆存当前任务的中间产出,比如已经完成的步骤、已经获取的数据、已经做出的决策。任务记忆用结构化格式存储,不直接进上下文,而是在需要的时候按需检索。这样上下文里永远只有当前步骤真正需要的信息,信噪比高很多。

2.5 观测层:没有日志的 Agent 等于没有 Agent

观测层是工程栈里最容易被跳过的一层,但也是出问题时最救命的一层。一个 Agent 如果只有最终输出,没有中间过程的记录,那它就是一个黑盒。你只能看到它错了,但不知道它为什么错。

我要求我的 Agent 在每个关键节点都打结构化日志。日志里至少包含:时间戳、当前阶段名称、输入摘要、输出摘要、耗时、状态码。这些日志汇总到一个地方,可以按任务 ID 过滤,也可以按阶段名称聚合。有一次一个任务连续失败,我拉出日志一看,发现规划层每次都在第三步把“查询数据”和“分析数据”合并成一个步骤,导致执行层拿到一个过于笼统的指令。这个问题如果只看最终输出,根本定位不到。

3. 实操过程:从零搭一个最小可用的 Agent 工程栈

3.1 环境准备与依赖选择

先说环境。我用的是一台普通的开发机,Python 3.11,没有用任何重型框架。依赖只有三个:一个 HTTP 客户端用来调模型接口,一个 JSON 库用来处理结构化数据,一个日志库用来打结构化日志。为什么不直接用现成的 Agent 框架?因为框架会隐藏太多细节,而我在搭第一版的时候,需要完全掌控每个环节的输入输出。

模型接口的选择上,我用的是一个支持结构化输出的通用模型接口。关键要求是它能稳定返回 JSON 格式,而不是自由文本。如果模型接口不支持强制 JSON 输出,那就需要在提示词里反复强调格式,并在解析时做容错。我的经验是,宁可多花一点时间找一个支持结构化输出的接口,也不要在这上面省事,因为格式解析的容错代码会越写越复杂。

3.2 定义核心数据结构

在写任何逻辑之前,先把数据结构定下来。我定义了三个核心结构:任务、步骤、工具返回信封。

任务结构包含任务 ID、用户原始输入、规划结果、执行状态、最终输出。步骤结构包含步骤编号、步骤描述、依赖步骤编号、执行状态、执行结果。工具返回信封包含状态码、数据体、错误信息。

这些结构用 Python 的 dataclass 来定义,好处是字段明确,序列化和反序列化都方便。定义好之后,整个 Agent 的流转就是这些结构在流水线上传递。规划层输出步骤列表,执行层消费步骤列表并产出执行结果,记忆层存储中间结果,观测层记录每一步的变化。

3.3 规划层的实现细节

规划层的核心是一个提示词模板加一个解析函数。提示词模板里,我明确要求模型输出一个 JSON 数组,每个元素包含 step_id、description、depends_on 三个字段。step_id 是整数,从 1 开始递增。depends_on 是一个整数数组,表示这个步骤依赖哪些前置步骤。

解析函数拿到模型输出后,先尝试直接 JSON 解析。如果失败,就尝试从文本中提取第一个 JSON 数组。如果还失败,就返回一个错误信封,让上层决定是重试还是终止。这里有一个细节:解析成功后,我会做一个依赖关系校验,确保 depends_on 里的编号都小于当前步骤编号,且没有循环依赖。这个校验看起来多余,但实际跑起来,模型偶尔会输出依赖自己的步骤,不校验就会死循环。

3.4 执行层的调度逻辑

执行层的调度逻辑是一个拓扑排序加状态机。首先根据 depends_on 构建依赖图,然后按拓扑顺序依次执行步骤。每个步骤执行前,检查它的所有依赖步骤是否都已经成功完成。如果是,就调用对应的工具;如果不是,就跳过这个步骤并标记为阻塞。

工具调用的部分,我用一个字典把步骤描述里的关键词映射到具体的工具函数。比如步骤描述里包含“查询”就调用查询工具,包含“计算”就调用计算工具。这个映射不是完美的,但在我这个最小版本里够用了。更成熟的做法是让规划层直接输出工具名称,而不是靠关键词匹配。但那是下一步的优化,第一版先跑通流程。

3.5 记忆层的读写策略

记忆层我用两个字典来实现。一个字典存会话记忆,键是会话 ID,值是一个列表,列表里是最近几轮对话的摘要。另一个字典存任务记忆,键是任务 ID,值是一个字典,字典里是各个步骤的产出。

读写策略上,规划层在生成步骤之前,会先读一次会话记忆,把最近的对话摘要拼进提示词。执行层在完成一个步骤后,会把该步骤的产出写入任务记忆。当后续步骤需要引用前置步骤的产出时,执行层从任务记忆里读取,而不是从上下文里读取。这样上下文里永远只有当前步骤需要的信息,不会随着步骤增多而膨胀。

3.6 观测层的日志格式与查询

观测层的日志我用 JSON Lines 格式,每行一个 JSON 对象。字段包括 timestamp、task_id、stage、input_summary、output_summary、duration_ms、status。input_summary 和 output_summary 都是字符串,长度限制在 200 字符以内,避免日志文件过大。

查询的时候,我用一个简单的命令行工具,支持按 task_id 过滤和按 stage 过滤。比如我想看某个任务在规划阶段的所有日志,就过滤 task_id 和 stage=planning。这个工具不到五十行代码,但在调试的时候非常有用。有一次一个任务在执行阶段卡住,我过滤日志发现执行层在等待一个永远不会完成的依赖步骤,原因是规划层输出的依赖关系里有一个步骤编号写错了。如果没有日志,这个问题可能要花几个小时才能定位。

4. 常见问题与排查技巧实录

4.1 规划层输出格式不稳定怎么办

这是最常见的问题。模型有时候输出 JSON,有时候输出 Markdown 代码块包裹的 JSON,有时候在 JSON 前后加解释文字。我的处理策略是三层容错。第一层,尝试直接解析整个输出。第二层,用正则表达式提取第一个花括号或方括号包裹的内容,再解析。第三层,如果还失败,就把原始输出和错误信息一起返回,让上层决定是否重试。

重试的时候,我会在提示词里加一句“上次输出格式不正确,请只输出 JSON,不要加任何其他文字”。这个提示对大部分模型有效。如果重试两次还失败,我就认为这个任务在当前模型能力下无法可靠完成,直接终止并上报。

4.2 执行层工具调用超时怎么处理

超时处理的关键是区分“可重试超时”和“不可重试超时”。网络类工具的超时通常是可重试的,因为可能是瞬时网络抖动。本地计算类工具的超时通常是不可重试的,因为可能是逻辑死循环或者数据量过大。

我的做法是给每个工具定义一个超时时间和重试次数。网络类工具超时 10 秒,重试 2 次,间隔 1 秒、2 秒。本地计算类工具超时 30 秒,不重试。如果重试后仍然超时,就返回一个不可重试失败信封,执行层终止当前任务并记录日志。

4.3 记忆层上下文膨胀怎么控制

上下文膨胀的根源是把所有历史信息都塞进提示词。控制方法有两个。第一,会话记忆只存摘要,不存原文。摘要用模型生成,限制在 100 字以内。第二,任务记忆不直接进上下文,而是按需检索。执行层在需要前置步骤产出时,从任务记忆里读取,然后只把相关部分拼进当前步骤的提示词。

我实测下来,这两个措施可以把上下文长度控制在初始长度的两倍以内,而不是随着步骤数量线性增长。对于一个十步左右的任务,上下文长度基本稳定在 2000 到 3000 token 之间。

4.4 观测层日志太多怎么筛选

日志太多是因为每个步骤都打了详细日志。筛选的方法是给日志加一个级别字段。级别分为 debug、info、warn、error。正常流程打 info,异常情况打 warn 或 error。调试的时候看 debug,日常监控看 warn 和 error。

另外,我给日志加了一个采样率配置。对于高频重复的任务,可以设置只记录 10% 的日志。这样既保留了统计信息,又不会让日志文件爆炸。采样率按任务类型配置,关键任务 100% 记录,非关键任务 10% 记录。

4.5 常见问题速查表

问题现象可能原因排查方法解决措施
规划层输出无法解析模型未按格式输出查看原始输出增加格式提示,启用重试
执行层卡住不动依赖步骤未完成检查依赖图修正依赖关系或跳过阻塞步骤
上下文长度暴涨历史信息全量注入统计 token 消耗启用摘要和按需检索
任务成功率波动大工具返回不稳定查看工具日志增加超时和重试策略
最终输出质量差中间步骤产出污染逐步骤检查产出增加步骤间校验

提示:这张表是我在实际项目中反复遇到并总结出来的,建议在搭第一版的时候就对照检查,能省掉很多事后补救的时间。

5. 从最小可用到生产可用的扩展方向

5.1 规划层的自我修正能力

最小版本里,规划层只规划一次。如果执行过程中发现规划有问题,只能终止任务。生产版本里,可以增加一个重新规划的能力。当执行层连续两个步骤失败,或者某个步骤的产出明显偏离预期时,触发重新规划。重新规划时,把已经完成的步骤和当前状态作为输入,让模型生成一个新的步骤列表,替换掉未完成的部分。

这个能力的关键是控制重新规划的频率。如果每次小失败都重新规划,任务会陷入无限循环。我的做法是设置一个重新规划次数上限,比如最多 2 次。超过上限就终止并上报。

5.2 执行层的并行化

最小版本里,步骤是串行执行的。但很多步骤之间没有依赖关系,可以并行。比如“查询数据 A”和“查询数据 B”如果互不依赖,就可以同时执行。并行化可以显著缩短任务总耗时。

实现并行化的前提是依赖图足够准确。如果依赖关系有遗漏,并行执行会导致数据竞争。我的做法是,在规划层输出依赖关系后,做一个保守的并行分组。只有明确没有依赖关系的步骤才分到同一组并行执行,有疑问的一律串行。

5.3 记忆层的持久化

最小版本里,记忆层是内存字典,进程重启就丢失。生产版本里,需要持久化到数据库或文件系统。持久化的关键是设计好键的结构。我用的是“会话 ID + 任务 ID + 步骤编号”作为复合键,这样可以精确检索到某个任务的某个步骤的产出。

持久化之后,还可以增加记忆的过期策略。会话记忆保留最近 7 天,任务记忆保留最近 30 天。过期数据自动清理,避免存储无限增长。

5.4 观测层的可视化

日志是文本,查询靠命令行。生产版本里,可以把日志导入到一个可视化面板,按任务 ID 展示时间线,按阶段展示耗时分布,按状态码展示成功率。这样一眼就能看出哪个阶段是瓶颈,哪个工具最容易失败。

我用的是一个轻量的本地面板,不需要联网,数据也不出本地。面板上主要有三个视图:任务时间线、阶段耗时柱状图、工具成功率饼图。这三个视图覆盖了日常调试和监控的大部分需求。

6. 我踩过的坑和几条实在建议

第一个坑是过早引入框架。我一开始用了一个流行的 Agent 框架,结果发现它的抽象层太厚,我想改一个工具调用的超时逻辑,要翻好几层源码。后来我干脆把框架去掉,自己写了一个两百行的最小实现,反而更可控。我的建议是,第一版一定自己写,把每个环节都摸清楚,之后再考虑用框架来减少重复代码。

第二个坑是忽视日志。我早期版本没有打结构化日志,出问题只能靠 print 大法。有一次一个任务在半夜失败,第二天早上我完全不知道发生了什么。从那以后,我把日志当成和业务逻辑同等重要的部分来写。现在我的 Agent 每个关键节点都有日志,排查问题的时间从小时级降到分钟级。

第三个坑是提示词里塞太多规则。我曾经在一个提示词里写了二十多条规则,结果模型顾此失彼,遵守了这条忘了那条。后来我把规则拆到不同的层,规划层只管拆解,执行层只管调用,每层的提示词只包含该层需要的规则。规则数量降下来了,遵守率反而上去了。

最后分享一个小技巧:在规划层的提示词里加一句“如果任务无法拆解为可执行步骤,请输出一个空数组”。这个兜底规则可以避免模型在遇到无法处理的任务时胡编乱造步骤。空数组会被执行层识别为“无法执行”,然后触发上报流程,而不是让 Agent 在错误的方向上浪费资源。这个技巧看起来简单,但在实际运行中帮我避免了很多无效执行。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询