整体业务流程

这里有三处设计需要先看懂。
第一,关键词抽取之后,字段信息、指标信息、字段取值可以并行召回。它们都依赖用户问题和关键词,但彼此之间没有强制先后顺序。
第二,召回结果不能直接交给大模型生成 SQL。召回阶段通常宁可多召回一些,避免漏掉关键信息;但生成 SQL 阶段需要尽量干净、准确的上下文。因此中间还需要合并和过滤。
第三,生成 SQL 后不能直接执行。大模型生成的 SQL 可能字段名写错、表名写错、join 条件不完整,或者不符合当前数据库方言。所以后面还要校验 SQL;校验失败时,再进入校正流程。
State: 共享状态,不是随手塞变量的字典
Node: 节点只负责一件事,并返回局部状态
在本项目里,节点函数一般长这样:
async def extract_keywords(
state: DataAgentState,
runtime: Runtime[DataAgentContext],
):
...
这里最重要的是两个参数:
state:读取和写回业务中间状态;runtime:读取运行时上下文,或者通过stream_writer输出进度。
| 参数 | 含义 | 本项目当前是否重点使用 |
|---|---|---|
state |
当前图状态,节点读取它并返回局部更新 | 重点使用 |
runtime |
运行时上下文,包含 context、stream_writer 等能力 |
重点使用 |
config |
本次运行的配置信息,例如线程 ID、可配置参数、追踪信息 | 暂时不重点展开 |
读节点代码时,先抓住一条主线就够了:
节点读取 state
-> 使用 runtime.context 里的依赖做事
-> 必要时用 runtime.stream_writer 输出进度
-> 最后返回 Partial State
一个工程习惯:节点应该返回 Partial State,而不是每次返回整份 State
它不应该顺手把其他不属于自己职责的字段都重新返回一遍。这样做有两个好处:
- 节点职责更清楚,读代码时容易知道这个节点到底改了什么;
- 避免把不属于当前节点职责的状态误覆盖,尤其是后面有并行分支时更重要。
放到问数智能体里,一个节点最好只做一件事:
extract_keywords -> 只抽关键词
recall_column -> 只召回字段
merge_retrieved_info -> 只合并召回信息
validate_sql -> 只校验 SQL 并写入 error
run_sql -> 只执行 SQL 并输出结果
Edge: 边
edge 负责描述节点之间怎么流转。本项目目前主要用两类:普通边和条件边。
普通边:
graph_builder.add_edge("add_extra_context", "generate_sql")
graph_builder.add_edge("generate_sql", "validate_sql")
条件边:
graph_builder.add_conditional_edges(
source="validate_sql",
path=lambda state: "run_sql" if state["error"] is None else "correct_sql",
path_map={"run_sql": "run_sql", "correct_sql": "correct_sql"},
)
条件边也要注意一个细节:path 函数只负责根据当前 state 决定下一步走向,不适合顺手修改状态。如果某个节点既要更新状态,又要决定下一跳,LangGraph 还有更进阶的 Command 能力。不过本章先把“状态更新”和“流程分支”拆开讲:节点负责返回状态更新,条件边负责选择后续节点
Runtime context: 把依赖和状态分开
不要把运行时依赖硬塞进 State。后续节点会用到数据库连接、向量检索仓储、全文检索仓储、Embedding 客户端等对象。这些对象不是业务状态,而是运行时工具。它们不应该放进 DataAgentState,而应该放进 DataAgentContext,再通过 runtime.context 读取。本章还没有开始放具体依赖,所以 DataAgentContext 暂时是空的
Streaming流: 优先使用custom流
流式输出是本章另一个重点。invoke() 更像“等整张图跑完再拿最终结果”,而 stream() / astream() 更像“边执行边看图里发生了什么”。
LangGraph 的 stream_mode 有多种模式:
| 模式 | 适合看什么 |
|---|---|
values |
每个节点执行后的完整 State 快照 |
updates |
每个节点本次更新了哪些字段 |
messages |
大模型逐 token 输出 |
custom |
节点里通过 stream_writer 主动写出的自定义事件 |
debug |
调试信息 |
当前选择custom模式,原因很直接:前端最关心的是“当前执行到哪个步骤了”“最终查询结果是什么”“有没有错误”,这些都适合由节点主动用 runtime.stream_writer 写出
1. 定义State和Context
from typing import TypedDict
class DataAgentSate(TypedDict):
query: str
error: str
class DataAgentContext(TypedDict):
pass
2. 创建节点占位函数
有了state,下一步是先把节点函数都定义出来,
from langgraph.runtime import Runtime
from app.agent.context import DataAgentContext
from app.agent.state import DataAgentState
async def extract_keywords(state: DataAgentState, runtime: Runtime[DataAgentContext]):
writer = runtime.stream_writer
writer("抽取关键词")
import asyncio
await asyncio.sleep(0.5)
当前节点只做两件事:输出一条进度消息,然后 await asyncio.sleep(0.5)。这个 sleep 不是业务逻辑,只是为了测试流式输出时更容易观察节点顺序
当前项目中先创建了这些节点:
extract_keywords # 抽取关键词
recall_column # 召回字段信息
recall_metric # 召回指标信息
recall_value # 召回字段取值
merge_retrieved_info # 合并召回信息
filter_metric # 过滤指标信息
filter_table # 过滤表信息
add_extra_context # 添加额外上下文
generate_sql # 生成 SQL
validate_sql # 校验 SQL
correct_sql # 校正 SQL
run_sql # 执行 SQL
只要求它们“能被图调度、能输出进度
在graph.py中组装工作流
import asyncio
from langgraph.constants import END,START
from langgraph.graph import StateGraph
from app.agent.context import DataAgentContext
from app.agent.state import DataAgentState
# 然后创建图建构器
graph_build = StateGraph(
state_schema = DataAgentState,
context_schema = DataAgentContext
)
# 图运行时的共享状态按 DataAgentState 理解
# 运行时上下文按 DataAgentContext 理解。
# 注册节点:
graph_builder.add_node("extract_keywords", extract_keywords)
graph_builder.add_node("recall_column", recall_column)
graph_builder.add_node("recall_value", recall_value)
graph_builder.add_node("recall_metric", recall_metric)
graph_builder.add_node("merge_retrieved_info", merge_retrieved_info)
graph_builder.add_node("filter_metric", filter_metric)
graph_builder.add_node("filter_table", filter_table)
graph_builder.add_node("add_extra_context", add_extra_context)
graph_builder.add_node("generate_sql", generate_sql)
graph_builder.add_node("validate_sql", validate_sql)
graph_builder.add_node("correct_sql", correct_sql)
graph_builder.add_node("run_sql", run_sql)
# 添加普通边
graph_builder.add_edge(START, "extract_keywords")
graph_builder.add_edge("extract_keywords", "recall_column")
graph_builder.add_edge("extract_keywords", "recall_value")
graph_builder.add_edge("extract_keywords", "recall_metric")
graph_builder.add_edge("recall_column", "merge_retrieved_info")
graph_builder.add_edge("recall_value", "merge_retrieved_info")
graph_builder.add_edge("recall_metric", "merge_retrieved_info")
graph_builder.add_edge("merge_retrieved_info", "filter_table")
graph_builder.add_edge("merge_retrieved_info", "filter_metric")
graph_builder.add_edge("filter_table", "add_extra_context")
graph_builder.add_edge("filter_metric", "add_extra_context")
graph_builder.add_edge("add_extra_context", "generate_sql")
graph_builder.add_edge("generate_sql", "validate_sql")
# 添加条件边
graph_builder.add_conditional_edges(
source="validate_sq1",
path= lambda state: "run_sql" if state["error"] is None else "correct_sql",
path_map = {"run_sql": "run_sql", "correct_sql": "correct_sql"}
)
graph_builder.add_edge("correct_sql","run_sql")
graph_builder.add_edge("run_sql", END)
# 如果 state["error"] is None -> 进入 run_sql
# 如果 state["error"] is not None -> 进入 correct_sql
# 节点和边都加完后,可以先把图画出来检查。
print(graph.get_graph().draw_mermaid())
# 编译图
graph = graph_builder.compile()
使用流式输出观察执行进度
如果前端一直没有反馈,用户不知道系统是在正常执行、卡在某个节点,还是已经报错.所以本项目需要在工作流执行过程中实时输出进度。
1. 在节点里写出进度
writer = runtime.stream_writer
writer("抽取关键词")
只要工作流执行到这个节点,外部通过流式调用就能拿到这条消息,不需要等整张图执行结束
还有 get_stream_writer()。它是另一种获取 writer 的方式
from langgraph.config import get_stream_writer
def node(state: DataAgentState):
writer = get_stream_writer()
writer({"type": "progress", "step": "抽取关键词"})
这两种方式拿到的本质上都是同一个东西:一个可以把自定义数据写入当前图执行流的 writer 函数。本项目当前节点函数都已经声明了 runtime 参数,所以直接使用 runtime.stream_writer 更自然。
2. 用astream接收流式输出
如果只是普通执行一张图,可以使用类似 invoke 的方式。但普通调用通常只能拿到整张图结束后的最终结果,拿不到中间节点实时写出的内容
本项目需要实时输出进度,所以使用 astream(...)。
async for chunk in graph.astream(
input=state,
context=context,
stream_mode="custom",
):
print(chunk)
常见的流式模式可以先记住这几种:
| 模式 | 含义 |
|---|---|
values |
每个节点执行后输出完整状态快照 |
updates |
每个节点执行后只输出本节点更新的状态 |
custom |
输出 stream_writer 写出的自定义数据 |
messages |
输出大模型生成过程中的 token |
debug |
输出调试信息 |
3. 最小测试
if __name__ == "__main__":
async def test():
state = DataAgentState()
context = DataAgentContext()
async for chunk in graph.astream(
input=state, context=context, stream_mode="custom"
):
print(chunk)
asyncio.run(test())
在后端项目根目录下,运行:
uv run python -m app.agent.graph
这个测试不是为了验证最终 SQL 查询结果,而是先确认几件事:
- 图能不能成功编译;
- 节点能不能按预期顺序执行;
stream_writer写出的内容能不能被astream(...)接收到;validate_sql返回的{"error": None}能不能让条件边走到run_sql。

浙公网安备 33010602011771号