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: # 6/17 · Guide Loop · Inner-Outer Two-Layer Graph
| Stage | User Visible | Code Entry | Article |
|---|---|---|---|
| Create Session | Welcome Message | POST /api/sessions | 09 |
| Multi-turn Dialogue | SSE Streaming | chat/stream → run_guide_single_turn | 06, 14 |
| Information 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 this | Section 05: Outer layer guide_node |
| After reading this | Draw the inner 5-node subgraph, explain API goes through run_guide_single_turn |
| Next loop | Article 14: Stage Prompts (Article 7) |
Full series loop index: SERIES-LOOP.md
1. What Problem to Solve
The iCan top-level workflow has 5 Agent nodes (Guide → ResumeParser → ProfileAnalyzer → CareerMatcher → Reporter). If the 5 dialogue stages inside Guide (Welcome, Need Assessment, Basic Collection, Deep Mining, Sufficiency Check) are also flattened into the same StateGraph, it would cause:
- State field explosion: Guide’s
collected_info,current_stagemixed with top-levelstructured_profile,final_reportin the same TypedDict. - Modifying Guide affects global: Adjusting
check_sufficiency‘s routing logic might accidentally trigger top-levelroute_after_guide. - High testing cost: Verifying “back to dig_deeper when info insufficient” requires running all four analysis stages to isolate.
The actual approach is outer 5 nodes + inner Guide subgraph: the outer workflow.py‘s guide_node only does state mapping, the inner agents/guide.py‘s create_guide_graph() encapsulates 5 function nodes and conditional edges.
2. Implementation Location: Two-Layer State + Two-Layer Graph
| Layer | File | State Type | Entry |
|---|---|---|---|
| Outer | workflow.py |
iCanWorkflowState |
create_workflow() → guide_node |
| Inner | agents/guide.py |
GuideState |
create_guide_graph() → run_guide_agent() |
Two sets of TypedDict in core/state.py separate responsibilities:
1 | |
The outer layer only cares about needs_more_info, conversation_history, raw_input; the inner layer holds current_stage, missing_fields, emotion_state.
3. Outer guide_node: Facade, Not a Guide Class
There is no GuideAgent class in workflow.py; only the async function guide_node. It is responsible for extract → call → write back:
1 | |
The outer layer does not know inner node names like welcome, dig_deeper; it only reads is_info_sufficient and messages[-1].
The outer loop is controlled by route_after_guide:
1 | |
4. Inner create_guide_graph(): Five Nodes + Conditional Loop
The inner graph is built in agents/guide.py, with all nodes being async functions, not class methods:
1 | |
run_guide_agent calls create_guide_graph() each time and then ainvoke, with recursion_limit=15:
1 | |
The inner loop is determined by should_continue: is_info_sufficient=True → handoff (END); otherwise back to dig_deeper. Additionally, if loop_count >= 8, force handoff (estimated from messages list length).
Each inner node calls LLM via get_chat_model() + invoke_llm() (see Article 8). On exception, returns fixed phrases without retrying the model.
5. Difference from API Path: Subgraph Not Used by All Entries
This is key to understanding the nested architecture: The HTTP dialogue API does NOT go through the inner 5-node graph by default.
| Entry | Call Chain | Uses create_guide_graph? |
|---|---|---|
Top-level run_workflow() |
guide_node → run_guide_agent |
Yes |
POST /api/sessions/.../chat |
run_guide_chat → run_guide_single_turn |
No (single-turn LLM) |
POST .../chat/stream |
Direct model.astream + keyword sufficiency check |
No |
run_guide_chat in workflow.py explicitly uses single-turn mode:
1 | |
Thus: The nested subgraph serves the batch-processing top-level workflow; online per-turn chat uses run_guide_single_turn or SSE streaming, whose logic differs from inner check_sufficiency (LLM judges sufficient/insufficient).
6. Position in the Pipeline
Complete top-level edges (create_workflow):
1 | |
A single ainvoke of the inner graph runs sequentially through welcome → … → check_sufficiency, and if necessary loops between dig_deeper ↔ check_sufficiency. Each time the outer guide_node is scheduled, it calls create_initial_guide_state() and starts from welcome again — this repeats the welcome message in scenarios without real-time user input and running the full workflow in one go. It is a design trade-off rather than a LangGraph framework limitation.
The other four Agents (resume_parser, profile_analyzer, etc.) also follow the pattern of outer node functions + inner run_* subgraph/pipeline, same as Guide but with different inner node counts; the top-level file has only one create_workflow() in workflow.py.
7. Pitfalls
① Comment says “loop at most 2 times”, code doesn’t use 2
The comment in should_continue says “loop at most 2 times”, but the code actually uses loop_count >= 8; the outer route_after_guide uses user_msg_count >= 3 to force into analysis. When documenting or changing requirements, rely on grep results, not docstrings.
② run_guide_agent recompiles the graph each timecreate_guide_graph() calls graph.compile() each time run_guide_agent is called, without module-level caching. For frequent Guide calls, caching the compiled graph is possible but not implemented in the current MVP.
③ Outer guide_node always starts from welcomecreate_initial_guide_state() fixes current_stage="greeting", so the inner entry point is always welcome. If the outer route_after_guide returns to guide_node multiple times, the welcome node is repeated—this matters when running the workflow in batch; the online API is unaffected because it uses run_guide_single_turn.
④ Dual track: messages reducer and conversation_history
Inner AI replies go into GuideState.messages (Annotated add); outer persistence uses conversation_history (list of role/content dicts). guide_node only maps messages[-1] into history; intermediate multi-message outputs from inner nodes are not fully carried to the outer layer.
8. Summary
- Nested structure: outer
iCanWorkflowState+guide_node, innerGuideState+create_guide_graph(), implemented as function nodes rather than an Agent class. - The outer facade only does field mapping; the inner 5 nodes +
should_continuehandle dialogue stages and dig_deeper loop. - API chat goes through
run_guide_single_turn, NOT the inner subgraph; the subgraph is mainly used for therun_workflow/guide_nodepath. - Each of the two layers has its own exit conditions (inner:
loop_count/LLM sufficiency, outer:needs_more_info/user turns). When debugging, clarify which layer is looping. - To modify Guide behavior, first confirm whether the change is in the subgraph nodes or in the single-turn/API streaming path.
Next article: LangGraph error handling and fault tolerance (
workflow.pynode excepts,run_analysis_pipelinedegradation).
Appendix: Key Source Code (Line-by-Line Annotations)
The following code is extracted from the iCan implementation. Each line has Chinese comments above. You can follow along even without a public repository.
Generation command: python3 bin/build-ican-annotated-snippets.py
create_guide_graph
1 | |
Outer guide_node facade
1 | |
run_guide_single_turn (API actual path)
1 | |
Series Navigation
| Article | Topic |
|---|---|
| 1 | System Overview |
| 2 | Five-Agent Collaboration |
| 3 | Holland RIASEC |
| 4–7 | State · Routing · 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 |