0. Series Loop (Read Along Without Public Source Code)
End-to-End Pipeline: Vue frontend → api/routes/chat.py → Guide multi-turn SSE → run_analysis_pipeline (parse → analyze → match → report) → tools/pdf_exporter PDF.
This Article: 5/17 · Control Loop · Conditional Edges
| Stage | User Visible | Code Entry | Article |
|---|---|---|---|
| Create Session | Welcome message | POST /api/sessions | 09 |
| Multi-turn Chat | SSE streaming | chat/stream → run_guide_single_turn | 06, 14 |
| Info Sufficient | Start Analysis | _run_analysis_background | 05, 07 |
| Resume Parsing | Progress 30% | run_resume_parser | 12 |
| Profile/RIASEC | Progress 50% | run_profile_analyzer | 03, 13 |
| Career Matching | Progress 70% | run_career_matcher | 02 |
| Report | Progress 90% | run_reporter | 11 |
| Download PDF | File | GET …/report/pdf | 11, 15 |
| Description | |
|---|---|
| Before reading | Article 04 state fields, Article 06 Guide subgraph |
| After reading | Can write the exit conditions for route_after_guide and should_continue |
| Next loop | Article 07: Error tolerance when routing fails (Article 6) |
Full series loop index: SERIES-LOOP.md
1. What Problem Does It Solve
iCan’s dialogue guidance stage needs to “continue asking if info is insufficient, enter analysis pipeline when enough.” Using plain Python if-else to chain functions makes it hard to express the loop of “go back to the previous step and run another round,” and lacks LangGraph’s built-in execution tracing and recursion_limit fallback.
The project uses add_conditional_edges in a two-layer StateGraph:
- Inner layer (
create_guide_graph()inagents/guide.py): The Guide subgraph, aftercheck_sufficiency, decides whether to continue withdig_deeperorEND. - Outer layer (
create_workflow()inworkflow.py): The top-level graph, afterguide_node, decides based onneeds_more_infowhether to return toguide_nodeor proceed toresume_parser_node.
Additionally, the online HTTP dialogue uses the run_guide_chat single-turn mode, which does not run the full top-level graph on every request — this differs from the CLI’s run_workflow and will be explained separately below.
2. Implementation Locations
| Layer | File | Key Symbols |
|---|---|---|
| Inner subgraph | agents/guide.py |
should_continue, create_guide_graph, run_guide_agent |
| Outer orchestration | workflow.py |
guide_node, route_after_guide, create_workflow |
| API single-turn | workflow.py + api/routes/chat.py |
run_guide_chat → run_guide_single_turn |
| State fields | core/state.py |
needs_more_info, conversation_history |
3. LangGraph Conditional Edge API
The core implementation is in create_workflow() in workflow.py:
1 | |
The routing function only reads state and returns a string key, without initiating side effects — this is LangGraph’s recommended pattern for easier debugging and visualization.
4. Inner Layer: should_continue and Guide Subgraph
The Guide subgraph structure in agents/guide.py:
1 | |
The logic of should_continue:
1 | |
Registration method:
1 | |
When compiling the subgraph, run_guide_agent sets recursion_limit=15, which is tighter than the outer layer’s 50, preventing the subgraph from spinning without user input.
5. Outer Layer: guide_node and route_after_guide
guide_node: Writing the subgraph result back to the top-level state
guide_node in workflow.py does three things:
- Appends
raw_inputtoconversation_history; - Calls
run_guide_agent(guide_state); - Writes back
needs_more_info = not is_sufficientto the top-level state.
1 | |
If the subgraph throws an error, the node catches it and still sets needs_more_info: True, preventing accidental entry into the analysis phase.
route_after_guide: Info sufficient or forced advancement
1 | |
Decision order: First trust the LLM/subgraph’s sufficiency judgment, then count user turns, only then continue the loop. The routing itself also has try/except, defaulting to resume_parser_node on exception to avoid getting stuck in the guide loop.
Full top-level path (from create_workflow comment):
1 | |
run_workflow (CLI entry) does a single ainvoke with recursion_limit=50:
1 | |
6. run_guide_chat vs Full Workflow
These are the two most easily confused paths in iCan:
| Path | Entry | Uses route_after_guide? |
Typical Caller |
|---|---|---|---|
| Single-turn Chat | run_guide_chat |
No | /chat, /sessions in api/routes/chat.py |
| Full Graph | run_workflow → create_workflow |
Yes | cli.py |
run_guide_chat internally calls run_guide_single_turn from agents/guide.py — it calls the LLM directly without running the Guide subgraph loop. Each HTTP message from the user advances one turn; when info is sufficient, the API layer calls run_analysis_pipeline via asyncio.create_task, no longer passing through the top-level LangGraph.
1 | |
Design rationale: The Web scenario requires “wait for user input before proceeding,” so multi-turn cannot be stuffed into one ainvoke and idle; the user_msg_count >= 3 forced exit in route_after_guide is mainly to prevent infinite loops when the CLI runs the full graph in one shot.
7. Triple Insurance Against Infinite Loops
| Insurance Layer | Location | Mechanism |
|---|---|---|
| Layer 1 | agents/guide.py should_continue |
loop_count >= 8 forces handoff |
| Layer 2 | workflow.py route_after_guide |
user_msg_count >= 3 forces resume_parser_node |
| Layer 3 | LangGraph framework | Outer recursion_limit=50, subgraph recursion_limit=15 |
The three layers have different meanings: the inner layer limits the number of messages in the subgraph, the outer layer limits the number of user-role messages in the top-level conversation_history, and the framework limits the total number of graph steps.
8. Pitfalls and Edge Cases
Comment says “max 2 times”, but code does 3 user messages
The docstrings forroute_after_guideandshould_continueboth say “max 2 loops,” but the actual check isuser_msg_count >= 3/loop_count >= 8. When writing documentation and adjusting thresholds, refer to the source code, not the comments.loop_countcounts non-emptymessagesentries, not user turnsshould_continueusesGuideState.messages(accumulated via Annotated reducer), which is inconsistent with the counting method of the top-levelconversation_history. When adjusting loop limits, verify both layers of state separately.The API path lacks the outer
route_after_guide
If you only test/chatin a browser, you won’t see the effect ofuser_msg_count >= 3; you need to runrun_workflowin the CLI or test the top-level graph with unit tests to verify the forced exit.needs_more_infodefaults to True
Incore/state.py, the initial workflow state hasneeds_more_infoset to False, butroute_after_guideusesstate.get("needs_more_info", True)— if the field is missing, it tends to continue the guide, which is a conservative design.
9. Summary
- Conditional routing uses
add_conditional_edges(source, router_fn, edge_map); the router only reads state and returns a string. - Guide’s inner layer in
agents/guide.pyusesshould_continueto controldig_deeper↔END; the outer layer inworkflow.pyusesroute_after_guideto control the guide loop ↔ analysis chain. - Production API uses
run_guide_chatsingle-turn + user-driven loop; CLI usesrun_workflowto run the full graph, which triggers the turn limit inroute_after_guide. - Loops must have independent counters + framework
recursion_limitto prevent LangGraph from spinning without user input. - The next article (Article 6) will discuss how two-layer StateGraph nesting decouples the Guide subgraph from top-level orchestration.
Appendix: Key Source Code (Annotated Line by Line)
The following code is taken from the iCan implementation, with Chinese comments above each line, so you can follow along even without the public repository.
Generation command: python3 bin/build-ican-annotated-snippets.py
Inner should_continue
1 | |
Outer route_after_guide
1 | |
Conditional Edge Registration
1 | |
Series Navigation
| Article | Topic |
|---|---|
| 1 | System Panorama |
| 2 | Five Agent Collaboration |
| 3 | Holland RIASEC |
| 4–7 | State · 5 Routing (This) · Nesting · Fault Tolerance |
| 8–11 | LLM Layer · SSE/WS · DB Migration · PDF |
| 12–14 | JSON Prompt · RIASEC Prompt · Guide Prompt |
| 15–17 | Docker · Middleware · Configuration |