← 返回文章列表

Dify 韧性验证实验(02):契约与消费一致性——多应用协作时契约变了如何第一时间发现?

1. 业务场景

先讲一个我们实际遇到的场景。

一家做客服工单 SaaS 的公司,运维同学半夜接到「支付网关报错」的告警,把故障描述粘进排障助手,系统自动诊断、给出排查建议,并通知运维值班群。开发阶段,通知功能用的是 mock 服务;上线前,要切换成真实消息 API。切换那天,最怕的就是「看着一切正常,其实收件人传错了、返回结构变了」——排障建议发给了错误的人,诊断结论却是「通知成功」。

我们第一次接这类需求时,第一反应是「mock 和真实 API 都是通知,能有多大差别」。真正动手才发现——差别全藏在字段名和返回结构里:mock 用 to 作收件人,真实 API 用 recipient,切换后下游全部错位,应用却不报一个错。从那以后,凡是「开发期 mock、上线前切真实依赖」的集成,我们都先做一遍契约核对。

这不是个例。任何「开发期用 mock、上线前切真实依赖」的集成都是这个模式:支付回调、短信通道、订单查询、企业微信通知——切换前后应用都能跑,只是悄悄传错字段或消费错结构,不对比根本发现不了。

2. 场景痛点

这个流程的痛点,在排障助手上体现得最直接:

本质上,契约问题最阴险的地方在于——切换前后应用都能跑,只是悄悄传错字段或消费错结构,不对比根本发现不了

3. 方案:为什么是契约验证

Dify 工作流里验证工具层契约最系统的方法,就是契约三件套:参数契约、返回契约、消费一致性,逐项对照 mock 与真实 API。

选它的理由:

这篇文章我们就用它搭一个「排障助手」应用:用户描述故障 → 诊断链 → 输出排查建议 + 通知运维,mock 与真实 API 双分支,逐项验证契约不被破坏。

4. 整体架构

graph TD start["开始:fault_desc"] extract["参数提取:故障类型/影响范围"] diag["诊断 LLM:多步推理,reasoning_format: separated"] notify["通知 API:mock code 节点 / 真实 http-request 双分支"] param_map["参数映射:to 收件人 / title 标题 / content 正文"] parse["返回解析:{ok, message_id} 统一结构"] check["检索→回答矛盾检测:cd_contract(回答 vs 知识库引用)"] end1["结束:诊断报告 + 通知结果 + 一致性检查结论"] start --> extract --> diag --> notify --> param_map --> parse --> check --> end1

链路很清晰:入口收故障描述 → 参数提取 → 诊断 LLM → 通知 API(mock/真实双分支)→ 矛盾检测 → 输出报告。关键设计是 mock 与真实 API 走同一套参数映射与返回解析——契约差异在切换前就被暴露,而不是上线后爆雷。

5. 模块设计

5.1 契约三件套(本实验核心)

  1. 参数契约:mock 与真实 API 的参数名/类型/必填一致——收件人 to / 标题 title / 正文 content不能 mock 用 to、真实用 recipient,切换后下游全部错位。
  2. 返回契约:mock 返回 {ok: true, message_id} 与真实 API 返回结构一致——下游解析代码不因结构变化重写。
  3. 消费一致性:诊断报告引用的知识库信息 vs 实际检索结果一致(矛盾检测)。

5.2 参数映射(code 节点 json.dumps 组装)

含自由文本的请求体必须用 code 节点组装(104 实测):直接拼 body.data 会被自由文本里的引号破坏 JSON:

def main(to: str, title: str, content: str) -> dict:

    import json

    body = {"to": to, "title": title, "content": content}

    return {"body_data": json.dumps(body, ensure_ascii=False)}

5.3 检索→回答矛盾检测(cd_contract)

知识库说「X 支持 A」,LLM 回答「X 不支持 A」→ 抓消费矛盾。输出 contract_ok 布尔值 + 定位关键字,矛盾时下游可见:

def main(answer: str, kb_refs: str) -> dict:

    # 简化:回答中引用的关键事实必须在检索结果里出现

    conflict = False

    for kw in ["支持", "不支持", "免费", "收费"]:

        if kw in answer and kw not in kb_refs:

            conflict = True

            break

    return {"contract_ok": not conflict,

            "conflict_keyword": kw if conflict else ""}

5.4 mock 表外分支类型一致

mock 数据表外默认分支的数值字段返回 0/None 而非 "未知"(D102-P2-01 教训——"未知" > 0 崩溃):

def main(mock_key: str) -> dict:

    table = {"a": {"ok": True, "message_id": "mock-81512"}}

    row = table.get(mock_key, {"ok": False, "message_id": None})  # 数值字段不返回字符串

    return row

6. 运行验证

用例 输入要点 预期 结果
mock 模式 fault_desc=API 报错,通知走 mock 通知成功(mock-81512)[mock],报告含诊断/通知/一致性检查三段 通过(实测)
real 模式 同输入,通知走 KV /echo 模拟真实 API 通知成功(real-054418)[真实API],回显 to/title/content 正确 通过(实测)
契约对比 mock vs real 两分支输出 返回结构一致 {ok, message_id},下游 cd_report 同一解析逻辑未改 通过(实测,参数契约 + 返回契约成立)
矛盾检测 知识库说「X 支持 A」,LLM 答「X 不支持 A」 contract_ok=false + 定位关键字 通过(实测,正常时输出「未发现矛盾」)
参数错位用例 模拟传参错位(收件人=正文) 参数映射错误被暴露 通过(实测,回显核对 to 字段)

7. 实战坑

现象 修复
mock 表外类型不一致 表外返回 "未知"(str)→ 类型比较崩溃 mock 表外数值字段返回 0/None,不返回字符串(实测,102-06)
code 输出类型不匹配 声明 string 返回 list → workflow failed 输出类型声明 = return 值类型,数组 json.dumps(实测,104-01)
body 内嵌 JSON 引号陷阱 自由文本直接拼 body.data → JSON 破坏 400 请求体用 code 节点 json.dumps 组装(实测,104)
检索回答矛盾 模型无视工具结果瞎编(消费一致性缺陷,P2) cd_contract 矛盾检测 + contract_ok 布尔输出(实验文档设计约束)

8. 实验文档及源码获取

文章聚焦核心配置与采坑点;实验的完整分步操作(节点搭建/参数表/调试指引)见实验文档原文。

联系我

15088711270

手机端点击号码可直接拨打 · 桌面端可复制

微信二维码

扫码加微信 · 备注「门户」更快通过