1. 项目概述:为什么“别让LLM写文件”成了Agent开发的第一课?

如果你正在用大语言模型(LLM)构建AI Agent,并且尝试过让它直接生成代码、配置文件,甚至操作系统的脚本,那你大概率踩过同一个坑:Agent信誓旦旦地告诉你文件已经写好了,结果你一看,目录空空如也,或者文件内容是一堆无法解析的乱码。这不仅仅是LLM“幻觉”的问题,更是我们在设计Agent工作流时,对“动作”和“状态”的边界认知模糊所导致的。今天要聊的“Harness方式”,正是解决这个痛点的核心方法论。它不是一个具体的工具,而是一种工程思想: 将Agent的“决策”与系统的“执行”进行安全、可控的分离,并通过强类型和进度跟踪来确保整个过程的可靠性。

简单来说, “别让LLM写文件” 是一个形象的比喻,其本质是**“别让LLM直接执行任何具有副作用的、不可逆的、或依赖精确环境状态的操作”**。写文件只是其中最典型的一个例子,其他类似操作还包括:直接执行Shell命令、修改数据库、调用未经验证的外部API等。LLM作为一个基于概率生成文本的模型,它擅长规划和描述,但在精确执行层面极其不可靠。Harness(我们可以理解为“缰绳”或“安全执行器”)就是套在LLM这匹“野马”上的控制器,确保它的想法能以安全、可预测的方式落地。

为什么TypeScript和zod在这个上下文中如此重要?因为Harness方式的核心是 契约(Contract) 类型安全(Type Safety) 。TypeScript提供了静态类型检查,让Agent输出的“行动计划”在编译阶段就能发现结构错误;而zod作为一个运行时验证库,能对LLM输出的、不确定的JSON进行严格的校验和类型推断,确保传递给执行器的数据是100%符合预期的。这二者结合,构成了Harness的“安全护栏”。

本文适合所有正在或计划将LLM应用于生产级Agent开发的工程师、架构师。无论你是用LangChain、LangGraph、Dify Workflow还是自研框架,理解并实施Harness模式,都能让你的Agent从“玩具”升级为“工具”,真正承担起自动化任务的责任。接下来,我将拆解如何设计一个具备进度跟踪能力的Harness系统,并分享在TypeScript全栈环境中落地这一模式的关键细节与避坑指南。

2. 核心架构:理解Harness与Agent的职责分离

要正确实施Harness模式,首先必须从架构上厘清Agent和Harness的职责。一个常见的误区是将LLM Agent视为一个“黑盒”,让它从思考到执行全权负责。这种架构在原型阶段或许可行,但一旦涉及真实环境,其脆弱性就会暴露无遗。

2.1 Agent的职责:规划与生成“任务说明书”

在这个模式下,Agent(即LLM)的职责被严格限定为**“战略规划师”和“任务描述者”**。它的核心输出不再是可执行代码或直接的操作,而是一份结构化的、声明式的“任务说明书”(Task Specification)。这份说明书应该包含:

  • 意图(Intent) : 要达成的业务目标,例如“创建用户登录模块的React组件”。
  • 动作序列(Action Sequence) : 为达成目标需要执行的一系列离散动作,例如 [“创建文件”, “写入代码”, “安装依赖”]
  • 动作参数(Action Parameters) : 每个动作所需的精确数据。对于“创建文件”动作,参数必须包括 filePath (完整路径)、 content (文件内容字符串)等。
  • 依赖与约束(Dependencies & Constraints) : 任务执行的前提条件,例如“需要Node.js环境”、“需要先执行数据库迁移”。

关键设计点 :Agent输出的这份“说明书”必须是纯粹的、无副作用的JSON数据。它只描述“要做什么”,绝不包含“如何去做”的具体实现代码(如 fs.writeFileSync )。LLM非常擅长生成这种结构化的任务描述,因为它本质上是将自然语言指令“翻译”成一种更形式化的中间表示。

2.2 Harness的职责:安全执行与进度跟踪

