ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

OJCP开放职位消费协议:面向AI Agent的职位数据标准化实践

OJCP开放职位消费协议:面向AI Agent的职位数据标准化实践 最近在落地 AI Agent 招聘助手的时候遇到一个很现实的问题职位数据散落在不同平台、不同接口、不同字段定义里Agent 想要统一消费这些数据非常困难。如果有一套面向 Agent 消费的开放职位数据协议整个链路就会清晰很多。这也正是 OJCPOpen Job Consumption Protocol这个方向要解决的问题。本文将围绕 OJCP 的设计理念、核心数据模型、Agent 消费流程、服务端接入示例以及常见踩坑点展开希望给正在做 Agent 开发、职位聚合平台或招聘数据服务的朋友一些可落地的参考。1. 背景与核心概念1.1 为什么需要面向 Agent 的职位数据协议先看一个常见场景你正在开发一个 AI 招聘助手用户说“帮我找最近一周发布的、深圳的、Java 后端岗位要求月薪 25K 以上”。传统做法是去调用某个招聘平台的开放 API然后把返回的 JSON 字段映射到自己的数据结构。问题也随之而来每个平台的字段命名不同有的叫salary、有的叫payRange、有的叫compensation。数据嵌套层级不同有的返回到data.list有的返回到content.positions。匹配规则不透明平台可能默认做了相关性排序Agent 无法判断数据是否完整。状态字段混乱status可能是数字、字符串、枚举含义完全不一样。当你的 Agent 只需要对接一个平台时这些问题还能通过硬编码解决但一旦要对接多个来源或者想要做一个通用的职位数据消费层协议不一致的维护成本会快速膨胀。OJCP 的思路是在职位数据提供方和 Agent 消费方之间定义一个统一的、可扩展的开放协议。数据提供方按协议输出标准化结构Agent 按协议解析数据两边都不需要关心对方内部实现。1.2 OJCP 是什么从命名来看OJCP 是 “Open Job Consumption Protocol” 的缩写翻译过来是“开放职位消费协议”。它不是一个具体的软件也不是某个公司的 SDK而是一套描述职位数据如何暴露、如何获取、如何解析的规范。它关注的核心问题有三个数据格式职位数据应该包含哪些字段字段类型和含义是什么。交互方式Agent 如何发现职位数据源如何发起查询如何获取详情。状态语义职位从发布、下架、暂停到关闭状态如何表达。你可以把它理解成职位数据领域的“通用语言”。只要提供方和消费方都遵循这套语言Agent 就能像阅读标准文档一样读取职位数据。1.3 OJCP 与 MCP、普通招聘 API 的区别近两年 Agent 领域经常提到 MCPModel Context Protocol、Agent Skills、Agent CLI 等概念很多人会把它们混在一起这里简单区分一下。概念定位与 OJCP 的关系MCP大模型与外部工具之间的标准化调用协议OJCP 可以作为 MCP Server 暴露的某一种数据协议两者是不同层面的东西Agent SkillsAgent 可复用技能的定义OJCP 可以理解为职位消费场景下的“领域技能”普通招聘 API面向人类开发者设计的接口OJCP 更强调数据结构的可解释性、状态语义明确性适合 Agent 直接消费简单来说普通招聘 API 是给人看的返回数据需要人去读文档、写映射OJCP 是给 Agent 消费的数据结构和语义尽量做到自解释减少 Agent 的猜测成本。2. 协议设计目标与适用范围2.1 设计目标一套协议如果设计得太复杂Agent 解析成本高平台接入意愿低设计得太简单又覆盖不了真实业务。OJCP 的设计目标可以拆成以下几条数据自描述 Agent 拿到一条职位记录后不需要外部文档仅凭字段名和结构就能理解这条数据的含义。格式中立 协议定义的是数据语义和结构不绑定特定传输方式。HTTPS 可以gRPC 可以甚至离线 JSON 文件也可以。渐进式扩展 基础字段是所有接入方必须支持的扩展字段允许各自补充。比如基础字段有title、location、salary扩展字段可以有equity、visaSponsorship。状态机清晰 职位在不同平台上有不同的生命周期协议要定义一个通用状态机避免出现“A 平台下架B 平台关闭”这种语义混乱。查询语义统一 Agent 发起查询时筛选条件、排序方式、分页方式要有一致约定。2.2 适用场景从实际经验来看OJCP 适合这几类场景招聘聚合平台把多个渠道的职位数据统一转为 OJCP 格式输出给 Agent 或下游系统。AI 招聘助手Agent 通过标准接口获取职位数据做筛选、匹配、推荐。企业内部职位流转HR 系统与内部招聘工具之间的数据同步。数据服务商向外部提供职位数据订阅服务时用 OJCP 作为输出标准。如果你的项目只是内部使用的简单职位表不需要对接外部 Agent那引入这套协议会有一定成本可以根据实际情况权衡。3. OJCP 核心数据模型设计3.1 基础字段设计职位数据的核心字段不需要太多但每个字段都要语义清晰。下面是一份建议的基础字段设计实际使用时可在此基础上扩展。字段名类型必填说明idstring是职位唯一标识建议由提供方生成titlestring是职位名称descriptiontext是职位描述支持纯文本或结构化文本locationobject是工作地点包含城市、区域、是否远程salaryobject否薪资范围包含币种、最小值、最大值、周期employmentTypestring是工作类型如 full_time、part_time、contractstatusstring是职位状态遵循协议状态机publishedAtstring是发布时间ISO 8601 格式updatedAtstring是更新时间ISO 8601 格式companyobject否公司信息包含名称、Logo、规模等skillsarray否技能标签数组sourcestring是数据来源标识用于追踪数据归属这里强调几个容易踩坑的点salary不要设计成字符串比如 “25K-35K”。Agent 解析这种字符串需要额外的模式匹配一旦格式不统一就会出错。建议拆成min、max、currency、period。employmentType使用枚举字符串不要用数字。数字无法自解释Agent 必须查表才能理解。时间字段统一使用 ISO 8601不要用时间戳。时间戳虽然简洁但缺少时区语义Agent 需要额外换算。3.2 状态机定义职位状态看起来简单实际在多方协作时会很混乱。一个职位可能经历以下过程发布方创建职位。审核通过后上线。招聘满员后暂停。最终关闭或删除。如果协议不统一状态定义A 平台把暂停叫pausedB 平台叫on_holdC 平台叫suspendedAgent 处理起来会非常痛苦。建议定义如下状态集合状态值含义后续可能状态draft草稿尚未公开可见published、closedpublished已发布Agent 可正常获取paused、closedpaused暂停招聘数据仍存在但不应推荐published、closedclosed已关闭不再招聘draftremoved已删除应从本地缓存中移除无需要注意的是removed状态代表数据不可恢复提供方在返回该状态时应确保 Agent 删除本地缓存。3.3 查询与分页约定Agent 需要按条件筛选职位协议中建议约定一套统一的查询参数风格。常见查询参数q全文搜索关键词。location地点筛选如shenzhen或beijing。employment_type工作类型筛选。min_salary、max_salary薪资范围筛选。status状态筛选默认只返回published。published_after按发布时间筛选。page、page_size分页参数。分页响应建议包含以下字段{ data: [], pagination: { page: 1, page_size: 20, total: 156, has_more: true } }has_more字段非常重要Agent 可以通过它判断是否继续翻页避免额外请求。4. Agent 消费 OJCP 数据实战4.1 场景设定假设我们现在要开发一个 AI 招聘助手从一个遵循 OJCP 协议的职位数据服务中拉取深圳地区的 Java 后端岗位然后交给大模型做筛选和推荐。整体流程Agent 构造查询请求。职位服务返回 OJCP 格式的数据。Agent 解析数据并转换为内部结构。Agent 调用大模型对职位进行筛选匹配。Agent 返回推荐结果给用户。4.2 提供方接口示例FastAPI我们先实现一个简单的 OJCP 职位数据服务接口。这里以 Python FastAPI 为例演示如何把职位数据以 OJCP 格式暴露出去。# 文件路径main.py from datetime import datetime, timezone from typing import List, Optional from fastapi import FastAPI, Query from pydantic import BaseModel app FastAPI(titleOJCP Job Data Service) class Salary(BaseModel): currency: str min: Optional[int] None max: Optional[int] None period: str year class Location(BaseModel): city: str region: Optional[str] None remote: bool False class JobPosting(BaseModel): id: str title: str description: str location: Location salary: Optional[Salary] None employment_type: str status: str published_at: str updated_at: str company: Optional[dict] None skills: List[str] [] source: str # 模拟数据库中的职位数据 JOBS_DB [ JobPosting( idjob-001, title高级Java后端工程师, description负责核心交易系统的设计与开发..., locationLocation(city深圳, region南山区, remoteFalse), salarySalary(currencyCNY, min25000, max40000, periodmonth), employment_typefull_time, statuspublished, published_at2025-01-10T10:00:00Z, updated_at2025-01-10T10:00:00Z, company{name: 示例科技, size: 200-500人}, skills[Java, Spring Boot, MySQL, Redis], sourcedemo-source, ), JobPosting( idjob-002, titleJava开发工程师远程, description参与金融风控系统研发支持远程办公..., locationLocation(city上海, regionNone, remoteTrue), salarySalary(currencyCNY, min20000, max30000, periodmonth), employment_typefull_time, statuspublished, published_at2025-01-08T10:00:00Z, updated_at2025-01-09T10:00:00Z, company{name: 金融科技公司, size: 50-150人}, skills[Java, 分布式系统, Kafka], sourcedemo-source, ), ] app.get(/ojcp/jobs, response_modeldict) def list_jobs( q: Optional[str] None, location: Optional[str] None, employment_type: Optional[str] None, min_salary: Optional[int] None, status: str Query(published, description职位状态), page: int Query(1, ge1), page_size: int Query(20, ge1, le100), ): 以 OJCP 协议返回职位列表。 这里演示了过滤、分页的参考实现。 filtered [job for job in JOBS_DB if job.status status] if q: filtered [job for job in filtered if q.lower() in job.title.lower()] if location: filtered [job for job in filtered if job.location.city location] if employment_type: filtered [job for job in filtered if job.employment_type employment_type] if min_salary is not None: filtered [ job for job in filtered if job.salary and job.salary.min and job.salary.min min_salary ] total len(filtered) start (page - 1) * page_size end start page_size page_data filtered[start:end] return { data: [job.model_dump() for job in page_data], pagination: { page: page, page_size: page_size, total: total, has_more: end total, }, protocol: ojcp, version: 1.0, }这段代码实现了最基础的 OJCP 列表接口。注意几个细节响应中带有protocol和version字段方便 Agent 识别协议版本。分页参数限制了page_size最大为 100避免单次返回数据量过大。model_dump()是 Pydantic v2 的写法如果使用 Pydantic v1需要改成dict()。启动服务uvicorn main:app --reload --port 8000访问http://127.0.0.1:8000/ojcp/jobs?location深圳可以验证返回结果。4.3 Agent 消费端示例Python接下来写一个 Agent 端的消费程序从 OJCP 服务拉取数据并交给大模型处理。# 文件路径agent_consumer.py import requests class OJCPClient: 一个极简的 OJCP 客户端负责从服务端拉取职位数据。 更完整的实现可以加入重试、缓存、异步请求等能力。 def __init__(self, base_url: str, timeout: int 10): self.base_url base_url.rstrip(/) self.timeout timeout def fetch_jobs(self, params: dict) - list: 按照 OJCP 协议拉取职位列表自动处理分页。 all_jobs [] page 1 page_size params.get(page_size, 20) while True: query_params {**params, page: page, page_size: page_size} resp requests.get( f{self.base_url}/ojcp/jobs, paramsquery_params, timeoutself.timeout, ) resp.raise_for_status() payload resp.json() # 协议版本校验 if payload.get(protocol) ! ojcp: raise ValueError(响应不是 OJCP 协议格式) data payload.get(data, []) all_jobs.extend(data) pagination payload.get(pagination, {}) if not pagination.get(has_more, False): break page 1 return all_jobs def filter_jobs_for_user(jobs: list, user_keyword: str, user_min_salary: int) - list: 从 OJCP 数据中筛选岗位。 这里只做规则过滤实际项目中可以接入大模型做语义匹配。 matched [] for job in jobs: title job.get(title, ) salary_obj job.get(salary) or {} if user_keyword.lower() not in title.lower(): continue salary_min salary_obj.get(min) or 0 if salary_min user_min_salary: continue matched.append( { id: job.get(id), title: title, city: job.get(location, {}).get(city), company: (job.get(company) or {}).get(name), salary: salary_obj, description: job.get(description), published_at: job.get(published_at), } ) return matched if __name__ __main__: client OJCPClient(base_urlhttp://127.0.0.1:8000) jobs client.fetch_jobs( params{ location: 深圳, employment_type: full_time, status: published, page_size: 50, } ) print(f共拉取职位: {len(jobs)} 条) result filter_jobs_for_user(jobs, user_keywordJava, user_min_salary25000) print(f符合用户条件的职位: {len(result)} 条) for item in result: print(f- {item[title]} | {item[city]} | {item[company]})在这个示例中OJCPClient自动处理了分页逻辑Agent 只需要传入查询参数即可拿到全部符合条件的职位。4.4 接入大模型做语义推荐规则筛选能解决“Java 25K 深圳”这样的明确条件但用户往往会有模糊需求比如“想找一个不那么卷的公司”或“希望团队技术氛围好”。这时候需要借助大模型对职位描述进行语义分析。下面是一个接入大模型的示例思路核心是把 OJCP 数据转换为适合 LLM 的文本片段# 文件路径llm_recommend.py # 注意这里演示的是流程思路实际调用时请根据你使用的模型服务调整。 import json # 假设这是从 OJCP 接口拿到的职位数据 job_data { id: job-001, title: 高级Java后端工程师, description: 负责核心交易系统的设计与开发团队技术氛围好没有强制加班文化。, location: {city: 深圳, region: 南山区, remote: False}, salary: {currency: CNY, min: 25000, max: 40000, period: month}, company: {name: 示例科技, size: 200-500人}, skills: [Java, Spring Boot, MySQL, Redis], } def build_prompt(user_requirement: str, job: dict) - str: 将 OJCP 职位数据转换为 LLM 可处理的 prompt。 job_summary json.dumps(job, ensure_asciiFalse, indent2) return f 用户需求{user_requirement} 职位数据OJCP 格式 {job_summary} 请根据用户需求评估该职位的匹配度并给出推荐理由。 如果匹配请说明哪些信息让该职位适合用户如果不匹配请给出原因。 prompt build_prompt(我在找一份能兼顾生活和工作的后端岗位, job_data) print(prompt) # 实际调用大模型时 # response your_llm_client.chat.completions.create( # modelyour-model, # messages[{role: user, content: prompt}], # ) # print(response.choices[0].message.content)这里要强调的是不要把全部原始描述直接丢给大模型可以先做字段裁剪和清洗减少 token 消耗。对于长文本可以只保留description的前 N 个字符。4.5 运行与验证按照上面的步骤先启动 FastAPI 服务再运行消费端脚本。预期输出类似共拉取职位: 1 条 符合用户条件的职位: 1 条 - 高级Java后端工程师 | 深圳 | 示例科技如果返回 0 条可以排查以下几个方面fastapi 服务是否正常启动。查询条件是否过严。职位状态是否为published。薪资字段是否满足min_salary条件。5. 常见问题与排查思路在实际开发中Agent 消费职位数据时会遇到各种问题。下面整理成表格方便遇到类似报错时快速定位。问题现象常见原因解决思路Agent 请求超时服务端处理慢或网络不通检查服务状态确认接口 URL 可访问返回数据为空查询条件过严或数据源未发布职位先不带筛选条件请求一次确认是否有数据字段解析报错提供方未严格遵循协议缺失必填字段增加字段校验对缺失字段设置默认值薪资字段无法比较薪资格式不统一存在字符串表示协议规定使用结构化对象做好数据转换分页循环不终止has_more一直为 true检查服务端分页逻辑设置最大页数限制大模型内容被截断职位描述过长对 description 做截断或分段处理出现重复职位多次翻页或接口幂等性不足消费端对id做去重另外很多朋友在 Agent 开发中会看到类似the agent execution provider did not respond in time的报错。这通常不是 OJCP 本身的问题而是 Agent 运行时调用某个工具或模型服务时超时。处理思路是调大执行超时时间。检查依赖的外部服务是否可达。为 Agent 调用增加重试机制。把大任务拆分为多个小任务避免单次执行时间过长。OJCP 主要解决的是数据协议的标准化问题但 Agent 工程的健壮性还需要在超时、重试、缓存、幂等这些基础设施能力上下工夫。6. 最佳实践与工程建议6.1 数据提供方的最佳实践如果你负责提供 OJCP 格式的职位数据以下建议值得参考字段语义要稳定 协议中最怕“同一个字段不同时期含义不同”。比如statuspublished早期表示“已发布”后来改成“审核中”下游 Agent 的行为就会错乱。字段语义一旦定下来尽量保持稳定。增加协议版本号 响应体中加入protocol_version或version字段。未来有破坏性变更时通过版本号平滑过渡。控制单次返回的数据量 支持page_size限制建议最大不超过 100。单条职位数据要避免塞入过长的字段比如超长的 HTML 描述。提供数据变更感知能力 Agent 往往需要增量同步提供方可以增加updated_since参数或者提供 Webhook 订阅机制减少 Agent 全量拉取的压力。明确数据归属和更新频率 在响应中增加source和updated_at方便 Agent 判断数据新鲜度。6.2 Agent 消费方的最佳实践本地缓存 增量更新 Agent 每次请求都全量拉取会非常低效。可以本地缓存职位数据配合updated_since做增量更新。字段容错 OJCP 协议要求必填字段但真实环境下总有服务方不按协议输出。在消费端做一层字段容错比如缺失salary时使用默认值避免解析异常。对职位 ID 做去重 多页拉取、多次同步都可能产生重复数据消费端要维护一个已处理 ID 集合。敏感信息过滤 职位数据中可能包含联系方式、内部备注等信息。Agent 在输出内容前要过滤敏感字段避免泄露。大模型调用成本控制 不是所有职位都需要调用大模型。先用规则筛选缩小范围再对候选职位做语义分析能显著降低成本。6.3 安全与合规建议涉及生产环境和外部数据时要特别注意以下边界OJCP 接口的访问权限要控制至少使用 API Key 或 Token 认证不能裸奔在公网。职位数据可能包含个人信息输出给 Agent 前要完成脱敏。Agent 消费数据后不应无限期缓存建议设置合理的 TTL生存时间。涉及跨平台职位数据聚合时要注意数据来源的授权问题不要未经授权抓取数据。删除接口、批量更新操作要在测试环境充分验证生产环境遵循最小权限原则。6.4 协议演进与扩展基础 OJCP 协议可以覆盖大部分通用场景但真实业务中还会有垂直需求比如候选人投递链路职位数据除了展示还需要投递简历。薪酬福利的更多维度期权、股票、签字费、年终奖。职位与技能的关联技能标签的层级与权重。多语言职位同一职位在不同地区有不同语言的描述。面对这类需求建议采用扩展字段的方式{ id: job-001, title: Senior Java Engineer, description: ..., extensions: { visaSponsorship: true, equity: {min: 0.01, max: 0.05, unit: percent}, interviewProcess: [HR Screen, Tech Interview, Onsite] } }extensions字段为自定义扩展保留空间Agent 如果认识这些字段可以解析不认识可以直接忽略不影响基础功能。7. 从 OJCP 到完整 Agent 服务进阶方向到这里我们已经完成了 OJCP 协议从概念到实战的闭环。但要把一个 Agent 招聘助手真正落地到生产还有几个方向值得继续深入。Agent 与 MCP 的集成 如果你已经在使用 MCP可以把 OJCP 数据源封装成 MCP Server 的 Tool。Agent 通过 MCP 协议调用 ToolTool 内部再访问 OJCP 接口这样 Agent 就不需要直接面对 HTTP 细节。多数据源聚合 一个 Agent 往往要消费多个平台的职位数据。你可以为每个平台写一个适配器统一转换为 OJCP 结构再由上层的 Agent 统一消费。这样即使新增数据源也只需要新增适配器。职位数据的语义增强 从 OJCP 基础数据中可以进一步抽取技能图谱、公司标签、薪资分布等衍生数据构建更丰富的岗位画像让 Agent 的匹配能力更强。Agent 记忆与个性化推荐 通过记录用户的搜索历史、点击行为和投递反馈让 Agent 逐步理解用户的职业偏好。这里的偏好数据可以独立于职位数据存储但在推荐阶段与 OJCP 职位数据做交汇。Agent 编排与任务拆分 一个完整的招聘流程可能包含职位搜索、简历生成、岗位投递、面试安排。每个环节都可以设计成一个独立的 Agent 子任务主 Agent 负责任务编排。这里就涉及多 Agent 协作模式比如主从模式、任务规划与执行分离等。OJCP 解决了“Agent 读不懂职位数据”这个基础问题但真正让 Agent 有价值的是它背后的业务闭环。数据标准是地基任务拆解、模型调用、反馈优化才是上层建筑。建议你在动手实现 OJCP 接入的同时把 1 到 2 个真实的业务闭环跑通比如“搜索职位 → 分析匹配度 → 生成投递建议”这样才能真正体会到协议标准带来的效率提升。如果这篇文章对你有帮助建议先在自己负责的模块里把职位数据按 OJCP 结构整理一遍再写一个最小的 Agent 消费脚本。不用一开始就追求完美先让数据流动起来后面再逐步完善协议细节。
返回列表