公司动态

基于MCP协议构建个人简历知识库:从PDF解析到AI工具化查询实战

📅 2026/8/9 14:07:15
基于MCP协议构建个人简历知识库:从PDF解析到AI工具化查询实战
1. 项目概述当AI能读懂你的简历最近在折腾AI Agent开发的朋友估计都绕不开一个词MCPModel Context Protocol。这玩意儿说白了就是给大模型比如Claude、GPT装上一个“外挂大脑”让它能按需读取你电脑里、数据库里、甚至各种API里的信息而不再局限于它自己训练时学到的那些“旧知识”。这就像给一个博学的学者配了一个随身的、实时更新的数字图书馆。那么一个很自然的需求就来了我们每个人手里都有一份或多份PDF格式的简历。无论是求职、接项目还是做个人介绍简历都是我们技能和经历的数字化名片。但这份名片是“静态”的躺在文件夹里AI看不见也摸不着。我们能不能让AI Agent比如Claude Desktop直接“读懂”我的简历并且在我需要查询某个具体技能、某段工作经历时能像查数据库一样精准地给我答案这就是“从PDF到MCP”这个项目的核心。它不是一个简单的PDF文本提取工具而是一个构建专属、动态、可查询个人知识库的实践。通过搭建一个专为简历设计的MCP Server我们将一份冰冷的PDF文件转化为AI Agent可以理解和交互的“活数据”。当你在和Claude讨论一个技术方案突然想不起自己三年前某个项目的具体技术栈时不用再去翻文件夹直接问你的AI助手“查一下我简历里XX项目用了哪些后端技术”它就能立刻给你答案。这个项目非常适合有一定Python基础对AI应用开发感兴趣并且希望将自己的数据与AI深度整合的开发者。它涉及PDF解析、信息结构化、语义理解、以及最新的MCP协议开发是一个能写进简历的、非常“性感”的实战项目。接下来我会带你从零开始拆解每一个环节。2. 核心思路与技术选型2.1 为什么是MCP而不是简单的RAG看到“查询PDF”很多人的第一反应可能是RAG检索增强生成。确实用LangChain或LlamaIndex搭建一个本地的RAG应用也能实现对PDF内容的问答。但这里选择MCP有更深层的考量生态与集成度MCP是Anthropic力推的协议旨在成为AI模型与外部工具、数据源连接的标准。通过MCP Server暴露你的简历数据意味着任何支持MCP协议的客户端如Claude Desktop、Cursor、Windsurf都能无缝使用无需为每个客户端单独开发插件或适配。这是一种“一次开发处处可用”的思路。按需查询与工具化MCP的核心思想是“工具调用”Tool Calling。我们的目标不是让AI通读简历后生成一段摘要而是为AI提供一系列精确的“工具”。例如“查询工作经历”、“按技能筛选项目”、“获取教育背景”。AI Agent可以根据对话的上下文自主决定调用哪个工具获取最相关的信息片段来辅助它的回答。这比RAG的“提问-检索”模式更灵活、更精准。低延迟与实时性MCP Server运行在本地数据查询几乎是瞬时的避免了调用云端RAG服务可能产生的网络延迟。对于简历这种个人私有、高频查询但数据量小的场景本地化方案在速度和隐私上都有绝对优势。学习价值亲手实现一个MCP Server能让你深入理解AI Agent如何与外部世界交互的底层机制这比单纯调用高阶RAG框架更有助于构建复杂的AI应用。因此技术栈的路径很清晰Python作为后端语言利用成熟的PDF解析和NLP库处理简历最后通过MCP SDK将处理后的数据以工具的形式暴露出去。2.2 工具链拆解与选型理由一个完整的“简历MCP Server”包含以下几个核心环节每个环节的选型都经过了实际踩坑后的考量PDF解析与文本提取候选库PyPDF2,pdfplumber,pymupdf (fitz),pdfminer.six。最终选择pdfplumber为主pymupdf为辅。理由pdfplumber在提取表格数据和保持文字顺序方面非常出色这对于解析包含教育背景、工作经历表格的简历至关重要。而pymupdf速度极快且能更好地处理一些复杂排版或扫描件需配合OCR。我们的策略是先用pdfplumber如果遇到解析异常如返回空文本则降级使用pymupdf尝试。信息结构化与命名实体识别核心任务从纯文本中识别出“人名”、“公司名”、“职位”、“时间段”、“技能关键词”等结构化信息。选择规则匹配 轻量级NLP模型。理由简历格式虽多样但仍有较强规律如“公司 | 职位 | 时间”。完全依赖大模型如调用API成本高、延迟大。我们采用折中方案用spaCy的中等模型如zh_core_web_md进行基础的分词和实体识别再结合正则表达式和自定义词典如技术栈关键词列表进行精准匹配和补全。对于极度非标简历可以预留一个“备用方案”调用本地运行的轻量LLM如Qwen2.5-7B-Instruct进行结构化但这会增加复杂度初期不建议。数据存储与查询选择内存数据结构 SQLite。理由一份简历数据量很小完全可以在服务启动时解析并加载到内存中的字典或列表里查询速度最快。但为了持久化和支持更复杂的查询如“找出所有使用了Python和AWS的项目”可以同时将结构化数据存入一个轻量的SQLite数据库。内存数据用于快速工具响应SQLite用于支撑复杂查询逻辑。MCP Server 开发选择官方mcpPython SDK。理由这是最权威、最兼容的选择。Anthropic官方提供了Python和TypeScript的SDK封装了协议通信、工具注册等底层细节让我们能专注于工具逻辑的实现。使用SDK能确保与Claude Desktop等客户端的最大兼容性。开发与调试环境选择Poetry管理依赖 标准的MCP开发流程。理由MCP项目通常依赖明确Poetry能很好地管理虚拟环境和依赖锁定。调试时我们可以使用MCP CLI工具来模拟客户端调用快速验证工具是否按预期工作。注意整个项目应遵循“渐进式复杂”原则。第一版可以先实现最基本的文本提取和关键词查询后续再迭代增加智能解析、多简历管理、技能图谱生成等高级功能。3. 实战构建简历MCP Server3.1 环境准备与项目初始化首先确保你的Python版本在3.8以上。我强烈建议使用pyenv或conda管理Python版本用Poetry管理项目依赖这能让环境隔离和依赖管理变得清晰。# 1. 创建项目目录并初始化Poetry项目 mkdir resume-mcp-server cd resume-mcp-server poetry init -n # 交互式初始化按提示填写项目信息 poetry add mcp pdfplumber pymupdf spacy poetry add --dev pytest # 可选用于测试 # 2. 下载spacy中文模型 poetry run python -m spacy download zh_core_web_md # 3. 创建项目结构 mkdir src/resume_mcp touch src/resume_mcp/__init__.py touch src/resume_mcp/server.py touch src/resume_mcp/parser.py touch src/resume_mcp/models.py touch pyproject.toml在pyproject.toml中需要配置Poetry的脚本入口这是后续通过Claude Desktop加载MCP Server的关键[tool.poetry.scripts] resume-mcp resume_mcp.server:main3.2 核心一简历解析器的实现这是项目的基石目标是将PDF简历转化为结构化的Python对象。我们在models.py中定义数据结构在parser.py中实现解析逻辑。models.py- 定义数据模型from pydantic import BaseModel from typing import List, Optional from datetime import datetime class Experience(BaseModel): company: str position: str start_date: str # 如 “2020-03” end_date: str # 如 “2022-11” 或 “至今” description: List[str] # 工作职责和成就分点存储 skills: List[str] # 从描述中提取的技术关键词 class Education(BaseModel): school: str degree: str major: str graduation_date: str class Resume(BaseModel): name: str email: str phone: Optional[str] summary: Optional[str] experiences: List[Experience] educations: List[Education] skills: List[str] # 独立的技能清单部分 raw_text: str # 保留原始文本用于兜底查询parser.py- 实现解析逻辑这里展示核心的解析函数它融合了PDF提取和基于规则的信息抽取。import re import spacy from typing import List import pdfplumber import pymupdf from .models import Resume, Experience, Education class ResumeParser: def __init__(self): # 加载spacy模型用于中文分词和实体识别 self.nlp spacy.load(zh_core_web_md) # 预编译一些常用的正则表达式提升性能 self.date_pattern re.compile(r(\d{4})[-年](\d{1,2})[-月]?) self.email_pattern re.compile(r[a-zA-Z0-9._%-][a-zA-Z0-9.-]\.[a-zA-Z]{2,}) self.phone_pattern re.compile(r1[3-9]\d{9}) # 简单匹配中国大陆手机号 # 定义一个技术关键词库用于技能提取 self.tech_keywords [Python, Java, Go, AWS, Docker, Kubernetes, MySQL, Redis, React, Vue.js, 机器学习, 深度学习, TensorFlow, PyTorch] # 可根据需要扩充 def parse_pdf(self, pdf_path: str) - Resume: 主解析函数输入PDF路径返回Resume对象 # 1. 提取文本 raw_text self._extract_text_from_pdf(pdf_path) # 2. 使用spacy处理文本获取基础实体 doc self.nlp(raw_text) # 3. 提取基础个人信息基于规则和简单实体 name self._extract_name(doc, raw_text) email self._extract_email(raw_text) phone self._extract_phone(raw_text) # 4. 分割文本尝试定位“工作经历”、“教育背景”等章节 sections self._split_into_sections(raw_text) # 5. 解析工作经历部分这是最难的部分需要较强的规则 experiences self._parse_experience_section(sections.get(work_experience, )) # 6. 解析教育背景部分 educations self._parse_education_section(sections.get(education, )) # 7. 提取技能清单 skills_section sections.get(skills, ) skills self._extract_skills_from_text(skills_section) if skills_section else [] # 同时从工作经历描述中补充技能关键词 for exp in experiences: skills.extend(self._extract_skills_from_text( .join(exp.description))) skills list(set(skills)) # 去重 # 8. 构建并返回Resume对象 return Resume( namename, emailemail, phonephone, experiencesexperiences, educationseducations, skillsskills, raw_textraw_text ) def _extract_text_from_pdf(self, pdf_path: str) - str: 尝试用pdfplumber和pymupdf提取文本提高兼容性 text try: with pdfplumber.open(pdf_path) as pdf: for page in pdf.pages: page_text page.extract_text() if page_text: text page_text \n except Exception as e: print(fpdfplumber failed: {e}, trying pymupdf...) try: doc pymupdf.open(pdf_path) for page in doc: text page.get_text() \n except Exception as e2: print(fpymupdf also failed: {e2}) raise RuntimeError(f无法解析PDF文件: {pdf_path}) return text.strip() def _extract_name(self, doc, text: str) - str: 简单的姓名提取通常出现在文本开头且被识别为PERSON实体 # 规则1找spacy识别出的PERSON实体 for ent in doc.ents: if ent.label_ PERSON: return ent.text # 规则2如果找不到取第一行的前2-4个字符假设是姓名 first_line text.split(\n)[0].strip() # 过滤掉明显的非姓名关键词如“简历”、“个人简历” if 简历 not in first_line and len(first_line) 4: return first_line return 未知 def _split_into_sections(self, text: str) - dict: 根据常见章节标题将简历文本分割成不同部分 # 定义可能的中文章节标题关键词 section_keywords { work_experience: [工作经历, 工作经验, 职业经历, employment], education: [教育背景, 教育经历, 学历, education], skills: [专业技能, 技术技能, 个人技能, skills], project: [项目经历, 项目经验, project], summary: [个人总结, 自我评价, summary] } lines text.split(\n) sections {key: for key in section_keywords} current_section None for line in lines: line_stripped line.strip() # 检查当前行是否是某个章节的标题 for section_name, keywords in section_keywords.items(): if any(keyword in line_stripped for keyword in keywords): current_section section_name break # 匹配到一个就跳出 # 如果不是标题行且当前有活跃的章节则将内容追加到该章节 if current_section and line_stripped and not any(keyword in line_stripped for keyword in [item for sublist in section_keywords.values() for item in sublist]): sections[current_section] line_stripped \n return sections def _parse_experience_section(self, section_text: str) - List[Experience]: 解析工作经历部分这是核心难点需要大量规则和调优 experiences [] # 假设每个工作经历之间用空行或明显的日期/公司分隔 # 这里是一个简化版的解析逻辑实际中需要更复杂的正则和状态机 blocks re.split(r\n\s*\n, section_text) # 按空行分割 for block in blocks: if not block.strip(): continue lines block.strip().split(\n) if len(lines) 2: continue # 假设第一行是“公司 | 职位 | 时间” first_line lines[0] # 尝试用多种分隔符拆分如空格、|、·等 parts re.split(r[|\-·\s]{2,}, first_line) if len(parts) 3: company, position, date_range parts[0], parts[1], parts[2] elif len(parts) 2: company, date_range parts[0], parts[1] position else: # 如果解析失败跳过这个block或尝试其他启发式方法 continue # 解析时间范围 date_matches self.date_pattern.findall(date_range) start_date, end_date 未知, 至今 if len(date_matches) 2: start_date f{date_matches[0][0]}-{date_matches[0][1].zfill(2)} end_date f{date_matches[1][0]}-{date_matches[1][1].zfill(2)} elif len(date_matches) 1: start_date f{date_matches[0][0]}-{date_matches[0][1].zfill(2)} # 剩余行作为描述 description [line.strip(·- ) for line in lines[1:] if line.strip()] # 从描述中提取技能关键词 skills_in_desc self._extract_skills_from_text( .join(description)) exp Experience( companycompany.strip(), positionposition.strip(), start_datestart_date, end_dateend_date, descriptiondescription, skillsskills_in_desc ) experiences.append(exp) return experiences def _extract_skills_from_text(self, text: str) - List[str]: 从文本中匹配预定义的技术关键词 found_skills [] for keyword in self.tech_keywords: if keyword.lower() in text.lower(): found_skills.append(keyword) return found_skills实操心得简历解析是“脏活累活”没有一劳永逸的算法。上述解析器只是一个起点针对你个人简历的格式你需要不断调整正则表达式和分割逻辑。一个实用的技巧是先用自己的几份简历做测试打印出_split_into_sections和_parse_experience_section的中间结果观察规律然后针对性修改规则。对于极度非标的简历可以保留raw_text并提供一个“全文关键词搜索”的兜底工具。3.3 核心二MCP Server工具封装解析器准备好了接下来就是通过MCP协议将数据暴露出去。我们在server.py中创建MCP Server并定义AI可以调用的工具。server.py- MCP服务器主程序import asyncio from contextlib import asynccontextmanager from typing import Any, List import json import sqlite3 from pathlib import Path from mcp import ClientSession, StdioServerParameters from mcp.server import Server from mcp.server.models import InitializationOptions import mcp.server.stdio from mcp.shared.exceptions import McpError from .parser import ResumeParser from .models import Resume # 全局变量用于存储解析后的简历数据 resume_data: Resume None db_conn None asynccontextmanager async def lifespan(server: Server): 服务器生命周期管理启动时加载简历关闭时清理 global resume_data, db_conn print(正在加载简历数据...) # 假设简历PDF放在项目根目录下的data文件夹名为my_resume.pdf pdf_path Path(__file__).parent.parent.parent / data / my_resume.pdf if not pdf_path.exists(): raise FileNotFoundError(f简历文件未找到: {pdf_path}) parser ResumeParser() resume_data parser.parse_pdf(str(pdf_path)) print(f简历加载成功姓名: {resume_data.name}) # 初始化SQLite数据库可选用于复杂查询 db_path Path(__file__).parent.parent.parent / resume_data.db db_conn sqlite3.connect(db_path) _init_database(db_conn, resume_data) yield # 在此处服务器运行 # 清理 if db_conn: db_conn.close() print(简历数据服务已关闭。) def _init_database(conn, resume: Resume): 将简历结构化数据存入SQLite cursor conn.cursor() # 创建表 cursor.execute( CREATE TABLE IF NOT EXISTS experiences ( id INTEGER PRIMARY KEY AUTOINCREMENT, company TEXT, position TEXT, start_date TEXT, end_date TEXT, description TEXT ) ) cursor.execute( CREATE TABLE IF NOT EXISTS skills ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE ) ) cursor.execute( CREATE TABLE IF NOT EXISTS experience_skills ( experience_id INTEGER, skill_id INTEGER, FOREIGN KEY (experience_id) REFERENCES experiences (id), FOREIGN KEY (skill_id) REFERENCES skills (id) ) ) # 清空旧数据简单处理 cursor.execute(DELETE FROM experience_skills) cursor.execute(DELETE FROM experiences) cursor.execute(DELETE FROM skills) # 插入工作经历 for exp in resume.experiences: cursor.execute( INSERT INTO experiences (company, position, start_date, end_date, description) VALUES (?, ?, ?, ?, ?), (exp.company, exp.position, exp.start_date, exp.end_date, json.dumps(exp.description, ensure_asciiFalse)) ) exp_id cursor.lastrowid # 插入技能并关联 for skill in exp.skills: cursor.execute(INSERT OR IGNORE INTO skills (name) VALUES (?), (skill,)) cursor.execute(SELECT id FROM skills WHERE name ?, (skill,)) skill_id cursor.fetchone()[0] cursor.execute(INSERT INTO experience_skills (experience_id, skill_id) VALUES (?, ?), (exp_id, skill_id)) conn.commit() # 创建MCP Server实例 server Server(resume-mcp-server) server.list_tools() async def handle_list_tools() - list[dict[str, Any]]: 向客户端声明本服务器提供的所有工具 return [ { name: get_resume_basic_info, description: 获取简历持有人的基本信息包括姓名、联系方式和简要总结。, inputSchema: { type: object, properties: {} } }, { name: query_work_experience, description: 查询工作经历。可以按公司名称、职位、时间段或技能关键词进行过滤。, inputSchema: { type: object, properties: { company_keyword: { type: string, description: 公司名称关键词模糊匹配。 }, position_keyword: { type: string, description: 职位关键词模糊匹配。 }, skill_keyword: { type: string, description: 需要具备的技能关键词。 }, time_period: { type: string, description: 时间段如2020-2022查询在此时间段内的工作经历。 } } } }, { name: search_resume_by_keyword, description: 在简历全文包括工作描述、技能列表等中搜索包含特定关键词的内容。这是一个兜底搜索工具当其他工具无法满足时使用。, inputSchema: { type: object, properties: { keyword: { type: string, description: 需要搜索的关键词。, required: True }, max_results: { type: integer, description: 返回的最大结果数量默认5。, default: 5 } } } }, { name: list_all_skills, description: 列出简历中提到的所有技能关键词。, inputSchema: { type: object, properties: {} } } ] server.call_tool() async def handle_call_tool(name: str, arguments: dict[str, Any]) - list[dict[str, Any]]: 处理客户端发来的工具调用请求 global resume_data, db_conn if not resume_data: raise McpError(简历数据未加载。) if name get_resume_basic_info: return [{ type: text, text: f姓名{resume_data.name}\n邮箱{resume_data.email}\n电话{resume_data.phone or 未提供}\n个人总结{resume_data.summary or 暂无} }] elif name query_work_experience: results [] query_params arguments for exp in resume_data.experiences: match True # 公司关键词过滤 if company_kw : query_params.get(company_keyword): if company_kw.lower() not in exp.company.lower(): match False # 职位关键词过滤 if position_kw : query_params.get(position_keyword): if position_kw.lower() not in exp.position.lower(): match False # 技能关键词过滤 (检查该经历是否包含此技能) if skill_kw : query_params.get(skill_keyword): if skill_kw not in exp.skills: match False # 时间段过滤 (简化处理仅检查开始年份) if time_period : query_params.get(time_period): # 假设time_period格式为“2020-2022” try: start_year, end_year map(int, time_period.split(-)) exp_start_year int(exp.start_date.split(-)[0]) if not (start_year exp_start_year end_year): match False except: pass # 如果解析失败跳过时间过滤 if match: desc_text \n.join([f • {item} for item in exp.description]) results.append(f- **{exp.company}** | {exp.position} ({exp.start_date} 至 {exp.end_date})\n 技能{, .join(exp.skills)}\n 描述\n{desc_text}) if results: return [{type: text, text: \n\n.join(results)}] else: return [{type: text, text: 未找到匹配的工作经历。}] elif name search_resume_by_keyword: keyword arguments[keyword].lower() max_results arguments.get(max_results, 5) found_snippets [] # 在原始文本中搜索 lines resume_data.raw_text.split(\n) for line in lines: if keyword in line.lower(): found_snippets.append(line.strip()) if len(found_snippets) max_results: break if found_snippets: return [{type: text, text: f找到包含“{keyword}”的内容\n \n.join([f- {s} for s in found_snippets])}] else: return [{type: text, text: f未在简历中找到包含“{keyword}”的内容。}] elif name list_all_skills: all_skills resume_data.skills if all_skills: return [{type: text, text: 简历中提到的技能有\n , .join(all_skills)}] else: return [{type: text, text: 简历中未识别出明确的技能列表。}] else: raise McpError(f未知工具: {name}) async def main(): 主函数启动Stdio服务器 async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): async with ClientSession(read_stream, write_stream) as session: await session.initialize( server_nameresume-mcp-server, server_version0.1.0, capabilitiesserver.get_capabilities( initialization_optionsInitializationOptions( server_nameresume-mcp-server ) ) ) # 运行服务器开始处理请求 await server.run(session, lifespanlifespan) if __name__ __main__: asyncio.run(main())3.4 配置与运行连接Claude Desktop代码写好了如何让Claude Desktop识别并使用我们的MCP Server呢这需要通过Claude Desktop的配置文件来添加。首先在本地安装并运行你的MCP Server进行测试# 在项目根目录下 poetry install poetry run resume-mcp如果看到“正在加载简历数据...”和“简历加载成功”的输出说明服务器已启动并在等待Stdio连接。配置Claude Desktop找到Claude Desktop的配置文件夹。macOS:~/Library/Application Support/Claude/claude_desktop_config.jsonWindows:%APPDATA%\Claude\claude_desktop_config.json如果文件不存在就创建一个。编辑这个JSON文件添加你的MCP Server配置{ mcpServers: { resume-mcp: { command: poetry, args: [ run, resume-mcp ], env: { PYTHONPATH: /你的/项目/绝对/路径/resume-mcp-server/src }, cwd: /你的/项目/绝对/路径/resume-mcp-server } } }关键提示cwd当前工作目录必须设置为你的项目根目录PYTHONPATH需要指向包含你代码的src目录否则poetry run会找不到模块。command也可以直接指向你虚拟环境中的Python解释器路径但用poetry run更便于管理依赖。重启Claude Desktop。验证连接重启后在Claude Desktop的聊天界面你应该能看到一个“工具”图标可能是个螺丝刀或连接图标。点击它如果能看到“resume-mcp”服务器以及下列出的工具get_resume_basic_info,query_work_experience等就说明配置成功了4. 效果演示与高级查询场景配置成功后你就可以在Claude Desktop中与你的简历对话了。以下是一些真实的对话示例场景一快速自我介绍你“帮我介绍一下我自己。”Claude调用get_resume_basic_info工具 “根据您的简历您的姓名是[你的名字]邮箱是[你的邮箱]...”场景二针对性技能查询你“我有哪些项目用到了Docker和Kubernetes”Claude调用query_work_experience工具skill_keyword设为“Docker”和“Kubernetes” “在[XX公司]的[XX项目]中您负责...技术栈包括Docker容器化和Kubernetes集群管理...”场景三模糊记忆回溯你“我记得20年到22年之间在某个电商公司做过具体做了什么”Claude调用query_work_experience工具time_period设为“2020-2022”company_keyword设为“电商” “在[XX电商公司]2020-05至2022-08您担任后端开发工程师主要职责包括...”场景四全文深度挖掘你“我的简历里提到过‘性能优化’吗具体在哪些地方”Claude调用search_resume_by_keyword工具keyword设为“性能优化” “找到以下相关内容1. 在[项目A]描述中‘负责数据库查询性能优化将接口响应时间降低50%’。 2. 在技能章节提到‘擅长系统性能调优与瓶颈分析’。”你会发现AI不再是基于过时知识的泛泛而谈而是能精准调用你“数字分身”中的具体信息来回答问题交互的深度和实用性大大提升。5. 避坑指南与进阶优化在实际开发和使用的过程中我踩过不少坑这里总结几个关键点PDF解析质量是最大瓶颈市面上的简历模板千奇百怪双栏排版、图标、特殊字体都会导致解析失败或乱序。解决方案除了使用pdfplumber和pymupdf双引擎对于重要的简历可以手动校对解析后的raw_text或者考虑将PDF转换为高精度的图片再使用OCR如paddleocr或easyocr进行识别虽然更重但准确率更高。MCP Server配置路径问题claude_desktop_config.json中的路径必须是绝对路径。使用相对路径或~家目录符号会导致Claude Desktop启动失败且无明确报错。一个调试技巧是先在终端手动运行poetry run resume-mcp确保命令能独立运行再配置到Claude中。工具描述description至关重要MCP工具的描述是AI决定是否调用、如何调用的主要依据。描述必须清晰、准确说明工具的用途、输入参数的格式和含义。模糊的描述会导致AI错误调用或干脆不调用。结构化数据的局限性我们的解析器不可能100%准确。兜底方案是保留并利用好raw_text。search_resume_by_keyword这个全文搜索工具就是为此而生。当结构化查询无果时AI可以退而求其次使用全文搜索总能找到一些相关信息。性能与扩展性多简历支持当前设计只加载一份简历。可以修改lifespan和工具函数支持从某个文件夹加载所有PDF并通过工具参数指定查询哪份简历例如query_work_experience(resume_name张三_2024.pdf, ...)。增量更新可以监听简历文件夹当文件变化时自动重新解析实现简历内容的“热更新”。向量化搜索对于raw_text可以将其切分成片段chunk使用sentence-transformers等库生成向量存入ChromaDB或FAISS。这样search_resume_by_keyword工具可以升级为语义搜索即使关键词不匹配也能找到相关内容。错误处理与日志MCP Server运行在后台出错时不易察觉。务必在代码中加入详细的日志记录如Python的logging模块记录解析过程、工具调用和错误信息便于排查。这个项目从想法到实现最耗时的部分往往是简历解析规则的打磨和MCP配置的调试。但一旦跑通你将获得一个高度个性化、随时待命的AI数字助手它对你职业生涯的了解可能比你自己记忆的还要清晰和即时。这不仅仅是查询简历更是构建个人专属知识库与AI高效协同的一个绝佳起点。