0. 系列闭环(不公开源码也能跟读)
端到端链路:Vue 前端 → api/routes/chat.py → Guide 多轮 SSE → run_analysis_pipeline(解析→分析→匹配→报告)→ tools/pdf_exporter PDF。
本篇:第 5/17 篇 · 控制环 · 条件边
| 阶段 | 用户可见 | 代码入口 | 对应篇 |
|---|---|---|---|
| 建会话 | 欢迎语 | POST /api/sessions | 09 |
| 多轮对话 | SSE 流式 | chat/stream → run_guide_single_turn | 06, 14 |
| 信息充分 | 开始分析 | _run_analysis_background | 05, 07 |
| 履历解析 | 进度 30% | run_resume_parser | 12 |
| 画像/RIASEC | 进度 50% | run_profile_analyzer | 03, 13 |
| 职业匹配 | 进度 70% | run_career_matcher | 02 |
| 报告 | 进度 90% | run_reporter | 11 |
| 下载 PDF | 文件 | GET …/report/pdf | 11, 15 |
| 说明 | |
|---|---|
| 读本篇前 | 第 04 篇状态字段、第 06 篇 Guide 子图 |
| 读完本篇 | 写出 route_after_guide 与 should_continue 的退出条件 |
| 下一环 | 第 07 篇:路由失败时的容错(第 6 篇) |
全系列闭环索引:SERIES-LOOP.md
一、要解决什么问题
iCan 的对话引导阶段需要「信息不够就继续问、够了就进分析流水线」。如果用普通 Python if-else 串函数,很难表达「回到上一步再跑一轮」的循环,也没有 LangGraph 自带的执行追踪和 recursion_limit 兜底。
项目在两层 StateGraph 里用 add_conditional_edges 解决这件事:
- 内层(
agents/guide.py的create_guide_graph()):Guide 子图在check_sufficiency后决定继续dig_deeper还是END。 - 外层(
workflow.py的create_workflow()):顶层图在guide_node后根据needs_more_info决定回到guide_node还是进入resume_parser_node。
另外,线上 HTTP 对话走的是 run_guide_chat 单轮模式,并不每次请求都跑完整顶层图——这一点和 CLI 的 run_workflow 不同,下文会单独说明。
二、实现位置
| 层级 | 文件 | 关键符号 |
|---|---|---|
| 内层子图 | agents/guide.py |
should_continue、create_guide_graph、run_guide_agent |
| 外层编排 | workflow.py |
guide_node、route_after_guide、create_workflow |
| API 单轮 | workflow.py + api/routes/chat.py |
run_guide_chat → run_guide_single_turn |
| 状态字段 | core/state.py |
needs_more_info、conversation_history |
三、LangGraph 条件边 API
核心写法在 workflow.py 的 create_workflow():
1 | |
路由函数只读 state、只返回字符串 key,不发起副作用——这是 LangGraph 推荐的模式,便于调试和可视化。
四、内层:should_continue 与 Guide 子图
agents/guide.py 中 Guide 子图结构:
1 | |
should_continue 的逻辑:
1 | |
注册方式:
1 | |
run_guide_agent 编译子图时设 recursion_limit=15,比外层的 50 更紧,防止子图在无用户输入时自旋。
五、外层:guide_node 与 route_after_guide
guide_node:把子图结果写回顶层 state
workflow.py 的 guide_node 做三件事:
- 把
raw_input追加进conversation_history; - 调用
run_guide_agent(guide_state); - 用
needs_more_info = not is_sufficient写回顶层 state。
1 | |
若子图抛错,节点 catch 后仍设 needs_more_info: True,避免误进分析阶段。
route_after_guide:信息充分或强制推进
1 | |
决策顺序:先信 LLM/子图的充分性判断,再数用户轮次,最后才继续循环。路由本身也有 try/except,异常时默认 resume_parser_node,避免卡死在 guide 环。
完整顶层路径(create_workflow 注释):
1 | |
run_workflow(CLI 入口)一次性 ainvoke,并设 recursion_limit=50:
1 | |
六、run_guide_chat vs 完整 workflow
这是 iCan 里最容易混淆的两条路径:
| 路径 | 入口 | 是否走 route_after_guide |
典型调用方 |
|---|---|---|---|
| 单轮对话 | run_guide_chat |
否 | api/routes/chat.py 的 /chat、/sessions |
| 完整图 | run_workflow → create_workflow |
是 | cli.py |
run_guide_chat 内部调用 agents/guide.py 的 run_guide_single_turn——直接调 LLM,不跑 Guide 子图循环。用户每发一条 HTTP 消息才推进一轮;信息充分后由 API 层 asyncio.create_task 调 run_analysis_pipeline,不再经过顶层 LangGraph。
1 | |
设计原因:Web 场景需要「等用户输入再往下走」,不能把多轮对话塞进一次 ainvoke 里空转;route_after_guide 的 user_msg_count >= 3 强制退出,主要是给 CLI 一次性跑完整图 时防死循环用的。
七、三重保险防止无限循环
| 保险层 | 位置 | 机制 |
|---|---|---|
| 第 1 层 | agents/guide.py should_continue |
loop_count >= 8 强制 handoff |
| 第 2 层 | workflow.py route_after_guide |
user_msg_count >= 3 强制进 resume_parser_node |
| 第 3 层 | LangGraph 框架 | 外层 recursion_limit=50,子图 recursion_limit=15 |
三层阈值含义不同:内层限制 子图消息条数,外层限制 顶层 conversation_history 里 user 角色条数,框架限制 图步数总和。
八、踩坑与边界
注释写「最多 2 次」,代码是 3 轮 user 消息
route_after_guide和should_continue的 docstring 都写「最多循环 2 次」,实际判断是user_msg_count >= 3/loop_count >= 8。写文档和改阈值时要对源码,不要抄注释。loop_count数的是messages非空条数,不是 user 轮次should_continue用GuideState.messages(Annotated reducer 累积),和顶层conversation_history的计数方式不一致。调循环上限时要分别验证两层 state。API 路径没有外层
route_after_guide
若只在浏览器里测/chat,看不到user_msg_count >= 3的效果;要在 CLI 跑run_workflow或单测顶层图才能验证强制退出。needs_more_info默认 Truecore/state.py初始 workflow state 里needs_more_info为 False,但route_after_guide用state.get("needs_more_info", True)——字段缺失时会偏向继续 guide,设计上是保守策略。
九、小结
- 条件路由用
add_conditional_edges(source, router_fn, edge_map),router 只读 state、返回字符串。 - Guide 内层在
agents/guide.py用should_continue控制dig_deeper↔END;外层在workflow.py用route_after_guide控制 guide 环 ↔ 分析链。 - 生产 API 用
run_guide_chat单轮 + 用户驱动循环;CLI 用run_workflow跑完整图,才触发route_after_guide的轮次上限。 - 循环必须有独立计数 + 框架
recursion_limit,避免无用户输入时 LangGraph 空转。 - 下一篇(第 6 篇)讲双层 StateGraph 嵌套如何把 Guide 子图和顶层编排解耦。
附录:关键源码(逐行注释)
以下代码摘自 iCan 实现,每行上方均有中文注释,不公开仓库也可跟读。
生成命令:python3 bin/build-ican-annotated-snippets.py
内层 should_continue
1 | |
外层 route_after_guide
1 | |
条件边注册
1 | |
系列导航
| 篇 | 主题 |
|---|---|
| 1 | 系统全景 |
| 2 | 五 Agent 协作 |
| 3 | 霍兰德 RIASEC |
| 4–7 | 状态 · 5 路由(本篇) · 嵌套 · 容错 |
| 8–11 | LLM 层 · SSE/WS · DB 迁移 · PDF |
| 12–14 | JSON Prompt · RIASEC Prompt · Guide Prompt |
| 15–17 | Docker · 中间件 · 配置 |