OJCP开放职位消费协议:面向AI Agent的职位数据标准化实践
2026/8/30 12:32:50 网站建设 项目流程

最近在落地 AI Agent 招聘助手的时候,遇到一个很现实的问题:职位数据散落在不同平台、不同接口、不同字段定义里,Agent 想要统一消费这些数据非常困难。如果有一套面向 Agent 消费的开放职位数据协议,整个链路就会清晰很多。这也正是 OJCP(Open Job Consumption Protocol)这个方向要解决的问题。

本文将围绕 OJCP 的设计理念、核心数据模型、Agent 消费流程、服务端接入示例以及常见踩坑点展开,希望给正在做 Agent 开发、职位聚合平台或招聘数据服务的朋友一些可落地的参考。

1. 背景与核心概念

1.1 为什么需要面向 Agent 的职位数据协议

先看一个常见场景:你正在开发一个 AI 招聘助手,用户说“帮我找最近一周发布的、深圳的、Java 后端岗位,要求月薪 25K 以上”。

传统做法是去调用某个招聘平台的开放 API,然后把返回的 JSON 字段映射到自己的数据结构。问题也随之而来:

  1. 每个平台的字段命名不同,有的叫salary、有的叫payRange、有的叫compensation
  2. 数据嵌套层级不同,有的返回到data.list,有的返回到content.positions
  3. 匹配规则不透明,平台可能默认做了相关性排序,Agent 无法判断数据是否完整。
  4. 状态字段混乱,status可能是数字、字符串、枚举,含义完全不一样。

当你的 Agent 只需要对接一个平台时,这些问题还能通过硬编码解决;但一旦要对接多个来源,或者想要做一个通用的职位数据消费层,协议不一致的维护成本会快速膨胀。

OJCP 的思路是:在职位数据提供方和 Agent 消费方之间,定义一个统一的、可扩展的开放协议。数据提供方按协议输出标准化结构,Agent 按协议解析数据,两边都不需要关心对方内部实现。

1.2 OJCP 是什么

从命名来看,OJCP 是 “Open Job Consumption Protocol” 的缩写,翻译过来是“开放职位消费协议”。它不是一个具体的软件,也不是某个公司的 SDK,而是一套描述职位数据如何暴露、如何获取、如何解析的规范。

它关注的核心问题有三个:

  • 数据格式:职位数据应该包含哪些字段,字段类型和含义是什么。
  • 交互方式:Agent 如何发现职位数据源,如何发起查询,如何获取详情。
  • 状态语义:职位从发布、下架、暂停到关闭,状态如何表达。

你可以把它理解成职位数据领域的“通用语言”。只要提供方和消费方都遵循这套语言,Agent 就能像阅读标准文档一样读取职位数据。

1.3 OJCP 与 MCP、普通招聘 API 的区别

