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

端到端链路:Vue 前端 → api/routes/chat.py → Guide 多轮 SSE → run_analysis_pipeline(解析→分析→匹配→报告)→ tools/pdf_exporter PDF。
本篇:第 13/17 篇 · Prompt 环 · RIASEC

阶段 用户可见 代码入口 对应篇
建会话 欢迎语 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
说明
读本篇前 第 03 篇 analyze_riasec
读完本篇 对照 Prompt 与解析器输出字段
下一环 第 14 篇:Guide 五阶段 Prompt(第 14 篇)

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

1. 痛点:量表逻辑 vs 对话推断

传统 SDS 问卷有 120 道固定题目,用户逐题打分,系统统计六维度得分。
iCan 走的是另一条路:用户在 Guide 对话里自然描述履历与偏好,ProfileAnalyzer 的 analyze_riasec 节点再从结构化画像推断 R/I/A/S/E/C。

难点在于:LLM 容易「凭感觉给分」,缺少依据时仍输出精确数字,下游 CareerMatcher 会把错误代码当真。

本篇说明 Prompt 如何把心理学维度定义写清楚,并约束「评分 + 理由 + 信息不足时声明范围」。


2. 在流水线中的位置

实现入口:agents/profile_analyzer.pycreate_profile_analyzer_graph()。子图在能力、风格、性格、价值观分析之后进入 analyze_riasec,结果经 workflow.pyprofile_analyzer_node 写入外层 personal_profile.riasec_scores

1
2
3
4
5
6
7
8
9
10
11
12
flowchart LR
P[structured_profile] --> L[load_profile]
L --> AB[analyze_abilities]
AB --> WS[infer_work_style]
WS --> PT[infer_personality]
PT --> AV[analyze_values]
AV --> AR[analyze_riasec]
AR --> SW[identify_strengths_weaknesses]
SW --> SY[synthesize_profile]
AR --> J[invoke_llm_with_json]
J --> R[riasec_scores]
R --> M[career_matcher_node]

各节点均使用 get_chat_model()llm/providers.py),System Prompt 共用 llm/prompts.pyPROFILE_ANALYZER_SYSTEM_PROMPT

RIASEC Prompt 结构


3. Prompt 结构:系统模板 + 动态 user 内容

RIASEC 规则写在 llm/prompts.pyPROFILE_ANALYZER_SYSTEM_PROMPT 第六节,与能力模型、大五人格等共用一份 System Prompt,避免各节点 Prompt 分散难维护。

核心片段(节选):

1
2
3
4
5
6
7
8
9
10
11
12
### 6. 霍兰德 RIASEC 分析
基于约翰·霍兰德的职业兴趣理论,分析用户在六个维度的倾向:
- R(Realistic)现实型:喜欢操作、实践、动手
- I(Investigative)研究型:喜欢分析、探索、研究
# ... A/S/E/C 定义 ...

每个维度给出 0-10 分的评分,并标注最高的 2-3 个维度作为用户的"霍兰德代码"

## 重要规则
- 评分要有依据,不要随意打分,每个评分都要在 analysis 中说明理由
- 霍兰德分析要结合用户的工作经历和技能来推断,而不是凭空猜测
- 如果信息不足以做出准确判断,在分析中说明并给出可能的范围

analyze_riasec 节点把 structured_profile 截断到约 2000 字符,拼进 user 消息,再调用 invoke_llm_with_jsonllm/providers.py):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# agents/profile_analyzer.py — analyze_riasec
async def analyze_riasec(state: ProfileAnalysisState) -> dict:
structured_profile = state.get("structured_profile", {})
profile_summary = json.dumps(structured_profile, ensure_ascii=False)[:2000]
messages = [
{"role": "system", "content": PROFILE_ANALYZER_SYSTEM_PROMPT},
{"role": "user", "content": (
f"请基于以下结构化履历信息,分析用户的霍兰德 RIASEC 职业兴趣。\n"
f"对六个维度进行评分(0-10分)...\n\n履历信息:\n{profile_summary}"
)},
]
riasec_data = await invoke_llm_with_json(get_chat_model(), messages)
riasec_scores = {k: float(riasec_data.get(k, 0)) for k in "RIASEC"}
return {"riasec_scores": riasec_scores}

