1. 项目缘起当AI面试官需要一份“活”的简历最近在折腾AI Agent开发时遇到一个挺实际的问题我想让一个负责技术招聘的AI Agent帮我筛选简历或者模拟面试。最直接的想法就是把候选人的PDF简历文件喂给它。但实际操作起来问题一大堆。PDF文件对AI来说就像一本被胶水粘起来的书——它能“看到”文字但很难理解里面的结构。一份简历里的“工作经历”、“项目经验”、“技能清单”是几个关键模块但PDF解析后常常变成一锅粥AI分不清哪段文字属于哪个部分。更别提那些用设计软件做的、文字是矢量图形或者图片的简历了OCR识别效果时好时坏格式更是丢得一干二净。这时候我接触到了MCPModel Context Protocol。简单来说MCP不是一个具体的工具而是一套“协议”或“标准”。它由Anthropic提出目的是让大模型比如Claude能够安全、结构化地访问外部工具、数据和计算资源。你可以把它想象成给AI Agent装上一个标准的“USB接口”通过这个接口AI可以按需、精准地调用各种“外设”服务器而不是一次性吞下所有杂乱无章的原始数据。于是一个想法就诞生了为什么不把静态的PDF简历通过一个MCP Server转换成AI Agent可以按需、精准查询的“动态数据库”呢这样一来AI不需要每次都全文阅读并理解整个PDF它只需要像我们人类面试官一样提出具体问题“请列出这位候选人过去三年的工作经历”、“他在XX项目中承担的核心职责是什么”、“他的Java熟练程度如何”。而我们的MCP Server就扮演那个对简历内容了如指掌的“秘书”快速从结构化的数据中找出答案精准回复给AI Agent。这个项目就是搭建这样一个“简历查询秘书”——一个能将PDF简历内容结构化并通过MCP协议暴露给AI Agent进行智能查询的服务器。2. 核心架构设计简历MCP Server的蓝图要实现“按需查询”核心是构建一个MCP Server。这个Server需要完成两件大事一是解析与结构化把非结构化的PDF简历变成机器好懂的数据二是接口与查询按照MCP协议提供工具让AI Agent能调用这些工具来提问。2.1 技术栈选型与理由首先得把技术架子搭起来。经过一番调研和对比我选择了以下组合后端框架FastAPI。选择它的理由很直接轻量、异步支持好、自动生成API文档Swagger UI。MCP Server本质上是一个HTTP服务器需要处理AI Agent客户端发来的工具调用请求。FastAPI的异步特性在处理可能耗时的PDF解析或复杂查询时能更好地保持服务的响应性。而且其基于Pydantic的数据验证能让我们非常严谨地定义工具输入输出的“形状”这对于和AI交互至关重要——数据格式错一点AI可能就理解不了。PDF解析库pdfplumber 和 PyMuPDF (fitz)。为什么选两个因为它们互补。pdfplumber在提取文本、特别是保留文本的布局信息如坐标、表格结构方面非常出色对于格式规整的简历解析准确率高。而PyMuPDF速度极快处理某些复杂文档或需要渲染页面为图片进行备用OCR时更有优势。我的策略是优先使用pdfplumber如果遇到解析结果异常比如提取出的文字杂乱无章则降级使用PyMuPDF并考虑触发OCR流程。OCR引擎Tesseract。这是开源界的常青树。当简历是扫描件或包含图片形式的文字时它就是救星。虽然云服务如Azure、Google的OCR可能更准但考虑到项目的可离线部署性和成本Tesseract是更独立自主的选择。我会配合Pillow库进行图像预处理如二值化、降噪来提升识别精度。大模型交互可选OpenAI / Anthropic API。注意这里是“可选”且用于Server内部增强功能。MCP Server本身不一定要用大模型但我们可以利用大模型来做一个更智能的“信息提取与标准化”层。例如将pdfplumber提取的原始文本块喂给大模型指令其按照预定格式如JSON Schema输出结构化的简历信息。这能极大提升从五花八门的简历格式中抽取关键信息的鲁棒性。这一步不是必须的但它代表了进阶玩法。MCP协议实现我们需要实现MCP的特定接口。核心是提供tools列表和resources本项目以工具为主。Anthropic提供了官方的mcpPython SDK它大大简化了协议层的实现。我们会用它来注册我们定义的查询工具。整个数据流的设计思路是这样的初始化阶段Server启动后可以预加载一批简历PDF到指定目录或者提供上传接口。解析阶段当一份新简历加入Server后台自动或手动触发解析流水线pdfplumber - 若效果差则降级 - 若为图片则OCR最终产出结构化的简历数据字典或JSON对象。存储阶段将结构化的简历数据存储起来。为了简单第一期我用内存字典或本地小文件如JSON来存key是简历ID可以是文件名哈希value是结构化数据。生产环境可以考虑SQLite或轻量级文档数据库。服务阶段AI Agent如Claude Code通过MCP连接到本Server。Agent看到我们暴露的“工具”例如query_resume。当Agent想了解某个候选人的信息时它调用这个工具传入参数简历ID 问题。Server收到请求后根据简历ID找到结构化数据再根据“问题”在数据中查找或进行简单的自然语言匹配后期可集成向量搜索将结果返回。返回阶段结果以清晰的文本格式返回给AI AgentAgent再将其整合到自己的思考或回复中。2.2 MCP工具的设计定义AI的“提问方式”这是项目的灵魂。我们不能只给AI一个“简历数据”资源让它自己读。我们要设计好工具引导它如何提问。首先我设计了一个核心工具叫get_resume_info。功能获取一份简历的概览信息。参数resume_id(字符串 必填)。比如可以是”candidate_zhangsan_2024”。返回一个结构化的文本摘要例如“候选人张三 | 当前职位高级Java开发工程师 | 工作年限8年 | 主要技能Java, Spring Cloud, MySQL, Redis | 最近公司ABC科技”。这个工具让AI能快速建立对候选人的第一印象。然后是最重要的query_resume_section工具。功能查询简历的特定部分。参数resume_id(字符串 必填)section(字符串 必填)。这里我们不是让AI自由输入问题而是给它一个“下拉菜单”。选项包括work_experience,project_experience,education,skills,self_introduction。这样设计是为了保证查询的精确性避免歧义。query(字符串 可选)。在指定部分内的进一步查询。例如section为work_experiencequery可以为“在2020年至2022年间的工作经历”。返回对应部分的详细内容。如果提供了query则尝试在该部分内容中进行关键词匹配或简单语义筛选后返回。例如AI Agent可以这样发起调用调用工具query_resume_section 参数{“resume_id”: “candidate_zhangsan_2024”, “section”: “project_experience”, “query”: “微服务架构”}Server收到后会在张三的“项目经验”里寻找描述中包含“微服务”字样的项目返回具体描述。最后我还设计了一个辅助工具list_resumes。功能列出当前Server管理的所有简历ID和候选人姓名。参数无。返回一个简历列表。这帮助AI Agent在开始一系列查询前知道有哪些候选人可供选择。注意工具的参数设计遵循“结构化优于非结构化”原则。直接让AI问一个自由文本问题如“张三会Redis吗”虽然更灵活但实现起来复杂很多需要Server端集成一个轻量级的NLP模型或进行复杂的规则匹配容易出错。而通过section参数约束范围能极大提高查询的准确率和Server的响应速度。这是一种在能力与复杂度之间的权衡。3. 实现细节从PDF乱码到结构化的JSON蓝图画好了接下来就是动手编码。最棘手、最核心的一步就是把PDF里那些格式不一的文字变成整齐的结构化数据。3.1 PDF解析的实战与陷阱我首先用pdfplumber写了一个解析函数。基础代码很简单import pdfplumber def parse_resume_with_pdfplumber(pdf_path): text_content [] with pdfplumber.open(pdf_path) as pdf: for page in pdf.pages: # 提取文本并尝试保留布局 text page.extract_text(layoutTrue) # layoutTrue有助于保留一些顺序 if text: text_content.append(text) return \n.join(text_content)但很快就踩了坑。坑一布局保留并非万能。layoutTrue对于简单的两栏简历可能有效但遇到更复杂的排版提取出的文本顺序依然可能是乱的。比如“技能”部分的标题跑到了“工作经历”的内容后面。坑二表格处理。很多简历用表格来排列时间线和经历。pdfplumber的page.extract_table()能提取表格但需要指定区域且不同简历的表格样式天差地别。我的应对策略是“分层解析与启发式规则”原始文本提取先用pdfplumber以layoutFalse更依赖PDF内部的文本流顺序和layoutTrue各提取一次对比结果选择段落顺序更合理的一个。区块探测利用pdfplumber获取每个文本字符的坐标。我写了一个简单的算法将同一水平线上、间距接近的字符聚合成“行”再将垂直方向接近的“行”聚合成“区块”。这样能得到一个粗略的物理布局区块列表。关键词锚定与分割这是将无结构文本转为结构的关键。我定义了一个简历章节的关键词词典SECTION_KEYWORDS { work_experience: [工作经历, 工作经验, employment, work history], project_experience: [项目经验, 项目经历, project experience], education: [教育背景, 学历, education], skills: [专业技能, 技术栈, skills, technologies], # ... 其他部分 }然后遍历文本行或区块寻找包含这些关键词的行将其作为“章节标题”。从这个标题开始到下一个章节标题出现之前的所有内容都归属于这个章节。这是一种简单但非常有效的规则方法。表格特殊处理对于疑似表格的区域我会尝试用pdfplumber的表格提取功能。如果提取成功则将表格内容转换为Markdown格式的字符串附加到对应章节通常是工作或项目经历中这样AI也能较好地理解表格信息。当pdfplumber解析出的文本质量极差比如全是乱码或空白时流程会降级到PyMuPDFimport fitz # PyMuPDF def parse_resume_with_fitz(pdf_path): doc fitz.open(pdf_path) text_content [] for page in doc: text page.get_text(text) # 获取纯文本 text_content.append(text) return \n.join(text_content)如果PyMuPDF提取的文字还是很少且页面中有图像就会触发OCR流程用PyMuPDF将页面渲染为图像然后用Pillow预处理最后交给Tesseract识别。3.2 利用大模型进行智能结构化基于规则的分割在大多数情况下工作良好但对于那些设计花哨、关键词不标准比如用图标代替“工作经历”标题的简历就力不从心了。这时我引入了“大模型清洗”这一步作为增强。我的做法是将前面步骤提取出的“相对干净的全文文本”发送给大模型API比如GPT-4或Claude-3并给出一个非常详细的指令Prompt你是一个专业的简历解析助手。请将以下简历文本严格按照下面的JSON格式输出只输出JSON不要任何其他解释。 JSON格式 { candidate_name: 候选人姓名, work_experience: [{company: 公司名, position: 职位, period: 时间段, description: 工作描述}], project_experience: [{project_name: 项目名, role: 担任角色, period: 时间段, description: 项目描述, technologies: [技术1, 技术2]}], education: [{school: 学校, degree: 学历, major: 专业, period: 时间段}], skills: {programming_languages: [语言1, 语言2], frameworks: [框架1, 框架2], tools: [工具1, 工具2]}, self_introduction: 自我介绍文本 } 需要解析的简历文本 {这里是提取的简历全文}大模型强大的理解能力能很好地从自由文本中抽取出结构化信息并填到指定的JSON字段中。这样得到的结构化数据质量非常高直接就可以用于后续的查询。当然这会产生API调用成本并且依赖网络。因此我在Server中将其配置为一个可选项对于重要或解析困难的简历才开启。实操心得不要试图让大模型直接从原始PDF二进制数据开始处理。先利用本地库进行初步的文本提取和清理哪怕只是把文本按顺序拼出来也能极大降低大模型处理的难度和Token消耗提高解析成功率。这是一种“本地预处理云端精加工”的混合策略。3.3 构建MCP Server并暴露工具有了结构化数据接下来就是用mcp库来搭建Server了。以下是核心代码框架from mcp import Server, Tool import json # 假设我们有一个全局的简历存储字典 resume_store { resume_1: {...}, # 结构化的简历数据 resume_2: {...}, } # 定义工具 list_resumes_tool Tool( namelist_resumes, description列出所有可查询的简历ID和候选人姓名。, input_schema{type: object, properties: {}} # 无输入参数 ) query_resume_section_tool Tool( namequery_resume_section, description查询特定简历的某个部分。, input_schema{ type: object, properties: { resume_id: {type: string, description: 简历的唯一标识符}, section: {type: string, enum: [work_experience, project_experience, education, skills, self_introduction], description: 要查询的简历部分}, query: {type: string, description: 在该部分内进行筛选的查询词可选} }, required: [resume_id, section] } ) # 创建Server实例 server Server(resume-mcp-server) # 注册工具 server.tool() async def list_resumes() - str: 实现列出简历的逻辑 result [] for rid, data in resume_store.items(): result.append(fID: {rid}, 姓名: {data.get(candidate_name, N/A)}) return \n.join(result) if result else 当前没有简历数据。 server.tool() async def query_resume_section(resume_id: str, section: str, query: str None) - str: 实现查询简历部分的逻辑 if resume_id not in resume_store: return f错误未找到ID为 {resume_id} 的简历。 resume_data resume_store[resume_id] if section not in resume_data: return f错误该简历中没有 {section} 部分。 content resume_data[section] # 如果content是列表如工作经历将其格式化为易读文本 if isinstance(content, list): formatted_content [] for item in content: formatted_content.append(json.dumps(item, ensure_asciiFalse, indent2)) result_text \n---\n.join(formatted_content) else: result_text str(content) # 如果提供了query进行简单过滤这里用字符串包含作为示例 if query: if isinstance(content, list): filtered [item for item in content if any(query.lower() in str(v).lower() for v in item.values())] result_text \n---\n.join([json.dumps(item, ensure_asciiFalse) for item in filtered]) if filtered else 未找到匹配内容。 else: if query.lower() in result_text.lower(): pass # 保留全文 else: result_text 未找到匹配内容。 return result_text # 运行Server (例如使用uvicorn) if __name__ __main__: import uvicorn uvicorn.run(server.app, host0.0.0.0, port8000)这样一个具备基本查询功能的简历MCP Server就搭建完成了。它运行在http://localhost:8000等待AI Agent通过MCP客户端来连接和调用工具。4. 与AI Agent集成以Claude Code为例Server跑起来了怎么让AI Agent用上它呢这里以集成到Claude Code或任何支持MCP的Claude环境为例。首先你需要在运行AI Agent的环境比如你的开发机上确保我们的简历MCP Server正在运行python server.py。然后配置AI Agent的MCP客户端来连接我们的Server。具体方式因客户端而异。对于Claude Desktop或支持MCP的IDE插件通常需要一个配置文件。例如在Claude Desktop的配置中位于~/Library/Application Support/Claude/claude_desktop_config.jsonon Mac添加{ mcpServers: { resume-server: { command: python, args: [/绝对路径/到/你的/server.py], env: { PYTHONPATH: /你的/项目/路径 } } } }或者如果Server已经作为独立进程运行可以配置为通过Stdio或HTTP连接。更常见的是通过HTTP连接一个已启动的Server{ mcpServers: { resume-query: { url: http://localhost:8000 } } }配置完成后重启Claude Code。当你在聊天框中与Claude交互时Claude就能“看到”我们Server提供的工具了。你可以这样和它对话你“我现在要筛选一些Java开发者的简历。请先帮我看看有哪些候选人。”Claude调用list_resumes工具 “当前可查询的简历有1. ID: resume_1, 姓名: 张三 2. ID: resume_2, 姓名: 李四。”你“我想了解张三的项目经验特别是涉及微服务和Redis的。”Claude调用query_resume_section工具参数resume_id“resume_1”, section“project_experience”, query“微服务 Redis” “以下是张三相关的项目经验1. 项目XX电商平台微服务重构... 使用了Spring Cloud, Redis作为缓存... 2....”你“他的工作年限是多少”Claude需要先理解“工作年限”可能从work_experience中的时间段推算或直接查询skills部分是否有注明。它可能会先调用query_resume_section查看work_experience然后自己计算或者如果简历结构数据中有直接字段它也可能尝试查询一个不存在的字段而报错。这提示我们工具设计可以更细致比如增加一个get_candidate_summary工具直接返回年限、当前职位等摘要信息。这个过程展示了AI Agent如何“按需”查询而不是被动接收整个文档。它可以根据对话的上下文主动决定调用哪个工具、传递什么参数来获取它完成任务所需的具体信息。5. 踩坑实录与进阶优化方向在实际开发和测试中我遇到了不少问题也总结出一些优化思路。坑一PDF解析的准确率是波动最大的因素。我收集了十几份风格各异的简历进行测试发现对于纯文本、排版简单的简历解析和分割准确率能达到90%以上。但对于使用了复杂字体、大量图标、多栏排版、或者根本就是图片的简历规则方法就捉襟见肘了。解决方案建立了一个“解析信心度”评分机制。根据提取出的文本长度、章节关键词匹配数量、是否存在明显的乱序等指标给每次解析打分。低信心的简历会自动标记并建议通过“大模型清洗”通道重新处理或者提醒用户可能需要手动校正。坑二MCP工具调用的错误处理。最初当工具调用出错如简历ID不存在我直接返回Python异常信息给AI Agent。结果Claude有时会被这些技术性的错误信息搞糊涂影响后续对话。解决方案在所有工具函数内部进行完善的错误捕获并返回对AI友好、可操作的错误信息。例如“未找到该简历请使用list_resumes工具查看可用简历。”而不是“KeyError: resume_xyz”。坑三AI Agent对工具的理解和使用。即使工具描述写得很清楚AI有时也会以意想不到的方式调用或者不理解某些参数的枚举值enum。解决方案一是优化工具的描述description使用更自然、包含示例的语言。二是在Server端对输入参数做更严格的验证和人性化的提示。三是提供更细粒度的工具。例如除了按部分查询可以增加一个search_resume工具允许跨字段的简单关键词搜索满足AI更自由的提问方式。进阶优化方向向量搜索集成当前的查询主要基于关键词匹配。要实现“类似经历查询”或更模糊的语义搜索如“有高并发处理经验的项目”可以将每段工作经历、项目描述的文本转换为向量嵌入embedding存入向量数据库如Chroma、Qdrant。在query参数传入时也将其向量化进行相似度搜索。这能让查询能力产生质变。简历比对与排序可以开发新工具如compare_candidates输入两个简历ID和一个能力维度如“Java深度”、“架构经验”让Server基于结构化数据给出对比分析。或者rank_by_skill输入一个技能列表返回候选人匹配度的排序。持久化与数据库将内存存储换成SQLite或TinyDB支持简历的增删改查CRUD管理并记录每次查询日志用于分析AI Agent的查询模式。标准化与扩展定义更详细、行业通用的简历JSON Schema可参考JSON Resume标准使解析输出的数据结构更统一。同时支持更多文件格式如Word (.docx)、Markdown甚至从LinkedIn等平台导出的数据。这个项目从一个简单的需求点出发却串联起了PDF处理、规则引擎、大模型应用、协议开发等多个知识点。它最让我兴奋的地方在于通过MCP这个“标准接口”我们为AI Agent赋予了安全、可控、深度的数据访问能力。未来不仅仅是简历任何格式的文档产品文档、知识库、报表都可以通过类似的MCP Server变成AI可灵活查询的“知识源”。这或许是构建真正实用AI应用的一个小而美的基石。