近两年 Agent 领域经常提到 MCP(Model 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 的设计目标可以拆成以下几条:

  1. 数据自描述 Agent 拿到一条职位记录后,不需要外部文档,仅凭字段名和结构就能理解这条数据的含义。

  2. 格式中立 协议定义的是数据语义和结构,不绑定特定传输方式。HTTPS 可以,gRPC 可以,甚至离线 JSON 文件也可以。

  3. 渐进式扩展 基础字段是所有接入方必须支持的,扩展字段允许各自补充。比如基础字段有titlelocationsalary,扩展字段可以有equityvisaSponsorship

  4. 状态机清晰 职位在不同平台上有不同的生命周期,协议要定义一个通用状态机,避免出现“A 平台下架=B 平台关闭”这种语义混乱。

  5. 查询语义统一 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、contract
statusstring职位状态,遵循协议状态机
publishedAtstring发布时间,ISO 8601 格式
updatedAtstring更新时间,ISO 8601 格式
companyobject公司信息,包含名称、Logo、规模等
skillsarray技能标签数组
sourcestring数据来源标识,用于追踪数据归属

这里强调几个容易踩坑的点:

  • salary不要设计成字符串,比如 “25K-35K”。Agent 解析这种字符串需要额外的模式匹配,一旦格式不统一就会出错。建议拆成minmaxcurrencyperiod
  • employmentType使用枚举字符串,不要用数字。数字无法自解释,Agent 必须查表才能理解。
  • 时间字段统一使用 ISO 8601,不要用时间戳。时间戳虽然简洁,但缺少时区语义,Agent 需要额外换算。

3.2 状态机定义

职位状态看起来简单,实际在多方协作时会很混乱。一个职位可能经历以下过程:

  • 发布方创建职位。
  • 审核通过后上线。
  • 招聘满员后暂停。
  • 最终关闭或删除。

如果协议不统一状态定义,A 平台把暂停叫paused,B 平台叫on_hold,C 平台叫suspended,Agent 处理起来会非常痛苦。

建议定义如下状态集合:

状态值含义后续可能状态
draft草稿,尚未公开可见published、closed
published已发布,Agent 可正常获取paused、closed
paused暂停招聘,数据仍存在但不应推荐published、closed
closed已关闭,不再招聘draft
removed已删除,应从本地缓存中移除

需要注意的是,removed状态代表数据不可恢复,提供方在返回该状态时应确保 Agent 删除本地缓存。

3.3 查询与分页约定

Agent 需要按条件筛选职位,协议中建议约定一套统一的查询参数风格。

常见查询参数:

  • q:全文搜索关键词。
  • location:地点筛选,如shenzhenbeijing
  • employment_type:工作类型筛选。
  • min_salarymax_salary:薪资范围筛选。
  • status:状态筛选,默认只返回published
  • published_after:按发布时间筛选。
  • pagepage_size:分页参数。

分页响应建议包含以下字段:

{ "data": [], "pagination": { "page": 1, "page_size": 20, "total": 156, "has_more": true } }

has_more字段非常重要,Agent 可以通过它判断是否继续翻页,避免额外请求。

4. Agent 消费 OJCP 数据实战

4.1 场景设定

假设我们现在要开发一个 AI 招聘助手,从一个遵循 OJCP 协议的职位数据服务中拉取深圳地区的 Java 后端岗位,然后交给大模型做筛选和推荐。

整体流程:

  1. Agent 构造查询请求。
  2. 职位服务返回 OJCP 格式的数据。
  3. Agent 解析数据并转换为内部结构。
  4. Agent 调用大模型对职位进行筛选匹配。
  5. 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(title="OJCP 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( id="job-001", title="高级Java后端工程师", description="负责核心交易系统的设计与开发...", location=Location(city="深圳", region="南山区", remote=False), salary=Salary(currency="CNY", min=25000, max=40000, period="month"), employment_type="full_time", status="published", published_at="2025-01-10T10:00:00Z", updated_at="2025-01-10T10:00:00Z", company={"name": "示例科技", "size": "200-500人"}, skills=["Java", "Spring Boot", "MySQL", "Redis"], source="demo-source", ), JobPosting( id="job-002", title="Java开发工程师(远程)", description="参与金融风控系统研发,支持远程办公...", location=Location(city="上海", region=None, remote=True), salary=Salary(currency="CNY", min=20000, max=30000, period="month"), employment_type="full_time", status="published", published_at="2025-01-08T10:00:00Z", updated_at="2025-01-09T10:00:00Z", company={"name": "金融科技公司", "size": "50-150人"}, skills=["Java", "分布式系统", "Kafka"], source="demo-source", ), ] @app.get("/ojcp/jobs", response_model=dict) 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, ge=1), page_size: int = Query(20, ge=1, le=100), ): """ 以 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 列表接口。注意几个细节:

  • 响应中带有protocolversion字段,方便 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", params=query_params, timeout=self.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_url="http://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_keyword="Java", user_min_salary=25000) 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_ascii=False, indent=2) return f""" 用户需求:{user_requirement} 职位数据(OJCP 格式): {job_summary} 请根据用户需求评估该职位的匹配度,并给出推荐理由。 如果匹配,请说明哪些信息让该职位适合用户;如果不匹配,请给出原因。 """ prompt = build_prompt("我在找一份能兼顾生活和工作的后端岗位", job_data) print(prompt) # 实际调用大模型时: # response = your_llm_client.chat.completions.create( # model="your-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 运行时调用某个工具或模型服务时超时。处理思路是:

  1. 调大执行超时时间。
  2. 检查依赖的外部服务是否可达。
  3. 为 Agent 调用增加重试机制。
  4. 把大任务拆分为多个小任务,避免单次执行时间过长。

OJCP 主要解决的是数据协议的标准化问题,但 Agent 工程的健壮性还需要在超时、重试、缓存、幂等这些基础设施能力上下工夫。

6. 最佳实践与工程建议

6.1 数据提供方的最佳实践

如果你负责提供 OJCP 格式的职位数据,以下建议值得参考:

  1. 字段语义要稳定 协议中最怕“同一个字段,不同时期含义不同”。比如status=published早期表示“已发布”,后来改成“审核中”,下游 Agent 的行为就会错乱。字段语义一旦定下来,尽量保持稳定。

  2. 增加协议版本号 响应体中加入protocol_versionversion字段。未来有破坏性变更时,通过版本号平滑过渡。

  3. 控制单次返回的数据量 支持page_size限制,建议最大不超过 100。单条职位数据要避免塞入过长的字段,比如超长的 HTML 描述。

  4. 提供数据变更感知能力 Agent 往往需要增量同步,提供方可以增加updated_since参数,或者提供 Webhook 订阅机制,减少 Agent 全量拉取的压力。

  5. 明确数据归属和更新频率 在响应中增加sourceupdated_at,方便 Agent 判断数据新鲜度。

6.2 Agent 消费方的最佳实践

  1. 本地缓存 + 增量更新 Agent 每次请求都全量拉取会非常低效。可以本地缓存职位数据,配合updated_since做增量更新。

  2. 字段容错 OJCP 协议要求必填字段,但真实环境下总有服务方不按协议输出。在消费端做一层字段容错,比如缺失salary时使用默认值,避免解析异常。

  3. 对职位 ID 做去重 多页拉取、多次同步都可能产生重复数据,消费端要维护一个已处理 ID 集合。

  4. 敏感信息过滤 职位数据中可能包含联系方式、内部备注等信息。Agent 在输出内容前要过滤敏感字段,避免泄露。

  5. 大模型调用成本控制 不是所有职位都需要调用大模型。先用规则筛选缩小范围,再对候选职位做语义分析,能显著降低成本。

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 招聘助手真正落地到生产,还有几个方向值得继续深入。

  1. Agent 与 MCP 的集成 如果你已经在使用 MCP,可以把 OJCP 数据源封装成 MCP Server 的 Tool。Agent 通过 MCP 协议调用 Tool,Tool 内部再访问 OJCP 接口,这样 Agent 就不需要直接面对 HTTP 细节。

  2. 多数据源聚合 一个 Agent 往往要消费多个平台的职位数据。你可以为每个平台写一个适配器,统一转换为 OJCP 结构,再由上层的 Agent 统一消费。这样即使新增数据源,也只需要新增适配器。

  3. 职位数据的语义增强 从 OJCP 基础数据中,可以进一步抽取技能图谱、公司标签、薪资分布等衍生数据,构建更丰富的岗位画像,让 Agent 的匹配能力更强。

  4. Agent 记忆与个性化推荐 通过记录用户的搜索历史、点击行为和投递反馈,让 Agent 逐步理解用户的职业偏好。这里的偏好数据可以独立于职位数据存储,但在推荐阶段与 OJCP 职位数据做交汇。

  5. Agent 编排与任务拆分 一个完整的招聘流程可能包含:职位搜索、简历生成、岗位投递、面试安排。每个环节都可以设计成一个独立的 Agent 子任务,主 Agent 负责任务编排。这里就涉及多 Agent 协作模式,比如主从模式、任务规划与执行分离等。

OJCP 解决了“Agent 读不懂职位数据”这个基础问题,但真正让 Agent 有价值的是它背后的业务闭环。数据标准是地基,任务拆解、模型调用、反馈优化才是上层建筑。建议你在动手实现 OJCP 接入的同时,把 1 到 2 个真实的业务闭环跑通,比如“搜索职位 → 分析匹配度 → 生成投递建议”,这样才能真正体会到协议标准带来的效率提升。

如果这篇文章对你有帮助,建议先在自己负责的模块里把职位数据按 OJCP 结构整理一遍,再写一个最小的 Agent 消费脚本。不用一开始就追求完美,先让数据流动起来,后面再逐步完善协议细节。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询