目录
-
- [一、Graph Engineering到底在解决什么问题?](#一、Graph Engineering到底在解决什么问题?)
- 二、先搞清楚:Graph不是知识图谱,也不是LangGraph
- 三、核心一:State------先定义状态,再定义节点
- 四、核心二:Node------节点是一项可验收的工作
- 五、核心三:Edge------路由不只是"下一步去哪"
- 六、核心四:Reducer------并行结果怎么合并
- 七、核心五:Checkpoint------保存什么,什么时候保存
- 八、核心六:Command与Interrupt------控制流也是业务数据
- 九、完整案例:把支付指标变更流程串一遍
- 十、Graph该怎么评测?
- [十一、什么时候别做Graph Engineering?](#十一、什么时候别做Graph Engineering?)
- 最后说两句
大家好,我是小马过河R。上次咱们把Graph Engineering的理论聊了个底朝天,但不少朋友后台私信说:"概念我懂了,代码咋写?"今天咱们就接着上回的话题,把Graph Engineering的六个核心对象,用LangGraph的代码挨个撸一遍。
事情还得从上周说起。我团队在做一个涉及多个Agent协作的自动化变更系统,结果跑了一周,各种幺蛾子层出不穷:A改完的代码被B覆盖了,测试挂了整个流程从头跑,人工审批等了一晚上醒来发现进程早就超时了......当时我就在想,单Agent能力确实越来越强了,但多个Agent凑在一起怎么就变成了"三个和尚没水喝"?
后来研究了一圈LangGraph的官方文档和LangChain团队的一些分享,才发现这个问题早就有了名字------Graph Engineering。今天咱们就理论结合实践,掰开揉碎了聊。

一、Graph Engineering到底在解决什么问题?
先别急着看概念,咱们先感受一下痛点。过去两年,大模型的能力突飞猛进:能读代码、能查资料、能调用工具、失败了还能自己重试。听起来很完美对吧?
但一旦任务从"十分钟"变成"十小时",从"一次调用"变成"几十个步骤",问题就来了:
- 规划做得挺好,执行到一半忘了最初的验收标准长啥样;
- 三个Agent并行改代码,结果同时修改同一个文件,互相覆盖;
- 测试失败了,整个流程从头再来,明明前面已经做对的步骤也得重跑;
- 人工审批等了一晚上,第二天发现没法从断点继续;
- 工具调用成功了但进程超时了,重试一下,好家伙,生产记录创建了两条;
- 最终结果失败了,翻半天日志也不知道是哪个节点、哪条路径出了问题。
这些问题,早就不属于Prompt Engineering的范畴了。模型知道"下一步大概该干啥",不代表系统知道"当前走到哪了""哪些步骤可以并行""失败后该重试还是回退"。
Graph Engineering要解决的,就是把这堆乱麻梳理成一张可执行、可暂停、可恢复、可观察的状态图。
二、先搞清楚:Graph不是知识图谱,也不是LangGraph
很多朋友一听到"Graph",第一反应是知识图谱。千万别搞混了。
这里的Graph指的是工作流图或控制流图。它的核心元素就几个:
- Node(节点):完成一个明确任务的单元;
- Edge(边):决定状态往哪流;
- State(状态):贯穿整个任务的结构化数据;
- Checkpoint(检查点):可以恢复执行的状态快照;
- Subgraph(子图):有独立职责和边界的子流程。
打个比方:知识图谱像是在画"人物关系图"------谁认识谁、谁依赖谁;而Graph Engineering像在画"任务流程图"------先干啥、后干啥、出错了咋办。两者可以组合使用,但完全是两码事。
另外,Graph Engineering是设计方法,LangGraph是实现工具,别把两者划等号。你不用LangGraph照样可以做Graph Engineering,自己写个调度器也行。
三、核心一:State------先定义状态,再定义节点
很多人的习惯是先画流程图,最后才想数据怎么流动。结果就是每个节点都输出一大段自然语言,下游节点还得"猜"上一轮说了啥,任务越长语义漂移越严重。
更好的顺序是先定义State 。在LangGraph中,State通常用TypedDict来定义:
python
from typing_extensions import TypedDict
from langgraph.graph import add_messages
from typing import Annotated, List
class GraphState(TypedDict):
"""Graph的共享状态"""
# 消息列表------用add_messages做归约器,自动追加而非覆盖
messages: Annotated[List[AnyMessage], add_messages]
# 当前任务阶段
stage: str
# 代码变更记录
changes: List[dict]
# 测试结果
test_results: dict
# 审批状态
approval_status: str
一个好的State有三个特点:
- 结构明确:关键字段是可校验的数据,不是一大段聊天记录;
- 责任明确:每个节点只能修改自己负责的字段;
- 证据外置:大文件、日志只存引用,别往共享状态里硬塞。
四、核心二:Node------节点是一项可验收的工作
一个节点可以是一次数据库查询、一个完整Agent、一组测试脚本、甚至是一次人工审批。在LangGraph里,节点就是一个普通的Python函数,接收当前状态,返回状态更新:
python
from langgraph.graph import StateGraph, START, END
# 定义一个代码分析节点
def analyze_code(state: GraphState) -> dict:
"""分析代码变更的影响范围"""
changes = state.get("changes", [])
# 执行影响分析逻辑
affected_modules = analyze_impact(changes)
return {
"stage": "analysis_done",
"affected_modules": affected_modules
}
# 定义一个测试执行节点
def run_tests(state: GraphState) -> dict:
"""执行测试并返回结果"""
changes = state.get("changes", [])
test_result = execute_tests(changes)
return {
"test_results": test_result,
"stage": "tests_done" if test_result["passed"] else "tests_failed"
}
节点的边界不该按"模型一次能干多少"来划分,而该按"能否独立验收和恢复"来划分。如果一个节点同时负责需求分析、写代码、测试、发布、总结,那它就是一个披着节点外衣的大Loop,出了问题根本不知道从哪下手。
下面我们把这些节点组装成图:
python
# 初始化图,传入状态定义
workflow = StateGraph(GraphState)
# 添加节点
workflow.add_node("analyze", analyze_code)
workflow.add_node("test", run_tests)
# 定义入口和流程
workflow.add_edge(START, "analyze")
workflow.add_edge("analyze", "test")
workflow.add_edge("test", END)
# 编译图
app = workflow.compile()
五、核心三:Edge------路由不只是"下一步去哪"
Edge的类型包括固定边、条件边、语义路由、动态分发、循环边等。
设计原则很简单:能用确定性条件判断的,就别交给模型。比如"测试退出码是不是0"这种,写个if就完了,没必要让LLM来决策:
python
def should_continue(state: GraphState) -> str:
"""条件路由:根据测试结果决定下一步"""
test_results = state.get("test_results", {})
if test_results.get("passed", False):
return "approve" # 测试通过,进入审批
elif test_results.get("failed_count", 0) < 3:
return "fix" # 失败次数不多,尝试修复
else:
return END # 失败太多,终止流程
# 添加条件边
workflow.add_conditional_edges(
"test", # 源节点
should_continue, # 路由函数
{
"approve": "approval",
"fix": "fix_code",
END: END
}
)
只有确实需要理解语义的------比如"这个需求属于支付领域还是订单领域"------才考虑用模型路由。
六、核心四:Reducer------并行结果怎么合并
并行不是画三条分支就结束了。多个节点同时改同一个状态,必须定义合并规则。
在LangGraph中,Reducer就是解决这个问题的。比如多个Agent并行发现受影响文件,汇合时可以做集合并集:
python
from typing import Annotated, List
from langgraph.graph import add_messages
class GraphState(TypedDict):
# messages用add_messages归约------新消息追加到列表末尾
messages: Annotated[List[AnyMessage], add_messages]
# 受影响文件用集合归约------自动去重合并
affected_files: Annotated[Set[str], lambda a, b: a | b]
# 普通字段直接覆盖
stage: str
add_messages是LangGraph提供的归约器,能智能地合并消息列表:追加新消息、按ID更新已有消息、支持删除操作。没有显式的合并策略,并行就是在更快地制造不一致。
七、核心五:Checkpoint------保存什么,什么时候保存
长任务不能依赖一个"活着的进程"。进程退出、机器重启、人工等待,都不该让整个任务失忆。
LangGraph的Checkpoint机制在每一步执行后自动保存状态快照。先看内存版(适合开发测试):
python
from langgraph.checkpoint.memory import InMemorySaver
# 创建内存检查点
checkpointer = InMemorySaver()
# 编译时传入
app = workflow.compile(checkpointer=checkpointer)
# 执行时指定thread_id,实现多会话隔离
result = app.invoke(
{"messages": [HumanMessage("开始代码变更")]},
config={"configurable": {"thread_id": "session-001"}}
)
生产环境得用持久化存储,比如PostgreSQL:
python
from langgraph.checkpoint.postgres import PostgresSaver
import psycopg
# PostgreSQL连接
DB_URI = "postgresql://user:password@localhost:5432/langgraph_db"
conn = psycopg.connect(DB_URI, autocommit=True)
checkpointer = PostgresSaver(conn)
checkpointer.setup() # 自动建表
# 业务代码一行不改,只换checkpointer
app = workflow.compile(checkpointer=checkpointer)
# 中断后从checkpoint恢复
result = app.invoke(
{"messages": [HumanMessage("继续之前的任务")]},
config={"configurable": {"thread_id": "session-001"}}
)
从InMemorySaver换成PostgresSaver,只需要改一行代码------这就是接口设计的力量。
八、核心六:Command与Interrupt------控制流也是业务数据
需要人工确认的时候,千万别用"暂停线程干等着"这种粗暴方式。
正确做法是:保存当前状态,向外部返回待审批信息,等结果到了再从Checkpoint恢复。LangGraph的interrupt机制就是干这个的:
python
from langgraph.types import interrupt
def approval_node(state: GraphState) -> dict:
"""人工审批节点------等待外部输入"""
# 挂起执行,等待人工反馈
feedback = interrupt({
"type": "approval",
"message": f"请审批以下变更: {state.get('changes')}",
"options": ["approve", "reject", "request_changes"]
})
return {
"approval_status": feedback,
"stage": "approved" if feedback == "approve" else "rejected"
}
人工审批不是Graph之外的异常,而是一个有输入、有输出、有审计记录的普通节点。
九、完整案例:把支付指标变更流程串一遍
上回咱们聊了一个企业级例子:支付成功率指标从"成功请求/全部请求"调整为"成功请求/有效请求"。这次我们把完整代码贴出来:
python
from typing_extensions import TypedDict, Annotated
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.postgres import PostgresSaver
from typing import List, Set
# 1. 定义State
class MetricChangeState(TypedDict):
messages: Annotated[List[AnyMessage], add_messages]
impacted_services: Set[str] # 受影响服务
code_changes: List[dict] # 代码变更
dashboard_updates: List[dict] # 看板变更
test_cases: List[dict] # 测试用例
doc_updates: List[dict] # 文档更新
conflict_report: dict # 冲突检测报告
approval_status: str # 审批状态
# 2. 定义节点
def planning_node(state: MetricChangeState) -> dict:
"""规划节点:分析影响范围"""
return {"impacted_services": {"payment_api", "monitoring", "docs"}}
def backend_node(state: MetricChangeState) -> dict:
"""后端计算节点"""
return {"code_changes": [{"file": "metrics.py", "change": "update_formula"}]}
def monitoring_node(state: MetricChangeState) -> dict:
"""监控看板节点"""
return {"dashboard_updates": [{"dashboard": "payments", "metric": "success_rate"}]}
def testing_node(state: MetricChangeState) -> dict:
"""测试生成节点"""
return {"test_cases": [{"name": "test_new_metric", "status": "generated"}]}
def docs_node(state: MetricChangeState) -> dict:
"""文档更新节点"""
return {"doc_updates": [{"file": "metrics.md", "change": "update_definition"}]}
def integration_node(state: MetricChangeState) -> dict:
"""集成检查节点:检测四个分支是否冲突"""
conflicts = detect_conflicts(
state["code_changes"],
state["dashboard_updates"],
state["test_cases"],
state["doc_updates"]
)
return {"conflict_report": conflicts}
# 3. 构建图
workflow = StateGraph(MetricChangeState)
# 添加节点
workflow.add_node("plan", planning_node)
workflow.add_node("backend", backend_node)
workflow.add_node("monitoring", monitoring_node)
workflow.add_node("testing", testing_node)
workflow.add_node("docs", docs_node)
workflow.add_node("integrate", integration_node)
# 定义流程
workflow.add_edge(START, "plan")
# 规划完成后,四个分支并行执行
workflow.add_edge("plan", "backend")
workflow.add_edge("plan", "monitoring")
workflow.add_edge("plan", "testing")
workflow.add_edge("plan", "docs")
# 四个分支汇合到集成节点
workflow.add_edge("backend", "integrate")
workflow.add_edge("monitoring", "integrate")
workflow.add_edge("testing", "integrate")
workflow.add_edge("docs", "integrate")
workflow.add_edge("integrate", END)
# 4. 持久化与执行
checkpointer = PostgresSaver(psycopg.connect(DB_URI))
app = workflow.compile(checkpointer=checkpointer)
result = app.invoke(
{"messages": [HumanMessage("调整支付成功率指标定义")]},
config={"configurable": {"thread_id": "metric-change-001"}}
)
这个案例的精髓在于:失败以后不是整个重来,而是精准定位到哪个分支出了问题,定向修复。
十、Graph该怎么评测?
只看最终任务成功率远远不够。失败的时候,得知道失败在哪一层。
需要关注的指标包括:
- 节点层:成功率、延迟、Token成本、重试率;
- 路由层:路由准确率、不安全路由率;
- 状态层:Schema违规率、合并冲突率;
- 恢复层:Checkpoint恢复成功率、重复副作用率;
- 整图层:端到端成功率、人工介入率、高风险失败率。
还要用故障注入主动制造问题------模型超时、Checkpoint写入失败、节点无响应。不搞故障注入,你验证的只是"正常路径能跑",根本没验证Graph最重要的恢复能力。
十一、什么时候别做Graph Engineering?
Graph虽好,但不是银弹。下面这些情况别瞎折腾:
- 改一个明确的小函数;
- 一次简单的知识查询;
- 只有两三个固定步骤的脚本;
- 没有并行、分支、等待、恢复需求;
- 失败了直接重跑成本很低。
引入Graph会增加State Schema维护成本、节点和版本数量、Checkpoint存储、路由与并发测试、运维复杂度。
最后说两句
Graph Engineering最容易被误解成"把Agent流程画得更复杂"。其实它做的恰恰相反------把原来隐藏在长Prompt、聊天历史、Agent自主判断里的流程,变成人和程序都能理解的状态、路径和契约。
模型负责需要理解和判断的部分,代码负责确定性规则,验证系统负责证明结果,人负责目标、标准和高风险决策。
这次咱们从理论到代码,把State、Node、Edge、Reducer、Checkpoint、Command六个核心概念全部过了一遍。代码有了,概念清晰了,剩下的就是动手实践了。
Prompt不会消失,Context不会消失,Harness不会消失,Loop不会消失。它们只是被放进了更大的工程结构里:
Prompt决定怎么表达,Context决定模型看到什么,Harness决定怎么运行,Loop决定怎么修正,Graph决定整个系统怎么协作。
好了,今天的分享就到这里。代码已经贴上了,评论区等你们来交流踩坑经验!