Action Caching & Replay Pattern
Nikola Balic (@nibzard)· emerging
问题
基于LLM的Agent执行成本高昂(涉及金钱成本与延迟成本)且具有非确定性。多次运行同一工作流会产生不同结果,还会重复消耗LLM相关成本。
这会引发以下多方面问题:
- 成本激增:即便执行的是完全相同的任务,每次运行工作流都会消耗LLM令牌
- 非确定性:相同输入在多次运行中会产生不同输出
- 缺乏回归测试能力:无法验证修复操作不会破坏现有工作流
- 迭代缓慢:不支付LLM成本就无法快速测试变更
- 无法集成CI/CD:对Agent工作流开展自动化测试不具备可行性
方案
在执行过程中记录每个操作及其精准元数据(XPaths、框架索引、执行细节),无需调用LLM即可实现确定性重放。即便页面结构发生细微变化,缓存也能保留足够信息以重放操作。
核心方案
操作缓存条目 存储完整的执行元数据:
interface ActionCacheEntry {
stepIndex: number; // 工作流中的执行顺序
instruction: string; // 自然语言描述的操作指令
elementId: string; // 编码后的框架索引-后端节点ID
method: string; // 操作方法:click、fill、type等
arguments: string[]; // 方法参数列表
frameIndex: number; // 针对iframe的框架上下文索引
xpath: string; // 标准化后的元素XPath
actionType: string; // 操作分类
success: boolean; // 执行结果状态
message: string; // 输出信息或错误提示
}
interface ActionCacheOutput {
actions: ActionCacheEntry[];
finalState: {
success: boolean;
error?: string;
duration: number; // 总执行时长
};
}
执行过程中构建缓存条目:
export const buildActionCacheEntry = ({
stepIndex,
action,
actionOutput,
domState,
}: {
stepIndex: number;
action: ActionType;
actionOutput: ActionOutput;
domState: A11yDOMState; // 无障碍DOM状态
}): ActionCacheEntry => {
const instruction = extractInstruction(action); // 提取操作指令
const elementId = extractElementId(action); // 提取元素ID
const method = extractMethod(action); // 提取操作方法
const args = extractArguments(action); // 提取方法参数
const frameIndex = extractFrameIndex(elementId); // 从元素ID中解析框架索引
const xpath = normalizeXPath(
// 优先从DOM状态的XPath映射表获取,备用从调试信息中提取
domState.xpathMap?.[encodedId] || extractXPathFromDebug(actionOutput)
);
return {
stepIndex,
instruction,
elementId,
method,
arguments: args,
frameIndex,
xpath,
actionType: action.type,
success: actionOutput.success,
message: actionOutput.message,
};
};
智能降级的重放机制:
// 基于缓存重放,支持XPath重试与LLM降级备选
const replay = await page.runFromActionCache(cache, {
maxXPathRetries: 3, // 最多重试XPath解析3次
fallbackToLLM: true, // 若XPath解析失败则调用LLM作为备选
debug: true,
});
// 重放流程:
// 1. 直接尝试使用缓存的XPath
// 2. 若失败,尝试标准化后的XPath变体
// 3. 若仍失败,调用LLM解析目标元素
// 4. 将成功解析结果更新至缓存
生成独立可执行脚本:
// 将缓存的工作流导出为TypeScript脚本
const script = generateScriptFromCache(cache);
// 生成的可运行代码示例:
// import { chromium } from 'playwright';
// const page = await chromium.newPage();
// await page.click('div[data-testid="login-button"]');
// await page.fill('input[name="email"]', 'user@example.com');
// ...
架构设计
graph TB
subgraph "首次执行(LLM驱动)"
A[用户请求] --> B[搭载LLM的Agent]
B --> C[操作1:点击登录按钮]
C --> D[构建缓存条目#1]
D --> E[操作2:填写邮箱]
E --> F[构建缓存条目#2]
F --> G[执行完成]
end
subgraph "后续执行(缓存驱动)"
H[用户请求] --> I{是否存在缓存?}
I -->|是| J[加载操作缓存]
J --> K[重放操作1]
K --> L{XPath可用?}
L -->|是| M[重放操作2]
L -->|否| N[LLM降级备选]
N --> M
M --> O{XPath可用?}
O -->|是| P[执行完成 - 零LLM调用成本]
O -->|否| Q[LLM降级备选]
Q --> P
end
style B fill:#f9f,stroke:#333
style P fill:#9f9,stroke:#333
如何使用
1. 启用动作缓存
大多数Agent框架都提供缓存配置项,执行任务时开启即可:
const agent = new HyperAgent(browser);
const cache = await agent.executeTask("登录并导航至仪表盘", {
enableActionCache: true,
});
2. 持久化缓存
将缓存保存以备后续复用:
import fs from 'fs';
fs.writeFileSync(
'workflows/login-cache.json',
JSON.stringify(cache, null, 2)
);
3. 从缓存重放
const savedCache = JSON.parse(
fs.readFileSync('workflows/login-cache.json', 'utf-8')
);
const result = await page.runFromActionCache(savedCache, {
maxXPathRetries: 3,
fallbackToLLM: true,
});
4. CI/CD 集成
// test.js - 自动化回归测试
describe('用户登录流程', () => {
it('应成功完成登录', async () => {
const cache = JSON.parse(readFileSync('workflows/login-cache.json'));
const result = await page.runFromActionCache(cache);
expect(result.finalState.success).toBe(true);
});
});
5. 脚本生成
将工作流导出为独立脚本,用于手动调试或CI环境:
# 从缓存生成TypeScript脚本
npx hyperagent script workflows/login-cache.json > login.test.ts
来源摘要
正在获取来源并生成中文摘要…