Agent Skill 是一种很自然的能力封装方式:用 Markdown 描述任务目标、输入输出、约束和执行步骤,模型读完后就知道应该怎么做。
但当任务从"一次提示词"发展成多步骤流程,问题也随之出现:
- 参数由谁校验?
- 脚本执行顺序由谁决定?
- 一个步骤的输出怎样传给下一步?
- 分支条件来自模型判断,还是结构化状态?
- 中途失败后,已经生成的产物还能不能继续使用?
- 最后怎样还原这个 Agent 到底调用了哪些工具?
如果所有事情都依赖模型在上下文里"记住并正确执行",Skill 虽然容易编写,却很难获得稳定、可检查的执行过程。
我和我朋友最近开源的 FlowScript 就是在尝试解决这个问题:保留 Markdown Skill 的可读性,同时为它增加一套由 Runtime 执行的受控工作流,并且在工作流执行过程中生成 Agent Skill 的执行路径,当出现工作流无法处理的问题,还可以回滚到 Agent Skill 模式尝试修复。
项目目前还是验证设计思路的 MVP,不是生产级编排平台,也不是安全沙箱。但它已经能够跑通一个包含 LLM、参数校验、Python 脚本、结构化分支、artifact 交接和最终报告的完整 Demo。
双模式 Skill:同一份能力,两种执行方式
FlowScript Skill 同时面向两类执行者。
普通 Agent 阅读 SKILL.md,按照自然语言说明完成任务;兼容 FlowScript 的 Runtime 则加载 FLOWSCRIPT.md 中的 DSL,按照明确的节点和分支执行。
一个典型 Skill 包结构如下:
text
my-skill/
├── SKILL.md
├── FLOWSCRIPT.md
├── input_schema.json
├── agents/
│ └── openai.yaml
├── prompts/
├── schemas/
├── scripts/
├── references/
└── tests/
└── replay_cases.jsonl
这里有一个刻意的职责划分:
SKILL.md负责解释能力、输入、输出和约束;FLOWSCRIPT.md负责声明执行顺序、artifact 路径和分支;- JSON Schema 负责约束模型输出;
- Python 脚本负责确定性处理;
- Runtime 负责调度、工具调用和状态记录。
完整 DSL 不会再复制一遍到 SKILL.md。这样既避免两份流程逐渐不一致,也不会让普通 Agent 被大量执行细节淹没。
FlowScript DSL 长什么样
FLOWSCRIPT.md 仍然是普通 Markdown,只是在其中包含一个 fenced flow 代码块。
下面是一个精简后的参数校验节点:
flow
version: 1
mode: controlled
inputs:
schema: input_schema.json
steps:
- id: validate_params
type: validator
command:
executable: python
args:
- scripts/validate.py
- --input
- ${input.params}
- --output
- ${output.status}
- --normalized-output
- ${output.params}
accepted_exit_codes: [0, 1, 2]
status_source: ${output.status}
timeout_seconds: 30
output:
status: runs/{run_id}/artifacts/validation_status.json
params: runs/{run_id}/valid_params.json
branches:
valid:
when: ${validate_params.output.status.state} == "valid"
next: generate_and_profile
needs_input:
when: ${validate_params.output.status.state} == "needs_input"
next: request_clarification
invalid:
when: ${validate_params.output.status.state} == "invalid"
next: fallback_skill_mode
这里最重要的不是 YAML 语法,而是执行权的归属。
模型不会生成一段 shell 命令,也不能决定下一步去哪。Runtime 根据 DSL 构造 argv,执行已经声明的 Python 脚本,然后读取状态 JSON 中的 state 字段选择分支。
当前 MVP 的分支表达式故意只支持显式的 == 和 !=。能力不算复杂,但行为比较容易预测和检查。
LLM 负责内容,不负责流程控制
FlowScript 不是要消除 LLM,而是缩小 LLM 在流程中的职责范围。
以 Demo 中的请求解析节点为例:
- Runtime 读取对应语言的 prompt;
- Runtime 读取
input_schema.json; - 模型调用专用的
submit_skill_inputs工具; - Runtime 校验工具参数是否符合 Schema;
- Runtime 将参数写入声明好的
resolved_params.json; - 后续校验节点再决定是否可以继续。
LLM 内容节点也采用同样的模式。模型提交 summary、risks、recommendations 和 cluster_insights,但模型不能自行选择保存路径,也不能绕过结构校验直接生成最终报告。
这种方式的核心原则可以概括为:
模型提交内容,Runtime 控制持久化;模型参与理解,Runtime 控制执行。
Runtime 如何执行工作流
当前 Runtime 的执行过程可以简化为:
text
用户请求或结构化 JSON
│
▼
加载 FLOWSCRIPT.md
│
▼
校验节点、引用、路径和 Schema
│
▼
┌────── 当前节点 ──────┐
│ │
│ LLM 节点 │ Script / Validator 节点
│ 读取 prompt + schema │ 构造声明好的 Python argv
│ 获取结构化工具参数 │ 执行并捕获退出码
│ 写入 JSON artifact │ 读取状态 artifact
│ │
└──────────┬───────────┘
▼
选择 next / branch
│
▼
读取最终主要产物
│
▼
写入 context、trace 和 result
WorkflowEngine 是唯一能够决定执行顺序的组件。模型适配层在代码中叫 FakeAgent,但它并不是另一个会自主规划的 Agent,而是负责把模型输出规范化为工具参数,并记录为可检查的工具交互。
为什么重点设计 skill_agent_context.json
普通执行日志适合排查 Runtime,但并不一定适合回答另一个问题:
如果把这次运行看成一个 Skill Agent 会话,这个 Agent 对外表现为做了什么?
因此 FlowScript 每次运行都会生成 skill_agent_context.json。它的顶层不是包含大量内部状态的对象,而是一组 OpenAI 风格的消息:
json
[
{
"role": "user",
"content": "为 Alice 和 Bob 生成模拟数据,并按员工分组。"
},
{
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_0001",
"type": "function",
"function": {
"name": "read_file",
"arguments": "{\"path\":\"prompts/parse_request_cn.md\",\"max_bytes\":64000}"
}
}
]
},
{
"role": "tool",
"tool_call_id": "call_0001",
"name": "read_file",
"content": "{\"status\":\"success\",\"path\":\"prompts/parse_request_cn.md\"}"
}
]
它会依次记录:
- 用户原始请求;
- prompt 和 Schema 的读取;
- 模型提交的结构化输入;
write_json持久化;- Python
exec调用及结果; - 状态和 artifact 的读取;
- LLM 解读提交;
- 最终报告读取;
- 最终 Assistant 回复。
其中 function.arguments 和工具消息的 content 都是 JSON 编码后的字符串。消费者如果需要结构化字段,需要先解析外层消息,再解析内层 JSON 字符串。
这样设计的好处是,上层 Agent、调试器或回放工具不需要理解 WorkflowEngine 的全部内部事件,就可以读取一条接近真实 Agent 工具调用链的上下文。当发生问题无法继续用Workflow处理时,可以切换到skill Agent下继续运行
context、trace 和 result 分别解决什么问题
一次执行会生成三种 Runtime 产物:
text
runs/{run_id}/runtime/
├── skill_agent_context.json
├── trace.jsonl
└── result.json
它们面向不同的使用场景。
skill_agent_context.json
面向 Agent 风格的回放、审计和下游上下文。只保留用户、Assistant 和工具之间的可观察交互。
trace.jsonl
面向 Runtime 调试和测试。每一行是一个事件,包含:
json
{
"seq": 12,
"timestamp": 1783512626.49,
"run_id": "demo-001",
"step_id": "validate_params",
"actor": "runtime",
"event": "branch_selected",
"payload": {
"branch": "valid",
"next": "generate_and_profile"
}
}
节点开始、模型请求、artifact 写入、分支选择和节点失败等内部细节都放在这里。
result.json
面向快速判断执行结果。它汇总运行状态、已完成步骤、所选分支、主要产物、工具调用次数、模型调用次数和耗时。
即使执行失败,Runtime 也会尽量写入失败结果、刷新上下文,并保留已经生成的 trace,避免只得到一个退出码。
大文件不会直接塞进上下文
完整记录工具调用不等于把所有文件原样复制进 JSON。
当前 Runtime 对读取结果做了简单限制:
- 小于 64,000 字节的文本或 JSON 可以完整记录;
- 更大的文件记录路径、字节数、SHA-256 和预览;
- CSV 文件额外记录数据行数;
exec只保留 stdout 和 stderr 的最后 16,000 个字符。
这套策略还比较基础,但至少能避免一个几万行的 CSV 直接撑爆 Agent 上下文。
Demo:模拟 CSV 数据质量报告
仓库包含一套中英文 Demo,用来验证完整工作流,而不只是 DSL 解析。
它会:
- 从用户请求中提取员工、区域和分组维度;
- 校验参数并补全默认时间范围;
- 生成确定性的模拟 CSV;
- 按时间筛选并计算字段画像;
- 按员工或区域生成聚类统计;
- 让模型生成结构化风险解读;
- 校验解读结构;
- 输出最终 Markdown 报告。
一次成功运行的目录结构如下:
text
runs/<run_id>/
├── resolved_params.json
├── valid_params.json
├── data/
│ └── generated_data.csv
├── artifacts/
│ ├── validation_status.json
│ ├── profile.json
│ ├── profile_status.json
│ ├── interpretation.json
│ ├── interpretation_status.json
│ ├── final_report.md
│ ├── finalize_status.json
│ └── artifacts_manifest.json
└── runtime/
├── trace.jsonl
├── skill_agent_context.json
└── result.json
如何运行
项目要求 Python 3.11 或更高版本,以及一个可用于请求的模型接口,比如lmstudio上部署qwen3.5-0.8B。
bash
git clone https://github.com/whale-agent-lab/flowscript.git
cd flowscript
python -m pip install -e minimal_flowscript_agent
使用自然语言请求运行中文 Demo:
bash
minimize-agent run \
--skill skills_cn/csv-quality-report-demo \
--request "为张伟和李娜生成 500 条华东、华南区域的模拟数据,并按员工分组。" \
--language zh \
--model qwen3.5-0.8b \
--endpoint http://127.0.0.1:1234/v1/chat/completions
也可以传入结构化 JSON,跳过第一个请求解析模型调用:
bash
minimize-agent run \
--skill skills_cn/csv-quality-report-demo \
--input-json input.json \
--run-id demo-001 \
--language zh
需要注意,--input-json 只跳过请求解析;后面的画像解读节点仍然需要模型 endpoint。
当前安全边界
FlowScript 试图减少模型对执行过程的自由度,但不能因此把它当成安全沙箱。
目前实现的约束包括:
- 只执行 DSL 中预先声明的 Python 脚本;
- 子进程使用 argv 和
shell=False; - 脚本与 artifact 路径必须位于 Skill 根目录内;
- 工作目录固定为 Skill 根目录;
- 显式声明超时和可接受退出码;
- 模型不能选择命令、保存路径或分支;
- 分支读取结构化状态,而不是模型自由文本。
但被允许执行的 Python 脚本依然继承 Runtime 进程的系统权限。因此运行第三方 Skill 前,仍然必须审查其中的脚本和资源。
MVP 尚未解决的问题
为了先验证最小闭环,当前版本有不少明确限制:
- 不支持暂停、恢复或覆盖已有运行;
- 不支持多轮澄清;
- clarification 和 fallback terminal 目前会记录后停止;
- 不支持循环、并行节点和动态节点;
- 只实现了 JSON Schema 的一个小型子集;
- replay case 已经定义,但还没有完整的自动 replay runner。
后续更值得探索的方向包括 artifact 缓存、人工审批节点、可恢复执行、并行但可重放的调度,以及在执行出错后回滚到 Agent Skill 模式。
这个项目更像一个可以讨论和验证的设计提案,而不是已经完成的框架。