怎样让 Codex 真正会用你的工具
原创 · 约 45 分钟阅读 · 阅读 --

怎样让 Codex 真正会用你的工具

作者: Alex Xiang


最近读到 Thariq Shihipar 写的一篇帖子:The new rules of context engineering for Claude 5 models。其中有一句很值得做工具的人认真想一想:

过去给模型大量工具调用示例,现在更应该设计好接口。

原帖讨论的是 Claude Code。这个结论能不能直接搬到 Codex,我不想只靠感觉判断,于是做了一个小实验。

我实现了一个真正对编程 Agent 有用的 MCP 工具:代码变更影响分析器。它读取 Git diff、CODEOWNERS 和仓库结构,告诉 Codex:

  • 哪些模块受到影响;
  • 应该找哪些人 review;
  • 哪些测试和生成脚本必须运行;
  • 是否涉及数据库迁移、认证边界、API 契约和部署流程;
  • 输出是否因为数量上限被截断。

同一套底层逻辑,我做了三个接口版本,再让 Codex CLI 完成 6 类任务,每类重复两次,共运行 36 次。

结果很直接:工具后端一行算法都没改,只调整接口,最终答案通过率就从 7/12 变成 12/12。更早的无安全注解版本,12 次调用则全部被 Codex 默认策略拦截。

这篇文章记录完整过程。重点不是证明某个 Schema 可以包治百病,而是回答一个更实用的问题:

一个工具到底应该长成什么样,Codex 才能稳定发现它、填对参数、拿到够用而不过量的结果,并且知道这次调用是否安全?

接上 MCP,不等于 Codex 会用

MCP 把外部能力接进 Codex。按照 Codex 的 MCP 文档,Server 可以提供工具、资源和提示词;工具通过名称、描述和 JSON Schema 暴露参数。

从协议上看,下面这个工具完全合法:

{
  "name": "analyze",
  "description": "Analyze repository data and return useful information.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "data": { "type": "string" },
      "type": { "type": "string" },
      "options": { "type": "object" }
    },
    "required": ["data"]
  }
}

它的问题也很明显。

analyze 分析什么?data 是文本、文件路径、Git revision 还是 URL?type 有哪些合法值?options 里能放什么?它会不会修改仓库?返回的是结论、原始数据,还是一大段日志?

人类开发者会去翻 README、看示例、读实现。Codex 做一次工具选择时,主要依赖的就是眼前的工具定义和当前任务。Schema 留下的空白,模型只能自己补。

这不是模型偶尔粗心,而是接口要求调用方猜协议。

工具定义本身就是模型看到的界面

传统 API 的主要调用者是程序。编译器、类型检查器和 IDE 能在开发阶段发现很多问题。

Agent 工具的调用者是模型。它会先根据名称和描述判断要不要使用,再把自然语言目标翻译成参数,最后理解返回结果并决定下一步。

一条工具链至少有四层契约:

  1. 发现契约:工具叫什么,何时应该用,何时不该用;
  2. 输入契约:用户意图怎样落到有限、合法的参数;
  3. 执行契约:是否只读、是否有副作用、能否重复调用;
  4. 输出契约:返回什么、证据在哪里、是否截断、失败后怎么办。

任何一层含糊,Codex 都可能在错误的地方“发挥智能”。

Codex 使用一个工具时依次经过发现、输入、执行和输出四层契约

示例工具:代码变更影响分析器

测试仓库里有两个提交。第二个提交改了 5 个文件:

.github/workflows/deploy.yml
backend/auth/service.py
backend/db/migrations/20260727_add_sessions.py
backend/openapi/schema.json
frontend/src/login.ts

仓库还包含一份 CODEOWNERS

/backend/auth/        @identity-team
/backend/db/          @data-platform
/backend/openapi/     @api-platform
/frontend/            @web-team
/.github/workflows/   @release-engineering

工具做的是确定性分析,不调用模型:

def assess_change_impact(base_ref, head_ref, component, focus):
    changed_files = git_diff(base_ref, head_ref)
    scoped_files = filter_by_component(changed_files, component)

    return {
        "changed_files": scoped_files,
        "components": detect_components(scoped_files),
        "owners": match_codeowners(scoped_files),
        "required_checks": infer_checks(scoped_files),
        "risks": detect_release_risks(scoped_files),
        "truncated": False,
    }

