mirror of
https://github.com/shareAI-lab/analysis_claude_code.git
synced 2026-09-20 12:13:38 +08:00
Rewrite s16 workflow chapter for clarity and teaching fidelity
Retell the Runtime lesson as a progressive story (two doors, kitchen primitives, null-isolation, longest-prefix resume) in EN/ZH/JA, and align the mini-runtime with Claude Code / Pi semantics so the docs and code agree. Co-authored-by: Xinlu Lai <CrazyBoyM@users.noreply.github.com>
This commit is contained in:
@@ -1,51 +1,57 @@
|
||||
# s16: Workflow Runtime — 模型决定单步,脚本决定编排
|
||||
# s16: Workflow Runtime — 把菜谱写进代码
|
||||
|
||||
[English](README.md) · [中文](README.zh.md) · [日本語](README.ja.md)
|
||||
|
||||
s01 → ... → s14 → [s15](../s15_integrated_harness/) → `s16` → [s17](../s17_goal_loop/)
|
||||
|
||||
> *"一次 tool_use,跑完一整套编排"* — `Workflow` 工具启动一个可恢复的脚本运行时,协调多次 agent 调用。
|
||||
> *“一轮轮聊天,像每隔十秒给厨师发一条短信。Workflow 是厨房能照着做的菜谱。”*
|
||||
>
|
||||
> **Harness 层**: 编排 — 在单 agent 循环之上,执行保存好的多 agent 脚本。
|
||||
> **Harness 层**: 编排 — 在单 agent 循环之上,跑一套多 agent 脚本。
|
||||
|
||||
---
|
||||
|
||||
从 s01 到 s15,每一轮都由模型决定调用哪些工具。工具结果进入 `messages[]` 后,模型再根据更新后的上下文决定下一步。当后续路径取决于上一步发现了什么时,这种方式很合适。
|
||||
想象你和朋友用微信一起做饭。你发“先切洋葱”,等他回,再问“切好了吗?”,然后“热锅……”。一道菜还行;要办二十桌宴席,聊天就成了瓶颈:步骤记丢、反复叮嘱,手机一死还得从头来。
|
||||
|
||||
有些任务会重复一套固定流程。例如代码审查可以同时检查多个维度,再逐条验证发现、合并重复项并按严重程度排序。执行前已经知道步骤及其先后关系,这时宿主需要三样东西:
|
||||
普通“模型当总指挥”的对话就是这样。**Workflow** 是写好的菜谱:厨房(runtime)按谱做,帮手(子 agent)负责判断,中间结果放在台面上的碗里 —— 不塞进群聊记录。
|
||||
|
||||
- **并行**,别一个一个串着等;
|
||||
- **稳定的结果结构**,即使每个 agent 的回答会变化;
|
||||
- **可恢复**,跑到一半断了,已经做完的部分别从头再来。
|
||||
## 问题在哪
|
||||
|
||||
如果这套编排只存在于对话历史里,步骤顺序和检查点也只存在于历史里。保存好的 workflow 把固定流程写进代码,并在 journal 中记录已经完成的调用。
|
||||
从 s01 到 s15,每一轮都由模型决定下一步调用什么工具。当“下一步取决于刚才发现了什么”时,这很合适。
|
||||
|
||||
## 计划写在代码里,不是靠聊天一轮轮凑
|
||||
有些任务的形状事先就知道:
|
||||
|
||||
在 harness 的工具池里加入一个 `Workflow` 工具。宿主注册由 `agent() / parallel() / pipeline() / phase()` 组成的可信脚本。模型只提供保存好的 workflow 名称、参数和可选的续跑 run ID,不会提交可执行代码或元数据。
|
||||
- 按多个维度审查很多文件
|
||||
- 先调研,再验证,再合并
|
||||
- 用同一种方式迁移 N 个模块
|
||||
|
||||
workflow 以一次 `tool_use` 进入主循环。脚本运行时,runtime 会发出生命周期和进度事件,并把每一步写进磁盘上的 journal。脚本结束后,这次调用返回启动信息、结果和任务状态。脚本里的中间结果存在变量里,不会塞进对话历史。下次用 `resume_from_run_id` 重启时,没改过的 `agent()` 会直接使用 journal 中的结果。
|
||||
如果模型只能把计划“记”在 `messages[]` 里,会发生三件事:编排噪音占满上下文、中途计划漂移、崩了就得把做完的活重做一遍。
|
||||
|
||||
你需要并行、稳定的结果形状,以及能续跑。把这三样只寄存在对话历史里,太脆弱。
|
||||
|
||||
## 一句话说清想法
|
||||
|
||||
**把计划写进代码。** 子 agent 仍然负责判断;脚本负责循环、分发和合并。中间结果存在变量里,不进对话。
|
||||
|
||||

