Chuyển đến nội dung chính

第 12 課:工作流程引擎 — 設計基於節點的自動化

工作流程引擎設計:DAG(有向無環圖)、節點類型(觸發、動作、條件、循環)。模板解析,變數插值。工作流程架構和驗證。

🧠 人工智慧與機器學習 — 第 11 課 第 12 課:工作流引擎 — 設計 基於節點的自動化

從零開始搭建AI代理平台-與xClaw實戰

第 4 部分:工作流程引擎

亞洲開發網

簡介

工作流程引擎允許使用者使用視覺節點建立自動化 - 拖放、連接節點、無需程式碼即可定義複雜邏輯。本文從核心概念出發設計引擎。


1. 工作流程資料模型

// packages/core/src/workflow/types.ts
export interface Workflow {
  id: string;
  name: string;
  description?: string;
  tenantId: string;
  nodes: WorkflowNode[];
  edges: WorkflowEdge[];
  variables: Record<string, unknown>;
  status: 'draft' | 'active' | 'paused';
  createdAt: Date;
  updatedAt: Date;
}

export interface WorkflowNode {
  id: string;
  type: NodeType;
  label: string;
  config: Record<string, unknown>;
  position: { x: number; y: number };
}

export type NodeType =
  | 'trigger'          // Start node — webhook, schedule, manual
  | 'llm_call'         // Call LLM with prompt
  | 'tool_call'        // Execute a tool
  | 'condition'        // If/else branching
  | 'loop'             // For each / while
  | 'transform'        // Data transformation
  | 'http_request'     // External API call
  | 'code'             // Custom code (sandboxed)
  | 'delay'            // Wait N seconds
  | 'parallel'         // Run branches in parallel
  | 'merge'            // Merge parallel results
  | 'rag_query'        // Query knowledge base
  | 'set_variable'     // Set workflow variable
  | 'notification'     // Send notification
  | 'sub_workflow'     // Call another workflow
  | 'end'              // End node

export interface WorkflowEdge {
  id: string;
  source: string;       // Source node ID
  target: string;       // Target node ID
  label?: string;       // Edge label (for conditions)
  condition?: string;   // Condition expression
}

2. 節點處理程序

// packages/core/src/workflow/workflow-engine.ts
type NodeHandler = (
  node: WorkflowNode,
  context: WorkflowContext,
) => Promise<unknown>;

export class WorkflowEngine {
  private handlers = new Map<NodeType, NodeHandler>();
  private llmRouter: LLMRouter;
  private toolRegistry: ToolRegistry;

  constructor(llmRouter: LLMRouter, toolRegistry: ToolRegistry) {
    this.llmRouter = llmRouter;
    this.toolRegistry = toolRegistry;
    this.registerBuiltinHandlers();
  }

  private registerBuiltinHandlers() {
    this.handlers.set('trigger', async (node, ctx) => {
      return ctx.triggerData; // Pass-through trigger data
    });

    this.handlers.set('llm_call', async (node, ctx) => {
      const prompt = this.resolveTemplate(
        node.config.prompt as string,
        ctx.variables,
      );
      const response = await this.llmRouter.chat([
        { role: 'system', content: node.config.systemPrompt as string || '' },
        { role: 'user', content: prompt },
      ]);
      return response.content;
    });

    this.handlers.set('condition', async (node, ctx) => {
      const expression = this.resolveTemplate(
        node.config.expression as string,
        ctx.variables,
      );
      // Safe evaluation — no eval()
      return this.evaluateCondition(expression, ctx.variables);
    });

    this.handlers.set('transform', async (node, ctx) => {
      const input = ctx.variables[node.config.inputVar as string];
      const template = node.config.template as string;
      return this.resolveTemplate(template, { ...ctx.variables, input });
    });

    this.handlers.set('http_request', async (node, ctx) => {
      const url = this.resolveTemplate(node.config.url as string, ctx.variables);
      const method = node.config.method as string || 'GET';

      const response = await fetch(url, {
        method,
        headers: node.config.headers as Record<string, string>,
        body: method !== 'GET' ? JSON.stringify(node.config.body) : undefined,
        signal: AbortSignal.timeout(30_000),
      });

      return response.json();
    });

    this.handlers.set('loop', async (node, ctx) => {
      const items = ctx.variables[node.config.collection as string] as unknown[];
      const results: unknown[] = [];

      for (const item of items) {
        ctx.variables['_current'] = item;
        const loopBody = this.getLoopBody(node, ctx.workflow);
        const result = await this.executeSubgraph(loopBody, ctx);
        results.push(result);
      }

      return results;
    });

    this.handlers.set('code', async (node, ctx) => {
      const code = this.resolveTemplate(node.config.code as string, ctx.variables);
      const sandbox = new VMSandbox();
      return sandbox.execute(code);
    });

    this.handlers.set('set_variable', async (node, ctx) => {
      const name = node.config.variableName as string;
      const value = this.resolveTemplate(
        node.config.value as string,
        ctx.variables,
      );
      ctx.variables[name] = value;
      return value;
    });
  }
}

3. 模板解析

// Variable interpolation: {{variableName}} → actual value
private resolveTemplate(
  template: string,
  variables: Record<string, unknown>,
): string {
  return template.replace(/\{\{(\w+(?:\.\w+)*)\}\}/g, (_, path) => {
    const value = this.getNestedValue(variables, path);
    return value !== undefined ? String(value) : `{{${path}}}`;
  });
}

private getNestedValue(obj: Record<string, unknown>, path: string): unknown {
  return path.split('.').reduce((current: unknown, key: string) => {
    if (current && typeof current === 'object') {
      return (current as Record<string, unknown>)[key];
    }
    return undefined;
  }, obj);
}

例如:

Template: "Summarize this article: {{article.content}}"
Variables: { article: { content: "AI is transforming..." } }
Result:   "Summarize this article: AI is transforming..."

4. 總結

  • DAG模型 — 節點+邊,不允許循環
  • 16 種節點類型 — 從 LLM 呼叫到 HTTP 請求
  • 模板解析度 — {{variable}} 插值法
  • 沙盒代碼 — 自訂程式碼節點的虛擬機器隔離
  • 可組合 — 可重複使用的子工作流程

下一篇文章: 工作流程驗證與執行 — 運行端對端工作流程。