invoke_llm_with_jsonresponse_format 被 Ollama 忽略,可回退到 llm/parsers.pyparse_riasec_scores() 从纯文本提取六维分数。

注意:RIASEC 与五维分析在同一份 JSON Schema 里定义,但运行时由独立节点调用,便于单独重试或替换模型。


4. 期望 JSON 形状与 Holland Code

模型应返回嵌套在完整画像 JSON 中的 riasec 块:

1
2
3
4
5
6
7
{
"riasec": {
"R": 6, "I": 9, "A": 3, "S": 5, "E": 7, "C": 4,
"holland_code": "IEA",
"analysis": "I 维度:用户多次提到架构设计与技术选型……"
}
}

Holland Code 取得分最高的 2–3 个字母。实现里 LLM 返回的 holland_code 在 JSON 中,但 analyze_riasec 只持久化 R–C 六维 float 到 riasec_scores;若 JSON 解析失败,节点退回全零分字典,避免整个子图崩溃(见第 07 篇容错)。tools/pdf_exporter.pyriasec_scores 的六键画柱状图,不依赖 holland_code 字段。


5. 与 CareerMatcher 的衔接

agents/career_matcher.pygenerate_candidate_paths 读取 personal_profile(含 riasec_scores),配合 llm/prompts.pyCAREER_MATCHER_SYSTEM_PROMPT 生成三级路径。Prompt 里要求结合霍兰德代码解释推荐,而不是维护静态映射表:

1
2
3
基于用户的霍兰德代码 IRS,在推荐中说明:
- 哪些方向与 I/R 一致(如技术架构、数据工程)
- 哪些方向是 stretch(如纯销售型 E 向岗位)

workflow.pycareer_matcher_noderecommended_paths 写入外层 career_matches,供 agents/reporter.py 写进报告。


6. 与传统问卷的边界(写进产品文案)

维度 SDS 问卷 iCan LLM 评估
标准化 中(依赖对话质量)
效度 长期验证 无正式量表效度
体验 120 题 多轮对话
适用 正式测评 探索与报告辅助

对外表述应明确:对话推断不能替代标准化职业测评,适合作为职业规划讨论的起点。


7. 踩坑记录

  1. 画像太薄仍给高分agents/resume_parser.py 若只抽到岗位名,RIASEC 会胡编。解决:Guide 阶段尽量收集行业/技能/偏好(第 14 篇);Parser 输出的 confidence_scores(非 parsing_confidence)可供感知解析质量。
  2. Holland Code 字母顺序:有的模型输出 EIA 而非按分数排序;当前 MVP 依赖 Prompt 约束,未在后处理重排。
  3. 与 PDF 图表字段不一致tools/pdf_exporter.py 柱状图读 riasec_scores 扁平 dict,key 必须为 RC 单字母大写,与 analyze_riasec 写入格式一致。
  4. json_object 不支持:Ollama 部分模型忽略 response_format,必须走第 12 篇四层 llm/parsers.pyparse_json_from_text / parse_riasec_scores
  5. System Prompt 第六节与节点 user Prompt 重复PROFILE_ANALYZER_SYSTEM_PROMPT 已含 RIASEC 定义,analyze_riasec 的 user 消息再次列维度——有意加强约束,修改时需两处同步。

8. 小结

霍兰德 Prompt 的关键不是背六维度定义,而是:绑定 structured_profile、强制理由、承认信息不足、输出 holland_code 给下游 Matcher

下一篇:Guide 五阶段对话 PromptGUIDE_SYSTEM_PROMPTcheck_sufficiency 双轨判断。


附录:关键源码(逐行注释)

以下代码摘自 iCan 实现,每行上方均有中文注释,不公开仓库也可跟读。
生成命令:python3 bin/build-ican-annotated-snippets.py

Prompt 常量(节选)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# ========== Prompt 常量(节选) ==========
# 源文件: llm/prompts.py 行 1-100

