1. 从单体到群体:为什么智能体需要图工程
1.1 一个让我彻底改变认知的踩坑经历
去年下半年我接手了一个内部知识问答智能体的项目,需求听起来很朴素:用户提问,系统检索文档,大模型生成答案。我当时的做法非常典型——一个 ReAct 循环,一个工具调用列表,一个提示词模板,跑通了就上线。前两周效果还行,到了第三周问题开始集中爆发:多跳问题答不对、需要对比两份文档时只会各说各话、遇到需要先算再查再总结的任务直接卡死。我盯着日志看了整整两天,最后意识到一件事——问题不在模型能力,而在我把所有逻辑都塞进了一个循环里。
这就是单体智能体的天花板。它像一个什么活都接的个体户,能力边界取决于那一个提示词能承载多少上下文。而真实业务里的任务,往往是"先分类、再检索、再校验、再生成、最后自检"这样一条有分支、有回路、有并行的工作链。把这条链压扁成一个循环,等于让一个人同时扮演前台、会计、审核和经理,出错是必然的。
Graph Engineering(图工程)要解决的正是这件事:把智能体的行为从"一个循环"升级为"一张有向图",节点是能力单元,边是流转规则,状态在图上流动。个体智能变成系统智能,靠的不是换一个更强的模型,而是换一种组织方式。
1.2 个体智能与系统智能的本质差别
我用一个生活化的类比来说明。个体智能像一位经验丰富的全科医生,什么都能看一点,但遇到复杂病例容易漏诊;系统智能像一家医院,分诊台、专科门诊、检验科、药房、复诊环节各司其职,每个环节只做自己最擅长的事,通过流程把整体能力放大。医院的整体诊疗水平,不取决于某一个医生有多强,而取决于流程设计是否合理、信息传递是否无损。
落到技术层面,这个差别体现在四个维度上,我整理成一张表方便对照:
| 维度 | 个体智能(单体 Agent) | 系统智能(图结构 Agent) |
|---|---|---|
| 控制流 | 单一循环,线性推进 | 有向图,支持分支、并行、回路 |
| 状态管理 | 全部塞进对话历史 | 显式状态对象,按节点读写 |
| 错误恢复 | 靠模型自我纠错 | 可设计重试节点、回退边、人工介入点 |
| 可观测性 | 只能看最终输出 | 每个节点可单独打点、评估、替换 |
| 扩展方式 | 改提示词,越改越臃肿 | 加节点、加边,局部替换不影响全局 |
这张表里最关键的一行是可观测性。单体智能体出问题时,你只能看到输入和输出,中间发生了什么全靠猜。而图结构把每一步都暴露出来,哪个节点拖慢了、哪个节点判断错了、哪条边走岔了,一目了然。这一点在工程化落地阶段是决定性的——你不能优化你看不见的东西。
1.3 为什么是现在:三个条件同时成熟
图工程这个概念不是凭空冒出来的,它踩在了三个同时成熟的支点上。
第一是模型能力的稳定化。早期模型连结构化输出都不稳定,你让它返回一个 JSON 都经常多带解释文字,根本没法做节点间的状态传递。现在主流模型对函数调用、结构化输出的支持已经相当可靠,节点之间的数据契约才立得住。
第二是编排框架的成熟。以 LangGraph 为代表的图编排框架把"状态、节点、边、检查点"这几个抽象做成了标准件,开发者不用再从零手写调度器。这类框架的核心价值不是省代码,而是把一套经过验证的工程范式固化下来。
第三是业务复杂度的倒逼。早期智能体做的是"问答""摘要"这类单步任务,单体足够。现在需求变成了"帮我完成一份竞品分析报告""自动处理这批工单",这类任务天然是多步骤、多角色、多校验的,单体架构根本扛不住。
我的判断是:2026 年之后,评价一个智能体项目是否专业,看的不是它用了哪个模型,而是它的图设计得是否合理。模型是发动机,图是传动系统,发动机再好,传动系统拉胯,车也跑不起来。
2. 图工程的核心构件:节点、边与状态
2.1 状态设计:整张图的地基
如果只能给一条建议,我会说:先把状态设计对,再谈其他。状态是图工程里最容易做错、也最影响后续一切的东西。
状态本质上是一个在节点之间流转的数据对象。每个节点读取它需要的字段,写入它产出的字段。设计状态时我踩过的最大坑是"什么都往里塞"——把原始文档、中间推理、工具返回、用户历史全堆进一个巨大的字典,结果节点之间耦合严重,改一个字段到处报错。
我的做法是按生命周期分层。把状态字段分成三类:
- 输入层:用户原始输入、会话标识、外部参数,全程只读。
- 工作层:检索结果、中间推理、工具调用记录,节点间读写,任务结束即丢弃。
- 输出层:最终答案、置信度、引用来源,只在收尾节点写入。
这样分层之后,每个节点只需要声明自己读哪些、写哪些,耦合度立刻降下来。下面是一个我用得比较顺的状态定义示例,用 Python 的 TypedDict 表达:
from typing import TypedDict, Annotated from operator import add class AgentState(TypedDict): # 输入层:全程只读 user_query: str session_id: str # 工作层:节点间流转 retrieved_docs: list[dict] reasoning_trace: Annotated[list[str], add] # 累加式写入 tool_calls: list[dict] # 输出层:收尾写入 final_answer: str confidence: float citations: list[str]这里有个细节值得说:reasoning_trace用了Annotated[list[str], add],意思是多个节点写入时自动做列表拼接而不是覆盖。这个机制在并行节点场景下特别有用——两个节点同时往同一个字段写,不会互相覆盖。这是图工程里一个非常实用但容易被忽略的设计,很多人第一次做并行分支时数据莫名丢失,就是栽在这里。
2.2 节点设计:单一职责是铁律
节点是图里的执行单元,可以是一次模型调用、一次工具调用、一段纯代码逻辑,甚至是一个人工审核的等待点。节点设计我遵循一条铁律:一个节点只做一件事,且这件事能用一句话说清楚。
反面例子是我早期做的一个"处理节点",它同时负责意图识别、文档检索和答案生成。听起来省事,实际上这个节点既没法单独评估,也没法单独替换,出了错根本定位不到是哪一步的问题。后来我把它拆成三个节点,每个节点单独打点,问题立刻现形——原来是意图识别把"对比"类问题误判成了"查询"类,导致检索策略用错了。
节点的粒度怎么把握?我的经验是看可测试性。如果一个节点你没法为它单独写一个输入输出明确的测试用例,那它大概率太大了,需要拆。反过来,如果两个节点永远一起出现、从不单独使用,那它们可能该合并。
2.3 边设计:控制流的真正表达
边决定了节点之间怎么流转,这是图工程区别于普通工作流引擎的核心。边分几种类型,各有各的用途:
- 普通边:A 执行完无条件进入 B,用于固定顺序。
- 条件边:根据状态里的某个字段决定走哪条分支,用于路由。
- 回路边:从后面的节点指回前面的节点,用于重试和迭代。
- 并行边:一个节点同时触发多个下游节点,用于并发处理。
条件边是使用频率最高的。举个我实际项目里的例子:一个客服智能体需要先判断用户意图,然后路由到不同的处理链。这个路由逻辑就写在条件边里:
def route_by_intent(state: AgentState) -> str: intent = state.get("intent", "unknown") if intent == "refund": return "refund_flow" elif intent == "technical": return "tech_support_flow" elif intent == "complaint": return "escalation_flow" return "general_flow" graph.add_conditional_edges( "classify_intent", route_by_intent, { "refund_flow": "handle_refund", "tech_support_flow": "handle_tech", "escalation_flow": "escalate", "general_flow": "handle_general", } )这里有个实操心得:路由函数的返回值一定要穷举所有可能,并且给一个兜底分支。我见过太多项目因为路由函数漏了一种情况,导致状态卡在某个节点再也走不下去,整个图直接挂死。兜底分支不是可选项,是必选项。
2.4 检查点:让图具备"记忆"和"恢复"能力
检查点(Checkpoint)是图工程里被低估最严重的能力。它的作用是在每个节点执行后把当前状态持久化下来,带来三个直接好处:断点续跑、人工介入、时间旅行调试。
断点续跑很好理解,长任务中途挂了不用从头再来。人工介入是指你可以在某个节点设置一个"暂停点",等人工确认后再继续——这在需要审核的场景里是刚需。时间旅行调试是我用得最多的:任务跑完后,我可以回放任意一个节点的状态快照,看当时模型到底看到了什么、输出了什么,定位问题效率提升非常明显。
检查点的存储选型上,开发阶段用内存或 SQLite 就够了,生产环境建议用支持并发写入的数据库。这里要注意一个坑:检查点会显著增加存储开销,如果状态里塞了大量文档原文,快照体积会爆炸。我的做法是状态里只存文档 ID 和摘要,原文放外部存储,需要时再取。
3. 多智能体协作:从一张图到多张图
3.1 什么时候该拆成多智能体
单张图能解决的问题,不要拆成多智能体。这是我用血泪换来的原则。多智能体带来的协调成本、状态同步成本、调试成本都是指数级上升的。那什么时候必须拆?我总结出三个信号:
第一,角色之间存在根本性的提示词冲突。比如一个智能体要"严格按事实回答",另一个要"发挥创意生成方案",这两种人格塞进一个提示词会互相打架,拆开反而各自清晰。
第二,子任务可以独立评估和迭代。如果检索质量可以单独度量、单独优化,那它就该是一个独立的智能体,有自己的评估指标和迭代节奏。
第三,需要不同的工具集和权限。一个智能体只能读数据库,另一个可以写数据库,权限隔离在工程上是硬需求,拆开是最自然的实现方式。
3.2 三种主流协作拓扑
多智能体的协作结构,我实践下来主要用三种拓扑,各有适用场景:
| 拓扑 | 结构 | 适用场景 | 典型问题 |
|---|---|---|---|
| 主管制 | 一个主管智能体调度多个工人智能体 | 任务可分解、需要统一决策 | 主管成为瓶颈 |
| 流水线 | 智能体按顺序接力处理 | 步骤明确、依赖线性 | 单点故障传导 |
| 对等协商 | 多个智能体互相讨论达成共识 | 需要多视角、有争议 | 容易陷入循环 |
主管制是我用得最多的。主管智能体不干具体活,只负责拆解任务、分派、汇总。它的提示词里核心就三件事:任务分解规则、分派依据、结果整合方式。这里的关键是主管不能太"聪明"——如果主管自己也开始做具体任务,整个结构就退化成单体了。
对等协商要慎用。我做过一个需要多视角评估的方案评审智能体,三个智能体互相讨论,结果经常陷入"你说得对但我补充一点"的无限循环。后来我加了一个硬性的轮次上限和一个仲裁节点,才把这个问题压住。任何可能形成回路的协作结构,都必须有终止条件,这是铁律。
3.3 状态在多个智能体之间怎么传
多智能体最大的工程难点是状态传递。我的做法是共享一个全局状态,但每个智能体只被允许读写自己的命名空间。这样既保证了信息可达,又避免了互相污染。
具体实现上,全局状态里给每个智能体划一块子字典,智能体 A 只能写state["agent_a"]下的字段,读的时候可以读全局。这个约束看起来简单,但它把"谁能改什么"这件事显式化了,调试时能快速定位是哪个智能体改坏了数据。
还有一个细节是消息传递的格式统一。智能体之间传的不是自然语言,而是结构化的消息对象,包含发送方、接收方、意图、载荷、时间戳。用自然语言传消息看起来灵活,实际上会让下游智能体不得不做额外的解析,既慢又不可靠。
4. 工程化落地:评估、可观测与成本控制
4.1 图结构智能体的评估怎么做
单体智能体的评估很简单:给一批输入,看输出对不对。图结构智能体的评估要复杂得多,因为错误可能发生在任何一个节点。我的评估体系分三层:
端到端评估看最终结果,这是最基本的。节点级评估看每个关键节点的输出质量,比如检索节点的召回率、路由节点的准确率。路径评估看整个执行路径是否合理,比如一个本该走"退款流程"的请求有没有走错分支。
节点级评估是图工程带来的独特价值。因为节点是独立的,你可以为每个节点准备专门的测试集。我通常会给路由节点准备几百条标注好的意图样本,每次改路由逻辑就跑一遍,准确率掉了立刻能发现。这在单体架构里是做不到的。
4.2 可观测性:让每一步都可见
可观测性我按三个维度搭建:日志、指标、追踪。
日志记录每个节点的输入输出和耗时,格式统一成结构化 JSON,方便检索。指标关注几个关键数字:节点平均耗时、路由分支分布、重试次数、失败率。追踪则是把一次完整请求的所有节点串成一条链路,用 trace_id 关联。
这里有个独家经验:路由分支分布这个指标特别有价值。如果某个分支的命中率突然从 20% 涨到 60%,往往意味着上游的意图识别出了问题,或者用户行为发生了变化。这个信号比单纯的错误率更早暴露问题。
4.3 成本控制的几个实操手段
图结构智能体天然比单体贵,因为节点多了、调用多了。控制成本我有几个手段:
缓存高频路径。很多请求走的是完全相同的路径,把路径的中间结果缓存起来,命中时直接返回。小模型做路由,大模型做生成。路由和分类这类任务用小模型完全够用,把大模型留给真正需要它的生成节点。并行化能并的节点。检索多个数据源这种操作完全可以并行,总耗时取决于最慢的那个而不是累加。
我实测下来,这三个手段组合使用,成本能压到原来的三分之一左右,而效果几乎无损。关键是要先测量再优化,不要凭感觉砍节点,砍错了效果掉得比成本快。
5. 常见问题与排查实录
5.1 状态卡死与无限循环
这是图工程最高频的问题。表现是任务跑着跑着不动了,或者日志里同一个节点反复出现。根因通常是回路边缺少终止条件,或者条件边的路由函数返回了未定义的分支。
排查方法很直接:看追踪链路,找到重复出现的节点,检查它的入边条件。如果是回路,确认循环计数器有没有正确递增;如果是路由,确认路由函数的返回值是否都在边映射表里。我的预防措施是给每个回路强制加一个最大迭代次数,超过就强制跳出并记录告警。
5.2 节点间数据丢失
表现是下游节点读不到上游写的字段。常见原因有三个:字段名拼写不一致、状态定义里没声明该字段、并行写入时被覆盖。
排查时先打印每个节点执行前后的完整状态快照,对比差异。如果是并行覆盖,检查是否用了累加式的注解。我现在的习惯是状态字段全部集中定义在一个文件里,所有节点引用同一份定义,从源头杜绝拼写问题。
5.3 并行节点的结果合并
并行节点跑完后需要合并结果,这里容易出问题。如果两个节点都往同一个列表写,没有累加注解就会互相覆盖。如果合并逻辑写在某个节点里,要确保这个节点在所有并行分支都完成后才执行。
我的做法是用一个专门的"汇聚节点",它的入边来自所有并行分支,框架会等所有分支完成后再触发它。合并逻辑就写在这个节点里,清晰且不会出错。
5.4 问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 任务卡死不动 | 回路无终止条件 | 检查循环计数器与最大迭代 |
| 走错分支 | 路由函数逻辑错误 | 打印路由输入与返回值 |
| 字段读不到 | 状态未声明或拼写错 | 对比状态定义与节点读写 |
| 并行结果丢失 | 缺少累加注解 | 检查 Annotated 配置 |
| 成本异常高 | 大模型用在了简单节点 | 分析各节点调用分布 |
| 效果不稳定 | 节点粒度过粗 | 拆分节点单独评估 |
5.5 几条踩坑换来的经验
第一,先画图再写代码。我现在的习惯是拿张纸把节点和边画出来,确认逻辑通了再动手。跳过这一步直接写代码,返工率极高。
第二,每个节点都要能单独跑。给节点写一个独立的测试入口,输入一个构造好的状态,看输出对不对。这个习惯能省下大量联调时间。
第三,日志要带 trace_id 和节点名。没有这两个字段的日志,在排查多节点问题时基本没用。
第四,不要过早优化图结构。先用最直白的结构跑通,再根据实际瓶颈调整。我见过有人一上来就设计了一个十几节点的复杂图,结果一半节点从没被走到过。
6. 一个完整的图结构智能体搭建示例
6.1 需求与图设计
我拿一个实际做过的"技术文档问答智能体"来演示。需求是:用户提问技术问题,系统检索内部文档,判断检索结果是否充分,不充分就改写查询重试,充分则生成带引用的答案,最后自检答案是否有据可依。
这张图的节点和边是这样的:入口节点接收问题,进入查询改写节点,然后检索节点,接着是一个判断节点——如果检索结果相关度低于阈值,走回路回到查询改写;如果达标,进入生成节点;生成后进入自检节点,自检不通过则回到生成重试,通过则输出。
6.2 关键节点的实现要点
查询改写节点的核心是让模型把口语化问题转成适合检索的关键词组合。这里我用的提示词很克制,只要求输出关键词,不要求解释。生成节点要求模型严格基于检索到的文档作答,并在每个论断后标注来源编号。自检节点检查答案里的每个论断是否都能在检索文档里找到支撑,找不到就标记出来触发重试。
判断节点的阈值设定是个经验活。我一开始设了 0.7,结果大量正常问题被判定为不充分,反复重试。后来降到 0.5,并改成"取 top3 文档的平均分",稳定性好了很多。阈值没有标准答案,要靠实际数据调。
6.3 回路与重试的边界控制
这张图有两个回路:检索不充分回改写,自检不通过回生成。两个回路都必须有次数上限。我的设置是改写最多重试 2 次,生成最多重试 1 次。超过上限就走兜底分支,返回当前最好的结果并附上"未能完全确认"的提示。
这个设计很重要。没有上限的回路在生产环境就是定时炸弹,某个刁钻的问题可能让系统无限循环,既烧钱又拖垮服务。兜底不是失败,是工程上的必要妥协。
7. 我对图工程未来的一点个人判断
做了一年多的图结构智能体,我最大的体会是:这个领域的门槛不在模型,而在设计。模型能力会持续变强,但把能力组织成可靠系统的能力,是需要工程师一点点积累的。图工程本质上是一种系统设计思维,它要求你既懂业务逻辑,又懂状态管理,还得有分布式系统的那套工程直觉。
我现在看一个智能体项目,第一眼看的不是它用了什么模型,而是它的图长什么样、状态怎么设计、回路有没有边界。这三点过关了,项目基本就稳了。至于未来,我猜图会越来越动态——不是工程师画死的,而是根据任务类型自动生成或调整的。但那是下一步的事,眼下把静态图设计扎实,已经能解决绝大多数实际问题了。
最后分享一个小技巧:如果你刚开始接触图工程,别急着上框架,先用一个字典当状态、一个函数当节点、一个 if-else 当边,手写一个最小的图跑通。理解了状态怎么流、边怎么控,再上框架会顺畅得多。框架帮你省的是样板代码,但状态和边的设计思路,得你自己长在脑子里。