Slickflow.AI 多智能体交互落地:用 Harness 工程规范把 ReAct 流程接进 BPMN 编排 1. 从人工审批到多智能体接力Slickflow.AI 里 ReAct 流程为什么需要 Harness 规范先说清楚这篇要解决什么问题。Slickflow.AI 是一套把 LLM Agent 节点嵌进 BPMN 工作流的引擎它能让「需求分析 → 供应商搜索 → 合规检查 → 价格评估 → 下单」这条链路里的每个节点都由一个智能体驱动而不是靠人点「通过」。但真正落地时麻烦不在模型本身而在工程约束ReAct 的推理循环怎么和 BPMN 的节点生命周期对齐工具怎么注册、怎么发现、怎么在运行时绑定到具体活动如果这些没有一套规范代码很快就会变成一堆 lambda 和手写 JSON Schema 的泥潭。Harness 就是 Slickflow.AI 给出的答案。它借用 Claude 工程体系里「注册 → 发现 → 执行」三段式的思路把框架代码和业务代码彻底分开流程设计师在 BPMN 属性面板里声明toolSetName和 System Prompt业务开发者只写普通的 C# Service 类框架层负责 ReAct 循环、工具注册、参数绑定和上下文管理。这样做的直接好处是一个不懂 Agent 内部机制的开发者也能在半小时内给流程加一个新节点。这篇文章面向的是需要把智能体交互过程纳入可审计工作流的开发者。我会给出可复制的 Harness 配置片段、BPMN 节点与智能体角色的绑定示例以及一次端到端交互链路的验证步骤和预期输出。你不需要先读完整个 Slickflow.AI 源码跟着配置走一遍就能跑通。核心检索词先摆出来Slickflow.AI 多智能体、Harness 工程规范、ReAct 与 BPMN 编排、Agent 工具注册。适合谁适合正在做企业流程自动化、又想把 LLM 推理能力接进现有 BPMN 引擎的后端开发者尤其是那些被 if-else 硬编码折磨过的人。传统 BPM 的模型是「人驱动流程」人工节点做决策系统节点执行预定义规则。遇到非结构化输入自然语言需求、图片、文档时规则节点直接歇菜复杂决策只能堆 if-else维护成本高得离谱多系统协作还得人工中转。多智能体的价值在于每个 Agent 节点具备推理能力、工具调用能力和协作能力通过共享上下文接力完成复杂任务。但能力越强越需要工程规范来约束否则就是不可审计的黑盒。Harness 规范解决的正是这个「可审计」问题。2. TaoToken 前置给 ReAct 循环接一个稳定的模型入口在跑通 Slickflow.AI 的多智能体流程之前得先有一个能稳定响应tool_calls的模型入口。Slickflow.AI 的OpenAIToolCallLlmClient走的是标准 OpenAI 兼容协议HTTP POST 里带tools和messages所以任何兼容该协议的端点都能接。我这边实测下来用 TaoToken 的 API 端点比较省事它同时支持对话和工具调用返回的finish_reason字段和tool_calls结构跟 OpenAI 一致Slickflow.AI 的解析逻辑不用改。TaoToken 是什么简单说它是一个聚合式的模型 API 服务提供 OpenAI 兼容的接口能调用多种模型。对 Slickflow.AI 这种依赖tool_calls的场景来说关键是它返回的响应结构要标准。你可以在模型对话页面先手动测一下工具调用是否正常确认没问题再写进配置。适合谁适合不想自己维护模型网关、又需要标准 OpenAI 协议的中小团队。能做什么提供Base URLAPI KeyModel ID三件套直接填进 Slickflow.AI 的 LLM 客户端配置。前置准备分三步。第一步拿到 API Key。登录后在控制台的 API Keys 页面创建一个注意保存页面关闭后不再显示完整 Key。第二步确认 Base URL。Slickflow.AI 的OpenAIToolCallLlmClient默认拼接/v1/chat/completions所以 Base URL 填https://taotoken.net/api即可不要带 UTM 参数。第三步选一个支持工具调用的 Model ID比如qwen-plus或同类支持 function calling 的模型填进配置。这里要提醒一句Slickflow.AI 的 ReAct 循环依赖模型返回finish_reasontool_calls如果模型不支持工具调用循环会在第一轮就返回文本工具永远不会被触发。所以选模型时务必确认它支持 function calling。你可以在模型对话里发一条带 tools 定义的请求看返回里有没有tool_calls字段。配置写在哪里Slickflow.AI 的 LLM 客户端配置通常在appsettings.json或测试入口的初始化代码里。下面是一个可复制的 JSON 片段路径和字段名按 Slickflow.AI 的约定来{ LlmClient: { Provider: OpenAICompatible, BaseUrl: https://taotoken.net/api, ApiKey: sk-你的Key, ModelId: qwen-plus, TimeoutSeconds: 60, MaxIterations: 10 } }注意MaxIterations这个字段它对应 ReAct 循环的保护上限默认 10。如果你的流程里某个 Agent 节点工具调用链特别长可以调到 15但别调太大否则死循环时排查困难。TimeoutSeconds建议 60 起步工具调用多轮往返时容易超时。如果你用的是 Claude Code 类的编码助手来辅助写 Slickflow.AI 的 Service 类可以在编码计划里配置同样的三件套让助手直接生成符合 Harness 规范的代码。但注意编码助手只是辅助最终跑流程的还是 Slickflow.AI 引擎本身。3. 可复制配置Harness 规范下的工具注册与 BPMN 绑定这一节是全文的技术核心给出可以直接抄的配置片段。Harness 规范的三原则是声明在设计时、注册在启动时、执行在运行时。下面按这三段展开。先看工具集的定义。业务开发者只写普通 C# 类加两个属性[AgentToolSet]标在类上[AgentTool]标在方法上。方法名会被反射引擎自动转成 snake_case 作为工具名参数类型自动生成 JSON SchemaCancellationToken自动注入且不出现在 Schema 里。using Slickflow.AI.Agent.Tools; [AgentToolSet(SupplierSearch)] public class SupplierSearchService { [AgentTool(搜索供应商目录返回符合条件的供应商列表)] public async TaskListSupplier SearchSuppliers(string category, string specification ) { // 实际场景查询供应商数据库 await Task.Delay(5); return new ListSupplier { new() { SupplierId SUP001, Name TechNet Industrial Ltd, Rating 4.2 }, new() { SupplierId SUP002, Name GlobalRouter Solutions, Rating 4.5 } }; } [AgentTool(向指定供应商发起报价请求返回单价、货期和总金额)] public async TaskSupplierQuote RequestQuote(string supplierId, int quantity) { // 实际场景调用供应商门户 API 或 EDI 接口 await Task.Delay(5); return new SupplierQuote { SupplierId supplierId, UnitPrice 2950m, LeadTimeDays 10, QualityScore 4.5, TotalAmount 2950m * quantity }; } }反射引擎AgentToolReflector.BuildToolsFromTypeT()会扫描这个类自动完成几件事方法名RequestQuote转成request_quote参数supplierId生成{type:string}quantity生成{type:integer}有默认值的specification不进入required数组返回值SupplierQuote被JsonSerializer.Serialize()成 LLM 可读的字符串。你不需要手写任何 JSON Schema。接下来是注册。注册在应用启动时一次性完成不在请求处理中动态注册。AgentToolSetRegistry.RegisterT()泛型重载会从[AgentToolSet]属性读取toolSetName避免重复写字符串。// Program.cs 或测试入口 AgentToolSetRegistry.Global .RegisterNeedsAnalysisService() .RegisterSupplierSearchService() .RegisterComplianceCheckService() .RegisterPriceEvaluationService() .RegisterOrderExecutionService();然后是数据流配置。多 Agent 之间通过共享变量键传递数据键名集中定义为常量数据流配置与工具注册分离。这个配置决定了每个 Agent 节点的输入从哪个变量读、输出写到哪个变量。private const string VarPurchaseRequest PurchaseRequest; private const string VarStructuredRequirements StructuredRequirements; private const string VarSupplierQuotes SupplierQuotes; private const string VarComplianceReport ComplianceReport; private const string VarPurchaseRecommendation PurchaseRecommendation; private const string VarPurchaseOrderId PurchaseOrderId; private static readonly IReadOnlyDictionarystring, AgentRunConfig RunConfigs new Dictionarystring, AgentRunConfig(StringComparer.OrdinalIgnoreCase) { [NeedsAnalysis] new(InputKey: VarPurchaseRequest, OutputKey: VarStructuredRequirements), [SupplierSearch] new(InputKey: VarStructuredRequirements, OutputKey: VarSupplierQuotes), [ComplianceCheck] new(InputKey: VarStructuredRequirements, OutputKey: VarComplianceReport), [PriceEvaluation] new(InputKey: VarSupplierQuotes, OutputKey: VarPurchaseRecommendation), [OrderExecution] new(InputKey: VarPurchaseRecommendation, OutputKey: VarPurchaseOrderId) };最后是 BPMN 侧的绑定。流程设计师在 BPMN 设计器的属性面板里为每个 Agent 节点设置三个属性type设为AgenttoolSetName填注册时的名称如SupplierSearchsystem_prompt存入ai_activity_config表定义该节点的角色。这里的关键是toolSetName和activityId的解耦BPMN 定义里的activityId是运行时才知道的每个流程实例可能不同而toolSetName是设计时确定的。两层注册表在运行时完成映射。AgentToolSetRegistry负责「名称注册」SupplierSearch → IReadOnlyListIAgentTool。AgentToolRegistry负责「活动注册」Activity_ABC123 → IReadOnlyListIAgentTool。引擎执行节点时先通过TryResolveToActivityRegistry(toolSetName, activityId)完成映射再调用AgentNodeBase.ExecuteReActAsync()。如果你用 Cline MCP 或 Codex 来辅助生成这些配置记得把 Base URL、Key、Model ID 三件套写全否则生成的代码里模型调用会缺参数。CC Switch 这类工具切换配置时也要注意别把toolSetName的字符串写错大小写敏感。4. 验证请求跑一次端到端交互链路看预期输出配置写完后得验证整条链路是否真的跑通。这一节给出验证步骤和预期输出你可以对照日志排查。验证的入口是一个测试方法模拟用户提交采购需求然后驱动整个流程。先看测试代码的结构[Fact] public async Task WorkflowDrivenProcurementTest() { // 1. 注册工具集一次性 AgentToolSetRegistry.Global .RegisterNeedsAnalysisService() .RegisterSupplierSearchService() .RegisterComplianceCheckService() .RegisterPriceEvaluationService() .RegisterOrderExecutionService(); // 2. 加载 BPMN 流程定义 var process await _workflowService.LoadProcessAsync(SmartProcurement, 1); Console.WriteLine($[OK] Loaded process: {process.Name} (id{process.Id})); // 3. 找出所有 Agent 节点 var agentNodes process.Activities.Where(a a.Type Agent).ToList(); Console.WriteLine($[OK] Found {agentNodes.Count} Agent node(s):); foreach (var node in agentNodes) { Console.WriteLine($ [{node.ToolSetName}] toolSetName{node.ToolSetName} id{node.Id}); } // 4. 启动流程实例传入初始变量 var instance await _workflowService.StartAsync(process.Id, new Dictionarystring, object { [VarPurchaseRequest] 需要采购 10 台工业级 5G 路由器用于车间设备联网 }); // 5. 等待流程结束 var result await _workflowService.WaitForCompletionAsync(instance.Id, TimeSpan.FromMinutes(5)); // 6. 断言最终输出 Assert.True(result.IsCompleted); Assert.NotNull(result.Variables[VarPurchaseOrderId]); Console.WriteLine($订单号{result.Variables[VarPurchaseOrderId]}); }跑起来后预期日志大致如下。注意每个 Agent 节点的 ReAct 迭代过程 WorkflowDrivenProcurementTest ProcessCode : SmartProcurement Version: 1 Model : qwen-plus [OK] Loaded process: 智能采购流程 (id1001) [OK] Found 5 Agent node(s): [NeedsAnalysis] toolSetNameNeedsAnalysis idActivity_001 [SupplierSearch] toolSetNameSupplierSearch idActivity_002 [ComplianceCheck] toolSetNameComplianceCheck idActivity_003 [PriceEvaluation] toolSetNamePriceEvaluation idActivity_004 [OrderExecution] toolSetNameOrderExecution idActivity_005 ─── Agent 1/5: NeedsAnalysis [NeedsAnalysis] ─── [Agent] ReAct iteration 1/8 [Agent][LLM] POST ... modelqwen-plus tools2 msgs2 [Agent][LLM] tool_calls: classify_category idcall_001 [Agent] Calling tool classify_category | args{description:...routers...} [NeedsAnalysisService.ClassifyCategory] → IT_HARDWARE [Agent] ReAct iteration 2/8 [Agent][LLM] tool_calls: extract_specs idcall_002 [NeedsAnalysisService.ExtractSpecs] 提取规格中... [Agent] ReAct iteration 3/8 [Agent][LLM] text response (256 chars) [Agent] Final answer produced ─── Agent 2/5: SupplierSearch [SupplierSearch] ─── [Agent] ReAct iteration 1/8 [Agent][LLM] tool_calls: search_suppliers idcall_003 [SupplierSearchService.SearchSuppliers] 搜索中... [Agent] ReAct iteration 2/8 [Agent][LLM] tool_calls: request_quote idcall_004 [SupplierSearchService.RequestQuote] SUP002 → ¥2950×10 [Agent] ReAct iteration 3/8 [Agent][LLM] text response (312 chars) [Agent] Final answer produced ─── Agent 5/5: OrderExecution [OrderExecution] ─── [OrderExecutionService.CreatePurchaseOrder] 已创建 PO-20260531-7823 ═══════════════════════════════════════ 采购完成 订单号PO-20260531-7823 供应商GlobalRouter Solutions 金额¥29,500 ═══════════════════════════════════════看到Final answer produced和最后的订单号说明整条链路跑通了。这里有几个观察点第一每个 Agent 节点的 ReAct 迭代次数不一定相同NeedsAnalysis用了 3 轮SupplierSearch也用了 3 轮取决于工具调用链的长度。第二finish_reasonstop时输出最终文本写入OutputVariableKey成为下一个 Agent 的输入。第三AgentConversationMemory以processInstanceId为隔离键共享输出流程实例结束后调用Evict()释放避免内存泄漏。如果你想单独验证某个 Agent 节点可以只注册对应的工具集手动构造输入变量调用AgentNodeBase.ExecuteReActAsync()。这样排查问题时能快速定位是哪个节点出的错。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth跑多智能体流程时报错往往集中在几个地方。这一节对照真实报错给出排查路径。401 Unauthorized。最常见的原因是 API Key 没填对或过期。检查appsettings.json里的ApiKey字段确认没有多余空格。如果用的是环境变量注入确认变量名和代码里读的一致。还有一种情况是 Base URL 写成了带 UTM 参数的地址导致请求路径拼接错误返回 401。Base URL 应该是https://taotoken.net/api不带任何查询参数。排查时可以在模型对话页面用同样的 Key 发一条测试请求确认 Key 本身有效。local proxy failed。这个报错通常出现在网络层说明请求没到达模型端点。检查BaseUrl是否可达可以用curl手动发一条请求验证curl -X POST https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer sk-你的Key \ -H Content-Type: application/json \ -d {model:qwen-plus,messages:[{role:user,content:hi}]}如果 curl 能通但 Slickflow.AI 报 local proxy failed检查HttpClient的代理配置确认没有误设系统代理。另外TimeoutSeconds设太短也可能导致连接被中断建议 60 起步。reading choices 相关报错。这个报错说明响应体解析失败通常是模型返回的结构和OpenAIToolCallLlmClient预期的不一致。检查模型是否真的支持工具调用如果模型不支持返回的choices[0].message里没有tool_calls字段解析时就会出错。换一个支持 function calling 的 Model ID 再试。还有一种情况是响应被截断finish_reason是length而不是stop或tool_calls这时需要调大max_tokens。OAuth 相关报错。如果你在工具方法里调用了需要 OAuth 的企业系统 API报错可能来自工具内部而不是框架层。检查工具方法里的 token 获取逻辑确认 token 没过期。Slickflow.AI 的框架层不管理业务工具的认证这部分由业务开发者自己处理。建议把 token 刷新逻辑封装在工具类内部对 LLM 透明。工具名不匹配。反射引擎把方法名RequestQuote转成request_quote如果 BPMN 里配置的工具名和转换后的不一致LLM 调用时会找不到工具。排查时打印AgentToolReflector.BuildToolsFromTypeT()生成的工具列表对照 LLM 返回的tool_calls里的name字段。变量键拼写错误。RunConfigs里的InputKey和OutputKey如果和实际写入的变量名不一致下一个 Agent 读不到输入会拿到空值。建议把变量键集中定义为常量避免手写字符串。排查时在AgentConversationMemory.RecordOutput处打日志确认写入的键名。MaxIterations 超限。如果某个 Agent 节点反复调用工具不收敛会触发MaxIterations保护返回降级答案。这时检查 System Prompt 是否清晰工具描述是否准确。工具描述太模糊会导致 LLM 反复尝试。另外工具返回值太大也会让 LLM 难以决策建议在工具方法里做裁剪只返回关键字段。排查时有个通用技巧把日志级别调到 Debug观察每一轮 ReAct 的Reason → Act → Observe过程。哪一轮开始偏离预期问题就在那一轮的工具或 Prompt 上。6. 把智能体交互纳入可审计工作流从写代码到填配置的演进跑通演示流程只是第一步。真正落地到企业客户时会遇到一个更深的问题谁来为每家客户写那些 Service 类如果每个客户都要定制开发成本下不来。Slickflow.AI 的落地路线图分三个阶段从「写代码」演进到「填配置」最终达到「零配置自发现」。近期阶段是预置企业工具库。平台方提前实现 80% 的高频企业操作比如 ERP 的库存查询、采购订单创建审批系统的发起审批、查询状态HR 的员工查询、假期余额通知类的邮件、钉钉、企业微信消息。客户集成时只需选择需要的工具集并配置连接参数不需要写代码。工具类通过配置文件或数据库读取连接参数不硬编码。比如InventoryQueryTools从appsettings.json读取ErpConfigs.ERP_MAIN的BaseUrl和ApiKey。注册时平台预置的工具集和客户自己写的业务特定工具集一起注册客户只需要关注那 20% 的差异化部分。中期阶段是声明式 HTTP 工具适配器。为 REST API 类工具提供 JSON 或数据库配置方式业务人员在设计器里填写 API 地址和参数映射不需要写 C# 代码。工具描述格式存储在数据库里包含toolSetName、tools数组每个工具定义name、description、type、method、url、headers、parameters、responseMapping。框架层新增HttpToolAdapter实现IAgentTool接口在ExecuteAsync里根据描述符解析 URL 模板、请求头、请求体发送 HTTP 请求并返回结果。注册时从数据库加载所有声明式工具集批量注册。BPMN 设计器新增「HTTP 工具配置」面板业务人员填 Endpoint、拖拽参数映射、测试连接并保存。这一步实现后新增一个企业系统集成的成本从「写代码 部署」降为「填表单 保存」。长期阶段是 MCP 协议集成。企业各系统各自部署 MCP ServerAgent 节点通过标准协议动态发现和调用工具平台侧零代码、零配置完成集成。MCP 是 Anthropic 推动的 AI 工具标准协议OpenAI、Microsoft 等主流厂商均已跟进支持。Slickflow.AI 项目里已经有McpClientTool的实现基础位于source/core/Slickflow.AI/Agent/Tools/McpClientTool.cs。架构上AgentNodeBase的 ReAct 循环里Tools列表可以是McpClientTool的集合通过McpServerClient走 JSON-RPC 2.0 协议向各个 MCP Server 发tools/list和tools/call请求。ERP 系统、CRM 系统、OA 审批系统、第三方供应商门户各自暴露 MCP ServerAgent 节点动态发现工具不需要预先注册。这三个阶段的核心逻辑是一致的把「智能体交互过程」从不可审计的黑盒变成可审计的工作流。每个 Agent 节点的输入、输出、工具调用记录都落在AgentConversationMemory里以processInstanceId为隔离键流程结束后可以导出审计日志。BPMN 的节点定义、toolSetName绑定、System Prompt 都存在数据库里变更可追溯。ReAct 的每一轮迭代都有日志Reason → Act → Observe的过程可回放。如果你现在就要落地建议从近期阶段开始先把高频企业操作封装成预置工具库用 Harness 规范注册跑通一两个流程。等业务方看到效果再推进声明式 HTTP 工具适配器把集成成本降下来。MCP 协议集成可以作为长期目标等生态成熟再投入。最后给一个实用技巧在AgentConversationMemory的RecordOutput和Evict处加监控埋点统计每个流程实例的 Agent 节点数、平均 ReAct 迭代次数、工具调用成功率。这些指标能帮你判断哪些节点的 Prompt 需要优化哪些工具的描述不够清晰。实测下来工具描述优化后ReAct 迭代次数能降 30% 左右直接省 token 成本。