
去年做第二个 Agent 项目时我差点被自己的代码堆埋掉。当时的架构很简单一条链模型跑完接工具工具结果再喂给模型循环往复。前五十个流程内一切正常等业务要求变成模型需要根据上游判断决定是继续追问还是直接收尾链式编排开始失控我在一个函数里塞了七八层 if-else改一处崩三处最后连自己都说不清某个分支到底在什么条件下触发。后来我把这套东西整个推翻换成基于 Graph 架构的 Agent Harness——把执行流程建模成一张有向图节点是工作单元边是流转条件运行时负责调度、状态恢复和检查点。改完以后复杂流程的改动变成了改几条边、加一个节点的事而不是在调用链里大海捞针。这篇就围绕 Graph 架构的 Agent Harness 展开讲讲它到底解决了什么问题、核心抽象怎么设计、真实需求怎么拆成一张图以及我在落地过程中踩过的几个比较深的坑。适合已经做过 Agent 原型、想往可维护、可观测、可恢复方向走的开发者也适合正在评估编排架构、纠结要不要从链式切到图式的人。1. 为什么非用 Graph 不可链式编排的边界与 Graph 的解围方式1.1 链式编排的最初便利与失控现场链式编排在项目起步阶段非常舒服。代码写出来就是流程本身调用模型得到结果把结果传给工具工具返回后再次调用模型拼接最终输出。每一步的输入输出清晰测试也直观只要把函数按顺序排好一个能跑的 Agent 五分钟就能立起来。这是我一开始选链式的原因也是绝大多数人从 demo 走向真实业务时的起点。但真实业务从来不是直线。比如一个客服质检 Agent它要先判断用户情绪再决定走安抚话术还是业务流程说明业务说明又分售前咨询和售后问题售后里再分退款、换货、投诉升级。如果用链式实现每个分支判断都要写在调用链里流程一旦超过三四层代码里会同时出现根据上一轮结果决定下一步和根据工具返回决定是否继续两种逻辑二者交织在一起状态散落在各个函数返回值里想追一个数据从哪儿来、被谁改过几乎只能靠肉眼追代码。更麻烦的是并行和回退。链式架构天然只表达顺序多个 Agent 并行处理同一批任务时要么加线程池维护状态要么把并行结果手动合并而某个分支失败后回退到上一个节点重新走这种高频需求在链式里实现起来等于手工维护一个栈。流程稍微一变整个调用链重写一遍测试用例全部失效。我在某次复盘时数过当时一个信息收集 → 分类 → 生成回复 → 人工复核 → 发送的流程光状态字段就有十几个函数之间互相传参有一半参数其实只是透传没人知道哪一步真正消费它。那已经不是写得快不快的问题了是根本不敢改。1.2 把流程当图而不是当代码链式编排的本质是用函数调用来表达控制流而控制流一旦复杂函数调用这种表达方式就不够用了。Graph 架构换了一个视角流程不是下一步调用谁的代码而是谁能从哪儿流向哪儿的结构。在这个模型里每个节点是一个独立工作单元处理一件事输入输出都是被定义好的。边描述节点之间的依赖关系和流转条件。分支是条件边并行是多个节点指向同一个收束点循环是从某个节点回退到之前的节点回退就是一条反向边。也就是说分支、并行、循环、回退这些在代码里很难维护的控制逻辑在图里都变成了可声明的结构而不是散落在 if-else 里的逻辑。我常用一个地铁换乘的类比链式编排像一条只跑一辆车的单线公交乘客只能按固定站点顺序走中途不能换乘、不能并线、不能折返。Graph 是地铁网络每个站点是一个节点你可以从 A 到 B、从 B 到 C也可能从 C 发现前面封站了回头走 B2 线再绕到 D。修网络的一条线不用把所有列车都改一遍。Graph 还带来两个链式难以匹敌的收益。一是可重放因为每个节点输入输出都是显式的跑完一次可以回看数据流定位问题出在哪个节点。二是可恢复节点之间通过共享状态传递数据某个节点失败后可以从检查点恢复而不是整条链从头跑。这两点直接决定了复杂 Agent 能不能稳定落在生产环境。我见过不少团队最初觉得 Graph 是过度设计等流程复杂到第三个月就默默切回去了——不是切回链式把代码堆得更厚而是换了一套图编排框架来用。反过来看与其等代码失控再重构不如在架构选型时就把流程会变复杂这个预期提前放进设计里。2. Harness 到底是什么Graph Agent 的执行外壳与关键抽象2.1 Harness 的职责边界Graph 解决的是流程怎么表达的问题但光有 Graph 还不够你还需要一层东西来让它真正跑起来并且跑得稳、停得住、能恢复。这层东西就是 Harness。Harness 这个词直译是挽具放在 AI Agent 场景里我把它理解为执行外壳。它不负责写提示词不负责训练模型也不负责实现具体工具它负责的是把这些部件组装起来之后的一切工程问题节点如何启动、状态如何流转、失败如何重试、中断如何恢复、日志如何记录、权限如何控制。如果没有 HarnessGraph 只是一张描述性的图画得再清楚也不能自动执行。有了 Harness图才会变成一台可以被调度、被监控、被中断、被恢复的机器。我见过一些刚好相反的做法把 Harness 当成业务框架往里塞各种业务逻辑最后图和业务纠缠在一起改业务等于改框架。这里的原则很简单Harness 只处理流程如何执行不处理每个节点里到底做什么。节点自己是黑盒Harness 只需要知道节点有输入、有输出、可能失败、可能超时。2.2 节点、边、运行时三要素设计Graph Harness 的核心抽象我总结了三个节点Node、边Edge、运行时Runtime。听起来简单但每部分都有需要认真设计的地方。节点的类型划分直接影响图的表达能力。我把节点分成几类节点类型职责典型场景LLM 节点调用大模型处理自然语言任务意图识别、内容生成、总结归纳工具节点调用外部能力如数据库、API、文件系统查库存、发消息、写文件路由节点根据输入决定走哪条边不产生业务输出判断继续追问还是直接结束并行节点同时触发多个子节点并等待全部完成多源信息收集收束节点汇总并行节点的结果合并数据把多个渠道的结构化信息拼成一份人工确认节点暂停流程等待人类审批或输入高危操作确认、方案审核循环边界节点控制某个子流程的循环次数和退出条件反复优化方案直到满足指标边的设计同样重要不只是谁流向谁还分不同类型。我常用的边有三种顺序边无条件的 A 到 B条件边根据节点输出判断走 A 还是走 B数据流边描述的是数据依赖而不是执行依赖这类边不触发节点执行只是告诉运行时B 节点的输入依赖 A 节点的输出主要用于状态可见性和校验。运行时是 Harness 的大脑。它维护一个可执行队列从起始节点拿到所有出边按边类型决定是直接入队还是等条件满足节点执行完把结果写回共享状态再触发新一轮出边调度。它还要处理并行节点的并发控制、超时熔断、重试策略、检查点保存。可以说图只负责长得好看运行时负责跑得可靠。2.3 状态设计集中式共享内存与局部隔离Graph 里最容易忽略的是状态。链式架构状态散落在变量里Graph 则适合采用集中式共享状态整个流程共享一个顶层状态对象每个节点从里面读输入、往里面写输出。这样数据流变的显式排查问题时打开状态对象就能看清每一步产生了什么。但集中并不意味着所有节点共享一棵大树。我的做法是给状态划分命名空间每个节点只允许读写自己的命名空间跨节点读取必须走数据流边声明。比如多源聚合案例里信息源 A 节点的输出放在sources.a信息源 B 放在sources.b收束节点读取整个sources命名空间做合并。这样既满足集中共享又不会发生两个节点写同一个字段互相覆盖。状态还要做版本化。每次节点写入状态时生成一个单调递增的版本号结合检查点机制可以把整个流程恢复到任意节点执行完后的状态。这在调式、回放、异常恢复中非常有用。我经常跑完一轮流程直接把状态快照拉出来看每个节点写入了什么很多时候 bug 当场就定位了。如果你发现自己频繁需要手动读写状态的中间层说明状态设计有问题。应该让节点输入输出尽可能自包含中间的组装交给收束类节点。3. 把真实需求拆成一张有向图多源聚合 Agent 的完整案例3.1 业务需求与拆分思路用前面说的抽象来做一个具体的虚构项目多源信息聚合与行动建议 Agent。它的任务是接收来自不同渠道的结构化或非结构化信息统一格式后检测冲突生成一份带优先级的行为建议提交人工确认后再落地到下游系统。这个场景选它是因为它有并行多个信息源同时处理、有分支是否有冲突走不同流程、有回退人工驳回后重新生成方案、有收束多源结果合并几乎涵盖了 Graph 的典型能力。第一步是拆节点collect_sources并行触发多个信息源采集节点每个节点负责一个渠道输出原始内容。normalize_format把原始内容统一成结构化的标准字段如时间、主体、类型、原始文本。detect_conflict检查各来源之间是否存在冲突比如时间对不上、主体不一致。generate_plan根据标准化的信息生成行为建议方案输出候选计划和理由。human_review暂停流程等待人工确认或驳回意见。apply_plan方案通过后把计划落地到下游系统输出执行结果。图本身看起来像一段话先并行收集归一化检查冲突有冲突走生成方案 → 人工确认 → 落地没冲突直接生成方案 → 人工确认 → 落地人工驳回就回到生成方案再来一次。3.2 节点、边与状态的具体定义图中的节点定义需要明确输入输出。以normalize_format为例输入是collect_sources输出的原始内容集合输出是标准化记录集合。这里的输入输出都必须走图状态中的命名空间不能在节点内部偷偷引用外部变量。边规则可以定义为条件边detect_conflict的输出是有冲突还是无冲突。运行时拿到这个输出后自动决定走哪条边——有冲突的话进入一轮冲突分析子流程或多走一次generate_plan无冲突则直接进入generate_plan生成常规方案。这个结构最关键的改动是新增一个分支逻辑时我只加一条边不用改节点内部代码。状态对象长这样伪代码表达意图state { sources: { source_a: None, source_b: None, source_c: None }, normalized_items: [], conflict_flag: False, conflict_details: [], draft_plan: None, review_status: pending, # pending / approved / rejected review_comment: , final_output: None }人工确认节点比较特别它不是一个常规的函数调用而是把流程挂起写一条待办等外部用户通过接口提交确认结果后恢复执行。运行时需要支持这种外部中断后恢复的能力这也是 Harness 比普通代码库多出来的关键机制。没做暂停恢复机制之前人工节点只能靠轮询后来我加了基于检查点的挂起机制简单高效而且能记录完整上下文。3.3 图构建与运行时的核心代码形态graph 的构建在代码里其实很直观。我不强调具体语言和框架用接近 Python 的伪代码表达意图graph Graph() graph.add_node(collect_sources, is_parallelTrue) # 并行子节点 graph.add_node(normalize_format) graph.add_node(detect_conflict) graph.add_node(generate_plan) graph.add_node(human_review, is_humanTrue) # 人工节点 graph.add_node(apply_plan) graph.add_edge(START, collect_sources) graph.add_edge(collect_sources, normalize_format) graph.add_edge(normalize_format, detect_conflict) # 条件边detect_conflict 决定走哪条路 graph.add_conditional_edge(detect_conflict, if_is_conflict, targetgenerate_plan) graph.add_edge(generate_plan, human_review) # 人工驳回则回退到 generate_plan graph.add_conditional_edge(human_review, if_is_approved, targetapply_plan, if_elsegenerate_plan) graph.add_edge(apply_plan, END)运行时执行时大概长这样runtime Runtime(graph, state) runtime.run()关键是run()内部不是线性调用它会维护一个待执行队列初始放入START节点的所有出边节点执行节点后根据边规则动态出入队。并行节点则把多个子节点同时放入队列等全部完成后触发收束。任何节点抛异常都会落到统一的重试逻辑超过阈值则暂停整图并保存检查点。我不在这里展开框架细节因为凡是能表达节点回调 条件边 共享状态能力的实现都可以承载这套设计。核心是思路具体选型看团队熟悉度。3.4 为什么这样切内聚、可测、可恢复拆节点看着简单真正值得讲的是为什么这样拆。首先是内聚性。每个节点只做一件事因此每个节点可以单独测试只给它构造一份固定输入看输出是否符合预期。比如detect_conflict输入一份标准化记录输出一个布尔值测试用例就是各种冲突组合不依赖任何外部模型。其次是可恢复性。假设在apply_plan这步调用下游系统时超时整条流程不会从头再来检查点机制可以把状态恢复到review_status approved那一刻重新尝试落地。这在真实生产里直接决定用户的体感——从头重跑几十分钟流程和从失败点恢复几秒钟体验差别巨大。最后是节点可复用。多个不同 Agent 可以共用normalize_format、detect_conflict这类通用节点因为节点对输入输出有明确约定跨项目迁移成本很低。这是我切换到 Graph 架构后感受到的最大红利不同业务里的大量基础设施能力沉淀成了图里的标准积木。4. 落地期间最常踩的五个 Graph Agent 坑Graph 架构理论上是清爽的但实际用起来我踩了不少坑。挑了最典型的五个展开讲每一个都是教训换来的。4.1 重试导致重复副作用工具调用不是天然幂等的有一次落地apply_plan节点向外部的任务系统发创建指令第一次请求超时运行时自动重试结果外部系统同时收到两次请求产生了两条重复任务。问题根源在于我把重试逻辑放在了节点外层而节点自己的调用不保证幂等。排查过程很典型我先看日志发现重试只发生了一次然后去外部系统查请求时间戳发现两条记录相差不到一分钟再对比请求 ID确认是同一次图执行的两次调用。解决方案分两层。第一层给外部调用加幂等键把图执行的run_id和节点名组合成一个全局唯一的task_id外部系统靠它去重。第二层在状态里记录每个工具节点的执行结果重试前先查这个节点是不是已经成功执行过成功了就直接复用结果不重复调用。后来我把这个逻辑抽象成幂等执行器节点在本地状态维护一个executed_flag执行前检查执行成功立即写入带重试和超时。这次踩坑之后我所有的工具节点都默认带幂等保护。4.2 循环路由没设收敛条件Agent 自己把自己绕晕在我设计的human_review驳回回退到generate_plan的场景里原本预期最多驳回两次就稳定结果有一次用户连续驳回了五次Agent 每次重新生成方案都会产生一组新的候选计划状态里的draft_plan被反复覆盖整个流程像进入了死循环。排查时我先用日志打印了每次驳回的理由和时间发现每次驳回理由都不一样也就是说不是上游数据问题而是方案本身每次都差一点塔的死循环源自于没有对循环次数做限制。解决方案是给循环边界加收敛控制。我在状态里新增了迭代计数每次进入generate_plan时attempt_count 1节点内部如果发现attempt_count 3就不再重新生成而是把当前方案标记为推荐但待人工强制确认走另一条边到人工节点。同时运行时增加整体熔断一个节点在同一个 run 里最多执行 N 次超过 N 次直接暂停整个图等待人工介入。循环收敛条件必须在设计阶段就定好。没有限制的循环在链式里最多是代码写得臭在 Graph 里会直接表现为图跑不完对生产系统是致命的。4.3 并行节点共享状态竞争最后一个写入者赢并行处理多个信息源时我曾经让三个子节点并行写同一个顶层字段sources.current_content结果发现只有最后一个完成写入的数据保留了下来前面两个源的信息全部丢失。我把日志打开挨个看三个节点的完成时间发现三个节点几乎同时结束但状态里只有一个值。原因很明确三个节点读到的共享状态是同一个对象各自往里赋值后写的覆盖先写的串行的状态模型根本无法感知并发写入。根子上的问题是状态设计没有给并行节点做隔离。修复方案是改状态结构并行子节点只能写自己命名空间下的字段用源 ID 做键比如sources.source_a、sources.source_b收束节点再去合并。这样各写各的不存在竞争。如果确实需要多个节点写同一个聚合字段就得给运行时加合并策略常见做法是锁、版本号或者写入队列。但说实话最省心的还是让写入目标分散合并交给收束节点统一处理。4.4 checkpoint 恢复时把外部工具也回放了一遍检查点机制有一个隐藏陷阱状态快照回滚没问题但如果恢复时把已经执行过的节点再跑一遍外部工具会被重复调用产生和 4.1 类似的副作用而且更难察觉因为本地状态里一切看起来都是正常的。有一次我主动触发一次检查点恢复想复现一个模型输出问题结果下游发货通知真的又发了一遍。排查花了很长时间因为不是超时重试而是我在恢复机制上只做了整个流程回到某个状态的实现没有区分状态回滚和动作重放的区别。正确做法是检查点只恢复状态不重放已经成功执行的外部副作用节点。运行时需要在状态里维护一个已执行节点表记录哪些节点已经成功跑完、输出是什么。恢复时从这张表里读出历史输出而不是重新执行。模型节点如果不涉及外部副作用可以安全重放但工具节点必须走执行结果缓存 幂等的路径。说白了检查点机制必须搭配执行历史表否则你恢复的是状态、重复的是副作用看起来日志正常、效果全错。4.5 图变大之后看不到图了可视化与调试断层图到了十几个节点以后我发现自己越来越难定位问题。以前链式代码可以看调用栈Graph 架构下节点之间不是函数调用而是状态流转你根本没法用传统堆栈来Debug。这个坑是最隐蔽的因为它是慢性病最开始节点少出问题很快能看出来等到节点多了每个节点内部都有复杂逻辑状态数据在命名空间间流动你在日志里看到一堆输出但说不清从哪个节点流过来的。我的解法有四个方向。第一运行时必须记录流转轨迹每次节点执行时都往 trace 里写一行当前节点名、输入状态版本、输出状态版本、耗时。第二把 trace 可视化我在调试面板里看一张图节点高亮显示已执行、失败、卡住的状态。第三支持单节点重放给定输入状态单独跑一个节点看输出不需要整个图全部跑完。这是我最常用的调试方式几乎取代了以前打日志的方式。第四状态 diff 功能对比两个版本状态之间的差异一眼看出谁改了哪个字段。没有可视化和 trace 的图编排节点少时无害节点一多就会让你陷入改一个节点、跑一遍全图、又找不到问题在哪的循环。我强烈建议在引入 Graph 架构的第一天就把 trace 机制做上别等出了问题再补。5. 让 Harness 真正可用可观测、可治理以及该用和不该用 Graph 的判断5.1 可观测性设计从图到轨迹前面提到 trace 和可视化这里展开说。可观测性对 Graph Agent 来说不是选配是标配。我从项目中期开始给运行时加了三类 trace 数据。第一类是执行轨迹也就是哪个节点在什么时间被执行、依赖哪些状态、输出是什么。第二类是耗时与 token 消耗每个 LLM 节点的调用耗时、token 数、重试次数都要记录这样你能清楚知道整个流程钱花在哪、时间耗在哪。第三类是状态变更日志每个节点执行前后的状态版本差能自动输出一个 JSON diff。这套数据除了方便调试还有一个更实用的用途回归测试。我把每次 Graph 运行产生的 trace 保存下来作为一组行为基线下一次改完代码再跑一遍相同输入对比 trace 是否仍然符合预期。模型输出可能有随机性但流程结构、节点顺序、状态变更应该稳定否则说明图逻辑被意外改动了。对于生产环境我建议把 trace 和业务日志分两个通道输出trace 走管道业务日志进日志平台。trace 保留最近一段时间用于回放不需要长期留全量按需归档即可。5.2 治理权限边界与资源预算Graph 架构跑起来之后我很快意识到一个问题图里有多少个 LLM 节点就有多少个不受控的资源消耗点。如果流程里有三个模型节点每个节点内部还悄悄调了好几次模型最终费用会非常可观。资源预算需要从运行时层面控制。我给每个节点设置了执行时间上限和 token 上限超了就终止节点并返回错误。整张图设一个总执行预算当累计耗时长或 token 数达到阈值运行时自动暂停并在状态中标记原因等待人工处理。除此之外还要看住并发数量并行节点最多同时跑几个子节点防止一次性拉起几十个请求把下游打挂。权限治理也同样重要。图中的工具节点往往有不同权限级别比如读库、写库、调用外部服务。我的做法是给每个节点声明一个最小权限范围运行时统一做鉴权节点要访问某个资源必须本身带对应权限否则直接拒绝执行。这样即使某个 Agent 被提示词误导它所在节点的权限边界也能限制危害面。5.3 什么时候别用 Graph不要为画图而画图Graph 架构不是银弹。我见过有人把三步流程硬拆成五个节点加三条边跑起来代码反而比链式更繁琐调试也更绕。我的判断标准很简单如果流程是固定的、简单的、没有分支和循环的直接用链式代码如果流程只需要一层判断、最多两三种分支链式加简单的条件语句也能驾驭如果流程涉及分支嵌套、多源并行、循环回退、人工介入、状态恢复或者你强烈需要把流程可视化纳入治理体系Graph 才有明显优势。Graph 还有一个不值得被低估的隐性成本它要求一开始就把状态设计和节点划分理清楚这对团队的设计能力有要求。链式代码写错了改一处就行图的结构错了可能要重新连线甚至拆节点。所以我的建议是先在项目里用一个小流程跑通这套抽象验证团队熟悉度再决定要不要全面铺开。不要因为标题看起来高级就强迫自己上 Graph——等需求复杂到链式真的撑不住时你自然会知道该切换了。开发 Agent 一段时间后我最大的体会是真正难的地方往往不在模型调用而在于把一个复杂目标拆成一段可以验证、可以恢复、可以收敛的执行过程。Graph 架构恰好逼着你这么做——每个节点有边界每条边有理由整个流程可以被观察、被中断、被恢复、被审计。我现在把所有新的 Agent 项目都默认按这套思路来设计哪怕一开始只有五六个节点。等流程真的复杂起来时你会发现已经把地基打好了。