OJCP 这个名字很直白:开放的、agent 可消费的 job data 协议。我在看这个项目时最大的感受是,它正好切中了 agent 开发里一个长期没被正式化的痛点——模型能力越来越强,但 agent 之间、agent 与系统之间传递任务的格式仍然各写各的。如果你正在做 agent 框架选型、任务编排,或者想把多个 agent 真正串进业务链路里,这篇会用偏落地的方式拆一拆这个协议到底在定义什么、能解决什么问题,以及接入时最容易卡住哪些点。
先说结论:OJCP 这类协议值得关注的不是某个具体字段,而是它对 job data 的结构化约束。有了这样一个统一层,任务发起方不用关心执行方内部细节,执行方也不用到处兼容私有 JSON 格式。理解清楚它的边界,比记住几个字段更重要。
1. Agent 开发里被忽视的问题:任务数据格式没有标准
1.1 模型能力提升了,任务数据反而更乱了
最近这两年,agent 开发最热的方向一直是模型推理能力、工具调用和上下文管理。但真正把 agent 放进业务环境之后,你会发现最难维护的根本不是模型,而是数据。一个 agent 可能需要把任务交给另一个 agent,或者把任务下发给一套自动化流程,又或者等待另一条异步任务返回结果。每个环节都要在系统之间传数据。
问题就在于,这些数据格式几乎没有标准。
有的团队用 JSON 字符串描述任务,有的团队用消息队列里的字节数组,有的团队干脆把指令直接写在 prompt 里。短期跑通很容易,一旦任务变多、agent 变复杂,字段不一致、状态不统一、错误信息对不上,都是常见情况。接口文档写得很完整,但实现方总能找到办法“灵活处理”——最后所有所谓的轻量对接,都会变成临时兼容层。
在团队协作里,"这个任务格式是我定的"A 和"我接收的是另一个格式"的 B,其实他们负责的是同一个链路。任务数据是 agent 交换的公共语言,没有统一结构,后续任何调度、监控、重试都很难做扎实。
1.2 Agent 和 Agent 之间,缺的不是工具,是任务描述
之前很多人关注的是工具接入标准,比如大模型怎么调用外部 API、怎么执行本地命令、怎么读取文件。这些能力解决的是"agent 能干什么"。但还有一个更基础的问题:怎么描述"现在要干一件事"?
这个描述必须包含任务标识、输入参数、期望输出、超时规则、重试策略、状态流转。它不能只是一句自然语言,因为多个系统要基于这个描述做判断。没有统一的 job data 结构,任务交接就永远处于手写状态。
OJCP 提出的方向,就是把这些内容收敛成一个开放协议。任务发起方、任务执行方、任务观察者都按照同一套字段来读写这个 job data。agent 不关心数据是怎么被传输的,重点是在协议的语义约束下,知道这个任务处于什么状态、需要什么输入、结果怎么回。
2. OJCP 协议最核心要定义的东西
2.1 任务数据长什么样
如果让我从零理解一个 job data 协议,我第一件想确认的事是:最大的数据单元是什么。按 OJCP 的思路,它的核心单元就是 job,也就是一个可以被执行、可以被追踪、可以被回传结果的任务数据单元。
下面是一个典型的结构示例。这个结构也适合拿来理解这类协议的共同特征。
{ "schema_version": "1.0", "job_id": "job_20250101_001", "type": "text_generation", "input": { "model": "qwen2.5-7b-instruct", "prompt": "请总结这段文本", "max_tokens": 2048 }, "output": { "summary": "这里是执行结果" }, "status": "succeeded", "created_at": "2025-01-01T10:00:00Z", "updated_at": "2025-01-01T10:05:00Z", "attempt": 1, "max_attempts": 3, "error": null }这里有几个字段我认为是比较关键的:
schema_version用于区分数据协议版本。job_id是任务的全局唯一标识,后续所有日志、重试、状态更新都依赖它。type代表了任务类型,执行方可以根据 type 来选择对应的处理器。input和output是任务输入输出,建议用嵌套对象,便于扩展新字段。status是任务当前状态。attempt和max_attempts表达的是重试进度。error在失败时保存结构化错误信息。
如果你要落地一个 OJCP 兼容格式,不用一开始就定义得非常完整,先把这条骨架定下来就是好的起点。我见过不少系统跑了很久,连job_id都没有统一规则,最后排查问题时完全靠猜。
2.2 状态流转怎么表达
协议是否成熟,很大程度看状态定义是否清晰。至少要包含这几个基础状态:
| 状态 | 含义 | 可转换到 |
|---|---|---|
pending | 已创建,等待执行 | running,failed,canceled |
running | 执行中 | succeeded,failed,canceled |
succeeded | 执行成功,可读取输出 | 终态 |
failed | 执行失败 | pending(可重试)、终态 |
canceled | 被取消 | 终态 |
状态字段看起来简单,但真正常被坑的反而是“中间状态”。例如一个任务要跑很久,是否需要processing、「数据下载中」这样的过渡状态?如果没有中间状态,下游想实时看到进度就没有依据;如果中间状态太多,状态机维护成本又上升。
我的建议是,先用核心状态跑通,过渡状态放在input或output里做补充描述。协议的第一优先是稳定,不是覆盖所有业务细节。
2.3 错误和重试怎么表示
任务执行失败是必然的。协议里如果只给一个error: "failed"字符串,几乎等于什么都没说。更好的做法是分层表达:
error.code:机器可读的错误码,比如timeout、memory_limit、invalid_input。error.message:人类可读的错误描述。error.details:附加信息,例如进程退出码、相关日志路径。
重试信息也需要单独表达。attempt表示已经尝试的次数,max_attempts表示最大允许次数。执行方看到attempt < max_attempts并且错误属于可重试类型时,可以选择将任务状态从failed转回pending,等待下次调度。这比让任务一失败就终结要灵活得多。
3. 一个 OJCP 兼容客户端的落地流程
3.1 先定义 Schema
落地第一步是确定 job data 的 Schema。这里不一定要求用严格的 JSON Schema 标准,但至少要让所有参与方共享同一份结构定义。
我通常的做法是,先建一个单独目录维护 schema 文件,再让生产者、消费者、监控系统都通过同一个 schema 库来读取字段。
# job_schema.py JOB_SCHEMA = { "job_id": str, "type": str, "status": str, "input": dict, "output": dict, "error": dict, "attempt": int, "max_attempts": int }这里有一个容易忽略的点:字段类型最好统一。比如attempt和max_attempts必须用整数,不能用字符串"3"。很多任务数据对接出错,不是因为协议设计得复杂,而是因为字段类型在不同系统里被写成了不同类型。
3.2 生产者把任务发出去
生产者是任务的发起方。它只需要做三件事:生成 job 数据、指定任务type、把数据投递到执行方。
投递方式取决于你的架构。小规模可以直接通过 HTTP 接口 POST,大规模通常走消息队列。协议本身没有绑定传输方式,这一点设计得比较灵活。
import json import uuid def create_job(job_type: str, input_data: dict, queue): job = { "schema_version": "1.0", "job_id": f"job_{uuid.uuid4().hex[:12]}", "type": job_type, "input": input_data, "output": None, "status": "pending", "created_at": "2025-01-01T10:00:00Z", "updated_at": "2025-01-01T10:00:00Z", "attempt": 0, "max_attempts": 3, "error": None } queue.send(json.dumps(job)) return job["job_id"]这里要特别注意job_id的唯一性。如果两个任务共用同一个 ID,重试、日志、监控全都会错乱。我见过因为job_id用了时间戳导致重复的案例,最后只能人工清理数据。建议直接使用 UUID 或分布式 ID 生成器。
3.3 消费者消费任务并回传结果
消费者负责接收 job,根据type选择处理器,执行后更新状态并回传。
def handle_job(job: dict): job["status"] = "running" job["updated_at"] = get_current_time() try: executor = get_executor(job["type"]) result = executor.run(job["input"]) job["output"] = result job["status"] = "succeeded" except RetryableError as e: job["error"] = { "code": e.code, "message": str(e), "details": e.details } if job["attempt"] < job["max_attempts"]: job["attempt"] += 1 job["status"] = "pending" # 重新排队 else: job["status"] = "failed" except Exception as e: job["error"] = {"code": "unknown_error", "message": str(e)} job["status"] = "failed" finally: job["updated_at"] = get_current_time() save_job(job)这段代码里的核心逻辑是异常分支的处理:
- 可重试错误:记录错误信息,增加
attempt,把状态改回pending。 - 不可重试错误:直接把状态改为
failed,不再排队。 - 未知异常:按
failed处理,但需要保留完整堆栈或上下文信息,方便排查。
很多人在这一步把最终状态和重试逻辑混在一起,导致任务失败后既不知道能不能重跑,也不知道下一次重跑要传什么参数。协议单独定义attempt和error,就是希望把这两种信息分开存放。
4. OJCP、MCP、Skill、Agent 框架到底什么关系
4.1 MCP 管的是工具,OJCP 管的是任务数据
最近在 agent 社区里,MCP(Model Context Protocol)是绕不开的词。MCP 解决的核心问题是:模型怎么通过统一接口访问外部工具。比如一个 agent 要查天气、查数据库、读文件,MCP 把这类工具调用标准化了。
OJCP 的关注点不在工具层,而在任务数据层。它处理的是"有一个任务要从 A 流转到 B"时的数据结构。MCP 是 agent 与工具之间的协议,OJCP 更像是 agent 与任务系统之间的数据协议。
举个例子:agent 要生成一份报表。MCP 负责让它调用某个数据分析工具;而这份报表任务本身的描述、状态、结果,需要一套 job data 结构来承载,这就是 OJCP 的领域。两者不是竞争关系,而是不同层级的标准化。
4.2 Skill 是能力层,OJCP 是数据层
现在很多 agent 框架里都有 "Skill" 的概念。Skill 一般表示一种可复用的能力封装,比如"写总结"是一个 skill,"翻译"是另一个 skill。Skill 描述的是 agent 能做什么,以及怎么把能力拆成可执行的步骤。
Skill 和 OJCP 的关系在于:Skill 在执行时需要输入参数、需要返回结果、可能要跨 agent 协作。如果 Skill 之间的参数格式不一致,再好的能力封装也接不起来。OJCP 能提供一种标准化的任务数据格式,让 Skill 的执行输入和输出有一个公共的、可解析的载体。
简单来说:
- Skill 回答"这个 agent 会什么"。
- OJCP 回答"这个任务现在处于什么状态,输入输出是什么"。
4.3 一个 Agent 架构里的完整链路
把 OJCP 放在完整的 agent 链路里看,位置会更清楚。
外层是业务系统,产生任务需求。中间层是 agent 编排层,负责拆解任务、分配执行单元。最底层是工具层,通过 MCP 或普通 API 完成具体动作。
在这个链路中,任务需求从业务系统到 agent 编排层,再到具体的执行 agent,每一跳都需要传递 job data。业务系统不关心 agent 内部怎么调度,它只需要知道任务的job_id和status。执行 agent 不知道任务来自哪个业务方,它只读取input,执行完毕写入output。OJCP 就是要让这两端在数据结构上达成一致。
这也解释了为什么像 OJCP 这样的协议会逐渐被讨论:当 agent 从单体 Demo 走向多模块、多团队协作时,数据层的统一是比模型选型更基础的需求。
5. 落地 OJCP 最容易掉的四个坑
5.1 Schema 版本不兼容
第一个坑是字段升级不兼容。一个任务最初只有input和output,后来增加了priority字段。如果所有消费者都按旧版本解析,新字段会被忽略,旧字段的默认值又可能与新逻辑冲突。
解决思路是:协议从第一天就带上schema_version。字段变更时,尽量做向后兼容的增量更新,不要删除已有字段。如果确实要破坏性升级,建议让消费者同时兼容新旧两个版本,或者在消费端增加字段名映射层。
5.2 状态字段被人为扩展
第二个坑是自定义状态满天飞。协议里定义了pending、running,但有人觉得不够用,新增了waiting_for_confirm、middle_process、almost_done这类状态。短期看,状态表达更灵活了;长期看,状态机变得不可维护,下游判断逻辑越来越多分支。
我的建议是:核心状态保持精简,必要时把补充状态写入output或专门的metadata字段,而不是无限扩展状态枚举。状态枚举越多,协议的约束力就越弱。
5.3 任务结果编码不统一
第三个坑比较隐蔽:两个任务看起来都返回了结果,但结果格式完全不同。一个任务的output是纯文本,另一个任务的output是 JSON 字符串,第三个又是数组。消费者拿到之后,需要写一堆isinstance判断才能处理。
更好做法是:在同一任务类型下,输出结构保持一致。可以由协议约定每个 task type 对应的输出 Schema,并在注册任务处理器时提供样例。这样既保留了不同任务类型的灵活性,又限制了单一类型内的混乱。
5.4 任务中断没有恢复策略
第四个坑是任务跑到一半进程崩了。这时 task 的状态可能还停留在running,但实际已经没有进程在跑。如果没有恢复策略,这个任务会一直卡在运行中,占用重试额度,也不产生结果。
稳妥一点的处理是:
- 启动时扫描所有超过某个时间阈值且仍为
running的任务。 - 判断执行节点是否还活着。
- 如果节点已经不在了,把任务状态改为
pending并重新入队。 - 如果节点还在,可以通过回调或心跳判断是否还在处理中。
这一层逻辑在单体 Demo 里可以不写,但只要涉及的 agent 数量多了,就一定要考虑进去。
6. 排查链路:任务数据接不上时先查什么
6.1 数据层排查
遇到任务对接不上,第一个要看的是 job 数据本身是否完整。我会按这个顺序检查:
job_id是否为空,是否符合格式。type是否在消费者侧有对应的处理器。input是否完整,是否缺少必填参数。schema_version是否在消费者支持的版本范围内。- 字段类型是否一致,特别是整数、布尔值和字典。
这一步看起来很简单,但它能过滤掉大多数问题。很多所谓"协议对接失败",最后定位到的原因是生产者少写了一个字段,或者把整数写成了字符串。
6.2 状态层排查
数据没问题,任务还是不动,那就要看状态流转是否卡住了。
常见的卡点包括:
- 任务状态一直是
pending,但队列里没有消费者在拉取。 - 任务状态是
running,但执行进程已经不存在。 - 任务状态是
failed,但重试没有生效,因为attempt没有被正确增加。
排查状态问题时,日志里的时间戳很关键。看updated_at有没有更新,如果长时间没更新,大概率是执行节点挂掉或者状态写回失败。
6.3 执行层排查
最后要检查的是执行器本身。
- 执行器是否注册了对应的任务
type。 - 上下游依赖是否就绪,比如模型服务、数据库、外部 API。
- 执行进程是否因为内存、超时、权限被系统杀掉。
- 有没有独立的日志文件,可以把协议层日志和执行层日志分开,这样定位更快。
我建议在接入 OJCP 这类协议时,把协议层日志和业务日志区分开。协议层记录 job 数据的收发明细、状态变化、错误码;业务日志记录具体执行过程。这样遇到问题,先看协议层找到卡点,再去业务层看执行细节,效率会高很多。
结尾
如果你只是跑一个 agent Demo,OJCP 这种协议可能显得多余,直接用字典传递参数就够了。但一旦任务开始跨系统、跨团队流转,统一的 job data 结构就变成了基础设施。它的价值不是让某个任务跑得更快,而是让任务在多个 agent 之间交接时,不再各说各话。
我个人建议,无论最终要不要完整落地 OJCP,至少先把两件事做起来:定义统一的job_id生成规则,建立清晰的任务状态流转。这两点做到了,后续接入任何协议都会顺手很多。