|
||||
|
||||
```python
|
||||
SAMPLE_META = {"name": "review-changes", "description": "审查代码改动", "phases": ["Review", "Verify"]}
|
||||
一次 `Workflow` 工具调用启动这次脚本运行。运行中会发出生命周期和进度事件;最后一条工具结果带回启动信息、结果和任务状态。
|
||||
|
||||
async def sample_workflow(ctx, args):
|
||||
ctx.phase("Review")
|
||||
results = await ctx.pipeline(DIMENSIONS, audit, verify) # 每个维度独立走 审计 → 验证
|
||||
confirmed = [f for r in results if r for f in r["confirmed"]]
|
||||
ctx.log(f"确认了 {len(confirmed)} 个真实问题")
|
||||
return {"confirmed": confirmed}
|
||||
```
|
||||
## 两扇门
|
||||
|
||||
## Workflow 工具:一次调用,完成整次运行
|
||||
Claude Code 对“工作流怎么启动”是诚实的:
|
||||
|
||||
`Workflow` 会加入 s15 宿主已有的工具池。用户可以要求运行一个保存好的 workflow,模型也可以在任务匹配已知编排时选择这个工具。适配器会用名称查询宿主管理的 `WORKFLOWS` registry,再把可信的元数据和函数交给运行时;s15 的其他工具仍在同一个循环里可用。
|
||||
| 门 | 你传什么 | 什么时候用 |
|
||||
|----|----------|------------|
|
||||
| **动态(Dynamic)** | 一段编排用的 JavaScript(`script`,或之后的 `scriptPath`) | 模型为**这次任务**现写菜谱 |
|
||||
| **已保存(Saved)** | `name` + `args` | 好用的菜谱放进例如 `.claude/workflows/`,按名字再跑 |
|
||||
|
||||
模型可见的 schema 只接受 `name`、`args` 和 `resume_from_run_id`。名称未知或参数格式错误时,适配器会返回错误工具结果,不会让宿主循环退出。随后运行时校验已经注册的元数据、经过权限检查、注册本地 workflow 任务,并在执行脚本前发出 `async_launched`。进度事件和最终的 `task_notification` 随后到达;调用返回可写入 JSON 的启动信息、结果和任务状态。
|
||||
同一间厨房。动态是“现在写菜谱”,已保存是“从卡片盒里抽一张”。
|
||||
|
||||
**本课是一个 Python 教学运行时。** 用同样的想法,但每行你都能读懂。演示按名字注册一个已保存的 workflow;概念和 Claude Code 的脚本世界一一对应。我们**不会**再说“模型不能提交可执行代码”——那是对 Claude Code 的误述。这里只是不嵌入完整的 JS 解释器。
|
||||
|
||||
```python
|
||||
# 教学适配器:已保存这扇门(name + args)。
|
||||
# Claude Code 还接受 script / scriptPath / resumeFromRunId。
|
||||
WORKFLOW_TOOL = {
|
||||
"name": "Workflow",
|
||||
"input_schema": {
|
||||
@@ -54,192 +60,148 @@ WORKFLOW_TOOL = {
|
||||
"name": {"type": "string"},
|
||||
"args": {"type": "object"},
|
||||
"resume_from_run_id": {"type": "string"},
|
||||
"resumeFromRunId": {"type": "string"},
|
||||
},
|
||||
"required": ["name"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
}
|
||||
|
||||
async def run_workflow(name, args=None, resume_from_run_id=None):
|
||||
meta, script_fn = WORKFLOWS[name]
|
||||
out = await WorkflowTool().call(
|
||||
meta, script_fn,
|
||||
args=args,
|
||||
resume_from_run_id=resume_from_run_id,
|
||||
)
|
||||
return {"launched": out["launched"], "result": out["result"],
|
||||
"task": serialize_task(out["task"])}
|
||||
```
|
||||
|
||||
## Workflow 元数据:启动前先校验
|
||||
## 原语:用一次义卖来讲
|
||||
|
||||
每个保存好的 workflow 都会注册一份可信元数据,包含 `name`、`description` 和可选的 `phases`。运行时会在执行 workflow 代码前校验它:`name` 和 `description` 用来标识任务,`phases` 给进度显示分组命名。这些字段属于宿主 registry,不是模型输入。
|
||||
学校义卖要烤很多蛋糕。每张桌子都要:搅拌 → 烘烤 → 装箱。帮手负责尝和判断;菜谱决定顺序。
|
||||
|
||||
注册内容不合法时,运行时会在启动前抛出 `WorkflowInputError`。这和 s12 校验 cron 表达式是一个思路:保存好的 workflow 有问题,就不要等到执行时才发现。
|
||||
| 原语 | 在厨房里的意思 |
|
||||
|------|----------------|
|
||||
| `agent(prompt, {schema, label, phase})` | 请一个帮手做一件事 |
|
||||
| `pipeline(items, *stages)` | **默认。** 每块蛋糕自己走完搅拌→烘烤→装箱。A 在装箱时,B 可能还在搅拌 |
|
||||
| `parallel(thunks)` | 等**所有**托盘都回来 —— 只有下一步真的需要全部结果时才用 |
|
||||
| `phase(title)` | 在进度板上宣布“现在进入烘烤” |
|
||||
| `log(message)` | 喊一句短状态 |
|
||||
| `workflow(name, args)` | 套用一份更小的菜谱(只嵌一层) |
|
||||
| `args` | 这次运行的“食材清单” |
|
||||
| `budget` | 还能烧多少“烤箱分钟”(token) |
|
||||
|
||||
运行时会把 `meta.name` 用在本地产物文件名中,因此还要求它是 1-64 个字符的安全 slug,只能包含字母、数字、`.`、`_`、`-`。
|
||||
默认用 `pipeline`。只有下一步必须凑齐上一阶段全部结果时,才用 `parallel` —— 比如要先尝完所有托盘再写评分表。
|
||||
|
||||
```python
|
||||
def validate_meta(meta):
|
||||
if not isinstance(meta, dict):
|
||||
raise WorkflowInputError("meta 必须是对象字面量")
|
||||
if not meta.get("name") or not meta.get("description"):
|
||||
raise WorkflowInputError("meta 必须包含 name 和 description")
|
||||
if not isinstance(meta["name"], str) or not WORKFLOW_NAME_RE.fullmatch(meta["name"]):
|
||||
raise WorkflowInputError("meta.name 必须是 1-64 字符的安全 slug")
|
||||
if "phases" in meta and (
|
||||
not isinstance(meta["phases"], list)
|
||||
or not all(isinstance(p, str) and p for p in meta["phases"])
|
||||
):
|
||||
raise WorkflowInputError("meta.phases 必须包含非空字符串")
|
||||
return meta
|
||||
# 每个审查维度独立走 审计 → 验证(阶段之间不等齐)。
|
||||
results = await ctx.pipeline(DIMENSIONS, audit, verify)
|
||||
confirmed = [f for r in results if r for f in r["confirmed"]]
|
||||
```
|
||||
|
||||
## 编排原语
|
||||
## 让答案机器能读
|
||||
|
||||
脚本收到一个只暴露少量编排原语的 `ExecutionState`,本身不直接读写文件,也不运行 shell。默认交互模式把 `agent()` 接到与宿主相同的真实 API client;每个子 agent 只读取 workflow 参数中提供的内容。`demo` 和单元测试使用 `MockAgentRunner`,便于重复观察事件和 journal。
|
||||
|
||||
| 原语 | 作用 |
|
||||
|------|------|
|
||||
| `agent(prompt, {schema, label, phase})` | 派一个子 agent 干活 |
|
||||
| `parallel(thunks)` | **等齐屏障**:所有任务并行跑完,一起等结果回来 |
|
||||
| `pipeline(items, *stages)` | 每个 item 分阶段跑,**不等齐**,跑完一个往下走一个 |
|
||||
| `phase(title)` | 标记当前进度阶段(更新进度条) |
|
||||
| `log(message)` | 打一行进度日志 |
|
||||
| `workflow(name, args)` | 嵌套子工作流(只支持一层) |
|
||||
|
||||
每个 item 都要独立经过相同步骤时,可以使用 `pipeline`。item A 跑到第 3 阶段时,item B 可能还在第 1 阶段;下一步必须同时使用上一阶段全部结果时,再使用 `parallel` 等待所有调用完成。
|
||||
如果帮手回来写散文,下一阶段就很难把 finding 和 verdict 一一对应。传入 `schema`:运行时要求 JSON、做校验,不对就**重试一次**。再不对,这次调用报错(见下面的空值隔离)。
|
||||
|
||||
```python
|
||||
async def pipeline(self, items, *stages):
|
||||
async def run_item(item, idx):
|
||||
value = item
|
||||
for stage in stages: # 每个 item 独立跑完所有 stage
|
||||
value = await stage(value, item, idx)
|
||||
return value
|
||||
return await asyncio.gather(*[run_item(it, i) for i, it in enumerate(items)])
|
||||
out = await ctx.agent(
|
||||
f"检查这段变更里有没有{dimension}相关的问题:\n{changes}",
|
||||
schema=FINDINGS_SCHEMA,
|
||||
label=f"audit:{dimension}",
|
||||
)
|
||||
# out 是带 "findings" 的字典,不是一段话
|
||||
```
|
||||
|
||||
## 结构化输出:别让子 agent 回来写散文
|
||||
跟你聊天可以用自然语言;流水线需要接口对得上。
|
||||
|
||||
`agent({schema})` 会要求子 agent 只返回匹配 schema 的 JSON 对象。运行时解析并校验结果,不符合时重试一次。这样下游代码拿到的是对象,不必再从自然语言中提取字段。
|
||||
## 一个帮手失败时
|
||||
|
||||
s05 就说过,工具的参数不能全信;这里是同一个道理反过来:子 agent 的输出也不能全信。加一层校验,不对就给一次机会重试,把不确定性挡在编排层外面。
|
||||
不能因为一个托盘糊了,整支队伍停工。
|
||||
|
||||
- **`parallel`**:失败的 thunk 在该槽位变成 `null` / `None`;整个 gather 不会因此拒绝。
|
||||
- **`pipeline`**:某个 stage 失败时,**该 item** 变成 `null` / `None`,并跳过它后面的 stage;其他 item 继续。
|
||||
|
||||
合并前要小心过滤 —— 常见写法是 `if r` / `.filter(Boolean)`。
|
||||
|
||||
```python
|
||||
run = await asyncio.to_thread(self.runner.run, prompt, schema, label)
|
||||
result = run.value
|
||||
if schema is not None:
|
||||
ok, err = SimpleJsonSchema(schema).validate(result)
|
||||
if not ok: # 提醒一次重试,再不对就报错
|
||||
retry = await asyncio.to_thread(
|
||||
self.runner.run, prompt + "\n\n返回合法的 JSON。", schema, label
|
||||
)
|
||||
result = retry.value
|
||||
ok, err = SimpleJsonSchema(schema).validate(result)
|
||||
if not ok:
|
||||
raise WorkflowInputError(f"agent({{schema}}) 输出不合法: {err}")
|
||||
verdicts = await ctx.parallel([...]) # 有些位置可能是 None
|
||||
confirmed = [
|
||||
f for f, v in zip(findings, verdicts)
|
||||
if v and v.get("isReal")
|
||||
]
|
||||
```
|
||||
|
||||
## 任务状态和进度事件
|
||||
## Journal 与续跑
|
||||
|
||||
`LocalWorkflowTask` 维护状态和 token 用量,向外发一条 SDK 风格的事件流:`task_started` → 一串 `task_progress`(包含阶段切换、子 agent 启动和日志输出)→ 最后一个 `task_notification`(完成或失败,带输出文件、agent 数和 token 数)。
|
||||
每次运行都有一个 `runId`。每个 `agent()` 结束后,运行时往磁盘上的 journal 追加一行。把它想成笔记本:按你**召唤**帮手的顺序记,而不是按他们从烤箱回来的先后。
|
||||
|
||||
演示会按顺序打印这些事件,并在最终通知后返回任务状态。
|
||||
续跑时(`resume_from_run_id` / `resumeFromRunId`),脚本仍从开头执行,但是:
|
||||
|
||||
```python
|
||||
class LocalWorkflowTask:
|
||||
def progress_event(self, ptype, **data): # 阶段/子agent/日志
|
||||
self.progress.append({"type": ptype, **data})
|
||||
print(f" 进度 {ptype} ...")
|
||||
1. 按调用顺序,把每次 `agent()` 和下一条 journal 记录比对。
|
||||
2. **最长未改前缀** → 缓存命中(直接回放)。
|
||||
3. 遇到**第一个**改过或未完成的调用,前缀断开。
|
||||
4. **之后全部实跑** —— 即使 journal 更后面还躺着旧 key,也不能偷懒命中。
|
||||
|
||||
所以真正的 JS workflow 运行时会禁止 `Date.now()`、`Math.random()` 和裸的 `new Date()`:不确定的时钟和骰子会改 prompt 或调用顺序,笔记本就对不上了。这个 Python 演示不会完整沙箱这些 —— 但脚本仍应写成确定性的。
|
||||
|
||||
```text
|
||||
journal: [A ✓] [B ✓] [C ✓] [D ✓]
|
||||
续跑: A 命中 → B 命中 → C 改过 → D 实跑(不会悄悄命中旧的 D)
|
||||
```
|
||||
|
||||
## 存储:快照 + journal,断了能续
|
||||
## 跟着示例走:`review-changes`
|
||||
|
||||
运行时把每次运行的数据存在 `s16_workflow_runtime/.runtime/`:快照 `<runId>.json`、输出 `<runId>.output.json`、journal `<runId>.journal.jsonl` 和协调文件 `<runId>.lock`。每次新运行都会在打开 journal 前,用排他式文件创建预留新的 `runId`。整次执行和最终持久化期间都持有 run lock,另一个进程不能同时 resume 同一次运行。快照记录 workflow 名称、参数和任务状态;resume 会先验证已保存的快照和 journal,再改动原有的成功产物。
|
||||
四个审查维度走同一条两阶段路径:
|
||||
|
||||
journal 是断点续跑的核心,它一条一条记下来每个 `agent()` 的结果:
|
||||
|
||||
```python
|
||||
class WorkflowJournal:
|
||||
def record(self, key, value):
|
||||
self._f.write(json.dumps({"key": key, "value": value}) + "\n")
|
||||
self._f.flush()
|
||||
self.cache[key] = value
|
||||
```text
|
||||
correctness ── 审计 ── 验证 ──┐
|
||||
security ── 审计 ── 验证 ──┤── 合并确认过的问题
|
||||
performance ── 审计 ── 验证 ──┤
|
||||
style ── 审计 ── 验证 ──┘
|
||||
```
|
||||
|
||||
## resume:用 runId 续跑,没改的直接用缓存
|
||||
|
||||
带着 `resume_from_run_id` 再次调用 workflow 时,脚本会重新执行,但每个 `agent()` 都会计算一个确定的语义 key:key 在 journal 里有记录,就直接返回缓存结果;只有改过的调用以及依赖它的后续步骤才会真的运行。
|
||||
|
||||
这里有个关键点:key 不能依赖并发顺序。`parallel` 和 `pipeline` 里 agent 完成的顺序是不确定的,用"第几个完成"当 key,两次跑缓存就对错位了。所以 key 是根据调用内容(类型、标签、prompt、schema)算的稳定哈希,不是一个会竞争的计数器:
|
||||
|
||||
```python
|
||||
def key(self, kind, label, prompt, schema):
|
||||
basis = f"{kind}|{label}|{prompt}|{json.dumps(schema, sort_keys=True)}"
|
||||
return f"{kind}-{_stable_hash(basis) % 10**10:010d}"
|
||||
|
||||
# agent() 内部:
|
||||
cached = self.journal.cached(key)
|
||||
if cached is not MISS:
|
||||
self.task.progress_event("workflow_agent", label=label, status="cached")
|
||||
return cached
|
||||
```
|
||||
|
||||
## 稳定调用键
|
||||
|
||||
续跑时,运行时需要把当前 `agent()` 与 journal 中的旧调用对应起来。稳定哈希让同一份 workflow 和同样的参数产生相同的调用 key。真实模型的回答可以变化;只要调用内容没有变化,resume 就直接使用 journal 中已经保存的结果。
|
||||
|
||||
## 跑起来看看
|
||||
|
||||
示例 workflow `review-changes` 用 `pipeline` 让每个审查维度独立走“审计 → 验证”。默认交互模式使用真实 API,并从 `args.changes` 读取待审查内容;`demo` 使用固定 runner 数据来展示 pipeline、结构校验、journal 和续跑。
|
||||
1. **Review** — 每个维度的审计员返回结构化 findings。
|
||||
2. **Verify** — 每条 finding 交给对抗性检查(在 verify 阶段里用 `parallel`)。
|
||||
3. 只保留被标成真实的问题,再按严重程度排序。
|
||||
|
||||
```python
|
||||
async def sample_workflow(ctx, args):
|
||||
ctx.phase("Review")
|
||||
changes = args.get("changes", "")
|
||||
|
||||
async def audit(_v, dimension, _i):
|
||||
out = await ctx.agent(f"检查这段变更里有没有{dimension}相关的问题:\n{changes}",
|
||||
schema=FINDINGS_SCHEMA, label=f"audit:{dimension}", phase="Review")
|
||||
return {"dimension": dimension, "findings": out["findings"]}
|
||||
|
||||
async def verify(audited, dimension, _i):
|
||||
ctx.phase("Verify")
|
||||
verdicts = await ctx.parallel([ # 每条发现独立做对抗性验证
|
||||
(lambda f=f: ctx.agent(f"根据变更内容验证这条 finding:\n{changes}\n\n{f}",
|
||||
schema=VERDICT_SCHEMA, label=f"verify:{dimension}:{f['title']}"))
|
||||
for f in audited["findings"]])
|
||||
return {"dimension": dimension,
|
||||
"confirmed": [f for f, v in zip(audited["findings"], verdicts) if v and v["isReal"]]}
|
||||
|
||||
results = await ctx.pipeline(DIMENSIONS, audit, verify)
|
||||
...
|
||||
confirmed = [f for r in results if r for f in r["confirmed"]]
|
||||
ctx.log(f"确认了 {len(confirmed)} 个真实问题")
|
||||
return {"confirmed": confirmed}
|
||||
```
|
||||
|
||||
## 相对 s15 的变更
|
||||
## 怎样接到 s15
|
||||
|
||||
| | s15 Agent Harness 集成 | s16 Workflow Runtime |
|
||||
|--|-----------|---------------------|
|
||||
| 循环 | 单个、模型驱动 | 主循环不变;工具背后执行脚本编排 |
|
||||
| 谁决定下一步 | 模型逐轮决定 | 脚本预先写好编排流程 |
|
||||
| 多 agent | s06 子 agent,一次性派出去 | 通过 agent-runner 边界执行脚本化、可续跑的调用 |
|
||||
| 新增机制 | — | 编排原语、宿主 registry 与工具适配器、任务生命周期、进度事件、journal/续跑、结构化输出 |
|
||||
s15 仍是宿主循环。s16 只多一个工具:`Workflow`。模型(或你)给出已保存的名字;适配器查 registry,再跑脚本。
|
||||
|
||||
s16 不替换主循环,它只是在工具层暴露 `Workflow`,背后启动一个本地 workflow 运行时:一份保存好的脚本通过 agent-runner 边界协调 N 次调用。s06 的子 agent 是模型临场派一次;s16 把编排写成可续跑的宿主代码。
|
||||
| | Claude Code / Pi(产品) | 本课教学 CLI |
|
||||
|--|--------------------------|--------------|
|
||||
| 脚本语言 | 沙箱里的 JavaScript | 可读的 Python 函数 |
|
||||
| 动态门 | 模型写 `script` / 改 `scriptPath` | 文档说明;演示走已保存的 `name` |
|
||||
| 运行时宿主 | 后台 + 通知,会话保持可响应 | `demo` / `resume` 前台跑,方便观察 |
|
||||
| 想法 | 同一套原语、journal、前缀续跑 | 教学模型 —— 简化处会说清楚 |
|
||||
|
||||
主循环不会变成 workflow 引擎。它只是多借一把工具,就像借 `bash` 或 `task` 一样。
|
||||
|
||||
## 试一下
|
||||
|
||||
```bash
|
||||
python s16_workflow_runtime/code.py # 主模型和 Workflow 子 agent 都使用真实 API
|
||||
python s16_workflow_runtime/code.py demo # 运行确定性的 review-changes 测试数据并观察事件流
|
||||
python s16_workflow_runtime/code.py resume # 用上次的 runId 续跑,每个 agent() 都命中 journal 缓存
|
||||
python s16_workflow_runtime/code.py # s15 宿主 + Workflow 工具(真实 API)
|
||||
python s16_workflow_runtime/code.py demo # 固定数据:观察阶段和 agent
|
||||
python s16_workflow_runtime/code.py resume # 同一个 runId;前缀应全部缓存命中
|
||||
```
|
||||
|
||||
默认命令里,可以先让模型读取改动,再把内容放进 `args.changes` 并运行保存好的 `review-changes` workflow。主模型和 workflow 子 agent 都使用真实 API。`demo` 命令使用固定 runner 数据,便于重复观察生命周期和续跑;续跑命中全部缓存时显示 `agents=0 tokens=0`。
|
||||
留意这些:
|
||||
|
||||
## 接下来
|
||||
- `workflow_phase`:先 Review,再 Verify
|
||||
- 每个 `workflow_agent`:第一次是 `done`,完整续跑变成 `cached`
|
||||
- 结尾有一份简短的确认列表;全命中续跑显示 `agents=0 tokens=0`
|
||||
|
||||
[s17 Goal Loop](../s17_goal_loop/) 会使用一个更小、独立的循环检查既定目标是否已经达成,并据此决定是否还需要下一轮。
|
||||
## 相对 s15 → 下一站 s17
|
||||
|
||||
<!-- translation-sync: zh@v10, en@v10, ja@v10 -->
|
||||
| | s15 Agent Harness 集成 | s16 Workflow Runtime |
|
||||
|--|------------------------|----------------------|
|
||||
| 循环 | 单个、模型驱动 | 同一循环;一个工具跑脚本 |
|
||||
| 谁决定下一步 | 模型逐轮决定 | 脚本规定整批形状 |
|
||||
| 多 agent | 一次性子 agent | 可脚本化、可续跑的 `agent()` |
|
||||
| 失败 / 续跑 | 靠对话记忆 | 空值隔离 + journal 前缀 |
|
||||
|
||||
**s16 = 一批活怎么跑。s17 = 整个目标算不算做完。**
|
||||
|
||||
[s17 Goal Loop](../s17_goal_loop/) 会问一个独立判断器:该停,还是再来一轮?
|
||||
|
||||
<!-- translation-sync: zh@v11, en@v11, ja@v11 -->
|
||||
|
||||
Reference in New Issue
Block a user