整体业务流程

image

 

这里有三处设计需要先看懂。

第一,关键词抽取之后,字段信息、指标信息、字段取值可以并行召回。它们都依赖用户问题和关键词,但彼此之间没有强制先后顺序。

第二,召回结果不能直接交给大模型生成 SQL。召回阶段通常宁可多召回一些,避免漏掉关键信息;但生成 SQL 阶段需要尽量干净、准确的上下文。因此中间还需要合并过滤

第三,生成 SQL 后不能直接执行。大模型生成的 SQL 可能字段名写错、表名写错、join 条件不完整,或者不符合当前数据库方言。所以后面还要校验 SQL;校验失败时,再进入校正流程。

State: 共享状态,不是随手塞变量的字典

Node: 节点只负责一件事,并返回局部状态

在本项目里,节点函数一般长这样:

async def extract_keywords(
    state: DataAgentState,
    runtime: Runtime[DataAgentContext],
):
    ...

这里最重要的是两个参数:

  • state:读取和写回业务中间状态;
  • runtime:读取运行时上下文,或者通过 stream_writer 输出进度。
参数含义本项目当前是否重点使用
state 当前图状态,节点读取它并返回局部更新 重点使用
runtime 运行时上下文,包含 contextstream_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
posted @ 2026-05-25 10:34  幻影之舞  阅读(17)  评论(0)    收藏  举报