这次我们看一个偏工程的话题:把 Agent 的 SOP 画成 Graph,然后让它自己跑。
很多团队写 Agent 还是老办法:一个 prompt 接一个 prompt,线性往下传。流程简单时没问题,一旦出现分支判断、条件回退、多步骤并行、超时重试,代码就变成一锅粥。Graph Engineering 的思路是在 Agent 和任务之间加一层“流程结构”:把 SOP 里的每一个环节标成节点,把流转规则标成边,Agent 按图执行,走到哪一步、分支怎么选、哪些步骤可以并行、失败从哪里重试,都由图结构决定。
这篇文章先拆解 Graph Engineering 的核心概念,再给出一套可落地的实现思路、验证方法、接口封装方式和常见坑。全文以工程落地为主线,不绕理论。
1. 核心能力速览
在深入代码之前,先用一张表把 Graph Engineering 能解决的问题、需要什么基础、适合什么场景说清楚。
| 能力项 | 说明 |
|---|---|
| 核心思想 | 用有向图描述 Agent 的 SOP,节点是任务步骤,边是流转条件 |
| 解决的问题 | 流程分支、循环、并行、失败重试、中间状态恢复 |
| 主要功能 | 流程定义、节点编排、状态管理、条件路由、并行执行、人工审批节点 |
| 依赖基础 | Python 3.9+,图编排框架(LangGraph 等,按项目需要选择) |
| 硬件要求 | 纯 CPU 可跑,Graph 本身不依赖 GPU;如果节点内部接大模型推理,则按模型要求配置 GPU |
| 启动方式 | 源码运行 / API 服务 / 命令行批量跑 |
| 是否支持 API | 支持,Graph 可以封装成 HTTP 服务 |
| 是否支持批量任务 | 支持,多个任务实例共享同一张 Graph,各自维护独立状态 |
| 适合场景 | 客服工单处理、内容生成流水线、数据清洗流程、审批流、多智能体协作 |
从这张表里能看出一个重点:Graph Engineering 不是某个具体模型,而是一套流程编排范式。它不挑显卡,门槛主要在流程建模能力和状态管理设计。
2. 适用场景与使用边界
Graph Engineering 适合的是流程明确、步骤多、有分支有回退的任务。举几个典型例子:
- 客服工单:先做意图识别,再走对应处理流程,超时或处理失败要退回人工。
- 内容生产:选题 -> 生成大纲 -> 分节扩写 -> 配图 -> 排版 -> 审核,任何一步失败都要定位到具体节点。
- 数据清洗:多规则逐层过滤,不同数据走不同清洗分支。
- 多智能体协作:一个主 Agent 拆任务,多个子 Agent 分别执行,最后汇总。
它不适合的任务也有:单轮问答、无流程定义的自由对话、纯文本生成。这些场景用线性 prompt chain 就够了,引入 Graph 反而增加维护成本。
使用边界要注意三点:
第一,Graph 里的每个节点都可能调用外部模型或第三方服务,输入输出数据要提前定义好 schema,否则节点之间传参容易失控。
第二,如果节点涉及隐私数据或版权素材,比如客服对话记录、人脸图片、语音样本,必须确保处理链路合规,模型调用和数据存储都要在授权范围内。
第三,Graph 引擎只负责流程编排,不负责模型安全。节点内部是否对 prompt 做了注入防护、输出是否做了敏感信息过滤,需要业务层自己把关。
3. 环境准备与前置条件
Graph Engineering 的环境准备不复杂,核心是 Python 环境和图编排依赖。以当前比较常见的 LangGraph 为例,一套最小运行环境如下:
| 项目 | 建议配置 | 说明 |
|---|---|---|
| 操作系统 | Windows 10/11、Ubuntu 20.04+、macOS | 以本机已有环境为准 |
| Python | 3.9 及以上 | 低版本可能缺少新语法支持 |
| 虚拟环境工具 | conda 或 venv | 避免依赖冲突 |
| 核心依赖库 | langgraph、typing、pydantic | 版本按官方文档安装 |
| 模型访问方式 | OpenAI SDK / 本地模型 HTTP 服务 | Graph 节点内调用,方式不限 |
| 可选 Web 服务 | fastapi、uvicorn | 用于把 Graph 封装成 API |
先确认本机 Python 版本:
python --version然后创建虚拟环境并安装依赖。下面的命令是通用模板,真实项目可能还需要安装额外组件。
# 创建虚拟环境,建议使用 conda conda create -n graph-agent python=3.10 -y conda activate graph-agent # 安装图编排核心库,版本以官方最新为准 pip install langgraph # 安装接口服务相关依赖(可选) pip install fastapi uvicorn安装过程如果很慢,可以把 pip 源切换为国内镜像。不要混装多个版本,出现依赖冲突时优先重建虚拟环境而不是手动改包。
4. 安装部署与启动方式
安装完成后,先跑通一个最小 Graph,再往里面填业务节点。
4.1 最小 Graph 示例
下面这段代码定义了一张三节点图:开始节点 -> 判断节点 -> 两个结果节点。
from typing import TypedDict from langgraph.graph import StateGraph, END class AgentState(TypedDict): input_text: str intent: str result: str def node_receive(state: AgentState): # 节点1:接收输入,做意图识别,这里用简单规则代替真实模型 text = state["input_text"] if "退款" in text: state["intent"] = "refund" elif "咨询" in text: state["intent"] = "consult" else: state["intent"] = "unknown" return state def node_refund(state: AgentState): # 节点2:退款流程 state["result"] = "已受理退款申请" return state def node_consult(state: AgentState): # 节点3:咨询流程 state["result"] = "已转接人工客服" return state def router(state: AgentState): # 条件路由:根据 intent 决定走哪个节点 if state["intent"] == "refund": return "refund" elif state["intent"] == "consult": return "consult" else: return END # 建图 graph = StateGraph(AgentState) graph.add_node("receive", node_receive) graph.add_node("refund", node_refund) graph.add_node("consult", node_consult) # 设置入口和边 graph.set_entry_point("receive") graph.add_conditional_edges("receive", router, { "refund": "refund", "consult": "consult", END: END }) graph.add_edge("refund", END) graph.add_edge("consult", END) app = graph.compile()这段代码演示了 Graph 最核心的四个东西:节点函数、状态对象、条件路由、边连接。编译之后,真正执行如下:
result = app.invoke({ "input_text": "我要退款,怎么操作?" }) print(result["intent"]) # refund print(result["result"]) # 已受理退款申请invoke一次就是跑完一个完整 SOP。调用方不需要关心图内部怎么流转,只需要传入初始输入,拿回最终状态。
4.2 启动方式选择
最小 Graph 跑通后,有三种启动方式:
| 启动方式 | 适用场景 | 说明 |
|---|---|---|
| Python 脚本直接跑 | 本地调试、单次验证 | 最快,适合验证节点逻辑 |
| FastAPI 封装 | 对外提供 HTTP 接口 | 适合接入业务系统 |
| 批量任务脚本 | 离线处理大批量请求 | 适合数据清洗、批处理 |
建议先以 Python 脚本方式跑通逻辑,再封装 API,不要一开始就套 Web 框架。
5. 功能测试与效果验证
Graph 写完之后,单纯跑一次成功不算数。要把每个节点、每条边、每个异常路径都验一遍。
5.1 测试目标
| 测试维度 | 测试内容 | 判断标准 |
|---|---|---|
| 节点独立性 | 每个节点函数能否单独调用 | 输入正确状态,返回正确更新 |
| 路由正确性 | 不同输入是否走到预期分支 | 结果节点符合预期 |
| 状态传递 | 上游节点输出能正确传给下游 | 下游拿到的字段完整 |
| 异常处理 | 节点抛异常时行为正确 | 有捕获或回退机制 |
| 幂等性 | 同一输入多次执行结果一致 | 最终状态稳定 |
| 并发安全 | 多实例同时跑是否互相干扰 | 状态互相独立 |
这里重点测路由。把不同意图的文本各跑一遍,确认每次走的路径都是预期的那条,返回的结果字段完整。路由错误是 Graph 最常见的 Bug,问题往往藏在条件判断里。
5.2 批量验证脚本
推荐写一个简单的批量验证脚本,把所有测试用例放到一张表里统一跑:
test_cases = [ {"input_text": "我要退款", "expect_intent": "refund"}, {"input_text": "你们几点上班", "expect_intent": "consult"}, {"input_text": "你好", "expect_intent": "unknown"}, ] for case in test_cases: result = app.invoke({"input_text": case["input_text"]}) ok = "PASS" if result["intent"] == case["expect_intent"] else "FAIL" print(f"{case['input_text']} -> {result['intent']} [{ok}]")测出来的输出应该能直接看出哪条分支没走对。出现 FAIL 时,优先检查路由函数里的判断条件,其次检查节点是否真正更新了 state 字段。
5.3 失败注入测试
正常路径测完后,故意让某个节点抛异常,验证失败处理是否有效:
def node_refund_broken(state: AgentState): raise RuntimeError("模拟退款节点故障")如果 Graph 没有设置异常处理,整个任务会直接终止。生产环境里建议在每个节点外面包一层 try-except,并把错误信息写回 state,这样失败时也能拿到部分结果。
6. 接口 API 与批量任务
Graph 本身只是流程引擎,接入业务系统通常要做一层 HTTP API。
6.1 FastAPI 封装示例
from fastapi import FastAPI from pydantic import BaseModel app_api = FastAPI() class TaskRequest(BaseModel): input_text: str class TaskResponse(BaseModel): intent: str result: str @app_api.post("/api/agent/run") def run_task(req: TaskRequest): result = app.invoke({"input_text": req.input_text}) return TaskResponse( intent=result["intent"], result=result["result"] ) if __name__ == "__main__": import uvicorn uvicorn.run(app_api, host="0.0.0.0", port=8000)启动接口服务后,用 curl 做一次最基础的验证:
curl -X POST http://127.0.0.1:8000/api/agent/run \ -H "Content-Type: application/json" \ -d '{"input_text": "我要退款"}'返回结果大致是:
{ "intent": "refund", "result": "已受理退款申请" }6.2 批量任务队列设计
接口跑通后,批量任务不要直接在 HTTP 请求里同步跑完,建议设计一层任务队列。输入数据放到队列,后端 Worker 逐个调用 Graph。
| 设计点 | 建议 |
|---|---|
| 任务输入 | 统一存为 JSON,字段和 Graph state 保持一致 |
| 执行日志 | 每个任务记录启动时间、结束时间、经过节点列表 |
| 失败重试 | 重试次数建议不超过 3 次,重试间隔递增 |
| 输出保存 | 按任务 ID 分目录存储,避免覆盖 |
| 并发控制 | 同时运行的任务数根据模型接口限流调整 |
Python 里可以直接用concurrent.futures.ThreadPoolExecutor做简单并发池,也可以用 Celery 做完整队列。关键点在于每个任务必须使用独立的 state 实例,不能共享同一个 dict。Graph 框架通常会处理这一点,但在自定义代码里要格外注意。
7. 资源占用与性能观察
Graph 编排层本身吃的是 CPU 和内存,不吃 GPU。性能瓶颈通常在节点内部的模型调用上。
观察性能时,重点看三个指标:
第一,图调度耗时。这部分是 Graph 框架自身的开销,通常只有几毫秒到几十毫秒,取决于节点数量和路由复杂度。如果单次调度耗时超过 100ms,就要检查是不是在路由函数里做了重计算或其他耗时操作。
第二,节点内模型调用耗时。这是大头。一个节点调用大模型可能要几秒甚至几十秒,图里的并行设计能显著缩短总耗时。比如三个互不依赖的节点并行执行,和串行执行相比,总耗时能降很多。
第三,内存占用。Graph 会把每个节点的 state 保存在内存里。如果 state 里存了大文本或长文档,任务量大时内存会快速上涨。批量任务建议控制 state 体积,中间大文本写文件,不要全部塞进内存。
通过 Python 自带工具可以快速看内存消耗:
pip install psutilimport psutil def print_memory(): info = psutil.Process().memory_info() print(f"RSS: {info.rss / 1024 / 1024:.1f} MB")在任务前、任务中、任务后各调用一次,对比内存增长情况。如果内存持续不释放,优先检查是否有全局缓存被反复写入。
并发提升带来的收益通常不是线性的。Graph 内部节点并行时,如果所有节点同时打同一个模型接口,很容易触发限流。实际情况是:串行跑 10 条任务可能用时 100 秒,并发 5 条可能能压到 30 秒,但并发 20 条可能因为限流退回 80 秒。并发数需要根据模型服务能力反复调。
8. 常见问题与排查方法
Graph Engineering 的报错和传统单线程代码不太一样,错误往往跨节点,所以排查思路也要跟着变。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 任务执行到一半终止 | 节点抛异常未被捕获 | 查看异常栈定位节点 | 在节点外部包异常处理 |
| 走错路由分支 | 条件判断逻辑不对 | 打印路由函数的输入 | 增加路由函数单元测试 |
| 下游节点拿不到字段 | state 字段未初始化或命名不一致 | 检查节点返回值 | 统一用 TypedDict 定义字段 |
| 状态被多个任务串数据 | 共享了全局 dict 或缓存 | 检查是否使用实例级变量 | 每个任务使用独立 state |
| 接口并发调用超时 | 模型接口限流或服务过载 | 查看模型服务日志 | 降低并发数或加缓存 |
| 跑一次后服务内存上涨 | state 里存了大文本没释放 | 打印内存占用 | 大文本写文件,减少 state 体积 |
| Graph 编译时报边重复 | 相同边被添加两次 | 检查 add_edge 调用 | 使用覆盖或删除重复定义 |
| 调试时看不到中间状态 | 只打印最终返回结果 | 使用中间状态回调或逐步 invoke | 增加调试打印函数 |
这里有一个特别值得注意的坑:条件路由的 END 处理。在 LangGraph 这类框架里,如果路由函数返回的 key 没有在映射表里注册,运行时大概率直接报错。解决办法是把所有可能的分支都显式写在路由映射里,即使走 END 也要写出来。
还有一个实践技巧:给每个节点加统一的日志装饰器,让所有节点在进入和退出时都打印 state 摘要。这样跑一条任务就能看到从入口到结束的完整路径,定位问题快得多。
9. 最佳实践与使用建议
Graph 跑通并不难,难的是长期维护。从工程角度给几个建议。
第一个建议:先画图,再写代码。不要边写边想流程结构。在纸上或文档里把 SOP 的每一个步骤、每一个判断条件、每一个异常出口列出来,确认没有环和死路,再开始定义节点。
第二个建议:状态对象要设计成强类型。TypedDict或 Pydantic 模型都可以,字段要在图定义前定清楚。下游节点需要什么字段,上游节点必须保证输出里有。弱类型 dict 一旦项目变大,字段拼写错误很难查。
第三个建议:节点函数要小,判断条件要集中。一个节点只做一件事,路由判断集中到一个函数里。这样改流程时不用到处翻代码。
第四个建议:中间结果持久化。长任务跑了一半挂了,如果中间结果都在 state 里,重启后会丢失。建议把关键中间结果写数据库或文件,Graph 可以从最近一个成功节点恢复。
第五个建议:涉及人脸图片、语音样本、版权素材、用户对话记录时,必须确认处理和调用已经获得必要授权。批量任务尽量使用脱敏数据做测试。
第六个建议:接口服务要限定访问范围。Graph 封装成 API 后,不要默认绑定0.0.0.0对外网开放。尤其在一个节点能调用大模型的情况下,无鉴权的开放接口会被当成免费算力滥用。生产环境至少加个简单 token 校验。
第七个建议:版本控制 Graph 定义。Graph 的节点函数和边结构是业务逻辑的一部分,建议像普通代码一样走 git review,不要直接在生产环境改。
10. 总结与下一步
Graph Engineering 的价值在于把 Agent 的流程从“prompt 链”升级成“可执行的有向图”。对于一个有分支、有回退、有并行的 SOP,图结构比线性链式调用更好维护、更好追踪、也更好扩展。每加一个节点,就是给 Agent 增加一段确定的执行能力;每加一条边,就是给流程增加一个明确的流转规则。
最开始验证的时候,不要急着设计一个大而全的图,从最简单的三节点跑通,确认路由和状态传递没问题,再逐步增加节点、并行分支、失败重试。最容易踩的坑也集中在这些地方:路由函数返回了未注册的分支、state 字段跨节点传递不一致、并发任务共享了同一份可变状态。
下一步可以往两个方向延伸:一个是把 Graph 节点里的规则判断逐步替换成真正的模型调用,让节点具备语义理解能力;另一个是给 Graph 增加人工审批节点,让关键决策保留人在回路里。等这两块都稳定了,再把图定义、节点代码、接口服务放到统一工程目录里,按版本管理,就是一个能持续演进的 Agent 平台底座。