flare-agent Bài 6: Workflow Engine — Graph-based Multi-step
Xây dựng @flare-agent/workflow — graph-based workflow engine cho phép định nghĩa multi-step agent flows, conditional branching và parallel execution trên Cloudflare.
Nghe bài viết này dưới dạng podcastflare-agent Bài 6: Workflow Engine — Graph-based Multi-step
Series: Build Your Own AI Agent Framework trên Cloudflare Bài: 6 / 7 —
@flare-agent/workflow
Tại sao cần Workflow Engine?
Agent Loop đơn giản (bài 5) phù hợp cho single-turn Q&A hoặc simple tool calling. Nhưng nhiều use case cần multi-step flow có cấu trúc:
- Vocabulary quiz: present word → user trả lời → evaluate → reward/hint → next word
- Onboarding: collect info → generate study plan → assign first lesson
- RAG pipeline: rephrase query → retrieve context → generate answer → validate
Agent Loop không đủ vì LLM không đảm bảo thứ tự bước, còn Workflow Engine thì deterministic.
Cấu trúc package
packages/workflow/
src/
WorkflowContext.ts # shared state giữa steps
Step.ts # step definition
Graph.ts # DAG builder
WorkflowEngine.ts # executor
index.ts
Workflow Context
// src/WorkflowContext.ts
import type { AgentContext } from '@flare-agent/types';
export class WorkflowContext {
private results = new Map<string, unknown>();
constructor(
public readonly agentCtx: AgentContext,
public readonly input: unknown,
public metadata: Record<string, unknown> = {}
) {}
// Lưu kết quả của 1 step
set(stepName: string, result: unknown): void {
this.results.set(stepName, result);
}
// Lấy kết quả của step trước
get<T = unknown>(stepName: string): T | undefined {
return this.results.get(stepName) as T;
}
// Lấy kết quả của step gần nhất
getLast<T = unknown>(): T | undefined {
const keys = [...this.results.keys()];
if (!keys.length) return undefined;
return this.results.get(keys[keys.length - 1]) as T;
}
getAll(): Record<string, unknown> {
return Object.fromEntries(this.results);
}
}
Step Definition
// src/Step.ts
import type { WorkflowContext } from './WorkflowContext';
export interface StepDefinition<TOutput = unknown> {
name: string;
execute(ctx: WorkflowContext): Promise<TOutput>;
// Quyết định step tiếp theo dựa trên kết quả
// null = kết thúc workflow
next?(result: TOutput, ctx: WorkflowContext): string | null;
}
// Step chạy LLM call
export interface LLMStepDefinition extends StepDefinition<string> {
prompt: string | ((ctx: WorkflowContext) => string);
}
// Step chạy logic thuần (không cần LLM)
export interface FunctionStepDefinition<TOutput>
extends StepDefinition<TOutput> {
execute(ctx: WorkflowContext): Promise<TOutput>;
}
Graph Builder
// src/Graph.ts
import type { StepDefinition } from './Step';
export class Graph {
private steps = new Map<string, StepDefinition>();
private startStep?: string;
// Thêm step
step<T>(definition: StepDefinition<T>): this {
this.steps.set(definition.name, definition as StepDefinition);
return this;
}
// Set step bắt đầu
start(stepName: string): this {
if (!this.steps.has(stepName)) {
throw new Error(`Step "${stepName}" not found in graph`);
}
this.startStep = stepName;
return this;
}
getStep(name: string): StepDefinition | undefined {
return this.steps.get(name);
}
getStartStep(): string {
if (!this.startStep) {
// Default: step đầu tiên được thêm vào
const first = this.steps.keys().next().value;
if (!first) throw new Error('Graph has no steps');
return first;
}
return this.startStep;
}
validate(): void {
// Kiểm tra không có dangling step references
for (const step of this.steps.values()) {
if (step.next) {
// next là dynamic function — không validate được statically
// sẽ validate at runtime
}
}
}
}
Workflow Engine
// src/WorkflowEngine.ts
import type { AgentContext } from '@flare-agent/types';
import { Graph } from './Graph';
import { WorkflowContext } from './WorkflowContext';
export interface WorkflowResult {
output: unknown;
steps: Array<{ name: string; result: unknown; durationMs: number }>;
totalDurationMs: number;
}
export class WorkflowEngine {
constructor(
private graph: Graph,
private maxSteps = 20 // tránh infinite loop
) {}
async run(
input: unknown,
agentCtx: AgentContext
): Promise<WorkflowResult> {
const ctx = new WorkflowContext(agentCtx, input);
const stepLog: WorkflowResult['steps'] = [];
const startTime = Date.now();
let currentStepName: string | null = this.graph.getStartStep();
let stepCount = 0;
while (currentStepName !== null) {
if (stepCount >= this.maxSteps) {
throw new Error(`Workflow exceeded max steps (${this.maxSteps})`);
}
const step = this.graph.getStep(currentStepName);
if (!step) {
throw new Error(`Step "${currentStepName}" not found`);
}
// Execute step
const stepStart = Date.now();
const result = await step.execute(ctx);
const durationMs = Date.now() - stepStart;
// Lưu kết quả
ctx.set(currentStepName, result);
stepLog.push({ name: currentStepName, result, durationMs });
stepCount++;
// Quyết định step tiếp theo
if (step.next) {
currentStepName = step.next(result, ctx);
} else {
currentStepName = null; // step cuối, kết thúc
}
}
return {
output: ctx.getLast(),
steps: stepLog,
totalDurationMs: Date.now() - startTime,
};
}
}
Ví dụ thực tế — Vocabulary Quiz Flow
import { Graph, WorkflowEngine } from '@flare-agent/workflow';
const quizGraph = new Graph()
.step({
name: 'fetch-word',
execute: async (ctx) => {
const db = ctx.agentCtx.env.DB as D1Database;
const userId = ctx.agentCtx.userId!;
// Lấy từ chưa học
return db.prepare(
`SELECT * FROM vocabulary
WHERE id NOT IN (
SELECT word_id FROM progress WHERE user_id = ?
) LIMIT 1`
).bind(userId).first();
},
next: (word) => word ? 'present-word' : 'no-more-words',
})
.step({
name: 'present-word',
execute: async (ctx) => {
const word = ctx.get<any>('fetch-word');
// Trả về word data — frontend hiển thị
return { word: word.word, hint: word.definition };
},
next: () => 'evaluate-answer',
})
.step({
name: 'evaluate-answer',
execute: async (ctx) => {
const word = ctx.get<any>('fetch-word');
const userAnswer = ctx.input as string;
const correct = userAnswer.toLowerCase() === word.word.toLowerCase();
return { correct, word: word.word };
},
next: (result) => result.correct ? 'save-progress' : 'give-hint',
})
.step({
name: 'give-hint',
execute: async (ctx) => {
const word = ctx.get<any>('fetch-word');
return { hint: word.example_sentence };
},
next: () => null, // kết thúc — chờ user thử lại
})
.step({
name: 'save-progress',
execute: async (ctx) => {
const word = ctx.get<any>('fetch-word');
const db = ctx.agentCtx.env.DB as D1Database;
await db.prepare(
'INSERT INTO progress (user_id, word_id, status) VALUES (?, ?, ?)'
).bind(ctx.agentCtx.userId, word.id, 'learned').run();
return { saved: true, nextStep: 'fetch-word' };
},
next: () => 'fetch-word', // loop — lấy từ tiếp theo
})
.step({
name: 'no-more-words',
execute: async () => ({ message: 'Bạn đã học hết tất cả từ vựng! 🎉' }),
// no next → kết thúc
})
.start('fetch-word');
// Chạy workflow
const engine = new WorkflowEngine(quizGraph);
const result = await engine.run(userAnswer, agentCtx);
Checklist
- Tạo
packages/workflow/ - Test quiz flow với mock D1
- Verify conditional branching hoạt động đúng
- Test max steps protection
Chưa có bình luận
Để lại bình luận
Bình luận sẽ được phê duyệt trước khi hiển thị.