# AI智能简历求职系统 **Repository Path**: yakumo12/jianli ## Basic Information - **Project Name**: AI智能简历求职系统 - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-06-06 - **Last Updated**: 2026-06-09 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # AI 简历推荐系统 (JobPulse AI) 面向学生的 AI 驱动简历分析与岗位推荐系统。用户上传或粘贴简历后,系统通过 LLM 自动提取学生画像,结合 RAG 向量检索匹配实习/校招岗位,并支持 AI 自荐信生成。 ## 系统架构 ``` ┌─────────────────────────────────────────────────────────┐ │ 前端 (jobpulse-ai) │ │ React + TypeScript + Vite + Tailwind │ └────────────────────────┬────────────────────────────────┘ │ REST API + SSE (流式) ┌────────────────────────▼────────────────────────────────┐ │ 后端 (aijianli) │ │ FastAPI + Uvicorn │ │ ┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ │ │ │ API 路由层 │ │ 业务服务层 │ │ Agent 运行时 │ │ │ │ routes.py │ │ services/* │ │ runtime.py │ │ │ └──────┬──────┘ └──────┬───────┘ └───────┬─────────┘ │ │ │ │ │ │ │ ┌──────▼────────────────▼───────────────────▼─────────┐ │ │ │ 核心组件 │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────┐ │ │ │ │ │ 技能系统 │ │ 工具注册 │ │ 记忆系统 │ │ 中间件 │ │ │ │ │ │ Skills │ │ Tools │ │ Memory │ │ .ware │ │ │ │ │ └──────────┘ └──────────┘ └──────────┘ └────────┘ │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ │ ┌──────────────────────▼──────────────────────────────┐ │ │ │ 数据层 │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ │ │ SQLite │ │ ChromaDB │ │ 文件存储 │ │ │ │ │ │ 会话/Fact│ │ 向量检索 │ │ 岗位CSV │ │ │ │ │ └──────────┘ └──────────┘ └──────────┘ │ │ │ └─────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘ ``` --- ## Agent 设计与开发 ### 1. Agent 运行时 (`agent/runtime.py`) AgentRuntime 是整个 Agent 的核心,封装了模型、工具、记忆和技能,提供统一的聊天接口。 ```python class AgentRuntime: """Agent 运行时""" def __init__(self, model, tools, fact_store, short_term, skills, ...): self._model = model # 已绑定工具的 LLM self._tools = tools # 工具列表 self._tools_by_name = {...} # 工具名称 -> 工具函数映射 self._fact_store = fact_store # 长期记忆 self._short_term = short_term # 短期记忆 self._skills = skills # 技能字典 self._current_skill_name = initial_skill # 当前技能 ``` **核心方法**: | 方法 | 说明 | |------|------| | `chat()` | 同步聊天接口(内部调用 stream_chat) | | `stream_chat()` | 流式聊天接口,实现手动 agent 循环 | | `switch_skill()` | 切换技能,触发 on_exit/on_enter 钩子 | **Agent 循环流程**: ``` 用户消息 │ ▼ ┌─────────────────────────────────────────────────────────┐ │ stream_chat() 手动 agent 循环(最多 5 轮) │ │ │ │ 1. 构建输入消息 │ │ - 注入当前技能的系统提示词 │ │ - 注入待发送的系统消息(技能切换提示) │ │ - 注入历史聊天消息(最近 6 条) │ │ - 注入用户消息(附带额外上下文) │ │ │ │ 2. 调用 LLM │ │ response = self._model.invoke(input_messages) │ │ │ │ 3. 判断是否有工具调用 │ │ ├── 有 tool_calls → 执行工具 → 添加 ToolMessage → 循环 │ │ └── 无 tool_calls → 输出文本 → 结束 │ │ │ │ 4. 让 skill 决定是否附加 recommendations │ └─────────────────────────────────────────────────────────┘ ``` ### 2. 技能系统 (`agent/skills/`) 技能系统是 Agent 的核心抽象,每个技能定义了独立的业务逻辑、工具集合和提示词。 **BaseSkill 抽象基类**: ```python class BaseSkill(ABC): """技能基类""" @property @abstractmethod def name(self) -> str: ... # 技能名称 @property @abstractmethod def description(self) -> str: ... # 技能描述 @property @abstractmethod def prompt(self) -> str: ... # 系统提示词 @property @abstractmethod def tools(self) -> list[str]: ... # 工具名称列表 @abstractmethod def activate(self, runtime) -> list: ... # 激活技能 def on_enter(self, runtime, **context): ... # 进入钩子 def on_exit(self, runtime): ... # 退出钩子 def format_response(self, tool_invocations, final_text) -> SkillResponse: ... ``` **内置技能**: | 技能 | 类 | 说明 | |------|-----|------| | `resume_analyst` | ResumeAnalystSkill | 简历分析师(默认技能),提供简历解析、岗位推荐、反馈收集 | | `cover_letter` | CoverLetterSkill | 自荐信撰写,为特定岗位生成 600-800 字自荐信 | **技能切换流程**: ``` 用户对某岗位感兴趣 │ ▼ 调用 offer_cover_letter 工具 │ ▼ runtime.switch_skill("cover_letter", job_id=xxx) │ ├── 退出当前技能:resume_analyst.on_exit(runtime) │ ├── 切换技能:self._current_skill_name = "cover_letter" │ ├── 进入新技能:cover_letter.on_enter(runtime, job_id=xxx) │ └── 追加系统消息:"已进入「自荐信撰写助手」模式" │ ▼ 自荐信生成完成后自动切回简历分析师 ``` **添加新技能**: 1. 在 `agent/skills/` 下创建新目录 2. 继承 `BaseSkill` 实现技能类 3. 在 `config/skills.yml` 中注册 ```yaml # config/skills.yml skills: resume_analyst: path: "agent.skills.resume_analyst.skill:ResumeAnalystSkill" cover_letter: path: "agent.skills.cover_letter.skill:CoverLetterSkill" your_new_skill: path: "agent.skills.your_new_skill.skill:YourNewSkill" ``` ### 3. 工具系统 (`agent/tools/`) 工具系统提供 Agent 可调用的外部能力,使用 LangChain `@tool` 装饰器定义。 **ToolRegistry 注册表**: ```python class ToolRegistry: """工具注册表""" def register(self, name: str, fn: Any): ... # 注册工具 def get(self, name: str) -> Any: ... # 获取工具 def all(self) -> list[Any]: ... # 所有工具 def names(self) -> list[str]: ... # 所有工具名称 # 全局单例 TOOL_REGISTRY = ToolRegistry() ``` **内置工具**: | 工具 | 说明 | 参数 | |------|------|------| | `extract_profile_from_resume_text` | 从简历文本提取学生画像 | `text: str` | | `apply_profile_patch` | 手动补录/更新画像字段 | `patch: dict, session_id: str` | | `search_jobs_by_query` | 语义检索相似岗位 | `query: str, top_k: int` | | `search_jobs_with_summary` | 语义检索 + LLM 摘要 | `query: str, top_k: int, max_summary: int` | | `offer_cover_letter` | 切换到自荐信技能 | `job_id: str` | **工具设计说明**: - `apply_profile_patch` 会从 SessionManager 获取当前画像作为基础,避免覆盖已有数据 - `session_id` 参数用于定位会话,获取正确的画像状态 **工具定义示例**: ```python @tool("search_jobs_by_query", args_schema=SearchJobsInput) def search_jobs_by_query(query: str, top_k: int = 15) -> list[dict[str, Any]]: """根据查询文本语义检索相似岗位。""" search_service = VectorStoreSearch() return search_service.search(query, top_k=top_k) ``` **添加新工具**: 1. 在 `agent/tools/agent_tools.py` 中定义参数 Schema(Pydantic BaseModel) 2. 使用 `@tool` 装饰器定义工具函数 3. 工具会自动注册到 `TOOL_REGISTRY` 4. 在对应技能的 `tools` 列表中引用工具名称 ### 4. 记忆系统 (`agent/memory/`) 记忆系统分为长期记忆(FactStore)和短期记忆(ShortTermMemory)。 **FactStore - 长期记忆**: ```python class FactStore: """基于 SQLite 的长期记忆存储""" def read_facts(self, user_id, session_id) -> dict: ... # 读取事实 def write_facts(self, user_id, session_id, facts): ... # 写入事实 def delete(self, user_id, session_id): ... # 删除事实 ``` - 持久化存储到 SQLite - 按 `(user_id, session_id)` 隔离 - 存储画像、偏好、反馈等长期数据 **ShortTermMemory - 短期记忆**: ```python class ShortTermMemory: """基于 LangGraph MemorySaver 的短期记忆""" @property def checkpointer(self) -> MemorySaver: ... ``` - 进程内存储,不写盘 - 按 `thread_id` 隔离 - 由 LangGraph 自动管理消息历史 ### 5. 会话状态机 (`agent/session_graph.py`) 使用 LangGraph `StateGraph` 编排会话级状态变更,所有业务动作都表示为图节点。 **状态字段**: ```python class GraphState(TypedDict, total=False): session_id: str created_at: str updated_at: str title: str # 会话标题 profile: dict[str, Any] # 学生画像 messages: list[dict[str, Any]] # 对话历史 interested_job_ids: list[str] # 感兴趣的岗位 dismissed_job_ids: list[str] # 不感兴趣的岗位 preference_overrides: dict[str, Any] # 偏好覆盖 feedback_preferences: dict[str, list] # 反馈偏好 recommendations: list[dict[str, Any]] # 推荐列表 recommendation_context: dict[str, Any] # 推荐上下文 preference_weights: dict[str, Any] # 偏好权重 pending_action: str # 待处理动作 user_input: str # 用户输入 pending_resume_text: str # 待解析简历文本 pending_profile: dict[str, Any] # 待更新画像 pending_patch: dict[str, Any] # 待补录数据 pending_feedback_job_id: str # 待反馈岗位 ID pending_feedback_action: str # 待反馈动作 ``` **图节点**: | 节点 | 说明 | |------|------| | `chat` | 处理普通聊天消息 | | `resume` | 解析简历文本,提取画像 | | `profile` | 更新画像信息 | | `manual_patch` | 手动补录画像字段 | | `feedback` | 记录用户对岗位的反馈 | **路由逻辑**: ```python def route_pending(state: GraphState) -> str: action = state.get("pending_action", "").strip() if action in {"resume", "profile", "manual_patch", "feedback"}: return action return "chat" # 默认走聊天节点 ``` **状态流转图**: ``` ┌──────────────┐ │ START │ └──────┬───────┘ │ ┌──────▼───────┐ │ route_pending │ └──────┬───────┘ │ ┌──────────────────┼──────────────────┐ │ │ │ ▼ ▼ ▼ ┌─────────┐ ┌──────────┐ ┌──────────────┐ │ chat │ │ resume │ │ profile │ └────┬────┘ └────┬─────┘ └──────┬───────┘ │ │ │ │ │ │ ▼ ▼ ▼ ┌─────────┐ ┌──────────┐ ┌──────────────┐ │ END │ │ END │ │ END │ └─────────┘ └──────────┘ └──────────────┘ │ ▼ ┌──────────────┐ ┌──────────────┐ │ manual_patch │ │ feedback │ └──────┬───────┘ └──────┬───────┘ │ │ ▼ ▼ ┌─────────┐ ┌─────────┐ │ END │ │ END │ └─────────┘ └─────────┘ ``` ### 6. Agent 工厂 (`agent/factory.py`) `build_agent()` 函数负责组装所有组件: ```python def build_agent(skill_names, *, user_id, session_id) -> dict: """构建 Agent 运行时组件""" # 1. 加载配置 agent_config = load_yaml_config("config/agent.yml") # 2. 加载技能 load_all_skills() # 3. 创建模型 model = create_chat_model(model_config) # 4. 收集工具 tools = [] for skill in skills.values(): for tool_name in skill.tools: tool_fn = TOOL_REGISTRY.get(tool_name) if tool_fn: tools.append(tool_fn) # 5. 创建记忆层 short_term = ShortTermMemory() fact_store = FactStore() # 6. 绑定工具到模型 model_with_tools = model.bind_tools(tools) return { "model": model_with_tools, "tools": tools, "fact_store": fact_store, "short_term": short_term, "skills": skills, "initial_skill": initial_skill_name, } ``` --- ## RAG 设计 ### 1. 整体架构 ``` ┌─────────────────────────────────────────────────────────┐ │ RAG 模块架构 │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Agent 层 │ │ │ │ ┌──────────────────┐ ┌──────────────────────────┐ │ │ │ │ │ VectorStoreSearch│ │ RagSummarizeService │ │ │ │ │ │ (线程安全单例) │ │ (检索 + LLM 摘要) │ │ │ │ │ └────────┬─────────┘ └───────────┬──────────────┘ │ │ │ └───────────┼────────────────────────┼────────────────┘ │ │ │ │ │ │ ┌───────────▼────────────────────────▼────────────────┐ │ │ │ 服务层 │ │ │ │ ┌──────────────────────────────────────────────┐ │ │ │ │ │ rag/rag_service.py │ │ │ │ │ │ - upsert_jobs() - delete_jobs() │ │ │ │ │ │ - search_similar_jobs() │ │ │ │ │ │ - summarize_results() │ │ │ │ │ └──────────────────────┬───────────────────────┘ │ │ │ └─────────────────────────┼───────────────────────────┘ │ │ │ │ │ ┌─────────────────────────▼───────────────────────────┐ │ │ │ 存储层 │ │ │ │ ┌──────────────────────────────────────────────┐ │ │ │ │ │ rag/vector_store.py │ │ │ │ │ │ - ChromaDB PersistentClient │ │ │ │ │ │ - OpenAI-compatible embedding │ │ │ │ │ │ - 余弦距离相似度 │ │ │ │ │ └──────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘ ``` ### 2. 向量存储 (`rag/vector_store.py`) 基于 ChromaDB 的向量存储,使用 OpenAI-compatible embedding 模型。 **核心类**: ```python class VectorStore: """基于 ChromaDB + OpenAI-compatible embedding 的向量存储""" def __init__(self): # 初始化 ChromaDB 客户端 self._client = chromadb.PersistentClient(path=persist_dir) self._collection = self._client.get_or_create_collection( name=self._collection_name, metadata={"hnsw:space": "cosine"}, # 余弦距离 ) # 初始化 embedding 客户端 self._embedding_client = OpenAI(base_url=base_url, api_key=api_key) def _embed(self, texts: list[str]) -> list[list[float]]: """通过 OpenAI-compatible 接口生成 embedding""" response = self._embedding_client.embeddings.create( model=self._embedding_model, input=texts, ) return [item.embedding for item in response.data] def upsert_jobs(self, jobs: list[dict]): ... # 批量写入/更新 def delete_jobs(self, job_ids: list[str]): ... # 按 ID 删除 def search(self, query_text, top_k, where): ... # 向量检索 ``` **岗位文本构建**: ```python def _build_job_text(job: dict) -> str: """构建岗位的检索文本,合并职责、技术要求、加分项""" parts = [ job.get("title", ""), job.get("responsibilities", ""), " ".join(job.get("technical_requirements", [])), " ".join(job.get("bonus_points", [])), ] return " ".join(part for part in parts if part) ``` **岗位 ID 生成**: ```python def _generate_id(self, job: dict) -> str: """基于公司+标题+地点生成稳定的岗位向量 ID""" key = f"{job.get('company', '')}::{job.get('title', '')}::{job.get('location', '')}" return hashlib.md5(key.encode("utf-8")).hexdigest() ``` **Metadata 序列化**: ChromaDB 不支持嵌套对象,数组字段需序列化为 JSON 字符串: ```python metadatas = [ { "company": job.get("company", ""), "title": job.get("title", ""), "required_skills": json.dumps(job.get("required_skills", [])), "technical_requirements": json.dumps(job.get("technical_requirements", [])), # ... } for job in jobs ] ``` 检索时自动反序列化: ```python def _deserialize_metadata(self, metadata: dict) -> dict: """将 metadata 中的 JSON 字符串反序列化为数组""" result = dict(metadata) for key in ["required_skills", "majors", "technical_requirements", "bonus_points"]: if key in result and isinstance(result[key], str): result[key] = json.loads(result[key]) return result ``` ### 3. RAG 服务 (`rag/rag_service.py`) 提供高层接口,封装向量存储操作。 ```python # 全局单例,延迟初始化 _vector_store: VectorStore | None = None def get_vector_store() -> VectorStore: global _vector_store if _vector_store is None: _vector_store = VectorStore() return _vector_store def upsert_jobs(jobs: list[dict]): ... # 批量写入/更新岗位 def delete_jobs(job_ids: list[str]): ... # 按 ID 删除 def search_similar_jobs(query_text, top_k, where): ... # 语义检索 def summarize_results(query, results, max_results): ... # LLM 摘要 ``` **LLM 摘要流程**: ``` 用户查询 + 检索结果 │ ▼ 构建参考资料(前 N 个结果) │ ▼ 调用 LLM 生成摘要 │ ├── 成功 → 返回摘要文本 │ └── 失败 → 回退到简单格式化 ``` ### 4. Agent RAG 服务 (`agent/services/`) 为 Agent 提供线程安全的 RAG 服务。 **VectorStoreSearch**: ```python class VectorStoreSearch: """线程安全的单例向量检索服务""" _instance: VectorStoreSearch | None = None _vector_store: VectorStore | None = None _lock = threading.Lock() def __new__(cls) -> VectorStoreSearch: """双重检查锁实现线程安全单例""" if cls._instance is None: with cls._lock: if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance def search(self, query_text, top_k, where) -> list[dict]: """向量检索相似岗位""" store = self._get_vector_store() return store.search(query_text, top_k=top_k, where=where) ``` **RagSummarizeService**: ```python class RagSummarizeService: """线程安全的单例 RAG 摘要服务""" _instance: RagSummarizeService | None = None _lock = threading.Lock() def query(self, query_text, top_k, max_summary) -> dict: """检索并摘要""" # 1. 检索 results = self._search_service.search(query_text, top_k=top_k) # 2. 摘要 summary = self._summarize(query_text, results, max_results=max_summary) return { "results": results, "summary": summary, "total": len(results), } def _summarize(self, query, results, max_results) -> str: """对检索结果进行 LLM 摘要""" if not results: return "未找到匹配的岗位信息。" # 取最相关的前 N 个结果 top_results = results[:max_results] # 构建参考资料 references = [] for i, item in enumerate(top_results, 1): # 格式化岗位信息... # 调用 LLM 生成摘要 try: from model.factory import safe_chat_completion summary = safe_chat_completion(...) if summary: return summary except Exception: logger.warning("LLM 摘要失败,使用回退格式", exc_info=True) # 回退:返回简单的格式化结果 return self._format_results_fallback(top_results) ``` ### 5. RAG 配置 **向量存储配置 (`config/chroma.yml`)**: ```yaml collection_name: jobs # ChromaDB 集合名称 persist_directory: data/vector_store # 持久化目录 chunk_size: 500 # 分块大小 chunk_overlap: 50 # 分块重叠 k: 15 # 默认检索数量 ``` **RAG 检索配置 (`config/rag.yml`)**: ```yaml chunk_size: 500 chunk_overlap: 50 top_k: 15 # 默认返回数量上限 ``` **Agent 配置 (`config/agent.yml`)**: ```yaml model: base_url: ${OPENAI_BASE_URL} # OpenAI-compatible API 地址 api_key: ${OPENAI_API_KEY} # API 密钥 chat_model: ${CHAT_MODEL} # 聊天模型 embedding_model: ${EMBEDDING_MODEL} # Embedding 模型 ``` ### 6. 岗位数据同步 **导入流程**: ``` CSV 文件 │ ▼ admin_service.preview_csv() # 预览 │ ▼ admin_service.import_csv() # 导入到 SQLite │ ▼ rag_service.sync_jobs_on_import() # 同步向量 │ ├── 比对新旧岗位 ID │ ├── 删除被覆盖/移除的岗位向量 │ └── upsert 所有当前岗位(幂等) ``` **向量同步函数**: ```python def sync_jobs_on_import(previous_jobs, current_jobs): """岗位导入后同步向量:比对新旧岗位,upsert 变更部分,删除被覆盖的""" store = get_vector_store() prev_ids = {store._generate_id(job) for job in previous_jobs} curr_ids_map = {store._generate_id(job): job for job in current_jobs} curr_ids = set(curr_ids_map.keys()) # 删除被覆盖/移除的岗位向量 ids_to_delete = list(prev_ids - curr_ids) if ids_to_delete: store.delete_jobs(ids_to_delete) # upsert 所有当前岗位(ChromaDB upsert 幂等) store.upsert_jobs(current_jobs) ``` --- ## LangGraph 会话状态机 后端使用 LangGraph `StateGraph` 编排会话级状态变更,所有业务动作(聊天、解析简历、更新画像、反馈岗位)都表示为图节点,通过 `SqliteSaver` 持久化到 SQLite。 ![alt text](exported_image.png) ### 状态字段(全部 checkpoint 持久化) | 字段 | 说明 | |------|------| | `session_id` / `created_at` / `updated_at` / `title` | 会话元数据 | | `profile` | 学生画像(学历、技能、项目经历等) | | `messages` | 完整对话历史 | | `recommendations` / `recommendation_context` | 推荐岗位列表及上下文 | | `interested_job_ids` / `dismissed_job_ids` | 用户反馈的岗位 | | `preference_overrides` / `feedback_preferences` / `preference_weights` | 偏好与权重 | | `pending_action` / `user_input` / `pending_resume_text` / `pending_profile` / `pending_patch` / `pending_feedback_job_id` / `pending_feedback_action` | 瞬时输入(节点消费后清空) | --- ## 核心功能 | 功能 | 说明 | |------|------| | **简历解析** | 支持上传 PDF/DOCX 文件或直接粘贴文本,LLM 自动提取学生画像(学历、技能、项目经历等) | | **智能推荐** | 基于 ChromaDB 向量检索 + LLM 语义匹配,返回个性化岗位推荐 | | **对话式交互** | Agent 运行时支持流式对话,引导用户补充信息以优化推荐 | | **反馈学习** | 用户对推荐岗位标记"感兴趣/不感兴趣",系统据此调整偏好权重 | | **自荐信生成** | 用户选择心仪岗位后,AI 自动生成 600-800 字的个性化自荐信 | | **岗位管理** | 管理员通过 CSV 批量导入岗位数据,自动同步向量索引 | | **会话管理** | 多会话隔离,支持创建、切换、删除会话 | --- ## 技术栈 ### 后端 (`aijianli/`) | 组件 | 技术 | |------|------| | Web 框架 | FastAPI + Uvicorn | | LLM 编排 | LangChain + LangGraph | | 向量数据库 | ChromaDB (余弦距离) | | Embedding | OpenAI-compatible API | | 数据存储 | SQLite (会话/画像/Fact) | | 数据校验 | Pydantic v2 | | 文档解析 | pypdf + python-docx | ### 前端 (`jobpulse-ai/`) | 组件 | 技术 | |------|------| | 框架 | React 19 + TypeScript | | 构建工具 | Vite | | 样式 | Tailwind CSS | | HTTP 客户端 | Fetch API + SSE | --- ## 快速启动 ### 环境要求 - Python >= 3.11 - Node.js >= 18 - uv (Python 包管理器) ### 1. 后端启动 ```bash cd aijianli # 安装依赖 uv sync # 配置环境变量 cp .env.example .env # 编辑 .env 填入你的 API 配置 ``` `.env` 配置项: ```env OPENAI_API_KEY=your_api_key_here OPENAI_BASE_URL=http://localhost:8000/v1 # OpenAI-compatible API 地址 CHAT_MODEL=your_chat_model_here # 聊天模型名称 EMBEDDING_MODEL=your_embedding_model_here # Embedding 模型名称 ``` ```bash # 启动后端服务 uv run python app.py # 服务运行在 http://127.0.0.1:8000 ``` ### 2. 前端启动 ```bash cd jobpulse-ai # 安装依赖 npm install # 启动开发服务器 npm run dev # 默认访问 http://localhost:3000 ``` ### 3. 验证模型连通性 ```bash cd aijianli uv run python scripts/check_models.py ``` --- ## 目录结构 ``` aijianli/ ├── agent/ # Agent 核心 │ ├── memory/ # 记忆系统 │ │ ├── fact_store.py # 长期事实存储 (SQLite) │ │ └── short_term.py # 短期对话记忆 │ ├── middleware/ # 中间件 │ │ ├── summarization.py # 对话摘要中间件 │ │ └── tool_call.py # 工具调用中间件 │ ├── services/ # Agent 服务 │ │ ├── rag_search.py # RAG 检索服务(线程安全单例) │ │ └── rag_summarize.py # RAG 摘要服务(线程安全单例) │ ├── skills/ # 技能系统 │ │ ├── base.py # 技能抽象基类 │ │ ├── registry.py # 技能注册表 │ │ ├── loader.py # 技能动态加载器 │ │ ├── resume_analyst/ # 简历分析师技能 (默认) │ │ └── cover_letter/ # 自荐信撰写技能 │ ├── tools/ # 工具系统 │ │ ├── agent_tools.py # Agent 工具实现 │ │ ├── base.py # 工具基类 │ │ └── registry.py # 工具注册表 │ ├── factory.py # Agent 工厂 │ ├── runtime.py # Agent 运行时 │ └── session_graph.py # 会话状态图 ├── backend/ # 后端服务 │ ├── api/ │ │ └── routes.py # API 路由定义 │ ├── schemas/ │ │ └── models.py # Pydantic 数据模型 │ ├── services/ # 业务服务 │ │ ├── admin_service.py # 管理员导入服务 │ │ ├── conversation_service.py # 对话服务 │ │ ├── profile_service.py # 画像服务 │ │ ├── recommendation_service.py # 推荐服务 │ │ ├── resume_service.py # 简历解析服务 │ │ └── scoring_service.py # 评分服务 │ └── storage/ │ ├── job_store.py # 岗位数据存储 │ └── session_manager.py # 会话管理器 ├── config/ # 配置文件 │ ├── agent.yml # Agent/模型配置 │ ├── chroma.yml # ChromaDB 配置 │ ├── prompts.yml # Prompt 文件映射 │ ├── rag.yml # RAG 检索配置 │ └── skills.yml # 技能注册配置 ├── prompts/ # Prompt 模板 ├── rag/ # RAG 模块 │ ├── rag_service.py # RAG 统一服务 │ └── vector_store.py # ChromaDB 向量存储 ├── scripts/ # 工具脚本 │ ├── check_models.py # 模型连通性检查 │ └── scrape_sxs.py # 岗位数据爬取 ├── tests/ # 测试 │ └── unit/ # 单元测试 ├── utils/ # 工具函数 │ ├── config_handler.py # YAML 配置加载 │ ├── file_handler.py # 文件处理 │ ├── logger_handler.py # 日志配置 │ ├── path_tool.py # 路径工具 │ └── prompt_loader.py # Prompt 加载器 └── app.py # 应用入口 jobpulse-ai/ ├── src/ │ ├── api/ # API 客户端 │ │ ├── client.ts # HTTP 客户端封装 │ │ ├── sessions.ts # 会话 API │ │ ├── chat.ts # 聊天 API (SSE) │ │ ├── resume.ts # 简历 API │ │ ├── recommendations.ts # 推荐 API │ │ └── admin.ts # 管理 API │ ├── components/ # React 组件 │ │ ├── App.tsx # 应用主组件 │ │ ├── ChatPanel.tsx # 聊天面板 │ │ ├── SessionSidebar.tsx # 会话侧栏 │ │ ├── RecommendationCard.tsx # 推荐卡片 │ │ ├── AdminPanel.tsx # 管理面板 │ │ └── WelcomeScreen.tsx # 欢迎页 │ ├── hooks/ # React Hooks │ │ ├── useChat.ts # 聊天 Hook │ │ ├── useSessionList.ts # 会话列表 Hook │ │ ├── useActiveSession.ts # 当前会话 Hook │ │ └── useRecommendations.ts # 推荐 Hook │ ├── types/ │ │ └── api.ts # TypeScript 类型定义 │ └── main.tsx # 入口文件 └── vite.config.ts # Vite 配置 ``` --- ## API 接口 ### 会话管理 | 方法 | 路径 | 说明 | |------|------|------| | `POST` | `/api/sessions` | 创建新会话 | | `GET` | `/api/sessions` | 获取会话列表 | | `GET` | `/api/sessions/{id}` | 获取会话详情 | | `DELETE` | `/api/sessions/{id}` | 删除会话 | ### 简历与画像 | 方法 | 路径 | 说明 | |------|------|------| | `POST` | `/api/sessions/{id}/resume-text` | 粘贴简历文本 | | `POST` | `/api/sessions/{id}/resume-file` | 上传简历文件 | | `GET` | `/api/sessions/{id}/profile` | 获取学生画像 | | `PUT` | `/api/sessions/{id}/profile` | 更新画像 | | `PATCH` | `/api/sessions/{id}/profile/manual` | 手动补录画像 | ### 对话与推荐 | 方法 | 路径 | 说明 | |------|------|------| | `POST` | `/api/sessions/{id}/chat` | 流式聊天 (SSE) | | `GET` | `/api/sessions/{id}/recommendations` | 获取推荐列表 | | `GET` | `/api/sessions/{id}/recommendation-state` | 获取推荐状态 | | `POST` | `/api/sessions/{id}/feedback` | 提交反馈 | | `POST` | `/api/sessions/{id}/offer-cover-letter` | 切换到自荐信技能 | ### 管理接口 | 方法 | 路径 | 说明 | |------|------|------| | `POST` | `/api/admin/jobs/preview` | 预览 CSV 导入 | | `GET` | `/api/admin/jobs/preview/{id}` | 获取预览结果 | | `POST` | `/api/admin/jobs/import` | 确认导入 | --- ## 配置说明 ### Agent 配置 (`config/agent.yml`) ```yaml model: base_url: ${OPENAI_BASE_URL} # OpenAI-compatible API 地址 api_key: ${OPENAI_API_KEY} # API 密钥 chat_model: ${CHAT_MODEL} # 聊天模型 embedding_model: ${EMBEDDING_MODEL} # Embedding 模型 agent: default_skill: resume_analyst # 默认技能 middleware: summarize_threshold: 4000 # 对话摘要触发阈值 (token) memory: sessions_dir: data/sessions # 会话数据目录 facts_db: data/sessions/facts.db # Fact 存储数据库 ``` ### 技能配置 (`config/skills.yml`) ```yaml skills: resume_analyst: path: "agent.skills.resume_analyst.skill:ResumeAnalystSkill" cover_letter: path: "agent.skills.cover_letter.skill:CoverLetterSkill" ``` 添加新技能:继承 `BaseSkill`,实现 `name/description/prompt/tools/activate`,在 `skills.yml` 注册即可。 --- ## 开发指南 ### 运行测试 ```bash cd aijianli # 安装开发依赖 uv sync --extra dev # 代码检查 uv run ruff check . # 类型检查 uv run mypy . # 运行单元测试 uv run pytest tests/unit ``` ### 添加新工具 1. 在 `agent/tools/agent_tools.py` 中使用 `@tool` 装饰器定义工具函数 2. 工具会自动注册到 `ToolRegistry` 3. 在对应技能的 `tools` 列表中引用工具名称 ### 添加新技能 1. 在 `agent/skills/` 下创建新目录 2. 继承 `BaseSkill` 实现技能类 3. 在 `config/skills.yml` 中注册 --- ## 数据存储 | 存储 | 位置 | 用途 | |------|------|------| | SQLite | `data/sessions/facts.db` | 用户画像、偏好等长期记忆 | | SQLite | `data/sessions/*.json` | 会话状态持久化 | | ChromaDB | `data/vector_store/` | 岗位向量索引 | | CSV | `data/jobs/` | 岗位原始数据 | --- ## 更新日志 ### 2026-06-09 **架构修复**: - 修复 `self._agent` 未定义问题:`chat()` 改为复用 `stream_chat()` - `ProfileService` 改为模块级单例,避免重复实例化 - `session_graph.py` 服务改为惰性初始化,消除模块导入副作用 - `VectorStoreSearch`/`RagSummarizeService` 添加线程安全锁 - `rag_summarize.py` 异常处理添加日志记录 **功能增强**: - `SummarizationMiddleware` 改为 LLM 真实摘要(失败抛异常) - `stream_chat` 注入用户画像(md5 检测变化,只注入一次) - `apply_profile_patch` 添加 `session_id` 参数,从 SessionManager 获取当前画像 **代码清理**: - 删除 `rag_service.summarize_results` 死代码 - `ResumeAnalystSkill` 添加明确的 `on_enter`/`on_exit` 重写 **文档**: - README 新增 Agent 和 RAG 设计文档 --- ## License MIT