LangGraph 06:Prompt Chaining 分步生成与质检循环

让大语言模型一次性写出一篇结构完整、事实准确、语言流畅的万字技术文章或深度报告,现实中往往容易翻车:模型写到中途容易遗漏关键论点、上下文注意力分散,或者各段落风格割裂。一旦生成质量不合格,唯一的补救手段就是全盘重跑,不仅成本翻倍,而且很难定位到底是哪一段出了偏差。

工程上更靠谱的做法是 Prompt Chaining(提示词链式分步生成):把单一的大提示词拆解为相互衔接的多个工序,每个工序用专门的 Prompt 聚焦单一目标,上游输出作为下游输入。

Prompt Chaining 本身是一种通用的工作流设计模式,即便写原生 Python 循环也能串起来。将它放进 LangGraph 的 StateGraph 中,价值在于:

  1. 统一的状态共享池:每一步的中间产物显式记录在 state 中,各步骤间输入输出透明。
  2. 中间质量门禁与自修正:可以插入条件边校验中间产物,不达标就定向回退重试,无需从头再来。
  3. 细粒度观测与流式调试:利用 app.stream() 可以实时看到哪个步骤跑完、输出了什么。

一次性生成与分步生成的取舍

在决定是否使用 Prompt Chaining 之前,先对比两种模式的工程特征:

评估维度 单次全量生成(One-shot Generation) Prompt Chaining(分步流水线)
调用频次与延迟 单次调用,首字返回快,总耗时较短 多次串行调用,端到端延迟较长
Token 消耗 相对较低(Prompt 复用度低) 相对较高(后续步骤需携带前置产物)
注意力分配 模型需要兼顾结构、细节、行文风格,容易顾此失彼 每个节点只专注单一职责(如仅规划章节、仅扩写段落)
可观测性与调试 黑盒输出,无法洞察中间推演过程 步骤边界清晰,每一步产物都能断点检查
容错与干预 失败只能整体重新生成 某一步质量不合格可就地修正或引入人工干预

当任务具备明确的阶段性阶段(如:需求分析 $\rightarrow$ 架构设计 $\rightarrow$ 代码实现,或 大纲规划 $\rightarrow$ 正文初稿 $\rightarrow$ 术语审校),拆分节点是控制质量的有效方式。


博客文章三步生成工作流

以撰写技术文章为例,流水线拆分为三个有序步骤:

  1. 生成大纲(outline):输入主题,提炼核心章节架构与论述要点。
  2. 正文展开(expand):依据大纲与主题,分章节填充具体论据与代码示例,形成初稿。
  3. 润色优化(polish):针对初稿进行术语对齐、语句润色与排版整理,输出最终定稿。

其基础拓扑结构如下:


基础实现:基于 StateGraph 的分步流

在这一小节中,节点内部暂时使用格式化模板模拟 LLM 调用(下一节会完整换成真实模型调用),重点展现状态如何在节点间流动。

1. 定义状态结构

每个节点负责更新自己的专属字段:

1
2
3
4
5
6
7
8
9
from typing import TypedDict
from langgraph.graph import StateGraph, END


class BlogState(TypedDict):
topic: str # 初始输入主题
outline: str # 第一步产出:大纲
draft: str # 第二步产出:初稿
final_article: str # 第三步产出:最终定稿

2. 编写聚焦单一职责的节点函数

每个节点只从 state 中读取需要的前置字段,并返回属于该节点的增量字典:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def generate_outline_node(state: BlogState) -> dict:
"""第一步:规划大纲"""
topic = state["topic"]

# 实际场景中这里调用 llm.invoke(...)
prompt = f"请为主题【{topic}】规划 3 个核心技术小节大纲。"

outline = (
f"## 1. 为什么在复杂场景下需要状态机\n"
f"## 2. LangGraph 的核心抽象与拓扑流\n"
f"## 3. 生产环境的错误隔离与重试实践"
)
return {"outline": outline}


def expand_content_node(state: BlogState) -> dict:
"""第二步:根据大纲扩写正文初稿"""
topic = state["topic"]
outline = state["outline"]

draft_content = f"# 深入理解 {topic}\n\n"
sections = outline.strip().split("\n")
for sec in sections:
draft_content += f"{sec}\n这里是针对该小节的详细架构解析、代码片段与工程论证...\n\n"

return {"draft": draft_content.strip()}


def polish_article_node(state: BlogState) -> dict:
"""第三步:润色正文与格式整理"""
draft = state["draft"]

# 模拟润色处理:统一标点、排版代码块并添加尾注
final_article = draft + "\n\n---\n*注:本文由自动化工作流生成并完成规范性校验。*"
return {"final_article": final_article}

3. 构建并编译图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
workflow = StateGraph(BlogState)

# 注册三个节点
workflow.add_node("outline", generate_outline_node)
workflow.add_node("expand", expand_content_node)
workflow.add_node("polish", polish_article_node)

# 设置线性边
workflow.set_entry_point("outline")
workflow.add_edge("outline", "expand")
workflow.add_edge("expand", "polish")
workflow.add_edge("polish", END)

app = workflow.compile()

运行与中间状态流式观察

使用 app.invoke() 可以直接获取最终状态:

1
2
3
4
5
6
7
8
9
10
initial_input = {
"topic": "LangGraph 状态机编排",
"outline": "",
"draft": "",
"final_article": "",
}

result = app.invoke(initial_input)
print("=== 最终产物 ===")
print(result["final_article"])

