首页
/ Dify Agent V2 E2E 测试体系:标签语义、Seed 契约与 Runtime 边界的设计解析

Dify Agent V2 E2E 测试体系:标签语义、Seed 契约与 Runtime 边界的设计解析

2026-09-06 14:50:32作者:伍希望

本文以 e2e/features/agent-v2/ 目录下的 AGENTS.md 为核心,系统讲解 Dify 为新一代 Agent Builder(Agent v2)构建的 Cucumber + Playwright 端到端测试约定:包括 @core@prepared@external-model 等执行标签的严格语义、world.agentBuilder 状态模型、API 前置数据与浏览器行为的职责边界,以及依赖 dify-agent 独立服务与 shellctl 沙箱的 runtime 契约。读完本文,你可以理解 Dify 如何在 CI 中稳定、可复现地验证 Agent v2 的配置持久化、发布与 Access Point 等用户可观测行为,并掌握 post-merge 种子化流水线 E2E_START_AGENT_BACKEND=1 pnpm -C e2e e2e:post-merge 的运行方式。

1. 文档定位:Agent v2 场景的作用域约定

e2e/features/agent-v2/AGENTS.md 是 Dify 仓库中一个典型的“特性级 Agent 约定文件”。它声明了自己的管辖边界:该文件只负责 features/agent-v2/ 目录下的场景(.feature 文件)及其步骤定义(step definitions);而包级的 runner、locator、断言、生命周期与清理规则统一放在 e2e/AGENTS.md 中。这种“包级约定 + 特性级约定”的分层,与目录结构一一对应:e2e/features/agent-v2/ 下同时存放了 configure-persistence.featureaccess-point.featurepublish.feature 等场景文件,以及 support/ 子目录中的 fixture 解析、seed 任务和 API 助手(见 e2e/features/agent-v2/support/seed.ts)。

文档开宗明义给出两条总纲:

  1. 能力标签与环境开关:Agent v2 场景统一使用 @agent-v2 能力标签,E2E Web 环境通过环境变量 NEXT_PUBLIC_ENABLE_AGENT_V2=true 启用 Agent v2 功能。
  2. 覆盖范围与取舍原则:场景必须覆盖用户可观测的 Configure、Build draft、已保存配置、发布、Access Point、文件、高级设置与运行时行为;不允许保留“仅为检查就绪状态(readiness-only)”、“功能不可用”或“永久跳过”的场景。前置条件一律用 API 准备(API setup),断言目标必须是可见行为或持久化的公开契约。

从仓库实际场景可以看到这一原则的落地。以 configure-persistence.feature 为例,“选择兼容模型后刷新仍持久化”的场景中,Given 部分用 API 创建测试 Agent(And an Agent v2 test agent has been created via API),When 部分是纯浏览器动作(打开配置页、选择模型、填写 prompt),Then 部分同时断言界面上可见的保存状态和草稿中持久化的模型/提示词——这正是“API 准备前置、浏览器验证行为”的分工。

2. 执行标签:一套精确划分 CI 通道的语义系统

Agent v2 约定文档最核心的贡献,是把“场景需要什么外部依赖”编码为一组执行标签(execution tags)。这些标签不是装饰,而是直接决定场景会被哪条 runner 命令选中:

标签 语义
@core 稳定、非运行时(non-runtime)的用户行为,构成确定性 PR 核心集
@prepared 确定性用户行为,但依赖入库(checked-in)的 post-merge seed 产物
@external-model 执行过程中会真实调用模型提供方
@external-tool 执行过程中会真实调用第三方工具提供方
@agent-backend-runtime 执行依赖独立的 dify-agent 服务器和 shellctl 沙箱
@web-app-runtime 已发布 Web 应用的聊天行为
@service-api-runtime 后端 Service API 的聊天行为
@microphone 使用伪造音频 fixture 的隔离 Chromium 上下文

文档特别强调了两条判别规则,这是保证 CI 确定性(determinism)的关键:

  • fixture 标签不改变执行方式@stable-model@tool-fixture@knowledge-fixture@full-config-agent@tool-states-agent@dual-retrieval-fixture@workflow-reference 等标签只描述场景消费了哪个具体的种子资源,本身不改变执行;
  • 外部标签只用于运行时调用:只有真正在运行时发起外部调用的场景才标 @external-model / @external-tool;“仅仅选择或持久化一个已激活的模型/工具”的场景应标 @prepared,而不是 external。

这条规则在 publish.feature 中有清晰体现:Publish a configured Agent v2 draft 场景标了 @core @prepared @stable-model——它选择并发布了稳定模型,但没有真实推理调用;而同文件的 Published Agent v2 answers through Web app 则标了 @web-app-runtime @external-model @agent-backend-runtime @published-web-app @stable-model,因为它真正打开了 Web 应用发消息并期待模型回答。e2e/AGENTS.md 也印证了这一点:确定性命令排除 @external-model/@external-tool 标签,external 命令是显式 opt-in 的。

