feat:新增代码注释
This commit is contained in:
+47
-6
@@ -48,10 +48,11 @@ def route_after_scope(state: QueryState) -> str:
|
||||
def build_graph(checkpointer=None):
|
||||
from app.config import settings
|
||||
|
||||
# 最大重试次数
|
||||
max_retries = settings.max_action_retries
|
||||
|
||||
def route_failure(state: QueryState) -> str:
|
||||
# 上限检查放在 Reflection 前,避免最后一次无效模型调用
|
||||
# 上限检查,放在 Reflection 前,避免最后一次无效模型调用
|
||||
return (
|
||||
"reflection_node"
|
||||
if state.get("retry_count", 0) < max_retries
|
||||
@@ -59,8 +60,13 @@ def build_graph(checkpointer=None):
|
||||
)
|
||||
|
||||
def route_after_action(state: QueryState) -> str:
|
||||
# 如果动作节点已明确标记失败,直接收尾,不再进入内容修正
|
||||
if state.get("status") == "failed":
|
||||
return "fallback_node"
|
||||
|
||||
# validation_passed 为 False:动作生成时解析或结构校验失败,未达重试上限则进入修正分析,已达上限则失败收尾
|
||||
# validation_passed 为 None:动作已生成但尚未模拟执行,进入 validator_node 检查动作范围和执行结果
|
||||
# 必须用 is False 区分失败与尚未校验,不能将 None 也当作失败
|
||||
return (
|
||||
route_failure(state)
|
||||
if state["validation_passed"] is False
|
||||
@@ -68,16 +74,20 @@ def build_graph(checkpointer=None):
|
||||
)
|
||||
|
||||
def route_after_validation(state: QueryState) -> str:
|
||||
# 整批动作通过校验则结束,否则根据剩余重试次数决定修正或失败收尾
|
||||
return END if state["validation_passed"] else route_failure(state)
|
||||
|
||||
def route_after_reflection(state: QueryState) -> str:
|
||||
# 获取失败分析产生的修正建议,生成建议失败时该字段可能为空
|
||||
plan = state.get("repair_plan")
|
||||
|
||||
# 只有存在建议且明确允许重试时才重新生成动作,其余情况统一失败收尾
|
||||
return "action_node" if plan is not None and plan.retryable else "fallback_node"
|
||||
|
||||
# 定义工作流
|
||||
# 使用 QueryState 定义节点之间共享的状态结构
|
||||
workflow = StateGraph(QueryState)
|
||||
|
||||
# 实例化节点
|
||||
# 为本次构建的图创建节点实例,字典键同时作为注册名称和路由目标
|
||||
nodes = {
|
||||
"input_node": InputNode(),
|
||||
"intent_node": IntentNode(),
|
||||
@@ -96,14 +106,17 @@ def build_graph(checkpointer=None):
|
||||
|
||||
# 注册节点,异步节点使用 BaseNode 的异步入口
|
||||
for node_name, node in nodes.items():
|
||||
# 根据 process 是否为异步函数选择调用入口,保留 BaseNode 的统一执行封装
|
||||
action = node._acall if inspect.iscoroutinefunction(node.process) else node
|
||||
# 将节点名称与实际调用对象绑定,供后面的边引用
|
||||
workflow.add_node(node_name, action)
|
||||
|
||||
# 定义入口
|
||||
# 首次执行从输入节点开始,完成上下文校验后再判断意图
|
||||
workflow.set_entry_point("input_node")
|
||||
|
||||
# 添加业务边
|
||||
# 输入处理完成后固定进入意图判断
|
||||
workflow.add_edge("input_node", "intent_node")
|
||||
|
||||
# 新建和修改进入需求理解,意图不明进入澄清
|
||||
workflow.add_conditional_edges(
|
||||
source="intent_node",
|
||||
@@ -115,13 +128,20 @@ def build_graph(checkpointer=None):
|
||||
"unclear": "clarify_node",
|
||||
},
|
||||
)
|
||||
|
||||
# 需求信息不足时提问,信息完整时继续判断修改范围
|
||||
workflow.add_conditional_edges(
|
||||
source="requirement_node",
|
||||
path=route_after_requirement,
|
||||
path_map={True: "clarify_node", False: "scope_node"},
|
||||
)
|
||||
|
||||
# 先保存澄清问题和等待状态,再进入调用 interrupt 的节点暂停执行
|
||||
workflow.add_edge("clarify_node", "wait_for_user_node")
|
||||
|
||||
# 问答生成文本后直接结束,不进入规划和动作流程
|
||||
workflow.add_edge("qa_node", END)
|
||||
|
||||
# 先读取组件能力再规划,规划后的业务数据查询后续接入
|
||||
workflow.add_conditional_edges(
|
||||
"scope_node",
|
||||
@@ -132,12 +152,18 @@ def build_graph(checkpointer=None):
|
||||
"fallback_node": "fallback_node",
|
||||
},
|
||||
)
|
||||
|
||||
# 组件目录查询成功才允许规划,未找到或不支持时进入失败收尾
|
||||
workflow.add_conditional_edges(
|
||||
"component_resource_node",
|
||||
lambda state: state["component_resources"].status == "resolved",
|
||||
{True: "planner_node", False: "fallback_node"},
|
||||
)
|
||||
|
||||
# 规划完成后交给动作节点转换,动作节点还会检查规划是否可执行
|
||||
workflow.add_edge("planner_node", "action_node")
|
||||
|
||||
# 动作生成后按结果进入校验、修正分析或失败收尾
|
||||
workflow.add_conditional_edges(
|
||||
"action_node",
|
||||
route_after_action,
|
||||
@@ -147,6 +173,8 @@ def build_graph(checkpointer=None):
|
||||
"fallback_node": "fallback_node",
|
||||
},
|
||||
)
|
||||
|
||||
# 校验成功结束,失败则在重试上限内分析修正原因
|
||||
workflow.add_conditional_edges(
|
||||
"validator_node",
|
||||
route_after_validation,
|
||||
@@ -156,6 +184,8 @@ def build_graph(checkpointer=None):
|
||||
"fallback_node": "fallback_node",
|
||||
},
|
||||
)
|
||||
|
||||
# 修正分析允许重试时回到动作生成,形成生成、校验、分析的循环
|
||||
workflow.add_conditional_edges(
|
||||
"reflection_node",
|
||||
route_after_reflection,
|
||||
@@ -164,14 +194,21 @@ def build_graph(checkpointer=None):
|
||||
"fallback_node": "fallback_node",
|
||||
},
|
||||
)
|
||||
|
||||
# 失败节点统一写入最终失败状态后结束流程
|
||||
workflow.add_edge("fallback_node", END)
|
||||
|
||||
# 用户补充后重新合并输入并判断意图,取消则直接结束
|
||||
workflow.add_conditional_edges(
|
||||
"wait_for_user_node",
|
||||
route_after_wait,
|
||||
{"input_node": "input_node", END: END},
|
||||
)
|
||||
|
||||
# TODO 本地默认保存在内存,可传入持久化 checkpointer
|
||||
# 仅在调用方未提供检查点存储时创建默认实现,外部传入的存储直接复用
|
||||
if checkpointer is None:
|
||||
# 允许恢复状态中使用的业务模型类型,使检查点读取后仍可按模型对象访问字段
|
||||
serde = JsonPlusSerializer(
|
||||
allowed_msgpack_modules=[
|
||||
("app.schemas.input_context", "InputContext"),
|
||||
@@ -193,16 +230,20 @@ def build_graph(checkpointer=None):
|
||||
("app.schemas.clarification_reply", "ClarificationReply"),
|
||||
]
|
||||
)
|
||||
|
||||
# 将检查点保存在当前进程内存中,支持暂停恢复,但进程退出后不会保留
|
||||
checkpointer = InMemorySaver(serde=serde)
|
||||
|
||||
# 编译节点和边,并绑定检查点存储,供执行时按 thread_id 保存和恢复状态
|
||||
return workflow.compile(checkpointer=checkpointer).with_config(
|
||||
{
|
||||
# 随修正上限增加图执行步数预算,每轮预留分析、生成、校验三步,至少允许 25 步
|
||||
"recursion_limit": max(25, 12 + 3 * max_retries),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# 创建全局图实例
|
||||
# 模块加载时创建共享图实例,复用该实例的调用也共用其检查点存储
|
||||
query_app = build_graph()
|
||||
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ from langgraph.errors import GraphInterrupt
|
||||
|
||||
from app.schemas.node_progress import NodeProgress, NodeProgressStatus
|
||||
|
||||
# 将内部节点名映射为用户能理解的进度说明,未配置的节点仍发送事件但不附带 label
|
||||
NODE_PROGRESS_MESSAGES = {
|
||||
"requirement_node": "正在理解你的需求",
|
||||
"qa_node": "正在整理回答",
|
||||
@@ -25,42 +26,58 @@ NODE_PROGRESS_MESSAGES = {
|
||||
@contextmanager
|
||||
def node_progress(node: str, state):
|
||||
"""每次调用独立计时和编号,同步与异步入口共用"""
|
||||
|
||||
try:
|
||||
# 读取当前图执行配置,用于判断是否处于图上下文,并获取任务标识
|
||||
config = get_config()
|
||||
except RuntimeError:
|
||||
# 独立调用节点没有图上下文,直接执行业务逻辑
|
||||
# 将控制权交给 with 块中的节点逻辑,此分支不发送进度事件
|
||||
yield
|
||||
# 节点逻辑正常执行后直接退出,不再获取图的流写入器
|
||||
return
|
||||
|
||||
# 获取当前图的 custom 流写入器,供本次节点执行推送进度事件
|
||||
writer = get_stream_writer()
|
||||
# 状态支持键值读取时用于提取标识,其他类型按空字典处理,避免调用 get 出错
|
||||
values = state if isinstance(state, Mapping) else {}
|
||||
# 以实际检查点配置为准,兼容没有在 state 中填写 ID 的本地示例
|
||||
thread_id = config.get("configurable", {}).get("thread_id") or values.get(
|
||||
"thread_id"
|
||||
)
|
||||
# 为本次节点调用生成唯一编号,区分同一节点的首次执行、重试和恢复执行
|
||||
execution_id = str(uuid4())
|
||||
# 使用单调计时器记录起点,后续计算本次执行耗时
|
||||
started = perf_counter()
|
||||
|
||||
def emit(status: NodeProgressStatus):
|
||||
# 将任务、用户消息和本次执行编号放入同一事件,便于消费方关联进度
|
||||
event = NodeProgress(
|
||||
thread_id=thread_id,
|
||||
message_id=values.get("message_id"),
|
||||
execution_id=execution_id,
|
||||
node=node,
|
||||
# 未配置用户说明时返回 None,序列化时省略该字段
|
||||
label=NODE_PROGRESS_MESSAGES.get(node),
|
||||
status=status,
|
||||
# 开始事件不携带耗时,其他状态携带从本次调用开始计算的毫秒数
|
||||
duration_ms=None
|
||||
if status == "started"
|
||||
else round((perf_counter() - started) * 1000, 3),
|
||||
)
|
||||
|
||||
# 转成可序列化的字典并发送到 custom 流,省略空字段,不写入业务状态
|
||||
writer(event.model_dump(mode="json", exclude_none=True))
|
||||
|
||||
# 节点业务逻辑开始前,先通知消费方本次执行已经启动
|
||||
emit("started")
|
||||
try:
|
||||
# 执行 with 块中的节点逻辑,节点抛出的异常会回到下面对应的异常分支
|
||||
yield
|
||||
except GraphInterrupt:
|
||||
# interrupt 是正常暂停,恢复后会产生新的执行 ID
|
||||
emit("waiting_for_input")
|
||||
# 将暂停信号重新抛给 LangGraph,由图负责保存检查点和中断执行
|
||||
raise
|
||||
except asyncio.CancelledError:
|
||||
# 协程被取消也要结束这次节点执行,继续向上传递取消信号
|
||||
@@ -72,4 +89,5 @@ def node_progress(node: str, state):
|
||||
raise
|
||||
else:
|
||||
# 校验不通过等业务结果仍属于节点正常执行完成
|
||||
# completed 仅表示本次节点调用未抛异常,不代表整个任务成功或前端已执行
|
||||
emit("completed")
|
||||
|
||||
Reference in New Issue
Block a user