它能识别几类具体规则:

变更风险必要检查
backend/auth/**AUTH_BOUNDARY认证测试与后端测试
backend/db/migrations/**DATABASE_MIGRATION升级与回滚迁移
backend/openapi/**API_CONTRACT重新导出并检查文档 diff
frontend/**前端构建与登录链路单测、构建、端到端登录测试
.github/workflows/**RELEASE_WORKFLOWactionlint 与部署验证

这个例子有意选得比天气查询复杂。它既有文件、枚举和数量限制,又有只读安全语义;有些任务需要完整结果,有些只需要某个组件的责任人或检查命令。只有这种任务,才能看出接口设计是否真的帮到了 Codex。

第一版:把复杂度塞进 options

第一版就是前面的 analyze(data, type, options)

为了先排除安全注解的影响,我给它补上了只读、幂等和封闭世界注解,但没有改变名称与输入 Schema。Codex 随后生成了这些参数:

{
  "data": "HEAD~1...HEAD",
  "type": "git_range",
  "options": {
    "scope": "authentication",
    "focus": "release_risks",
    "include_only_risks": true,
    "include_changed_files": true,
    "include_risk_codes": true,
    "run_checks": false
  }
}

另一轮要求“最多返回两个文件”时,它写的是:

{
  "data": "HEAD~1...HEAD",
  "type": "git",
  "options": {
    "maxFiles": 2
  }
}

第二次又变成:

{
  "data": "HEAD~1...HEAD",
  "type": "git_range",
  "options": {
    "max_files": 2
  }
}

从自然语言角度看,这些参数都很合理。问题是工具并不认识 scopeinclude_only_risksmaxFilesmax_files。它只认识实现里约定的 focuspathslimit

模型认真表达了意图,接口却没有给它合法的表达方式。

最终答案正确,工具调用也可能已经错了

更有迷惑性的是,Codex 有时仍能给出正确答案。

工具忽略了不认识的字段,扫描完整 diff,返回 5 个文件、所有 owner、10 条检查命令和 5 项风险。Codex 再从这堆结果里筛出数据库迁移的两条命令:

uv run alembic upgrade head
uv run alembic downgrade -1

只看最终文本,这一题通过了。

但工具调用实际有三个问题:

  • 用户只问数据库,工具却分析了完整仓库;
  • 返回结果包含大量无关内容;
  • 这次是 Codex 筛对了,下次可能把无关风险一起带进答案。

所以评测工具不能只看最终回答。还要看原始参数、工具结果和完整调用轨迹。

第二版:参数应该表达业务意图

我先把第一版改成下面这样:

{
  "name": "assess_change_impact",
  "description": "Read a Git diff and report affected components, CODEOWNERS, required checks, and release risks.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "base_ref": { "type": "string", "default": "HEAD~1" },
      "head_ref": { "type": "string", "default": "HEAD" },
      "focus": {
        "type": "string",
        "enum": ["all", "tests", "owners", "release_risk"]
      },
      "path_filters": {
        "type": "array",
        "items": { "type": "string" }
      },
      "max_changed_files": {
        "type": "integer",
        "minimum": 1,
        "maximum": 200
      }
    },
    "additionalProperties": false
  }
}

名称终于说明了动作,focus 把输出目的限制成四种,数量上限也有明确边界。

认证、迁移和前端任务都明显改善了。不过 API 契约任务仍然失败:Codex 知道用户要 tests,却不知道 API 契约在这个仓库里对应 backend/openapi/**,于是没有填写 path_filters,工具还是返回了完整 diff 的检查命令。

这次不是描述不够长,而是参数抽象层级不对。

path_filters 是存储结构,api_contract 才是用户和 Codex 想表达的业务意图。

最终版:让工具自己知道仓库知识

最终版增加了一个语义参数:

{
  "component": {
    "type": "string",
    "enum": [
      "all",
      "authentication",
      "database_migration",
      "api_contract",
      "frontend",
      "ci_cd"
    ],
    "description": "Stable semantic scope. The tool maps it to repository paths.",
    "default": "all"
  }
}

目录映射留在工具内部:

COMPONENT_FILTERS = {
    "authentication": ["backend/auth/**"],
    "database_migration": ["backend/db/migrations/**"],
    "api_contract": ["backend/openapi/**"],
    "frontend": ["frontend/**"],
    "ci_cd": [".github/workflows/**"],
}

path_filters 没有删除,但降级成高级参数:只有用户要求某个特殊目录时才需要填写。常见任务走稳定的组件枚举。

含糊接口要求 Codex 猜自由字段和仓库路径,语义化接口让组件与关注点沿着类型明确的轨道进入工具

现在,API 契约任务的真实调用变成:

{
  "base_ref": "HEAD~1",
  "head_ref": "HEAD",
  "component": "api_contract",
  "focus": "tests"
}

工具只返回:

{
  "changed_files": [
    "backend/openapi/schema.json"
  ],
  "required_checks": [
    "git diff --exit-code docs/openapi",
    "uv run python scripts/export_openapi.py"
  ],
  "summary": "1 changed file affects the API contract."
}

Codex 不需要知道 OpenAPI 文件放在哪,也不需要从十条命令里猜哪两条与问题有关。

这里有一条很实用的设计原则:

参数应该接近用户意图,目录、表名、供应商字段和内部 ID 的映射尽量留在工具内部。

当然,前提是这个语义稳定。如果组件集合每天变化,硬编码枚举会变成新的维护负担。可以由工具提供一个只读的组件发现能力,或者从仓库配置动态生成枚举,但不要把不稳定的内部结构直接推给模型。

36 次真实调用的结果

测试环境如下:

  • Codex CLI 0.145.0
  • Python 3.14.4
  • Git 2.53.0
  • STDIO MCP Server;
  • 只读沙箱;
  • 每次任务使用独立的非交互 Codex 会话;
  • 6 类任务,每类 2 次,每个版本 12 次。

任务包括:

  1. 只查认证部分的发布风险;
  2. 只列数据库迁移的验证命令;
  3. 只找前端登录文件的 owner;
  4. 只列 API 契约的必要检查;
  5. 分析完整 diff;
  6. 最多返回两个文件并说明是否截断。

三个版本

版本接口特征
A:旧式接口analyze(data, type, options),没有工具注解
B:含糊接口同样的自由参数,补齐只读等注解
C:语义接口明确名称、组件与关注点枚举、数量边界、结构化输出

结果

指标A:旧式接口B:含糊接口C:语义接口
运行次数121212
工具调用错误1200
最终答案通过0712
参数准确表达意图2312
工具结果范围正确0212
平均返回变更文件数05.001.83
平均工具结果字符数217661455
平均完成时间17.43 秒31.00 秒16.45 秒

同一底层能力的三个接口版本经过固定任务、Codex 调用轨迹和多维评分器进行对照评测

有几个数字需要单独解释。

A 版本为什么全部报错

它没有声明 readOnlyHint。MCP 规范里的工具注解只是提示,而且客户端不能盲目信任不可信 Server;但在受信任的本地 Server 上,Codex 会用这些信息参与副作用和审批判断。

在这组非交互、只读评测里,A 版本被当成可能有副作用的工具,12 次调用都进入了取消状态。这不是工具业务逻辑失败,而是工具没有把自己的执行语义告诉 Codex。

B 版本为什么耗时反而更长

B 版本每次都返回全部 5 个文件。Codex 需要从长结果中再次筛选,有些任务还把无关项目带进最终答案。

平均 31 秒不能被解读成严格性能基准。模型服务时延存在波动,样本也只有 12 次。但它至少说明:返回更多内容不会自动让 Codex 更快、更可靠。

C 版本的字符数只减少了约 18%

完整 diff 任务本来就需要全部内容,而且结构化 JSON 带有字段名,因此字符数没有像文件数一样大幅下降。

真正明显的变化是平均文件数从 5 降到 1.83,目标范围正确率从 2/12 提升到 12/12。对于更大的真实仓库,这种范围控制通常比省几百个字符更重要。

这不是通用排行榜

这组实验的限制也很明确:

  • 只有一个小型仓库和一种工具;
  • 每个任务只重复两次;
  • 提示词明确要求使用 MCP,没有评估“上百个工具中能否主动发现它”;
  • 评测的是接口设计,不是变更影响算法的准确率;
  • 没有读取或评判模型的隐藏推理,只检查可观察的调用、结果和答案;
  • 平均耗时受模型服务状态影响,不适合作为不同 Codex 版本之间的性能结论。

它足以说明接口改造在这个场景里产生了稳定差异,但不应该被写成“所有工具都能提升多少”的结论。

工具名首先解决路由问题

analyzequeryexecuteprocess 这类名字很适合内部函数,不适合放进一组 Agent 工具。

工具名最好同时包含资源和动作:

assess_change_impact
search_incidents
get_deployment_status
create_release_candidate
cancel_pending_job

如果多个 MCP Server 里都有 search,应加服务或资源命名空间:

github_search_issues
docs_search_pages
metrics_search_series

名称不是越长越好。关键是 Codex 只看名字时,也能大致判断它与当前任务有没有关系。

工具描述的第一句则回答“做什么”和“什么时候用”:

Read a Git diff and report affected components, CODEOWNERS,
required checks, and release risks. Use before planning
verification, assigning reviewers, or merging a change.

接着说明重要边界:

This tool is read-only. It does not run tests, edit files,
or contact GitHub.

不要在每个工具描述里重复整套团队流程。跨工具都适用的限制可以放在 MCP Server 的 instructions 中。Codex 文档建议 Server Instructions 的前 512 个字符能够自解释,适合放限流、共同约束和跨工具流程。

Schema 是给 Codex 使用的一门小语言

JSON Schema 不只是参数校验器,它定义了 Codex 可以怎样表达意图。

用枚举表达有限选择

下面的字段让 Codex 自己发明值:

{
  "mode": { "type": "string" }
}

实际调用里很快会出现:

release_risk
release_risks
release-risk
deployment_risk
risks_only

如果系统只有四种合法行为,直接写枚举:

{
  "focus": {
    "type": "string",
    "enum": ["all", "tests", "owners", "release_risk"]
  }
}

枚举值应该描述语义,而不是把数据库里的 type=3 暴露出来。

不要让自由对象承担协议

options: object 看起来扩展性很好,实际上等于没有契约。

真正可选的能力应成为明确字段。确实需要键值映射时,也要限制键和值:

{
  "labels": {
    "type": "object",
    "additionalProperties": {
      "type": "string",
      "maxLength": 80
    },
    "maxProperties": 20
  }
}

默认值要符合最安全的常见路径

只读查询可以有方便的默认值:

{
  "base_ref": { "type": "string", "default": "HEAD~1" },
  "head_ref": { "type": "string", "default": "HEAD" },
  "component": { "enum": ["all", "..."], "default": "all" }
}

写操作不要用危险默认值。删除工具不应默认“删除全部”,发布工具也不应在省略环境时默认生产。

限制范围要成为一等参数

分页、时间范围、最大结果数、字段投影和截断标志,应该在接口里明确存在:

{
  "max_changed_files": {
    "type": "integer",
    "minimum": 1,
    "maximum": 200,
    "default": 100
  }
}

否则 Codex 只能在拿到过量结果以后自己截断。此时费用和上下文已经消耗,工具端的查询压力也已经发生。

禁止悄悄接收拼错的字段

{
  "type": "object",
  "additionalProperties": false
}

如果第一版接口设置了这一项,maxFilesmax_filesscope 会立即变成可见错误,而不是被工具静默忽略。

严格失败并不可怕。静默忽略参数、返回一个看似成功的宽泛结果,才最难排查。

输出也需要 Schema

MCP Tools 规范支持 outputSchemastructuredContent。它们能让客户端和模型知道返回字段的类型,也能在工具边界做校验。

这个示例的核心输出是:

{
  "changed_files": ["backend/auth/service.py"],
  "components": ["authentication", "backend"],
  "owners": {
    "backend/auth/service.py": ["@identity-team"]
  },
  "required_checks": [
    "uv run pytest backend/tests/test_auth.py"
  ],
  "risks": [
    {
      "code": "AUTH_BOUNDARY",
      "severity": "high",
      "evidence": "backend/auth/service.py",
      "reason": "Authentication changes can alter authorization boundaries."
    }
  ],
  "truncated": false,
  "summary": "1 changed file; 1 high release risk."
}

这比一段混合 owner、命令和风险的长文本更适合 Codex继续处理。

每个结论带证据

不要只返回:

{ "risk": "high" }

至少要说明:

  • 风险代码;
  • 严重程度;
  • 触发它的文件或数据;
  • 判断原因;
  • 下一步可以执行的检查。

模型不应该凭一枚 high 标签重新编造解释。

明确区分空结果、部分结果和失败

这三种情况完全不同:

{
  "status": "complete",
  "items": [],
  "truncated": false
}
{
  "status": "partial",
  "items": ["..."],
  "truncated": true,
  "next_cursor": "..."
}
{
  "status": "error",
  "error": {
    "code": "INVALID_GIT_REVISION",
    "retryable": false,
    "hint": "Use a revision visible in this repository."
  }
}

如果失败时仍返回 HTTP 200 和一句藏在正文里的错误,Codex 很容易把它当成空结果继续推理。

返回高信号,不返回后台仓库

工具结果要帮助下一步决策,不是把底层所有字段搬进上下文。

常见的降噪方法包括:

  • 默认只返回必要字段;
  • 让调用方选择 focus 或字段投影;
  • 对日志保留证据行与前后文,而不是完整文件;
  • 对列表设置上限和游标;
  • 原始大结果保存成资源链接,正文只返回摘要;
  • 稳定排序,避免同一结果每次顺序不同。

安全注解不是装饰

MCP 工具可以提供:

{
  "readOnlyHint": true,
  "destructiveHint": false,
  "idempotentHint": true,
  "openWorldHint": false
}

这些注解分别说明:

  • 是否只读;
  • 是否可能产生破坏性更新;
  • 同样参数重复调用是否会产生额外效果;
  • 是否会与开放的外部世界交互。

规范明确说明它们只是 hint,客户端不能把来自不可信 Server 的声明当作安全事实。但对于受控的 MCP Server,正确注解能帮助 Codex 选择审批策略。

本次实验里,无注解版本在默认非交互环境下 12 次全部被取消,就是一个很具体的例子。

反过来也不能为了少一次审批,把写工具谎报成只读。工具真正的权限、租户隔离、参数校验和审批仍要由执行端强制保证。

工具、Skill 和 AGENTS.md 各管什么

很多团队发现工具不好用,就继续往系统提示词或 AGENTS.md 里加说明:

使用 analyze 时,type 必须写 git_range。
数据库任务在 options.focus 写 database migration。
限制数量时写 limit,不要写 maxFiles。

这相当于用文档修补一个本可以由类型系统表达的问题。

在 Codex 里,更合理的分工是:

载体适合放什么
Tool Schema单次能力、参数、输出和副作用
MCP Server Instructions同一 Server 的共同约束、限流和跨工具规则
Skill多步工作流、团队经验、何时组合哪些工具
AGENTS.md仓库约定、命令、验证方式和容易踩的坑
用户 Prompt这一次任务的目标、范围和交付要求

Codex 对 Skill 使用渐进式加载:先看名称和描述,选择后再加载完整 SKILL.md,需要时才读取引用和脚本。复杂工作流适合放 Skill,原子能力适合放 Tool。

例如:

  • assess_change_impact 是 Tool;
  • “发布 PR 前先分析影响,按风险补测试,生成 reviewer 清单”是 Skill;
  • “本仓库 OpenAPI 文件必须提交生成结果”是 AGENTS.md 的仓库约定。

把三者揉成一段巨长工具描述,会让工具难以复用,也让每次调用都背上无关上下文。

示例到底要不要写

原帖提出“从给示例转向设计接口”,不是说示例从此没有价值。

示例最适合放在三个地方:

  1. 文档里,帮助人理解;
  2. 测试里,证明输入输出;
  3. 评测集里,检查 Codex 是否会用。

不应该依赖十几个 few-shot 示例,去解释一个本来可以由枚举、格式和字段描述表达的协议。示例可能让模型过拟合到某个路径,也会长期占用上下文。

本次实验的 api_contract 迭代正说明了这一点。第一版 path_filters 描述里给过认证目录示例,认证任务做对了,API 契约仍然没有映射成功。真正解决问题的不是再加五个目录示例,而是增加 component 这个语义字段。

怎样给自己的工具做评测

Anthropic 在 Writing effective tools for AI agents 中建议从真实任务构造工具评测,记录工具选择、参数、调用次数、错误和结果质量。虽然文章以 Claude 为主,这套实验方法同样适合 Codex。

先保存可观察轨迹

Codex CLI 的 --json 会输出 JSONL 事件,可以看到:

  • 调用了哪个 MCP Server 和工具;
  • 参数是什么;
  • 工具结果是什么;
  • 调用是否失败;
  • 最终回答是什么;
  • 一次任务用了多长时间。

本次评测命令的核心形式是:

codex exec \
  --ephemeral \
  --json \
  --sandbox read-only \
  -c 'mcp_servers.impact.command="python3"' \
  -c 'mcp_servers.impact.args=["impact_server.py", "--variant", "good"]' \
  "Use the change-impact capability to report only API contract checks."

输出事件中可以直接看到:

{
  "type": "mcp_tool_call",
  "server": "impact",
  "tool": "assess_change_impact",
  "arguments": {
    "base_ref": "HEAD~1",
    "head_ref": "HEAD",
    "component": "api_contract",
    "focus": "tests"
  },
  "status": "completed"
}

至少评四类指标

指标要回答的问题
最终任务通过率用户目标是否完成
参数意图准确率参数是否真正表达了请求
结果范围准确率工具是否只返回需要的数据
工具错误率Schema、审批和执行是否失败

还可以记录调用次数、结果大小、任务时延和重试次数。

评测集要包含相邻但不同的任务

如果只测“分析完整 diff”,含糊接口也可能表现很好,因为它本来就返回所有内容。

真正能拉开差异的是:

  • 只要风险,不要测试;
  • 只看一个组件;
  • 只找 owner;
  • 限制最多两条;
  • 输入 revision 非法;
  • 没有匹配文件;
  • 结果超过上限;
  • 工具明确返回不可重试错误。

这些任务在检查接口边界,不是在检查模型会不会复述结果。

一次只改一个变量

先补安全注解,再改名称和 Schema,再改输出。每次重跑同一组任务。

如果同时换模型、改 Prompt、重写工具逻辑和调整测试数据,最后即使分数提高,也不知道是谁起了作用。

一份可以直接使用的检查表

发现

  • 工具名是否同时包含资源和动作;
  • 描述第一句是否说明做什么、何时用;
  • 是否写清“不会做什么”;
  • 相似工具是否有清楚边界;
  • 工具数量是否可以用 enabled_tools 做最小化配置。

输入

  • 参数名是否无歧义;
  • 有限选择是否使用枚举;
  • 日期、ID、路径和单位是否注明格式;
  • 用户意图是否被迫翻译成内部实现细节;
  • 是否有分页、范围、字段投影和数量上限;
  • 是否设置 additionalProperties: false
  • 默认值是否安全。

执行

  • readOnlyHintdestructiveHintidempotentHintopenWorldHint 是否真实;
  • 写操作是否有确定性权限检查;
  • 重试是否会重复产生副作用;
  • 长任务是否有超时、进度和取消;
  • Secret 是否留在执行层。

输出

  • 是否提供 outputSchemastructuredContent
  • 结论是否带证据;
  • 空结果、部分结果和错误是否可区分;
  • 是否返回 truncated 与继续读取方式;
  • 错误是否包含稳定代码、retryable 和修复提示;
  • 是否把大量低信号原始数据塞回上下文。

评测

  • 是否保存原始工具调用和结果;
  • 是否同时看最终答案与参数;
  • 是否有相邻能力、边界和错误任务;
  • 是否至少重复运行;
  • 是否每次只改变一个主要变量;
  • 线上失败是否会进入回归集。

最后

一个适合 Codex 的工具,不是把现有 REST API 原样包成 MCP 就结束了。

REST API 可能为了兼容多年调用方,保留宽泛的 options、内部状态码和复杂对象;Codex 需要的是另一层面向意图的接口:

  • 名称帮助它找到能力;
  • 描述告诉它何时使用;
  • Schema 限制合法表达;
  • 语义参数隐藏内部结构;
  • 注解说明副作用;
  • 结构化结果提供证据和边界;
  • 错误告诉它能不能重试;
  • Skill 再把多个工具组织成稳定流程。

模型越强,越不应该让它把能力浪费在猜字段、猜目录和过滤垃圾结果上。

最好的工具接口并不是给 Codex 更多“自由发挥”的空间,而是把真正需要判断的部分留给模型,把协议、权限、范围和错误处理做成清楚的工程契约。

打开原图 ↗