Agent工程栈分层设计与最小可用实现指南
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 和 stageplanning。这个工具不到五十行代码但在调试的时候非常有用。有一次一个任务在执行阶段卡住我过滤日志发现执行层在等待一个永远不会完成的依赖步骤原因是规划层输出的依赖关系里有一个步骤编号写错了。如果没有日志这个问题可能要花几个小时才能定位。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 在错误的方向上浪费资源。这个技巧看起来简单但在实际运行中帮我避免了很多无效执行。