feat: add workflow toolkit and runtime support

This commit is contained in:
2026-07-06 10:45:19 +08:00
parent efc6fa7067
commit ed7f89d44f
44 changed files with 3770 additions and 127 deletions
+325
View File
@@ -0,0 +1,325 @@
# TOOLKIT WORKFLOW — YAML 驱动的 HTTP 工具链引擎
## 概述
TOOLKIT WORKFLOW 让你通过编写 YAML 文件来定义 LLM 工具。一个 YAML 文件描述一个工具:它接受哪些参数、调用哪些 HTTP API、如何从响应中提取数据、以及最终返回什么给 LLM。
核心能力:
- 一个 YAML 文件 = 一个完整的 LLM 工具,无需写代码
- 支持多步 HTTP 请求链,步骤之间可以传递变量
- 流程控制:`next` 跳转步骤、`jump` 降级到备源、`abort` 终止执行
- 键级缓存:`cache` 缓存变量,同一调用中缺键自动重请求
- 统一提取:`extract` 字段,语法自描述引擎(路径 / 正则)
---
## YAML 顶层结构
一个工具 YAML 文件包含以下顶层字段:
| 字段 | 必填 | 类型 | 说明 |
|------|------|------|------|
| `name` | 是 | string | 工具名,对应 LLM function.name |
| `desc` | 是 | string | 工具描述,对应 LLM function.description |
| `params` | 否 | array | LLM 可见的参数定义,映射为 function.parameters JSON Schema |
| `env` | 否 | array[string] | 该工具依赖的环境变量名列表,仅作文档和校验 |
| `steps` | 是 | array | 请求步骤链,按顺序执行 |
---
## `params` — 参数定义
定义 LLM 可以传入的参数,映射为 function calling 的 parameters schema。
| 字段 | 必填 | 类型 | 说明 |
|------|------|------|------|
| `name` | 是 | string | 参数名,在模板中通过 `${param:key}` 引用 |
| `type` | 是 | string | 取值:`string` / `number` / `integer` / `boolean` |
| `required` | 是 | boolean | 是否必填 |
| `description` | 是 | string | 参数说明,LLM 据此理解参数含义 |
| `default` | 否 | any | 默认值,LLM 未传时自动使用 |
---
## `steps` — 请求步骤
每个步骤定义一个 HTTP 请求及其后续处理。
### 步骤字段
| 字段 | 必填 | 类型 | 默认值 | 说明 |
|------|------|------|--------|------|
| `url` | 是 | string | — | 请求 URL,支持模板语法 |
| `method` | 否 | string | `GET` | HTTP 方法:`GET` / `POST` / `PUT` / `DELETE` / `PATCH` |
| `headers` | 否 | map | — | 请求头键值对,值支持模板语法 |
| `query` | 否 | array | — | URL 查询参数,格式为 `[{key, value}]` 元组列表,值支持模板语法 |
| `body` | 否 | string | — | 请求体,支持模板语法 |
| `timeout` | 否 | integer | 30 | 超时时间(秒) |
| `cache` | 否 | boolean | false | 是否对该步骤启用键级缓存 |
| `next` | 否 | integer | — | 步骤成功后跳转到第几个步骤(0-based),不设则顺序执行下一步 |
| `var` | 否 | array | — | 变量提取列表 |
| `result` | 否 | string | — | 最终返回模板,有此字段则该步为终步 |
### 步骤示例
```yaml
steps:
- url: ${env:BASE_URL}/auth/token
method: POST
headers:
Content-Type: application/json
body: |
{"client_id": "${env:CLIENT_ID}", "client_secret": "${env:CLIENT_SECRET}"}
timeout: 15
cache: true
var:
- name: token
extract: access_token
abort: true
next: 1
- url: ${env:BASE_URL}/orders/${param:order_id}
method: GET
headers:
Authorization: Bearer ${step0:token}
var:
- name: status
extract: data.status
- name: amount
extract: data.amount
result: |
订单号: ${param:order_id}
状态: ${step1:status}
金额: ${step1:amount}
```
---
## `var` — 变量提取
每个步骤执行完 HTTP 请求后,按顺序处理 `var` 列表中的条目,从响应中提取命名变量。提取的变量通过 `${stepN:var_name}` 在后续步骤中引用。
### `extract` — 提取表达式
一个字段覆盖所有提取场景。值的语法决定使用的引擎:
| 语法 | 引擎 | 说明 |
|------|------|------|
| 不以 `/` 开头 | 路径引擎 | 对 JSON/YAML 响应执行结构化路径导航 |
| 以 `/` 开头且以 `/` 结尾 | 正则引擎 | 对原始响应文本执行正则匹配,取捕获组 1 |
**路径引擎**:自动检测响应格式(先 JSON,失败则 YAML),然后按路径导航取值。
| 语法 | 含义 | 示例 |
|------|------|------|
| `key` | 访问对象字段 | `data.status` |
| `key[N]` | 访问数组第 N 个元素 | `items[0].name` |
| `key[]` | 遍历数组所有元素 | `books[]` |
| `key[].field` | 遍历数组,对每个元素取字段 | `books[].title` |
> 键名建议仅使用 `[a-zA-Z0-9_]`,避免含 `.` `[` `]` 的特殊键名。
**正则引擎**`/pattern/` 形式,对原始响应文本执行正则匹配,取第一个捕获组。
```yaml
# 路径:从 JSON 响应中提取
var:
- name: token
extract: auth.access_token
# 正则:从任意文本中提取
var:
- name: session
extract: /session[= ]([a-f0-9]+)/
```
### 提取失败处理(每个条目最多选一种)
| 配置 | 提取成功 | 提取失败 |
|------|---------|---------|
| 不设任何失败处理 | 存入变量 | 存入空字符串 `""`,继续执行 |
| `default: "值"` | 存入变量 | 存入默认值,继续执行 |
| `abort: true` | 存入变量 | 终止整个工具,返回错误给 LLM |
| `jump: N` | 存入变量 | 跳转到第 N 个步骤执行,N 从 0 开始 |
### var 条目完整字段
| 字段 | 必填 | 类型 | 默认值 | 说明 |
|------|------|------|--------|------|
| `name` | 是 | string | — | 变量名,通过 `${stepN:name}` 引用 |
| `extract` | 否 | string | — | 提取表达式,语法自描述引擎(路径 / 正则) |
| `default` | 否 | string | — | 提取失败时的默认值 |
| `abort` | 否 | boolean | false | 提取失败时是否终止工具 |
| `jump` | 否 | integer | — | 提取失败时跳转到第几个步骤(0-based) |
---
## 流程控制
三种流程控制机制,覆盖顺序执行、条件跳转和异常终止。
### `next` — 成功后跳转
- 位置:步骤级别
- 触发条件:步骤所有 var 都没有因 abort/jump 而失败
- 效果:跳转到指定步骤号(0-based),不设则顺序执行下一步
- 用途:跳过某些步骤、实现条件分支
### `jump` — 提取失败时降级
- 位置:var 条目级别
- 触发条件:该条目的提取失败且未设置 default
- 效果:跳转到指定步骤号(0-based),当前步骤剩余 var 不再执行
- 用途:主数据源失败时自动切换到备数据源
### `abort` — 提取失败时终止
- 位置:var 条目级别
- 触发条件:该条目的提取失败
- 效果:终止整个工具执行,返回错误信息给 LLM
- 用途:关键变量(如认证 token)获取失败时立即报错
---
## `cache` — 键级缓存
当步骤设置 `cache: true` 时,该步骤提取的所有变量会被缓存。
缓存行为:
- **命中**:缓存中存在该步骤所有 var.name → 跳过 HTTP 请求,直接返回缓存值
- **未命中**:任意一个 var.name 缺失 → 发起 HTTP 请求,提取所有变量,全部存入缓存
- **生命周期**:单次工具调用期间(内存级),工具返回后释放
- **缓存键**:由步骤的 `url + method + headers + body` 模板展开后的哈希确定
典型用法:第一步获取认证 token 并缓存,后续多步复用 token 时不会重复请求认证接口。
---
## 模板语法
以下字段支持模板语法:`url``headers` 的值、`body``result`
### 三种引用
| 语法 | 来源 | 示例 |
|------|------|------|
| `${param:key}` | LLM 传入的工具参数 | `${param:city}``${param:order_id}` |
| `${env:VAR}` | 系统环境变量 | `${env:API_KEY}``${env:BASE_URL}` |
| `${stepN:var_name}` | 第 N 个步骤提取的变量,N 从 0 开始 | `${step0:token}``${step1:status}` |
### 约束
1. 禁止嵌套引用:`${step0:${env:KEY}}` 不允许
2. 禁止原始步骤引用:`${step:0}` 不允许,必须先通过 var 提取为命名变量
---
## `result` — 最终返回
步骤的 `result` 字段定义该工具最终返回给 LLM 的内容。
- 如果某步设置了 `result`,该步即为**终步**,执行后不再继续后续步骤
- 如果所有步骤都未设置 `result`,默认返回最后一步的原始响应体
- `result` 支持模板语法,可以拼接多个变量和文字
```yaml
# 返回单个变量
result: ${step1:status}
# 多行模板拼接
result: |
订单号: ${param:order_id}
状态: ${step1:status}
金额: ${step1:amount}
```
---
## 执行流程
```
1. 加载 YAML → 解析为 YamlToolDef
2. 解析 LLM 参数 → params HashMap
3. 加载 env 声明的环境变量 → env HashMap
4. 初始化 cache = {}
5. step_idx = 0
while step_idx < steps.len():
┌─ 当前步骤执行 ──────────────────────────────┐
│ │
│ [cache: true?] │
│ ├─ 缓存命中(所有 var 键都存在)→ 跳过HTTP │
│ └─ 缓存未命中 → 继续 │
│ │
│ 展开 url/headers/body 中的模板 │
│ ${param:key} → params["key"] │
│ ${env:VAR} → env["VAR"] │
│ ${stepN:var} → ctx.vars[(N, var)] │
│ │
│ HttpRequest 执行 → HttpResponse │
│ 保存到 ctx.steps[step_num] │
│ │
│ for each var: │
│ extract 语法分发: │
│ ├─ /.../ → 正则引擎,取捕获组1 │
│ └─ 其他 → 路径引擎(JSON→YAML 自动检测) │
│ ├─ 成功 → 存入 ctx.vars + step_vars │
│ └─ 失败 → │
│ ├─ abort:true → 终止,返回错误 │
│ ├─ default:x → 使用默认值 │
│ ├─ jump:N → step_idx = N, break │
│ └─ (无) → 空字符串 │
│ │
│ [cache: true?] → 将 step_vars 存入 cache │
│ │
│ [result 存在?] │
│ └─ 展开 result 模板 → 返回给 LLM → 结束 │
│ │
│ [next 存在?] │
│ ├─ next:N → step_idx = N │
│ └─ (无) → step_idx += 1 │
│ │
└──────────────────────────────────────────────┘
6. 循环结束 → 返回最后一步原始响应体
```
---
## 错误处理
| 场景 | 处理方式 |
|------|---------|
| YAML 文件格式错误 | 启动时打印警告,跳过该文件 |
| 模板引用了未定义的参数 | 替换为空字符串,记录警告日志 |
| 环境变量未设置 | 替换为空字符串,记录警告日志 |
| HTTP 请求失败(连接/超时) | 返回错误详情给 LLM |
| `var.abort: true` 且提取失败 | 终止,返回 "变量 xxx 提取失败" |
| `var.jump: N` 且提取失败 | 跳转到第 N 个步骤,N 从 0 开始,不报错 |
| `var.default` 且提取失败 | 使用默认值,不报错 |
| 路径语法错误(空段、非法索引) | 返回错误给 LLM,附带路径表达式 |
| 路径键不存在 / 索引越界 | 提取失败,触发 default/abort/jump |
| 正则语法错误 | 返回错误给 LLM,附带正则表达式 |
| 正则无匹配 | 提取失败,触发 default/abort/jump |
| 响应体非 JSON 也非 YAML(路径引擎) | 返回错误给 LLM |
| `next` / `jump` 目标步骤不存在 | 终止,返回配置错误 |
---
## 扩展预留
以下字段预留供后续版本实现:
| 字段 | 位置 | 用途 |
|------|------|------|
| `retry` | `steps[]` | 失败重试次数与退避策略 |
| `cache.ttl` | `steps[]` | 缓存有效期(秒) |
| `validate` | `params[]` | 参数级正则校验 |
| `sensitive` | `env[]` | 标记敏感环境变量,日志脱敏 |
| `loop` | `steps[]` | 条件循环(分页自动翻页) |
| `export` | `steps[]` | 将变量导出为环境变量供子进程使用 |
| `pre_request` | 顶层 | 工具级别的前置请求(全局认证) |