Harness则是**“战术执行器”和“状态管理器”**。它接收来自Agent的“任务说明书”,并负责将其安全、可靠地转化为系统级的操作。它的核心职责包括:

  1. 输入验证与消毒 : 使用zod对Agent输出的JSON进行严格校验。检查路径是否安全(防止路径遍历攻击)、内容是否合规、参数是否齐全。
  2. 动作映射与执行 : 维护一个“动作注册表”(Action Registry)。将说明书中的抽象动作(如“创建文件”)映射到具体的、经过审计的执行函数(如一个调用了 fs.writeFile 的函数)。这些执行函数是开发者编写的、可信的代码。
  3. 副作用隔离与回滚 : 在执行具有副作用的操作(如写文件、改数据库)时,Harness应提供隔离机制,例如在临时目录操作,或支持事务性操作。更高级的实现可以设计补偿动作(Compensation Action),用于任务失败时的回滚。
  4. 进度跟踪与状态持久化 这是Harness模式的价值核心。 Harness需要维护一个任务状态机,记录每个动作的执行状态(Pending, Running, Success, Failed)。这个状态必须持久化到数据库或文件系统中,即使进程重启,也能恢复任务上下文。它需要向外部暴露进度查询接口。
  5. 错误处理与重试 : 当某个动作执行失败时,Harness不能直接崩溃。它需要捕获异常,更新任务状态,并根据预定义的策略(如“重试3次”)决定后续行为,或将错误信息格式化后反馈给上层系统或人工处理。

两者的关系类比 : 你可以把Agent想象成建筑设计师,他画出详细、标准的施工蓝图(任务说明书)。而Harness是项目经理和施工队,他理解蓝图,准备材料(验证输入),指挥各个工种的工人(执行函数)按步骤施工,并每天记录工程进度(状态跟踪),处理施工中遇到的意外问题(错误处理)。设计师绝不亲自去砌砖,施工队也绝不随意修改设计。

2.3 为什么TypeScript和zod是黄金组合?

在实现这一架构时,TypeScript和zod从不同层面提供了保障:

  • TypeScript:编译时类型安全 。我们可以为“任务说明书”定义清晰的接口(Interface)。

    interface TaskSpec {
      id: string;
      intent: string;
      actions: Array<{
        type: 'CREATE_FILE' | 'RUN_SHELL' | 'HTTP_REQUEST'; // 联合类型限定动作类型
        params: Record<string, any>; // 具体参数由zod细化
      }>;
    }
    

    这样,在开发Harness的执行函数时,我们可以获得完美的IDE自动补全和类型提示。

  • zod:运行时数据验证 。LLM的输出不可信,我们必须假设它可能返回任何格式的JSON。zod的作用是在运行时构建一道坚固的防线。

    import { z } from 'zod';
    
    const CreateFileActionSchema = z.object({
      type: z.literal('CREATE_FILE'), // 精确匹配字面量
      params: z.object({
        filePath: z.string().min(1).refine(p => !p.includes('..'), 'Path traversal not allowed'),
        content: z.string(),
        encoding: z.enum(['utf8', 'base64']).default('utf8')
      })
    });
    
    // 验证LLM的输出
    const parsedAction = CreateFileActionSchema.safeParse(llmOutput);
    if (!parsedAction.success) {
      // 优雅地处理验证错误,而不是直接崩溃
      console.error('Invalid action from LLM:', parsedAction.error.format());
      return;
    }
    // 此时,parsedAction.data 的类型已被TypeScript推断为强类型
    

    zod的 safeParse 方法避免了抛出异常,让我们可以更优雅地处理LLM的“胡言乱语”。同时,zod的 .refine 方法允许我们添加自定义的安全校验规则。

实操心得 : 在项目初期,就应使用zod为所有可能的Action定义Schema。这不仅是验证工具,更成为了项目的“动作契约”文档。当LLM输出的动作不符合Schema时,错误信息可以非常清晰地指出是哪个字段出了问题,这比直接调试LLM的原始输出高效得多。

3. 实现详解:构建一个带进度跟踪的TypeScript Harness

理论清晰后,我们着手实现一个最小可行但功能完整的Harness系统。我们将它命名为 TaskHarness

3.1 定义核心类型与状态

首先,定义整个系统的核心数据类型。这是保证类型安全的基石。

// types.ts
import { z } from 'zod';

// 1. 定义所有支持的动作类型及其参数Schema
export const ActionSchemas = {
  CREATE_FILE: z.object({
    filePath: z.string(),
    content: z.string(),
    overwrite: z.boolean().default(false),
  }),
  RUN_SHELL: z.object({
    command: z.string(),
    cwd: z.string().optional(),
    timeout: z.number().optional(),
  }),
  // ... 可以扩展更多动作,如 HTTP_REQUEST, SQL_QUERY 等
};

export type ActionType = keyof typeof ActionSchemas;

// 2. 定义单个动作的描述,这是Agent输出的单元
export const ActionSpecSchema = z.discriminatedUnion('type', [
  z.object({ type: z.literal('CREATE_FILE'), params: ActionSchemas.CREATE_FILE }),
  z.object({ type: z.literal('RUN_SHELL'), params: ActionSchemas.RUN_SHELL }),
]);
export type ActionSpec = z.infer<typeof ActionSpecSchema>;

