(最后更新: 2026-04-20T10:50:00+08:00) Agent Workflow

AI 自动化项目为什么跑不稳?用日志、重试和回退把 Agent workflow 排查清楚

Agent workflow 跑不稳时,不要只调 prompt。更有效的排查方式是给每一步加日志、记录输入输出、区分模型判断错误和工具调用错误,并为关键步骤设计重试、人工确认和回退路径。这篇文章给一套可复制的排障流程和 TypeScript 示例。

#Agent Workflow#AI 自动化#Workflow Debug#日志#重试#回退#TypeScript

需要继续找相关内容?

如果你想继续查工具名、术语、对比页或相关问题,可以直接搜全站,不用回到博客列表页重找。

Quick Summary

核心结论

Agent workflow 跑不稳时,优先补日志、状态记录、重试策略、人工确认和回退路径,而不是只继续调 prompt。

适合谁看

适合已经搭出 AI 自动化流程,但遇到偶发失败、结果漂移、重复运行不一致、API 成功但产物不可用等问题的开发者和小团队。

关键判断

排障时要先判断失败来自输入、模型判断、工具调用、下游 API、状态写入还是人工确认缺失。不同失败来源需要不同处理方式。

下一步建议

如果你的流程还没有显式步骤和状态记录,先补最小 WorkflowRun 结构,再逐步加日志、重试和回退。

你将学到

  • + 怎样给 Agent workflow 设计最小可排障状态结构
  • + 如何用 TypeScript 包一层 runStep 日志
  • + 什么时候应该重试,什么时候应该直接转人工
  • + 如何区分输入问题、模型问题、工具调用问题和下游 API 问题
  • + 为什么没有回退机制的自动化流程很容易越修越乱

很多 Agent workflow demo 第一次跑通时,看起来很顺。
但一旦开始真实运行,就会出现这些问题:

  • 今天能跑,明天不行
  • 同一条输入重复运行,结果不一样
  • API 返回成功,但产物不能用
  • 模型判断偶尔跑偏
  • 出错后不知道该重试、停止还是人工接管

这时不要只盯着 prompt。
真正该补的,往往是日志、状态、重试和回退。

先给结论

Agent workflow 跑不稳时,按这个顺序排查:

  1. 先给每次运行一个 runId
  2. 给每一步记录 stepstatusstartedAtendedAt
  3. 保存输入摘要,而不是只保存最终结果
  4. 区分输入错误、模型错误、工具错误和下游错误
  5. 只对低风险、幂等步骤做自动重试
  6. 给高风险步骤加人工确认
  7. 给写入型动作设计回退路径

这比继续堆 prompt 更容易让 workflow 变稳。

最小状态结构:先知道流程跑到哪一步

如果一个 workflow 出错后,你只能看到一句“失败了”,那就很难排障。

先给每次运行一个最小结构:

type WorkflowStatus = 'pending' | 'running' | 'failed' | 'completed' | 'needs_review';

type WorkflowRun = {
  id: string;
  inputHash: string;
  currentStep: string;
  status: WorkflowStatus;
  startedAt: string;
  endedAt?: string;
  error?: string;
};

如果你需要记录每一步,再加一个 WorkflowStepLog

type WorkflowStepLog = {
  runId: string;
  step: string;
  status: 'started' | 'completed' | 'failed' | 'skipped';
  startedAt: string;
  endedAt?: string;
  inputPreview?: string;
  outputPreview?: string;
  errorName?: string;
  errorMessage?: string;
};

这两个结构不复杂,但能回答最关键的问题:

  • 哪次运行失败了
  • 失败在哪一步
  • 输入是不是变了
  • 错误是哪个环节抛出来的
  • 是否需要人工接管

给每一步包一层日志

最小可用的 TypeScript 包装函数可以这样写:

