docs(s16): naturalize Chinese prose (v20)

This commit is contained in:
Xinlu Lai
2026-08-13 03:29:54 +08:00
parent cb6062258a
commit e8cf4b299e

View File

@@ -1,18 +1,20 @@
# s16: Workflow Runtime — 把编排写进代码 # s16: Workflow Runtime — 把步骤写进代码
[English](README.md) · [中文](README.zh.md) · [日本語](README.ja.md) [English](README.md) · [中文](README.zh.md) · [日本語](README.ja.md)
[s15](../s15_integrated_harness/) → `s16` → [s17](../s17_goal_loop/) [s15](../s15_integrated_harness/) → `s16` → [s17](../s17_goal_loop/)
> *计划别只活在嘴上。* 脚本管先后,模型管每一步判断 > *别把计划只留在聊天里。* 谁先谁后由脚本定,每一步怎么判断交给模型
> >
> **Harness 层**: 编排 — 在单 agent 循环上,再跑一套多 agent 脚本。 > **Harness 层**: 编排 — 在单个 Agent 循环上,再跑一套多 Agent 脚本。
## 问题 ## 问题
你已经会让模型在一个循环里读文件、改代码、看报错。可有些活你其实**早就知道先后顺序**:先分维度审,再找人挑刺,最后汇总。若顺序只靠聊天记住,模型会做到一半喊完工,自己批改自己偏甜,压几轮上下文后连「别动 X」都丢了。 到这一章,你已经会让模型在循环里读文件、改代码、看报错了。
软对话扛不住并行、稳定结果形状,也扛不住崩了接着跑。你需要的不是更会聊天的模型,是一份**写下来的编排** 可有些活,你其实早就想好了顺序:先按几个角度审一遍,再找人挑刺,最后汇总。顺序如果只记在对话里,模型很容易做到一半就喊「做完了」;让它自己评自己,分数又往往偏高;上下文压了几轮之后,「别动 X」这种约束也可能悄悄不见
聊天记不住并行,也扛不住中途崩掉再接着跑。你缺的不是一个更会聊天的模型,而是一份**写下来的步骤**。
## 解决方案 ## 解决方案
@@ -23,43 +25,43 @@
脚本agent / pipeline / parallel 脚本agent / pipeline / parallel
变量 + journal半成品放这儿,不塞群聊 变量 + journal中间结果放这里,别塞回对话
``` ```
帮手(agent)仍负责想**脚本**循环、分发、合并。中间结果进变量和 journal不进主对话。 Agent 还是负责想**脚本**负责循环、分发、合并。中间结果进变量和 journal不进主对话。
一句话:**编排从「智力」挪到「结构。** 一句话:**把「怎么排步骤」从临场发挥,变成写死的结构。**
![静态 harness vs 动态 workflow](images/dynamic-vs-static.png) ![静态 harness 动态 workflow](images/dynamic-vs-static.png)
*:通吃的固定流水线。右:为这次任务现裁的编排。* *边:事先写好的通用流水线。右:为这次任务临时裁出来的编排。*
Claude Code 里有两扇门:**动态**——模型为这次任务写 JS`script` / `scriptPath`**已保存**——好脚本`name` + `args`跑。门外还有用 SDK 事先写死的静态编排。本章是 **Python 教学 runtime**不嵌 JS):思想对齐,演示走「已保存」门。模型在产品里本来就能交脚本——我们只是不在这里跑 JS。 Claude Code 里,入口大致有两种。**动态**模型为这次任务写一段 JS`script` / `scriptPath`**已保存**:跑通了的脚本放进仓库,`name` + `args`调。外面还有一种事先用 SDK 写死的静态编排。本章是 **Python 教学版运行时**不嵌 JS 引擎:想法对齐,演示走「已保存」这条路。产品里模型本来就能交脚本我们只是不在这里跑 JS。
## 工作原理 ## 工作原理
**1. 三个** **1. 三个词就够用**
```text ```text
agent 一个帮手,一件事(可带 schema拿到能往下传的 JSON agent 一个帮手,一件事(可带 schema拿到能往下传的 JSON
pipeline 每个 item 自己走完各阶段(默认,不等齐 pipeline 每个条目自己走完各阶段(默认这样,不用互相等
parallel 等所有结果齐再往下(屏障,少用) parallel 等所有结果齐再往下(屏障,少用)
``` ```
谁失手`parallel` 那个槽变成 `null``pipeline` 丢掉那个 item。舰队不整船沉。合并前先过滤 有人失败了也别整队停工`parallel` 里失败的那一格变成 `null``pipeline` 丢掉那一条。合并前先把空的滤掉
**2. 续跑本子,靠聊天记** **2. 续跑,靠小本子,靠聊天记**
journal 按 `agent()` **召唤顺序**记。续跑回放最长未改前缀;碰到第一处改动,后面全跑。真 JS 运行时 `Date.now()` / `Math.random()`免得本子对不齐——教学脚本也写成确定性的。 每次调用 `agent()`journal 按**调用顺序**记一笔。续跑时,从头核对:还没改过的前缀直接复用;碰到第一处改动,后面全部重跑。真正的 JS 运行时不许用 `Date.now()` / `Math.random()`不然本子对不齐教学脚本也尽量写成确定性的。
```text ```text
journal [A] [B] [C] [D] journal [A] [B] [C] [D]
续跑 命中 命中 续跑 复用 复用
``` ```
**3. 一个样本:分发 + 对抗** **3. 一个例子:拆开审,再找人对着挑**
`review-changes` 不是「一种模式」,是 **Fanout** 里嵌 **Adversarial**:多维度 `pipeline(audit, verify)`,验证里再 `parallel` 挑刺,只留站得住的 finding `review-changes` 不是单一招式。它是「拆开干」外面,套了一层「别人来挑刺」:几个维度各自 `pipeline(audit, verify)`,验证阶段再用 `parallel` 并行检查,只留下仍然站得住的问题
```text ```text
correctness ── 审计 ── 验证 ──┐ correctness ── 审计 ── 验证 ──┐
@@ -69,7 +71,7 @@ journal 按 `agent()` **召唤顺序**记账。续跑回放最长未改前缀;
``` ```
```python ```python
# code.py 节选形状就这些 # code.py 节选,看形状就
async def sample_workflow(ctx, args): async def sample_workflow(ctx, args):
ctx.phase("Review") ctx.phase("Review")
results = await ctx.pipeline(DIMENSIONS, audit, verify) results = await ctx.pipeline(DIMENSIONS, audit, verify)
@@ -77,28 +79,28 @@ async def sample_workflow(ctx, args):
return {"confirmed": confirmed} return {"confirmed": confirmed}
``` ```
舰队不能早停,作者不当裁判,拓扑也不靠 chat 每轮改 这样一来,队伍没法提早收工,作者不当自己的裁判,步骤也不会被聊天一轮轮改
<details> <details>
<summary>六种常见形状(模式库)</summary> <summary>六种常见形状</summary>
![六种 Workflow 模式](images/six-workflow-patterns.png) ![六种 Workflow 模式](images/six-workflow-patterns.png)
| 模式 | 人话 | 原语速写 | | 名字 | 人话 | 怎么拼 |
|------|------|----------| |------|------|--------|
| Classify-And-Act | 先分拣再交给对的人 | `agent` → 分支 → `agent` | | Classify-And-Act | 先分拣再交给对的人 | `agent` → 分支 → `agent` |
| Fanout-And-Synthesize | 拆开干,再合并 | `pipeline` / `parallel` → 汇总 | | Fanout-And-Synthesize | 拆开干,再合并 | `pipeline` / `parallel` → 汇总 |
| Adversarial Verification | 别让狐狸评鸡窝 | 产出 → `parallel(verify)` → 过滤 | | Adversarial Verification | 别让自己给自己打分 | 产出 → `parallel(verify)` → 过滤 |
| Generate-And-Filter | 先多再筛 | `parallel(gens)` → 过滤 | | Generate-And-Filter | 先多写几份,再筛 | `parallel` 生成 → 过滤 |
| Tournament | 两两比出冠军 | 裁判 `agent` | | Tournament | 两两比,决出更好的 | 裁判 `agent` |
| Loop Until Done | 还有新发现?」就继续 | `while` + 停止 + `budget` | | Loop Until Done | 还有新发现就继续 | `while` + 停止条件 + `budget` |
`review-changes` Fanout + Adversarial。研究类常叠:分发 → 过滤 → 验证 → 汇总。 `review-changes` 大约等于 Fanout + Adversarial。做调研时,常常再叠:分发 → 过滤 → 验证 → 汇总。
</details> </details>
<details> <details>
<summary>动态 / 已保存 / 静态 & 官方原语图</summary> <summary>动态、已保存、静态,以及官方原语图</summary>
```python ```python
# 教学示意 # 教学示意
@@ -111,19 +113,21 @@ Workflow({ "name": "review-changes", "args": { "changes": "..." } })
</details> </details>
<details> <details>
<summary>不可信输入时:隔离读写</summary> <summary>输入不可信时,把读和写隔开</summary>
读工单的,不该同时着开 PR 的钥匙。读者只读 → 摘要;受信任的一侧只看摘要行动 读工单的 Agent,不该同时着开 PR 的钥匙。一边只读、整理成摘要;另一边只看摘要再动手
```text ```text
积压 → [隔离区: 读 / 去重 / 摘要] → [受信任: 行动] 积压 → [只读区: 读 / 去重 / 摘要] → [可信区: 行动]
``` ```
![隔离分流](images/quarantine-triage.png) ![隔离分流](images/quarantine-triage.png)
</details> </details>
谁握计划s06 一次性派工,s13 邮箱同伴,s15 单循环聊天,**s16 是脚本 + journal**s17 在门口问整件事做完没有。普通改几个文件s15 或一个 s06 往往够。Workflow 贵在 token 和协调——**结构必须比单次对话活得更久**时再用。 计划到底谁说了算s06 一次性派出去;s13 是长期队友加邮箱;s15 单循环里聊着决定;**s16 把步骤写进脚本,进度记在 journal**s17 在门口问整件事做完没有。
平时改几个文件s15 或一个 s06 多半够了。Workflow 又费 token 又费协调——**只有步骤必须比单次对话活得更久**时,再拿出来用。
## 试一试 ## 试一试
@@ -132,8 +136,8 @@ python s16_workflow_runtime/code.py demo
python s16_workflow_runtime/code.py resume python s16_workflow_runtime/code.py resume
``` ```
第一次看 Review Verify;第二次同一 runagent 应大量 `cached`(理想情况 `agents=0 tokens=0`)。想挂进完整宿主:不加参数直接跑 `code.py` 第一次跑,看 Review 再到 Verify同一 run 再跑一次,大多应显示 `cached`(理想情况 `agents=0 tokens=0`)。想挂进上一章的完整程序,不加参数直接跑 `code.py`
s15 还是那个循环;这里只多了一个 `Workflow` 工具。[s17](../s17_goal_loop/) 问另一个问题:该停了吗? s15 还是那个循环;这里只多了一个 `Workflow` 工具。[s17](../s17_goal_loop/) 问的是另一件事:该停了吗?
<!-- translation-sync: zh@v19, en@v19, ja@v19 --> <!-- translation-sync: zh@v20, en@v19, ja@v19 -->