AI 自动化项目为什么跑不稳?用日志、重试和回退把 Agent workflow 排查清楚
Agent workflow 跑不稳时,不要只调 prompt。更有效的排查方式是给每一步加日志、记录输入输出、区分模型判断错误和工具调用错误,并为关键步骤设计重试、人工确认和回退路径。这篇文章给一套可复制的排障流程和 TypeScript 示例。
需要继续找相关内容?
如果你想继续查工具名、术语、对比页或相关问题,可以直接搜全站,不用回到博客列表页重找。
核心结论
Agent workflow 跑不稳时,优先补日志、状态记录、重试策略、人工确认和回退路径,而不是只继续调 prompt。
适合谁看
适合已经搭出 AI 自动化流程,但遇到偶发失败、结果漂移、重复运行不一致、API 成功但产物不可用等问题的开发者和小团队。
关键判断
排障时要先判断失败来自输入、模型判断、工具调用、下游 API、状态写入还是人工确认缺失。不同失败来源需要不同处理方式。
下一步建议
如果你的流程还没有显式步骤和状态记录,先补最小 WorkflowRun 结构,再逐步加日志、重试和回退。
你将学到
- + 怎样给 Agent workflow 设计最小可排障状态结构
- + 如何用 TypeScript 包一层 runStep 日志
- + 什么时候应该重试,什么时候应该直接转人工
- + 如何区分输入问题、模型问题、工具调用问题和下游 API 问题
- + 为什么没有回退机制的自动化流程很容易越修越乱
很多 Agent workflow demo 第一次跑通时,看起来很顺。
但一旦开始真实运行,就会出现这些问题:
- 今天能跑,明天不行
- 同一条输入重复运行,结果不一样
- API 返回成功,但产物不能用
- 模型判断偶尔跑偏
- 出错后不知道该重试、停止还是人工接管
这时不要只盯着 prompt。
真正该补的,往往是日志、状态、重试和回退。
先给结论
Agent workflow 跑不稳时,按这个顺序排查:
- 先给每次运行一个
runId - 给每一步记录
step、status、startedAt、endedAt - 保存输入摘要,而不是只保存最终结果
- 区分输入错误、模型错误、工具错误和下游错误
- 只对低风险、幂等步骤做自动重试
- 给高风险步骤加人工确认
- 给写入型动作设计回退路径
这比继续堆 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 最怕这种场景:
- 帖子其实已经创建成功
- 网络返回超时
- workflow 以为失败
- 自动重试
- 创建出重复帖子
这不是模型问题,是工程边界问题。
人工确认:高风险步骤默认不要自动到底
以下步骤建议默认进入人工确认:
- 对外发布内容
- 覆盖线上配置
- 删除数据
- 向用户发送通知
- 修改计费、权限或账号状态
- 生成会被长期使用的知识库记忆
可以把状态设成:
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 个问题:
- 这一步失败后会不会留下半成品?
- 半成品能不能标记为 draft?
- 已写入的数据能不能撤销或覆盖?
- 如果不能撤销,能不能冻结并转人工?
一个简单的状态机:
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 跑不稳时,按这张清单问:
- 每次运行有没有唯一
runId? - 每一步有没有记录开始、完成和失败?
- 输入有没有 hash 或摘要?
- 失败发生在哪一步?
- 错误是输入、模型、工具、API、状态还是人工确认问题?
- 这个步骤是否幂等?
- 这个步骤是否适合自动重试?
- 如果重试失败,是否会转人工?
- 写入动作有没有幂等键?
- 关键动作有没有回退路径?
如果这 10 个问题大半答不上来,先不要继续调 prompt。
最后一句判断
Agent workflow 的稳定性,通常不是来自“更长的 prompt”,而是来自更清楚的工程边界:
- 输入可校验
- 步骤可观察
- 失败可分类
- 重试有限制
- 人工确认有位置
- 写入可幂等
- 出错可回退
这几层补上以后,prompt 优化才有意义。
继续阅读
继续延伸
要点总结
- - 跑不稳的 workflow 通常不是缺一个更长 prompt,而是缺可观测性
- - 每一步都应该有 step、status、error 和输入摘要
- - 只有幂等或低风险步骤适合自动重试
- - 高风险写入和对外发布必须有人工确认或回退路径
- - 排障记录应该沉淀成固定日志字段,而不是散落在聊天记录里
常见问题
Agent workflow 偶尔失败,应该先调 prompt 吗?
不建议先调 prompt。先确认失败发生在哪一步、输入是否变化、工具调用是否成功、下游 API 是否返回正常,再决定是否需要改 prompt。
哪些步骤适合自动重试?
适合重试的是幂等、低风险、失败原因可能是网络或临时服务异常的步骤。不适合重试的是支付、发布、删除、覆盖写入等高风险动作。
为什么 API 返回成功,workflow 结果还是不可用?
因为 API 成功只说明调用完成,不代表产物符合业务规则。还需要做字段完整性、格式、状态和人工确认等结果校验。
什么时候应该转人工处理?
当失败原因无法自动分类、输出影响外部用户、会改动重要数据,或重试可能放大错误时,就应该转人工。