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

资讯详情

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

基于MCP协议构建简历查询API:让AI精准读取非结构化文档

基于MCP协议构建简历查询API:让AI精准读取非结构化文档 1. 项目概述当AI能读懂你的简历最近在折腾AI Agent开发的朋友可能都绕不开一个核心痛点如何让AI真正理解并高效利用我们手头那些非结构化的文档数据比如一份精心打磨的PDF简历。我们常常希望AI助手能像一位专业的HR一样快速从简历中提取关键信息回答诸如“候选人上一份工作的起止时间是”、“他主导过哪些项目”这类具体问题。传统的做法要么是把PDF全文扔给大模型消耗大量上下文窗口和Token回答还不一定精准要么是手动把信息整理成结构化数据费时费力。这个项目要解决的正是这个痛点。它的核心思路是将一份静态的PDF简历转化成一个动态的、可查询的“数据服务”并通过MCPModel Context Protocol协议让各类AI Agent比如Claude Code、Cursor等能够像调用一个函数或查询一个数据库那样按需、精准地从你的简历中获取信息。简单来说这就像为你的简历搭建了一个专属的API。AI Agent不需要再去“阅读理解”整个PDF文件它只需要向这个“简历API”发送一个查询请求比如“get_work_experience”就能立刻获得结构化的、清洗过的工作经历列表。这极大地提升了信息检索的效率和准确性也让AI Agent的能力变得更加模块化和实用。对于开发者、求职者或是任何需要频繁向AI提供个人背景信息的人来说这个项目提供了一个非常优雅的解决方案。它不仅仅是“PDF解析”更是“PDF服务化”。下面我就来详细拆解一下我是如何实现这个过程的其中涉及的思路、技术选型、踩过的坑都会毫无保留地分享出来。2. 核心思路与技术选型为什么是MCP在动手之前明确技术路线至关重要。市面上处理PDF和对接AI的方案很多我选择MCP协议作为核心桥梁是经过一番考量的。2.1 为什么选择MCP协议首先我们需要理解MCP是什么。MCP即模型上下文协议是由Anthropic提出的一种开放协议。它的目标是为AI模型尤其是大语言模型提供一个标准化的方式来发现、调用外部工具和数据源。你可以把它想象成AI世界的“USB协议”或“插件系统标准”。对于本项目而言MCP带来了几个关键优势标准化与兼容性MCP正在成为AI Agent生态中的一个重要标准。通过将简历数据封装成MCP Server你的“简历查询服务”可以无缝接入任何支持MCP协议的客户端如Claude Desktop、Cursor、Windsurf等。这意味着你构建一次就可以多处使用。结构化工具定义MCP要求Server明确定义它提供哪些“工具”Tools。在我们的场景下一个工具就是一个查询能力例如get_basic_info、query_skills_by_keyword、list_all_projects。这种设计迫使我们将模糊的“读取简历”需求拆解成一系列清晰、具体的原子操作这本身就是对业务逻辑的很好梳理。上下文感知与安全MCP协议下AI Agent客户端是发起请求的一方它知道自己需要什么然后通过协议调用Server。Server只负责执行具体的工具并返回结果不涉及AI的推理过程。这种职责分离更清晰也避免了将整个PDF文本塞进提示词Prompt所带来的上下文浪费和潜在的信息泄露风险。2.2 PDF解析方案选型PDF解析是底层的基础。我们的目标是从PDF中准确提取文本、位置和可能的表格信息。我评估了几个主流方案PyPDF2 / pdfminer老牌、轻量但对复杂排版和现代PDF格式的支持有时会力不从心提取的文本顺序可能错乱。pdfplumber在pdfminer基础上做了大量优化特别擅长保留文本的位置坐标信息对于有栏位结构的简历比如两栏式解析效果更好能较好地还原视觉上的阅读顺序。AWS Textract / Google Document AI云服务OCR和表单识别能力极强能处理扫描件但会产生费用且依赖网络。LayoutParser 深度学习模型学术前沿方案能进行非常精细的版面分析识别标题、段落、列表等但环境配置复杂运行开销大。我的选择是 pdfplumber。原因在于对于绝大多数数字生成的简历非扫描件pdfplumber在准确性和易用性上取得了很好的平衡。它提供的字符级坐标信息为我们后续可能需要的“智能字段定位”比如即使简历模板变了也能找到“工作经历”这个章节留下了扩展空间。同时它是一个纯Python库离线可用部署简单。2.3 文本向量化与语义检索可选进阶基础的解析完成后我们得到的是纯文本。但AI Agent的查询可能是模糊的、语义化的。例如“找出所有和机器学习相关的经历”。单纯的关键词匹配如搜索“机器学习”可能会漏掉“ML”、“AI模型开发”等表述。因此一个进阶的架构是引入嵌入模型Embedding Model和向量数据库。我们可以将简历文本按段落或句子切分转换成向量存入如ChromaDB、FAISS或Qdrant中。当收到语义查询时先将查询语句转换成向量然后在向量数据库中进行相似度搜索找到最相关的文本片段再返回给AI Agent。这是一个“增强模式”。对于初版我们可以先实现基于规则和关键词的精确工具如get_skill_list。在后续迭代中可以增加一个semantic_search工具内部调用向量检索来应对开放性问题。我建议分阶段实施先让核心流程跑通。2.4 整体架构图概念性基于以上选择项目的核心架构变得清晰[PDF简历文件] - [pdfplumber解析] - [结构化信息提取/文本清洗] - [MCP Server封装] | [可选向量化索引] - [向量数据库] | [AI Agent (Claude, Cursor)] --[MCP协议通信]-- [简历MCP Server] --[查询]-- [用户/Agent]Server内部维护着解析后的简历数据或向量索引并对外暴露几个定义好的工具函数。AI Agent通过MCP协议调用这些工具。3. 实战构建从零搭建简历MCP Server理论说完了我们开始动手。我将以一份假设的、格式相对规范的Markdown风格简历导出为PDF为例演示全流程。3.1 环境准备与依赖安装首先创建一个新的项目目录并设置Python虚拟环境强烈推荐避免依赖冲突。mkdir resume-mcp-server cd resume-mcp-server python -m venv venv # Windows: venv\Scripts\activate # Mac/Linux: source venv/bin/activate安装核心依赖pip install mcp pdfplumber pydantic # 如果考虑后续向量检索可以一并安装 # pip install sentence-transformers chromadbmcpAnthropic官方提供的MCP协议Python SDK用于快速构建Server和Client。pdfplumber我们的PDF解析引擎。pydantic用于数据验证和设置管理定义工具的参数和返回类型会非常方便。3.2 PDF解析与信息提取层我们创建一个pdf_parser.py文件负责最底层的解析工作。import pdfplumber import re from typing import Dict, List, Optional, Any class ResumeParser: def __init__(self, pdf_path: str): self.pdf_path pdf_path self.text self.sections {} # 用于存储按章节划分的内容 self._raw_pages [] def extract_text(self): 提取PDF中的所有文本并尝试按章节进行初步划分 all_text [] with pdfplumber.open(self.pdf_path) as pdf: for page in pdf.pages: # 提取页面文本并保留一些布局线索如‘\n’ page_text page.extract_text(layoutTrue) if page_text: all_text.append(page_text) self._raw_pages.append(page) self.text \n.join(all_text) return self.text def identify_sections(self): 一个简单的基于规则和关键词的章节识别方法。 实际应用中这里可以做得非常复杂比如利用字体大小、位置信息(pdfplumber提供坐标)。 lines self.text.split(\n) current_section Header section_content [] # 定义可能的关键词可根据实际简历调整 section_keywords { education: [教育背景, 学历, EDUCATION], experience: [工作经历, 工作经验, EXPERIENCE], skills: [专业技能, 技术栈, SKILLS], projects: [项目经历, PROJECTS], contact: [联系方式, CONTACT] } for line in lines: line_stripped line.strip() if not line_stripped: continue # 检查该行是否是新的章节标题 is_section_header False for sec_name, keywords in section_keywords.items(): for kw in keywords: # 简单匹配行内容以关键词开头或包含关键词且较短 if line_stripped.startswith(kw) or (kw in line_stripped and len(line_stripped) 50): # 保存上一个章节的内容 if current_section and section_content: self.sections[current_section] \n.join(section_content).strip() # 开始新章节 current_section sec_name section_content [] is_section_header True break if is_section_header: break if not is_section_header: section_content.append(line_stripped) # 保存最后一个章节 if current_section and section_content: self.sections[current_section] \n.join(section_content).strip() return self.sections def get_basic_info(self) - Dict[str, str]: 从Header或全文提取基本信息姓名、电话、邮箱等 # 这是一个非常简单的正则示例实际需要更健壮的规则 email_pattern r[a-zA-Z0-9._%-][a-zA-Z0-9.-]\.[a-zA-Z]{2,} phone_pattern r1[3-9]\d{9} # 简单匹配中国大陆手机号 # 可以从self.sections[“Header”]或self.text的前几行提取姓名通常是最醒目的一行 name “” header_text self.sections.get(“Header”, self.text[:500]) # 取头部文本 # 假设姓名是Header中非空的第一行简化逻辑 lines header_text.split(‘\n’) for line in lines: if line.strip() and len(line.strip()) 20: # 姓名通常不会太长 name line.strip() break emails re.findall(email_pattern, self.text) phones re.findall(phone_pattern, self.text) return { “name”: name, “email”: emails[0] if emails else “”, “phone”: phones[0] if phones else “”, “raw_header”: header_text[:200] # 返回部分原始文本供参考 } # 示例用法 if __name__ “__main__”: parser ResumeParser(“./my_resume.pdf”) parser.extract_text() sections parser.identify_sections() print(“识别出的章节”, list(sections.keys())) basic_info parser.get_basic_info() print(“基本信息”, basic_info)注意identify_sections函数是一个非常基础的基于规则的实现。对于格式千变万化的简历这可能是最脆弱的部分。在生产环境中你需要根据你的简历模板定制规则或者考虑使用更高级的NLP模型如NER命名实体识别或基于视觉的版面分析来提升鲁棒性。这里提供的是一个起点。3.3 构建MCP Server这是项目的核心。我们创建mcp_server.py文件。我们将使用mcpSDK 的Server类。import asyncio from mcp import Server, types from pdf_parser import ResumeParser from pydantic import BaseModel, Field from typing import List # 定义工具的参数模型Pydantic Model class QuerySkillsArgs(BaseModel): keyword: str Field(description“用于筛选技能的关键词例如 ‘Python’ ‘数据库’。”) class QueryExperienceArgs(BaseModel): company_keyword: str Field(default“”, description“按公司名称关键词过滤工作经历。”) limit: int Field(default5, description“返回经历的最大条数。”) # 初始化我们的简历解析器假设简历路径固定或通过环境变量配置 RESUME_PATH “./my_resume.pdf” parser ResumeParser(RESUME_PATH) parser.extract_text() parser.identify_sections() # 预先解析好 # 创建MCP Server实例 server Server(“resume-mcp-server”) # 注册工具获取基本信息 server.list_tools() async def handle_list_tools() - List[types.Tool]: “”“声明本Server提供的所有工具。”“” return [ types.Tool( name“get_basic_info”, description“获取简历中的个人基本信息包括姓名、邮箱、电话等。”, inputSchema{ “type”: “object”, “properties”: {}, # 此工具无需参数 }, ), types.Tool( name“get_skills”, description“获取简历中列出的所有技能列表。”, inputSchema{ “type”: “object”, “properties”: {}, }, ), types.Tool( name“query_skills”, description“根据关键词查询相关的技能。”, inputSchemaQuerySkillsArgs.schema(), ), types.Tool( name“get_work_experience”, description“获取详细的工作经历列表。”, inputSchemaQueryExperienceArgs.schema(), ), ] # 实现工具获取基本信息 server.call_tool() async def handle_call_tool(name: str, arguments: dict) - List[types.TextContent]: “”“根据工具名和参数执行具体的工具逻辑。”“” if name “get_basic_info”: info parser.get_basic_info() result_text f”””姓名{info[‘name’]} 邮箱{info[‘email’]} 电话{info[‘phone’]} “”” return [types.TextContent(type“text”, textresult_text)] elif name “get_skills”: skills_section parser.sections.get(“skills”, “”) # 简单按逗号、分号或换行分割技能需根据简历实际格式调整 import re skill_list re.split(r‘[,;\n]\s*’, skills_section) skill_list [s.strip() for s in skill_list if s.strip()] result_text “专业技能列表\n” “\n”.join(f”- {skill}” for skill in skill_list) return [types.TextContent(type“text”, textresult_text)] elif name “query_skills”: args QuerySkillsArgs(**arguments) skills_section parser.sections.get(“skills”, “”) import re skill_list re.split(r‘[,;\n]\s*’, skills_section) filtered_skills [s.strip() for s in skill_list if args.keyword in s] result_text f”包含 ‘{args.keyword}’ 的技能\n” “\n”.join(f”- {skill}” for skill in filtered_skills) if filtered_skills else f”未找到包含 ‘{args.keyword}’ 的技能。” return [types.TextContent(type“text”, textresult_text)] elif name “get_work_experience”: args QueryExperienceArgs(**arguments) exp_section parser.sections.get(“experience”, “”) # 这里需要更复杂的解析来分割每一段经历例如按时间或公司分割。 # 此处为演示简单按空行分割。 experiences [exp.strip() for exp in exp_section.split(‘\n\n’) if exp.strip()] if args.company_keyword: experiences [exp for exp in experiences if args.company_keyword in exp] experiences experiences[:args.limit] result_text “工作经历\n” “\n”.join(f”{i1}. {exp}” for i, exp in enumerate(experiences)) return [types.TextContent(type“text”, textresult_text)] else: raise ValueError(f”未知的工具{name}”) async def main(): # 启动Server使用stdio传输这是与MCP客户端通信的标准方式 async with server.run_stdio() as (read_stream, write_stream): await asyncio.gather( server.run(read_stream, write_stream), # 这里可以添加其他异步任务 ) if __name__ “__main__”: asyncio.run(main())这个Server通过标准输入输出stdio与客户端通信。它定义了四个工具并实现了对应的处理函数。当AI Agent客户端调用query_skills工具并传入{“keyword”: “Python”}时Server就会在解析出的技能章节中搜索包含“Python”的项并返回。3.4 配置AI客户端以Claude Desktop为例要让Claude Desktop能连接到我们的Server需要创建一个配置文件。找到Claude Desktop的配置目录macOS:~/Library/Application Support/Claude/claude_desktop_config.jsonWindows:%APPDATA%\Claude\claude_desktop_config.json编辑或创建claude_desktop_config.json 如果文件不存在就新建一个。如果存在在”mcpServers”字段中添加我们的Server配置。{ “mcpServers”: { “resume-server”: { “command”: “python”, “args”: [“/ABSOLUTE/PATH/TO/YOUR/resume-mcp-server/mcp_server.py”], “env”: { “PYTHONPATH”: “/ABSOLUTE/PATH/TO/YOUR/resume-mcp-server” } } } }关键点command必须是python。args必须提供你mcp_server.py文件的绝对路径。env设置PYTHONPATH确保Python能找到你的pdf_parser等模块。同样使用绝对路径。配置完成后重启Claude Desktop。验证连接 重启后在Claude Desktop的聊天框中你应该能看到一个“螺丝刀”或“工具”图标。点击它如果能看到get_basic_info、get_skills等工具列表就说明连接成功了你可以直接在聊天中让Claude调用这些工具例如“请调用get_basic_info工具看看我的基本信息。”4. 避坑指南与进阶优化在实际搭建和运行过程中我遇到了不少问题这里总结一下希望能帮你绕开这些坑。4.1 PDF解析的准确性问题问题解析出的文本顺序混乱特别是对于多栏排版、带有图标和表格的简历。解决优先使用pdfplumber的layoutTrue参数它能更好地保持文本的视觉顺序。利用坐标信息pdfplumber可以提取每个字符的(x0, top, x1, bottom)坐标。对于复杂的版面你可以编写算法根据坐标将文本块重新排序例如先按top坐标排序行再按x0坐标排序行内字符。表格处理pdfplumber的.extract_tables()方法对简单的表格识别效果不错。对于复杂表格可能需要结合camelot或tabula-py库。备用方案如果格式过于复杂可以考虑先将PDF转换为高分辨率图片再使用OCR如Tesseract。但这是最后的手段因为会损失文本的样式和结构信息。4.2 MCP Server连接失败问题Claude Desktop无法识别工具或提示连接错误。排查步骤检查配置文件路径args和env中的路径必须是绝对路径不能使用~或相对路径。检查Python环境确保配置中指定的python命令与你安装依赖的虚拟环境中的是同一个。最稳妥的方式是使用虚拟环境Python解释器的绝对路径作为command。检查依赖确保mcp,pdfplumber等库已安装在Server运行的环境中。查看日志Claude Desktop通常会在其日志文件中输出MCP连接的错误信息。在macOS上日志可能在~/Library/Logs/Claude/下。通过日志可以定位是协议错误、导入错误还是执行错误。手动测试Server可以先在终端直接运行python mcp_server.py看是否有明显的Python错误。4.3 工具设计与Agent体验问题工具不好用AI Agent不知道什么时候调用或者调用后结果不理想。优化建议工具描述要清晰description字段至关重要。清晰说明工具的用途、输入参数的意义。例如query_skills的描述可以写“根据关键词在技能列表中模糊匹配”。设计原子化工具不要设计一个“获取所有信息”的巨无霸工具。而是拆分成get_contact、get_education、get_experience_at_company等小工具。这样AI更容易理解和调用。返回结构化数据虽然MCP工具返回的是TextContent但我们可以返回格式良好的文本如JSON字符串或Markdown列表。这有助于AI进一步处理。例如get_work_experience可以返回每个经历的“公司”、“职位”、“时间”、“描述”字段。提供示例在工具描述中可以加入示例输入。虽然MCP协议本身不支持但可以在描述文本里写例如“例如参数可以是{“keyword”: “Python”}”。4.4 性能与扩展性问题每次查询都重新解析PDF速度慢。解决在Server初始化时__init__或启动时就解析好PDF将结构化的数据如sections字典、向量索引保存在内存中。工具函数直接查询这些内存数据速度极快。进阶扩展多简历支持可以修改Server使其能加载指定路径的PDF。甚至可以将工具设计为load_resume和query_current_resume的组合。向量语义检索如前所述集成sentence-transformers和chromadb。在Server启动时将简历文本分块向量化并存入内存数据库。新增一个semantic_search工具接收自然语言查询返回最相关的文本片段。缓存机制对于频繁的相同查询可以添加一个简单的内存缓存如functools.lru_cache。5. 实际应用场景与效果搭建好这个系统后我将其应用到了几个实际场景中效果提升非常明显。场景一AI辅助求职沟通在准备面试或撰写求职信时我直接对Claude说“根据我的简历帮我起草一封针对[某公司]后端开发岗位的求职信突出我的Java和微服务经验。” Claude会先调用get_skills和get_work_experience工具获取到结构化的技能和项目列表然后生成的信件就能非常具体地引用我简历中的真实项目和技术栈而不是凭空编造。场景二快速生成个人简介在需要不同长度、不同侧重点的个人简介时如社交媒体、技术社区、会议演讲我只需给AI一个指令“用200字总结我的技术背景侧重云计算。” AI通过工具查询能快速抓取到“AWS”、“Docker”、“Kubernetes”等相关技能和经历组合成一段准确的描述。场景三信息核对与提取“我2022年在上一家公司主导的项目叫什么名字”这类需要精确记忆的问题AI通过调用工作经历查询工具能立刻给出答案避免了手动翻找PDF的麻烦。效果对比传统方式打开PDF肉眼查找复制粘贴。容易遗漏格式错乱效率低下。MCP Server方式AI在几秒内通过标准化接口获取精准信息并可直接用于后续的文本生成、分析或决策。整个过程自动化、可编程。这个项目的价值不仅在于“查简历”更在于它展示了一种范式如何将任何有价值的私有文档论文、手册、报告、笔记转化为AI可实时、精准调用的知识源。通过MCP协议我们为自己的数据世界和AI能力之间架起了一座高效、标准的桥梁。你可以举一反三构建你的“论文查询MCP”、“公司制度问答MCP”、“个人知识库MCP”。
返回列表