// 3. 定义任务说明书
export interface TaskSpec {
  id: string; // 任务唯一ID
  intent: string; // 任务描述
  actions: ActionSpec[]; // 有序的动作列表
}

// 4. 定义动作执行状态
export type ActionStatus = 'PENDING' | 'RUNNING' | 'SUCCESS' | 'FAILED' | 'CANCELLED';

export interface ActionState {
  spec: ActionSpec; // 动作描述
  status: ActionStatus;
  startedAt?: Date;
  finishedAt?: Date;
  output?: any; // 执行成功后的输出
  error?: string; // 执行失败的错误信息
}

// 5. 定义任务整体状态
export interface TaskState {
  spec: TaskSpec;
  currentActionIndex: number; // 当前执行到第几个动作
  actionStates: ActionState[];
  status: 'PENDING' | 'RUNNING' | 'COMPLETED' | 'FAILED';
  createdAt: Date;
  updatedAt: Date;
}

这个类型系统清晰地勾勒出了从任务规划到执行状态的全貌。 z.discriminatedUnion 是zod的一个强大特性,它根据 type 字段自动选择对应的 params Schema进行验证,完美匹配我们的动作类型定义。

3.2 实现Harness核心引擎

接下来,实现Harness的核心类 TaskHarness 。它负责状态管理和动作执行调度。

// TaskHarness.ts
import { EventEmitter } from 'events';
import { TaskSpec, TaskState, ActionState, ActionStatus, ActionSpecSchema } from './types';
import * as fs from 'fs/promises';
import * as path from 'path';
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);

export class TaskHarness extends EventEmitter {
  private taskState: TaskState;
  private isRunning: boolean = false;
  // 动作执行器映射表
  private actionExecutors: Record<string, (params: any) => Promise<any>> = {};

  constructor(spec: TaskSpec) {
    super();
    this.taskState = {
      spec,
      currentActionIndex: 0,
      actionStates: spec.actions.map(spec => ({ spec, status: 'PENDING' })),
      status: 'PENDING',
      createdAt: new Date(),
      updatedAt: new Date(),
    };
    this.registerDefaultExecutors();
  }

  // 注册默认的内置动作执行器
  private registerDefaultExecutors() {
    this.registerExecutor('CREATE_FILE', this.executeCreateFile.bind(this));
    this.registerExecutor('RUN_SHELL', this.executeRunShell.bind(this));
  }

  // 允许外部注册自定义执行器
  public registerExecutor(actionType: string, executor: (params: any) => Promise<any>) {
    this.actionExecutors[actionType] = executor;
  }

  // 核心执行方法
  public async run(): Promise<TaskState> {
    if (this.isRunning) {
      throw new Error('Task is already running');
    }
    this.isRunning = true;
    this.taskState.status = 'RUNNING';
    this.updateState();

    try {
      for (let i = 0; i < this.taskState.actionStates.length; i++) {
        this.taskState.currentActionIndex = i;
        const actionState = this.taskState.actionStates[i];

        // 更新动作为执行中
        actionState.status = 'RUNNING';
        actionState.startedAt = new Date();
        this.updateState();
        this.emit('actionStart', actionState);

        try {
          // 1. 验证动作规格(二次校验,确保安全)
          const validatedSpec = ActionSpecSchema.parse(actionState.spec);
          
          // 2. 获取并执行对应的执行器
          const executor = this.actionExecutors[validatedSpec.type];
          if (!executor) {
            throw new Error(`No executor registered for action type: ${validatedSpec.type}`);
          }

          const output = await executor(validatedSpec.params);
          
          // 3. 执行成功,更新状态
          actionState.status = 'SUCCESS';
          actionState.output = output;
          actionState.finishedAt = new Date();
          this.emit('actionSuccess', actionState);

        } catch (error: any) {
          // 4. 执行失败,更新状态
          actionState.status = 'FAILED';
          actionState.error = error.message;
          actionState.finishedAt = new Date();
          this.updateState();
          this.emit('actionError', actionState);
          
          // 任务整体失败
          this.taskState.status = 'FAILED';
          this.updateState();
          throw error; // 或根据策略决定是否继续
        }
        this.updateState();
      }
      // 所有动作成功完成
      this.taskState.status = 'COMPLETED';
      this.updateState();
      this.emit('taskComplete', this.taskState);
    } finally {
      this.isRunning = false;
    }
    return this.taskState;
  }

