0. Series Loop (Follow 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: 2/17 · Pipeline · Five Agent Nodes
| Stage | User Visible | Code Entry | Corresponding 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 this | Architecture diagram from Article 01 |
| After reading this | For each node in workflow.py, state its input/output fields |
| Next in loop | Article 04: Meaning of iCanWorkflowState fields (Article 03) |
Full series loop index: SERIES-LOOP.md
1. Why Multiple Agents
A common misconception is that giving an LLM a super prompt can accomplish everything. But in practice you will find:
Problems with the single-prompt approach:
- Wasted context window: Stuffing all task prompts into one call dilutes useful information.
- Degraded output quality: The LLM simultaneously parses, analyzes, matches, and writes—none of which it does well.
- No reusability: Switching to a different scenario (e.g., only resume parsing) requires rewriting the entire prompt.
- Difficult debugging: When output is faulty, it’s unclear which step went wrong.
Advantages of the multi-agent approach:
- Each agent focuses on one task, so prompts are more precise.
- State is passed between agents; output of the upstream is input to the downstream.
- Individual agents can be tested and optimized independently.
- Failure of one agent does not affect the design of others.
2. Responsibilities of the 5 Agents
1 | |
Guide — Dialogue Conductor (agents/guide.py)
Responsibility: Collect user career information through multi-turn dialogue.
It has its own internal StateGraph (5 nodes):
1 | |
- welcome: Generate a welcome message and understand the user’s intention.
- assess_need: Determine the core need (career planning / transition / skill improvement).
- collect_basic_info: Gather education, years of work experience, current position, etc.
- dig_deeper: Probe work preferences, values, personality traits.
- check_sufficiency: The LLM judges whether the information is sufficient; if not, return to dig_deeper.
Key design: check_sufficiency uses conditional routing via should_continue(). The inner loop executes at most 8 times to prevent infinite loops.
ResumeParser — Resume Parser (agents/resume_parser.py)
Responsibility: Convert the user’s natural language descriptions into structured JSON data. In the top-level workflow.py, the resume_parser_node concatenates all user utterances from conversation_history and raw_input into combined_text, then calls run_resume_parser().
Input is a block of text containing all dialogue content; output is a standardized structured profile:
1 | |
Technical challenge: The JSON format output by the LLM is unstable. agents/resume_parser.py works with llm/parsers.py using a four-level fallback parsing + llm/providers.py‘s invoke_llm_with_json (response_format={"type": "json_object"}) to force JSON mode; the model is taken from get_light_model().
ProfileAnalyzer — Personal Analysis (agents/profile_analyzer.py)
Responsibility: Conduct a deep five‑dimension analysis based on the structured profile.
Five dimensions:
- Ability model: Hard skills / soft skills / learning ability / innovation / leadership (score 0‑10)
- Work style: Decision‑making approach / collaboration preference / pace preference / communication style
- Personality traits: Big Five personality scores on five dimensions
- Career values: Ranking of 8 dimensions (material rewards / growth / balance / influence / autonomy / stability / innovation / interpersonal)
- Holland RIASEC: Six‑dimension scores (R/I/A/S/E/C) + Holland Code
Technical highlight: The sub‑graph create_profile_analyzer_graph() executes sequentially after load_profile: ability → style → personality → values → analyze_riasec → strengths/weaknesses → synthesize_profile (the comment says “parallel” as design intent, but currently the edges are linear). The RIASEC scoring is covered in Articles 03 and 13.
CareerMatcher — Career Matching (agents/career_matcher.py)
Responsibility: Recommend a three‑tier career path based on the personal profile.
Three‑tier recommendation strategy:
| Tier | Strategy | Match Score | Description |
|---|---|---|---|
| First | Vertical deepening | 80‑95% | Go deeper in the current industry |
| Second | Horizontal expansion | 60‑80% | Move to a related field |
| Third | Transformation exploration | 40‑60% | New directions based on personal traits |
Each tier includes: target position, skill gap, market outlook (demand / salary / trends), and timeline.
Reporter — Report Output (agents/reporter.py)
Responsibility: Integrate all analysis results into a structured Markdown report.
Report structure:
- Personal profile overview
- Five‑dimension deep analysis
- Career direction recommendations (three tiers)
- Action suggestions (short / medium / long term)
- Market insights
3. Implementation of the StateGraph Connection
Implementation file: workflow.py (the sole top‑level orchestration entry; see SOURCE-ACCURACY.md).
Top‑level Workflow create_workflow()
1 | |
Outer guide_node: Bridging Inner and Outer State
guide_node maps iCanWorkflowState (core/state.py) to the inner GuideState, calls run_guide_agent(), and converts is_info_sufficient into the outer needs_more_info:
1 | |
Outer Routing route_after_guide
Working together with the inner should_continue (loop_count >= 8) in agents/guide.py, there is an additional safety net at the outer level: when user_msg_count >= 3, the flow is forced into parsing to avoid infinite loops when there is no user interaction:
1 | |
Production: run_analysis_pipeline
After information is sufficient, the front‑end SSE dialogue usually does not go through the full ainvoke(create_workflow()), but instead calls run_analysis_pipeline() in workflow.py: it skips Guide, directly chains the four agents, and falls back to a rule‑engine report when Ollama is unavailable. The entry points are in api/routes/chat.py and api/routes/report_gen.py.
Data Transfer Between Nodes
Pattern for each node function:
1 | |
Key point: The node only returns the fields that need updating; LangGraph automatically merges them into the global state.
4. Comparison with CrewAI / AutoGen
| Dimension | LangGraph | CrewAI | AutoGen |
|---|---|---|---|
| Orchestration model | Directed graph (DAG) | Flow / hierarchy | Dialogue rounds |
| State management | TypedDict + Reducer | Shared Memory | Message history |
| Conditional routing | ✅ Native support | ⚠️ Requires customization | ❌ Not supported |
| Loop control | ✅ Conditional edges + recursion_limit | ⚠️ Limited support | ✅ Dialogue loops |
| Visualization | ✅ Export graph structure | ❌ | ❌ |
| Learning curve | Medium | Low | Low |
| Use case | Complex workflows | Simple task orchestration | Multi‑model conversations |
Selection advice:
- If your agents require conditional routing and loops → LangGraph
- If it’s simple task assignment → CrewAI
- If you need multi‑model conversations / debates → AutoGen
5. Pitfalls and Lessons Learned
Pitfall 1: Confusing Reducer field levels
The outer iCanWorkflowState (core/state.py) uses workflow_messages: Annotated[list[str], operator.add] to accumulate workflow logs; the inner GuideState uses messages: Annotated[list[str], operator.add] to accumulate AI replies. If messages is mistakenly used at the top level, LangGraph will not merge as expected.
1 | |
Pitfall 2: Uncontrolled Loops
The Guide Agent could loop infinitely. The solution is a two‑layer limit:
- Inner:
should_continue()limits to at most 8 iterations. - Outer:
route_after_guide()limits to at most 3 rounds of user messages.
Pitfall 3: Node Exceptions and Empty Profiles
Each node in workflow.py has an independent try-except; on failure it returns an empty dict or a placeholder report (e.g., reporter_node returns “Report generation failed, please retry later”) to avoid crashing the entire graph. However, if structured_profile is empty but still enters profile_analyzer_node, downstream RIASEC scores will all be zero—input thickness must be guaranteed at the Guide or Parser layer.
Pitfall 4: Implementation is Functions + Sub‑graphs, Not Agent Classes
All five nodes are async def xxx_node + run_xxx() entry points; there is no GuideAgent Python class. When reading the source, search for agents/guide.py‘s run_guide_agent, and do not interpret it as a CrewAI Role class.
6. Summary (iCan Orchestration Key Points)
- The top level has only
create_workflow()inworkflow.py, with fixed node names:guide_node→route_after_guide→ four‑stage linear chain. - Each node follows four steps: outer TypedDict → inner TypedDict → run_xxx → map back to outer; field definitions are in
core/state.py. - Two‑layer loop limits: inner
should_continue(loop_count >= 8) + outerroute_after_guide(user_msg_count >= 3). - The online SSE path uses
run_guide_chat+run_analysis_pipeline, which differs from the CLI‑stylerun_workflow.
Next article: Engineering implementation of Holland RIASEC in
agents/profile_analyzer.py, and structured scoring under OpenAI‑compatible APIs (DeepSeek etc. as.envdeployment examples).