尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

EduAgent 项目全解析(四):简历审查 Agent——PDF 解析与六维并行评分

EduAgent 项目全解析(四):简历审查 Agent——PDF 解析与六维并行评分 本篇拆解简历审查 AgentResume。它是四个 Agent 里结构最简单的一个——无分支、无 interrupt、无 checkpointer 的直线图但恰好是学习 LangGraph 和并行 LLM 调用的最佳入门案例PDF 解析怎么不阻塞事件循环、LLM 结构化提取怎么做、asyncio.gather怎么六路并行评分、Think Tool 怎么提升诊断质量。一、整体结构8 个节点的直线图STARTupload_to_minio本地模式空跑download_pdf本地模式空跑extract_textPDF文本提取extract_structuredLLM结构化提取run_six_dimensions六维并行评分diagnose_issues逐条问题诊断generate_summary整体评价save_results结果持久化ENDgraph.py里就是 8 个add_node 9 条add_edge最后builder.compile()不传 checkpointer——因为简历审查是一次性任务不需要断点恢复。注释里写得很清楚“无分支、无 interrupt、无 checkpointer一次性任务”。defbuild_resume_graph():builderStateGraph(ResumeState)# ① 注册 8 个节点builder.add_node(upload_to_minio,upload_to_minio_node)builder.add_node(download_pdf,download_pdf_node)builder.add_node(extract_text,extract_text_node)builder.add_node(extract_structured,extract_structured_node)builder.add_node(run_six_dimensions,run_six_dimensions_node)builder.add_node(diagnose_issues,diagnose_issues_node)builder.add_node(generate_summary,generate_summary_node)builder.add_node(save_results,save_results_node)# ② 顺次连边builder.add_edge(START,upload_to_minio)# ... 一条直线连到 ENDreturnbuilder.compile()# ★ 不传 checkpointer一个值得学习的预留设计upload_to_minio_node和download_pdf_node在当前本地模式下是空跑return {}但它们把文件在哪的职责固定住了——将来要接对象存储只需要实现这两个节点图的拓扑和后续节点零改动。这是面向扩展的占位节点模式。二、PDF 文本提取线程池不阻塞事件循环2.1 同步解析函数def_sync_extract_text(pdf_path:str)-dict:同步方法 提取PDF文本不考虑分栏importfitz# PyMuPDFall_text_parts[]withfitz.open(pdf_path)asdoc:page_countlen(doc)forpageindoc:blockspage.get_text(blocks)# 每块 (x0,y0,x1,y1,text,block_no,type)text_blocks[bforbinblocksifb[6]0]# type0 文字块过滤图片ifnottext_blocks:continue# 按 y0 纵坐标升序同高度按 x0 排序处理同行文本的微小坐标差sorted_blockssorted(text_blocks,keylambdab:(round(b[1],1),b[0]))page_text\n.join(b[4].strip()forbinsorted_blocksifb[4].strip())all_text_parts.append(page_text)raw_text\n\n---PAGE BREAK---\n\n.join(all_text_parts)return{raw_text:raw_text,page_count:page_count}几个细节page.get_text(blocks)返回的元组第 7 个元素是块类型0文字块1图片块过滤掉图片。round(b[1], 1)消除同一行文本因字体微小差异导致的 y 坐标浮动保证按行排序正确。页间插入---PAGE BREAK---标记方便 LLM 感知分页。2.2 异步节点run_in_executor 桥接asyncdefextract_text_node(state:ResumeState)-dict:pdf_pathstate[pdf_local_path]loopasyncio.get_running_loop()# ★ 把同步阻塞的 PDF 解析丢到线程池避免卡住整个事件循环resultawaitloop.run_in_executor(None,_sync_extract_text,pdf_path)iflen(result[raw_text].strip())200:logger.warning(extract_text.text_too_short,...)# 可能是扫描件return{raw_text:result[raw_text],page_count:result[page_count]}这是全项目反复出现的模式FastAPI 是单线程事件循环任何同步阻塞调用模型推理、PDF 解析、文件 I/O都会卡住所有并发请求。loop.run_in_executor(None, fn, args)把同步函数丢进默认线程池await结果——既简单又安全。main.py里三个本地模型的预热也是用这个模式并行加载的。三、LLM 结构化提取Function Calling 的魅力extract_structured_node用with_structured_output(ResumeStructured)把简历原文提取成结构化对象。ResumeStructured是嵌套的 Pydantic 模型classResumeStructured(BaseModel):name:strField(description姓名)phone:strField(default,description手机号)email:strField(default,description邮箱)target_position:strField(default,description求职意向岗位)education:list[EducationItem]Field(default_factorylist)skills_list:list[str]Field(default_factorylist)projects:list[ProjectItem]Field(default_factorylist)work_experience:list[WorkItem]Field(default_factorylist)certificates:list[str]Field(default_factorylist)self_intro:strField(default,description个人简介)节点实现asyncdefextract_structured_node(state:ResumeState)-dict:raw_textstate[raw_text]# 超长截断截前 4000 字防止超出 context 并省 tokentext_for_llmraw_text[:4000]iflen(raw_text)4000elseraw_text promptEXTRACT_STRUCTURED_PROMPT.format(resume_texttext_for_llm)structured_llmget_structured_llm(resume,ResumeStructured)structured_dictNoneforattemptinrange(2):# ★ 最多尝试 2 次结构化输出偶发返回 Nonetry:resultawaitstructured_llm.ainvoke([SystemMessage(contentSYSTEM_PROMPT),HumanMessage(contentprompt),])ifresultisNone:raiseValueError(structured output returned None)structured_dictresult.model_dump()breakexceptExceptionase:ifattempt0:awaitasyncio.sleep(1)# 第1次失败等1秒重试ifstructured_dictisNone:structured_dictResumeStructured(name未能提取).model_dump()# 降级return{structured:structured_dict}两个工程要点重试 降级DeepSeek 结构化输出偶发返回 NoneJSON 生成失败所以循环尝试 2 次都失败就返回未能提取的空结构保证流程不中断。default_factorylist而不是default[]Pydantic 里用可变默认值会被所有实例共享这是经典坑state.py 注释里专门标注了。四、六维并行评分asyncio.gather 的威力4.1 六维度定义SIX_DIMENSIONS[{key:project_depth,name:项目深度,weight:0.30,focus:项目描述是否有量化数据、技术选型理由、个人贡献、难点解决},{key:tech_match,name:技术匹配度,weight:0.25},{key:expression,name:表达规范性,weight:0.15},{key:structure,name:简历结构,weight:0.15},{key:quantification,name:量化程度,weight:0.10},{key:authenticity,name:真实可信度,weight:0.05},]# 权重之和 1.0六个维度权重不同项目深度 30% 最重真实可信度 5% 最轻——符合 IT 求职简历的实际评审重点。4.2 六路并行调用asyncdefrun_six_dimensions_node(state:ResumeState)-dict:structuredstate.get(structured)or{}structured_summary_build_structured_summary(structured)# 精简摘要省tokenasyncdefreview_one_dimension(dim:dict)-dict:promptDIMENSION_REVIEW_PROMPTS[dim[key]].format(resume_textstate[raw_text][:3000],structured_summarystructured_summary,focusdim[focus])forattemptinrange(2):try:structured_llmget_structured_llm(resume,DimensionScore)result:DimensionScoreawaitstructured_llm.ainvoke([...])ifresultisNone:raiseValueError(structured output returned None)dresult.model_dump()d[dimension]dim[name]d[weight]dim[weight]returndexceptExceptionase:ifattempt0:awaitasyncio.sleep(1)return_empty_dimension_score(dim)# 降级中性50分标记复核# ★ 六路并行创建6个协程任务gather 一起跑、等全部完成tasks[review_one_dimension(dim)fordiminSIX_DIMENSIONS]dimension_scoresawaitasyncio.gather(*tasks)# 加权综合分 Σ(得分 × 权重)weighted_scoresum(d[score]*d[weight]fordindimension_scores)return{dimension_scores:list(dimension_scores),weighted_score:round(weighted_score,2)}为什么用asyncio.gather六个维度互相独立串行要等 6 次 LLM 往返每次几秒并行一次就够——总耗时从6 个请求之和变成最慢的那个请求。这是 LLM 应用中性价比最高的优化之一。降级设计某个维度两次都失败返回中性 50 分 issues 标记人工复核而不是让整个流程挂掉——单个维度的失败不应该拖垮整份简历的评审。4.3 省 token 的精简摘要def_build_structured_summary(structured:dict)-str:lines[]ifstructured.get(name):lines.append(f姓名{structured[name]})# 教育经历只要第一条最高学历# 技能列表只取前10个# 项目只列名称工作经历只写段数return\n.join(lines)iflineselse结构化提取失败请基于原文评审给 LLM 的提示词里不塞整份简历原文而是塞精简摘要——省 token、聚焦重点还模拟了人真实看简历的注意力分布。五、Think Tool先宏观分析再结构化诊断diagnose_issues_node用了Think 前置推理模式先让 LLM 自由思考整体问题再带着思考结论做结构化输出。# ① 汇总各维度已发现的问题all_raw_issues[f[{dim[dimension]}]{issue}fordimindimension_scoresforissueindim.get(issues,[])]# ② Think 宏观分析普通 LLM 先分析各维度评分的关联性、找共同短板think_promptDIAGNOSE_THINK_PROMPT.format(dimension_scores_summary...,raw_issuesraw_issues_text)think_respawaitget_llm(resume).ainvoke([HumanMessage(contentthink_prompt)])reasoning_trace...# 文本推理结果# ③ 结构化生成问题清单带上思考结论作为附加上下文think_contextf\n\n【诊断前宏观分析】\n{reasoning_trace}ifreasoning_traceelsepromptDIAGNOSE_ISSUES_PROMPT.format(...)think_context resultawaitget_structured_llm(resume,IssueList).ainvoke([...])issues[item.model_dump()foriteminresult.items]# ④ 按优先级排序high → medium → lowpriority_order{high:0,medium:1,low:2}issues.sort(keylambdax:priority_order.get(x.get(priority,low)))为什么要 Think 一步直接让 LLM 输出问题清单它可能只见树木不见森林比如重复列六个维度各自的零散问题。先宏观分析共同短板在哪、哪些维度问题其实是同一个根因再结构化输出诊断质量明显提升。而且 Think 步骤失败不影响主流程except里reasoning_trace保持空串继续往下走——把增强放在锦上添花的位置。六、结果持久化 临时文件清理save_results_node把完整结果写入 PostgreSQL 的resume_reviews表JSONB 字段然后清理本地临时 PDFasyncdefsave_results_node(state:ResumeState)-dict:structured_output{review_id:review_id,student_id:state[student_id],structured:state.get(structured),weighted_score:state.get(weighted_score,0),dimension_scores:state.get(dimension_scores,[]),issues:state.get(issues,[]),summary:state.get(summary),}asyncwithAsyncSessionLocal()assession:awaitsession.execute(text(UPDATE resume_reviews SET structured_data :structured_data, scores :scores, issues :issues, summary :summary, status done, updated_at NOW() WHERE id :review_id),{structured_data:json.dumps(...,ensure_asciiFalse),...})awaitsession.commit()# 清理本地临时 PDFlocal_pathstate.get(pdf_local_path,)iflocal_pathandos.path.exists(local_path):os.remove(local_path)return{fallback_used:False,structured_output:structured_output}注意ensure_asciiFalse——JSON 序列化中文时保留原文否则数据库里会是一堆\uXXXX转义。七、本篇小结知识点实现位置核心技巧直线图 Agentgraph.py无分支/无 interrupt/无 checkpointerPDF 解析extract_text_noderun_in_executor线程池桥接同步 I/O结构化提取extract_structured_nodewith_structured_output 2次重试 降级并行评分run_six_dimensions_nodeasyncio.gather六路并行 权重求和Think Tooldiagnose_issues_node先自由推理再结构化输出失败不影响主流程结果落库save_results_nodeJSONB 存储 ensure_asciiFalse 清理临时文件Resume Agent 展示的核心思想即使是最简单的图工程化细节也拉满——线程池、重试降级、并行加速、token 优化、Think 增强每个节点都在回答如果这里失败了怎么办。下篇预告下一篇《EduAgent 项目全解析五试卷批改 Agent——三轨并行与人在环中HitL》看最复杂的工程案例Word 试卷解析、三轨并行批改规则引擎/LLM语义/代码评估、知识薄弱点分析以及 LangGraph 最实用的能力之一——interrupt()暂停图等教师审核、Command(resume...)恢复执行。项目源码仅供教学参考欢迎评论区交流讨论。
返回列表