但在长链路生成任务中,我们更关注每一步具体产出了什么。调用 app.stream() 可以按节点粒度接收流式更新包:

1
2
3
4
5
6
for step in app.stream(initial_input):
for node_name, state_update in step.items():
print(f"\n>>>> 节点 [{node_name}] 执行完毕,产生状态变更:")
for k, v in state_update.items():
preview = v.replace("\n", " ")[:60]
print(f" - {k}: {preview}...")

控制台输出形如:

1
2
3
4
5
6
7
8
>>>> 节点 [outline] 执行完毕,产生状态变更:
- outline: ## 1. 为什么在复杂场景下需要状态机 ## 2. LangGraph 的核心抽象与拓扑流...

>>>> 节点 [expand] 执行完毕,产生状态变更:
- draft: # 深入理解 LangGraph 状态机编排 ## 1. 为什么在复杂场景下需要状态机...

>>>> 节点 [polish] 执行完毕,产生状态变更:
- final_article: # 深入理解 LangGraph 状态机编排 ## 1. 为什么在复杂场景下需要状态机...

这种逐步吐出增量的能力,正是前端向用户展示“正在构思大纲…”、“正在撰写正文…”等进度指示器的底层依托。


进阶:质量门禁与自修正重试循环

如果大模型在第一步生成大纲时偷懒,只输出了 1 个章节,或者输出格式完全不对,直接交给下一步扩写只会扩大错误。

在 LangGraph 中,我们可以在 outline 节点之后插入一个条件路由质检器:

  • 如果大纲质量过关,流向 expand;
  • 如果不合格,且重试次数未用尽,回退到 outline 重新生成;
  • 增加重试计数器防护,防止无限自旋。

代码实现:带重试计数的条件边

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
from typing import TypedDict, Literal
from langgraph.graph import StateGraph, END


class RobustBlogState(TypedDict):
topic: str
outline: str
draft: str
final_article: str
outline_retries: int # 重试计数器


def generate_outline(state: RobustBlogState) -> dict:
retries = state.get("outline_retries", 0)
topic = state["topic"]

# 模拟第一次生成可能不合格,重试后合格
if retries == 0:
bad_outline = "## 1. 简单介绍"
return {"outline": bad_outline, "outline_retries": retries + 1}
else:
good_outline = (
"## 1. 背景与核心问题\n"
"## 2. 核心架构设计\n"
"## 3. 生产部署与调优"
)
return {"outline": good_outline, "outline_retries": retries + 1}


def validate_outline_gate(state: RobustBlogState) -> Literal["expand", "retry", "abort"]:
"""路由器:判断大纲章节数与重试上限"""
outline = state.get("outline", "")
section_count = outline.count("##")
retries = state.get("outline_retries", 0)

if section_count >= 3:
return "expand"

if retries <= 2:
print(f"[警告] 大纲小节数不足(仅 {section_count} 个),触发第 {retries} 次重试...")
return "retry"

print("[错误] 大纲重试超限,任务终止")
return "abort"


# 组装图
workflow = StateGraph(RobustBlogState)
workflow.add_node("outline", generate_outline)
workflow.add_node("expand", expand_content_node)
workflow.add_node("polish", polish_article_node)

workflow.set_entry_point("outline")

# 添加条件边实现自旋纠错
workflow.add_conditional_edges(
"outline",
validate_outline_gate,
{
"expand": "expand",
"retry": "outline", # 指向自身,形成重试环路
"abort": END,
}
)

workflow.add_edge("expand", "polish")
workflow.add_edge("polish", END)

app = workflow.compile()

运行该图时,系统在第一次产出不合规大纲后自动拉起二次生成,质检合格后再推入扩写节点,整个恢复流程对外部调用者完全封装透明。


模型解耦:节点与执行引擎分离

在 Prompt Chaining 中,图的拓扑骨架只负责业务流程定义(输入、验证、状态路由),具体节点内部究竟使用的是 OpenAI GPT-4o、Claude 还是本地部署的 Qwen / Mistral,对图的结构没有任何影响。

在节点内部封装统一的 LLM 客户端接口:

1
2
3
4
5
6
7
8
# 伪代码:在节点内部平滑切换模型
def generate_outline_with_llm(state: BlogState) -> dict:
topic = state["topic"]
prompt = f"请为【{topic}】构思一篇技术架构深度博客的大纲,包含3-4个核心章节。"

# 这里可以使用任意兼容 LangChain 接口的 LLM
# response = model.invoke([HumanMessage(content=prompt)])
# return {"outline": response.content}

下一篇我们将深入讨论如何在节点中规范地接入商业云端大模型与开源本地模型,并做好连接池管理与异常熔断。


核心设计要点

  1. 单节点单一职责:不要在一个节点里让模型既写大纲又扩写前言。步骤越细,单步 Prompt 的指令遵循度就越高。
  2. 显式状态沉淀:每一步的生成成果都应当在 TypedDict 中开辟明确字段保存。避免将多步产物混在一个文本字段里反复字符串替换。
  3. 质检门禁配合重试上限:拆分步骤的最大红利是获得了“中间拦截修正”的能力。在关键分水岭节点后配合 add_conditional_edges 设立校验网,并务必绑定 retry_count 防止死循环。
  4. 利用 stream 提升人机交互体验:长生成任务天然耗时较长,通过 app.stream() 捕获节点粒度状态,为前端提供阶段性进度反馈。

系列导航与参考