From caec59dfd326b134504e151c5cfad68ef6a52944 Mon Sep 17 00:00:00 2001 From: zk Date: Tue, 28 Jul 2026 11:36:05 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0ai=E4=B8=9A=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/ai/extract/__init__.py | 0 app/ai/extract/announcement_extract.py | 42 ++++++++++++++ app/ai/extract/prompt.py | 79 ++++++++++++++++++++++++++ app/tool/json_helper.py | 32 +++++++++++ 4 files changed, 153 insertions(+) create mode 100644 app/ai/extract/__init__.py create mode 100644 app/ai/extract/announcement_extract.py create mode 100644 app/ai/extract/prompt.py create mode 100644 app/tool/json_helper.py diff --git a/app/ai/extract/__init__.py b/app/ai/extract/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/ai/extract/announcement_extract.py b/app/ai/extract/announcement_extract.py new file mode 100644 index 0000000..a2ca466 --- /dev/null +++ b/app/ai/extract/announcement_extract.py @@ -0,0 +1,42 @@ +"""招聘公告信息提取:调用 CONTENT_EXTRACT 模型,输出结构化字典。""" + +from __future__ import annotations + +from langchain_core.messages import HumanMessage, SystemMessage + +from app.ai.extract.prompt import EXTRACT_SYSTEM_PROMPT +from app.ai.model_config import SpiderModel +from app.core.logger import log +from app.tool.json_helper import parse_llm_json + + +def extract_announcement(content: str) -> dict | None: + """从公告全文中提取结构化信息。 + + Args: + content: 公告全文(正文 + 图片 OCR 文字 + 二维码内容拼接)。 + + Returns: + 提取出的字段字典;内容为空、模型调用失败或结果不是字典时返回 None。 + """ + if not content or not content.strip(): + return None + + messages = [ + SystemMessage(content=EXTRACT_SYSTEM_PROMPT), + HumanMessage(content=content), + ] + + try: + response = SpiderModel.CONTENT_EXTRACT.invoke(messages) + except Exception as exc: + log.error(f"公告信息提取失败: {exc}") + return None + + result = parse_llm_json(str(response.content)) + if not isinstance(result, dict): + log.warning(f"公告信息提取结果不是 JSON 对象: {type(result).__name__}") + return None + + log.info(f"公告信息提取完成(字段 {len(result)} 个): {result.get('title')}") + return result diff --git a/app/ai/extract/prompt.py b/app/ai/extract/prompt.py new file mode 100644 index 0000000..eb876fc --- /dev/null +++ b/app/ai/extract/prompt.py @@ -0,0 +1,79 @@ +"""招聘公告信息提取提示词""" + +# 招聘公告信息提取系统提示词:输入公告全文,输出结构化 JSON +EXTRACT_SYSTEM_PROMPT = """你是招聘公告信息提取专家。用户会给你一份校园招聘公告的全文(由网页正文、图片 OCR 文字、二维码解析内容拼接而成,可能存在顺序错乱、重复、噪声)。 + +请从中提取信息,严格按下面的 JSON 结构输出。 + +## 输出格式 + +```json +{ + "company_name": "公司名称", + "title": "公告标题", + "company_intro": "公司简介", + "target_audience": "面向对象", + "major_require": "专业要求", + "remark": "备注", + "written_exam": "有", + "apply_start_time": "2026-07-01 00:00:00", + "apply_end_time": "2026-09-30 23:59:59", + "apply_end_desc": "投递截止描述", + "invite_code": "内推码", + "apply_url": "投递地址", + "apply_email": "投递邮箱", + "source": "信息来源说明", + "publish_time": "2026-07-01 00:00:00", + "recruit_years": [2027], + "batches": ["暑期实习"], + "tags": ["秋招提前批"], + "cities": ["北京市"], + "categories": ["IT技术"], + "educations": ["本科", "硕士"] +} +``` + +## 字段说明 + +- company_name:招聘的公司名称,取全称或公告中最完整的写法 +- title:公告标题,如「XX公司2027届秋季校园招聘正式启动」 +- company_intro:公司介绍段落,原文摘录,最多 300 字。公告中没有公司介绍时,可以根据你自己对该公司的了解补充一段简介;不了解该公司时填 null +- target_audience:面向对象,如「2027届本硕博」「2026届及2027届毕业生」,最多 200 字 +- major_require:专业要求,如「计算机、电子信息、自动化等相关专业」,最多 200 字 +- remark:其他值得注意的补充信息,如薪资待遇、福利、流程安排、注意事项,最多 1000 字 +- written_exam:是否有笔试,只能填「有」「无」「未明确」三者之一 +- apply_start_time:投递开始时间,格式 yyyy-MM-dd HH:mm:ss +- apply_end_time:投递截止时间,格式 yyyy-MM-dd HH:mm:ss +- apply_end_desc:无法解析成具体时间的截止描述,如「尽快投递」「招满即止」「长期有效」,最多 64 字 +- invite_code:专属内推码,只填码本身,不要带「内推码:」这类前缀 +- apply_url:投递地址,只填一个,必须是完整 http/https 链接。二维码解出的投递链接也算。 + 公告中出现多个链接时,根据链接路径判断最可能是网申投递入口的那一个,优先级参考: + 路径含 campus / recruit / job / apply / talent / hr / zhaopin / xiaozhao 等招聘语义词 > + 企业招聘域名(如 campus.xxx.com、hr.xxx.com、xxx.zhiye.com)> + 第三方招聘平台投递页(如 mokahr、beisen、moseeker、workday、talent.liepin 等)> + 其他链接。公告首页、公众号文章、官网首页、下载链接、图片链接一律不算投递地址 +- apply_email:投递邮箱,只填一个。出现多个邮箱时,根据邮箱用途判断最可能用于简历投递的那一个, + 优先级参考:本地部分含 campus / recruit / hr / job / zhaopin / resume 等招聘语义词 > + 企业自有域名邮箱 > 公共邮箱(qq.com / 163.com 等)。咨询、客服、商务合作类邮箱不算投递邮箱 +- source:信息来源说明,如公告中提到的发布方、公众号名称 +- publish_time:公告发布或更新时间,格式 yyyy-MM-dd HH:mm:ss +- recruit_years:招聘届数数组,整数年份,如 [2027]、[2026, 2027] +- batches:批次数组,如「实习」「暑期实习」「寒假实习」「秋招专场」「春招提前批」「春招补招」「正式批」 +- cities:工作城市数组,规范到市级,如「北京市」「深圳市」 +- categories:岗位大类数组,如「IT技术」「人工智能」「通信」「芯片硬件」「产品运营」「职能支持」 +- educations:学历要求数组,如「大专」「本科」「硕士」「博士」 +- tags:其他关键标签数组,如「竞争力薪酬」「六险一金」「可远程」「不限专业」 + +## 提取规则 + +1. 只提取公告中明确出现的信息,不要推测、不要补全、不要编造(company_intro 例外)。 +2. 字符串字段找不到时填 null,数组字段找不到时填 []。不要填「无」「未知」「暂无」这类占位文字(written_exam 例外)。 +3. 时间统一输出 yyyy-MM-dd HH:mm:ss。 + - 只有日期没有时刻:开始时间补 00:00:00,截止时间补 23:59:59。 + - 只写了月日没写年份:结合公告届数或发布时间推断年份;无法推断则填 null。 + - 出现「即日起」「长期开放」这类无法定位到具体日期的表述:对应时间字段填 null,把原文写进 apply_end_desc。 +4. apply_end_time 和 apply_end_desc 可以同时有值,也可以只有一个。 +5. 数组元素去重,保持公告中出现的顺序,单个元素不超过 64 字。 +6. 一份公告涉及多家公司时,以主体招聘方为准。 +7. 直接输出 JSON,不要输出任何解释文字。 +""" diff --git a/app/tool/json_helper.py b/app/tool/json_helper.py new file mode 100644 index 0000000..b56e442 --- /dev/null +++ b/app/tool/json_helper.py @@ -0,0 +1,32 @@ +"""AI 输出 JSON 解析工具 + +将 LLM 返回的可能带 markdown 代码块、思考标签等包裹的文本解析为 Python 对象。 +""" + +import re + +from json_repair import repair_json + +# 匹配 任意内容,用于剥离 DeepSeek R1 等推理模型的思考过程 +# re.DOTALL 让 . 匹配换行,re.IGNORECASE 忽略大小写 +# .*? 非贪婪匹配,避免跨多个 think 标签 +_THINK_RE = re.compile(r".*?", re.DOTALL | re.IGNORECASE) + +# 匹配 ```json ... ``` 代码块,提取中间的 JSON 内容 +# (?:json\w*)? — 可选的语言标记,兼容 json / JSON / jsonc 等,也兼容无标记的裸 ``` +# \s*\n? — 跳过语言标记后的空白和换行 +# (.*?) — 非贪婪捕获代码块内容(第1组) +# \n?\s*``` — 匹配结尾的 ``` +_CODE_BLOCK_RE = re.compile(r"```(?:json\w*)?\s*\n?(.*?)\n?\s*```", re.DOTALL | re.IGNORECASE) + + +def parse_llm_json(text: str): + """解析 AI 输出的 JSON,自动去除思考标签、markdown 代码块,容错处理""" + # 1. 去掉 ... 思考内容 + cleaned = _THINK_RE.sub("", text).strip() + # 2. 如果有 ```json ... ``` 代码块,只取代码块里的内容 + match = _CODE_BLOCK_RE.search(cleaned) + if match: + cleaned = match.group(1).strip() + # 3. repair_json 容错解析:修复不规范的 JSON(多余逗号、缺引号等) + return repair_json(cleaned, return_objects=True)