  // 具体的动作执行器实现
  private async executeCreateFile(params: { filePath: string; content: string; overwrite?: boolean }): Promise<void> {
    const { filePath, content, overwrite = false } = params;
    
    // 安全检查:防止路径遍历攻击
    const resolvedPath = path.resolve(filePath);
    if (!resolvedPath.startsWith(process.cwd())) {
      throw new Error(`Unsafe file path: ${filePath}. Must be within current working directory.`);
    }

    // 检查文件是否存在
    if (!overwrite) {
      try {
        await fs.access(resolvedPath);
        throw new Error(`File already exists at ${resolvedPath}. Use 'overwrite: true' to replace it.`);
      } catch (err: any) {
        // 文件不存在,继续
        if (err.code !== 'ENOENT') throw err;
      }
    }

    // 创建目录(如果不存在)
    await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
    
    // 写入文件
    await fs.writeFile(resolvedPath, content, 'utf-8');
    
    return { path: resolvedPath, size: content.length };
  }

  private async executeRunShell(params: { command: string; cwd?: string; timeout?: number }): Promise<{ stdout: string; stderr: string }> {
    const { command, cwd = process.cwd(), timeout = 30000 } = params;
    
    // 简单的命令黑名单(可根据需要扩展)
    const dangerousPatterns = [/rm\s+-rf/, /mkfs/, /dd\s+if=.*\s+of=/];
    if (dangerousPatterns.some(pattern => pattern.test(command))) {
      throw new Error(`Potentially dangerous command blocked: ${command}`);
    }

    try {
      const { stdout, stderr } = await execAsync(command, { cwd, timeout });
      return { stdout, stderr };
    } catch (error: any) {
      // 将子进程错误信息包装后抛出
      throw new Error(`Command failed: ${error.message}\nStdout: ${error.stdout}\nStderr: ${error.stderr}`);
    }
  }

  // 状态更新辅助方法
  private updateState() {
    this.taskState.updatedAt = new Date();
    this.emit('stateUpdate', this.taskState);
  }

  // 获取当前状态(可用于进度查询)
  public getState(): TaskState {
    return JSON.parse(JSON.stringify(this.taskState)); // 返回深拷贝,防止外部修改
  }

  // 持久化状态到文件(简单示例)
  public async persistState(filePath: string): Promise<void> {
    const state = this.getState();
    await fs.writeFile(filePath, JSON.stringify(state, null, 2), 'utf-8');
  }

  // 从文件恢复状态
  public static async loadFromFile(filePath: string): Promise<TaskHarness> {
    const data = await fs.readFile(filePath, 'utf-8');
    const savedState: TaskState = JSON.parse(data);
    // 注意:需要处理Date对象从字符串的转换
    savedState.createdAt = new Date(savedState.createdAt);
    savedState.updatedAt = new Date(savedState.updatedAt);
    for (const as of savedState.actionStates) {
      if (as.startedAt) as.startedAt = new Date(as.startedAt);
      if (as.finishedAt) as.finishedAt = new Date(as.finishedAt);
    }
    
    const harness = new TaskHarness(savedState.spec);
    harness.taskState = savedState;
    harness.isRunning = savedState.status === 'RUNNING';
    return harness;
  }
}

代码解析与设计要点

  1. 状态驱动 TaskState 是整个引擎的核心。所有执行行为都围绕更新这个状态对象展开。 EventEmitter 的使用允许外部监听状态变化( actionStart , actionSuccess , stateUpdate 等),这是实现进度跟踪和UI响应的基础。
  2. 执行器模式 actionExecutors 注册表是一个关键设计。它将动作类型(字符串)映射到具体的执行函数。这带来了极大的灵活性:
    • 安全 :只有注册过的、经过审查的动作才会被执行。
    • 可扩展 :你可以轻松地通过 registerExecutor 方法添加新的动作类型,而无需修改核心引擎。
    • 可测试 :每个执行器都是纯函数或接近纯函数,可以独立进行单元测试。
  3. 错误处理与状态回滚 :每个动作的执行都被 try-catch 包裹。失败时,该动作的状态被标记为 FAILED 并记录错误信息。当前设计是“快速失败”(fail-fast),即一个动作失败整个任务就停止。你也可以修改逻辑,实现“继续执行”(continue-on-error)策略。
  4. 安全检查 :在 executeCreateFile 中,我们演示了基础的安全检查:防止路径遍历( path.resolve startsWith 检查)和防止覆盖已有文件(除非显式指定)。在 executeRunShell 中,有一个简单的命令黑名单。 在实际生产中,这些安全检查需要根据你的威胁模型极大地加强。
  5. 持久化与恢复 persistState loadFromFile 方法提供了简单的状态持久化能力。这对于长时间运行的任务或需要容错恢复的系统至关重要。更复杂的实现会使用数据库(如PostgreSQL、Redis)来存储状态。

3.3 集成LLM:让Agent生成任务说明书

