s15 - 执行日志
工作流跑起来了,但跑完之后你只知道"成功了"或"失败了"。中间发生了什么?哪个节点最慢?哪个节点出了错?输入输出是什么? 这一章要做的事情,是把每一次执行的完整过程记录下来。
这一章要解决什么问题
s14 的工作流编辑器能画流程、能导入导出、能运行。但运行完之后,你拿到的只是一个最终结果。
如果结果不对,你怎么排查?你知道是哪个节点出了问题吗?你知道传给 LLM 的 prompt 最终长什么样吗?你知道条件判断走了哪个分支吗?
没有执行日志的工作流编辑器,就像没有 print 的代码——能跑,但你不知道它在干什么。
这一章的目标:
- 记录每个节点的输入、输出、耗时、状态
- 保留历史执行记录,支持对比
- 用时间线可视化执行流程
- 错误发生时,能看到完整的堆栈信息
- 把日志导出为 JSON,方便离线分析
Logger 设计
核心是一个 ExecutionLogger 类。每次运行工作流时创建一个实例,它负责记录整个执行过程。
export class ExecutionLogger {
private run: ExecutionRun;
private listeners: Array<(run: ExecutionRun) => void> = [];
constructor(workflowId: string, workflowName: string) {
this.run = {
id: `run_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
workflowId,
workflowName,
status: "running",
startedAt: Date.now(),
finishedAt: 0,
duration: 0,
nodeLogs: [],
};
}
}每个节点的生命周期用三个方法记录:
// 节点开始执行
logger.startNode(nodeId, nodeType, input);
// 执行成功
logger.finishNode(nodeId, output);
// 执行失败
logger.failNode(nodeId, { message, stack });为什么是三个方法而不是一个?因为节点执行是异步的,开始和结束之间可能隔了几秒(LLM 调用)。你需要在开始时就记录状态,这样 UI 才能显示"正在执行"的动画。
数据结构
一次执行产生一棵日志树:
ExecutionRun
├── id, workflowId, workflowName
├── status: "running" | "success" | "error"
├── startedAt, finishedAt, duration
└── nodeLogs: NodeLog[]
├── nodeId, nodeType
├── status: "pending" | "running" | "success" | "error" | "skipped"
├── input, output // 节点的输入和输出数据
├── error: { message, stack } // 如果失败,记录错误
├── startedAt, finishedAt
└── duration // 毫秒skipped 状态用于条件分支:当上游节点出错时,后续节点全部标记为 skipped,而不是一个个报错。
执行器集成
s14 的执行器是"跑完就扔"——遍历节点、执行、返回结果。现在每一步都要经过 Logger:
for (const node of sorted) {
if (hasError) {
logger.skipNode(node.id, node.type);
continue;
}
logger.startNode(node.id, node.type, nodeInput);
try {
const output = await runNode(node, nodeInput);
context[node.id] = output;
logger.finishNode(node.id, output);
} catch (err) {
hasError = true;
logger.failNode(node.id, {
message: err instanceof Error ? err.message : String(err),
stack: err instanceof Error ? err.stack : undefined,
});
}
}一个关键设计:Logger 有 onUpdate 回调。每次节点状态变化时,UI 都能实时收到通知。这意味着你不需要等执行完才能看到日志——节点一个接一个地变绿(或变红),就像看一场比赛的实时比分。
时间线可视化
ExecutionTimeline 组件把日志渲染成水平条形图:
n_input [==] 12ms
n_llm [========] 234ms
n_tool [============] 456ms
n_output [==] 15ms
|----|----|----|----|----|
0 200 400 600 800ms每根条的颜色代表状态:绿色是成功,红色是失败,黄色是执行中(带脉冲动画),灰色是跳过。条的起始位置和长度按真实时间比例缩放。
点击任意一根条,下方会展开该节点的详细信息:完整的输入数据、输出数据、耗时、时间戳。如果节点报错,错误信息和堆栈都会完整显示。
执行历史
每次运行完成后,记录被存入 historyRuns 数组。历史面板列出所有过去的运行,每条记录显示:
- 运行时间(几点几分几秒)
- 工作流名称
- 执行状态(成功/失败/运行中)
- 节点数量
- 总耗时
点击任意一条历史记录,可以回看那次执行的完整时间线和节点详情。这意味着你可以:
- 对比两次运行,看为什么这次比上次慢
- 找到上次成功的运行,检查输入输出有什么不同
- 把失败的运行导出成 JSON,发给同事排查
导出日志
每条历史记录都可以导出为 JSON 文件。格式就是完整的 ExecutionRun 对象:
{
"id": "run_1748400000_abc123",
"workflowId": "wf_default",
"workflowName": "示例工作流",
"status": "error",
"startedAt": 1748400000000,
"finishedAt": 1748400003500,
"duration": 3500,
"nodeLogs": [
{
"nodeId": "n_input",
"nodeType": "input",
"status": "success",
"input": null,
"output": "示例输入",
"startedAt": 1748400000100,
"finishedAt": 1748400000112,
"duration": 12
},
{
"nodeId": "n_llm",
"nodeType": "llm",
"status": "error",
"input": "示例输入",
"output": null,
"error": {
"message": "LLM 调用失败: 429",
"stack": "Error: LLM 调用失败: 429\n at runLLMNode ..."
},
"startedAt": 1748400000200,
"finishedAt": 1748400003400,
"duration": 3200
}
]
}有了这个 JSON,你可以用任何工具分析:写脚本统计平均耗时、用 jq 按状态过滤、甚至导入到另一个界面做可视化。
节点状态指示
Canvas 里的节点组件现在会读取执行状态。通过 React Context,每个节点都能拿到自己的状态:
const status = useNodeStatus(node.id);状态用一个小圆点表示,在节点左上角:
- 灰色:未执行
- 黄色闪烁:执行中
- 绿色:成功
- 红色:失败
- 深灰:被跳过
连线的颜色也会跟着变——如果源节点执行失败,从它出发的线会变红。一眼就能看出问题出在哪条路径上。
三栏布局
页面现在是三栏:
┌──────────┬────────────────────┬──────────────┐
│ 节点库 │ │ 实时│历史│详情 │
│ │ │ │
│ 属性编辑 │ 工作流画布 │ 执行面板 │
│ │ │ │
│ │ │ 时间线 │
│ │ │ 节点日志 │
│ │ │ 详情面板 │
└──────────┴────────────────────┴──────────────┘右侧面板有三个标签页:
- 实时:当前运行的状态概览,每个节点一行,实时更新
- 历史:所有过去的运行记录,点击查看详情
- 详情:选中运行的时间线 + 节点日志列表 + 选中节点的完整数据
教学边界
这一章只做一件事:记录和展示执行过程。
不涉及:
- 日志持久化到数据库(目前存在内存中,刷新页面就没了)
- 日志搜索和过滤
- 性能分析(瓶颈检测、慢查询告警)
- 多人协作下的日志同步
- 日志量限制和自动清理
这些都在后面的章节里。
Phase 3 完结:你有了一个工作流引擎
从 s11 到 s15,你搭建了一个完整的工作流编排系统:
| 章节 | 你加了什么 | 系统有了什么能力 |
|---|---|---|
| s11 | 画布 + 节点 | 用拖拽定义工作流 |
| s12 | 连线 + 执行 | 节点之间传递数据,按顺序执行 |
| s13 | 条件分支 | 工作流能做判断,走不同路径 |
| s14 | 导入导出 | 工作流可保存、可分享、可复用 |
| s15 | 执行日志 | 每次执行可追溯、可调试、可分析 |
这不再是一个"画个流程图跑一下"的 demo。它是一个可以真正用于生产的工作流引擎——你能定义流程、执行它、看到每一步发生了什么、出了问题能定位到具体节点。
回顾整个教程:
- Phase 1(s01-s05):你理解了 Agent 的核心——模型调工具、工具返回结果、结果送回模型
- Phase 2(s06-s10):你把它做成了产品——聊天界面、流式输出、Markdown 渲染、工具调用可视化
- Phase 3(s11-s15):你跳出了对话模式——用画布编排多步骤流程,让 Agent 按你设计的路线工作
三个阶段,三个视角。从"模型怎么工作"到"用户怎么体验"到"流程怎么编排"。这是一个完整的 Agent 开发心智模型。
试着改改
1. 给日志加过滤
在历史面板加一个状态筛选:只显示成功 / 只显示失败 / 全部。当历史记录变多时,这个功能很关键。
2. 日志持久化
把 historyRuns 存到 localStorage。刷新页面后历史记录不丢失。这是一个很小的改动,但实用性提升很大。
3. 节点高亮动画
当时间线上的某根条被点击时,Canvas 里对应的节点加上高亮边框。帮助用户在时间线和画布之间建立视觉关联。
4. 统计面板
在历史标签页加一个简单的统计:平均执行时间、成功率、最慢节点排名。数据都在 historyRuns 里,做聚合就行。
一句话记住
执行日志不是锦上添花——看不到执行过程的工作流编辑器就是个黑箱,出了问题你只能重跑一遍猜原因。