UNPKG

@flowlab/all

Version:

A cool library focusing on handling various flows

294 lines (270 loc) 16.9 kB
## Detailed Usage Example (AI Flow - RAG + Function Call) ``` typescript import { WorkflowDefinition, createWorkflowContext, WorkflowExecutor, NodeRegistry, InMemoryEventManager, LocalScheduler, ConsoleLogger, StepType, // 引入 FlowLab 核心组件 // 假设 Core 包提供了一些基础节点: LogNode, // 用于在控制台打印日志的节点 HttpRequestNode, // 用于发起 HTTP 请求的节点 } from '@flowlab/core'; // 引入 @flowlab/ai 包的相关组件 import { aiProviderRegistry, // 全局 AI Provider 注册表实例 loadAIConfigFromFile, // (可选) 从文件加载 AI 配置的辅助函数 LLMCompletionNode, // 用于文本生成的 AI 节点 EmbeddingNode, // 用于生成文本向量嵌入的 AI 节点 FunctionCallingNode, // 用于让 AI 决定是否调用以及如何调用外部函数的节点 aiConditionHelper, // 用于在条件步骤中使用 AI 判断的辅助对象 // 假设存在一个用于向量搜索的节点 (可能在 @flowlab/ai 或其他扩展包中) VectorSearchNode, // 输入: 查询向量, TopK; 输出: 相关文档列表[] } from '@flowlab/ai'; // 假设这些都从 @flowlab/ai 导出 // --- 第 1 步: 初始化与配置 --- // 1.1 加载 AI Provider 的配置 (例如,从环境变量或配置文件) // loadAIConfigFromFile('path/to/ai-config.json'); // 可以取消注释这行,如果你的配置在文件中 aiProviderRegistry.loadGlobalConfigs({ // 使用代码直接加载配置,或者从环境变量读取 'openai': { // 定义名为 'openai' 的 Provider 配置 apiKey: process.env.OPENAI_API_KEY, // 从环境变量安全地读取 API Key // defaultModel: 'gpt-4' // 可以设置默认模型 }, // 'gemini': { apiKey: process.env.GEMINI_API_KEY }, // 可以添加其他 Provider 配置 }); // 注意: 如果你的 Provider (比如自定义的本地模型 Provider) 需要注册,可以在这里调用 aiProviderRegistry.registerProvider(...) // 1.2 设置 FlowLab 核心组件 const logger = new ConsoleLogger('[AI-Flow]'); // 创建日志记录器,方便调试 const eventManager = new InMemoryEventManager(); // 创建内存事件管理器,用于节点间或工作流间通信(本例未使用,但大型应用可能需要) // 创建调度器,如果 AI 任务可能耗时较长,希望异步执行而不阻塞主流程时会用到 (需要配合 BaseAINode 的 scheduleTask 输入) const scheduler = new LocalScheduler(eventManager, logger); // 本地调度器,用于在同一进程中延迟或异步执行 const nodeRegistry = new NodeRegistry(); // 创建节点注册表,用于管理所有可用的节点类型 // --- 第 2 步: 注册所有需要在工作流中使用的节点 --- // 必须将所有会用到的节点类型(无论是 Core 节点还是 AI 节点)注册到 NodeRegistry 中 // 注册 Core 基础节点 nodeRegistry.register(new LogNode()); nodeRegistry.register(new HttpRequestNode()); // 假设这个节点用于调用外部 API // 注册 AI 节点实例 (如果节点需要调度器,请在构造时传入) nodeRegistry.register(new LLMCompletionNode(scheduler)); // 文本生成节点 nodeRegistry.register(new EmbeddingNode(scheduler)); // 向量嵌入节点 nodeRegistry.register(new FunctionCallingNode(scheduler)); // 函数调用决策节点 nodeRegistry.register(new VectorSearchNode()); // 向量搜索节点 (假设它不需要调度器) // --- 第 3 步: 定义包含 RAG 和函数调用的 AI 工作流 --- const ragWorkflow = new WorkflowDefinition('rag-function-call-flow', 'RAG问答与函数调用流程'); // 创建工作流定义,给它一个 ID 和名称 ragWorkflow // 使用链式调用定义工作流的步骤 // 起始步骤: 记录收到的用户查询 .addStep({ id: 'logStart', // 步骤的唯一标识符 nodeId: 'LogNode', // 使用哪个注册的节点类型来执行此步骤 input: { // 传递给 LogNode 的输入参数 // 使用模板字符串动态插入工作流的初始输入 'query' message: "RAG 流程启动。用户查询: ${workflow.input.query}" }, nextStepId: 'generateQueryEmbedding' // 定义下一个要执行的步骤的 ID }) // RAG 步骤 1: 为用户查询生成向量嵌入 (Embedding) .addStep({ id: 'generateQueryEmbedding', nodeId: 'EmbeddingNode', // 使用向量嵌入 AI 节点 input: { // EmbeddingNode 的输入参数 providerName: 'openai', // 指定使用哪个 AI Provider (需要已配置) text: '${workflow.input.query}', // 从工作流初始输入获取查询文本 modelOptions: { model: 'text-embedding-ada-002' } // 指定嵌入模型 (示例) }, outputMapping: { // 将节点的输出映射到工作流变量中,供后续步骤使用 // 将 EmbeddingNode 输出的 embedding 字段,存入名为 'queryEmbedding' 的工作流变量 'workflow.variables.queryEmbedding': 'output.embedding' }, nextStepId: 'searchVectorDB' // 下一步:进行向量搜索 }) // RAG 步骤 2: 使用查询向量在向量数据库中搜索相关文档 .addStep({ id: 'searchVectorDB', nodeId: 'VectorSearchNode', // 使用(假设存在的)向量搜索节点 input: { // 向量搜索节点的输入 queryEmbedding: '${workflow.variables.queryEmbedding}', // 使用上一步生成的查询向量 topK: 3, // 指定返回最相关的 3 个文档 // 此处通常还需要传递向量数据库的连接信息或配置 // vectorDbConfig: { host: '...', indexName: '...' } }, outputMapping: { // 将搜索结果 (假设是文档数组) 存入 'retrievedDocs' 工作流变量 'workflow.variables.retrievedDocs': 'output.documents' // 期望输出格式: [{content: '...', score: 0.9}, ...] }, nextStepId: 'checkDocsFound' // 下一步:检查是否找到了文档 }) // 条件步骤: 检查 RAG 步骤是否找到了相关文档 .addConditionStep({ id: 'checkDocsFound', condition: (context) => // 使用一个函数来评估条件,函数接收当前工作流上下文 // 检查 'retrievedDocs' 变量是否存在且包含元素 (context.variables.retrievedDocs && context.variables.retrievedDocs.length > 0), branches: { // 定义不同条件结果对应的下一个步骤 ID 'true': 'synthesizeAnswer', // 如果条件为 true (找到文档),则跳转到 'synthesizeAnswer' 'false': 'handleNoDocs' // 如果条件为 false (未找到文档),则跳转到 'handleNoDocs' } // 注意:如果 condition 函数需要异步操作(例如调用 AI),需要使用 async 并确保返回 Promise<boolean|string> // condition: async (context) => { ... } }) // RAG 步骤 3a (路径: 找到文档): 使用 LLM 结合用户查询和检索到的文档来生成答案 .addStep({ id: 'synthesizeAnswer', nodeId: 'LLMCompletionNode', // 使用文本生成 AI 节点 input: { // 文本生成节点的输入 providerName: 'openai', // 指定 AI Provider modelOptions: { model: 'gpt-4', temperature: 0.5 }, // 指定模型和参数 // 核心: 构建 Prompt,包含原始查询和检索到的上下文信息 promptTemplate: `用户查询: \${workflow.input.query}\n\n相关上下文:\n${'${workflow.variables.retrievedDocs.map(d => `- ${d.content}`).join("\\n")}'}\n\n请*仅*根据上面提供的上下文来回答用户查询。如果上下文中没有答案,请明确说明。`, // 注意: 上面模板中使用了 JS 模板字符串来动态构建上下文部分。 // 对于复杂的模板处理,可能需要在专门的脚本节点中预处理,或者使用更强大的模板引擎。 // 这里假设 FlowLab 的模板渲染能处理简单的 JS 表达式(如果不能,需要调整)。 }, outputMapping: { // 将 LLM 生成的答案文本存入 'synthesizedAnswer' 工作流变量 'workflow.variables.synthesizedAnswer': 'output.completionText' }, nextStepId: 'decideNextAction' // 下一步:让 AI 决定是否需要额外操作 }) // RAG 步骤 3b (路径: 未找到文档): 处理未找到相关文档的情况 .addStep({ id: 'handleNoDocs', nodeId: 'LogNode', // 可以仅记录日志,或设置一个默认回答 input: { message: "未能根据查询找到相关文档。" }, outputMapping: { // 设置一个默认的、表示未找到答案的文本到 'synthesizedAnswer' 变量 'workflow.variables.synthesizedAnswer': '"抱歉,我无法在知识库中找到您查询的相关信息。"' }, nextStepId: 'decideNextAction' // 即使没找到文档,也继续下一步判断是否需要其他操作 }) // 函数调用步骤 1: 使用 Function Calling 节点让 AI 决定是否需要调用外部函数 (如下单、创建工单等) .addStep({ id: 'decideNextAction', nodeId: 'FunctionCallingNode', // 使用函数调用决策 AI 节点 input: { // 函数调用节点的输入 providerName: 'openai', // 指定 AI Provider modelOptions: { model: 'gpt-4' }, // 指定支持函数调用的模型 // Prompt 指导 AI 基于查询和已生成的答案,判断是否需要创建工单 promptTemplate: `根据用户原始查询 "\${workflow.input.query}" 以及系统提供的答案 "\${workflow.variables.synthesizedAnswer}",请判断是否需要为用户创建一个支持工单。仅当用户看起来不满意、问题未解决或需要进一步协助时才调用函数。`, // 定义可供 AI 选择调用的函数列表 (遵循特定格式,如 OpenAI Function Calling API) functions: [ { name: 'create_support_ticket', // 函数名称 description: '为用户创建一个支持工单。', // 函数描述,供 AI 理解其作用 parameters: { // 函数参数的 JSON Schema 定义 type: 'object', properties: { summary: { type: 'string', description: '用户问题的简短摘要。' }, query: { type: 'string', description: '用户的原始查询文本。' } }, required: ['summary', 'query'] // 必须提供的参数 } } // 可以定义更多函数供 AI 选择 ] }, outputMapping: { // 将 FunctionCallingNode 的整个输出 (包含 functionCall 或 responseText) 存入 'actionDecision' 变量 'workflow.variables.actionDecision': 'output' // 输出结构: { functionCall?: { name: string, arguments: object }, responseText?: string, usage?: ... } }, nextStepId: 'checkFunctionCall' // 下一步:检查 AI 是否要求调用函数 }) // 条件步骤: 检查上一步 AI 是否决定调用函数 .addConditionStep({ id: 'checkFunctionCall', // 检查 'actionDecision' 变量中是否存在 functionCall 字段 condition: (context) => !!context.variables.actionDecision?.functionCall, branches: { 'true': 'callCreateTicketAPI', // 如果 AI 要求调用函数,则跳转到执行 API 调用步骤 'false': 'logFinalAnswer' // 如果 AI 未要求调用函数,则直接记录最终答案 } }) // 函数调用步骤 2a (路径: 需要调用函数): 实际执行外部 API 调用来创建工单 .addStep({ id: 'callCreateTicketAPI', nodeId: 'HttpRequestNode', // 使用通用的 HTTP 请求节点 input: { // HTTP 请求节点的输入 method: 'POST', // 请求方法 url: 'https://api.example.com/support/tickets', // 你的实际工单系统 API 地址 headers: { // 请求头,例如认证信息 'Authorization': 'Bearer YOUR_API_TOKEN', // 替换为你的 API Token 'Content-Type': 'application/json' }, // 从 AI 决定的函数调用的参数中,动态构建请求体 (body) body: { summary: '${workflow.variables.actionDecision.functionCall.arguments.summary}', // 获取 AI 提供的摘要 original_query: '${workflow.variables.actionDecision.functionCall.arguments.query}', // 获取 AI 提供的原始查询 context_answer: '${workflow.variables.synthesizedAnswer}' // 可以将 RAG 生成的答案也一并提交 // 注意: 模板替换需要能正确处理嵌套 JSON 路径 } }, outputMapping: { // 将 API 调用的结果存储起来 'workflow.variables.ticketResult': 'output.data' // 假设 HTTP 节点将响应体放在 output.data 中 }, nextStepId: 'logTicketCreated' // 下一步:记录工单创建结果 }) // 日志步骤 (路径: 工单已创建) .addStep({ id: 'logTicketCreated', nodeId: 'LogNode', input: { // 记录日志,包含 API 的响应信息 (需要确保 ticketResult 是可序列化的) message: "已请求创建支持工单。API 响应: ${JSON.stringify(workflow.variables.ticketResult ?? 'N/A')}" }, nextStepId: 'finalLog' // 跳转到最终日志步骤 }) // 函数调用步骤 2b (路径: 无需调用函数): 记录 AI 直接提供的最终文本答案 .addStep({ id: 'logFinalAnswer', nodeId: 'LogNode', input: { // 从 actionDecision 中获取 AI 的直接文本回复,如果不存在则使用 RAG 生成的答案 message: "最终答案 (无需创建工单): ${workflow.variables.actionDecision.responseText || workflow.variables.synthesizedAnswer}" }, nextStepId: 'finalLog' // 跳转到最终日志步骤 }) // 结束步骤: 记录整个流程完成 .addStep({ id: 'finalLog', nodeId: 'LogNode', input: { message: "RAG 与函数调用流程已完成。" } // 没有 nextStepId,表示这个分支的流程结束 }) // 最后,设置整个工作流的起始步骤 ID .setStartStep('logStart'); // --- 第 4 步: 执行工作流 --- async function runFlow() { // 4.1 定义工作流的初始输入数据 const initialInput = { query: "我的密码重置链接好像失效了,无法重置密码,怎么办?" }; // 4.2 创建本次执行的工作流上下文,传入工作流定义 ID 和初始输入 const context = createWorkflowContext(ragWorkflow.id, initialInput); // 4.3 创建工作流执行器实例 const executor = new WorkflowExecutor( ragWorkflow, // 要执行的工作流定义 context, // 本次执行的上下文 { // 执行器选项 nodeRegistry, // 必须提供节点注册表 eventManager, // 提供事件管理器 (可选) scheduler, // 提供调度器 (可选, 仅当使用 scheduleTask 时需要) logger, // 提供日志记录器 (推荐) // maxLoopIterations: 100 // (可选) 防止无限循环的最大迭代次数 // 可以注入其他依赖项给节点使用,例如数据库连接池等 } ); // 4.4 运行工作流并处理结果 try { console.log("--- 开始执行工作流 ---"); const result = await executor.run(); // 异步执行工作流 // 工作流执行完毕后,输出结果 console.log(`\n--- 工作流执行结果 ---`); console.log(`最终状态: ${result.status}`); // 输出工作流的最终状态 (COMPLETED, FAILED) console.log(`最终工作流变量:`, JSON.stringify(result.variables, null, 2)); // 输出执行后所有的工作流变量 // (可选) 输出详细的执行历史记录,用于调试 // console.log(`\n--- 执行历史 ---`); // result.history.forEach(h => console.log( // `步骤: ${h.stepId} (节点: ${h.nodeId}) -> 状态: ${h.status} ` + // `[开始: ${h.startTime.toISOString()}${h.endTime ? ' - 结束: ' + h.endTime.toISOString() : ''}]` + // `${h.error ? ' 错误: ' + h.error.message : ''}` // )); } catch (error) { // 如果执行器在运行过程中抛出未捕获的异常 console.error(`工作流执行失败:`, error); } } // 启动执行 runFlow(); ```