0. 系列闭环(不公开源码也能跟读)
端到端链路:Vue 前端 → api/routes/chat.py → Guide 多轮 SSE → run_analysis_pipeline(解析→分析→匹配→报告)→ tools/pdf_exporter PDF。
本篇:第 4/17 篇 · 数据环 · 状态 TypedDict
| 阶段 | 用户可见 | 代码入口 | 对应篇 |
|---|---|---|---|
| 建会话 | 欢迎语 | 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 |
| 说明 | |
|---|---|
| 读本篇前 | 第 02 篇各 node 返回值 |
| 读完本篇 | 区分外层 iCanWorkflowState 与内层 GuideState |
| 下一环 | 第 05 篇:用 needs_more_info 做路由(第 5 篇) |
全系列闭环索引:SERIES-LOOP.md
一、LangGraph 的状态传递机制
LangGraph 的核心理念是状态驱动。每个节点接收 core/state.py 里定义的 TypedDict,处理完后返回部分字段更新,LangGraph 自动合并到全局状态。
1 | |
这个机制的关键问题是:如何合并?
二、TypedDict 定义 Agent 状态
实现位置:core/state.py。LangGraph 使用 Python 的 TypedDict 定义状态结构:
1 | |
total=False 表示所有字段都是可选的(节点可以只返回需要更新的字段)。
三、Annotated[list, operator.add] — Reducer 详解
默认行为:覆盖
如果没有 Reducer,LangGraph 的默认行为是新值覆盖旧值:
1 | |
这对于 current_agent、needs_more_info 这类”只有一个最新值”的字段是正确的。
Reducer 行为:累积
1 | |
Annotated[list[str], operator.add] 告诉 LangGraph:这个字段用 operator.add(列表 +)合并。
1 | |
为什么 Guide 内层 messages 必须用 Reducer
在 agents/guide.py 的多轮子图里,welcome / assess_need / collect_basic_info / dig_deeper 每个节点都会 return {"messages": [reply]}。如果不用 Reducer:
1 | |
四、如何选择覆盖 vs 累积
选择原则
| 字段特征 | 使用方式 | 例子 |
|---|---|---|
| 只有一个最新值 | 直接赋值(覆盖) | current_agent, needs_more_info |
| 需要历史记录 | Annotated + operator.add | GuideState.messages, workflow_messages |
| 逐步填充的字典 | 直接赋值(覆盖整个 dict) | structured_profile, personal_profile |
| 累积的列表 | Annotated + operator.add | messages(内层), workflow_messages(外层) |
常见错误
错误 1:conversation_history 用了 Reducer 但它是 dict 列表
1 | |
原因:conversation_history 包含 user 和 assistant 的消息,需要手动控制追加顺序(先 user 后 assistant),不能让 LangGraph 自动合并。
错误 2:dict 类型用了 Reducer
1 | |
五、分层状态设计
iCan 项目在 core/state.py 采用外层 + 内层分层:顶层 iCanWorkflowState 与各 Agent 的 GuideState / ProfileAnalysisState / CareerMatchState / ReporterState 等。
注意:PlannerState 也在 core/state.py 中定义,但 workflow.py 尚未接入 Planner 节点,勿在状态流转图里画第六段 Agent。
外层状态(core/state.py — iCanWorkflowState)
1 | |
内层状态(core/state.py — GuideState)
1 | |
为什么分层
- 职责隔离:Guide 的内部字段(如
emotion_state、missing_fields)定义在GuideState,不会进入iCanWorkflowState - 可独立测试:
run_guide_agent(guide_state)/run_profile_analyzer(analyzer_state)可脱离顶层图单测 - 数据转换:
workflow.py各*_node手动做内外层映射
数据转换示例(对照 workflow.py)
1 | |
profile_analyzer_node 同理:从外层取 structured_profile,构造 ProfileAnalysisState,调 run_profile_analyzer(),再把结果组装进 personal_profile。
六、其他 Reducer 的用法
除了 operator.add,还可以用其他 Reducer:
1 | |
七、踩坑记录
- **外层误用
messages**:只有GuideState.messages带 Reducer;顶层是workflow_messages。审计脚本与日志排查都应以core/state.py为准。 conversation_history不要加 Reducer:user/assistant 顺序由workflow.py的guide_node手动 append,自动合并会破坏对话结构。- **
ProfileAnalysisState.analysis_messages**:内层分析过程消息可累积,但外层只取结构化字段(ability_model、riasec_scores等),不要整包return guide_result。 - **初始
needs_more_info**:create_initial_workflow_state()默认为False,首次进入guide_node后才会被设为 True/False;写测试时注意初始值。
八、状态污染的防范
问题场景
如果 Guide Agent 的内部状态(如 emotion_state)意外出现在外层状态中,下游节点可能会错误地读取它。
防范措施
- 类型校验:TypedDict 严格定义每个状态的字段,未定义的字段不会出现
- 手动映射:节点函数只返回需要更新的字段,不透传无关字段
- 独立状态类型:每个 Agent 有自己的 TypedDict,编译时检查字段
九、小结
LangGraph 状态管理在 iCan 中的落地要点:
- 所有 TypedDict 集中在
core/state.py - 默认覆盖,适用于
current_agent、needs_more_info等单值字段 - Annotated + Reducer 用于
GuideState.messages与iCanWorkflowState.workflow_messages - 分层 + 手动映射:
workflow.py的*_node是内外层的唯一转换层 PlannerState已定义未接入,扩展时勿与现有五段流水线混淆
下一篇:
workflow.py的条件路由 —route_after_guide与 Guide 内层should_continue如何配合。