
AI Agent 正在成为招聘行业的新变量但一个尴尬的事实是Agent 目前读不懂大多数招聘网站上的职位数据。页面结构五花八门、字段语义模糊、缺乏统一标识导致 Agent 要么靠爬虫硬解析 HTML要么依赖各家私有的 API 格式。这也是我关注 OJCPOpen Job Content Protocol的原因——它是一个面向 Agent 消费场景的开放职位数据协议目标是让职位信息从给人看的网页变成给 Agent 读的标准数据。这篇文章会从协议设计的角度拆解 OJCP它解决什么问题、数据模型如何设计、传输机制怎么工作、Agent 端如何接入并提供一个完整的 Python 示例跑通发布职位 → Agent 订阅消费的全流程。无论你是做招聘平台、AI 求职助手还是想给 Agent 接入真实世界的结构化数据这篇文章都值得读完。1. 这篇文章真正要解决的问题先从一个真实场景说起。假设你正在开发一个 AI 求职助手用户会问帮我找一份北京的后端开发岗位要求薪资 30K 以上最好支持远程。Agent 接到这个需求后需要做三件事找到职位数据源。理解职位数据的结构。按用户条件进行过滤和匹配。第 2 步往往是最难的。因为现实中的职位数据通常以三种形态存在HTML 页面需要爬虫解析每个网站结构不同改版就崩。半结构化 JSON字段命名混乱有的用salary有的用pay有的把薪资写在描述文本里。PDF 或图片对 Agent 完全不友好只能靠 OCR 或 LLM 硬读。即使拿到了数据Agent 依然面临语义理解问题。3 年以上经验和3 years experience在语义上是等价的但在数据层没有任何统一标识。这就导致每个 Agent 都要单独实现一套解析和规范化逻辑工作量巨大且难以复用。OJCP 的定位就是解决这个痛点用一套开放的、机器可读的协议标准把职位数据从展示层解放出来让 Agent 只面向标准协议而不是面向每个网站的实现细节。这篇文章适合以下几类读者正在做 AI Agent 或 LLM 应用开发的工程师。招聘平台、猎头系统、ATS Applicant Tracking System的后端开发。对结构化数据协议、Schema 校验、事件驱动架构感兴趣的技术人。想了解 Agent 生态中数据层标准如何演进的产品经理和技术负责人。读完这篇文章你将了解 OJCP 的完整设计思路并能亲手实现一个基于 OJCP 的职位发布与消费最小示例。2. OJCP 核心概念与设计原则2.1 什么是 OJCPOJCP 全称 Open Job Content Protocol是一个面向 Agent 消费的开放职位数据协议。它的核心思路是职位数据不应该被锁定在某个平台或某种页面结构里而应该以标准的、结构化的、自描述的方式被任何 Agent 读取和处理。OJCP 并不是一个开箱即用的 SaaS 产品而是一套规范。任何招聘平台、HR 系统或职位发布方都可以按照这套规范暴露自己的职位数据任何 Agent 都可以按照这套规范去消费数据。这与 HTTP、RSS 这类开放标准的思路一脉相承。2.2 OJCP 与 RSS、MCP、JSON Schema 的关系理解 OJCP 最好的方式是看它和几个相近概念的区别概念定位类比RSS内容订阅格式标准报纸的订阅机制JSON Schema数据描述和校验标准数据库表结构定义MCPAgent 工具调用协议让 Agent 操控软件的 API 标准OJCP职位数据内容协议符合 Agent 可读需求的专用 Feed 格式更准确地说OJCP 借鉴了 RSS 的内容分发思想但又针对 Agent 场景做了三件事升级字段语义标准化所有字段都有明确的 JSON Schema 定义和语义说明。支持增量更新Agent 不需要反复拉取全量数据。事件驱动机制职位产生、更新、下线都能通过事件机制实时通知 Agent。2.3 OJCP 的设计原则从协议命名就能看出它的两个关键词开放和 Agent 可消费。围绕这两个词OJCP 的设计遵循以下原则开放性协议规范公开任何组织都可以免费实现和使用。机器优先设计目标是让 Agent 高效解析而不是让人阅读。字段精简化只保留职位消费所需的核心字段避免过度设计。自描述性数据本身携带 Schema 信息Agent 拿到数据即可理解结构。可扩展性允许在不同行业或地区扩展自定义字段。这些原则保证了 OJCP 既能解决通用问题又不会因为过度抽象而难以落地。3. OJCP 数据模型一个 Job Posting 长什么样3.1 核心字段设计OJCP 的数据模型围绕一份职位Job Posting来设计。一份标准化的 OJCP Job Posting 包含以下核心部分字段分类字段名说明标识信息id职位唯一标识职位信息title职位名称公司信息company公司名称与 Logo地点信息location工作地点支持远程标识薪资信息salary结构化薪资范围描述信息description职位描述纯文本或富文本要求信息requirements硬性要求和软性要求时间信息published_at发布时间状态信息statusactive / closed / paused扩展信息metadata自定义扩展字段3.2 为什么字段语义标准化很重要举一个最常见的例子薪资字段。在传统网页中薪资通常以文本形式呈现比如15K-25K·14薪。这种格式对 Agent 来说极其不友好。Agent 要解析15K代表什么14薪又代表什么还得处理薪资面议这种模糊表达。在 OJCP 中薪资字段会被拆成结构化数据{ salary: { currency: CNY, min: 15000, max: 25000, period: monthly, bonus_months: 14, negotiable: false } }Agent 拿到这个结构后不需要做任何 NLP 解析直接通过数值比较就能判断是否符合用户薪资 30K 以上的筛选条件。这就是语义标准化的价值。3.3 一个完整的 OJCP Job Posting 示例下面是一个符合 OJCP 规范的职位数据示例{ schema_version: 1.0, id: job-20250321-001, title: 高级后端开发工程师, company: { name: 示例科技有限公司, logo_url: https://example.com/logo.png, website: https://example.com }, location: { city: 北京, district: 海淀区, remote: true, address: 中关村软件园 }, salary: { currency: CNY, min: 30000, max: 50000, period: monthly, bonus_months: 14, negotiable: true }, description: 负责公司核心交易系统的架构设计与开发..., requirements: { hard: [ 本科及以上学历计算机相关专业, 5 年以上后端开发经验, 精通 Java 或 Go熟悉 Spring Boot 或 Gin 框架, 熟悉 MySQL、Redis、消息队列等常见中间件 ], soft: [ 具备良好的团队沟通能力, 有高并发系统设计经验者优先 ] }, published_at: 2026-03-21T10:00:0008:00, expires_at: 2026-04-21T23:59:5908:00, status: active, metadata: { industry: 互联网, employment_type: full_time, source: example_platform } }这个示例覆盖了 Agent 在做职位匹配时最常用的核心字段。相比直接从 HTML 解析这种数据质量有着天壤之别。4. OJCP 的传输机制与工作流程有了标准的数据模型下一步是解决数据怎么传给 Agent的问题。OJCP 设计了两种消费模式4.1 拉取模式Pull拉取模式类似 REST API。Agent 主动向职位数据源发起请求获取符合条件的数据。适用于低频查询场景。Agent 需要一次性获取大量历史数据。数据源方没有能力维护长连接。OJCP 在这个模式下的接口路径示例如下GET /ojcp/v1/jobs // 获取职位列表 GET /ojcp/v1/jobs/{id} // 获取单个职位详情 GET /ojcp/v1/jobs?city北京min_salary30000 // 条件筛选4.2 推送模式Push推送模式适合实时性要求高的场景。职位发布方将事件推送给订阅者Agent 实时消费。技术上可以基于 Webhook 或消息队列实现。流程如下Agent 向职位源注册订阅。职位源发布新职位时通过 Webhook 向 Agent 推送事件。Agent 收到事件后拉取详情或直接消费事件内容。4.3 完整工作流程无论采用哪种传输模式一次完整的 OJCP 数据流转都包含以下环节职位发布方 - OJCP Schema 校验 - 数据序列化 - 传输层HTTP/Webhook/MQ - Agent 订阅/拉取 - 反序列化 - Agent 内部业务处理值得强调的是Schema 校验是 OJCP 流程中不可省略的环节。它确保每条进入传输层的数据都符合协议规范从源头上杜绝脏数据。Agent 端则可以放心假设只要是 OJCP 协议的数据结构一定合法。5. 开发环境与最小实现下面进入实操环节。我们将实现一个最小可运行的 OJCP 示例包含一个 OJCP 服务端提供职位数据的发布和查询。一个 Agent 客户端向服务端拉取数据并执行筛选。一份 JSON Schema用于校验职位数据。5.1 环境准备本文示例使用 Python 3 实现依赖两个库Flask用于编写服务端 APIjsonschema用于数据校验。如果你的环境没有安装可以使用以下命令pip install flask jsonschema requests如果网络环境受限也可以将Flask替换为 Python 自带的http.server但可读性会差很多。这里我们统一使用 Flask。5.2 定义 OJCP JSON Schema首先创建一个 OJCP Schema 文件用于校验职位数据结构。文件路径ojcp_schema.json{ $schema: http://json-schema.org/draft-07/schema#, type: object, required: [id, title, company, location, salary, status], properties: { schema_version: { type: string, const: 1.0 }, id: { type: string, pattern: ^[a-zA-Z0-9_-]$ }, title: { type: string, minLength: 1 }, company: { type: object, required: [name], properties: { name: {type: string}, logo_url: {type: string, format: uri}, website: {type: string, format: uri} } }, location: { type: object, properties: { city: {type: string}, district: {type: string}, remote: {type: boolean}, address: {type: string} } }, salary: { type: object, required: [currency, min, max, period], properties: { currency: {type: string, enum: [CNY, USD]}, min: {type: integer, minimum: 0}, max: {type: integer, minimum: 0}, period: {type: string, enum: [monthly, yearly, hourly]}, bonus_months: {type: integer}, negotiable: {type: boolean} } }, description: {type: string}, requirements: { type: object, properties: { hard: {type: array, items: {type: string}}, soft: {type: array, items: {type: string}} } }, published_at: {type: string, format: date-time}, expires_at: {type: string, format: date-time}, status: { type: string, enum: [active, closed, paused] }, metadata: {type: object} } }这个 Schema 覆盖了 OJCP 规范的核心字段并强制了字段类型、必填项和取值枚举。任何不符合 Schema 的数据都会被校验拦截。5.3 实现 OJCP 服务端接下来实现一个简单的 OJCP 服务端。它负责内存中保存职位数据。提供职位查询接口。发布新职位时执行 Schema 校验。文件路径ojcp_server.py# -*- coding: utf-8 -*- OJCP 最小服务端示例 提供职位数据的发布与查询接口 import json import copy from datetime import datetime, timezone, timedelta from flask import Flask, request, jsonify from jsonschema import validate, ValidationError app Flask(__name__) # 模拟数据库内存字典 JOBS_DB {} # 加载 OJCP Schema with open(ojcp_schema.json, r, encodingutf-8) as f: OJCP_SCHEMA json.load(f) def parse_time(value): 解析带时区的时间字符串统一转为 ISO 格式 try: return datetime.fromisoformat(value) except ValueError: return None app.route(/ojcp/v1/jobs, methods[GET]) def list_jobs(): 职位列表查询接口 jobs list(JOBS_DB.values()) # 按条件筛选 city request.args.get(city) min_salary request.args.get(min_salary) remote request.args.get(remote) status request.args.get(status, active) if status ! all: jobs [job for job in jobs if job.get(status) status] if city: jobs [job for job in jobs if job.get(location, {}).get(city) city] if min_salary: try: min_salary_num float(min_salary) jobs [ job for job in jobs if job.get(salary, {}).get(max, 0) min_salary_num ] except ValueError: pass if remote: remote_flag remote.lower() true jobs [job for job in jobs if job.get(location, {}).get(remote) remote_flag] # 按发布时间倒序排序 jobs.sort(keylambda job: job.get(published_at, ), reverseTrue) return jsonify({ code: 0, total: len(jobs), data: jobs }) app.route(/ojcp/v1/jobs/job_id, methods[GET]) def get_job(job_id): 获取单个职位详情 job JOBS_DB.get(job_id) if not job: return jsonify({code: 404, message: Job not found}), 404 return jsonify({code: 0, data: job}) app.route(/ojcp/v1/jobs, methods[POST]) def create_job(): 发布新职位包含 Schema 校验 body request.get_json(forceTrue) # 执行 OJCP Schema 校验 try: validate(instancebody, schemaOJCP_SCHEMA) except ValidationError as e: return jsonify({ code: 400, message: fOJCP Schema validation failed: {e.message} }), 400 job_id body.get(id) if job_id in JOBS_DB: return jsonify({ code: 400, message: fJob {job_id} already exists }), 400 # 补充时间字段 if published_at not in body: body[published_at] datetime.now(timezone(timedelta(hours8))).isoformat() JOBS_DB[job_id] copy.deepcopy(body) return jsonify({code: 0, message: Job created, data: body}), 201 if __name__ __main__: app.run(host0.0.0.0, port8000, debugTrue)这段代码虽然简单但已经把 OJCP 服务端的三个核心能力体现出来了提供标准的职位数据查询接口。支持按城市、薪资、远程、状态等条件筛选。发布职位时强制进行 Schema 校验。5.4 实现 Agent 消费端Agent 端要做的事情更简单通过 HTTP 接口拉取数据直接使用结构化字段做业务匹配。文件路径ojcp_agent_client.py# -*- coding: utf-8 -*- Agent 消费端示例 演示如何拉取 OJCP 职位数据并进行条件匹配 import requests class OJCPAgentClient: 一个简单的 OJCP 职位数据 Agent 客户端 def __init__(self, base_url): self.base_url base_url def search_jobs(self, cityNone, min_salaryNone, remoteFalse): 按条件搜索职位 params {} if city: params[city] city if min_salary: params[min_salary] min_salary if remote: params[remote] true resp requests.get( f{self.base_url}/ojcp/v1/jobs, paramsparams, timeout10 ) resp.raise_for_status() return resp.json().get(data, []) def match_user_intent(self, user_intent): 根据用户意图匹配职位 这里演示了一个非常简化的匹配逻辑 只做字段级过滤不涉及语义理解 city user_intent.get(city) min_salary user_intent.get(min_salary) remote user_intent.get(remote, False) jobs self.search_jobs( citycity, min_salarymin_salary, remoteremote ) # 进一步过滤只看 active 职位 jobs [job for job in jobs if job.get(status) active] results [] for job in jobs: results.append({ job_id: job[id], title: job[title], company: job[company][name], city: job[location].get(city), remote: job[location].get(remote, False), salary_min: job[salary].get(min), salary_max: job[salary].get(max), salary_currency: job[salary].get(currency) }) return results if __name__ __main__: client OJCPAgentClient(http://127.0.0.1:8000) # 模拟用户意图 intent { city: 北京, min_salary: 30000, remote: True } print(用户意图, intent) print(匹配结果) matches client.match_user_intent(intent) for item in matches: print(item)这个 Agent 客户端的核心优势在于它不需要解析 HTML不需要处理语义差异直接使用结构化字段做数值比较和字符串匹配。5.5 验证数据校验能力我们还需要验证 Schema 校验是否生效。创建一个发布非法数据的测试脚本文件路径test_ojcp_validation.py# -*- coding: utf-8 -*- 测试 OJCP Schema 校验 import json import requests BASE_URL http://127.0.0.1:8000 def test_valid_job(): 测试合法职位数据 with open(job_post.json, r, encodingutf-8) as f: job json.load(f) resp requests.post(f{BASE_URL}/ojcp/v1/jobs, jsonjob) print(f[合法职位] 状态码: {resp.status_code}) print(f[合法职位] 响应: {resp.json()[message]}) def test_invalid_salary(): 测试薪资字段非法的情况 with open(job_post.json, r, encodingutf-8) as f: job json.load(f) # 将薪资改为字符串违反 Schema job[salary][min] 30000 resp requests.post(f{BASE_URL}/ojcp/v1/jobs, jsonjob) print(f[非法薪资] 状态码: {resp.status_code}) print(f[非法薪资] 响应: {resp.json()[message]}) if __name__ __main__: test_valid_job() print(- * 50) test_invalid_salary()6. 运行结果与效果验证6.1 启动服务端在项目目录下执行python ojcp_server.py看到类似输出说明服务启动成功* Running on all addresses (0.0.0.0) * Running on http://127.0.0.1:80006.2 准备测试数据创建一个合法的职位数据文件job_post.json{ schema_version: 1.0, id: job-20260324-beijing-go-01, title: 高级 Go 后端工程师, company: { name: 云启科技, logo_url: https://yq.example.com/logo.png, website: https://yq.example.com }, location: { city: 北京, district: 朝阳区, remote: true, address: 望京 SOHO }, salary: { currency: CNY, min: 35000, max: 55000, period: monthly, bonus_months: 16, negotiable: true }, description: 负责高并发实时消息系统的架构设计与开发, requirements: { hard: [ 5 年以上 Go 开发经验, 熟悉微服务架构, 熟悉 Kafka 或 RocketMQ ], soft: [ 有开源项目贡献经验者优先 ] }, published_at: 2026-03-24T10:00:0008:00, expires_at: 2026-04-24T23:59:5908:00, status: active, metadata: { industry: 云计算, employment_type: full_time } }6.3 执行测试先运行合法数据测试python test_ojcp_validation.py预期输出[合法职位] 状态码: 201 [合法职位] 响应: Job created -------------------------------------------------- [非法薪资] 状态码: 400 [非法薪资] 响应: OJCP Schema validation failed: 30000 is not of type integer可以看到合法的职位数据被成功创建而非法的薪资类型被 Schema 校验拦截。这正是 OJCP 保证数据质量的重要机制。6.4 运行 Agent 客户端python ojcp_agent_client.py预期输出用户意图 {city: 北京, min_salary: 30000, remote: True} 匹配结果 {job_id: job-20260324-beijing-go-01, title: 高级 Go 后端工程师, company: 云启科技, city: 北京, remote: True, salary_min: 35000, salary_max: 55000, salary_currency: CNY}Agent 成功通过结构化字段完成了北京 薪资 远程的匹配。整个流程使用了最普通的 HTTP 请求和字段比较没有任何页面解析和语义推断这就是 OJCP 带来的直接收益。7. 常见问题与排查思路在实际使用和扩展 OJCP 时你可能会遇到以下问题问题现象可能原因排查方式解决方案发布职位返回 400数据不满足 JSON Schema查看响应中的message字段对照 Schema 检查必填字段、字段类型和枚举值查询接口返回为空筛选条件过严先去掉筛选条件逐步添加确认字段名是否与 Schema 完全一致注意大小写Agent 无法连接服务端服务未启动或端口被占用使用curl http://127.0.0.1:8000/ojcp/v1/jobs测试确认服务端日志和防火墙规则中文乱码编码格式问题检查服务端和客户端的encoding配置统一在 HTTP 头设置Content-Type: application/json; charsetutf-8数据类型意外变化序列化精度丢失检查数字字段是否被转为字符串在 Schema 中明确字段类型并在服务端做类型转换此外在生产环境中有两点特别提醒不要忽略 Schema 校验的性能开销。高频发布场景下建议采用 Schema 编译缓存技术避免每次请求都重新解析 Schema 文件。注意时间字段的时区一致性。建议全链路统一使用 UTC 时间或固定时区否则 Agent 在做过期职位判断时可能出现偏差。8. 最佳实践与工程建议看完示例这里再总结一些从协议设计到落地工程的建议。8.1 协议设计层面的建议字段宜少不宜多。协议的价值在于通用性过多业务化字段会降低不同平台间的互操作性。扩展通过metadata完成。不要随意在顶层增加自定义字段所有个性化需求都收敛到metadata对象中。保守使用枚举值。枚举值越多后续变更越困难。建议枚举只覆盖最稳定的状态其余用开放字符串表达。8.2 服务端实现层面的建议Schema 校验必须前置。在数据写入数据库之前校验而不是在查询时校验。提供版本化接口。建议保留/v1前缀未来升级协议时避免破坏已有 Agent。合理设计筛选参数。不是所有字段都适合作为筛选项重点关注城市、薪资、远程、经验要求等 Agent 高频使用的字段。8.3 Agent 消费层面的建议先拉取 Schema 再拉取数据。理论上 OJCP 数据都是自描述的但显式获取 Schema 可以更安全地处理版本升级。缓存策略要带上时间戳。Agent 拉取数据后建议缓存但要根据updated_at或published_at做增量刷新。对异常数据保持容错。即使有 Schema 校验Agent 端依然要具备基本的异常数据兜底逻辑防止单条脏数据导致整个流程崩溃。8.4 安全与合规建议OJCP 设计用于公开职位数据的传播。但在实际落地时仍要注意涉密或内部职位不得通过 OJCP 暴露。对外提供数据时需要具备合法的数据授权。涉及个人信息的数据如联系方式不建议纳入 OJCP 协议字段。生产环境接入时建议配有访问日志和审计机制。9. 总结与后续学习方向OJCP 解决的核心问题是让 AI Agent 能够稳定、高效、低成本地消费职位数据。它通过标准化的 JSON Schema、清晰的字段语义和灵活的拉取/推送机制把原本散落在各种网页和 API 中的职位信息统一成了 Agent 可以直接理解的结构化数据。从本文的示例可以看出接入 OJCP 的技术门槛并不高服务端只需要提供标准的 HTTP 接口和 Schema 校验Agent 端只需要按照协议字段读取和处理数据。真正有价值的工作在于协议的扩展设计和生态建设——如何让更多招聘平台愿意暴露 OJCP 数据如何让更多 Agent 框架内置 OJCP 客户端。如果你对这个方向感兴趣后续值得深入研究的内容包括OJCP 与 MCPModel Context Protocol的组合使用让 Agent 不仅读职位数据还能直接调用职位发布工具。更复杂的 Agent 匹配逻辑把 OJCP 结构化和 LLM 语义理解结合实现更智能的人岗匹配。多数据源聚合与去重当多个平台都遵循 OJCP 时如何设计一套职位数据的汇聚与去重机制。AI Agent 的实际价值取决于它能否获取高质量的结构化数据。OJCP 这类专门为 Agent 设计的开放协议值得每一个做 AI 应用开发的工程师关注和实践。建议先照着本文的示例跑通一遍流程再思考你自己的业务场景是否适合引入这套协议。