0. 系列闭环(不公开源码也能跟读)

端到端链路:Vue 前端 → api/routes/chat.py → Guide 多轮 SSE → run_analysis_pipeline(解析→分析→匹配→报告)→ tools/pdf_exporter PDF。
本篇:第 1/17 篇 · 总览 · 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
说明
读本篇前 无(建议从此篇开始)
读完本篇 说出 FastAPI 接入层、LangGraph 五节点、两条执行路径(HTTP vs CLI)
下一环 第 02 篇:五个 node 函数如何串起来(第 2 篇)

全系列闭环索引:SERIES-LOOP.md

1. 要解决什么问题

常见职业规划产品有两类极端:

  • 纯问卷:霍兰德 120 题,填完得一个代码,缺少对你履历与困惑的上下文。
  • 纯聊天:一个大 Prompt 包打天下,解析、测评、匹配、写报告挤在一次调用里,输出不稳定。

iCan 的做法是:多轮对话采集上下文 → 结构化画像 → 分 Agent 做分析与匹配 → 生成可下载 PDF 报告
后端用 FastAPImain.py 挂载 api/routes/chat.py 等路由)提供 API 与 SSE/WebSocket,编排用 LangGraph StateGraphworkflow.pycreate_workflow())把 5 段流水线串起来。

下文说明架构与选型;关键逻辑会贴代码片段便于对照理解。


2. 技术选型(为什么不是「一个超级 Prompt」)

层次 选型 在本项目里的原因
Web FastAPI LLM 调用是 IO 密集;异步路由 + SSE 流式回复体验更好
编排 LangGraph Guide 要循环追问,后面四段要线性执行;图结构比 LCEL 链好表达
LLM ChatOpenAI 兼容 API 同一套 invoke_llm;换 DeepSeek / Ollama 只改 .envbase_urlmodel
报告 ReportLab + matplotlib 中文 PDF、雷达图/柱状图可控,不依赖浏览器打印
前端 Vue 3 + Vite 对话页 + 报告生成进度 + PDF 下载

没有选 CrewAI / AutoGen 的原因:当前流程是固定 DAG(Guide 条件环 + 四段顺序),LangGraph 的 add_conditional_edges 足够,且状态 TypedDict 清晰。


3. 系统架构

3.1 总览(draw.io)

iCan 系统架构:FastAPI 接入层、LangGraph 五 Agent 流水线、LLM/DB/PDF 工具层

3.2 数据流(Mermaid)

1
2
3
4
5
6
7
8
9
10
11
flowchart TB
U[用户 / Vue 前端] --> API[FastAPI routes]
API --> G[guide_node]
G --> R{route_after_guide}
R -->|信息不足| G
R -->|信息充分| P[resume_parser_node]
P --> A[profile_analyzer_node]
A --> M[career_matcher_node]
M --> T[reporter_node]
T --> PDF[ReportLab PDF]
API -.SSE/WS.-> U

顶层状态类型:iCanWorkflowStatecore/state.py)。Guide 另有内层 GuideState 与子图 create_guide_graph(),第 6 篇专讲嵌套。


4. 顶层工作流代码(核心 30 行)

实现位置:workflow.pycreate_workflow()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
graph = StateGraph(iCanWorkflowState)
graph.add_node("guide_node", guide_node)
graph.add_node("resume_parser_node", resume_parser_node)
graph.add_node("profile_analyzer_node", profile_analyzer_node)
graph.add_node("career_matcher_node", career_matcher_node)
graph.add_node("reporter_node", reporter_node)

graph.set_entry_point("guide_node")
graph.add_conditional_edges(
"guide_node",
route_after_guide,
{"guide_node": "guide_node", "resume_parser_node": "resume_parser_node"},
)
graph.add_edge("resume_parser_node", "profile_analyzer_node")
graph.add_edge("profile_analyzer_node", "career_matcher_node")
graph.add_edge("career_matcher_node", "reporter_node")
graph.add_edge("reporter_node", END)
return graph.compile()

route_after_guide 读 Guide 返回的 needs_more_info 等字段,决定继续对话还是进入解析链路(配合循环上限,见第 07 篇)。


5. 五个 Agent 分工

节点 模块 输入 → 输出 说明
guide_node agents/guide.py 对话历史 → collected_info、是否充分 内层 5 步:welcome → … → check_sufficiency
resume_parser_node agents/resume_parser.py 原始文本 → 结构化 JSON 画像 使用 get_light_model() + JSON 降级解析
profile_analyzer_node agents/profile_analyzer.py 画像 → 能力/性格/价值观 + RIASEC 多节点子图,第 3 篇讲霍兰德打分
career_matcher_node agents/career_matcher.py 画像 → 三级路径推荐 纵向 / 横向 / 转型三档
reporter_node agents/reporter.py 分析结果 → Markdown 报告 再经 tools/pdf_exporter.py 出 PDF