3. 步骤组织与 World 状态模型

3.1 按产品能力分组,而不是按文件机械配对

文档要求步骤定义按 Agent 产品能力分组(configuration、Build draft、resource configuration、lifecycle、Access Point、runtime behavior),并遵循两个硬性规则:

  • 按“拥有该措辞的领域动作”分组,而不是为每个 feature 文件机械地配一个 step 文件;
  • Cucumber 步骤定义是全局注册的,因此不得跨文件重复同一段步骤文本。

对应到源码,e2e/features/step-definitions/agent-v2/ 目录正是这样组织的:configure.steps.tspublish.steps.tsaccess-point.steps.tstools.steps.tsknowledge.steps.ts 等按能力切分;跨能力复用的资源型步骤集中在 fixtures.steps.ts。以 fixtures.steps.ts 为例,其中 the Agent Builder stable chat model is availablethe Agent v2 runtime backend is available 等步骤各自调用 requireAgentBuilderStableChatModelrequireAgentBackendRuntime 等解析函数,验证通过后才把解析结果写入 World——这正是文档所说“fixture 解析步骤与行为步骤分离,因为它们验证的是环境就绪而非执行用户旅程”。

3.2 world.agentBuilder:Agent v2 状态的单一归属

文档规定 Agent v2 的所有场景状态必须挂在 world.agentBuilder 命名空间下,并禁止向 DifyWorld 顶层新增 Agent v2 字段。world.ts 中的实现与约定完全一致:

  • fixtures:存放解析出的模型与种子资源,如 stableModelagentDecisionModelspeechToTextModelpreseededResources(见 world.ts 第 45-51 行createAgentBuilderWorldState);
  • accessPointconfigurespeechToTextworkflow:分别存放各自场景的运行时状态(打开的页面、快照、请求记录等)。

同时文档要求:创建出的 Agent ID、配置资产、工具凭据必须存入 DifyWorld已有的类型化清理字段。这一点同样有源码佐证:DifyWorld 上定义了 createdAgentIdscreatedAgentConfigFilescreatedAgentConfigSkillscreatedBuiltinToolCredentials 等数组字段(world.ts 第 89-94 行),并在 resetScenarioState() 中随场景重置——配合 e2e/AGENTS.md 中“已知资源类型走类型化清理字段、其余走 registerCleanup(...) 回调(LIFO 执行)”的清理契约,实现了场景级资源的确定性回收。

4. Setup 边界:三种 API 前置 Agent 的选用规则

文档给出了三句“setup 步骤”的选用准则,这是 Agent v2 场景编写中最重要的决策点:

  • a basic configured Agent v2 test agent has been created via API:用于无模型依赖、由场景自有的状态(model-free, scenario-owned state);
  • a runnable Agent v2 test agent has been created via API仅在 stable model fixture 解析完成后才能使用;
  • agent-decision 变体:只用于自主规划(autonomous planning)或资源选择行为。

这一“runnable 依赖 stable model fixture”的顺序约束,在 configure-persistence.feature 中体现得非常严格:Persisted Agent v2 instructions remain visible after refresh 场景先执行 And the Agent Builder stable chat model is available(fixture 解析),然后才执行 And a runnable Agent v2 test agent has been created via API;而使用 basic 变体的场景则完全没有模型 fixture 前置。

文档还规定了两条 setup 纪律:

  1. API setup 的权限范围:可以创建场景自有的 Agent、workflow、草稿、访问开关和文件,且每个创建的资源都必须注册清理;严禁为了通过场景去修改固定的种子 fixture。
  2. 自动保存断言的统一入口:Configure 的 autosave 必须使用 the Agent v2 configuration should be saved automatically 这个步骤——它等待发布栏(publish bar)上可见的“已保存”状态;不得用网络空闲等待(network-idle waits)或内部 store 断言来替代。这条规则把“用户可观测”的原则贯彻到了断言层:即使后端有专门的保存接口,测试也只信任 UI 上能看见的保存信号。

5. Seed 与 Fixture 契约:缺料即失败,绝不 skipped

这是 Agent v2 约定中最“硬”的部分,也是它与通用 E2E 实践最大的差异点:

  • Seed 任务负责创建或更新环境级(environment-owned)的模型、插件、数据集、Agent 与 workflow;
  • fixtures.steps.ts 在依赖场景执行之前解析并校验这些资源;
  • 缺失、未激活、未索引或漂移(drifted)的 fixture 必须抛出异常并使场景失败,永远不允许返回 skipped
  • @prepared 场景从确定性 PR core 中排除,只在 post-merge 流水线运行;
  • provider 凭据属于 seed/admin 阶段,绝不能出现在 Cucumber 步骤里

