步骤 1:发现阶段先缩小搜索空间
MCP 风格的服务通常先返回一整套平铺工具。ZCP 则允许原生 client 使用 `profile="semantic-workflow"`,并继续按 `groups / stages` 过滤,让模型在第一轮之前就进入更小的候选集。
一份基于代码实现的技术报告:它解释 ZCP 为什么更省 token、canonical runtime 是如何组织上下文的、为什么 JSON Schema 在原生路径里被去中心化,以及这些设计如何体现在代码和 benchmark 上。
这份报告要论证的不是泛泛的“ZCP 更先进”,而是一个更窄也更硬的命题:在模型主导、多轮、规划密集的工作负载下,ZCP 通过改变模型看到的执行合同,系统性地降低了 token 消耗和规划熵。
官方 MCP Python SDK 的默认路径是 schema-first 的:Python 函数先变成 Pydantic 模型,再变成 JSON Schema,`tools/list` 返回带完整 schema 的工具集合,`CallToolResult` 则把 `content` 和可选的 `structured_content` 继续暴露给调用方。这对互操作是合理的。
ZCP 的关键变化不是删掉 schema,而是让 schema 退出原生 runtime 的中心位置。原生 runtime 的核心对象变成 `ToolDefinition`、`SessionState`、`HandleRef`、`TaskExecutionContext`。JSON Schema 仍然保留,但只在校验和 provider adapter 边界上编译和使用。
因此,`8027.9` 对 `30723.7` 这个公开 benchmark 结果,并不是文案产物。它来自更小的工具可见集、更少的 schema 重复、更少的大结果重放,以及更少进入 prompt 的中间状态。
MCP 很擅长解决兼容边界;ZCP 优化的是这个边界之下的模型执行合同。
`full_semantic_compare_v5` 中,native ZCP 对 MCP surface 的总体 token 优势。
ZCP 保留 schema 校验,但不再让完整 JSON Schema 成为原生规划 surface 的中心。
这里强调的是分层:MCP 继续是外层兼容合同,ZCP 改变的是内部原生执行合同,而不是重写业务逻辑。
MCP 的目标是互操作协议。它要解决的是 host 和 client 之间如何共享地描述 tools、resources、prompts 和 transport。官方 Python SDK 的代码也完全围绕这个目标展开:函数签名变成 schema,schema 变成工具合同。
但模型执行成本不发生在“协议名字”这一层,而发生在模型每一轮到底要面对多少工具、多少 schema 文本、多少结果回放、多少中间状态。如果这些东西都持续暴露在 prompt 周围,模型就会持续为它们付 token。
因此,公平的比较方式不是问“二者能不能都表达 tool 调用”,而是问“模型每一轮到底需要对什么做规划”。只要问题换成这个角度,ZCP 的优势就不再抽象。
token 开销主要来自四类来源:第一,模型看到的工具太多;第二,每个工具附带的 schema 文本太大;第三,大结果不断回到后续 prompt;第四,后台或长任务状态没有被 runtime 保存,只能靠模型通过多轮调用和说明不断重建。
MCP 默认路径会自然放大这四项成本。`Tool.from_function(...)` 在注册阶段就生成 `model_json_schema(...)`,`list_tools()` 默认返回全部工具及其 `input_schema / output_schema`,而 `_handle_call_tool()` 再把结果包装回 `CallToolResult(content, structured_content)`。
ZCP 的做法不是写更短的文案,而是把策略下沉进 runtime:发现阶段先缩小工具集;结果阶段优先内联 scalar,大对象转成 `handle + summary`;长工作流则由 `TaskManager` 保持状态。token 因此下降,不需要靠口头解释。
ZCP 的决定性变化是架构性的:MCP-compatible surface 不是原生 runtime 本身。原生 runtime 的核心定义在 `src/zcp/canonical_protocol.py`,围绕的是 `ToolDefinition`、`SessionState`、`CallRequest`、`CallResult`、`HandleRef` 这些对象。
这些对象保存了 schema-first 设计里不居中的信息:`output_mode`、`handle_kind`、`defaults`、`flags`、当前 `tool_subset`、`registry_hash`、已存在的 handles 等等。这些字段决定了模型后续每一轮到底看到多少东西。
因为 runtime 先是 canonical 的,所以它可以向外投影成两条路径:`/mcp` 保持生态兼容,`/zcp` 改变模型看到的 discovery、calling、result 和 task state。兼容和优化因此可以同时成立,而不是二选一。
这里必须说清楚:ZCP 不是彻底抛弃 JSON Schema。它仍然需要 schema 来做参数校验,也仍然可以为 OpenAI 之类的 provider 编译 strict schema。真正被放弃的,是“让完整 JSON Schema 充当原生 runtime 的核心执行合同”这一做法。
在 MCP 中,schema 生成是上游且中心化的:`func_metadata(...)` 先构建 Pydantic 模型,`model_json_schema()` 生成 schema,`list_tools()` 再把这些 schema 直接作为对外工具定义返回。也就是说,同一个 schema 对象同时承担了注册元数据、transport 负载和模型可见描述三重角色。
在 ZCP 中,schema 只是 canonical contract 里的一个字段。`ToolDefinition` 依然有 `input_schema`,但原生 runtime 可以围绕工具 id、子集、handles、output mode、task state 组织执行。`OpenAIStrictSchemaCompiler` 只在 adapter 边界上,把当前选中的 `RegistryView` 编译成 provider 需要的 strict function tools。
这就是“去中心化 JSON Schema”的精确定义:schema 保留,但它不再主导原生规划 surface,也不再默认决定模型每一轮要吞下多少文本。
图 1 是架构边界图。它说明兼容层和优化层分别放在哪里。重点不是 ZCP 另起了一套业务逻辑,而是同一套 backend 在边界层上继续兼容,在原生层上收紧模型面对的执行合同。
图 2 是 token 因果图。它解释 token 是如何在 MCP-compatible 路径上被制造出来的:全量工具和全量 schema、宽规划、结果重放;以及在 native ZCP 路径上被削减的:子集发现、分阶段规划、scalar/handle、runtime 持久状态。
表 1 不是 benchmark 表,而是 token 成本来源映射表。表 2 则是 MCP 代码与 ZCP 代码的逐项对照。表 3 和表 4 才是实证表,分别给出总体 benchmark 和 Tier 拆解。这样读,结构才不会散。
benchmark 数字只有放回这条机制链中才有解释力。每一个步骤都对应一类 prompt-visible 浪费的消除。
MCP 风格的服务通常先返回一整套平铺工具。ZCP 则允许原生 client 使用 `profile="semantic-workflow"`,并继续按 `groups / stages` 过滤,让模型在第一轮之前就进入更小的候选集。
如果 `tools/list` 收窄了,但 `tools/call` 仍然能调用任何工具,那么收窄没有意义。ZCP 用 `enforce_tool_visibility_on_call` 保证 discovery surface 和 call surface 是一致的。
MCP 在注册时就生成 schema,并让它天然贯穿 transport。ZCP 则只在 adapter 需要时,从当前 `RegistryView` 编译 strict schema。模型不必默认面对整个 registry 的 schema 树。
canonical runtime 会检查 `output_mode`、值类型和大小。小值直接作为 `scalar` 返回,大值进入 `HandleStore`,只返回 `handle + summary`。下一轮上下文因此显著缩小。
tasks、handles、progress 和 session state 都由 runtime 保存。模型不需要再通过多轮读写和自然语言说明去重建一个已经存在的中间执行状态。
一旦服务端提供 workflow-level 工具,模型就不必在最低原语粒度上规划。Tier B、C、D 的优势,本质上都来自这个压缩过程,而不是某种 wire format 小技巧。
这里对比的是官方 MCP Python SDK 与本地 ZCP runtime 的实现策略,不是 Excel benchmark 本身。真正的问题是:状态、schema 和策略分别被放在了哪里。
| 关注点 | MCP 实现 | ZCP 实现 | token 后果 |
|---|---|---|---|
| 核心合同对象 | `src/mcp/types/_types.py::Tool` 把公共合同中心放在 `input_schema`、`output_schema` 和 `CallToolResult(content, structured_content)` 上。 | `src/zcp/canonical_protocol.py::ToolDefinition` 与 `SessionState` 则把 runtime 组织到工具 id、子集哈希、output mode、handles、defaults、flags 和 metadata 上。 | 更多状态由 runtime 保存,而不是让模型每轮重新推断。 |
| Schema 生成 | `src/mcp/server/mcpserver/tools/base.py::Tool.from_function` 在注册时就调用 `arg_model.model_json_schema(by_alias=True)`。 | `src/zcp/adapters/openai.py::compile_openai_tools` 只对选中的 `RegistryView` 编译 strict schema,而且只在 adapter 需要时才编译。 | 原生路径不会默认把整套 schema-bearing registry 暴露给模型。 |
| 发现逻辑 | `ToolManager.list_tools()` 返回全部工具;`server.py::list_tools()` 序列化所有带 schema 的工具定义。 | `src/zcp/server.py::_select_tools(...)` 在 discovery 前就按 profile、groups、stages 切子集。 | 第一次规划前的分支因子就下降。 |
| 调用纪律 | `ToolManager.call_tool(...)` 只检查名字是否存在,然后执行。 | `src/zcp/server.py::_tool_is_exposed(...)` 配合 `enforce_tool_visibility_on_call`,只允许调用当前暴露子集中的工具。 | discovery 的收窄不会在调用时失效。 |
| 结果形态 | `src/mcp/server/mcpserver/server.py::_handle_call_tool()` 把结果包装回 `CallToolResult(content, structured_content)`。 | `src/zcp/canonical_runtime.py::_build_result()` 在 `scalar` 和 `handle + summary` 之间自动选择。 | 大结果不再把后续每一轮 prompt 撑大。 |
| 原生模型语法 | 工具合同主要体现为带完整 schema 的 JSON 对象和 content blocks。 | `src/zcp/profiles/native.py::format_registry()` 可把工具压缩成 `TOOL @id alias(param:type) -> output_mode` 这种紧凑语法。 | 原生 planner 可以围绕紧凑签名工作,而不是围绕完整 schema 树工作。 |
| 长任务状态 | 任务能力存在,但通用 surface 仍容易自然滑向 prompt-visible 的 `CallToolResult` 循环。 | `TaskManager`、`TaskExecutionContext`、progress 通知和 handles 让状态成为 runtime 一等公民。 | 轮询和 repair loop 变小,尤其利好 Tier D。 |
| token 成本来源 | MCP 默认形态 | ZCP 对应机制 | 为什么会影响模型 |
|---|---|---|---|
| 重复的工具 schema 暴露 | 宽泛 `tools/list` 会返回完整 schema-bearing tool definitions。 | native discovery 只返回当前 profile / stage 的工具子集。 | 可见 schema 更少,prompt token 和规划熵都更低。 |
| schema 直接充当规划 surface | JSON Schema 从注册到 transport 一直处于中心位置。 | JSON Schema 只在 adapter 边界上,从选中的 registry view 里编译。 | runtime 不再迫使模型围绕整棵 schema 树去规划。 |
| 大结果反复回放 | tool 结果容易作为 content 或 structured_content 进入后续回合。 | 大值转为 handle,小值转为 scalar。 | 下一轮携带的是引用和摘要,而不是完整工件。 |
| 后台状态 prompt-visible | 中间状态容易泄漏回 tool loop 和说明文本。 | tasks、handles、progress、session state 由 runtime 保存。 | 长工作流更稳定,轮询更少。 |
| 发现和执行不一致 | 模型列出了某个 surface,但执行时仍可能乱跳到整个 registry。 | 调用也受当前可见性策略约束。 | 第一轮收窄的动作空间不会在执行时重新膨胀。 |
下面这些片段直接来自官方 MCP Python SDK 和本地 ZCP 实现。它们比任何概念性描述都更能说明为什么 ZCP 的原生路径会更省 token。
modelcontextprotocol/python-sdk/src/mcp/server/mcpserver/tools/base.py
官方 MCP server 路径会先把函数元数据变成 Pydantic 模型,再变成 JSON Schema,这个 schema 之后自然成为工具合同的一部分。
class Tool(BaseModel):
fn: Callable[..., Any] = Field(exclude=True)
name: str = Field(description="Name of the tool")
parameters: dict[str, Any] = Field(description="JSON schema for tool parameters")
fn_metadata: FuncMetadata = Field(...)
@classmethod
def from_function(cls, fn: Callable[..., Any], ...):
func_arg_metadata = func_metadata(fn, ...)
parameters = func_arg_metadata.arg_model.model_json_schema(by_alias=True)
return cls(
fn=fn,
name=func_name,
parameters=parameters,
fn_metadata=func_arg_metadata,
)
modelcontextprotocol/python-sdk/src/mcp/server/mcpserver/server.py
默认调用路径把结果包装进 `CallToolResult(content, structured_content)`。这对兼容很自然,但也意味着大结果更容易继续进入后续 prompt。
async def _handle_call_tool(self, ctx, params) -> CallToolResult:
result = await self.call_tool(params.name, params.arguments or {}, context)
if isinstance(result, CallToolResult):
return result
if isinstance(result, tuple) and len(result) == 2:
unstructured_content, structured_content = result
return CallToolResult(
content=list(unstructured_content),
structured_content=structured_content,
)
return CallToolResult(content=list(result))
zero-context-protocol-python/src/zcp/canonical_protocol.py
ZCP 没有让 schema 消失,而是让 schema 变成更大 runtime contract 里的一个字段。真正居中的,是子集、handles、output mode 和 session state。
@dataclass
class ToolDefinition:
tool_id: str
alias: str
description_short: str
input_schema: dict[str, Any]
output_schema: dict[str, Any] | None = None
output_mode: Literal["handle", "scalar"] = "handle"
handle_kind: str = "generic"
defaults: dict[str, Any] = field(default_factory=dict)
flags: frozenset[str] = field(default_factory=frozenset)
metadata: dict[str, Any] = field(default_factory=dict)
@dataclass
class SessionState:
session_id: str
registry_hash: str = ""
tool_subset: tuple[str, ...] = ()
handles: dict[str, HandleRef] = field(default_factory=dict)
zero-context-protocol-python/src/zcp/server.py
profile 和 stage 过滤是 runtime 规则,不是 prompt 工程;模型在第一轮之前就被限制在更小搜索空间里。
def _select_tools(app: FastZCP, params: dict[str, Any]) -> list[Any]:
tools = app.tool_registry.subset().tools
profile = _effective_tool_profile(app, params)
include_groups = _normalize_filter_values(params.get("groups"))
stages = _normalize_filter_values(params.get("stages"))
if profile == app.semantic_workflow_profile:
workflow_tools = [tool for tool in tools if app.semantic_group in _tool_groups(tool)]
if workflow_tools:
tools = workflow_tools
if include_groups:
tools = [tool for tool in tools if _tool_groups(tool) & include_groups]
if stages:
tools = [tool for tool in tools if _tool_stages(tool) & stages]
return tools
zero-context-protocol-python/src/zcp/adapters/openai.py
strict JSON Schema 仍然保留,但它是从当前 `RegistryView` 现编出来的 provider artifact,而不是 runtime 的中心对象。
def compile_openai_tools(self, session: SessionState, *, tool_subset=None, strict_mode=True):
subset_tuple = tuple(tool_subset or ())
registry_view = self.registry.subset(list(subset_tuple) if subset_tuple else None, limit=self.tool_limit)
session.registry_hash = registry_view.hash
session.tool_subset = subset_tuple
if key not in self._tool_cache:
tools = self.compiler.compile_registry(registry_view)
self._tool_cache[key] = tools
return self._tool_cache[key]
zero-context-protocol-python/src/zcp/profiles/native.py
这段代码最直接地体现了“去中心化 JSON Schema”:原生 planner 只需要工具 id、别名、紧凑类型和 output mode。
def format_registry(tools: list[ToolDefinition]) -> str:
entries = []
for tool in tools:
params = ",".join(
f"{name}:{_compact_type(schema)}"
for name, schema in tool.input_schema.get("properties", {}).items()
)
entries.append(f"TOOL @{tool.tool_id} {tool.alias}({params}) -> {tool.output_mode}")
return "\n".join(entries)
zero-context-protocol-python/src/zcp/canonical_runtime.py
这是 discovery 之后第二重要的 token 节约机制。大对象不再每轮原样回到 prompt,而是通过 handle 延迟展开。
if tool.output_mode == "scalar" and (tool.inline_ok or is_scalar_value(value)):
return CallResult(
cid=request.cid,
status="ok",
scalar=value,
summary=summary,
meta=meta,
)
handle = self.handle_store.create(
kind=handle_kind,
data=value,
summary=summary,
meta=meta,
)
return CallResult(
cid=request.cid,
status="ok",
handle=handle,
summary=handle.summary,
meta=meta,
)
token 优势是一条因果链:更小的 registry subset、更严格的调用纪律、更紧凑的结果传播,以及由 runtime 持有的状态。
| 路径 | Answer | Workbook | Tool | 平均总 token | 平均轮次 |
|---|---|---|---|---|---|
| `zcp_client_to_native_zcp` | 100.0% | 97.3% | 100.0% | 8027.9 | 2.8 |
| `mcp_client_to_zcp_mcp_surface` | 97.3% | 91.9% | 73.0% | 30723.7 | 4.1 |
| Tier | 结构上发生了什么 | Native ZCP | MCP Surface | 优势倍数 |
|---|---|---|---|---|
| A | 几乎没有多少规划浪费可以消除 | 15979.4 | 17613.2 | 1.10x |
| B | 短链路被 semantic chain tools 压缩 | 1826.6 | 29239.4 | 16.01x |
| C | workflow tools 移除了长 primitive 计划 | 2091.1 | 72113.9 | 34.49x |
| D | 自主规划获得了最小搜索空间 | 2018.3 | 19375.7 | 9.60x |
单次工具调用本来就没有多少规划浪费可以削掉,所以这个 tier 只能证明 ZCP 没有退化,不应该承担 headline。
当模型本来需要在多个强耦合 primitive 调用之间规划时,semantic chain tools 和 discovery 收窄会立刻降低内部决策点数量。
这说明优势不是 wire format 的小修小补,而是模型不再被迫规划每一个底层单步操作,token 下降因此是结构性的。
Tier D 最容易出现 repair loop、重复 readback 和状态噪声。native ZCP 能赢,是因为 runtime 在这些噪声进入 prompt 之前就先把它们压住了。
ZCP 比 MCP 更强,不是因为它改了 transport,也不是因为它把业务逻辑重写了一遍,而是因为它改变了模型真正面对的执行合同。
官方 MCP 代码路径是 schema-first、compatibility-first 的;ZCP 代码路径则是 canonical-runtime-first 的:schema 仍然保留,但它退到 adapter 边界,原生 runtime 则围绕工具子集、handles、output mode 和 task state 来组织。
这套设计会直接产生 benchmark:工具更少可见、schema 更少重复、大 payload 不再不断重放、中间状态不再泄漏进 prompt。因此,对于规划密集和多轮工作负载,ZCP 的 token 成本和规划熵都会更低。