跳到主要内容

chain 型技能 / 多步链(runner.py 口径)

什么时候读:要把几个技能串成一条链(chain 型技能、工作流「执行技能」、定时任务 steps), 或者要写一个会被放进链里的技能。来源:backend/core/chain/runner.py。

能做什么、不能做什么​

做不做(该用工作流)
顺序执行、同层并行、字段引用传值、单步失败策略暂停、等人回话、等外部回调、实例持久化、断点续跑、条件分支、循环

判据:全生命周期不超过一次工具调用墙钟(runner.py 核对 2026-10):

  • 链墙钟 CHAIN_WALL_SEC = 环境变量,未设时 max(30, TOOL_WALL_SEC − 30);runner 里 TOOL_WALL_SEC 未设时按 240 算, 而对话线 roles_chat_helpers 未设时按 1800 算——两边默认值不一致,不设环境变量时链默认 210 秒。
  • 单步墙钟 CHAIN_STEP_WALL_SEC 默认等于链墙钟,作为 policy 传给 pipeline;技能自声明的 wall_sec 优先,但整链 210 秒照样截断。
  • 要让链能跑更久,部署时显式设置 TOOL_WALL_SEC(两处同读一个变量),或单独设 CHAIN_WALL_SEC。

三处编排(chain 型技能 / 工作流 automate / _tool_schedule_task steps)共用这一套实现和引用语法,技能在链里和在对话里走同一条 pipeline.execute_skill,行为一致。

步骤定义​

{"skill_name": "datatable", "alias": "q", "params": {...},
"on_fail": "abort", "default_value": null}
字段规则
alias可空,缺省 step{N}。只能用字母、数字、下划线、中文(^[A-Za-z0-9_\u4e00-\u9fa5]+$)。经 /skills/chain/save 保存时别名重复直接拒绝;runner 对重复别名的自动改名(alias_2)只兜底手工配置
on_failabort(默认,整链终止,同层并发一起收掉)/ skip(results 里没有该 alias,下游引用解析为缺失→该参数不传)/ continue(results 里放 default_value)
default_value仅 continue 用。字符串会尝试 json.loads;不是 dict/list 时包成 {"success": false, "error": ..., "value": dv}
skill_name 为空按失败处理,走 on_fail

保存校验(/skills/chain/save → skills._validate,与前端 ChainSkillsView.validate() 同规则)​

规则不过时
链名 ^[a-z][a-z0-9_]*$(不允许连字符,与普通技能名不同);不能与本角色可见的已安装技能同名拒绝保存
至少一个步骤;description(「什么时候用」)非空;每步选了 skill_name拒绝
参数或 natural_desc 里的每个 {入参} 必须在链级 input_schema.properties 里声明;声明了却没被任何步骤引用的入参也拒绝拒绝
$steps.别名 必须存在且排在当前步之前拒绝(所以经 API 保存的链不会有环)
$steps.别名.顶层字段:被引用技能声明了 output_schema.properties 时,顶层字段名必须在其中拒绝,并列出可选字段
入参名撞保留字段(compat.is_reserved_ctx_key:org_id emp_no db session_id …)拒绝
步骤是另一条链拒绝(链不能套链)
cost_tier ∈ zero low medium high422

保存成功即注册为 skill_type="chain"、trigger=explicit 的技能,模型看到的与普通技能无异;enabled=false 等于对模型隐藏。 /skills/chain/test 真执行(会写库发通知),先校验链级 required 入参,可指定 emp_no「以谁的身份跑」。

引用语法(与前端 utils/stepRefs.js 对齐)​

