基于TypeScript与zod的AI Agent安全执行框架设计与实现
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的“任务说明书”,并负责将其安全、可靠地转化为系统级的操作。它的核心职责包括:
- 输入验证与消毒 : 使用zod对Agent输出的JSON进行严格校验。检查路径是否安全(防止路径遍历攻击)、内容是否合规、参数是否齐全。
- 动作映射与执行 : 维护一个“动作注册表”(Action Registry)。将说明书中的抽象动作(如“创建文件”)映射到具体的、经过审计的执行函数(如一个调用了
fs.writeFile的函数)。这些执行函数是开发者编写的、可信的代码。 - 副作用隔离与回滚 : 在执行具有副作用的操作(如写文件、改数据库)时,Harness应提供隔离机制,例如在临时目录操作,或支持事务性操作。更高级的实现可以设计补偿动作(Compensation Action),用于任务失败时的回滚。
- 进度跟踪与状态持久化 : 这是Harness模式的价值核心。 Harness需要维护一个任务状态机,记录每个动作的执行状态(Pending, Running, Success, Failed)。这个状态必须持久化到数据库或文件系统中,即使进程重启,也能恢复任务上下文。它需要向外部暴露进度查询接口。
- 错误处理与重试 : 当某个动作执行失败时,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;
}
}
代码解析与设计要点 :
- 状态驱动 :
TaskState是整个引擎的核心。所有执行行为都围绕更新这个状态对象展开。EventEmitter的使用允许外部监听状态变化(actionStart,actionSuccess,stateUpdate等),这是实现进度跟踪和UI响应的基础。 - 执行器模式 :
actionExecutors注册表是一个关键设计。它将动作类型(字符串)映射到具体的执行函数。这带来了极大的灵活性:- 安全 :只有注册过的、经过审查的动作才会被执行。
- 可扩展 :你可以轻松地通过
registerExecutor方法添加新的动作类型,而无需修改核心引擎。 - 可测试 :每个执行器都是纯函数或接近纯函数,可以独立进行单元测试。
- 错误处理与状态回滚 :每个动作的执行都被
try-catch包裹。失败时,该动作的状态被标记为FAILED并记录错误信息。当前设计是“快速失败”(fail-fast),即一个动作失败整个任务就停止。你也可以修改逻辑,实现“继续执行”(continue-on-error)策略。 - 安全检查 :在
executeCreateFile中,我们演示了基础的安全检查:防止路径遍历(path.resolve和startsWith检查)和防止覆盖已有文件(除非显式指定)。在executeRunShell中,有一个简单的命令黑名单。 在实际生产中,这些安全检查需要根据你的威胁模型极大地加强。 - 持久化与恢复 :
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}`);
}
}
}
提示工程与集成要点 :
- 结构化输出 :通过
response_format: { type: "json_object" }和详细的System Prompt,我们极大地提高了LLM输出合规JSON的概率。将zod Schema转换为JSON Schema并放入提示词中,是一种非常有效的“少样本学习”(Few-shot Learning)方式。 - 后置验证是关键 : 永远不要相信LLM的直接输出! 即使使用了
json_object格式,LLM仍可能产生格式错误或字段缺失的JSON。因此,ActionSpecSchema.safeParse这步验证 必不可少 。它是阻止错误或恶意动作进入执行环节的最后关卡。 - 错误处理与降级 :当LLM输出无效动作时,我们有多种策略:记录警告并跳过、让整个任务失败、或者尝试调用LLM进行修复。代码中展示了“跳过并记录”的策略,适用于非关键动作。对于关键任务,更安全的做法是直接失败,并通知用户“规划失败”。
- 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存储状态。但需要注意数据持久化策略,避免重启丢失。
- 关系型数据库(如PostgreSQL) :适合复杂查询和事务。可以设计
-
状态查询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、内存、网络和进程数。
- 绝对禁止 :永远不要在Harness中直接执行未经清洗的、用户或LLM提供的命令。即使是“安全”的命令列表也可能被绕过(如
-
网络请求 :
- 限制可访问的域名或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格式错误或字段缺失。 - 排查 :
- 检查提示词 :确保System Prompt中清晰地描述了JSON结构。尝试在提示词中提供1-2个完整的示例(Few-shot)。
- 使用更强的模型 :GPT-3.5-turbo在复杂JSON输出上稳定性不如GPT-4。如果关键任务,升级模型是值得的。
- 降低Temperature :将
temperature设为0.1或0,减少输出的随机性。 - 启用JSON Mode :确保在API调用中设置了
response_format: { type: "json_object" }(OpenAI API支持)。 - 后处理与修复 :如果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卡住。 - 解决方案 :
- 设置超时 :像示例中一样,为
execAsync提供timeout参数。这是第一道防线。 - 超时处理 :在
executeRunShell中,execAsync超时会抛出错误,被上层catch住,动作状态标记为FAILED。你需要决定是重试、跳过还是让整个任务失败。 - 使用AbortController :对于更精细的控制,可以使用Node.js的
AbortController来发送中止信号。 - 进程隔离 :考虑将每个动作放在独立的子进程甚至独立的容器中执行,这样即使某个动作卡死,也不会影响Harness主进程的状态管理和进度汇报。
- 设置超时 :像示例中一样,为
5.4 如何调试复杂的动作执行逻辑?
- 启用详细日志 :在Harness的每个关键阶段(开始验证、开始执行、执行成功、执行失败)都输出结构化日志。日志应包含任务ID、动作索引、动作类型和时间戳。
- 保存输入输出 :对于每个动作,除了在
ActionState中保存output和error,还可以考虑将LLM原始的params和执行器接收到的已验证后的params都记录下来。这在排查LLM输出歧义时非常有用。 - 提供“干跑”模式 :实现一个
dryRun选项。当启用时,Harness会执行所有验证和规划步骤,但跳过实际的文件写入、Shell命令执行等副作用操作。它只模拟执行并返回每个动作将会做什么。这对于在安全环境中测试LLM生成的任务计划至关重要。
5.5 性能瓶颈与扩展性
- 症状 :当同时运行数百个任务时,内存占用高,或数据库连接成为瓶颈。
- 优化方向 :
- 异步与并发 :
TaskHarness本身是顺序执行动作的。但如果任务间无依赖,可以运行多个Harness实例。注意管理共享资源(如文件系统)的竞争。 - 状态存储优化 :避免频繁将整个
TaskState写入数据库。可以只增量更新发生变化的动作状态。 - 队列化 :引入一个任务队列(如Bull、RabbitMQ)。Harness作为“工人”(Worker)从队列中领取任务说明书执行。这样可以轻松实现水平扩展、优先级调度和失败重试。
- 无状态执行器 :考虑将动作执行器本身设计为无状态的、可水平扩展的微服务。Harness核心引擎只负责调度和状态管理,通过RPC或消息队列将动作分发给执行器集群。
- 异步与并发 :
最后一点体会 :引入Harness模式会增加前期的架构复杂度,但它带来的可维护性、安全性和可观测性的提升是巨大的。它迫使你清晰地定义Agent的边界,将不确定的LLM输出转化为确定性的、可跟踪的工作流。当你需要向老板或客户演示一个AI功能时,能清晰地展示出“任务已规划5个步骤,当前第3步正在执行,已完成60%”,远比说“AI正在处理,请稍等…”要可靠得多。这不仅是技术实现,更是一种工程哲学的体现:对AI的能力保持乐观,但对它的输出保持审慎,用确定的系统逻辑去驾驭不确定的智能。
更多推荐

所有评论(0)