
1. Agent 工程的三层架构总览1.1 为什么需要分层从能跑到能上生产的鸿沟很多人第一次接触 Agent 开发都是从一段几十行的脚本开始的调一个模型接口塞几个工具函数写个 while 循环判断有没有工具调用有就执行、没有就返回。这段代码在 Demo 阶段跑得挺好但只要你想把它放到真实业务里问题就会像潮水一样涌上来——工具调用失败了怎么办模型陷入死循环反复调同一个工具怎么破多轮对话的上下文越来越长、Token 成本失控怎么办多个 Agent 之间要协作谁来编排、谁来兜底这些问题的本质是能跑和能上生产之间隔着一整套工程体系。我把这套体系拆成三层来看Harness执行外壳、Loop循环控制、Graph编排拓扑。这三层不是某个框架的官方术语而是我在多个 Agent 项目落地过程中总结出来的一套心智模型它能帮你快速定位一个问题到底该在哪一层解决。打个比方如果把 Agent 比作一辆车Graph 是导航地图决定从起点到终点走哪条路、经过哪些路口Loop 是发动机和变速箱负责把每一次感知—决策—行动的动力循环起来Harness 则是底盘、安全带和气囊保证这辆车在颠簸路面上不散架、出了事故能兜住。三者缺一不可但职责边界必须清晰否则代码会迅速腐化成一锅粥。1.2 三层各自的职责边界与协作关系先把三层的定义说清楚后面所有内容都围绕这个展开。Harness 层解决的是单次执行如何稳定可靠的问题。它管的是一次模型调用怎么发、工具怎么注册和调用、超时和重试怎么配、异常怎么捕获、日志和可观测性怎么埋、Token 和成本怎么统计。你可以把它理解成 Agent 的运行时容器它不关心业务逻辑只关心执行过程本身是否健壮。Loop 层解决的是一次任务如何推进到完成的问题。它管的是什么时候该继续循环、什么时候该终止、循环的最大轮次是多少、上下文如何裁剪和压缩、中间状态如何保存、人在回路Human-in-the-loop的介入点在哪里。Loop 是 Agent 的心跳它决定了 Agent 是走一步看一步还是能持续推进直到目标达成。Graph 层解决的是复杂任务如何拆解和编排的问题。它管的是任务被拆成哪些节点、节点之间的依赖和流转关系是什么、条件分支怎么走、并行分支怎么合并、子图如何嵌套、失败节点如何回退。Graph 是 Agent 的骨架它把一个大目标拆成一张有向图让每个节点只专注做一件小事。三层的协作关系是自上而下的Graph 定义拓扑Loop 驱动每个节点内部的循环Harness 保障每一次原子执行。反过来Harness 的异常会上抛给 Loop 决定是否重试Loop 的终止信号会上抛给 Graph 决定下一步走哪个节点。理解这个双向的数据流是设计好 Agent 系统的前提。1.3 一个典型 Agent 请求在三层中的流转我用一个具体场景把三层串起来用户说帮我查一下北京明天的天气如果下雨就提醒我带伞顺便看看有没有合适的室内活动推荐。在Graph 层这个任务会被拆成几个节点解析意图→查询天气→条件判断是否下雨→ 分支 A生成带伞提醒/ 分支 B跳过提醒→搜索室内活动→汇总输出。注意这里有个条件分支这就是 Graph 存在的意义——它让如果下雨这种逻辑变成显式的边而不是藏在某个 if-else 里。在Loop 层每个节点内部可能都需要多轮循环。比如搜索室内活动这个节点Agent 可能先搜一次发现结果不理想调整关键词再搜最多循环 3 次。Loop 负责控制这个搜索—评估—再搜索的过程并在达到最大轮次或结果满意时终止。在Harness 层每一次搜索工具的调用、每一次模型生成都要经过 Harness。它负责给搜索工具设 5 秒超时、失败重试 2 次、把请求和响应都记进日志、统计这次调用消耗了多少 Token。这样拆下来你会发现每一层的问题都能被精确定位如果搜索总是超时那是 Harness 的超时配置问题如果 Agent 反复搜同一个词停不下来那是 Loop 的终止条件没设计好如果任务整体跑偏了那是 Graph 的节点拆分或流转逻辑有问题。这就是分层最大的价值——让问题有处可查让修改有处可下手。2. Harness 层把单次执行做到稳如老狗2.1 Harness 到底管什么一份职责清单很多人会把 Harness 和 Agent 框架混为一谈其实 Harness 的边界很清晰。我列一份职责清单你可以对照自己的代码看看哪些该收进 Harness模型调用封装统一不同模型供应商的接口差异处理流式和非流式、处理多模态输入、处理函数调用Function Calling的格式转换。工具注册与调度工具的声明、参数校验、调用、结果序列化。工具是 Agent 的手脚Harness 要保证手脚听使唤。超时与重试每个模型调用、每个工具调用都要有独立的超时和重试策略不能一刀切。异常捕获与降级网络抖动、限流、模型返回格式错误这些都要在 Harness 层兜住转成 Loop 能理解的错误信号。可观测性结构化日志、调用链路追踪、Token 统计、成本核算。没有这些线上出问题你只能靠猜。安全与权限工具调用的权限校验、敏感信息的脱敏、输入输出的内容过滤。注意Harness 层不要掺入任何业务逻辑。一旦你在 Harness 里写了如果用户问天气就调天气工具这种判断说明你把 Graph 的活干到 Harness 里了后面必然混乱。2.2 工具注册与调用的工程细节工具是 Agent 能力的来源但工具注册这件事坑比想象中多。我踩过的几个典型坑第一个坑是参数校验缺失。模型生成的工具调用参数经常是看起来对但实际不能用的。比如它给一个日期参数传了明天而你的工具期望的是2024-01-15这种格式。如果你不在 Harness 层做参数校验和规范化错误就会一路传到工具内部报一个莫名其妙的错。我的做法是在工具注册时声明参数的 JSON SchemaHarness 在调用前先校验校验失败就把错误信息回传给模型让它重新生成。第二个坑是工具描述写得太随意。工具的描述description是模型决定要不要调用它的唯一依据。我见过有人写查询数据模型根本不知道查什么数据、什么时候该用。好的工具描述应该包含这个工具做什么、什么场景下用、参数的含义和格式、返回什么。这本质上是在给模型写使用说明书。第三个坑是工具返回结果过大。有些工具一返回就是几千字的文本直接塞进上下文Token 瞬间爆炸。Harness 层应该对工具返回结果做截断或摘要比如超过 2000 字符就截断并提示结果过长已截断。下面是一个工具注册的简化示例用 Python 伪代码展示结构class ToolRegistry: def __init__(self): self.tools {} def register(self, name, description, param_schema, handler, timeout10, max_retries2): self.tools[name] { name: name, description: description, param_schema: param_schema, handler: handler, timeout: timeout, max_retries: max_retries, } def invoke(self, name, params): tool self.tools.get(name) if not tool: return {error: f工具 {name} 不存在} # 参数校验 if not validate_schema(params, tool[param_schema]): return {error: 参数格式不正确请检查后重试} # 带超时和重试的调用 for attempt in range(tool[max_retries] 1): try: result call_with_timeout(tool[handler], params, tool[timeout]) return truncate_result(result, max_len2000) except TimeoutError: if attempt tool[max_retries]: return {error: 工具调用超时} except Exception as e: if attempt tool[max_retries]: return {error: f工具调用失败: {str(e)}}这段代码看着简单但每一行都是踩坑换来的。参数校验、超时、重试、结果截断缺一个都会在生产环境给你颜色看。2.3 超时、重试与降级策略的参数选择超时和重试的参数不能拍脑袋定要有依据。我的经验值是这样的调用类型超时时间重试次数重试间隔说明模型生成非流式60s2指数退避 1s/2s模型生成慢超时给足模型生成流式首包 10s整体 120s12s流式首包超时说明网络或服务有问题外部 API 工具10s2固定 1s大部分 API 10s 足够数据库查询5s1500ms数据库慢查询要单独优化文件读写3s0-本地操作失败重试意义不大重试策略上我强烈建议用指数退避而不是固定间隔。原因是如果服务端是因为过载而失败你固定间隔重试只会加剧过载。指数退避给服务端喘息时间成功率更高。但要注意加一个最大间隔上限比如不超过 10 秒否则用户等太久。降级策略要分场景。模型调用失败可以降级到备用模型工具调用失败可以返回暂时无法获取该信息让 Agent 继续走其他分支如果关键路径上的调用全挂了那就得触发 Loop 层的终止给用户一个明确的失败提示而不是让 Agent 卡在那里空转。实操心得重试一定要区分可重试错误和不可重试错误。网络超时、限流429可以重试参数错误400、权限错误403重试多少次都没用直接失败。我见过有人无脑重试结果一个参数错误重试了 5 次白白浪费了 5 倍的时间和成本。2.4 可观测性没有日志的 Agent 就是黑盒Agent 的可观测性比传统服务更重要因为它的行为是不确定的。同样一个输入模型可能这次调 A 工具下次调 B 工具。没有详细的日志你根本不知道线上发生了什么。我在 Harness 层埋的日志包括每次模型调用的完整请求和响应脱敏后、每次工具调用的参数和结果、每次调用的耗时和 Token 消耗、每次异常的堆栈和上下文。这些日志用结构化的 JSON 格式输出方便后续检索和分析。除了日志还有两个东西值得投入调用链路追踪和成本看板。链路追踪能让你看到一个请求经过了哪些节点、每个节点花了多久快速定位瓶颈。成本看板能让你知道每个用户、每个功能花了多少钱这在商业化场景里是刚需。我见过一个团队Agent 上线后成本失控一查发现是某个工具返回结果特别大导致上下文暴涨每次调用 Token 是正常情况的 10 倍。如果他们有成本看板这个问题第一天就能发现而不是等到月底账单出来才傻眼。3. Loop 层让 Agent 持续推进而不失控3.1 循环的终止条件Agent 工程最容易翻车的地方Loop 层最核心的问题就一个什么时候停。这个问题看似简单实则是最容易翻车的地方。我总结了几种常见的终止条件实际项目中往往是组合使用目标达成模型明确表示任务完成不再调用工具直接返回最终答案。这是最理想的终止。最大轮次设置一个硬上限比如最多循环 10 轮。这是兜底防止无限循环。无进展检测连续 N 轮没有产生新的有效信息比如反复调同一个工具、返回结果高度相似强制终止。超时终止整个任务设置一个总超时比如 5 分钟到点就停。人工中断在关键节点设置人工确认人可以选择继续或终止。这几种条件要组合使用缺一不可。只靠目标达成模型可能永远不认为任务完成只靠最大轮次可能在任务快完成时被强行打断。我的经验是最大轮次 无进展检测 超时三个兜底条件必须有目标达成作为正常终止路径。无进展检测的实现有个小技巧不要只比较工具名要比较工具名 参数。如果模型连续两次用完全相同的参数调同一个工具那基本可以判定它卡住了。这时候可以给模型一个提示你已经用相同参数调用过这个工具了请换一个思路或直接给出答案。3.2 上下文管理Token 成本与信息完整性的平衡Loop 每转一圈上下文就会增长一截。如果不加管理几轮下来上下文就爆了要么超出模型窗口限制要么 Token 成本高得离谱。上下文管理是 Loop 层的核心工作之一。我的策略是分层管理把上下文分成系统提示、任务目标、历史对话、工具结果四部分分别处理。系统提示固定不变始终保留。任务目标固定不变始终保留。这是 Agent 的北极星不能丢。历史对话滑动窗口 摘要。保留最近 N 轮完整对话更早的对话压缩成摘要。工具结果按需保留。如果工具结果已经被模型消化并体现在后续推理中可以替换成一句已获取 XX 信息。摘要的生成时机很关键。我的做法是当上下文 Token 数超过阈值比如模型窗口的 70%时触发一次摘要把最老的一批对话压缩。摘要本身也是一次模型调用所以要控制频率不能每轮都摘要。注意摘要会丢失信息所以关键信息比如用户明确提出的约束条件要单独提取出来放在任务目标里不参与摘要。我踩过的坑是用户说预算不超过 500 元结果摘要时把这个约束丢了Agent 推荐了一堆超预算的东西。3.3 人在回路什么时候该让人介入全自动的 Agent 听起来很酷但在生产环境里关键节点让人介入往往更靠谱。人在回路Human-in-the-loop的介入点设计是 Loop 层的重要考量。我一般在这几个场景设置人工介入高风险操作前比如删除数据、发送邮件、支付、模型置信度低时比如模型自己说我不确定、涉及敏感信息时比如处理用户隐私数据、成本超过阈值时比如某个任务已经花了不少钱还没完成。介入的实现方式有两种阻塞式和异步式。阻塞式是 Agent 停下来等人确认确认后才继续异步式是 Agent 先把能做的做完把需要确认的部分挂起人确认后再继续。阻塞式体验好但效率低异步式效率高但实现复杂。我的建议是高风险操作必须阻塞式其他场景可以异步。3.4 循环中的状态保存与恢复Agent 任务可能跑很久中间可能因为各种原因中断服务重启、网络故障、用户关闭页面。如果每次中断都从头开始体验极差。所以 Loop 层要支持状态保存和恢复。状态保存的核心是把每一轮的关键状态当前轮次、已执行的动作、已获取的信息、下一步计划序列化存下来。恢复时从上次的状态继续而不是从头来。这里有个设计选择存快照还是存事件。存快照是每轮存一份完整状态恢复时直接加载存事件是只存增量变化恢复时重放事件。快照简单但占空间事件省空间但恢复慢。我的经验是轮次少的任务用快照轮次多的任务用事件 定期快照。状态存储的介质也有讲究。内存最快但重启就丢Redis 快且支持持久化适合大多数场景数据库最稳但慢适合对可靠性要求极高的场景。我一般用 Redis 存运行中的状态任务完成后归档到数据库。4. Graph 层复杂任务的编排拓扑4.1 从线性链到有向图什么时候需要 Graph不是所有 Agent 都需要 Graph。如果你的任务就是用户问一句Agent 答一句那 Loop 层就够了硬套 Graph 反而是过度设计。Graph 的价值在任务变复杂时才体现出来。我判断要不要上 Graph看三个信号任务有明确的多阶段比如先调研、再分析、再生成报告、阶段之间有条件分支比如根据调研结果决定走哪条分析路径、需要并行处理比如同时查多个数据源再汇总。满足任意两个就值得上 Graph。从线性链到有向图的演进本质是把隐式的流程控制变成显式的拓扑结构。线性链里流程控制藏在代码的 if-else 和循环里改起来牵一发动全身有向图里流程控制就是节点和边改流程就是改图清晰得多。4.2 节点设计粒度、职责与复用Graph 的节点设计核心是粒度。节点太粗一个节点干太多事又变成了隐式流程节点太细图变得极其复杂维护成本高。我的经验法则是一个节点只做一件可以用一句话描述清楚的事。比如查询天气是一个节点根据天气生成建议是另一个节点。如果一个节点的描述里出现了并且、然后那它可能该拆了。节点的职责要单一但节点本身要可复用。比如调用某个 API 获取数据这种节点应该设计成通用的通过参数区分不同的 API而不是每个 API 写一个节点。复用的节点多了图的维护成本会大幅下降。节点之间通过**状态State**通信。每个节点读取状态、处理、写回状态。状态的结构要提前设计好它是节点之间的契约。我见过有人状态结构随意改导致节点之间对不上调试起来极其痛苦。4.3 条件分支与并行分支的实现条件分支是 Graph 的灵魂。它的实现方式是节点执行完后根据结果决定走哪条边。比如查询天气节点执行完如果结果是下雨走生成带伞提醒这条边否则走跳过提醒这条边。条件分支的实现要注意条件要穷举。如果只写了下雨和不下雨两条边但实际出现了查询失败的情况图就卡住了。所以每个条件分支都要有默认边fallback处理意外情况。并行分支是提升效率的利器。比如同时查天气、查活动、查交通三个节点并行执行最后汇总。并行的实现有两种真并行多线程/多进程和伪并行依次执行但逻辑上独立。真并行快但复杂伪并行简单但慢。我的建议是IO 密集型的任务用真并行CPU 密集型的任务谨慎用可能反而更慢。并行分支的难点在合并。多个并行节点的结果怎么汇总我的做法是定义一个汇总节点它等待所有并行分支完成然后统一处理。如果某个分支失败了汇总节点要能处理部分成功的情况而不是整个任务失败。4.4 子图嵌套与错误回退复杂任务往往可以拆成多个子任务每个子任务本身又是一张图。这就是子图嵌套。子图的好处是关注点分离。主图只关心子任务之间的流转子图内部怎么实现主图不关心。子图嵌套的实现关键是状态的隔离与传递。子图有自己的内部状态但对外只暴露必要的输入输出。这样主图改起来不会影响子图子图改起来也不会影响主图。错误回退是 Graph 层的兜底机制。当某个节点失败时不是直接终止整个任务而是回退到上一个稳定状态尝试其他路径。比如查询天气失败了可以回退到询问用户城市节点让用户确认城市后再试。回退的实现要记录检查点Checkpoint。每个关键节点执行前保存一份状态快照。失败时从最近的检查点恢复。检查点的粒度要权衡太密占空间太疏回退代价大。我的经验是在有副作用的节点前必须存检查点其他节点可以按需存。5. 三层架构的协作与生产实践5.1 一个完整案例从需求到落地的三层设计我把前面提到的天气 活动推荐案例完整走一遍展示三层如何协作。Graph 层设计节点包括解析意图、查询天气、天气判断、生成提醒、搜索活动、汇总输出。边包括解析意图→查询天气查询天气→天气判断天气判断→ 下雨走生成提醒不下雨走搜索活动生成提醒→搜索活动搜索活动→汇总输出。Loop 层设计搜索活动节点内部有循环最多搜 3 次每次根据上次结果调整关键词。终止条件是找到 3 个以上满意结果或达到最大轮次或连续两次结果高度相似。Harness 层设计天气 API 超时 10s、重试 2 次搜索工具超时 15s、重试 1 次模型调用超时 60s、重试 2 次。所有调用记录日志和 Token 消耗。这个案例里三层各司其职Graph 管流程Loop 管搜索的迭代Harness 管每次调用的稳定性。任何一层出问题都能快速定位。5.2 性能与成本的优化手段Agent 的性能和成本是生产环境的两大命脉。我总结几个实用的优化手段缓存模型调用和工具调用都可以缓存。相同的输入直接返回缓存结果能省大量时间和成本。但要注意缓存的失效策略尤其是涉及实时数据的场景。模型分级不是所有节点都需要用最强的模型。简单的意图解析用小模型复杂的推理用大模型。我见过一个项目所有节点都用最贵的模型成本是优化后的 5 倍。并行化能并行的节点尽量并行。前面说的并行分支能把总耗时从各节点之和降到最慢节点的时间。上下文精简前面 Loop 层讲的上下文管理直接决定了 Token 成本。精简上下文是最有效的降本手段之一。提前终止如果任务已经能给出满意答案不要为了完整而继续跑。比如搜索活动已经找到 3 个很好的结果就没必要再搜第 4 个。5.3 常见架构反模式与避坑指南最后说说我见过的几个典型反模式都是血泪教训反模式一Harness 里写业务逻辑。表现是 Harness 代码里出现大量 if-else 判断业务场景。后果是 Harness 越来越臃肿无法复用。正确做法是把业务逻辑上移到 Graph 或 Loop。反模式二Loop 没有兜底终止条件。表现是只靠目标达成终止结果模型陷入死循环。后果是任务永远跑不完成本失控。正确做法是必须有最大轮次、无进展检测、超时三重兜底。反模式三Graph 节点粒度过细。表现是一张图有几十个节点每个节点只做一行代码的事。后果是图极其复杂维护成本高。正确做法是节点粒度适中一个节点做一件有意义的事。反模式四状态结构随意变更。表现是节点之间通过全局变量通信状态结构随时改。后果是节点之间对不上调试困难。正确做法是提前设计好状态结构作为节点之间的契约。反模式五没有可观测性。表现是线上出问题只能靠猜。后果是排查效率极低问题反复出现。正确做法是从第一天就埋好日志、链路追踪、成本统计。实操心得三层架构不是银弹不要为了分层而分层。小项目可能只需要 Harness Loop硬加 Graph 是过度设计。分层的目的是让问题有处可查、让修改有处可下手如果达不到这个目的分层就是负担。6. 常见问题与排查技巧实录6.1 Agent 卡住不动了怎么办这是最常见的问题。排查思路是自下而上先看 Harness 层有没有调用超时或失败再看 Loop 层是不是陷入了无进展循环最后看 Graph 层是不是卡在某个节点等不到输入。具体操作先查日志看最后一次成功的调用是什么、之后发生了什么。如果日志显示某个工具调用一直超时那是 Harness 的超时配置问题如果日志显示模型反复调同一个工具那是 Loop 的终止条件问题如果日志显示某个节点执行完就没有下文了那是 Graph 的边配置问题。6.2 Token 消耗异常怎么定位Token 消耗异常八成是上下文管理出了问题。排查步骤先看单次调用的 Token 数如果单次就很大那是上下文里塞了太多东西再看调用次数如果次数很多那是 Loop 循环太多或 Graph 节点太多。定位到具体原因后对应的解法是上下文太大就做摘要和截断循环太多就优化终止条件节点太多就合并节点。我一般会做一个 Token 消耗的看板按用户、按功能、按节点维度统计异常一眼就能看出来。6.3 工具调用失败率高的排查路径工具调用失败率高先分类是超时、是参数错误、还是服务端错误。超时的话看是不是超时时间设太短或者工具本身太慢参数错误的话看是不是工具描述不清楚导致模型生成错误参数服务端错误的话看是不是限流或服务不稳定。我遇到过一个案例某个工具失败率特别高查下来发现是模型生成的参数里日期格式不对。解法是在工具描述里明确写清楚日期格式并在 Harness 层加参数校验和自动修正。改完之后失败率从 30% 降到了 2%。6.4 常见问题速查表问题现象可能原因排查方向解决手段Agent 卡住不动工具超时/循环无终止/节点卡住查日志看最后成功调用调超时/加终止条件/修边配置Token 消耗异常上下文过大/循环过多/节点过多看单次 Token 和调用次数摘要截断/优化终止/合并节点工具失败率高超时短/参数错/服务端问题分类统计失败原因调超时/改描述加校验/加降级任务结果跑偏Graph 节点拆分不当看哪个节点输出偏离重新设计节点和边成本失控模型选型不当/无缓存看各节点成本占比模型分级/加缓存/精简上下文恢复后重复执行状态保存不完整看检查点内容补全状态/加幂等处理6.5 几个独家避坑技巧技巧一给每个节点加心跳日志。节点开始和结束时各打一条日志这样一眼就能看出卡在哪个节点。没有心跳日志排查卡顿问题就像大海捞针。技巧二工具调用加幂等键。如果工具调用可能重试一定要加幂等键避免重复执行有副作用的操作。比如发送邮件这种工具重试时如果没幂等键用户会收到两封邮件。技巧三模型输出加格式校验。模型返回的 JSON 经常有格式问题多一个逗号、少一个引号。在 Harness 层加格式校验和自动修复能大幅降低下游节点的失败率。技巧四关键路径加熔断。如果某个工具连续失败 N 次暂时熔断它直接返回降级结果避免一直重试拖垮整个任务。熔断一段时间后再半开重试。技巧五定期做混沌测试。故意让某些工具超时、返回错误看 Agent 能不能优雅处理。我见过太多系统在正常路径下跑得很好一遇到异常就崩了。混沌测试能提前暴露这些问题。这套三层架构我在多个项目里反复验证过它最大的价值不是某个具体的技术点而是提供了一套定位问题和组织代码的框架。当你面对一个复杂的 Agent 系统时先问自己这个问题属于哪一层想清楚这个解决路径就清晰了。