If a career planning conversation starts by listing questions about education, years of experience, skills, the user feels “interrogated.” If it’s completely free-form chat, it’s easy to enter the analysis pipeline with insufficient information, leading to empty output from ResumeParser and RIASEC.
iCan’s guide_node (workflow.py) calls run_guide_agent in agents/guide.py, using an inner StateGraph with five nodes + stage Prompt templates for progressive mining. The outer route_after_guide decides when to enter resume_parser_node. The frontend SSE multi-turn dialogue goes through run_guide_chat() in workflow.py → run_guide_single_turn() in agents/guide.py, with the entry point at api/routes/chat.py.
2. Inner Five Nodes (Consistent with Code)
The mapping between node names in the implementation and Prompt stages:
Graph Node
Prompt Stage Key
Role
welcome
greeting
Welcome message, understand intent
assess_need
assess_need
Determine planning/transition/skills needs
collect_basic_info
collect_basic_info
Education, years, position, skills
dig_deeper
dig_deeper
Preferences, values, sources of achievement
check_sufficiency
—
LLM + keywords to decide if handoff is ready
1 2 3 4 5 6 7
flowchart LR W[welcome] --> A[assess_need] A --> C[collect_basic_info] C --> D[dig_deeper] D --> S[check_sufficiency] S -->|Insufficient| D S -->|Sufficient or loop>=8| H[handoff END]
The System Prompt (GUIDE_SYSTEM_PROMPT in llm/prompts.py) defines the role “Xiao C”, dialogue strategy (progressive mining, emotion recognition, 1–2 questions at a time, within 200 characters). Each node then injects the user instruction for the corresponding stage from GUIDE_STAGE_TEMPLATES. The inner graph is compiled by create_guide_graph() in agents/guide.py, and run_guide_agent() is the unified entry point.
3. Stage Template Examples
Templates are centralized in llm/prompts.py; node functions only assemble messages and call invoke_llm:
1 2 3 4 5 6 7 8 9 10 11 12
GUIDE_STAGE_TEMPLATES = { "greeting": ( "The user has just started the conversation. Please warmly welcome the user, briefly introduce the career planning services you can provide, " "and use a relaxed open-ended question to understand the user's intent." ), "dig_deeper": ( "Basic information has been mostly collected. Now please dive deeper into the following dimensions:\n" "1. Work preferences: Independent vs team? Stable vs challenging?\n" "2. Sources of achievement, career values, personality traits, root causes of confusion, expected goals\n" "You can combine previously collected information to ask targeted deep questions." ), }
Typical call in the welcome node (agents/guide.py):
collect_basic_info / dig_deeper append the user’s recent utterances to collected_info["collected_raw"], which will later be concatenated and parsed by resume_parser_node in workflow.py.
4. Information Sufficiency: LLM Judgment + Keyword Fallback
check_sufficiency (agents/guide.py) is not a Prompt template stage but an independent node:
Concatenate user history text, do a rough keyword scan using a keyword list (e.g., “years”, “industry”, “position”, “skills”).
Start another short dialogue, System uses an independent assistant Prompt (not GUIDE_SYSTEM_PROMPT), asking the LLM to only answer sufficient / insufficient.
1 2 3 4 5 6 7 8 9
optional_keywords = ["year", "industry", "position", "job", "skill", "experience", "company", "major", "education", "direction", "expectation", "confusion"] all_user_text = " ".join(msg["content"] for msg in conversation_history if msg.get("role") == "user") found_keywords = [kw for kw in optional_keywords if kw in all_user_text]
This differs from the outer route_after_guide in workflow.py: the outer reads needs_more_info, and user_msg_count >= 3 also forces entry into resume_parser_node, preventing the user from being stuck in Guide indefinitely.
Once information is sufficient, api/routes/chat.py calls run_analysis_pipeline() to trigger the subsequent four Agents, no longer running the full Guide subgraph.
5. Single-turn Mode vs Subgraph Mode
When the frontend engages in multi-turn chat, it often goes through run_guide_single_turn (agents/guide.py): each time the user sends a message, System + history + user are sent to the LLM, without the welcome→… chain. Sufficiency uses a keyword counting heuristic (found_keywords >= 6 or >=4 and text >=50 chars), different from the LLM stop decision in the subgraph:
For the first message or CLI, ainvoke(create_guide_graph()) can still run the full five-node flow.
Selection rationale:
Mode
Applicability
Trade-off
Subgraph
First message, needs automatic stage progression
Multiple LLM calls
Single-turn
SSE dialogue with existing multi-turn history
Weak stage sense, relies on System Prompt discipline
6. Prompt Writing Tips (Retrospective)
Put stage instructions in user messages; System only holds stable personality and rules, making it easy to change stages without altering character.
Avoid asking too many questions at once: Hard-code “at most 1–2 questions per turn” in System.
Empathize with negative emotions first: Write into System to reduce the model’s urge to collect fields immediately.
Sufficiency check separate from reply generation: Prevents the model from saying “enough info” in its chat reply while state hasn’t been updated.
7. Pitfalls
Inconsistent stage names: System Prompt text says confirm, but the graph node is check_sufficiency — during maintenance, use agents/guide.py nodes as the source of truth.
Double loop counting: The subgraph should_continue uses len(messages) as loop_count (max 8); the outer route_after_guide uses user_msg_count >= 3. Log separately during testing.
Light model not used in Guide: Guide always uses get_chat_model() (llm/providers.py); don’t mistakenly document it as light model.
Different stop criteria in single-turn vs subgraph: SSE path uses keyword heuristic; subgraph uses LLM sufficient/insufficient; the same user might get inconsistent “sufficiency” conclusions across the two modes.
collected_raw concatenation: collect_basic_info / dig_deeper only append the last 3 user messages; a very long history may lose early critical info — rely on conversation_history being fully restored in the Parser stage.
8. Summary
The Guide’s Prompt strategy = GUIDE_SYSTEM_PROMPT sets the tone + staged templates + structured check_sufficiency stop + should_continue to prevent infinite loops.
Next article: Docker Deployment Pitfalls — PyTorch CPU, Chinese fonts, and frontend build.
Appendix: Key Source Code (Annotated Line by Line)
The following code is excerpted from iCan’s implementation. Each line has a Chinese comment above it so you can follow along even without the public repository. Generated by: python3 bin/build-ican-annotated-snippets.py
# L2: [Doc] File description: Centralized management module for Prompt templates # L3: [Doc] Business description: Centralizes all Agent Prompt templates in the iCan project, including: # L4: [Doc] - GuideAgent (dialogue guidance): Multi-stage dialogue strategy, progressive user info mining # L5: [Doc] - ResumeParserAgent (resume parsing): Extracts structured info from user text # L6: [Doc] - ProfileAnalyzerAgent (profile analysis): Five-dimension deep analysis based on resume # L7: [Doc] - CareerMatcherAgent (career matching): Three-level career matching based on profile # L8: [Doc] - ReporterAgent (report generation): Integrates analysis results into structured report text # L9: [Doc] All Prompts follow PRD Chapter 9 Prompt design points: role setting, task description, # L10: [Doc] output format, constraints, example guidance. # L11: [Doc] Data flow: This module is imported by each Agent → fills variables → sends to LLM # (L1-12 are function/module docstrings, converted to comments for readability)
# L18: Assignment: Update local variable or state field GUIDE_SYSTEM_PROMPT = """You are a senior career planner with 20 years of experience, named "Xiao C". You possess the dual qualities of a psychological counselor and career consultant, excelling at deeply understanding a person's career development needs through natural conversation.
# L20: Your core abilities # L21: Execute the statement (details in business description above) 1. **Career planning expertise**: Proficient in career development paths, market trends, job requirements across industries # L22: Execute the statement (details in business description above) 2. **Psychological counseling ability**: Good at listening, empathizing, able to recognize users' emotional states and deep needs # L23: Execute the statement (details in business description above) 3. **Information mining ability**: Through progressive questioning, systematically collect users' key information # L24: Execute the statement (details in business description above) 4. **Analytical judgment ability**: Based on collected information, quickly form a preliminary profile of the user
# L26: Dialogue strategy # L27: Execute the statement (details in business description above) - **Progressive mining**: Start with open-ended questions, gradually focus on specific details, avoid asking too many structured questions upfront # L28: Execute the statement (details in business description above) - **Emotion recognition**: Pay attention to users' emotional changes, give encouragement and recognition at appropriate times, build trust # L29: Execute the statement (details in business description above) - **Flexible response**: Adjust the conversation direction based on the user's answers, don't mechanically follow a fixed process # L30: Execute the statement (details in business description above) - **Information completion**: Skillfully guide users to provide missing key information in natural conversation # L31: Execute the statement (details in business description above) - **Summary confirmation**: Proactively summarize collected information at key nodes to ensure accurate understanding
# L33: Dialogue stages # L34: Execute the statement (details in business description above) You automatically determine the current stage based on conversation progress: # L35: Execute the statement (details in business description above) 1. **greeting (Opening greeting)** : Warmly welcome the user, briefly introduce services, understand the user's basic intent # L36: Execute the statement (details in business description above) 2. **assess_need (Needs assessment)** : Determine if the user's core need is career planning, transition consultation, resume optimization, etc. # L37: Execute the statement (details in business description above) 3. **collect_basic_info (Basic info collection)** : Learn about the user's education background, years of experience, current position, etc. # L38: Execute the statement (details in business description above) 4. **dig_deeper (Deep mining)** : Deeply understand the user's core skills, career achievements, work preferences, values, etc. # L39: Execute the statement (details in business description above) 5. **confirm (Confirmation summary)** : Summarize all collected information, confirm with the user, then prepare for analysis report generation
# L41: Important rules # L42: Execute the statement (details in business description above) - Always reply in Chinese # L43: Execute the statement (details in business description above) - Tone should be warm and professional, like an experienced friend # L44: Execute the statement (details in business description above) - Keep each reply within 200 characters to maintain conversation rhythm # L45: Execute the statement (details in business description above) - Don't ask too many questions at once; at most 1-2 questions per turn # L46: Execute the statement (details in business description above) - If the user's answer is vague, use specific scenarios to guide # L47: Execute the statement (details in business description above) - If the user expresses negative emotions, provide emotional support first before continuing guidance # L48: Execute the statement (details in business description above) - When enough information is collected, proactively suggest entering the analysis stage # L51: [Doc] GUIDE_STAGE_TEMPLATES = { # L52: [Doc] "greeting": ( # L53: [Doc] "The user has just started the conversation. Please warmly welcome the user, briefly introduce the career planning services you can provide, " # L54: [Doc] "and use a relaxed open-ended question to understand the user's intent.\n\n" # L55: [Doc] "Example opening: Hi! I'm Xiao C, your personal career planning advisor. I have 20 years of career planning experience " # L56: [Doc] "and have helped thousands of professionals find their direction. What would you like to talk about today? Have you encountered a career bottleneck, " # L57: [Doc] "or are you considering a new direction?" # L58: [Doc] ), # L59: [Doc] "assess_need": ( # L60: [Doc] "The user has started the conversation. Use 1-2 precise questions to determine the user's core need category:\n" # L61: [Doc] "- Career planning: Needs to clarify career direction or make a development plan\n" # L62: [Doc] "- Career transition: Wants to change industry or position\n" # L63: [Doc] "- Resume optimization: Needs to optimize job application materials\n" # L64: [Doc] "- Skill improvement: Wants to know which abilities need to be supplemented\n" # L65: [Doc] "- Workplace confusion: Encountered specific work difficulties\n\n" # L66: [Doc] "Based on the user's response, naturally guide the conversation deeper. Don't directly ask 'What service do you need?' " # L67: [Doc] "but infer from the conversation content." # L68: [Doc] ), # L69: [Doc] "collect_basic_info": ( # L70: [Doc] "The user's basic needs are understood. Now collect the following basic information naturally in conversation (don't ask all at once):\n" # L71: [Doc] "1. Education background (degree, major, school)\n" # L72: [Doc] "2. Years of work experience and industry experience\n" # L73: [Doc] "3. Current/recent position and responsibilities\n" # L74: [Doc] "4. Core skills and tech stack\n" # L75: [Doc] "5. Important career achievements or project experience\n\n" # L76: [Doc] "Note: Obtain this information as naturally as a chat, not like an interview asking item by item." # L77: [Doc] "Start from the topic the user is most willing to discuss." # L78: [Doc] ), # L79: [Doc] "dig_deeper": ( # L80: [Doc] "Basic information has been mostly collected. Now dive deeper into the following dimensions:\n"
# L21: Async function welcome: can be awaited, suitable for IO-bound LLM/DB calls asyncdefwelcome(state: GuideState) -> dict: # L23: [Doc] Welcome node: Generates welcome message and initial guidance. # L25: [Doc] Function description: # L26: [Doc] Based on the greeting stage template in the System Prompt, calls the LLM to generate # L27: [Doc] a warm welcome message and an open-ended guiding question to start the conversation with the user. # L29: [Doc] Input parameter description: # L30: [Doc] state (GuideState): Dialogue guidance state object, containing conversation history, current stage, etc. # L32: [Doc] Output parameter description: # L33: [Doc] dict: State update dictionary, including messages update and current_stage update. # (L22-34 are function/module docstrings, converted to comments for readability) # L35: Start try block, except handles fallback try: # L36: Log for debugging node input/output logger.info("[welcome] Starting execution, input: state=%s", {k: str(v)[:100] for k, v in state.items()}) # L37: Multi-turn dialogue list, each element is {role, content} conversation_history = state.get("conversation_history", []) # L38: Assignment: Update local variable or state field stage_template = GUIDE_STAGE_TEMPLATES.get("greeting", "")
# L40: Assignment: Update local variable or state field messages = [ # L41: Execute the statement (details in business description above) {"role": "system", "content": GUIDE_SYSTEM_PROMPT}, # L42: Execute the statement (details in business description above) {"role": "user", "content": stage_template}, # L43: Execute the statement (details in business description above) ] # L44: If there is conversation history, append to messages # L45: Multi-turn dialogue list, each element is {role, content} for msg in conversation_history: # L46: Execute the statement (details in business description above) messages.append(msg)
# L48: Log for debugging node input/output logger.info("[welcome] Calling LLM to generate welcome message, message count: %d", len(messages)) # L49: Get the chat model instance (configured from settings.LLM_MODEL_CHAT) model = get_chat_model() # L50: Call LLM to return plain text, with 60s timeout and Qwen3 /no_think injection reply = await invoke_llm(model, messages)
# L52: Log for debugging node input/output logger.info("[welcome] LLM reply length: %d", len(reply) if reply else0) # L53: Assignment: Update local variable or state field result = { # L54: Execute the statement (details in business description above) "messages": [reply], # L55: Execute the statement (details in business description above) "current_stage": "assess_need", # L56: Execute the statement (details in business description above) } # L57: Log for debugging node input/output logger.info("[welcome] Execution complete, output: current_stage=assess_need, reply length=%d", len(reply) if reply else0) # L58: Return fields to be merged into state (LangGraph will merge) return result
# L60: Catch exceptions to avoid crashing the entire graph/request except Exception as e: # L61: Log for debugging node input/output logger.error("[welcome] Welcome node execution exception: %s", e, exc_info=True) # L62: Return fields to be merged into state (LangGraph will merge) return { # L63: Execute the statement (details in business description above) "messages": ["Hi! I'm Xiao C, your personal career planning advisor. What would you like to talk about today?"], # L64: Execute the statement (details in business description above) "current_stage": "assess_need", # L65: Execute the statement (details in business description above) }
# L251: Async function check_sufficiency: can be awaited, suitable for IO-bound LLM/DB calls asyncdefcheck_sufficiency(state: GuideState) -> dict: # L253: [Doc] Information sufficiency check node: Determines whether enough information has been collected. # L255: [Doc] Function description: # L256: [Doc] Comprehensively evaluates collected user information to determine if it is sufficient for the next step # L257: [Doc] of resume parsing and profile analysis. Checks if key fields are complete, # L258: [Doc] and sets the is_info_sufficient flag. # L260: [Doc] Input parameter description: # L261: [Doc] state (GuideState): Dialogue guidance state object, containing collected info and missing fields. # L263: [Doc] Output parameter description: # L264: [Doc] dict: State update dictionary, sets is_info_sufficient and missing_fields. # (L252-265 are function/module docstrings, converted to comments for readability) # L266: Start try block, except handles fallback try: # L267: Log for debugging node input/output logger.info("[check_sufficiency] Starting execution, input: state=%s", {k: str(v)[:100] for k, v in state.items()}) # L268: Assignment: Update local variable or state field collected_info = state.get("collected_info", {}) # L269: Multi-turn dialogue list, each element is {role, content} conversation_history = state.get("conversation_history", [])
# L271: Define key information fields # L272: Assignment: Update local variable or state field required_keys = ["collected_raw"] # L273: Assignment: Update local variable or state field optional_keywords = [ # L274: Execute the statement (details in business description above) "year", "industry", "position", "job", "skill", "experience", # L275: Execute the statement (details in business description above) "company", "major", "education", "direction", "expectation", "confusion", # L276: Execute the statement (details in business description above) ]
# L278: Check if conversation history contains key information # L279: Assignment: Update local variable or state field all_user_text = " ".join( # L280: Multi-turn dialogue list, each element is {role, content} msg.get("content", "") for msg in conversation_history if msg.get("role") == "user" # L281: Execute the statement (details in business description above) ) # L282: Assignment: Update local variable or state field found_keywords = [kw for kw in optional_keywords if kw in all_user_text]
# L284: Use LLM for more precise sufficiency judgment # L285: Assignment: Update local variable or state field sufficiency_prompt = ( # L286: Execute the statement (details in business description above) f"Based on the following collected user information and dialogue content, determine whether enough information has been gathered for career analysis.\n" # L287: Execute the statement (details in business description above) f"Collected info: {collected_info}\n" # L288: Execute the statement (details in business description above) f"User dialogue text: {all_user_text[:1000]}\n" # L289: Execute the statement (details in business description above) f"Keywords found: {found_keywords}\n\n" # L290: Execute the statement (details in business description above) f"Please answer only 'sufficient' or 'insufficient', with a brief reason." # L291: Execute the statement (details in business description above) )
# L293: Assignment: Update local variable or state field messages = [ # L294: Execute the statement (details in business description above) {"role": "system", "content": "You are an information completeness evaluation assistant. Judge whether the given information is sufficient."}, # L295: Execute the statement (details in business description above) {"role": "user", "content": sufficiency_prompt}, # L296: Execute the statement (details in business description above) ]
# L298: Log for debugging node input/output logger.info("[check_sufficiency] Calling LLM for information sufficiency check") # L299: Get the chat model instance (configured from settings.LLM_MODEL_CHAT) model = get_chat_model() # L300: Call LLM to return plain text, with 60s timeout and Qwen3 /no_think injection reply = await invoke_llm(model, messages)
# L302: Assignment: Update local variable or state field is_sufficient = "sufficient"in reply.lower() # L303: Assignment: Update local variable or state field missing = [] # L304: Conditional branch ifnot is_sufficient: # L305: Calculate missing information dimensions # L306: Conditional branch ifnotany(kw in all_user_text for kw in ["year", "experience"]): # L307: Execute the statement (details in business description above) missing.append("Work years/experience") # L308: Conditional branch ifnotany(kw in all_user_text for kw in ["industry", "company"]): # L309: Execute the statement (details in business description above) missing.append("Industry info") # L310: Conditional branch ifnotany(kw in all_user_text for kw in ["position", "job"]): # L311: Execute the statement (details in business description above) missing.append("Current position") # L312: Conditional branch ifnotany(kw in all_user_text for kw in ["skill", "tech"]): # L313: Execute the statement (details in business description above) missing.append("Core skills")
# L315: Log for debugging node input/output logger.info( # L316: Assignment: Update local variable or state field "[check_sufficiency] Sufficiency check result: is_sufficient=%s, missing_fields=%s, LLM reply length=%d", # L317: Execute the statement (details in business description above) is_sufficient, missing, len(reply) if reply else0, # L318: Execute the statement (details in business description above) )
# L320: Assignment: Update local variable or state field result = { # L321: Guide determines if user info is sufficient to enter analysis stage "is_info_sufficient": is_sufficient, # L322: Execute the statement (details in business description above) "missing_fields": missing, # L323: Execute the statement (details in business description above) } # L324: Guide determines if user info is sufficient to enter analysis stage logger.info("[check_sufficiency] Execution complete, output: is_info_sufficient=%s, missing_fields=%s", is_sufficient, missing) # L325: Return fields to be merged into state (LangGraph will merge) return result
# L327: Catch exceptions to avoid crashing the entire graph/request except Exception as e: # L328: Log for debugging node input/output logger.error("[check_sufficiency] Information sufficiency check node execution exception: %s", e, exc_info=True) # L329: On exception, default to insufficient information, continue conversation # L330: Return fields to be merged into state (LangGraph will merge) return { # L331: Guide determines if user info is sufficient to enter analysis stage "is_info_sufficient": False, # L332: Execute the statement (details in business description above) "missing_fields": ["Check process exception, needs reassessment"], # L333: Execute the statement (details in business description above) }