# L2: 【文档】文件说明:Prompt 模板集中管理模块
# L3: 【文档】业务说明:集中管理 iCan 项目所有 Agent 的 Prompt 模板,包括:
# L4: 【文档】- GuideAgent(对话引导):多阶段对话策略,渐进式挖掘用户信息
# L5: 【文档】- ResumeParserAgent(履历解析):从用户文本中提取结构化信息
# L6: 【文档】- ProfileAnalyzerAgent(个人分析):基于履历进行五维度深度分析
# L7: 【文档】- CareerMatcherAgent(职业匹配):基于个人画像进行三级职业匹配
# L8: 【文档】- ReporterAgent(报告输出):将分析结果整合为结构化报告文本
# L9: 【文档】所有 Prompt 遵循 PRD 第九章 Prompt 设计要点:角色设定、任务描述、
# L10: 【文档】输出格式、约束条件、示例引导。
# L11: 【文档】数据流向:本模块被各 Agent 导入使用 -> 填充变量 -> 发送给 LLM
# (L1-12 为函数/模块文档字符串,已转为注释便于阅读)

# L14: ==========================================================================
# L15: GuideAgent 对话引导 Prompt
# L16: ==========================================================================

# L18: 赋值:更新局部变量或 state 字段
GUIDE_SYSTEM_PROMPT = """你是一位拥有20年经验的高级职业规划师,名叫"小C"。你同时具备心理咨询师和职业顾问的双重素养,擅长通过自然对话深入了解一个人的职业发展需求。

# L20: 你的核心能力
# L21: 执行该语句(细节见上文业务描述)
1. **职业规划专业能力**:精通各行业职业发展路径、市场趋势、岗位要求
# L22: 执行该语句(细节见上文业务描述)
2. **心理咨询能力**:善于倾听、共情,能识别用户的情绪状态和深层需求
# L23: 执行该语句(细节见上文业务描述)
3. **信息挖掘能力**:通过渐进式提问,系统性地收集用户的关键信息
# L24: 执行该语句(细节见上文业务描述)
4. **分析判断能力**:基于收集的信息,快速形成对用户的初步画像

# L26: 对话策略
# L27: 执行该语句(细节见上文业务描述)
- **渐进式挖掘**:从开放式问题开始,逐步聚焦到具体细节,避免一上来就问太多结构化问题
# L28: 执行该语句(细节见上文业务描述)
- **情绪识别**:关注用户的情绪变化,适时给予鼓励和认可,建立信任感
# L29: 执行该语句(细节见上文业务描述)
- **灵活应对**:根据用户的回答灵活调整对话方向,不要机械地按照固定流程走
# L30: 执行该语句(细节见上文业务描述)
- **信息补全**:在自然对话中巧妙地引导用户提供缺失的关键信息
# L31: 执行该语句(细节见上文业务描述)
- **总结确认**:在关键节点主动总结已收集的信息,确保理解准确

# L33: 对话阶段
# L34: 执行该语句(细节见上文业务描述)
你会根据对话进展自动判断当前所处的阶段:
# L35: 执行该语句(细节见上文业务描述)
1. **greeting(开场问候)**:热情欢迎用户,简要介绍服务,了解用户的基本来意
# L36: 执行该语句(细节见上文业务描述)
2. **assess_need(需求评估)**:判断用户的核心需求是职业规划、转型咨询还是简历优化等
# L37: 执行该语句(细节见上文业务描述)
3. **collect_basic_info(基础信息收集)**:了解用户的教育背景、工作年限、当前岗位等基础信息
# L38: 执行该语句(细节见上文业务描述)
4. **dig_deeper(深度挖掘)**:深入了解用户的核心技能、职业成就、工作偏好、价值观等
# L39: 执行该语句(细节见上文业务描述)
5. **confirm(确认总结)**:总结所有收集到的信息,与用户确认后准备生成分析报告

# L41: 重要规则
# L42: 执行该语句(细节见上文业务描述)
- 始终使用中文回复
# L43: 执行该语句(细节见上文业务描述)
- 语气亲切专业,像一位经验丰富的朋友
# L44: 执行该语句(细节见上文业务描述)
- 每次回复控制在200字以内,保持对话节奏
# L45: 执行该语句(细节见上文业务描述)
- 不要一次性问太多问题,每次最多提1-2个问题
# L46: 执行该语句(细节见上文业务描述)
- 如果用户回答模糊,可以用具体场景来引导
# L47: 执行该语句(细节见上文业务描述)
- 如果用户表达了负面情绪,先给予情感支持再继续引导
# L48: 执行该语句(细节见上文业务描述)
- 当收集到足够信息后,主动提议进入分析阶段
# L51: 【文档】GUIDE_STAGE_TEMPLATES = {
# L52: 【文档】"greeting": (
# L53: 【文档】"用户刚刚开始对话。请热情地欢迎用户,简要介绍你能提供的职业规划服务,"
# L54: 【文档】"并用一个轻松的开放性问题了解用户的来意。\n\n"
# L55: 【文档】"示例开场白:你好!我是小C,你的专属职业规划顾问。我拥有20年的职业规划经验,"
# L56: 【文档】"帮助过上千位职场人找到自己的方向。今天想聊聊什么?是遇到了职业瓶颈,"
# L57: 【文档】"还是在考虑新的发展方向?"
# L58: 【文档】),
# L59: 【文档】"assess_need": (
# L60: 【文档】"用户已经开始了对话。请通过1-2个精准的问题,判断用户的核心需求类别:\n"
# L61: 【文档】"- 职业规划:需要明确职业方向或制定发展计划\n"
# L62: 【文档】"- 职业转型:想转换行业或岗位\n"
# L63: 【文档】"- 简历优化:需要优化求职材料\n"
# L64: 【文档】"- 技能提升:想了解需要补充哪些能力\n"
# L65: 【文档】"- 职场困惑:遇到具体的工作困境\n\n"
# L66: 【文档】"根据用户的回答,自然地引导对话深入。不要直接问'你需要什么服务',"
# L67: 【文档】"而是通过对话内容来推断。"
# L68: 【文档】),
# L69: 【文档】"collect_basic_info": (
# L70: 【文档】"已经了解了用户的基本需求。现在请在自然对话中收集以下基础信息(不要一次性全问):\n"
# L71: 【文档】"1. 教育背景(学历、专业、毕业院校)\n"
# L72: 【文档】"2. 工作年限和行业经验\n"
# L73: 【文档】"3. 当前/最近的岗位和职责\n"
# L74: 【文档】"4. 核心技能和技术栈\n"
# L75: 【文档】"5. 重要的职业成就或项目经历\n\n"
# L76: 【文档】"注意:要像聊天一样自然地获取这些信息,而不是像面试一样逐条询问。"
# L77: 【文档】"可以先从用户最愿意谈论的话题切入。"
# L78: 【文档】),
# L79: 【文档】"dig_deeper": (
# L80: 【文档】"基础信息已经收集得差不多了。现在请深入挖掘以下维度:\n"
# L81: 【文档】"1. **工作偏好**:更喜欢独立工作还是团队协作?喜欢稳定还是挑战?\n"
# L82: 【文档】"2. **成就感来源**:什么样的事情让用户最有成就感?\n"
# L83: 【文档】"3. **职业价值观**:用户最看重什么?(薪资、成长、平衡、影响力等)\n"
# L84: 【文档】"4. **性格特点**:偏外向还是内向?擅长分析还是直觉判断?\n"
# L85: 【文档】"5. **职业困惑的根源**:真正困扰用户的问题是什么?\n"
# L86: 【文档】"6. **期望和目标**:对未来的职业发展有什么期望?\n\n"
# L87: 【文档】"可以结合之前收集的信息,提出有针对性的深度问题。"
# L88: 【文档】"例如:'你提到之前带团队完成了XX项目,那个时候你觉得最有成就感的部分是什么?'"
# L89: 【文档】),
# L90: 【文档】"confirm": (
# L91: 【文档】"对话已经收集了足够的信息。请做以下事情:\n"
# L92: 【文档】"1. 用简洁的方式总结你了解到的用户信息(教育、经历、技能、偏好、目标等)\n"
# L93: 【文档】"2. 询问用户是否有需要补充或纠正的地方\n"
# L94: 【文档】"3. 如果用户确认信息无误,告知用户即将进入分析阶段,会生成以下内容:\n"
# L95: 【文档】" - 个人职业画像分析\n"
# L96: 【文档】" - 职业方向推荐\n"
# L97: 【文档】" - 行动建议\n\n"
# L98: 【文档】"示例总结格式:\n"
# L99: 【文档】'让我总结一下我们聊到的内容:你是一位有X年XX行业经验的专业人士,'
# L100: 【文档】'目前从事XX岗位,擅长XX和XX。你希望在XX方向有更好的发展,'

analyze_riasec 调用 LLM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# ========== analyze_riasec 调用 LLM ==========
# 源文件: agents/profile_analyzer.py 行 328-395

# L328: 异步函数 analyze_riasec:可被 await,适合 IO 型 LLM/DB 调用
async def analyze_riasec(state: ProfileAnalysisState) -> dict:
# L330: 【文档】分析霍兰德 RIASEC 职业兴趣。
# L332: 【文档】功能描述:
# L333: 【文档】基于结构化履历中的工作经历、技能特点和职业发展路径,
# L334: 【文档】分析用户在霍兰德六维度(R/I/A/S/E/C)上的倾向,
# L335: 【文档】计算各维度得分并确定用户的霍兰德代码(最高的2-3个维度)。
# L337: 【文档】入参说明:
# L338: 【文档】state (ProfileAnalysisState): 个人分析状态对象,需包含 structured_profile。
# L340: 【文档】出参说明:
# L341: 【文档】dict: 状态更新字典,包含 riasec_scores(RIASEC 六维得分)。
# (L329-342 为函数/模块文档字符串,已转为注释便于阅读)
# L343: 开始 try 块,后续 except 负责兜底
try:
# L344: 记录日志,便于线上排查节点入参/出参
logger.info("[analyze_riasec] 开始执行,入参: state=%s", {k: str(v)[:100] for k, v in state.items()})
# L345: 赋值:更新局部变量或 state 字段
structured_profile = state.get("structured_profile", {})

# L347: 赋值:更新局部变量或 state 字段
profile_summary = json.dumps(structured_profile, ensure_ascii=False)[:2000]
# L348: 赋值:更新局部变量或 state 字段
riasec_prompt = (
# L349: 执行该语句(细节见上文业务描述)
f"请基于以下结构化履历信息,分析用户的霍兰德 RIASEC 职业兴趣。\n"
# L350: 执行该语句(细节见上文业务描述)
f"对六个维度进行评分(0-10分):R(现实型)、I(研究型)、A(艺术型)、"
# L351: 执行该语句(细节见上文业务描述)
f"S(社会型)、E(企业型)、C(常规型)。\n\n"
# L352: 执行该语句(细节见上文业务描述)
f"履历信息:\n{profile_summary}\n\n"
# L353: 执行该语句(细节见上文业务描述)
f"请按以下 JSON 格式输出:\n"
# L354: 执行该语句(细节见上文业务描述)
f'{{"R": 0到10分, "I": 0到10分, "A": 0到10分, '
# L355: 执行该语句(细节见上文业务描述)
f'"S": 0到10分, "E": 0到10分, "C": 0到10分, '
# L356: 执行该语句(细节见上文业务描述)
f'"holland_code": "如 IAS", "analysis": "RIASEC 分析文本"}}'
# L357: 执行该语句(细节见上文业务描述)
)

# L359: 赋值:更新局部变量或 state 字段
messages = [
# L360: 执行该语句(细节见上文业务描述)
{"role": "system", "content": PROFILE_ANALYZER_SYSTEM_PROMPT},
# L361: 执行该语句(细节见上文业务描述)
{"role": "user", "content": riasec_prompt},
# L362: 执行该语句(细节见上文业务描述)
]

# L364: 记录日志,便于线上排查节点入参/出参
logger.info("[analyze_riasec] 调用 LLM 分析 RIASEC")
# L365: 获取对话大模型实例(配置来自 settings.LLM_MODEL_CHAT)
model = get_chat_model()
# L366: 调用 LLM 并解析 JSON;内部有 JSON mode → 文本降级链
riasec_data = await invoke_llm_with_json(model, messages)

# L368: 提取 RIASEC 得分到标准格式
# L369: 赋值:更新局部变量或 state 字段
riasec_scores = {
# L370: 执行该语句(细节见上文业务描述)
"R": float(riasec_data.get("R", 0)),
# L371: 执行该语句(细节见上文业务描述)
"I": float(riasec_data.get("I", 0)),
# L372: 执行该语句(细节见上文业务描述)
"A": float(riasec_data.get("A", 0)),
# L373: 执行该语句(细节见上文业务描述)
"S": float(riasec_data.get("S", 0)),
# L374: 执行该语句(细节见上文业务描述)
"E": float(riasec_data.get("E", 0)),
# L375: 执行该语句(细节见上文业务描述)
"C": float(riasec_data.get("C", 0)),
# L376: 执行该语句(细节见上文业务描述)
}

# L378: 记录日志,便于线上排查节点入参/出参
logger.info(
# L379: 赋值:更新局部变量或 state 字段
"[analyze_riasec] RIASEC 分析完成,holland_code=%s, 得分=%s",
# L380: 执行该语句(细节见上文业务描述)
riasec_data.get("holland_code", "未知"),
# L381: 执行该语句(细节见上文业务描述)
riasec_scores,
# L382: 执行该语句(细节见上文业务描述)
)

# L384: 赋值:更新局部变量或 state 字段
result = {
# L385: 执行该语句(细节见上文业务描述)
"riasec_scores": riasec_scores,
# L386: 执行该语句(细节见上文业务描述)
}
# L387: 记录日志,便于线上排查节点入参/出参
logger.info("[analyze_riasec] 执行完成,出参: riasec_scores=%s", riasec_scores)
# L388: 返回本节点要合并进 state 的字段(LangGraph 会 merge)
return result

# L390: 捕获异常,避免整图/整请求崩溃
except Exception as e:
# L391: 记录日志,便于线上排查节点入参/出参
logger.error("[analyze_riasec] 分析 RIASEC 异常: %s", e, exc_info=True)
# L392: 返回本节点要合并进 state 的字段(LangGraph 会 merge)
return {
# L393: 执行该语句(细节见上文业务描述)
"riasec_scores": {"R": 0.0, "I": 0.0, "A": 0.0, "S": 0.0, "E": 0.0, "C": 0.0},
# L394: 执行该语句(细节见上文业务描述)
}

parse_riasec_scores

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# ========== parse_riasec_scores ==========
# 源文件: llm/parsers.py 行 95-160

# L95: 同步函数 parse_riasec_scores:路由决策或工厂方法
def parse_riasec_scores(text: str) -> dict:
# L97: 【文档】从 LLM 回复中解析 RIASEC 六维得分。
# L99: 【文档】功能描述:
# L100: 【文档】从文本中解析霍兰德 RIASEC 六个维度(R/I/A/S/E/C)的得分。
# L101: 【文档】支持多种格式:
# L102: 【文档】1. JSON 格式:{"R": 7, "I": 8, ...}
# L103: 【文档】2. 列表格式:R: 7, I: 8, A: 5, ...
# L104: 【文档】3. 描述格式:现实型(R): 7分, 研究型(I): 8分, ...
# L105: 【文档】如果某个维度未找到,默认为 0.0。
# L107: 【文档】入参说明:
# L108: 【文档】text (str): LLM 回复文本,包含 RIASEC 评分信息
# L110: 【文档】出参说明:
# L111: 【文档】dict: {"R": float, "I": float, "A": float, "S": float, "E": float, "C": float}
# L112: 【文档】每个值为 0.0 到 10.0 的浮点数
# (L96-113 为函数/模块文档字符串,已转为注释便于阅读)
# L114: 开始 try 块,后续 except 负责兜底
try:
# L115: 记录日志,便于线上排查节点入参/出参
logger.info(f"[parse_riasec_scores] 开始执行,入参: 文本长度={len(text)}")
# L116: 记录日志,便于线上排查节点入参/出参
logger.debug(f"[parse_riasec_scores] 文本预览: {text[:300]}")

# L118: 条件分支
if not text or not text.strip():
# L119: 记录日志,便于线上排查节点入参/出参
logger.warning("[parse_riasec_scores] 入参文本为空,返回全零得分")
# L120: 返回本节点要合并进 state 的字段(LangGraph 会 merge)
return {"R": 0.0, "I": 0.0, "A": 0.0, "S": 0.0, "E": 0.0, "C": 0.0}

# L122: 赋值:更新局部变量或 state 字段
default_scores = {"R": 0.0, "I": 0.0, "A": 0.0, "S": 0.0, "E": 0.0, "C": 0.0}

# L124: 策略1:尝试从 JSON 中提取
# L125: 从 LLM 文本中提取 JSON(四层正则/解析策略)
json_data = parse_json_from_text(text)
# L126: 条件分支
if json_data:
# L127: 英文全名到 RIASEC 缩写的映射
# L128: 赋值:更新局部变量或 state 字段
name_mapping = {
# L129: 执行该语句(细节见上文业务描述)
"realistic": "R", "investigative": "I", "artistic": "A",
# L130: 执行该语句(细节见上文业务描述)
"social": "S", "enterprising": "E", "conventional": "C",
# L131: 执行该语句(细节见上文业务描述)
"现实型": "R", "研究型": "I", "艺术型": "A",
# L132: 执行该语句(细节见上文业务描述)
"社会型": "S", "企业型": "E", "常规型": "C",
# L133: 执行该语句(细节见上文业务描述)
}
# L134: 构建标准化后的字典
# L135: 赋值:更新局部变量或 state 字段
normalized = {}
# L136: 循环
for k, v in json_data.items():
# L137: 赋值:更新局部变量或 state 字段
key_lower = k.lower().strip()
# L138: 条件分支
if key_lower in name_mapping:
# L139: 赋值:更新局部变量或 state 字段
normalized[name_mapping[key_lower]] = v
# L140: 条件分支
elif key_lower.upper() in ["R", "I", "A", "S", "E", "C"]:
# L141: 赋值:更新局部变量或 state 字段
normalized[key_lower.upper()] = v
# L142: 条件分支 else
else:
# L143: 赋值:更新局部变量或 state 字段
normalized[k] = v

# L145: 赋值:更新局部变量或 state 字段
scores = {}
# L146: 循环
for key in ["R", "I", "A", "S", "E", "C"]:
# L147: 条件分支
if key in normalized:
# L148: 开始 try 块,后续 except 负责兜底
try:
# L149: 赋值:更新局部变量或 state 字段
val = float(normalized[key])
# L150: 赋值:更新局部变量或 state 字段
scores[key] = min(10.0, max(0.0, val))
# L151: 捕获异常,避免整图/整请求崩溃
except (ValueError, TypeError):
# L152: 赋值:更新局部变量或 state 字段
scores[key] = 0.0
# L153: 条件分支 else
else:
# L154: 赋值:更新局部变量或 state 字段
scores[key] = 0.0

# L156: 检查是否至少有一个非零值
# L157: 条件分支
if any(v > 0 for v in scores.values()):
# L158: 记录日志,便于线上排查节点入参/出参
logger.info(f"[parse_riasec_scores] 执行完成(策略1: JSON提取),返回: {scores}")
# L159: 返回本节点要合并进 state 的字段(LangGraph 会 merge)
return scores

系列导航

主题
1 系统全景
2 五 Agent 协作
3 霍兰德 RIASEC
4–7 状态 · 路由 · 嵌套 · 容错
8–11 LLM 层 · SSE/WS · DB 迁移 · PDF
12–14 JSON Prompt · 13 RIASEC Prompt(本篇) · Guide Prompt
15–17 Docker · 中间件 · 配置

← 返回 iCan 专题