OJCP:让AI Agent高效消费职位数据的开放协议

发布时间:2026/8/29 8:20:17
OJCP:让AI Agent高效消费职位数据的开放协议 先来思考一个很现实的问题当 AI Agent 越来越像“数字员工”时它最需要的到底是什么是更强的推理能力更多的工具调用还是更充分、更规范的数据供给最近在调研 Agent 落地场景时我注意到一个很有意思的方向OJCPOpen Job Consumption Protocol开放职位消费协议。它的目标非常聚焦——统一职位数据Job Data的格式和获取方式让 AI Agent 能够稳定、高效地消费招聘数据而不是继续面对一堆结构混乱的 HTML 页面和千奇百怪的自定义 API。这篇文章会围绕 OJCP 的定位、核心设计思路、与 MCP / Agent Skills 的关系以及如何用代码实际实现一套“Agent 可消费的职位数据服务”展开。如果你正在做 Agent 开发、招聘数据聚合或者只是好奇 Agent 如何读取结构化业务数据这篇内容应该能给你一些可落地的参考。1. 什么是 OJCP为什么 Agent 需要职位数据协议1.1 从一个很简单的场景说起假设你想让一个 AI Agent 帮你筛选合适的岗位你可能会让它“打开招聘网站找到前 5 个符合条件的工作”。听起来很简单但 Agent 实际做起来会遇到一堆问题网站结构经常变页面选择器失效。不同网站的职位字段命名完全不同。有的平台需要登录有的平台有反爬策略。即便拿到了数据Agent 也不确定“薪资范围”“工作地点”这些字段到底该从哪一段 HTML 里提取。这些问题本质上不是 Agent 的能力问题而是数据供给问题。Agent 需要的是干净、结构化、语义明确的职位数据最好通过一种统一协议直接读取而不是像人一样“看网页”。OJCP 想解决的就是这个问题定义一套开放、通用的职位数据契约让 Agent 可以像浏览一个标准化的数据源一样快速获取职位信息。1.2 什么是 OJCP从名字拆解Open开放协议不绑定某一家公司或某一个招聘平台。Job聚焦职位数据。Consumption强调“消费”也就是 Agent 读取、解析、使用数据的过程。Protocol协议意味着定义数据结构、接口规范、交互方式。用一句话概括OJCP 是一套面向 AI Agent 的职位数据获取协议它规定了“职位数据长什么样”以及“Agent 如何获取这些数据”。1.3 OJCP 要解决的核心问题问题现状OJCP 的思路数据结构不统一不同平台字段不同统一定义 JobPosting 数据结构获取方式混乱爬虫、RSS、私有 API 并存定义标准 HTTP 接口Agent 理解成本高需要大量提示词和工具去解析网页Agent 直接消费 JSON维护成本高网页改版导致解析失效数据源按协议输出即可1.4 适用场景招聘平台为 AI Agent 开放职位数据。ATSApplicant Tracking System招聘管理系统向内部 AI 助手暴露职位数据。企业招聘官网给 Agent 提供结构化岗位信息。求职类 Agent 聚合多个数据源的职位数据。2. OJCP 与 MCP、Agent Skills 的区别在 Agent 开发领域最近 MCPModel Context Protocol是一个非常热的话题很多人会把 OJCP 和 MCP 搞混。2.1 MCP 是什么MCP 是模型上下文协议它解决的是“Agent 如何连接到外部工具和数据源”的问题。你可以把 MCP 理解为 Agent 世界的 USB-C 接口通过 MCP ServerAgent 可以调用数据库、API、文件系统、浏览器等资源。MCP 是一个通用协议不限定业务领域。2.2 Agent Skills 是什么Agent Skills 更接近“技能包”它向 Agent 提供一组指令、示例和使用说明帮助 Agent 学会完成某类任务。技能不一定是实时数据接口更像是一份“操作手册”。2.3 OJCP 的位置如果说 MCP 是“ Agent 如何获取数据”的传输协议Agent Skills 是“Agent 如何执行任务”的技能包那么 OJCP 就是“职位数据长什么样”的数据契约。更准确地说OJCP 是业务领域的数据标准。它不负责怎么传输可以用 HTTP也可以借助 MCP Server但它定义了职位数据的字段、取值、语义和查询方式。三者的关系可以这样理解Agent 通过 Skills 知道“如何找工作”。Agent 通过 MCP 连接数据源。数据源返回的数据如果符合 OJCP 标准Agent 就能零成本理解。所以 OJCP 不是 MCP 的替代品而是可以共存的“数据层标准”。如果你的 MCP Server 返回的数据是 OJCP 结构Agent 消费起来会更轻松。3. OJCP 协议的核心设计思路虽然 OJCP 目前还处于早期阶段不同实现可能在细节上有差异但从“面向 Agent 的数据协议”这个角度看有四个设计维度是不能绕开的。3.1 数据结构设计职位数据的核心对象通常被称为 JobPosting。在 OJCP 框架下一个 JobPosting 应该尽量包含 Agent 做决策所需的字段例如职位标识外部 ID职位名称公司名称工作地点薪资范围职位类型全职、兼职、实习等远程/混合/坐班标记职位描述纯文本或 Markdown职位要求申请链接发布日期截止日期数据源标识这些字段的设计要兼顾两点Agent 能快速提取关键信息比如薪资、地点、岗位类型。人类开发者阅读文档时也能一眼看懂而不是一堆自定义缩写。3.2 接口设计OJCP 建议提供尽量简洁的 HTTP 接口核心是三个获取职位列表GET /jobs获取职位详情GET /jobs/{id}获取协议元信息GET /ojcp.json或类似端点列表接口应该支持分页以及按关键词、地点、职位类型等字段过滤。详情接口用于返回单个职位的完整信息。为什么接口要这么简单因为 Agent 的上下文窗口是有限的。如果首页返回一个 1MB 的 JSONAgent 会直接崩溃。协议应该让 Agent 先看列表再按需拉取详情。3.3 版本控制协议一定要有版本。职位数据字段未来肯定会调整如果不做版本控制老 Agent 可能因为字段变化而无法解析数据。建议在响应中携带schema_version字段或者通过 URL 路径区分版本例如/v1/jobs/v2/jobs3.4 错误处理Agent 消费数据时遇到错误怎么处理协议应该定义统一的错误响应格式。比如{ error: invalid_parameter, message: The parameter location is not supported., docs_url: https://example.com/docs#errors }统一错误格式能让 Agent 更快理解问题而不是面对一堆状态码和随心所欲的错误文本。4. 环境准备与项目结构接下来我们来实现一个完整的 OJCP 职位数据服务。为了控制依赖复杂度这里使用 Python 的 FastAPI 框架数据先用内存列表模拟方便你快速跑通整个流程。4.1 环境要求Python 3.10FastAPIUvicorn安装依赖的命令pip install fastapi uvicorn版本说明FastAPI 和 Uvicorn 的版本迭代比较快本文示例以较新的稳定版本为准。如果你使用的是旧版本接口写法可能略有差异需要根据你的实际环境调整。4.2 项目结构ojcp-demo/ ├── main.py # OJCP 服务入口 ├── models.py # 数据模型定义 ├── mock_data.py # 模拟职位数据 └── README.md # 项目说明我们下面按文件逐个实现。5. 完整实战实现一个 OJCP 职位数据服务5.1 定义数据模型文件路径ojcp-demo/models.pyfrom typing import Optional from pydantic import BaseModel, Field class JobPosting(BaseModel): OJCP 职位数据模型 id: str Field(..., description职位唯一标识) title: str Field(..., description职位名称) company: str Field(..., description公司名称) location: str Field(..., description工作地点) salary_min: Optional[int] Field(None, description最低薪资单位K/月) salary_max: Optional[int] Field(None, description最高薪资单位K/月) currency: str Field(CNY, description薪资币种) job_type: str Field(full_time, description职位类型full_time / part_time / intern / contract) work_mode: str Field(on_site, description工作模式on_site / remote / hybrid) description: str Field(, description职位描述纯文本或 Markdown) requirements: list[str] Field(default_factorylist, description职位要求列表) apply_url: Optional[str] Field(None, description申请链接) published_at: str Field(, description发布日期ISO 8601 格式) expires_at: Optional[str] Field(None, description截止日期ISO 8601 格式) source: str Field(, description数据源标识) class JobListResponse(BaseModel): 职位列表响应 schema_version: str Field(1.0, descriptionOJCP 协议版本) total: int Field(0, description总职位数量) page: int Field(1, description当前页码) page_size: int Field(10, description每页数量) jobs: list[JobPosting] Field(default_factorylist, description职位列表) class ErrorResponse(BaseModel): 统一错误响应 error: str message: str docs_url: Optional[str] None这里我们把字段都定义为 Pydantic 模型方便 FastAPI 自动生成 API 文档也方便做数据校验。5.2 准备模拟数据文件路径ojcp-demo/mock_data.pyfrom models import JobPosting MOCK_JOBS [ JobPosting( idjob-001, title资深后端开发工程师, company示例科技, location上海, salary_min30, salary_max50, currencyCNY, job_typefull_time, work_modehybrid, description负责核心交易系统的设计、开发与优化支撑高并发业务场景。, requirements[5 年以上后端开发经验, 熟悉 Python 或 Java, 有高并发系统经验优先], apply_urlhttps://example.com/careers/job-001, published_at2025-06-10T10:00:00Z, expires_at2025-07-10T10:00:00Z, sourcemock-data, ), JobPosting( idjob-002, titleAI Agent 开发工程师, company未来智能, location远程, salary_min40, salary_max70, currencyCNY, job_typefull_time, work_moderemote, description负责 AI Agent 产品后端开发包括 Agent 框架集成、工具编排、数据链路建设。, requirements[熟悉 LLM 应用开发, 了解 MCP / Agent 框架, 有 Python 项目经验], apply_urlhttps://example.com/careers/job-002, published_at2025-06-11T09:30:00Z, expires_atNone, sourcemock-data, ), JobPosting( idjob-003, title前端开发实习生, company示例科技, location杭州, salary_min3, salary_max5, currencyCNY, job_typeintern, work_modeon_site, description参与公司核心产品前端开发负责页面实现与性能优化。, requirements[熟悉 HTML/CSS/JavaScript, 了解 React 或 Vue, 每周至少到岗 4 天], apply_urlhttps://example.com/careers/job-003, published_at2025-06-12T08:00:00Z, expires_at2025-06-30T23:59:59Z, sourcemock-data, ), ]模拟数据覆盖了不同工作模式、岗位类型便于测试过滤功能。5.3 实现 OJCP 服务接口文件路径ojcp-demo/main.pyfrom fastapi import FastAPI, Query, HTTPException from models import JobPosting, JobListResponse, ErrorResponse from mock_data import MOCK_JOBS app FastAPI( titleOJCP Demo Server, descriptionOpen Job Consumption Protocol 示例服务, version1.0.0, ) app.get(/) def read_root(): 服务根路径输出协议元信息 return { protocol: OJCP, version: 1.0, endpoints: [ {path: /jobs, description: Get job list}, {path: /jobs/{id}, description: Get job detail}, ], } app.get(/jobs, response_modelJobListResponse) def list_jobs( page: int Query(1, ge1, description页码从 1 开始), page_size: int Query(10, ge1, le50, description每页数量最大 50), keyword: str Query(None, description关键词过滤匹配职位名称和描述), location: str Query(None, description工作地点过滤), job_type: str Query(None, description职位类型过滤), work_mode: str Query(None, description工作模式过滤), ): 获取职位列表支持分页和过滤 jobs MOCK_JOBS if keyword: jobs [ job for job in jobs if keyword.lower() in job.title.lower() or keyword.lower() in job.description.lower() ] if location: jobs [job for job in jobs if location in job.location] if job_type: jobs [job for job in jobs if job.job_type job_type] if work_mode: jobs [job for job in jobs if job.work_mode work_mode] total len(jobs) start (page - 1) * page_size end start page_size paged_jobs jobs[start:end] return JobListResponse( totaltotal, pagepage, page_sizepage_size, jobspaged_jobs, ) app.get(/jobs/{job_id}, response_modelJobPosting, responses{404: {model: ErrorResponse}}) def get_job(job_id: str): 获取单条职位详情 for job in MOCK_JOBS: if job.id job_id: return job raise HTTPException(status_code404, detailJob not found)说明一下接口设计思路/返回协议元信息让 Agent 能够“发现”这个服务支持哪些能力。/jobs支持分页和过滤减少不必要的响应体量。/jobs/{id}返回单条职位详情。错误响应统一使用ErrorResponse结构避免 Agent 面对五花八门的错误文本。5.4 启动服务在ojcp-demo目录下执行uvicorn main:app --reload --port 8000启动后FastAPI 会自动生成文档页面你可以访问接口文档http://127.0.0.1:8000/docsOpenAPI JSONhttp://127.0.0.1:8000/openapi.json5.5 用 curl 验证接口先用列表接口看看数据curl http://127.0.0.1:8000/jobs?location%E4%B8%8A%E6%B5%B7这里的%E4%B8%8A%E6%B5%B7是“上海”的 URL 编码。返回结果大致如下{ schema_version: 1.0, total: 1, page: 1, page_size: 10, jobs: [ { id: job-001, title: 资深后端开发工程师, company: 示例科技, location: 上海, salary_min: 30, salary_max: 50, currency: CNY, job_type: full_time, work_mode: hybrid, description: 负责核心交易系统的设计、开发与优化支撑高并发业务场景。, requirements: [ 5 年以上后端开发经验, 熟悉 Python 或 Java, 有高并发系统经验优先 ], apply_url: https://example.com/careers/job-001, published_at: 2025-06-10T10:00:00Z, expires_at: 2025-07-10T10:00:00Z, source: mock-data } ] }再测试详情接口curl http://127.0.0.1:8000/jobs/job-002返回就是对应的JobPosting对象。到这里一个最简单的 OJCP 服务已经跑起来了。6. 让 Agent 真正“消费”这个 OJCP 服务协议是给人看的更是给 Agent 用的。下面我们用 Python 写一个最简 Agent 脚本模拟 Agent 调用 OJCP 服务的过程。文件路径ojcp-demo/agent_client.pyimport json import urllib.request import urllib.parse BASE_URL http://127.0.0.1:8000 def fetch_jobs(base_url: str BASE_URL, **filters): 从 OJCP 服务获取职位列表 query_string urllib.parse.urlencode(filters) url f{base_url}/jobs?{query_string} with urllib.request.urlopen(url) as resp: return json.loads(resp.read().decode(utf-8)) def fetch_job_detail(job_id: str, base_url: str BASE_URL): 获取单条职位详情 url f{base_url}/jobs/{job_id} with urllib.request.urlopen(url) as resp: return json.loads(resp.read().decode(utf-8)) def main(): # Agent 的“招聘条件” conditions { work_mode: remote, page_size: 5, } print(Step 1: 获取符合条件的职位列表) result fetch_jobs(**conditions) print(f共找到 {result[total]} 个职位) for job in result[jobs]: print(f- {job[title]} / {job[company]} / {job[location]}) # 找到第一个职位后拉取详情 if result[jobs]: first_job result[jobs][0] print(\nStep 2: 获取第一个职位详情) detail fetch_job_detail(first_job[id]) print(f职位名称: {detail[title]}) print(f薪资范围: {detail[salary_min]}K - {detail[salary_max]}K) print(f申请链接: {detail[apply_url]}) if __name__ __main__: main()运行这个脚本python agent_client.py预期输出Step 1: 获取符合条件的职位列表 共找到 1 个职位 - AI Agent 开发工程师 / 未来智能 / 远程 Step 2: 获取第一个职位详情 职位名称: AI Agent 开发工程师 薪资范围: 40K - 70K 申请链接: https://example.com/careers/job-002这个示例说明了一个关键点一旦数据源遵循 OJCP 结构Agent 端只需要写一次通用解析逻辑就可以消费所有符合该协议的数据源。这正是协议存在的意义。在真实项目中Agent 不会用这么原始的urllib通常会接入 LangChain、LlamaIndex 或自研的 Agent 框架把 OJCP 服务封装成一个 Tool或者直接作为 MCP Server 的数据来源。7. 把现有招聘数据源适配成 OJCP现在很多公司已经有自己的招聘平台或 ATS完全推倒重来不现实。更务实的方式是写一层适配器把现有数据转换成 OJCP 结构。7.1 适配层设计思路招聘数据库 / 第三方招聘 API ↓ OJCP Adapter字段映射 数据清洗 ↓ OJCP HTTP Service ↓ AI Agent适配层主要做三件事字段映射把内部字段名映射为 OJCP 标准字段。数据清洗去掉 HTML 标签、过滤过期职位、补全缺失字段。增量同步定时从源系统拉取数据更新缓存。7.2 字段映射示例假设内部 ATS 的字段如下ATS 内部字段OJCP 标准字段说明position_nametitle职位名称comp_namecompany公司名称work_citylocation工作城市salary_lowsalary_min最低薪资salary_highsalary_max最高薪资job_desc_htmldescription清洗后纯文本apply_pageapply_url申请链接pub_timepublished_at发布时间这种映射看起来很简单但真正的难点在于内部字段语义不统一比如“工作地点”可能存的是城市 ID 而不是城市名。薪资单位不统一有的按月有的按年。岗位类型枚举不一致。所以适配层里最常见的代码就是枚举映射和单位换算JOB_TYPE_MAP { 正式: full_time, 兼职: part_time, 实习: intern, 外包: contract, } def convert_job_type(inner_type: str) - str: return JOB_TYPE_MAP.get(inner_type, full_time)7.3 适配层代码结构ojcp-adapter/ ├── adapters/ │ ├── __init__.py │ ├── ats_adapter.py # 适配某个 ATS 系统 │ └── company_site.py # 适配公司官网招聘页 ├── core/ │ ├── models.py # OJCP 数据模型 │ └── mapper.py # 字段映射工具 └── server.py # 启动 OJCP HTTP 服务这样改造后上游系统只需要维护自己的适配器下游 Agent 不变。8. 常见问题与排查思路在实际搭建 OJCP 服务时你可能会遇到下面这些问题。8.1 Agent 拿到数据后出现乱码问题现象Agent 读取职位描述时出现乱码尤其是中文内容。常见原因HTTP 响应没有正确声明 UTF-8 编码。源数据本身就是 GBK 等编码适配层没有转换。解决思路FastAPI 默认返回application/json一般不会有问题。如果从旧系统拉数据在适配层统一转换为 UTF-8text raw_data.decode(gbk).encode(utf-8)在响应头显式声明编码app.get(/jobs, response_modelJobListResponse) def list_jobs(): response JobListResponse(...) response.headers[Content-Type] application/json; charsetutf-88.2 接口返回太大Agent 上下文放不下问题现象Agent 一次拉取大量职位数据导致上下文超限或者响应超时。常见原因列表接口把全量数据一次性返回。职位描述包含大量 HTML 片段。解决思路列表接口只返回摘要字段详情走/jobs/{id}。默认page_size调小建议 10 或 20。过滤掉过期职位。在协议层建议 Agent 先使用关键字、地点、职位类型等条件筛选。8.3 请求 OJCP 服务时出现跨域问题问题现象Agent 运行在浏览器环境调用 OJCP 服务时被 CORS 拦截。解决思路如果 OJCP 服务就是给 Web 端 Agent 用的需要开启 CORSfrom fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins[*], allow_methods[GET], allow_headers[*], )如果 Agent 是纯后端服务一般不需要担心 CORS。8.4 职位详情页面需要登录才能申请问题现象Agent 能读到职位数据但申请链接需要登录Agent 无法完成申请。常见原因OJCP 服务只暴露了阅读数据没有覆盖申请流程。解决思路在协议中尽量暴露直接的apply_url让 Agent 跳转而不是模拟登录。如果必须支持 Agent 自动投递这是一个更大的工程涉及身份认证、表单提交等建议单独设计“投递接口”不要混在数据接口里同时要获得用户的明确授权。8.5 Agent 读到了过期职位问题现象Agent 推荐了已经关闭的职位。常见原因数据同步不及时expires_at字段没有维护。解决思路在服务端过滤查询时默认排除expires_at过期的职位。在协议层约定Agent 应该优先筛选expires_at IS NULL OR expires_at now的职位。9. 最佳实践与工程建议结合 Agent 开发的常见坑点这里给服务端开发同学几条建议。9.1 协议版本要早做OJCP 一旦被多个 Agent 接入字段变更的代价会很大。从第一天开始就在响应里带schema_version未来新增字段时尽量向后兼容不要随意删除旧字段。比较推荐的做法新增字段用 Optional 类型。删除字段前先废弃 1-2 个版本。重大不兼容变更使用 URL 路径区分例如/v2/jobs。9.2 数据安全与最小授权职位数据本身可能包含公司内部信息例如薪资范围、HC 数量、内部备注等。对外暴露时要注意只暴露 Agent 完成任务所需的最小字段集合。敏感字段如内部候选人备注禁止出现在 OJCP 响应中。如果需要认证使用轻量级 API Key而不是浏览器 Session。涉及生产数据变更、批量导出、跨域同步等操作时必须先经过合规评估和测试环境验证遵循最小权限原则。9.3 Agent 友好的协议设计为 Agent 设计协议与为普通 Web 应用设计 API 有一些区别响应字段命名要语义化避免a1、b2这类缩写。错误信息要自解释最好能带上修复建议。文本字段优先用纯文本或 Markdown而不是 HTML。货币、日期、薪资单位等字段要明确统一避免歧义。9.4 性能与缓存Agent 往往会在短时间内发起多次请求建议列表接口做 Redis 缓存TTL 控制在 5-10 分钟。详情接口做缓存TTL 可以更长一些。对全量爬取 OJCP 接口的行为做频率限制。9.5 可观测性OJCP 服务建议记录以下日志和指标请求来源Agent 标识。查询参数。响应耗时。错误码分布。数据完整率例如salary_min缺失的占比。这些数据能帮助你判断 Agent 是否真的“看懂”了你的数据还是每次都在解析失败边缘试探。10. 总结与下一步学习方向这篇文章从 Agent 消费职位数据时面临的真实问题出发介绍了 OJCP 的定位、核心设计思路以及如何从零实现一个 OJCP 职位数据服务。关键要点可以总结为四句话OJCP 是面向 Agent 的职位数据契约核心是统一结构和获取方式。它和 MCP、Agent Skills 不是竞争关系而是处在不同层MCP 解决连接Skills 解决能力OJCP 解决数据标准。实现 OJCP 服务并不复杂关键是字段语义要清晰、接口要轻量、错误要自解释。真正落地时适配层往往是最繁重的工作字段映射、编码转换、增量同步都需要仔细处理。如果你对这个方向感兴趣下一步可以沿着三个方向继续深入学习 MCP 规范尝试把 OJCP 服务封装成一个 MCP Server让支持 MCP 的 Agent 可以直接使用。研究真实招聘平台的职位数据结构尝试写一个完整的 OJCP 适配器。关注 Agent 数据消费的通用模型思考除了职位数据之外还有哪些垂直领域的数据值得定义类似的开放协议。如果说这一轮 Agent 技术浪潮里最缺的是什么我会说不是更强的模型而是更多“让 Agent 读得懂”的数据。OJCP 这类开放协议虽然看起来没有大模型那么性感但恰恰是让 Agent 从“聊天玩具”走向“数字员工”的关键基础设施。值得动手试一试。