注意:实现里是 async 函数 + 子 StateGraph,不是五个 Python 类。


6. LLM 怎么接(别写死「只用 DeepSeek」)

llm/providers.py 统一封装:

  • get_chat_model() / get_light_model()ChatOpenAI(api_key, base_url, model=...)
  • invoke_llm / invoke_llm_with_json → 普通文本与 JSON 模式

配置来自环境变量config.py + pydantic-settings):

场景 典型配置
本地开发 .env LLM_MODEL_CHAT=deepseek-v4-flashLLM_BASE_URL=https://api.deepseek.com/v1
Docker + Ollama qwen3.5:9b + http://host.docker.internal:11434/v1(Qwen3 需 /no_think 注入)
代码默认值 未配 env 时回落 gpt-4o(部署务必显式写 .env

第 8 篇展开双模型策略;当前 ResumeParser 走 light model,Reporter 仍走 chat model(与部分早期文档不一致,以代码为准)。


7. 部署要点(Docker)

多阶段 Dockerfile:Node 构建前端静态资源 → Python 镜像安装依赖 → uvicorn ican.main:app

踩坑集中在第 15 篇,这里只列三条:

  1. 先装 CPU 版 PyTorch,避免 sentence-transformers 拖入 CUDA 大包。
  2. **镜像内装 fonts-noto-cjk**,否则 PDF 中文方块。
  3. 前端 dist 单独 COPY,不要把宿主机 node_modules 打进镜像。

8. 目录结构(便于按篇跳转)

1
2
3
4
5
6
7
8
9
10
11
ican/
├── main.py # FastAPI 入口、lifespan、路由挂载
├── workflow.py # 顶层 LangGraph(本篇重点)
├── config.py # pydantic-settings
├── agents/ # 五个 Agent 子图
├── llm/ # providers / parsers / prompts
├── api/routes/ # chat、report、upload、ws
├── tools/ # pdf_exporter、doc_reader
└── db/ # SQLAlchemy + 自动迁移(第 10 篇)
frontend/ # Vue 3
Dockerfile / docker-compose.yml

9. 踩坑记录(对照源码)

  1. 代码默认模型不是 DeepSeek
    config.pyLLM_MODEL_CHAT 默认 gpt-4oLLM_BASE_URL 默认 OpenAI 官方地址。本地常用 deepseek-v4-flash.env 部署配置,不是框架写死;漏配 env 会直接连错端点。

  2. 线上对话与 create_workflow() 不是同一条路
    SSE 多轮聊天走 workflow.pyrun_guide_chat()(内部调 agents/guide.pyrun_guide_single_turn);信息充分后由 api/routes/chat.py 触发 run_analysis_pipeline(),直接串 resume_parserprofile_analyzercareer_matcherreporter,而不是每次 ainvoke 完整顶层图。CLI 或一次性跑通才用 run_workflow() + create_workflow()

  3. PlannerState 已定义但未接入
    core/state.py 里有 PlannerState,顶层 workflow.py 没有 Planner 节点;行动规划内容目前合并在 agents/reporter.py 的报告章节里,文章勿写「六 Agent」。

  4. 顶层状态用 workflow_messages 累积,不是 messages
    iCanWorkflowState 的 Reducer 字段是 workflow_messages: Annotated[list[str], operator.add]messages 只出现在内层 GuideState,混用会导致调试时看错日志字段。

  5. 双模型分工以代码为准
    llm/providers.py 模块注释曾描述 Reporter 可走 mini 档模型,但 agents/reporter.py 当前章节生成仍调 get_chat_model();只有 agents/resume_parser.py 确定走 get_light_model()

  6. Ollama 不可用时的降级路径
    run_analysis_pipeline() 会先 check_ollama_available();失败则走 workflow.py_regex_quick_profile + _generate_fallback_report,报告质量明显下降,需在运维层保证 LLM 可达。


10. 系列导读

主题
1 系统全景(本篇)
2 五 Agent 协作与 iCanWorkflowState
3 霍兰德 RIASEC + OpenAI 兼容 API 部署示例
4–7 LangGraph 状态、路由、嵌套、容错
8–11 FastAPI 集成、SSE/WS、DB、PDF
12–14 Prompt 与 JSON 稳定输出
15–17 Docker、中间件、配置管理

11. 小结

iCan 的核心不是「换一个更大的模型」,而是 用 LangGraph 把不确定的 LLM 步骤拆成可测试的节点,用 FastAPI 承接流式交互,用 ReportLab 交付可留存 PDF。

下一篇:LangGraph 多 Agent 编排 — 五个子图如何衔接、route_after_guide 与 Guide 内层循环如何配合。


← 返回 iCan 专题