17.12-工作流编排
要点
- 工作流是多个任务的有序组合——任务 A 完成后触发任务 B,B 完成后触发 C
- 简单工作流用链式调用——一个消费者完成后发消息到下一个队列
- 复杂工作流用 DAG(有向无环图)——支持并行分支和汇聚
- 工作流状态需要持久化——每个步骤的输入、输出、状态都要记录
- 失败处理要考虑回滚——前面的步骤成功了,后面的失败了怎么办
1. 什么是工作流
前面讲的异步任务是单个任务——上传文件、解析 PDF、生成 embedding。但真实业务经常需要多个任务串联:
用户上传文档
↓
[1] 解析文档 → 提取文本
↓
[2] 切 chunk → 分成段落
↓
[3] 生成 embedding → 向量化
↓
[4] 写入向量库 → 建立索引
↓
[5] 生成摘要 → 用 LLM 总结
↓
[6] 发送通知 → 告诉用户处理完成
这 6 个步骤构成一个工作流。每个步骤是独立的异步任务,步骤之间有依赖关系。
工作流的特点:
- 有序——步骤 2 必须等步骤 1 完成
- 有状态——每个步骤的输入是上一步的输出
- 可重试——某一步失败可以重试,不需要从头开始
- 可观测——需要知道当前执行到哪一步
2. 简单工作流:链式调用
最简单的实现:一个消费者完成后,发消息到下一个队列。
// 消费者 1:解析文档
export async function parseConsumer(batch: MessageBatch<ParseTask>, env: Env) {
for (const msg of batch.messages) {
const text = await parseDocument(msg.body.fileKey, env)
// 完成后发消息到下一个队列
await env.CHUNK_QUEUE.send({
workflowId: msg.body.workflowId,
text,
})
msg.ack()
}
}
// 消费者 2:切 chunk
export async function chunkConsumer(batch: MessageBatch<ChunkTask>, env: Env) {
for (const msg of batch.messages) {
const chunks = chunkText(msg.body.text)
await env.EMBED_QUEUE.send({
workflowId: msg.body.workflowId,
chunks,
})
msg.ack()
}
}
// 消费者 3:生成 embedding
export async function embedConsumer(batch: MessageBatch<EmbedTask>, env: Env) {
for (const msg of batch.messages) {
const vectors = await generateEmbeddings(msg.body.chunks, env)
await env.INDEX_QUEUE.send({
workflowId: msg.body.workflowId,
chunks: msg.body.chunks,
vectors,
})
msg.ack()
}
}链式调用的优点:简单、解耦。 缺点:步骤多了之后难以管理,失败时难以回滚。
3. 工作流状态
工作流状态需要持久化——记录每个步骤的执行情况。
CREATE TABLE workflows (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
type TEXT NOT NULL, -- 工作流类型
status TEXT NOT NULL DEFAULT 'pending',
current_step TEXT, -- 当前步骤
context TEXT NOT NULL, -- 工作流上下文(JSON,步骤间传递数据)
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE workflow_steps (
id TEXT PRIMARY KEY,
workflow_id TEXT NOT NULL,
step_name TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
input TEXT, -- 步骤输入(JSON)
output TEXT, -- 步骤输出(JSON)
error TEXT,
started_at TEXT,
completed_at TEXT
);interface WorkflowContext {
workflowId: string
userId: string
fileKey?: string
text?: string
chunks?: Chunk[]
vectors?: number[][]
summary?: string
[key: string]: unknown
}
async function updateWorkflowStep(
env: Env,
workflowId: string,
stepName: string,
status: 'pending' | 'processing' | 'completed' | 'failed',
input?: unknown,
output?: unknown,
error?: string
): Promise<void> {
const now = new Date().toISOString()
await env.DB.prepare(`
INSERT INTO workflow_steps (id, workflow_id, step_name, status, input, output, error, started_at, completed_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
status = excluded.status,
output = excluded.output,
error = excluded.error,
completed_at = excluded.completed_at
`).bind(
`${workflowId}:${stepName}`,
workflowId,
stepName,
status,
input ? JSON.stringify(input) : null,
output ? JSON.stringify(output) : null,
error ?? null,
status === 'processing' ? now : null,
status === 'completed' || status === 'failed' ? now : null,
).run()
// 更新工作流当前步骤
await env.DB.prepare(`
UPDATE workflows SET current_step = ?, status = ?, updated_at = ? WHERE id = ?
`).bind(stepName, status === 'failed' ? 'failed' : 'processing', now, workflowId).run()
}4. 工作流引擎
一个通用的工作流引擎,按步骤执行:
interface WorkflowStep {
name: string
execute: (context: WorkflowContext, env: Env) => Promise<WorkflowContext>
}
class WorkflowEngine {
private steps: WorkflowStep[] = []
addStep(step: WorkflowStep): this {
this.steps.push(step)
return this
}
async run(workflowId: string, initialContext: WorkflowContext, env: Env): Promise<void> {
let context = initialContext
for (const step of this.steps) {
// 检查是否已完成(断点续传)
const stepRecord = await env.DB.prepare(`
SELECT status, output FROM workflow_steps WHERE id = ?
`).bind(`${workflowId}:${step.name}`).first()
if (stepRecord?.status === 'completed') {
// 已完成,读取输出继续
context = { ...context, ...JSON.parse(stepRecord.output) }
continue
}
try {
await updateWorkflowStep(env, workflowId, step.name, 'processing', context)
context = await step.execute(context, env)
await updateWorkflowStep(env, workflowId, step.name, 'completed', context, context)
} catch (err) {
await updateWorkflowStep(env, workflowId, step.name, 'failed', context, undefined, String(err))
throw err
}
}
// 所有步骤完成
await env.DB.prepare(`
UPDATE workflows SET status = 'completed', updated_at = ? WHERE id = ?
`).bind(new Date().toISOString(), workflowId).run()
}
}使用:
// 定义工作流
const documentWorkflow = new WorkflowEngine()
.addStep({
name: 'parse',
execute: async (ctx, env) => {
const text = await parseDocument(ctx.fileKey!, env)
return { ...ctx, text }
},
})
.addStep({
name: 'chunk',
execute: async (ctx, env) => {
const chunks = chunkText(ctx.text!)
return { ...ctx, chunks }
},
})
.addStep({
name: 'embed',
execute: async (ctx, env) => {
const vectors = await generateEmbeddings(ctx.chunks!, env)
return { ...ctx, vectors }
},
})
.addStep({
name: 'index',
execute: async (ctx, env) => {
await indexVectors(ctx.chunks!, ctx.vectors!, env)
return ctx
},
})
// 消费者调用
export async function workflowConsumer(
batch: MessageBatch<{ workflowId: string }>,
env: Env
): Promise<void> {
for (const msg of batch.messages) {
const workflow = await env.DB.prepare(`
SELECT * FROM workflows WHERE id = ?
`).bind(msg.body.workflowId).first()
const context = JSON.parse(workflow.context) as WorkflowContext
try {
await documentWorkflow.run(msg.body.workflowId, context, env)
msg.ack()
} catch (err) {
msg.retry({ delaySeconds: 30 })
}
}
}5. DAG 工作流
复杂工作流有并行分支和汇聚:
步骤 A → 步骤 B ─┐
→ 步骤 C ─┼→ 步骤 D
→ 步骤 E ─┘
步骤 B、C、E 并行执行,全部完成后才能执行步骤 D。
interface DAGStep {
name: string
dependsOn: string[] // 依赖的步骤
execute: (context: WorkflowContext, env: Env) => Promise<WorkflowContext>
}
class DAGWorkflowEngine {
private steps: Map<string, DAGStep> = new Map()
addStep(step: DAGStep): this {
this.steps.set(step.name, step)
return this
}
async run(workflowId: string, initialContext: WorkflowContext, env: Env): Promise<void> {
const completed = new Set<string>()
let context = initialContext
while (completed.size < this.steps.size) {
// 找出所有依赖已完成的步骤
const ready = [...this.steps.values()].filter(
(step) =>
!completed.has(step.name) &&
step.dependsOn.every((dep) => completed.has(dep))
)
if (ready.length === 0) {
throw new Error('No ready steps - possible circular dependency')
}
// 并行执行所有就绪步骤
const results = await Promise.all(
ready.map(async (step) => {
await updateWorkflowStep(env, workflowId, step.name, 'processing')
const output = await step.execute(context, env)
await updateWorkflowStep(env, workflowId, step.name, 'completed', context, output)
return { step: step.name, output }
})
)
// 合并结果
for (const { output } of results) {
context = { ...context, ...output }
}
// 标记完成
for (const step of ready) {
completed.add(step.name)
}
}
await env.DB.prepare(`
UPDATE workflows SET status = 'completed', updated_at = ? WHERE id = ?
`).bind(new Date().toISOString(), workflowId).run()
}
}使用:
const dagWorkflow = new DAGWorkflowEngine()
.addStep({
name: 'A',
dependsOn: [],
execute: async (ctx, env) => { /* ... */ return ctx },
})
.addStep({
name: 'B',
dependsOn: ['A'],
execute: async (ctx, env) => { /* ... */ return ctx },
})
.addStep({
name: 'C',
dependsOn: ['A'],
execute: async (ctx, env) => { /* ... */ return ctx },
})
.addStep({
name: 'D',
dependsOn: ['B', 'C'],
execute: async (ctx, env) => { /* ... */ return ctx },
})6. 失败和回滚
步骤失败时,前面的步骤已经成功了。需要考虑回滚。
interface WorkflowStep {
name: string
execute: (context: WorkflowContext, env: Env) => Promise<WorkflowContext>
rollback?: (context: WorkflowContext, env: Env) => Promise<void> // 回滚操作
}
class WorkflowEngine {
async run(workflowId: string, initialContext: WorkflowContext, env: Env): Promise<void> {
const executedSteps: WorkflowStep[] = []
let context = initialContext
try {
for (const step of this.steps) {
await updateWorkflowStep(env, workflowId, step.name, 'processing')
context = await step.execute(context, env)
executedSteps.push(step)
await updateWorkflowStep(env, workflowId, step.name, 'completed')
}
} catch (err) {
// 回滚已执行的步骤(逆序)
for (const step of executedSteps.reverse()) {
if (step.rollback) {
try {
await step.rollback(context, env)
await updateWorkflowStep(env, workflowId, `${step.name}:rollback`, 'completed')
} catch (rollbackErr) {
console.error(`Rollback failed for ${step.name}:`, rollbackErr)
}
}
}
throw err
}
}
}不是所有步骤都能回滚。发邮件、调外部 API 通常无法撤回。对于这类步骤,用补偿操作替代回滚——比如发一封「取消」邮件。
7. 超时处理
步骤可能卡住——外部 API 不响应、死循环。需要超时控制。
async function withTimeout<T>(
promise: Promise<T>,
timeoutMs: number,
stepName: string
): Promise<T> {
return Promise.race([
promise,
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error(`Step ${stepName} timeout after ${timeoutMs}ms`)), timeoutMs)
),
])
}
// 使用
async function executeWithTimeout(step: WorkflowStep, context: WorkflowContext, env: Env) {
return withTimeout(step.execute(context, env), 60000, step.name) // 60 秒超时
}8. 工作流查询
// 查询工作流状态
app.get('/api/workflows/:id', async (c) => {
const [workflow, steps] = await Promise.all([
c.env.DB.prepare('SELECT * FROM workflows WHERE id = ?').bind(c.req.param('id')).first(),
c.env.DB.prepare('SELECT * FROM workflow_steps WHERE workflow_id = ? ORDER BY started_at').bind(c.req.param('id')).all(),
])
return c.json({ workflow, steps: steps.results })
})总结
回顾这一节的要点:
- 工作流是多个任务的有序组合——步骤之间有依赖关系
- 简单工作流用链式调用——一个消费者完成后发消息到下一个队列
- 复杂工作流用 DAG——支持并行分支和汇聚
- 工作流状态需要持久化——每个步骤的输入、输出、状态都记录在 D1
- 断点续传:从已完成的步骤继续执行,不需要从头开始
- 失败处理:回滚已执行的步骤(逆序),或执行补偿操作
- 超时控制:给每个步骤设超时,避免卡住
下一篇讲 Hono 作为任务调度入口——整合 HTTP、Queue、Cron。