仓库中的 seed 实现完整支撑了这套契约。e2e/features/agent-v2/support/seed.ts 通过 createAgentV2SeedTasks(profile) 暴露三个种子配置档案(seed profile):

  • post-merge:外部运行时任务 + 预置 fixture 任务(对应 @prepared 场景);
  • prepared:基础任务 + 预置 fixture 任务;
  • external-runtime:基础任务 + 语音转文字模型任务。

基础任务(agentV2BaseSeedTasks)包括:从插件市场引导 langgenius/openailanggenius/json_processlanggenius/tavily 三个插件(可通过 E2E_MARKETPLACE_PLUGIN_IDS 覆盖)、配置 stable 模型(默认 openai/gpt-5-nano,可用 E2E_STABLE_MODEL_* 环境变量覆盖)、配置 agent-decision 模型(默认 openai/gpt-5.5)、验证 JSON 替换与 Tavily 搜索两个内置工具、以及构建“ready-knowledge”知识库——后者会上传知识文档并轮询 indexing_status 直到全部 completed(最长 3 分钟),完成后还校验索引片段确实包含预期 token,任何一步不满足都返回 blocked(...) 状态使 seed 终止,而非静默跳过。预置 fixture 任务(agentV2PreparedFixtureSeedTasks)则创建 @full-config-agent@tool-states-agent@dual-retrieval-agent@workflow-reference 对应的种子 Agent 与 workflow,与执行标签一节列出的 fixture 标签一一对应。

种子配置档案也说明了文档中那句“具体资源清单与默认值属于 seed profile 与环境配置,而不属于本指引”的含义——AGENTS.md 刻意不硬编码资源明细,避免约定文档与 seed.ts 实现漂移。post-merge 流水线的执行命令为:

E2E_START_AGENT_BACKEND=1 pnpm -C e2e e2e:post-merge

该命令整体拥有 runtime 启动、strict seed、Cucumber 执行与 teardown;strict seed 必须在不产生 blocked 任务的前提下结束。此外文档还要求 fixture 辅助代码按“产品资源或基础设施能力”归属(而非按消费它的 feature 文件归属)、runtime 就绪适配器与 Console 资源 fixture 分离、所有 fixture 状态保存在当前 SeedContext 或场景 DifyWorld 中而不是模块级全局变量——这些约束都能在 e2e/features/agent-v2/support/fixtures/ 的子目录划分(models.tsdatasets.tstools.tsagents.tsagent-backend.ts)中看到对应。

6. Runtime 契约:dify-agent 与 shellctl 的就绪校验

对于带 @agent-backend-runtime 标签的场景,文档要求必须包含 the Agent v2 runtime backend is available 步骤,并给出两种运行方式:

  1. E2E_START_AGENT_BACKEND=1:由 E2E 自行拉起 dify-agent 服务器与 shellctl 沙箱;
  2. E2E_AGENT_BACKEND_URL / AGENT_BACKEND_BASE_URL:指向已存在的 dify-agent 服务。

这条“显式就绪步骤”在 fixtures.steps.ts 中注册,其实现 agent-backend.ts 展示了完整的 URL 解析与探活逻辑,值得作为范例细读:

  • URL 解析优先级getAgentBackendURL):E2E_AGENT_BACKEND_URLAGENT_BACKEND_BASE_URL → 若 E2E_START_AGENT_BACKEND 为真则默认 http://127.0.0.1:5050E2E_AGENT_BACKEND_PORT 可覆盖)→ 否则返回 undefined
  • shellctl URL 解析getShellctlURL):E2E_SHELLCTL_URLDIFY_AGENT_SHELLCTL_ENTRYPOINT → 若本地拉起模式则默认 http://127.0.0.1:5004E2E_SHELLCTL_PORT 可覆盖)→ 否则为 undefined(不探活);
  • 探活端点:对 dify-agent 请求 GET /openapi.json,对 shellctl 请求 GET /healthz
  • 失败即阻断:任一检查失败都调用 failFixturePrerequisite 抛出带 owner: 'e2e/runtime' 和 remediation 提示的错误——例如未配置任何后端时会明确提示“该场景需要独立的 dify-agent run server,而不仅仅是激活的模型提供方”,并给出两条修复路径(E2E_START_AGENT_BACKEND=1 或显式 URL)。