async function runStep<T>(
  runId: string,
  step: string,
  fn: () => Promise<T>
): Promise<T> {
  const startedAt = new Date().toISOString();
  console.log(JSON.stringify({
    level: 'info',
    event: 'workflow.step.started',
    runId,
    step,
    startedAt,
  }));

  try {
    const result = await fn();
    console.log(JSON.stringify({
      level: 'info',
      event: 'workflow.step.completed',
      runId,
      step,
      endedAt: new Date().toISOString(),
    }));
    return result;
  } catch (error) {
    console.error(JSON.stringify({
      level: 'error',
      event: 'workflow.step.failed',
      runId,
      step,
      endedAt: new Date().toISOString(),
      errorName: error instanceof Error ? error.name : 'UnknownError',
      errorMessage: error instanceof Error ? error.message : String(error),
    }));
    throw error;
  }
}

然后把 workflow 写成显式步骤:

async function runWorkflow(input: string) {
  const runId = crypto.randomUUID();

  const normalized = await runStep(runId, 'normalize-input', async () => {
    return input.trim();
  });

  const plan = await runStep(runId, 'create-plan', async () => {
    return createPlanWithModel(normalized);
  });

  const result = await runStep(runId, 'execute-plan', async () => {
    return executePlan(plan);
  });

  await runStep(runId, 'validate-result', async () => {
    return validateResult(result);
  });

  return result;
}

这样排障时,你不再只知道“AI 自动化失败”,而是知道:

runId=xxx 在 execute-plan 失败

这就是从“猜问题”进入“定位问题”。

输入摘要:不要只记录原文

很多 workflow 的问题来自输入变化。
但直接保存完整输入可能有隐私、体积和安全问题。

更稳的是保存摘要:

async function hashInput(input: string): Promise<string> {
  const data = new TextEncoder().encode(input);
  const digest = await crypto.subtle.digest('SHA-256', data);
  return Array.from(new Uint8Array(digest))
    .map((byte) => byte.toString(16).padStart(2, '0'))
    .join('');
}

再保存一小段可读预览:

function preview(input: string, maxLength = 200) {
  return input.length > maxLength
    ? `${input.slice(0, maxLength)}...`
    : input;
}

日志里记录:

const inputHash = await hashInput(input);
const inputPreview = preview(input);

这样你能判断:

  • 同一个输入是否真的重复失败
  • 用户是不是悄悄换了输入
  • 某类输入是否更容易触发问题

错误分类:不要把所有失败都叫模型不稳

排查时先把错误分成 6 类。

失败来源典型表现优先处理
输入问题字段缺失、格式变化、空值加 schema 校验
模型判断问题分类错、计划跑偏、输出不稳定改提示词或加约束
工具调用问题命令失败、文件不存在、权限不足修工具参数和环境
下游 API 问题超时、429、500、返回结构变了加重试和降级
状态写入问题重复写、漏写、顺序错加幂等键和事务边界
人工确认缺失自动发布错内容、覆盖重要数据加 review gate

很多团队会直接说“模型不稳定”。
但真实排查后,常常发现是输入没有校验、API 偶发超时,或者写入动作没有幂等设计。

输入问题:先加 schema,不要让模型猜

如果输入是结构化数据,先校验:

type TicketInput = {
  title: string;
  description: string;
  priority?: 'low' | 'medium' | 'high';
};

function validateTicketInput(input: unknown): TicketInput {
  if (!input || typeof input !== 'object') {
    throw new Error('Input must be an object');
  }

  const data = input as Record<string, unknown>;
  if (typeof data.title !== 'string' || data.title.trim() === '') {
    throw new Error('Missing title');
  }
  if (typeof data.description !== 'string' || data.description.trim() === '') {
    throw new Error('Missing description');
  }

  return {
    title: data.title,
    description: data.description,
    priority: data.priority === 'high' || data.priority === 'medium' || data.priority === 'low'
      ? data.priority
      : undefined,
  };
}

不要把这种问题交给模型自由发挥。
输入层越稳,后面的 agent 判断才越有意义。

模型判断问题:保存决策理由

如果某一步依赖模型判断,至少让它输出一个结构:

type ModelDecision = {
  decision: 'continue' | 'needs_review' | 'stop';
  reason: string;
  confidence: number;
};

然后检查:

function validateDecision(decision: ModelDecision) {
  if (!['continue', 'needs_review', 'stop'].includes(decision.decision)) {
    throw new Error('Invalid decision');
  }
  if (decision.confidence < 0.7) {
    return { ...decision, decision: 'needs_review' as const };
  }
  return decision;
}

