这份文档不是解释每一行代码,而是帮你抓住 agent.py 的主流程。读懂下面这条链路之后,再回头看源码会轻松很多。
ResumeAgent 做的事情可以概括成 4 步:
- 解析输入资料
- 把简历存成可检索的向量片段
- 根据问题或 JD 检索相关证据
- 调用大模型生成结构化结果或聊天回答
入口:D:\code\resume-agent\agent.py:38
__init__ 里主要初始化了 5 个核心能力:
-
self.llm- 负责所有生成类任务
- 比如 JD 关键词提取、简历评估、追问建议、聊天回答
-
self.embeddings- 负责把文本转成向量
- 给 Chroma 做语义检索用
-
self.ocr_engine- 负责图片 OCR
- 用来从 JD 截图里提文字
-
self.vector_store- 简历切片的向量存储
- 聊天和评估时的证据都从这里查
-
self.parser- 负责解析
.docx/.pdf - 并把简历切成 chunk
- 负责解析
另外还有一个很重要的变量:
self.system_instruction- 这是全局 prompt 规则
- 作用是把模型约束成“专业、客观、重证据的招聘顾问”
主入口方法:D:\code\resume-agent\agent.py:266
方法名:ingest_resume
这是“简历入库链路”的核心。
- 校验文件类型,只允许
.docx/.pdf - 把上传文件写到临时文件
- 调
self.parser.extract_text()提取纯文本 - 校验文本不能为空
- 调
self.parser.get_chunks()把文本切片 - 先删掉旧向量,避免重复数据
- 把 chunk + metadata 写入 Chroma
-
为什么要切 chunk
- 因为后面用户问问题时,不是整份简历都塞给模型
- 而是先从 chunk 里检索最相关的几段
-
metadata 有什么用
- 这里保存了
user_id、resume_id、candidate_name、phone - 后面检索时就能限制“只查当前用户、当前候选人”
- 这里保存了
入口:D:\code\resume-agent\agent.py:133
核心方法:
analyze_jd_extract_jd_keywords
把一段 JD 文本交给模型,抽取出真正适合招聘筛选的关键词。
- 例如:
React、TypeScript、Node.js、BFF、中后台这类词
因为后面评估简历时:
- 需要知道 JD 的重点能力是什么
- 雷达图也需要维度来源
- 聊天追问也会参考 JD 重点
如果结构化输出失败,会走 agent_utils.fallback_keywords
这个方法不会调用模型,而是:
- 从预置技术词库里匹配
- 再用正则从文本里提词
- 最后去重
也就是说:
- 正常情况:走 LLM
- 异常情况:走规则兜底
入口:D:\code\resume-agent\agent.py
核心方法:evaluate_resume
这是整个项目最关键的方法之一。
- 先抽 JD 关键词
- 生成
sources - 把
sources格式化成 prompt 可引用证据 - 调用结构化输出模型,要求返回:
- summary
- title
- decision
- match_score
- radar_metrics
- highlights
- risks
- sources
- 对模型结果做清洗和收口
- 如果失败,走
agent_utils.fallback_evaluation
相关方法:
prepare_evaluation_contextgenerate_evaluationagent_utils.py中的build_resume_sourcesagent_utils.py中的format_sources_for_promptagent_utils.py中的normalize_sourcesagent_utils.py中的normalize_source_idsagent_utils.py中的normalize_evaluation_items
这套设计的意义是:
- 先把简历拆成证据片段
- 给每个片段一个
source_id - 要求模型输出结论时只能引用这些
source_id - 再由后端验证
source_id是否真的合法
这就是你现在项目里“可解释性”的核心实现。
因为模型即使被要求结构化输出,也可能:
- 漏字段
- source_id 写错
- 返回字符串而不是对象
- sources 重复
所以必须在后端再做一次收口。
入口:D:\code\resume-agent\agent.py
核心方法:stream_chat
- 先查历史消息
_load_history - 再检索当前问题相关的简历片段
_retrieve_context_docs - 把“历史 + 检索结果 + 用户问题”一起拼进 prompt
- 用
chain.astream()流式生成回答 - 每拿到一个 chunk 就
yield - 最后把完整回答写入数据库
-
ask- 非流式接口的封装
- 本质上复用
stream_chat
-
get_chat_sources- 单独给前端返回这次聊天检索到的来源片段
-
_retrieve_context_docs- 真正负责从 Chroma 召回相关证据
位置:D:\code\resume-agent\agent.py 中的 _retrieve_context_docs
它做了两件重要的事:
-
用 metadata filter 限定检索范围
- 防止查到别人的简历
-
给每个召回片段分配
ctx_1、ctx_2这种source_id- 前端就可以展示“这条回答依据来自哪些片段”
入口:D:\code\resume-agent\agent.py 中的 _load_history
核心方法:_load_history
- 先查 Redis
- 命中就直接反序列化返回
- 未命中再查数据库
- 把数据库结果重新写回 Redis
因为聊天场景里每轮都要带历史消息:
- 不缓存会频繁查库
- 缓存可以减少延迟
相关方法:
_cache_get_cache_set_cache_delete_history_from_cache
你这个项目有一个很好的工程点:不是一出错就崩,而是尽量降级。
方法:agent_utils.fallback_keywords
模型失败时:
- 用技术词库 + 正则提词
方法:agent_utils.fallback_evaluation
模型失败时:
- 根据 JD 关键词在简历中的覆盖率粗略打分
- 生成 summary / highlights / risks
- 仍然带上 sources
方法:agent_utils.build_chat_fallback
流式调用失败时:
- 直接把已检索到的简历片段裁一段返回
- 告诉用户“这是降级回答”
这说明你的项目已经有 AI 工程化意识,而不是纯 demo。
如果你继续看“模拟面试”这条线,建议把它理解成 ResumeAgent 旁边新增的一条子流程。
主入口文件:D:\code\resume-agent\interview_agent.py
核心职责:
- 复用
ResumeAgent已经准备好的 JD / 简历 / 评估上下文 - 生成 10 道结构化模拟面试题
- 接收整场回答并做统一评分
- 用缓存和 fallback 保证体验稳定
核心方法:
_prepare_interview_materialspreparegenerate_interview_questions
执行顺序:
- 先校验简历内容是否存在
- 复用
agent.prepare_evaluation_context()准备关键词和证据来源 - 如果数据库里已有评估结果,就直接复用;没有再补一次评估
- 调面试 prompt 生成结构化题目
- 清洗
source_ids - 写入 Redis 缓存
核心方法:
submitsubmit_interview_answers
执行顺序:
- 同样先准备简历、JD、评估上下文
- 把 10 道题和答案拼成
qa_block - 让模型返回逐题评分和整场反馈
- 后端再根据平均分强制计算
verdict - 如果模型失败,走长度规则 fallback
这条链路的价值在于:
- 它复用了主评估能力
- 但没有继续挤进
agent.py - 所以主评估 agent 和面试 agent 的边界更清楚
我建议你按这个顺序看 agent.py:
__init__ingest_resume_extract_jd_keywordsevaluate_resumeprepare_evaluation_context/generate_evaluation/agent_utils.normalize_*stream_chat_retrieve_context_docs_load_historyinterview_agent.py- 各种 fallback 方法
这样你会先理解主流程,再理解细节工具函数。
如果面试官问你 agent.py 怎么工作的,你至少可以这样回答:
“我的 ResumeAgent 本质上是一个 RAG 驱动的招聘顾问 agent。上传简历后,我会先解析文档并切成 chunk,写入 Chroma 向量库。评估简历或聊天时,会先根据问题或 JD 检索相关片段,再把检索结果和 prompt 约束一起交给大模型生成结构化输出。为了提高可解释性,我额外设计了 source_id 机制,让 summary、亮点、风险和聊天回答都能映射回具体证据片段。为了保证稳定性,我还做了缓存、结构化输出清洗和 fallback 降级逻辑。”
这段如果你能顺畅讲出来,说明你已经真正理解这份代码了。