
当 HR 每天面对数百份简历时,如何快速筛选出匹配度最高的候选人?当面试官时间排满时,如何用 AI 完成初筛轮次的多轮对话?本文将完整复盘我从零构建 AI 招聘助手的全过程,涵盖 PDF 简历解析、RAG 知识库增强、多 Agent 协作面试、流式 SSE 推送等核心模块,并复盘了落地过程中的 5 个关键工程陷阱。
传统招聘系统存在三个核心痛点:
AI 招聘系统的核心能力:
组件 | 选型 | 理由 |
|---|---|---|
Web 框架 | FastAPI | 原生异步支持,自动生成 OpenAPI 文档,便于前后端联调 |
LLM 编排 | LangChain | 标准化 Agent/Tool 接口,便于切换模型(OpenAI/Claude/Qwen) |
向量数据库 | Chroma | 轻量级,嵌入式中无需独立部署,适合 MVP 阶段 |
会话管理 | Redis | 存储 Agent 对话历史,支持分布式部署 |
简历解析 | PyPDF2 + Tesseract OCR | PDF 文本提取 + 图片型 PDF 的文字识别 |
简历是非结构化文本,直接丢给 LLM 会消耗大量 Token。我采用了 "传统库初提取 + LLM 结构化补全" 的双阶段策略。
import PyPDF2
import io
from pydantic import BaseModel
from langchain.output_parsers import PydanticOutputParser
# 定义简历结构化 Schema
class ResumeSchema(BaseModel):
name: str
phone: str
email: str
skills: list[str]
work_experience: list[dict] # [{company, title, duration, achievements}]
education: list[dict]
summary: str
def parse_pdf_resume(file_bytes: bytes) -> str:
"""第一阶段:用传统 PDF 库提取纯文本(快速、低成本)"""
reader = PyPDF2.PdfReader(io.BytesIO(file_bytes))
text = ""
for page in reader.pages:
text += page.extract_text() or ""
return text
def extract_resume_with_llm(raw_text: str) -> ResumeSchema:
"""第二阶段:LLM 结构化提取(只在文本提取成功后调用)"""
parser = PydanticOutputParser(pydantic_object=ResumeSchema)
prompt = ChatPromptTemplate.from_messages([
("system", "你是一位资深 HR 助理。从简历文本中提取结构化信息。{format_instructions}"),
("human", "{raw_text}")
]).partial(format_instructions=parser.get_format_instructions())
chain = prompt | ChatOpenAI(model="gpt-4o-mini", temperature=0) | parser
return chain.invoke({"raw_text": raw_text})将岗位要求(JD)与候选人简历做向量相似度匹配,并让 Agent 给出可解释的评分依据。
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import Chroma
from langchain.tools import tool
# 初始化向量库(存储过往优秀简历/岗位描述)
embeddings = OpenAIEmbeddings()
vectorstore = Chroma(collection_name="jd_vectors", embedding_function=embeddings)
@tool
def match_resume_to_jd(resume_summary: str, jd_requirements: str) -> dict:
"""
计算简历与岗位匹配度,返回分数和理由。
"""
# 1. 将 JD 转为向量,检索相似简历
jd_vector = embeddings.embed_query(jd_requirements)
similar_docs = vectorstore.similarity_search_by_vector(jd_vector, k=5)
# 2. 让 LLM 评估匹配度
eval_prompt = f"""
岗位要求:{jd_requirements}
候选人摘要:{resume_summary}
相似成功案例:{similar_docs}
请输出 JSON 格式:
{{
"match_score": 0-100,
"strengths": ["候选人优势1", ...],
"gaps": ["候选人不足1", ...],
"recommendation": "建议进入下一轮/待定/不通过"
}}
"""
response = ChatOpenAI(model="gpt-4o-mini", temperature=0).invoke(eval_prompt)
return json.loads(response.content)这是系统的核心亮点。AI 面试官需要像真人一样追问技术细节,验证候选人是否真正掌握技能。
关键设计:追问策略(Follow-up Strategy)
from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain.prompts import MessagesPlaceholder
class InterviewerAgent:
def __init__(self, session_id: str):
self.session_id = session_id
self.redis_client = redis.Redis()
self.llm = ChatOpenAI(model="gpt-4-turbo", temperature=0.3)
def build_prompt(self, jd: str, resume: str) -> ChatPromptTemplate:
return ChatPromptTemplate.from_messages([
("system", f"""
你是资深技术面试官。岗位 JD:{jd},候选人背景:{resume}。
规则:
1. 每次只问一个问题。
2. 如果候选人回答过于笼统(缺少数据、方法论),必须追问细节。
3. 追问次数不超过 3 轮。
4. 面试结束后,输出技术评级(S/A/B/C)。
"""),
MessagesPlaceholder(variable_name="history"),
("human", "{input}")
])
async def chat(self, user_message: str) -> str:
# 从 Redis 获取对话历史
history = self._get_history(self.session_id)
prompt = self.build_prompt(...)
chain = prompt | self.llm
# 判断是否需要追问
if self._needs_followup(user_message):
followup = await self._generate_followup(user_message, history)
response = followup
else:
response = await chain.ainvoke({"history": history, "input": user_message})
# 存储历史
self._save_history(self.session_id, user_message, response)
return response
def _needs_followup(self, text: str) -> bool:
"""用规则或小模型快速判断回答是否过于笼统"""
vague_keywords = ["优化", "提升", "很多", "不错", "还行", "参与"]
has_number = any(char.isdigit() for char in text)
return any(kw in text for kw in vague_keywords) and not has_numberAI 面试需要实时反馈,不适合等待完整响应再返回。我使用 Server-Sent Events (SSE) 实现打字机效果。
from fastapi import FastAPI, UploadFile, File, Form
from fastapi.responses import StreamingResponse
import json
app = FastAPI(title="AI 招聘系统", version="1.0")
@app.post("/interview/chat")
async def interview_chat(
session_id: str = Form(...),
user_message: str = Form(...)
):
agent = InterviewerAgent(session_id)
async def event_generator():
# 使用 LangChain 的 astream 方法逐词生成
async for chunk in agent.astream_chat(user_message):
yield f"data: {json.dumps({'token': chunk, 'done': False})}\n\n"
yield f"data: {json.dumps({'done': True})}\n\n"
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache"}
)
@app.post("/resume/upload")
async def upload_resume(
file: UploadFile = File(...),
jd_id: str = Form(...)
):
content = await file.read()
raw_text = parse_pdf_resume(content)
structured = extract_resume_with_llm(raw_text)
# 自动触发匹配度评估
match_result = match_resume_to_jd(structured.summary, get_jd_by_id(jd_id))
return {
"resume": structured.dict(),
"match": match_result,
"recommendation": match_result["recommendation"]
}面试结束后,综合所有轮次对话生成评估报告。
@tool
def generate_evaluation_report(
session_id: str,
resume: ResumeSchema,
interview_history: list
) -> str:
"""
综合简历和面试表现,生成候选人评估报告。
"""
prompt = f"""
候选人简历:{resume.dict()}
面试对话记录:{interview_history}
请按以下维度输出报告:
1. 技术能力(1-10 分):基于面试中回答的技术深度
2. 沟通表达(1-10 分):逻辑清晰度、表达流畅度
3. 岗位匹配度(%):结合简历与 JD 的契合度
4. 优势亮点:3-5 条
5. 潜在风险:2-3 条
6. 录用建议:强烈推荐/推荐/待定/不推荐
输出 Markdown 格式。
"""
response = ChatOpenAI(model="gpt-4-turbo", temperature=0.2).invoke(prompt)
return response.content现象:中文 PDF 解析后全是乱码。
原因:部分 PDF 使用了非标准编码(如 GB2312)。
解法:使用 pdfplumber 替代 PyPDF2,它在处理中文编码时更鲁棒。同时增加编码检测逻辑:
import chardet
detected = chardet.detect(raw_bytes)
encoding = detected['encoding'] or 'utf-8'现象:面试官 Agent 在追问环节反复问同一个问题。
原因:LangChain 的 AgentExecutor 默认 max_iterations=15,如果 Tool 返回格式不符合预期,Agent 会重试而不是结束。
解法:设置 early_stopping_method="generate",并在 System Prompt 中明确:"如果候选人已经提供了具体数据,结束本轮追问。"
现象:面试进行到第 10 轮时,Token 消耗急剧增加,响应变慢。 解法:使用 滑动窗口(Sliding Window) 策略,只保留最近 5 轮对话 + 首轮简历摘要。历史摘要定期用 LLM 做压缩:
def compress_history(history: list) -> str:
summary_prompt = f"请将以下对话压缩为 100 字内的摘要:{history[:-4]}"
return llm.invoke(summary_prompt).content现象:部分候选人将问题复制到 ChatGPT 获得答案后再粘贴,AI 面试官难以分辨。 解法:增加 “反应时间检测” —— 如果回复速度 < 1 秒且内容完美,标记为可疑。同时,追问细节问题(如"你在那个项目中遇到的最大困难是什么?")来验证真实性。
现象:初期没有历史简历数据,向量库为空,匹配度打分不准。 解法:在系统初始化时,注入一批公开的基准简历(如 Kaggle 上公开的简历数据集)作为种子数据。随着使用量增加,逐步替换为真实优质简历。
这套系统在一家互联网公司的技术招聘中试运行了 2 个月:
在技术落地之余,我也想分享两点关于 AI 招聘伦理 的思考:
我的观点:AI 招聘的终局不是"用 AI 替代 HR",而是 "用 AI 消除 HR 的重复劳动,释放人类去完成更有温度的沟通和判断"。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。