这里的关键不是“相信 confidence 很准确”,而是给流程一个转人工的触发点。

当模型自己都不确定时,不要让 workflow 继续自动写入或发布。

工具调用问题:把命令、参数和工作目录记清楚

如果 workflow 会调用命令行工具,日志里至少记录:

  • command
  • args
  • cwd
  • exitCode
  • stderr 摘要

一个简单包装:

async function runCommand(command: string, args: string[], cwd: string) {
  const { spawn } = await import('node:child_process');

  return new Promise<void>((resolve, reject) => {
    const child = spawn(command, args, { cwd, shell: true });
    let stderr = '';

    child.stderr.on('data', (chunk) => {
      stderr += String(chunk);
    });

    child.on('close', (code) => {
      if (code === 0) {
        resolve();
        return;
      }
      reject(new Error(`Command failed: ${command} ${args.join(' ')}\n${stderr.slice(0, 1000)}`));
    });
  });
}

调用时:

await runStep(runId, 'build-project', async () => {
  await runCommand('npm', ['run', 'build'], process.cwd());
});

这样失败时不会只剩一句“build failed”,而能看到具体命令和错误摘要。

下游 API 问题:只对合适的错误重试

重试不是万能药。
只有这类情况适合自动重试:

  • 网络抖动
  • 429 限流
  • 502 / 503 / 504
  • 短暂超时
  • 幂等读操作或幂等写操作

可以写一个简单的重试函数:

async function withRetry<T>(
  fn: () => Promise<T>,
  options = { retries: 2, delayMs: 800 }
): Promise<T> {
  let lastError: unknown;

  for (let attempt = 0; attempt <= options.retries; attempt += 1) {
    try {
      return await fn();
    } catch (error) {
      lastError = error;
      if (attempt === options.retries) break;
      await new Promise((resolve) => setTimeout(resolve, options.delayMs * (attempt + 1)));
    }
  }

  throw lastError;
}

用在低风险步骤:

const profile = await runStep(runId, 'fetch-profile', async () => {
  return withRetry(() => fetchUserProfile(userId));
});

不要用在高风险动作:

// 不建议盲目重试
await chargeCustomer();
await publishPost();
await deleteRecords();

高风险动作需要幂等键、人工确认或事务设计,不是简单循环重试。

状态写入问题:给写操作加幂等键

如果 workflow 会写数据库、发消息、创建帖子或发布内容,要防止重复执行。

可以设计一个幂等键:

function createIdempotencyKey(runId: string, step: string) {
  return `${runId}:${step}`;
}

写入前先检查:

async function createPostOnce(runId: string, title: string, body: string) {
  const key = createIdempotencyKey(runId, 'create-post');

  const existing = await findPostByIdempotencyKey(key);
  if (existing) return existing;

  return createPost({
    title,
    body,
    idempotencyKey: key,
  });
}

没有幂等设计时,workflow 最怕这种场景:

  1. 帖子其实已经创建成功
  2. 网络返回超时
  3. workflow 以为失败
  4. 自动重试
  5. 创建出重复帖子

这不是模型问题,是工程边界问题。

人工确认:高风险步骤默认不要自动到底

以下步骤建议默认进入人工确认:

  • 对外发布内容
  • 覆盖线上配置
  • 删除数据
  • 向用户发送通知
  • 修改计费、权限或账号状态
  • 生成会被长期使用的知识库记忆

可以把状态设成:

type ReviewRequest = {
  runId: string;
  step: string;
  summary: string;
  proposedAction: string;
  status: 'waiting' | 'approved' | 'rejected';
};

当模型信心不足或动作风险高:

async function requireReview(runId: string, proposedAction: string) {
  return createReviewRequest({
    runId,
    step: 'human-review',
    summary: 'Workflow needs manual approval before publishing.',
    proposedAction,
    status: 'waiting',
  });
}

稳定的 workflow 不是没有人工,而是知道什么时候该让人出现。

回退路径:出问题后能不能回到安全状态

回退设计要在出事前写好。
至少问 4 个问题:

  1. 这一步失败后会不会留下半成品?
  2. 半成品能不能标记为 draft?
  3. 已写入的数据能不能撤销或覆盖?
  4. 如果不能撤销,能不能冻结并转人工?