{字段名} 取查找面:链级入参 / 工作流上下文 / 触发字段
$steps.别名.字段 取任意前序步骤的具名输出;可多级 $steps.q.data.0.name(数组下标写数字)
$steps.别名 该步骤整体输出
$ctx.字段 同 {字段名}
$prev.字段 旧写法,只兼容老配置,勿新增
任意 token 后可跟 ?? 默认值:$steps.a.x ?? 0 (支持 数字 / true / false / null / 引号字符串)
  • 整串就是一个 token → 返回原始类型;token 混在文本里 → 字符串插值(dict/list 序列化成 JSON,None 成空串)。
  • 引用取不到且无默认值 → 该参数整体不传,让技能自己的默认值生效。不会把 <MISSING> 当字符串塞进去。
  • 别名和字段允许中文;不能含 .、空白、{}、$、引号。
  • datatable 一侧的对应约束:filters 值里残留未解析的 {xxx} / $steps. 时拒绝执行(见 datatable.md §5)。所以写链时引用名必须打对,否则不是「传了个怪字符串」而是直接拒绝。

依赖与并行​

按 $steps. 引用图分层:同层互不依赖可并行,层间串行。链内并行度 MAX_CONCURRENT_CHAIN_STEPS(默认 3),独立于外层工具并发。

有环或引用了不存在的别名 → 剩余步骤降级为顺序执行,不抛异常,跑到那步再按 on_fail 处理。配置错误不会让链在启动时炸掉——保存接口已按上节拦住这类配置,只有绕过接口手改库的链才会走到这条降级。

两份 ctx,不要合并​

内容来源
run_ctx传给技能的身份 / 连接以调用方为准
lookup_ctx{字段名} 的查找面run_ctx 铺平 + 外部入参(模型填的 initial)

外部入参只进查找面,不进运行上下文;保留字段(RESERVED_CTX_KEYS,定义在 backend/core/skills/compat.py)会被过滤。这是为了让模型不能通过链入参覆盖 org_id / emp_no / db 等。

写「会被放进链里的技能」要注意​

  • 固定上下文:runner 2026-10 修复前只把 actor_* 传给步骤,raw_input / channel / triggered_at* / attachments 在链里都是空值; 修复后整套传。依赖这些字段的技能在未打补丁的部署里要能处理空值。
  • team_role:链 ctx 里有就沿用(对话线调起的链继承对话的值);没有就由 pipeline 按 user_id 解析(2026-10 补丁),解析不出按 member。
  1. 在 schema.json 里声明 x-output-schema({"type":"object","properties":{…}})。链编辑器按它校验 $steps.别名.字段 的顶层字段名、给出可选字段提示;一旦声明就要写全,漏写的顶层字段会让引用它的链保存失败。

  2. chat_mode 在链里也是 True(见 datatable.md §8)。删除命中 >10 行会等 confirm_token,链里没人能回,该步必然超时。链内删除控制在 ≤10 行。

  3. request 为 None,链不起子 agent。依赖 request 的技能在链里直接失败。

  4. 链内各步共用同一个 session_id(工作区按 role_id/session_id 定位),第 1 步的产物第 2 步能找到。技能写产物仍然只写会话工作区(_workspace(ctx):ctx 里没有 workspace_dir 时按 $AGENT_WORKSPACE_DIR/<role_id>/<session_id> 推,各步推出同一目录)。

  5. 链按 r.ok 判成功,即技能返回的 success / ok。缺省不算失败——所以技能失败必须显式 success: False,否则链把它当成功往下传。

  6. 单步策略以 SkillRunPolicy(CHAIN_POLICY)为准,只覆盖墙钟。要改链内行为(例如 chat_mode)在 policy 上改,不在技能里绕。

  7. 空结果护栏(规范 §9.3):上一步产出空则中止——靠 on_fail + 引用缺失→参数不传实现,不是 runner 内建的判断。需要「空即停」时让下一步技能对缺参返回 need_param,并把该步 on_fail=abort。

返回形状​

{"steps": {alias: output}, "last": output, "failed": [alias...],
"ok": bool, "error": str, "duration_ms": int}

ok = 无失败且未 abort;last 是最后一个有结果的步骤的输出。