现在,Harness引擎已经就绪,我们需要让LLM Agent来驱动它。这里的关键是 提示工程(Prompt Engineering) ,引导LLM输出符合我们 TaskSpec ActionSpecSchema 格式的JSON。

// llmAgent.ts
import { OpenAI } from 'openai'; // 或其他LLM SDK
import { TaskSpec, ActionSpec } from './types';
import { zodToJsonSchema } from "zod-to-json-schema";
import { ActionSpecSchema } from './types';

export class TaskPlanningAgent {
  private openai: OpenAI;

  constructor(apiKey: string) {
    this.openai = new OpenAI({ apiKey });
  }

  async planTask(userRequest: string): Promise<TaskSpec> {
    // 1. 将zod Schema转换为JSON Schema,用于提示词中
    const actionSpecJsonSchema = zodToJsonSchema(ActionSpecSchema, "ActionSpec");

    const systemPrompt = `你是一个任务规划AI。你的目标是将用户的自然语言请求,分解成一个结构化的、可执行的任务说明书。
任务说明书包含一系列有序的动作(Action)。每个动作必须有明确的类型(type)和参数(params)。

你可以调用的动作类型有:
- CREATE_FILE: 创建或覆盖文件。参数: filePath (字符串,文件路径), content (字符串,文件内容), overwrite (布尔值,可选,是否覆盖)。
- RUN_SHELL: 在安全环境下运行Shell命令。参数: command (字符串,命令), cwd (字符串,可选,工作目录), timeout (数字,可选,超时毫秒数)。

请严格按照以下JSON Schema格式输出,只输出JSON对象,不要有任何额外的解释或Markdown格式。

动作的JSON Schema约束如下:
${JSON.stringify(actionSpecJsonSchema, null, 2)}

任务说明书的格式是:
{
  "id": "生成一个唯一的UUID",
  "intent": "用户请求的总结",
  "actions": [ ... ] // 一个或多个符合上述Schema的动作对象数组
}

请确保:
1. filePath是相对或绝对路径,但不要包含不安全的路径遍历(如'..')。
2. Shell命令应是安全的,不要包含rm -rf, mkfs等危险操作。
3. 动作顺序是合理的(例如,先创建目录再创建文件,先安装依赖再运行命令)。
`;

    const userPrompt = `用户请求:${userRequest}`;

    try {
      const completion = await this.openai.chat.completions.create({
        model: "gpt-4", // 或 gpt-3.5-turbo,但GPT-4在结构化输出上更可靠
        messages: [
          { role: "system", content: systemPrompt },
          { role: "user", content: userPrompt }
        ],
        temperature: 0.1, // 低温度确保输出稳定、结构化
        response_format: { type: "json_object" }, // 强制JSON输出,OpenAI API支持
      });

      const responseText = completion.choices[0]?.message?.content;
      if (!responseText) {
        throw new Error('LLM returned empty response');
      }

      const parsedTaskSpec: Partial<TaskSpec> = JSON.parse(responseText);
      
      // 2. 基础校验和补全
      if (!parsedTaskSpec.id) {
        parsedTaskSpec.id = `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
      }
      if (!parsedTaskSpec.actions || !Array.isArray(parsedTaskSpec.actions)) {
        throw new Error('LLM did not generate a valid actions array');
      }

      // 3. 使用zod严格验证每一个动作!!!
      // 这是Harness模式安全性的最后一道,也是最关键的防线。
      const validatedActions: ActionSpec[] = [];
      for (const action of parsedTaskSpec.actions) {
        const result = ActionSpecSchema.safeParse(action);
        if (!result.success) {
          console.warn(`Invalid action schema from LLM, skipping:`, result.error.format());
          // 处理策略:可以跳过,可以抛出错误,也可以尝试让LLM重试。
          // 这里选择跳过无效动作,但记录日志。对于关键任务,应该抛出错误。
          continue;
        }
        validatedActions.push(result.data);
      }

      if (validatedActions.length === 0) {
        throw new Error('No valid actions were generated by the LLM.');
      }

      const finalTaskSpec: TaskSpec = {
        id: parsedTaskSpec.id,
        intent: parsedTaskSpec.intent || userRequest,
        actions: validatedActions,
      };

      return finalTaskSpec;

    } catch (error: any) {
      console.error('Failed to plan task with LLM:', error);
      // 优雅降级:可以返回一个兜底的任务,或者抛出一个更友好的错误。
      throw new Error(`Task planning failed: ${error.message}`);
    }
  }
}

提示工程与集成要点

  1. 结构化输出 :通过 response_format: { type: "json_object" } 和详细的System Prompt,我们极大地提高了LLM输出合规JSON的概率。将zod Schema转换为JSON Schema并放入提示词中,是一种非常有效的“少样本学习”(Few-shot Learning)方式。
  2. 后置验证是关键 永远不要相信LLM的直接输出! 即使使用了 json_object 格式,LLM仍可能产生格式错误或字段缺失的JSON。因此, ActionSpecSchema.safeParse 这步验证 必不可少 。它是阻止错误或恶意动作进入执行环节的最后关卡。
  3. 错误处理与降级 :当LLM输出无效动作时,我们有多种策略:记录警告并跳过、让整个任务失败、或者尝试调用LLM进行修复。代码中展示了“跳过并记录”的策略,适用于非关键动作。对于关键任务,更安全的做法是直接失败,并通知用户“规划失败”。
  4. ID生成 :我们为任务生成了一个简单的唯一ID。在生产环境中,应使用更健壮的UUID库(如 uuid )。

3.4 完整工作流示例

将以上所有部分组合起来,形成一个完整的工作流。

// main.ts
import { TaskHarness } from './TaskHarness';
import { TaskPlanningAgent } from './llmAgent';
import * as fs from 'fs/promises';

async function main() {
  // 1. 用户提出请求
  const userRequest = "请帮我创建一个简单的Node.js项目,包含一个index.js文件,内容为console.log('Hello from LLM Harness'),然后在项目根目录运行npm init -y。";

  // 2. LLM Agent 规划任务
  const planner = new TaskPlanningAgent(process.env.OPENAI_API_KEY!);
  let taskSpec;
  try {
    taskSpec = await planner.planTask(userRequest);
    console.log('任务规划成功:', JSON.stringify(taskSpec, null, 2));
  } catch (error) {
    console.error('任务规划失败:', error);
    return;
  }

  // 3. 创建并运行Harness
  const harness = new TaskHarness(taskSpec);

  // 4. 监听进度事件(实现进度跟踪)
  harness.on('stateUpdate', (state) => {
    console.log(`[状态更新] 任务状态: ${state.status}`);
    console.log(`          当前动作: ${state.currentActionIndex + 1}/${state.actionStates.length}`);
    const currentAction = state.actionStates[state.currentActionIndex];
    if (currentAction) {
      console.log(`          动作详情: ${currentAction.spec.type} - ${currentAction.status}`);
    }
  });

  harness.on('actionSuccess', (actionState) => {
    console.log(`[动作成功] ${actionState.spec.type} 完成`);
    if (actionState.output) {
      console.log(`          输出:`, JSON.stringify(actionState.output, null, 2));
    }
  });

  harness.on('actionError', (actionState) => {
    console.error(`[动作失败] ${actionState.spec.type} 失败:`, actionState.error);
  });

  harness.on('taskComplete', (state) => {
    console.log('[任务完成] 所有动作执行完毕!');
  });

  // 5. 持久化状态(例如,每10秒保存一次,用于故障恢复)
  const stateFilePath = `task_state_${taskSpec.id}.json`;
  const saveInterval = setInterval(async () => {
    await harness.persistState(stateFilePath);
    console.log(`[持久化] 状态已保存至 ${stateFilePath}`);
  }, 10000);

  try {
    // 6. 执行任务!
    const finalState = await harness.run();
    console.log('最终任务状态:', finalState.status);
  } catch (error: any) {
    console.error('任务执行过程中失败:', error.message);
  } finally {
    clearInterval(saveInterval);
    // 最终保存一次状态
    await harness.persistState(stateFilePath);
    
    // 7. (可选)清理状态文件
    // await fs.unlink(stateFilePath);
  }
}

main();

运行这个示例,你将看到控制台实时打印出任务进度。LLM可能会生成类似以下的任务说明书:

{
  "id": "task_123456789",
  "intent": "创建Node.js项目并初始化",
  "actions": [
    {
      "type": "CREATE_FILE",
      "params": {
        "filePath": "./my-llm-project/index.js",
        "content": "console.log('Hello from LLM Harness');"
      }
    },
    {
      "type": "RUN_SHELL",
      "params": {
        "command": "npm init -y",
        "cwd": "./my-llm-project"
      }
    }
  ]
}

Harness会依次执行创建文件和运行Shell命令的动作,并全程跟踪和报告每个步骤的状态。

4. 高级话题与生产级考量

上述实现是一个完整的原型。但要用于生产环境,还需要考虑更多方面。

4.1 进度跟踪的存储与查询

内存中的状态在进程退出后会丢失。生产系统需要将 TaskState 持久化到外部存储中。

  • 数据库选型

    • 关系型数据库(如PostgreSQL) :适合复杂查询和事务。可以设计 tasks action_states 两张表,便于按任务ID、状态、时间范围查询。
    • 文档数据库(如MongoDB) TaskState 本身就是一个文档,可以直接存储,查询简单。适合状态结构频繁变化的场景。
    • Redis :如果对读写速度和实时性要求极高,可以用Redis存储状态。但需要注意数据持久化策略,避免重启丢失。
  • 状态查询API :需要暴露REST或GraphQL API,供前端仪表盘或其他服务查询任务进度。

    // 示例:使用Express.js
    app.get('/api/tasks/:taskId', async (req, res) => {
      const state = await db.getTaskState(req.params.taskId);
      if (!state) return res.status(404).json({ error: 'Task not found' });
      // 计算进度百分比
      const totalActions = state.actionStates.length;
      const completedActions = state.actionStates.filter(a => a.status === 'SUCCESS').length;
      const progress = totalActions > 0 ? (completedActions / totalActions) * 100 : 0;
      
      res.json({
        ...state,
        progress,
        estimatedTimeRemaining: calculateETR(state), // 根据历史数据估算
      });
    });
    

4.2 动作执行器的安全强化

Harness模式的安全核心在于动作执行器。必须对每个执行器进行沙盒化(Sandboxing)处理。

  • 文件操作

    • 使用 path.resolve 并检查结果是否在允许的根目录(如一个临时工作区或项目目录)内。
    • 考虑使用虚拟文件系统(memfs)或容器隔离进行高风险操作。
  • Shell命令执行

    • 绝对禁止 :永远不要在Harness中直接执行未经清洗的、用户或LLM提供的命令。即使是“安全”的命令列表也可能被绕过(如 $(rm -rf /) )。
    • 白名单机制 :只允许执行预定义的安全命令模板,LLM只能填充参数。例如,定义一个 NpmInstall 动作,LLM只能提供 packageName 参数,执行器内部拼接成 npm install ${packageName}
    • 容器隔离 :使用Docker或类似技术,在一个干净的、临时的容器中执行命令。任务完成后销毁容器。这是最安全的方式。
    • 资源限制 :使用 ulimit 、cgroups 或容器配置来限制CPU、内存、网络和进程数。
  • 网络请求

    • 限制可访问的域名或IP白名单。
    • 设置超时和重试策略。
    • 对请求和响应体进行大小限制和内容检查。

4.3 错误处理、重试与补偿

  • 分级重试策略 :不是所有错误都值得重试。网络超时可以重试,权限错误则不应重试。可以为每个动作类型配置不同的重试策略(次数、间隔)。
  • 补偿动作(Saga模式) :对于已经成功但后续动作失败的情况,可能需要执行补偿动作来回滚。例如,如果“创建数据库表”成功,但“插入初始数据”失败,则需要一个“删除数据库表”的补偿动作。这需要更复杂的任务状态机和动作定义。
  • 人工审核节点 :对于某些高风险动作(如删除生产数据库),可以在任务流中插入“人工审核”节点。Harness执行到该节点时暂停,通过通知系统(如Slack、邮件)请求人工批准,批准后才继续执行。

4.4 与现有Agent框架集成

你不需要从头造轮子。可以将Harness模式集成到LangChain、LangGraph或Dify中。

  • 在LangChain中作为自定义Tool :将整个 TaskHarness 包装成一个LangChain Tool。LLM通过自然语言调用这个Tool,并传入任务描述。Tool内部调用 TaskPlanningAgent 生成 TaskSpec ,然后执行。这样,Harness就成了LLM的一个超级“多功能工具”。
  • 在LangGraph中作为状态节点 :在LangGraph的图中,可以设计一个“Harness执行”节点。当工作流到达该节点时,将当前上下文传递给Harness执行,并将执行结果返回给图,决定下一个节点。
  • 在Dify Workflow中作为自定义节点 :Dify支持自定义代码节点。你可以将Harness的核心逻辑封装成一个Dify节点,在可视化工作流中拖拽使用。

实操心得 :从简单开始,不要一开始就追求完美的安全性和完备性。先用一个完全受控的环境(如只允许在 /tmp 目录下创建文件)跑通核心流程。然后根据实际遇到的风险和需求,逐步加强安全措施、增加动作类型、完善状态管理。Harness模式的价值在于它的清晰边界和可扩展性,你可以按需迭代每个部分。

5. 常见问题与排查技巧实录

在实际开发和运维中,你会遇到各种问题。以下是一些典型问题及其解决方案。

5.1 LLM不按Schema输出JSON

  • 症状 ActionSpecSchema.safeParse 频繁失败,错误信息显示JSON格式错误或字段缺失。
  • 排查
    1. 检查提示词 :确保System Prompt中清晰地描述了JSON结构。尝试在提示词中提供1-2个完整的示例(Few-shot)。
    2. 使用更强的模型 :GPT-3.5-turbo在复杂JSON输出上稳定性不如GPT-4。如果关键任务,升级模型是值得的。
    3. 降低Temperature :将 temperature 设为0.1或0,减少输出的随机性。
    4. 启用JSON Mode :确保在API调用中设置了 response_format: { type: "json_object" } (OpenAI API支持)。
    5. 后处理与修复 :如果LLM输出的是Markdown包裹的JSON(如 json ... ),需要在解析前先剥离Markdown标记。可以写一个简单的修复函数来尝试提取JSON部分。
  • 根治方案 :接受LLM输出不完美的事实,将后置的zod验证作为 强制关卡 。验证失败时,可以将错误信息和原始请求再次发送给LLM,要求它修正输出。这构成了一个“规划-验证-修正”的循环。

5.2 任务状态持久化后恢复失败

  • 症状 :从文件或数据库加载保存的 TaskState 后, Date 对象变成了字符串,导致类型错误。
  • 解决方案 :在 loadFromFile 方法中,我们已经演示了如何将字符串转换回 Date 对象。如果使用数据库ORM(如Prisma、TypeORM),它们通常支持自动将数据库中的 DateTime 字段映射为JavaScript的 Date 对象。关键是在序列化( JSON.stringify )和反序列化时处理好类型转换。
  • 最佳实践 :在 TaskState 接口中,将所有日期字段定义为 string | Date 类型,并在Harness内部统一转换为 Date 对象使用。存储时,使用ISO格式字符串( toISOString() )。

5.3 动作执行超时或卡死

  • 症状 RUN_SHELL 动作执行一个长时间命令或无响应命令,导致整个Harness卡住。
  • 解决方案
    1. 设置超时 :像示例中一样,为 execAsync 提供 timeout 参数。这是第一道防线。
    2. 超时处理 :在 executeRunShell 中, execAsync 超时会抛出错误,被上层catch住,动作状态标记为 FAILED 。你需要决定是重试、跳过还是让整个任务失败。
    3. 使用AbortController :对于更精细的控制,可以使用Node.js的 AbortController 来发送中止信号。
    4. 进程隔离 :考虑将每个动作放在独立的子进程甚至独立的容器中执行,这样即使某个动作卡死,也不会影响Harness主进程的状态管理和进度汇报。

5.4 如何调试复杂的动作执行逻辑?

  • 启用详细日志 :在Harness的每个关键阶段(开始验证、开始执行、执行成功、执行失败)都输出结构化日志。日志应包含任务ID、动作索引、动作类型和时间戳。
  • 保存输入输出 :对于每个动作,除了在 ActionState 中保存 output error ,还可以考虑将LLM原始的 params 和执行器接收到的已验证后的 params 都记录下来。这在排查LLM输出歧义时非常有用。
  • 提供“干跑”模式 :实现一个 dryRun 选项。当启用时,Harness会执行所有验证和规划步骤,但跳过实际的文件写入、Shell命令执行等副作用操作。它只模拟执行并返回每个动作将会做什么。这对于在安全环境中测试LLM生成的任务计划至关重要。

5.5 性能瓶颈与扩展性

  • 症状 :当同时运行数百个任务时,内存占用高,或数据库连接成为瓶颈。
  • 优化方向
    1. 异步与并发 TaskHarness 本身是顺序执行动作的。但如果任务间无依赖,可以运行多个Harness实例。注意管理共享资源(如文件系统)的竞争。
    2. 状态存储优化 :避免频繁将整个 TaskState 写入数据库。可以只增量更新发生变化的动作状态。
    3. 队列化 :引入一个任务队列(如Bull、RabbitMQ)。Harness作为“工人”(Worker)从队列中领取任务说明书执行。这样可以轻松实现水平扩展、优先级调度和失败重试。
    4. 无状态执行器 :考虑将动作执行器本身设计为无状态的、可水平扩展的微服务。Harness核心引擎只负责调度和状态管理,通过RPC或消息队列将动作分发给执行器集群。

最后一点体会 :引入Harness模式会增加前期的架构复杂度,但它带来的可维护性、安全性和可观测性的提升是巨大的。它迫使你清晰地定义Agent的边界,将不确定的LLM输出转化为确定性的、可跟踪的工作流。当你需要向老板或客户演示一个AI功能时,能清晰地展示出“任务已规划5个步骤,当前第3步正在执行,已完成60%”,远比说“AI正在处理,请稍等…”要可靠得多。这不仅是技术实现,更是一种工程哲学的体现:对AI的能力保持乐观,但对它的输出保持审慎,用确定的系统逻辑去驾驭不确定的智能。

更多推荐