这与 e2e/AGENTS.md 中“Feature 自有服务使用自己的标签;Agent v2 runtime 场景使用 @agent-backend-runtime 并要求显式的 runtime 就绪步骤;E2E_START_AGENT_BACKEND=1 与显式 Agent backend URL 互斥”的全局规则完全咬合。文档还补充了模型选用纪律:通用 runtime 行为使用 stable 模型,只有当“模型推理质量本身是契约的一部分”时才使用 decision 模型;不得把 external runtime 标签扩大到纯配置场景。

7. Build 与 Preview 的验证边界

文档区分了两种模式的验证目标:

  • Build 模式覆盖 Configure 与 Build draft 持久化(即配置态的正确保存与恢复);
  • Preview / Test Run 覆盖真实生成、运行时失败恢复,以及通过回复内容证明工具或知识库真实命中(proven through replies)。

并给出一条工程约束:在所选 CI 通道能够执行之前,不要新增 Preview 场景——避免场景入库后长期无法运行,退化为文档开头所禁止的“readiness-only 场景”。从场景文件命名可以看到 Build 侧已被细粒度拆分为 configure-entryconfigure-persistencebuild-draft(见 e2e/features/agent-v2/build-draft.feature)、filesknowledgeadvanced-settingspublishaccess-pointspeech-to-textoutput-variables 等 feature 文件,每个文件聚焦一个产品能力面。

以 Access Point 为例,access-point.feature 覆盖了完整的访问入口契约:未发布时访问面不可用;Web 应用 URL 的复制/打开/嵌入式配置/定制/设置对话框且不改变编排草稿;Web 应用访问的禁用与恢复(含重发布后仍保持 out-of-service 的回归);workflow 引用在 Access Point 中的展示与跳转 Studio;后端 Service API endpoint 的复制、API key 的“一次可见”安全展示、API Reference 打开方式、API 访问的禁用/恢复;最后是两条真正的 runtime 场景(@service-api-runtime @external-model @agent-backend-runtime),验证发布后的 Agent 能通过后端 API 应答,以及禁用时请求被拒绝、恢复后重新成功。每一条都是“用户可观测行为 + 持久化公开契约”的直接实例。

8. API 契约:直接使用生成的 oRPC 类型

文档最后的 API 契约一节与包级 e2e/AGENTS.md 的“Browser, API, And Contract Boundaries”互为表里,要点如下:

  • Console/Web/Service API 的生成类型必须直接从 @dify/contracts/.../types.gen 导入;本地类型仅允许用于 E2E 自有状态、fixture 注册表条目、helper 入参和有意收窄的视图;
  • 若生成契约不完整,应修复后端 schema 并重新生成,而不是复制响应形状;
  • Agent detail 是 Agent 场景的状态拥有者:Agent 的底层 app identifier 只能用于路由共享的 app 命令,不能替代查询模型,更不能成为最终断言来源;Agent 的 Web 应用 URL 与持久化状态必须从生成的 Agent detail 契约推导,最终断言落在浏览器中可见的 Access Point 或 runtime 结果上。

seed.ts 顶部的导入可以看到这一契约的落地:AgentSoulConfigAgentKnowledgeDatasetConfigKnowledgeConfigModelType 等类型全部来自 @dify/contracts/api/.../types.gen,seed 中构建知识库文档请求体时还使用了 satisfies KnowledgeConfig 做类型收窄——生成契约既约束了 API 调用形状,也让 seed 行为本身可被类型检查。

9. 总结:一套面向可复现性的 Agent E2E 工程范式

回到 e2e/features/agent-v2/AGENTS.md 本身,它示范的是一套可迁移的 E2E 工程范式:

  1. 标签即 CI 拓扑@core / @prepared / @external-* / @*-runtime 把“场景依赖什么”变成机器可执行的调度依据,确定性 PR 核心集与 post-merge 全量集清晰分界;
  2. API 只做准备,浏览器负责断言:setup 步骤创建场景自有资源并全部注册清理,断言落在可见行为或持久化契约上,自动保存这类交互统一用可见信号(publish bar 保存态)验证;
  3. fixture 失败必须显式失败:seed 的 blocked 状态与 fixture 解析的 failFixturePrerequisite 抛错,杜绝 skipped 掩盖环境漂移,保证“green 一定意味着行为被验证过”;
  4. 状态与类型都有明确归属world.agentBuilder 的命名空间划分、类型化清理字段、@dify/contracts 生成类型的直接使用,共同消除了场景间状态污染与契约漂移。

对于在 Dify 中维护或扩展 Agent v2 场景的开发者,这份约定文件加上 e2e/AGENTS.md 的包级规则、world.ts 的状态模型与 support/seed.ts 的种子实现,构成了从“新增一个场景”到“post-merge 流水线跑通”的完整依据。

登录后查看全文
热门项目推荐
相关项目推荐