一个简单的状态机:

type PublishState =
  | 'draft'
  | 'reviewing'
  | 'published'
  | 'rollback_requested'
  | 'rolled_back';

发布前先进入 reviewing,不要直接 published

async function preparePublish(runId: string, content: string) {
  return saveDraft({
    runId,
    content,
    state: 'reviewing',
  });
}

很多自动化事故不是因为不能生成内容,而是因为生成后直接进了不可逆动作。

一个更完整的排障骨架

下面这个骨架适合作为第一版 workflow debug 模板:

type WorkflowContext = {
  runId: string;
  inputHash: string;
  startedAt: string;
};

async function debugFriendlyWorkflow(rawInput: unknown) {
  const inputText = JSON.stringify(rawInput);
  const context: WorkflowContext = {
    runId: crypto.randomUUID(),
    inputHash: await hashInput(inputText),
    startedAt: new Date().toISOString(),
  };

  const input = await runStep(context.runId, 'validate-input', async () => {
    return validateTicketInput(rawInput);
  });

  const decision = await runStep(context.runId, 'model-decision', async () => {
    const result = await classifyTicket(input);
    return validateDecision(result);
  });

  if (decision.decision === 'needs_review') {
    await runStep(context.runId, 'request-review', async () => {
      return requireReview(context.runId, decision.reason);
    });
    return { status: 'needs_review', runId: context.runId };
  }

  const output = await runStep(context.runId, 'execute-action', async () => {
    return withRetry(() => createPostOnce(context.runId, input.title, input.description));
  });

  return {
    status: 'completed',
    runId: context.runId,
    output,
  };
}

它不复杂,但已经具备了几个关键能力:

  • 有 runId
  • 有输入哈希
  • 有步骤日志
  • 有输入校验
  • 有模型决策校验
  • 有人工确认分支
  • 有幂等写入
  • 有低风险重试

这比“一个 prompt 跑到底”的流程稳得多。

一张排障清单

当 workflow 跑不稳时,按这张清单问:

  1. 每次运行有没有唯一 runId
  2. 每一步有没有记录开始、完成和失败?
  3. 输入有没有 hash 或摘要?
  4. 失败发生在哪一步?
  5. 错误是输入、模型、工具、API、状态还是人工确认问题?
  6. 这个步骤是否幂等?
  7. 这个步骤是否适合自动重试?
  8. 如果重试失败,是否会转人工?
  9. 写入动作有没有幂等键?
  10. 关键动作有没有回退路径?

如果这 10 个问题大半答不上来,先不要继续调 prompt。

最后一句判断

Agent workflow 的稳定性,通常不是来自“更长的 prompt”,而是来自更清楚的工程边界:

  • 输入可校验
  • 步骤可观察
  • 失败可分类
  • 重试有限制
  • 人工确认有位置
  • 写入可幂等
  • 出错可回退

这几层补上以后,prompt 优化才有意义。

继续阅读

继续延伸

要点总结

  • - 跑不稳的 workflow 通常不是缺一个更长 prompt,而是缺可观测性
  • - 每一步都应该有 step、status、error 和输入摘要
  • - 只有幂等或低风险步骤适合自动重试
  • - 高风险写入和对外发布必须有人工确认或回退路径
  • - 排障记录应该沉淀成固定日志字段,而不是散落在聊天记录里

常见问题

Agent workflow 偶尔失败,应该先调 prompt 吗?

不建议先调 prompt。先确认失败发生在哪一步、输入是否变化、工具调用是否成功、下游 API 是否返回正常,再决定是否需要改 prompt。

哪些步骤适合自动重试?

适合重试的是幂等、低风险、失败原因可能是网络或临时服务异常的步骤。不适合重试的是支付、发布、删除、覆盖写入等高风险动作。

为什么 API 返回成功,workflow 结果还是不可用?

因为 API 成功只说明调用完成,不代表产物符合业务规则。还需要做字段完整性、格式、状态和人工确认等结果校验。

什么时候应该转人工处理?

当失败原因无法自动分类、输出影响外部用户、会改动重要数据,或重试可能放大错误时,就应该转人工。

评论